Merge branch 'main' of github.com:immich-app/immich into feat/mobile-delete-local-only

This commit is contained in:
Alex Tran 2023-12-02 09:28:35 -06:00
commit 4643b4894e
No known key found for this signature in database
122 changed files with 4535 additions and 491 deletions

2
.gitattributes vendored
View file

@ -17,3 +17,5 @@ web/src/api/open-api/**/*.md -diff -merge
web/src/api/open-api/**/*.md linguist-generated=true
web/src/api/open-api/**/*.ts -diff -merge
web/src/api/open-api/**/*.ts linguist-generated=true
*.sh text eol=lf

View file

@ -35,7 +35,7 @@ jobs:
with:
ref: ${{ steps.get-ref.outputs.ref }}
- uses: actions/setup-java@v3
- uses: actions/setup-java@v4
with:
distribution: "zulu"
java-version: "12.x"

View file

@ -209,7 +209,7 @@ jobs:
exit 1
generated-typeorm-migrations-up-to-date:
name: TypeORM Migrations
name: TypeORM Checks
runs-on: ubuntu-latest
services:
postgres:
@ -236,7 +236,7 @@ jobs:
- name: Install server dependencies
run: npm ci
- name: Build the
- name: Build the app
run: npm run build
- name: Run existing migrations
@ -252,13 +252,30 @@ jobs:
with:
files: |
server/src/infra/migrations/
- name: Verify files have not changed
- name: Verify migration files have not changed
if: steps.verify-changed-files.outputs.files_changed == 'true'
run: |
echo "ERROR: Generated files not up to date!"
echo "ERROR: Generated migration files not up to date!"
echo "Changed files: ${{ steps.verify-changed-files.outputs.changed_files }}"
exit 1
- name: Run SQL generation
run: npm run sql:generate
- name: Find file changes
uses: tj-actions/verify-changed-files@v13.1
id: verify-changed-sql-files
with:
files: |
server/src/infra/sql
- name: Verify SQL files have not changed
if: steps.verify-changed-sql-files.outputs.files_changed == 'true'
run: |
echo "ERROR: Generated SQL files not up to date!"
echo "Changed files: ${{ steps.verify-changed-sql-files.outputs.changed_files }}"
exit 1
# mobile-integration-tests:
# name: Run mobile end-to-end integration tests
# runs-on: macos-latest

View file

@ -26,7 +26,10 @@ prod-scale:
docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans
api:
cd ./server && npm run api:generate
npm --prefix server run api:generate
sql:
npm --prefix server run sql:generate
attach-server:
docker exec -it docker_immich-server_1 sh

View file

@ -11,7 +11,7 @@
<p align="center">
<img src="design/immich-logo.svg" width="150" title="Login mit eigener URL">
</p>
<h3 align="center">Immich - Hoch performante, selbst gehostete Backup Lösung für Fotos und Videos</h3>
<h3 align="center">Immich - Hoch performante, selbst gehostete Backup-Lösung für Fotos und Videos</h3>
<br/>
<a href="https://immich.app">
<img src="design/immich-screenshots.png" title="Haupt-Screenshot">
@ -32,10 +32,10 @@
## Warnung
- ⚠️ Das Projekt befindet sich unter **sehr aktiver** Entwicklung.
- ⚠️ Das Projekt befindet sich in **sehr aktiver** Entwicklung.
- ⚠️ Erwarte Fehler und Änderungen mit Breaking-Changes.
- ⚠️ **Nutze die App auf keinen Fall als einziges Speichermedium für deine Fotos und Videos.**
- ⚠️ Befolge immer die [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) Backup Regel für deine wertvollen Fotos und Videos!
- ⚠️ Befolge immer die [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) Backup-Regel für deine wertvollen Fotos und Videos!
## Inhalt
@ -54,9 +54,9 @@ Die Hauptdokumentation, inklusive Installationsanleitungen, ist unter https://im
## Demo
Die Web-Demo kannst du unter https://demo.immich.app finden.
Die Web-Demo kannst Du unter https://demo.immich.app finden.
Für die Handy-App kannst du `https://demo.immich.app/api` als `Server Endpoint URL` angeben.
Für die Handy-App kannst Du `https://demo.immich.app/api` als `Server Endpoint URL` angeben.
```bash title="Demo Credential"
Die Anmeldedaten
@ -73,8 +73,8 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
| Funktionen | Mobil | Web |
| ---------------------------------------------------- | ------ | ----- |
| Fotos & Videos hochladen und ansehen | Ja | Ja |
| Automatisches Backup wenn die App offen ist | Ja | n. a. |
| Selektive Auswahl von Alben zum Backup | Ja | n. a. |
| Automatisches Backup wenn die App geöffnet ist | Ja | n. a. |
| Selektive Auswahl von Alben zum Sichern | Ja | n. a. |
| Fotos und Videos auf das Gerät herunterladen | Ja | Ja |
| Unterstützt mehrere Benutzer | Ja | Ja |
| Album und geteilte Alben | Ja | Ja |
@ -82,7 +82,7 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
| Unterstützt RAW Formate | Ja | Ja |
| Metadaten anzeigen (EXIF, Karte) | Ja | Ja |
| Suchen nach Metadaten, Objekten, Gesichtern und CLIP | Ja | Ja |
| Administrative Funktionen (Nutzerverwaltung) | Nein | Ja |
| Administrative Funktionen (Benutzerverwaltung) | Nein | Ja |
| Backup im Hintergrund | Ja | n. a. |
| Virtuelles Scrollen | Ja | Ja |
| OAuth Unterstützung | Ja | Ja |
@ -101,22 +101,22 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
## Unterstütze das Projekt
Ich habe mich diesem Projekt verpflichtet und werde nicht aufgeben. Ich werde die Dokumentation weiter aktualisieren, neue Funktionen hinzufügen und Fehler beheben. Allerdings kann ich das nicht alleine schaffen. Daher brauche ich Eure Unterstützung mir zusätzliche Motivation zu geben um weiterzumachen.
Ich habe mich diesem Projekt verpflichtet und werde nicht aufgeben. Ich werde die Dokumentation weiter aktualisieren, neue Funktionen hinzufügen und Fehler beheben. Allerdings kann ich das nicht alleine schaffen. Daher brauche ich Eure Unterstützung, um mir zusätzliche Motivation zu geben, weiterzumachen.
Wie unsere Gastgeber in der [selfhosted.show - In der Episode 'The-organization-must-not-be-name is a Hostile Actor'](https://selfhosted.show/79?t=1418) gesagt haben, ist dies ein riesiges Unterfangen, welchem das Team und ich uns annehmen. In Zukunft würde ich liebend gerne Vollzeit an dem Projekt arbeiten und bitte daher um Eure Unterstützung.
Wenn Du denkst, dass dies die richtige Sache ist und dich selbst die App für eine längere Zeit nutzen siehst, dann denke bitte darüber nach das Projekt mit einer der unten aufgelisteten Optionen zu unterstützen.
Wenn Du denkst, dass dies die richtige Sache ist und dich selbst die App für eine längere Zeit nutzen siehst, dann denke bitte darüber nach, das Projekt mit einer der unten aufgelisteten Optionen zu unterstützen.
### Spenden
- [Monatliche Spende](https://github.com/sponsors/alextran1502) via GitHub Sponsors
- [Einmalige Spende](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) via GitHub Sponsors
- [Monatliche Spende](https://github.com/sponsors/immich-app) via GitHub Sponsors
- [Einmalige Spende](https://github.com/sponsors/immich-app?frequency=one-time&sponsor=immich-app) via GitHub Sponsors
- [Librepay](https://liberapay.com/alex.tran1502/)
- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
- ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz
## Unterstützer
## Mitwirkende
<a href="https://github.com/alextran1502/immich/graphs/contributors">
<img src="https://contrib.rocks/image?repo=immich-app/immich" width="100%"/>
</a>

12
cli/package-lock.json generated
View file

@ -1604,9 +1604,9 @@
}
},
"node_modules/@types/node": {
"version": "20.10.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.0.tgz",
"integrity": "sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==",
"version": "20.10.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.1.tgz",
"integrity": "sha512-T2qwhjWwGH81vUEx4EXmBKsTJRXFXNZTL4v0gi01+zyBmCwzE6TyHszqX01m+QHTEq+EZNo13NeJIdEqf+Myrg==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
@ -7990,9 +7990,9 @@
}
},
"@types/node": {
"version": "20.10.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.0.tgz",
"integrity": "sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==",
"version": "20.10.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.1.tgz",
"integrity": "sha512-T2qwhjWwGH81vUEx4EXmBKsTJRXFXNZTL4v0gi01+zyBmCwzE6TyHszqX01m+QHTEq+EZNo13NeJIdEqf+Myrg==",
"dev": true,
"requires": {
"undici-types": "~5.26.4"

View file

@ -447,6 +447,12 @@ export interface AssetBulkDeleteDto {
* @interface AssetBulkUpdateDto
*/
export interface AssetBulkUpdateDto {
/**
*
* @type {string}
* @memberof AssetBulkUpdateDto
*/
'dateTimeOriginal'?: string;
/**
*
* @type {Array<string>}
@ -465,6 +471,18 @@ export interface AssetBulkUpdateDto {
* @memberof AssetBulkUpdateDto
*/
'isFavorite'?: boolean;
/**
*
* @type {number}
* @memberof AssetBulkUpdateDto
*/
'latitude'?: number;
/**
*
* @type {number}
* @memberof AssetBulkUpdateDto
*/
'longitude'?: number;
/**
*
* @type {boolean}
@ -4137,6 +4155,12 @@ export interface UpdateAlbumDto {
* @interface UpdateAssetDto
*/
export interface UpdateAssetDto {
/**
*
* @type {string}
* @memberof UpdateAssetDto
*/
'dateTimeOriginal'?: string;
/**
*
* @type {string}
@ -4155,6 +4179,18 @@ export interface UpdateAssetDto {
* @memberof UpdateAssetDto
*/
'isFavorite'?: boolean;
/**
*
* @type {number}
* @memberof UpdateAssetDto
*/
'latitude'?: number;
/**
*
* @type {number}
* @memberof UpdateAssetDto
*/
'longitude'?: number;
}
/**
*

View file

@ -108,11 +108,11 @@ services:
redis:
container_name: immich_redis
image: redis:6.2-alpine@sha256:80cc8518800438c684a53ed829c621c94afd1087aaeb59b0d4343ed3e7bcf6c5
image: redis:6.2-alpine@sha256:60e49e22fa5706cd8df7d5e0bc50ee9bab7c608039fa653c4d961014237cca46
database:
container_name: immich_postgres
image: postgres:14-alpine@sha256:50d9be76e9a90da4c781554955e0ffc79d9d5c4226838e64b36aacc97cbc35ad
image: postgres:14-alpine@sha256:54916702f83d9330d355e078f69a7ba42f2cf2e530c8fe63423d6680d8da45b0
env_file:
- .env
environment:

View file

@ -65,12 +65,12 @@ services:
redis:
container_name: immich_redis
image: redis:6.2-alpine@sha256:80cc8518800438c684a53ed829c621c94afd1087aaeb59b0d4343ed3e7bcf6c5
image: redis:6.2-alpine@sha256:60e49e22fa5706cd8df7d5e0bc50ee9bab7c608039fa653c4d961014237cca46
restart: always
database:
container_name: immich_postgres
image: postgres:14-alpine@sha256:50d9be76e9a90da4c781554955e0ffc79d9d5c4226838e64b36aacc97cbc35ad
image: postgres:14-alpine@sha256:54916702f83d9330d355e078f69a7ba42f2cf2e530c8fe63423d6680d8da45b0
env_file:
- .env
environment:

View file

@ -23,7 +23,7 @@ services:
- database
database:
image: postgres:14-alpine@sha256:50d9be76e9a90da4c781554955e0ffc79d9d5c4226838e64b36aacc97cbc35ad
image: postgres:14-alpine@sha256:54916702f83d9330d355e078f69a7ba42f2cf2e530c8fe63423d6680d8da45b0
command: -c fsync=off
environment:
POSTGRES_PASSWORD: postgres

View file

@ -69,12 +69,12 @@ services:
redis:
container_name: immich_redis
image: redis:6.2-alpine@sha256:80cc8518800438c684a53ed829c621c94afd1087aaeb59b0d4343ed3e7bcf6c5
image: redis:6.2-alpine@sha256:60e49e22fa5706cd8df7d5e0bc50ee9bab7c608039fa653c4d961014237cca46
restart: always
database:
container_name: immich_postgres
image: postgres:14-alpine@sha256:50d9be76e9a90da4c781554955e0ffc79d9d5c4226838e64b36aacc97cbc35ad
image: postgres:14-alpine@sha256:54916702f83d9330d355e078f69a7ba42f2cf2e530c8fe63423d6680d8da45b0
env_file:
- .env
environment:

View file

@ -144,7 +144,7 @@ PUBLIC_LOGIN_PAGE_MESSAGE="My Family Photos and Videos Backup Server"
From the directory you created in Step 1, (which should now contain your customized `docker-compose.yml` and `.env` files) run `docker-compose up -d`.
```bash title="Start the containers using docker compose command"
docker-compose up -d # or `docker compose up -d` based on your docker-compose version
docker compose up -d
```
:::tip
@ -162,7 +162,7 @@ If `IMMICH_VERSION` is set, it will need to be updated to the latest or desired
When a new version of Immich is [released](https://github.com/immich-app/immich/releases), the application can be upgraded with the following commands, run in the directory with the `docker-compose.yml` file:
```bash title="Upgrade Immich"
docker-compose pull && docker-compose up -d # Or `docker compose up -d`
docker compose pull && docker compose up -d
```
:::caution Automatic Updates

View file

@ -1,4 +1,4 @@
FROM python:3.11-bookworm@sha256:e5a1b0a194a5fbf94f6e350b31c9a508723f9eeb2f9e9e32c3b65df8520a40cc as builder
FROM python:3.11-bookworm@sha256:47c1829f72432c33609b3095259843a88c7ffc42cc9dbb55c43f2e7bbe46ca58 as builder
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
@ -13,7 +13,7 @@ ENV VIRTUAL_ENV="/opt/venv" PATH="/opt/venv/bin:${PATH}"
COPY poetry.lock pyproject.toml ./
RUN poetry install --sync --no-interaction --no-ansi --no-root --only main
FROM python:3.11-slim-bookworm@sha256:1bc6a3e9356d64ea632791653bc71a56340e8741dab66434ab2739ebf6aed29d
FROM python:3.11-slim-bookworm@sha256:8f82989e563d0dbad057a874a96438a360978c148e34f36c1db8d2d61b5fd6f0
RUN apt-get update && apt-get install -y --no-install-recommends tini libmimalloc2.0 && rm -rf /var/lib/apt/lists/*

View file

@ -8,9 +8,12 @@ import 'package:openapi/api.dart';
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**dateTimeOriginal** | **String** | | [optional]
**ids** | **List<String>** | | [default to const []]
**isArchived** | **bool** | | [optional]
**isFavorite** | **bool** | | [optional]
**latitude** | **num** | | [optional]
**longitude** | **num** | | [optional]
**removeParent** | **bool** | | [optional]
**stackParentId** | **String** | | [optional]

View file

@ -8,9 +8,12 @@ import 'package:openapi/api.dart';
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**dateTimeOriginal** | **String** | | [optional]
**description** | **String** | | [optional]
**isArchived** | **bool** | | [optional]
**isFavorite** | **bool** | | [optional]
**latitude** | **num** | | [optional]
**longitude** | **num** | | [optional]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View file

@ -13,13 +13,24 @@ part of openapi.api;
class AssetBulkUpdateDto {
/// Returns a new [AssetBulkUpdateDto] instance.
AssetBulkUpdateDto({
this.dateTimeOriginal,
this.ids = const [],
this.isArchived,
this.isFavorite,
this.latitude,
this.longitude,
this.removeParent,
this.stackParentId,
});
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? dateTimeOriginal;
List<String> ids;
///
@ -38,6 +49,22 @@ class AssetBulkUpdateDto {
///
bool? isFavorite;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
num? latitude;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
num? longitude;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
@ -56,26 +83,37 @@ class AssetBulkUpdateDto {
@override
bool operator ==(Object other) => identical(this, other) || other is AssetBulkUpdateDto &&
other.dateTimeOriginal == dateTimeOriginal &&
other.ids == ids &&
other.isArchived == isArchived &&
other.isFavorite == isFavorite &&
other.latitude == latitude &&
other.longitude == longitude &&
other.removeParent == removeParent &&
other.stackParentId == stackParentId;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(dateTimeOriginal == null ? 0 : dateTimeOriginal!.hashCode) +
(ids.hashCode) +
(isArchived == null ? 0 : isArchived!.hashCode) +
(isFavorite == null ? 0 : isFavorite!.hashCode) +
(latitude == null ? 0 : latitude!.hashCode) +
(longitude == null ? 0 : longitude!.hashCode) +
(removeParent == null ? 0 : removeParent!.hashCode) +
(stackParentId == null ? 0 : stackParentId!.hashCode);
@override
String toString() => 'AssetBulkUpdateDto[ids=$ids, isArchived=$isArchived, isFavorite=$isFavorite, removeParent=$removeParent, stackParentId=$stackParentId]';
String toString() => 'AssetBulkUpdateDto[dateTimeOriginal=$dateTimeOriginal, ids=$ids, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, removeParent=$removeParent, stackParentId=$stackParentId]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.dateTimeOriginal != null) {
json[r'dateTimeOriginal'] = this.dateTimeOriginal;
} else {
// json[r'dateTimeOriginal'] = null;
}
json[r'ids'] = this.ids;
if (this.isArchived != null) {
json[r'isArchived'] = this.isArchived;
@ -87,6 +125,16 @@ class AssetBulkUpdateDto {
} else {
// json[r'isFavorite'] = null;
}
if (this.latitude != null) {
json[r'latitude'] = this.latitude;
} else {
// json[r'latitude'] = null;
}
if (this.longitude != null) {
json[r'longitude'] = this.longitude;
} else {
// json[r'longitude'] = null;
}
if (this.removeParent != null) {
json[r'removeParent'] = this.removeParent;
} else {
@ -108,11 +156,18 @@ class AssetBulkUpdateDto {
final json = value.cast<String, dynamic>();
return AssetBulkUpdateDto(
dateTimeOriginal: mapValueOfType<String>(json, r'dateTimeOriginal'),
ids: json[r'ids'] is List
? (json[r'ids'] as List).cast<String>()
: const [],
isArchived: mapValueOfType<bool>(json, r'isArchived'),
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
latitude: json[r'latitude'] == null
? null
: num.parse(json[r'latitude'].toString()),
longitude: json[r'longitude'] == null
? null
: num.parse(json[r'longitude'].toString()),
removeParent: mapValueOfType<bool>(json, r'removeParent'),
stackParentId: mapValueOfType<String>(json, r'stackParentId'),
);

View file

@ -13,11 +13,22 @@ part of openapi.api;
class UpdateAssetDto {
/// Returns a new [UpdateAssetDto] instance.
UpdateAssetDto({
this.dateTimeOriginal,
this.description,
this.isArchived,
this.isFavorite,
this.latitude,
this.longitude,
});
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? dateTimeOriginal;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
@ -42,24 +53,51 @@ class UpdateAssetDto {
///
bool? isFavorite;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
num? latitude;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
num? longitude;
@override
bool operator ==(Object other) => identical(this, other) || other is UpdateAssetDto &&
other.dateTimeOriginal == dateTimeOriginal &&
other.description == description &&
other.isArchived == isArchived &&
other.isFavorite == isFavorite;
other.isFavorite == isFavorite &&
other.latitude == latitude &&
other.longitude == longitude;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(dateTimeOriginal == null ? 0 : dateTimeOriginal!.hashCode) +
(description == null ? 0 : description!.hashCode) +
(isArchived == null ? 0 : isArchived!.hashCode) +
(isFavorite == null ? 0 : isFavorite!.hashCode);
(isFavorite == null ? 0 : isFavorite!.hashCode) +
(latitude == null ? 0 : latitude!.hashCode) +
(longitude == null ? 0 : longitude!.hashCode);
@override
String toString() => 'UpdateAssetDto[description=$description, isArchived=$isArchived, isFavorite=$isFavorite]';
String toString() => 'UpdateAssetDto[dateTimeOriginal=$dateTimeOriginal, description=$description, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.dateTimeOriginal != null) {
json[r'dateTimeOriginal'] = this.dateTimeOriginal;
} else {
// json[r'dateTimeOriginal'] = null;
}
if (this.description != null) {
json[r'description'] = this.description;
} else {
@ -75,6 +113,16 @@ class UpdateAssetDto {
} else {
// json[r'isFavorite'] = null;
}
if (this.latitude != null) {
json[r'latitude'] = this.latitude;
} else {
// json[r'latitude'] = null;
}
if (this.longitude != null) {
json[r'longitude'] = this.longitude;
} else {
// json[r'longitude'] = null;
}
return json;
}
@ -86,9 +134,16 @@ class UpdateAssetDto {
final json = value.cast<String, dynamic>();
return UpdateAssetDto(
dateTimeOriginal: mapValueOfType<String>(json, r'dateTimeOriginal'),
description: mapValueOfType<String>(json, r'description'),
isArchived: mapValueOfType<bool>(json, r'isArchived'),
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
latitude: json[r'latitude'] == null
? null
: num.parse(json[r'latitude'].toString()),
longitude: json[r'longitude'] == null
? null
: num.parse(json[r'longitude'].toString()),
);
}
return null;

View file

@ -16,6 +16,11 @@ void main() {
// final instance = AssetBulkUpdateDto();
group('test AssetBulkUpdateDto', () {
// String dateTimeOriginal
test('to test the property `dateTimeOriginal`', () async {
// TODO
});
// List<String> ids (default value: const [])
test('to test the property `ids`', () async {
// TODO
@ -31,6 +36,16 @@ void main() {
// TODO
});
// num latitude
test('to test the property `latitude`', () async {
// TODO
});
// num longitude
test('to test the property `longitude`', () async {
// TODO
});
// bool removeParent
test('to test the property `removeParent`', () async {
// TODO

View file

@ -16,6 +16,11 @@ void main() {
// final instance = UpdateAssetDto();
group('test UpdateAssetDto', () {
// String dateTimeOriginal
test('to test the property `dateTimeOriginal`', () async {
// TODO
});
// String description
test('to test the property `description`', () async {
// TODO
@ -31,6 +36,16 @@ void main() {
// TODO
});
// num latitude
test('to test the property `latitude`', () async {
// TODO
});
// num longitude
test('to test the property `longitude`', () async {
// TODO
});
});

View file

@ -52,7 +52,8 @@
},
{
"groupName": "base-image",
"matchPackagePrefixes": ["ghcr.io/immich-app/base-server"]
"matchPackagePrefixes": ["ghcr.io/immich-app/base-server"],
"minimumReleaseAge": "0"
},
{
"matchDatasources": ["docker"],

View file

@ -1,5 +1,5 @@
# dev build
FROM ghcr.io/immich-app/base-server-dev:20231125@sha256:f33b6eaf384e76ef3705a6e2cc76d276144ad6d3366b82f9b45b07d6a19285e2 as dev
FROM ghcr.io/immich-app/base-server-dev:20231130@sha256:2f3b4bc0b50a0710e4a0867b4842ebde3a709d18fd19b095b8bfb884082cfa18 as dev
WORKDIR /usr/src/app
COPY server/package.json server/package-lock.json ./
@ -23,7 +23,7 @@ RUN npm run build
# prod build
FROM ghcr.io/immich-app/base-server-prod:20231125@sha256:a0e15f5bf87a97a79a399a5adffb5fe5befc18fb212e8341e744d958fe41e32a
FROM ghcr.io/immich-app/base-server-prod:20231130@sha256:dd91bfac4090357605a862823a99b50cf01cbc519723198f7aebb6b0517fab1d
WORKDIR /usr/src/app
ENV NODE_ENV=production
@ -37,6 +37,7 @@ COPY server/start*.sh ./
RUN npm link && npm cache clean --force
COPY LICENSE /licenses/LICENSE.txt
COPY LICENSE /LICENSE
ENV PATH="${PATH}:/usr/src/app/bin"
VOLUME /usr/src/app/upload
EXPOSE 3001
ENTRYPOINT ["tini", "--", "/bin/sh"]

View file

@ -1,2 +0,0 @@
#!/usr/bin/env sh
./start.sh admin-cli $1

View file

@ -1,2 +0,0 @@
#!/usr/bin/env bash
npx immich "$@"

2
server/bin/immich Executable file
View file

@ -0,0 +1,2 @@
#!/usr/bin/env bash
node /usr/src/app/node_modules/.bin/immich "$@"

2
server/bin/immich-admin Executable file
View file

@ -0,0 +1,2 @@
#!/usr/bin/env sh
/usr/src/app/start.sh immich-admin $1

View file

@ -6449,6 +6449,9 @@
},
"AssetBulkUpdateDto": {
"properties": {
"dateTimeOriginal": {
"type": "string"
},
"ids": {
"items": {
"format": "uuid",
@ -6462,6 +6465,12 @@
"isFavorite": {
"type": "boolean"
},
"latitude": {
"type": "number"
},
"longitude": {
"type": "number"
},
"removeParent": {
"type": "boolean"
},
@ -9343,6 +9352,9 @@
},
"UpdateAssetDto": {
"properties": {
"dateTimeOriginal": {
"type": "string"
},
"description": {
"type": "string"
},
@ -9351,6 +9363,12 @@
},
"isFavorite": {
"type": "boolean"
},
"latitude": {
"type": "number"
},
"longitude": {
"type": "number"
}
},
"type": "object"

170
server/package-lock.json generated
View file

@ -53,10 +53,6 @@
"typesense": "^1.7.1",
"ua-parser-js": "^1.0.35"
},
"bin": {
"immich": "bin/cli.sh",
"immich-admin": "bin/admin-cli.sh"
},
"devDependencies": {
"@nestjs/cli": "^10.1.16",
"@nestjs/schematics": "^10.0.2",
@ -93,6 +89,7 @@
"prettier-plugin-organize-imports": "^3.2.3",
"rimraf": "^5.0.1",
"source-map-support": "^0.5.21",
"sql-formatter": "^14.0.0",
"supertest": "^6.3.3",
"testcontainers": "^10.2.1",
"ts-jest": "^29.1.1",
@ -5504,6 +5501,12 @@
"node": ">=8"
}
},
"node_modules/discontinuous-range": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz",
"integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==",
"dev": true
},
"node_modules/docker-compose": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-0.24.2.tgz",
@ -6740,6 +6743,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-stdin": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz",
"integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==",
"dev": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-stream": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
@ -8723,6 +8738,12 @@
"node": ">=12.0.0"
}
},
"node_modules/moo": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz",
"integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==",
"dev": true
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@ -8895,6 +8916,34 @@
"ncp": "bin/ncp"
}
},
"node_modules/nearley": {
"version": "2.20.1",
"resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz",
"integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==",
"dev": true,
"dependencies": {
"commander": "^2.19.0",
"moo": "^0.5.0",
"railroad-diagrams": "^1.0.0",
"randexp": "0.4.6"
},
"bin": {
"nearley-railroad": "bin/nearley-railroad.js",
"nearley-test": "bin/nearley-test.js",
"nearley-unparse": "bin/nearley-unparse.js",
"nearleyc": "bin/nearleyc.js"
},
"funding": {
"type": "individual",
"url": "https://nearley.js.org/#give-to-nearley"
}
},
"node_modules/nearley/node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@ -9964,6 +10013,25 @@
"resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
"integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag=="
},
"node_modules/railroad-diagrams": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz",
"integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==",
"dev": true
},
"node_modules/randexp": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz",
"integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==",
"dev": true,
"dependencies": {
"discontinuous-range": "1.0.0",
"ret": "~0.1.10"
},
"engines": {
"node": ">=0.12"
}
},
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@ -10222,6 +10290,15 @@
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
},
"node_modules/ret": {
"version": "0.1.15",
"resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
"integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==",
"dev": true,
"engines": {
"node": ">=0.12"
}
},
"node_modules/retry": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
@ -10806,6 +10883,20 @@
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"dev": true
},
"node_modules/sql-formatter": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-14.0.0.tgz",
"integrity": "sha512-VcHYMRvZqg3RNjjxNB/puT9O1hR5QLXTvgTaBtxXcvmRQwSnH9M+oW2Ti+uFuVVU8HoNlOjU2uKHv8c0FQNsdQ==",
"dev": true,
"dependencies": {
"argparse": "^2.0.1",
"get-stdin": "=8.0.0",
"nearley": "^2.20.1"
},
"bin": {
"sql-formatter": "bin/sql-formatter-cli.cjs"
}
},
"node_modules/ssh-remote-port-forward": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/ssh-remote-port-forward/-/ssh-remote-port-forward-1.0.4.tgz",
@ -16768,6 +16859,12 @@
"path-type": "^4.0.0"
}
},
"discontinuous-range": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz",
"integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==",
"dev": true
},
"docker-compose": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-0.24.2.tgz",
@ -17718,6 +17815,12 @@
"integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==",
"dev": true
},
"get-stdin": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz",
"integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==",
"dev": true
},
"get-stream": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
@ -19188,6 +19291,12 @@
"integrity": "sha512-2dF2R6YMSZbpip1V1WHKGLNjr/k48uQClqMVb5H3MOvwc9qhYis3/IWbj02qIg/Y8MDXKFF4c5v0rxx2o6xTZw==",
"dev": true
},
"moo": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz",
"integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==",
"dev": true
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@ -19340,6 +19449,26 @@
"resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz",
"integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA=="
},
"nearley": {
"version": "2.20.1",
"resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz",
"integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==",
"dev": true,
"requires": {
"commander": "^2.19.0",
"moo": "^0.5.0",
"railroad-diagrams": "^1.0.0",
"randexp": "0.4.6"
},
"dependencies": {
"commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
}
}
},
"negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@ -20107,6 +20236,22 @@
"resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
"integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag=="
},
"railroad-diagrams": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz",
"integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==",
"dev": true
},
"randexp": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz",
"integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==",
"dev": true,
"requires": {
"discontinuous-range": "1.0.0",
"ret": "~0.1.10"
}
},
"randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@ -20313,6 +20458,12 @@
}
}
},
"ret": {
"version": "0.1.15",
"resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
"integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==",
"dev": true
},
"retry": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
@ -20747,6 +20898,17 @@
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"dev": true
},
"sql-formatter": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-14.0.0.tgz",
"integrity": "sha512-VcHYMRvZqg3RNjjxNB/puT9O1hR5QLXTvgTaBtxXcvmRQwSnH9M+oW2Ti+uFuVVU8HoNlOjU2uKHv8c0FQNsdQ==",
"dev": true,
"requires": {
"argparse": "^2.0.1",
"get-stdin": "=8.0.0",
"nearley": "^2.20.1"
}
},
"ssh-remote-port-forward": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/ssh-remote-port-forward/-/ssh-remote-port-forward-1.0.4.tgz",

View file

@ -5,10 +5,6 @@
"author": "",
"private": true,
"license": "UNLICENSED",
"bin": {
"immich": "./bin/cli.sh",
"immich-admin": "./bin/admin-cli.sh"
},
"scripts": {
"build": "nest build",
"format": "prettier --check .",
@ -36,7 +32,8 @@
"typeorm:schema:reset": "npm run typeorm:schema:drop && npm run typeorm:migrations:run",
"api:typescript": "bash ./bin/generate-open-api.sh web",
"api:dart": "bash ./bin/generate-open-api.sh mobile",
"api:generate": "node ./bin/sync-spec-version.js && bash ./bin/generate-open-api.sh"
"api:generate": "node ./bin/sync-spec-version.js && bash ./bin/generate-open-api.sh",
"sql:generate": "node ./dist/infra/sql-generator/"
},
"dependencies": {
"@babel/runtime": "^7.22.11",
@ -119,6 +116,7 @@
"prettier-plugin-organize-imports": "^3.2.3",
"rimraf": "^5.0.1",
"source-map-support": "^0.5.21",
"sql-formatter": "^14.0.0",
"supertest": "^6.3.3",
"testcontainers": "^10.2.1",
"ts-jest": "^29.1.1",

View file

@ -1,6 +1,6 @@
import { BadRequestException, UnauthorizedException } from '@nestjs/common';
import { AuthUserDto } from '../auth';
import { setDifference, setUnion } from '../domain.util';
import { setDifference, setIsEqual, setUnion } from '../domain.util';
import { IAccessRepository } from '../repositories';
export enum Permission {
@ -76,7 +76,7 @@ export class AccessCore {
async requirePermission(authUser: AuthUserDto, permission: Permission, ids: string[] | string) {
ids = Array.isArray(ids) ? ids : [ids];
const allowedIds = await this.checkAccess(authUser, permission, ids);
if (new Set(ids).size !== allowedIds.size) {
if (!setIsEqual(new Set(ids), allowedIds)) {
throw new BadRequestException(`Not found or no ${permission} access`);
}
}
@ -106,9 +106,24 @@ export class AccessCore {
}
switch (permission) {
case Permission.ASSET_READ:
return await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids);
case Permission.ASSET_VIEW:
return await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids);
case Permission.ASSET_DOWNLOAD:
return !!authUser.isAllowDownload
? await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids)
: new Set();
case Permission.ASSET_UPLOAD:
return authUser.isAllowUpload ? ids : new Set();
case Permission.ASSET_SHARE:
// TODO: fix this to not use authUser.id for shared link access control
return await this.repository.asset.checkOwnerAccess(authUser.id, ids);
case Permission.ALBUM_READ:
return await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids);
@ -116,46 +131,59 @@ export class AccessCore {
return !!authUser.isAllowDownload
? await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids)
: new Set();
}
const allowedIds = new Set();
for (const id of ids) {
const hasAccess = await this.hasSharedLinkAccess(authUser, permission, id);
if (hasAccess) {
allowedIds.add(id);
}
}
return allowedIds;
}
// TODO: Migrate logic to checkAccessSharedLink to evaluate permissions in bulk.
private async hasSharedLinkAccess(authUser: AuthUserDto, permission: Permission, id: string) {
const sharedLinkId = authUser.sharedLinkId;
if (!sharedLinkId) {
return false;
}
switch (permission) {
case Permission.ASSET_READ:
return this.repository.asset.hasSharedLinkAccess(sharedLinkId, id);
case Permission.ASSET_VIEW:
return await this.repository.asset.hasSharedLinkAccess(sharedLinkId, id);
case Permission.ASSET_DOWNLOAD:
return !!authUser.isAllowDownload && (await this.repository.asset.hasSharedLinkAccess(sharedLinkId, id));
case Permission.ASSET_SHARE:
// TODO: fix this to not use authUser.id for shared link access control
return this.repository.asset.hasOwnerAccess(authUser.id, id);
default:
return false;
return new Set();
}
}
private async checkAccessOther(authUser: AuthUserDto, permission: Permission, ids: Set<string>) {
switch (permission) {
case Permission.ASSET_READ: {
const isOwner = await this.repository.asset.checkOwnerAccess(authUser.id, ids);
const isAlbum = await this.repository.asset.checkAlbumAccess(authUser.id, setDifference(ids, isOwner));
const isPartner = await this.repository.asset.checkPartnerAccess(
authUser.id,
setDifference(ids, isOwner, isAlbum),
);
return setUnion(isOwner, isAlbum, isPartner);
}
case Permission.ASSET_SHARE: {
const isOwner = await this.repository.asset.checkOwnerAccess(authUser.id, ids);
const isPartner = await this.repository.asset.checkPartnerAccess(authUser.id, setDifference(ids, isOwner));
return setUnion(isOwner, isPartner);
}
case Permission.ASSET_VIEW: {
const isOwner = await this.repository.asset.checkOwnerAccess(authUser.id, ids);
const isAlbum = await this.repository.asset.checkAlbumAccess(authUser.id, setDifference(ids, isOwner));
const isPartner = await this.repository.asset.checkPartnerAccess(
authUser.id,
setDifference(ids, isOwner, isAlbum),
);
return setUnion(isOwner, isAlbum, isPartner);
}
case Permission.ASSET_DOWNLOAD: {
const isOwner = await this.repository.asset.checkOwnerAccess(authUser.id, ids);
const isAlbum = await this.repository.asset.checkAlbumAccess(authUser.id, setDifference(ids, isOwner));
const isPartner = await this.repository.asset.checkPartnerAccess(
authUser.id,
setDifference(ids, isOwner, isAlbum),
);
return setUnion(isOwner, isAlbum, isPartner);
}
case Permission.ASSET_UPDATE:
return await this.repository.asset.checkOwnerAccess(authUser.id, ids);
case Permission.ASSET_DELETE:
return await this.repository.asset.checkOwnerAccess(authUser.id, ids);
case Permission.ASSET_RESTORE:
return await this.repository.asset.checkOwnerAccess(authUser.id, ids);
case Permission.ALBUM_READ: {
const isOwner = await this.repository.album.checkOwnerAccess(authUser.id, ids);
const isShared = await this.repository.album.checkSharedAlbumAccess(authUser.id, setDifference(ids, isOwner));
@ -163,13 +191,13 @@ export class AccessCore {
}
case Permission.ALBUM_UPDATE:
return this.repository.album.checkOwnerAccess(authUser.id, ids);
return await this.repository.album.checkOwnerAccess(authUser.id, ids);
case Permission.ALBUM_DELETE:
return this.repository.album.checkOwnerAccess(authUser.id, ids);
return await this.repository.album.checkOwnerAccess(authUser.id, ids);
case Permission.ALBUM_SHARE:
return this.repository.album.checkOwnerAccess(authUser.id, ids);
return await this.repository.album.checkOwnerAccess(authUser.id, ids);
case Permission.ALBUM_DOWNLOAD: {
const isOwner = await this.repository.album.checkOwnerAccess(authUser.id, ids);
@ -178,16 +206,16 @@ export class AccessCore {
}
case Permission.ALBUM_REMOVE_ASSET:
return this.repository.album.checkOwnerAccess(authUser.id, ids);
return await this.repository.album.checkOwnerAccess(authUser.id, ids);
case Permission.ASSET_UPLOAD:
return this.repository.library.checkOwnerAccess(authUser.id, ids);
return await this.repository.library.checkOwnerAccess(authUser.id, ids);
case Permission.ARCHIVE_READ:
return ids.has(authUser.id) ? new Set([authUser.id]) : new Set();
case Permission.AUTH_DEVICE_DELETE:
return this.repository.authDevice.checkOwnerAccess(authUser.id, ids);
return await this.repository.authDevice.checkOwnerAccess(authUser.id, ids);
case Permission.TIMELINE_READ: {
const isOwner = ids.has(authUser.id) ? new Set([authUser.id]) : new Set<string>();
@ -205,22 +233,22 @@ export class AccessCore {
}
case Permission.LIBRARY_UPDATE:
return this.repository.library.checkOwnerAccess(authUser.id, ids);
return await this.repository.library.checkOwnerAccess(authUser.id, ids);
case Permission.LIBRARY_DELETE:
return this.repository.library.checkOwnerAccess(authUser.id, ids);
return await this.repository.library.checkOwnerAccess(authUser.id, ids);
case Permission.PERSON_READ:
return this.repository.person.checkOwnerAccess(authUser.id, ids);
return await this.repository.person.checkOwnerAccess(authUser.id, ids);
case Permission.PERSON_WRITE:
return this.repository.person.checkOwnerAccess(authUser.id, ids);
return await this.repository.person.checkOwnerAccess(authUser.id, ids);
case Permission.PERSON_MERGE:
return this.repository.person.checkOwnerAccess(authUser.id, ids);
return await this.repository.person.checkOwnerAccess(authUser.id, ids);
case Permission.PARTNER_UPDATE:
return this.repository.partner.checkUpdateAccess(authUser.id, ids);
return await this.repository.partner.checkUpdateAccess(authUser.id, ids);
}
const allowedIds = new Set();
@ -247,41 +275,6 @@ export class AccessCore {
(await this.repository.activity.hasAlbumOwnerAccess(authUser.id, id))
);
case Permission.ASSET_READ:
return (
(await this.repository.asset.hasOwnerAccess(authUser.id, id)) ||
(await this.repository.asset.hasAlbumAccess(authUser.id, id)) ||
(await this.repository.asset.hasPartnerAccess(authUser.id, id))
);
case Permission.ASSET_UPDATE:
return this.repository.asset.hasOwnerAccess(authUser.id, id);
case Permission.ASSET_DELETE:
return this.repository.asset.hasOwnerAccess(authUser.id, id);
case Permission.ASSET_RESTORE:
return this.repository.asset.hasOwnerAccess(authUser.id, id);
case Permission.ASSET_SHARE:
return (
(await this.repository.asset.hasOwnerAccess(authUser.id, id)) ||
(await this.repository.asset.hasPartnerAccess(authUser.id, id))
);
case Permission.ASSET_VIEW:
return (
(await this.repository.asset.hasOwnerAccess(authUser.id, id)) ||
(await this.repository.asset.hasAlbumAccess(authUser.id, id)) ||
(await this.repository.asset.hasPartnerAccess(authUser.id, id))
);
case Permission.ASSET_DOWNLOAD:
return (
(await this.repository.asset.hasOwnerAccess(authUser.id, id)) ||
(await this.repository.asset.hasAlbumAccess(authUser.id, id)) ||
(await this.repository.asset.hasPartnerAccess(authUser.id, id))
);
default:
return false;
}

View file

@ -509,7 +509,7 @@ describe(AlbumService.name, () => {
describe('addAssets', () => {
it('should allow the owner to add assets', async () => {
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123']));
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3']));
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
albumMock.getAssetIds.mockResolvedValueOnce(new Set());
@ -534,7 +534,7 @@ describe(AlbumService.name, () => {
it('should not set the thumbnail if the album has one already', async () => {
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123']));
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
albumMock.getById.mockResolvedValue(_.cloneDeep({ ...albumStub.empty, albumThumbnailAssetId: 'asset-id' }));
albumMock.getAssetIds.mockResolvedValueOnce(new Set());
@ -552,7 +552,7 @@ describe(AlbumService.name, () => {
it('should allow a shared user to add assets', async () => {
accessMock.album.checkSharedAlbumAccess.mockResolvedValue(new Set(['album-123']));
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3']));
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.sharedWithUser));
albumMock.getAssetIds.mockResolvedValueOnce(new Set());
@ -577,7 +577,7 @@ describe(AlbumService.name, () => {
it('should allow a shared link user to add assets', async () => {
accessMock.album.checkSharedLinkAccess.mockResolvedValue(new Set(['album-123']));
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3']));
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
albumMock.getAssetIds.mockResolvedValueOnce(new Set());
@ -607,8 +607,7 @@ describe(AlbumService.name, () => {
it('should allow adding assets shared via partner sharing', async () => {
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123']));
accessMock.asset.hasOwnerAccess.mockResolvedValue(false);
accessMock.asset.hasPartnerAccess.mockResolvedValue(true);
accessMock.asset.checkPartnerAccess.mockResolvedValue(new Set(['asset-1']));
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
albumMock.getAssetIds.mockResolvedValueOnce(new Set());
@ -621,12 +620,12 @@ describe(AlbumService.name, () => {
updatedAt: expect.any(Date),
albumThumbnailAssetId: 'asset-1',
});
expect(accessMock.asset.hasPartnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'asset-1');
expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['asset-1']));
});
it('should skip duplicate assets', async () => {
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123']));
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-id']));
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
albumMock.getAssetIds.mockResolvedValueOnce(new Set(['asset-id']));
@ -639,8 +638,6 @@ describe(AlbumService.name, () => {
it('should skip assets not shared with user', async () => {
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123']));
accessMock.asset.hasOwnerAccess.mockResolvedValue(false);
accessMock.asset.hasPartnerAccess.mockResolvedValue(false);
albumMock.getById.mockResolvedValue(albumStub.oneAsset);
albumMock.getAssetIds.mockResolvedValueOnce(new Set());
@ -648,8 +645,8 @@ describe(AlbumService.name, () => {
{ success: false, id: 'asset-1', error: BulkIdErrorReason.NO_PERMISSION },
]);
expect(accessMock.asset.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'asset-1');
expect(accessMock.asset.hasPartnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'asset-1');
expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['asset-1']));
expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['asset-1']));
});
it('should not allow unauthorized access to the album', async () => {

View file

@ -457,19 +457,15 @@ describe(AssetService.name, () => {
describe('downloadFile', () => {
it('should require the asset.download permission', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(false);
accessMock.asset.hasAlbumAccess.mockResolvedValue(false);
accessMock.asset.hasPartnerAccess.mockResolvedValue(false);
await expect(sut.downloadFile(authStub.admin, 'asset-1')).rejects.toBeInstanceOf(BadRequestException);
expect(accessMock.asset.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'asset-1');
expect(accessMock.asset.hasAlbumAccess).toHaveBeenCalledWith(authStub.admin.id, 'asset-1');
expect(accessMock.asset.hasPartnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'asset-1');
expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['asset-1']));
expect(accessMock.asset.checkAlbumAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['asset-1']));
expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['asset-1']));
});
it('should throw an error if the asset is not found', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
assetMock.getByIds.mockResolvedValue([]);
await expect(sut.downloadFile(authStub.admin, 'asset-1')).rejects.toBeInstanceOf(BadRequestException);
@ -480,7 +476,7 @@ describe(AssetService.name, () => {
it('should download a file', async () => {
const stream = new Readable();
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
assetMock.getByIds.mockResolvedValue([assetStub.image]);
storageMock.createReadStream.mockResolvedValue({ stream });
@ -496,7 +492,7 @@ describe(AssetService.name, () => {
stream: new Readable(),
};
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2']));
assetMock.getByIds.mockResolvedValue([assetStub.noResizePath, assetStub.noWebpPath]);
storageMock.createZipStream.mockReturnValue(archiveMock);
@ -516,7 +512,7 @@ describe(AssetService.name, () => {
stream: new Readable(),
};
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2']));
assetMock.getByIds.mockResolvedValue([assetStub.noResizePath, assetStub.noResizePath]);
storageMock.createZipStream.mockReturnValue(archiveMock);
@ -536,7 +532,7 @@ describe(AssetService.name, () => {
});
it('should return a list of archives (assetIds)', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2']));
assetMock.getByIds.mockResolvedValue([assetStub.image, assetStub.video]);
const assetIds = ['asset-1', 'asset-2'];
@ -602,7 +598,9 @@ describe(AssetService.name, () => {
});
it('should include the video portion of a live photo', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
const assetIds = [assetStub.livePhotoStillAsset.id];
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(assetIds));
when(assetMock.getByIds)
.calledWith([assetStub.livePhotoStillAsset.id])
.mockResolvedValue([assetStub.livePhotoStillAsset]);
@ -610,7 +608,6 @@ describe(AssetService.name, () => {
.calledWith([assetStub.livePhotoMotionAsset.id])
.mockResolvedValue([assetStub.livePhotoMotionAsset]);
const assetIds = [assetStub.livePhotoStillAsset.id];
await expect(sut.getDownloadInfo(authStub.admin, { assetIds })).resolves.toEqual({
totalSize: 125_000,
archives: [
@ -651,7 +648,6 @@ describe(AssetService.name, () => {
describe('update', () => {
it('should require asset write access for the id', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(false);
await expect(sut.update(authStub.admin, 'asset-1', { isArchived: false })).rejects.toBeInstanceOf(
BadRequestException,
);
@ -659,14 +655,14 @@ describe(AssetService.name, () => {
});
it('should update the asset', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
assetMock.save.mockResolvedValue(assetStub.image);
await sut.update(authStub.admin, 'asset-1', { isFavorite: true });
expect(assetMock.save).toHaveBeenCalledWith({ id: 'asset-1', isFavorite: true });
});
it('should update the exif description', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
assetMock.save.mockResolvedValue(assetStub.image);
await sut.update(authStub.admin, 'asset-1', { description: 'Test description' });
expect(assetMock.upsertExif).toHaveBeenCalledWith({ assetId: 'asset-1', description: 'Test description' });
@ -675,7 +671,6 @@ describe(AssetService.name, () => {
describe('updateAll', () => {
it('should require asset write access for all ids', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(false);
await expect(
sut.updateAll(authStub.admin, {
ids: ['asset-1'],
@ -685,7 +680,7 @@ describe(AssetService.name, () => {
});
it('should update all assets', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2']));
await sut.updateAll(authStub.admin, { ids: ['asset-1', 'asset-2'], isArchived: true });
expect(assetMock.updateAll).toHaveBeenCalledWith(['asset-1', 'asset-2'], { isArchived: true });
});
@ -693,8 +688,7 @@ describe(AssetService.name, () => {
/// Stack related
it('should require asset update access for parent', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
when(accessMock.asset.hasOwnerAccess).calledWith(authStub.user1.id, 'parent').mockResolvedValue(false);
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
await expect(
sut.updateAll(authStub.user1, {
ids: ['asset-1'],
@ -704,7 +698,7 @@ describe(AssetService.name, () => {
});
it('should update parent asset when children are added', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['parent']));
await sut.updateAll(authStub.user1, {
ids: [],
stackParentId: 'parent',
@ -713,7 +707,7 @@ describe(AssetService.name, () => {
});
it('should update parent asset when children are removed', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['child-1']));
assetMock.getByIds.mockResolvedValue([{ id: 'child-1', stackParentId: 'parent' } as AssetEntity]);
await sut.updateAll(authStub.user1, {
@ -724,7 +718,8 @@ describe(AssetService.name, () => {
});
it('update parentId for new children', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['child-1', 'child-2']));
accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['parent']));
await sut.updateAll(authStub.user1, {
stackParentId: 'parent',
ids: ['child-1', 'child-2'],
@ -734,7 +729,7 @@ describe(AssetService.name, () => {
});
it('nullify parentId for remove children', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['child-1', 'child-2']));
await sut.updateAll(authStub.user1, {
removeParent: true,
ids: ['child-1', 'child-2'],
@ -744,7 +739,8 @@ describe(AssetService.name, () => {
});
it('merge stacks if new child has children', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['child-1']));
accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['parent']));
assetMock.getByIds.mockResolvedValue([
{ id: 'child-1', stack: [{ id: 'child-2' } as AssetEntity] } as AssetEntity,
]);
@ -758,7 +754,9 @@ describe(AssetService.name, () => {
});
it('should send ws asset update event', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['asset-1']));
accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['parent']));
await sut.updateAll(authStub.user1, {
ids: ['asset-1'],
stackParentId: 'parent',
@ -772,7 +770,6 @@ describe(AssetService.name, () => {
describe('deleteAll', () => {
it('should require asset delete access for all ids', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(false);
await expect(
sut.deleteAll(authStub.user1, {
ids: ['asset-1'],
@ -781,7 +778,7 @@ describe(AssetService.name, () => {
});
it('should force delete a batch of assets', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset1', 'asset2']));
await sut.deleteAll(authStub.user1, { ids: ['asset1', 'asset2'], force: true });
@ -792,7 +789,7 @@ describe(AssetService.name, () => {
});
it('should soft delete a batch of assets', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset1', 'asset2']));
await sut.deleteAll(authStub.user1, { ids: ['asset1', 'asset2'], force: false });
@ -810,7 +807,6 @@ describe(AssetService.name, () => {
describe('restoreAll', () => {
it('should require asset restore access for all ids', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(false);
await expect(
sut.deleteAll(authStub.user1, {
ids: ['asset-1'],
@ -819,7 +815,7 @@ describe(AssetService.name, () => {
});
it('should restore a batch of assets', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset1', 'asset2']));
await sut.restoreAll(authStub.user1, { ids: ['asset1', 'asset2'] });
@ -984,19 +980,19 @@ describe(AssetService.name, () => {
describe('run', () => {
it('should run the refresh metadata job', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.REFRESH_METADATA }),
expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.METADATA_EXTRACTION, data: { id: 'asset-1' } });
});
it('should run the refresh thumbnails job', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.REGENERATE_THUMBNAIL }),
expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.GENERATE_JPEG_THUMBNAIL, data: { id: 'asset-1' } });
});
it('should run the transcode video', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.TRANSCODE_VIDEO }),
expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.VIDEO_CONVERSION, data: { id: 'asset-1' } });
});
@ -1004,9 +1000,7 @@ describe(AssetService.name, () => {
describe('updateStackParent', () => {
it('should require asset update access for new parent', async () => {
when(accessMock.asset.hasOwnerAccess).calledWith(authStub.user1.id, 'old').mockResolvedValue(true);
when(accessMock.asset.hasOwnerAccess).calledWith(authStub.user1.id, 'new').mockResolvedValue(false);
accessMock.asset.hasOwnerAccess.mockResolvedValue(false);
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['old']));
await expect(
sut.updateStackParent(authStub.user1, {
oldParentId: 'old',
@ -1016,8 +1010,7 @@ describe(AssetService.name, () => {
});
it('should require asset read access for old parent', async () => {
when(accessMock.asset.hasOwnerAccess).calledWith(authStub.user1.id, 'old').mockResolvedValue(false);
when(accessMock.asset.hasOwnerAccess).calledWith(authStub.user1.id, 'new').mockResolvedValue(true);
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['new']));
await expect(
sut.updateStackParent(authStub.user1, {
oldParentId: 'old',
@ -1027,7 +1020,9 @@ describe(AssetService.name, () => {
});
it('make old parent the child of new parent', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set([assetStub.image.id]));
accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['new']));
when(assetMock.getById)
.calledWith(assetStub.image.id)
.mockResolvedValue(assetStub.image as AssetEntity);
@ -1041,7 +1036,9 @@ describe(AssetService.name, () => {
});
it('remove stackParentId of new parent', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set([assetStub.primaryImage.id]));
accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['new']));
await sut.updateStackParent(authStub.user1, {
oldParentId: assetStub.primaryImage.id,
newParentId: 'new',
@ -1051,7 +1048,8 @@ describe(AssetService.name, () => {
});
it('update stackParentId of old parents children to new parent', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set([assetStub.primaryImage.id]));
accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['new']));
when(assetMock.getById)
.calledWith(assetStub.primaryImage.id)
.mockResolvedValue(assetStub.primaryImage as AssetEntity);

View file

@ -8,7 +8,7 @@ import { AccessCore, Permission } from '../access';
import { AuthUserDto } from '../auth';
import { mimeTypes } from '../domain.constant';
import { HumanReadableSize, usePagination } from '../domain.util';
import { IAssetDeletionJob, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job';
import { IAssetDeletionJob, ISidecarWriteJob, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job';
import {
CommunicationEvent,
IAccessRepository,
@ -393,10 +393,8 @@ export class AssetService {
async update(authUser: AuthUserDto, id: string, dto: UpdateAssetDto): Promise<AssetResponseDto> {
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, id);
const { description, ...rest } = dto;
if (description !== undefined) {
await this.assetRepository.upsertExif({ assetId: id, description });
}
const { description, dateTimeOriginal, latitude, longitude, ...rest } = dto;
await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude });
const asset = await this.assetRepository.save({ id, ...rest });
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSET, data: { ids: [id] } });
@ -404,7 +402,7 @@ export class AssetService {
}
async updateAll(authUser: AuthUserDto, dto: AssetBulkUpdateDto): Promise<void> {
const { ids, removeParent, ...options } = dto;
const { ids, removeParent, dateTimeOriginal, latitude, longitude, ...options } = dto;
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, ids);
if (removeParent) {
@ -424,6 +422,10 @@ export class AssetService {
await this.assetRepository.updateAll([options.stackParentId], { stackParentId: null });
}
for (const id of ids) {
await this.updateMetadata({ id, dateTimeOriginal, latitude, longitude });
}
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSET, data: { ids } });
await this.assetRepository.updateAll(ids, options);
this.communicationRepository.send(CommunicationEvent.ASSET_UPDATE, authUser.id, ids);
@ -587,4 +589,13 @@ export class AssetService {
}
}
}
private async updateMetadata(dto: ISidecarWriteJob) {
const { id, description, dateTimeOriginal, latitude, longitude } = dto;
const writes = _.omitBy({ description, dateTimeOriginal, latitude, longitude }, _.isUndefined);
if (Object.keys(writes).length > 0) {
await this.assetRepository.upsertExif({ assetId: id, ...writes });
await this.jobRepository.queue({ name: JobName.SIDECAR_WRITE, data: { id, ...writes } });
}
}
}

View file

@ -1,7 +1,19 @@
import { AssetType } from '@app/infra/entities';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsBoolean, IsEnum, IsInt, IsPositive, IsString, Min } from 'class-validator';
import {
IsBoolean,
IsDateString,
IsEnum,
IsInt,
IsLatitude,
IsLongitude,
IsNotEmpty,
IsPositive,
IsString,
Min,
ValidateIf,
} from 'class-validator';
import { Optional, QueryBoolean, QueryDate, ValidateUUID } from '../../domain.util';
import { BulkIdsDto } from '../response-dto';
@ -10,6 +22,10 @@ export enum AssetOrder {
DESC = 'desc',
}
const hasGPS = (o: { latitude: undefined; longitude: undefined }) =>
o.latitude !== undefined || o.longitude !== undefined;
const ValidateGPS = () => ValidateIf(hasGPS);
export class AssetSearchDto {
@ValidateUUID({ optional: true })
id?: string;
@ -172,6 +188,20 @@ export class AssetBulkUpdateDto extends BulkIdsDto {
@Optional()
@IsBoolean()
removeParent?: boolean;
@Optional()
@IsDateString()
dateTimeOriginal?: string;
@ValidateGPS()
@IsLatitude()
@IsNotEmpty()
latitude?: number;
@ValidateGPS()
@IsLongitude()
@IsNotEmpty()
longitude?: number;
}
export class UpdateAssetDto {
@ -186,6 +216,20 @@ export class UpdateAssetDto {
@Optional()
@IsString()
description?: string;
@Optional()
@IsDateString()
dateTimeOriginal?: string;
@ValidateGPS()
@IsLatitude()
@IsNotEmpty()
latitude?: number;
@ValidateGPS()
@IsLongitude()
@IsNotEmpty()
longitude?: number;
}
export class RandomAssetsDto {

View file

@ -98,7 +98,7 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As
tags: entity.tags?.map(mapTag),
people: entity.faces
?.map(mapFace)
.filter((person): person is PersonResponseDto => person !== null && !person.isHidden)
.filter((person): person is PersonResponseDto => person !== null)
.reduce((people, person) => {
const existingPerson = people.find((p) => p.id === person.id);
if (!existingPerson) {

View file

@ -155,18 +155,35 @@ export function Optional({ nullable, ...validationOptions }: OptionalOptions = {
// They should be replaced with native Set operations, when they are added to the language.
// Proposal reference: https://github.com/tc39/proposal-set-methods
export const setUnion = <T>(setA: Set<T>, setB: Set<T>): Set<T> => {
const union = new Set(setA);
for (const elem of setB) {
union.add(elem);
export const setUnion = <T>(...sets: Set<T>[]): Set<T> => {
const union = new Set(sets[0]);
for (const set of sets.slice(1)) {
for (const elem of set) {
union.add(elem);
}
}
return union;
};
export const setDifference = <T>(setA: Set<T>, setB: Set<T>): Set<T> => {
export const setDifference = <T>(setA: Set<T>, ...sets: Set<T>[]): Set<T> => {
const difference = new Set(setA);
for (const elem of setB) {
difference.delete(elem);
for (const set of sets) {
for (const elem of set) {
difference.delete(elem);
}
}
return difference;
};
export const setIsSuperset = <T>(set: Set<T>, subset: Set<T>): boolean => {
for (const elem of subset) {
if (!set.has(elem)) {
return false;
}
}
return true;
};
export const setIsEqual = <T>(setA: Set<T>, setB: Set<T>): boolean => {
return setA.size === setB.size && setIsSuperset(setA, setB);
};

View file

@ -96,6 +96,7 @@ export enum JobName {
QUEUE_SIDECAR = 'queue-sidecar',
SIDECAR_DISCOVERY = 'sidecar-discovery',
SIDECAR_SYNC = 'sidecar-sync',
SIDECAR_WRITE = 'sidecar-write',
}
export const JOBS_ASSET_PAGINATION_SIZE = 1000;
@ -168,6 +169,7 @@ export const JOBS_TO_QUEUE: Record<JobName, QueueName> = {
[JobName.QUEUE_SIDECAR]: QueueName.SIDECAR,
[JobName.SIDECAR_DISCOVERY]: QueueName.SIDECAR,
[JobName.SIDECAR_SYNC]: QueueName.SIDECAR,
[JobName.SIDECAR_WRITE]: QueueName.SIDECAR,
// Library management
[JobName.LIBRARY_SCAN_ASSET]: QueueName.LIBRARY,

View file

@ -9,7 +9,7 @@ export interface IAssetFaceJob extends IBaseJob {
export interface IEntityJob extends IBaseJob {
id: string;
source?: 'upload';
source?: 'upload' | 'sidecar-write';
}
export interface IAssetDeletionJob extends IEntityJob {
@ -33,3 +33,10 @@ export interface IBulkEntityJob extends IBaseJob {
export interface IDeleteFilesJob extends IBaseJob {
files: Array<string | null | undefined>;
}
export interface ISidecarWriteJob extends IEntityJob {
description?: string;
dateTimeOriginal?: string;
latitude?: number;
longitude?: number;
}

View file

@ -165,7 +165,19 @@ export class JobService {
await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: item.data });
break;
case JobName.SIDECAR_WRITE:
await this.jobRepository.queue({
name: JobName.METADATA_EXTRACTION,
data: { id: item.data.id, source: 'sidecar-write' },
});
case JobName.METADATA_EXTRACTION:
if (item.data.source === 'sidecar-write') {
const [asset] = await this.assetRepository.getByIds([item.data.id]);
if (asset) {
this.communicationRepository.send(CommunicationEvent.ASSET_UPDATE, asset.ownerId, mapAsset(asset));
}
}
await this.jobRepository.queue({ name: JobName.LINK_LIVE_PHOTOS, data: item.data });
break;

View file

@ -58,7 +58,8 @@ describe(LibraryService.name, () => {
ctime: new Date('2023-01-01'),
} as Stats);
accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([authStub.admin.id]));
// Always validate owner access for library.
accessMock.library.checkOwnerAccess.mockImplementation(async (_, libraryIds) => libraryIds);
sut = new LibraryService(
accessMock,

View file

@ -218,11 +218,11 @@ describe(MetadataService.name, () => {
const originalDate = new Date('2023-11-21T16:13:17.517Z');
const sidecarDate = new Date('2022-01-01T00:00:00.000Z');
assetMock.getByIds.mockResolvedValue([assetStub.sidecar]);
when(metadataMock.getExifTags)
when(metadataMock.readTags)
.calledWith(assetStub.sidecar.originalPath)
// higher priority tag
.mockResolvedValue({ CreationDate: originalDate.toISOString() });
when(metadataMock.getExifTags)
when(metadataMock.readTags)
.calledWith(assetStub.sidecar.sidecarPath as string)
// lower priority tag, but in sidecar
.mockResolvedValue({ CreateDate: sidecarDate.toISOString() });
@ -240,7 +240,7 @@ describe(MetadataService.name, () => {
it('should handle lists of numbers', async () => {
assetMock.getByIds.mockResolvedValue([assetStub.image]);
metadataMock.getExifTags.mockResolvedValue({ ISO: [160] as any });
metadataMock.readTags.mockResolvedValue({ ISO: [160] as any });
await sut.handleMetadataExtraction({ id: assetStub.image.id });
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
@ -257,7 +257,7 @@ describe(MetadataService.name, () => {
assetMock.getByIds.mockResolvedValue([assetStub.withLocation]);
configMock.load.mockResolvedValue([{ key: SystemConfigKey.REVERSE_GEOCODING_ENABLED, value: true }]);
metadataMock.reverseGeocode.mockResolvedValue({ city: 'City', state: 'State', country: 'Country' });
metadataMock.getExifTags.mockResolvedValue({
metadataMock.readTags.mockResolvedValue({
GPSLatitude: assetStub.withLocation.exifInfo!.latitude!,
GPSLongitude: assetStub.withLocation.exifInfo!.longitude!,
});
@ -289,7 +289,7 @@ describe(MetadataService.name, () => {
it('should apply motion photos', async () => {
assetMock.getByIds.mockResolvedValue([{ ...assetStub.livePhotoStillAsset, livePhotoVideoId: null }]);
metadataMock.getExifTags.mockResolvedValue({
metadataMock.readTags.mockResolvedValue({
Directory: 'foo/bar/',
MotionPhoto: 1,
MicroVideo: 1,
@ -310,7 +310,7 @@ describe(MetadataService.name, () => {
it('should create new motion asset if not found and link it with the photo', async () => {
assetMock.getByIds.mockResolvedValue([{ ...assetStub.livePhotoStillAsset, livePhotoVideoId: null }]);
metadataMock.getExifTags.mockResolvedValue({
metadataMock.readTags.mockResolvedValue({
Directory: 'foo/bar/',
MotionPhoto: 1,
MicroVideo: 1,
@ -367,7 +367,7 @@ describe(MetadataService.name, () => {
tz: '+02:00',
};
assetMock.getByIds.mockResolvedValue([assetStub.image]);
metadataMock.getExifTags.mockResolvedValue(tags);
metadataMock.readTags.mockResolvedValue(tags);
await sut.handleMetadataExtraction({ id: assetStub.image.id });
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
@ -406,7 +406,7 @@ describe(MetadataService.name, () => {
it('should handle duration', async () => {
assetMock.getByIds.mockResolvedValue([assetStub.image]);
metadataMock.getExifTags.mockResolvedValue({ Duration: 6.21 });
metadataMock.readTags.mockResolvedValue({ Duration: 6.21 });
await sut.handleMetadataExtraction({ id: assetStub.image.id });
@ -422,7 +422,7 @@ describe(MetadataService.name, () => {
it('should handle duration as an object without Scale', async () => {
assetMock.getByIds.mockResolvedValue([assetStub.image]);
metadataMock.getExifTags.mockResolvedValue({ Duration: { Value: 6.2 } });
metadataMock.readTags.mockResolvedValue({ Duration: { Value: 6.2 } });
await sut.handleMetadataExtraction({ id: assetStub.image.id });
@ -438,7 +438,7 @@ describe(MetadataService.name, () => {
it('should handle duration with scale', async () => {
assetMock.getByIds.mockResolvedValue([assetStub.image]);
metadataMock.getExifTags.mockResolvedValue({ Duration: { Scale: 1.11111111111111e-5, Value: 558720 } });
metadataMock.readTags.mockResolvedValue({ Duration: { Scale: 1.11111111111111e-5, Value: 558720 } });
await sut.handleMetadataExtraction({ id: assetStub.image.id });
@ -531,4 +531,41 @@ describe(MetadataService.name, () => {
});
});
});
describe('handleSidecarWrite', () => {
it('should skip assets that do not exist anymore', async () => {
assetMock.getByIds.mockResolvedValue([]);
await expect(sut.handleSidecarWrite({ id: 'asset-123' })).resolves.toBe(false);
expect(metadataMock.writeTags).not.toHaveBeenCalled();
});
it('should skip jobs with not metadata', async () => {
assetMock.getByIds.mockResolvedValue([assetStub.sidecar]);
await expect(sut.handleSidecarWrite({ id: assetStub.sidecar.id })).resolves.toBe(true);
expect(metadataMock.writeTags).not.toHaveBeenCalled();
});
it('should write tags', async () => {
const description = 'this is a description';
const gps = 12;
const date = '2023-11-22T04:56:12.196Z';
assetMock.getByIds.mockResolvedValue([assetStub.sidecar]);
await expect(
sut.handleSidecarWrite({
id: assetStub.sidecar.id,
description,
latitude: gps,
longitude: gps,
dateTimeOriginal: date,
}),
).resolves.toBe(true);
expect(metadataMock.writeTags).toHaveBeenCalledWith(assetStub.sidecar.sidecarPath, {
ImageDescription: description,
CreationDate: date,
GPSLatitude: gps,
GPSLongitude: gps,
});
});
});
});

View file

@ -3,10 +3,11 @@ import { Inject, Injectable, Logger } from '@nestjs/common';
import { ExifDateTime, Tags } from 'exiftool-vendored';
import { firstDateTime } from 'exiftool-vendored/dist/FirstDateTime';
import { constants } from 'fs/promises';
import _ from 'lodash';
import { Duration } from 'luxon';
import { Subscription } from 'rxjs';
import { usePagination } from '../domain.util';
import { IBaseJob, IEntityJob, JOBS_ASSET_PAGINATION_SIZE, JobName, QueueName } from '../job';
import { IBaseJob, IEntityJob, ISidecarWriteJob, JOBS_ASSET_PAGINATION_SIZE, JobName, QueueName } from '../job';
import {
ExifDuration,
IAlbumRepository,
@ -79,7 +80,6 @@ export class MetadataService {
private logger = new Logger(MetadataService.name);
private storageCore: StorageCore;
private configCore: SystemConfigCore;
private oldCities?: string;
private subscription: Subscription | null = null;
constructor(
@ -244,6 +244,37 @@ export class MetadataService {
return true;
}
async handleSidecarWrite(job: ISidecarWriteJob) {
const { id, description, dateTimeOriginal, latitude, longitude } = job;
const [asset] = await this.assetRepository.getByIds([id]);
if (!asset) {
return false;
}
const sidecarPath = asset.sidecarPath || `${asset.originalPath}.xmp`;
const exif = _.omitBy<Tags>(
{
ImageDescription: description,
CreationDate: dateTimeOriginal,
GPSLatitude: latitude,
GPSLongitude: longitude,
},
_.isUndefined,
);
if (Object.keys(exif).length === 0) {
return true;
}
await this.repository.writeTags(sidecarPath, exif);
if (!asset.sidecarPath) {
await this.assetRepository.save({ id, sidecarPath });
}
return true;
}
private async applyReverseGeocoding(asset: AssetEntity, exifData: ExifEntityWithoutGeocodeAndTypeOrm) {
const { latitude, longitude } = exifData;
if (!(await this.configCore.hasFeature(FeatureFlag.REVERSE_GEOCODING)) || !longitude || !latitude) {
@ -346,8 +377,8 @@ export class MetadataService {
asset: AssetEntity,
): Promise<{ exifData: ExifEntityWithoutGeocodeAndTypeOrm; tags: ImmichTags }> {
const stats = await this.storageRepository.stat(asset.originalPath);
const mediaTags = await this.repository.getExifTags(asset.originalPath);
const sidecarTags = asset.sidecarPath ? await this.repository.getExifTags(asset.sidecarPath) : null;
const mediaTags = await this.repository.readTags(asset.originalPath);
const sidecarTags = asset.sidecarPath ? await this.repository.readTags(asset.sidecarPath) : null;
// ensure date from sidecar is used if present
const hasDateOverride = !!this.getDateTimeOriginal(sidecarTags);

View file

@ -331,7 +331,7 @@ describe(PersonService.name, () => {
personMock.getById.mockResolvedValue(personStub.withName);
personMock.update.mockResolvedValue(personStub.withName);
personMock.getFacesByIds.mockResolvedValue([faceStub.face1]);
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
await expect(

View file

@ -6,11 +6,12 @@ export interface IAccessRepository {
hasAlbumOwnerAccess(userId: string, activityId: string): Promise<boolean>;
hasCreateAccess(userId: string, albumId: string): Promise<boolean>;
};
asset: {
hasOwnerAccess(userId: string, assetId: string): Promise<boolean>;
hasAlbumAccess(userId: string, assetId: string): Promise<boolean>;
hasPartnerAccess(userId: string, assetId: string): Promise<boolean>;
hasSharedLinkAccess(sharedLinkId: string, assetId: string): Promise<boolean>;
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: {

View file

@ -9,6 +9,7 @@ import {
IEntityJob,
ILibraryFileJob,
ILibraryRefreshJob,
ISidecarWriteJob,
} from '../job/job.interface';
export interface JobCounts {
@ -54,11 +55,11 @@ export type JobItem =
| { name: JobName.QUEUE_METADATA_EXTRACTION; data: IBaseJob }
| { name: JobName.METADATA_EXTRACTION; data: IEntityJob }
| { name: JobName.LINK_LIVE_PHOTOS; data: IEntityJob }
// Sidecar Scanning
| { name: JobName.QUEUE_SIDECAR; data: IBaseJob }
| { name: JobName.SIDECAR_DISCOVERY; data: IEntityJob }
| { name: JobName.SIDECAR_SYNC; data: IEntityJob }
| { name: JobName.SIDECAR_WRITE; data: ISidecarWriteJob }
// Object Tagging
| { name: JobName.QUEUE_OBJECT_TAGGING; data: IBaseJob }

View file

@ -33,5 +33,6 @@ export interface IMetadataRepository {
init(): Promise<void>;
teardown(): Promise<void>;
reverseGeocode(point: GeoPoint): Promise<ReverseGeocodeResult | null>;
getExifTags(path: string): Promise<ImmichTags | null>;
readTags(path: string): Promise<ImmichTags | null>;
writeTags(path: string, tags: Partial<Tags>): Promise<void>;
}

View file

@ -11,7 +11,6 @@ import {
sharedLinkResponseStub,
sharedLinkStub,
} from '@test';
import { when } from 'jest-when';
import _ from 'lodash';
import { AssetIdErrorReason } from '../asset';
import { ICryptoRepository, ISharedLinkRepository } from '../repositories';
@ -109,7 +108,6 @@ describe(SharedLinkService.name, () => {
});
it('should require asset ownership to make an individual shared link', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(false);
await expect(
sut.create(authStub.admin, { type: SharedLinkType.INDIVIDUAL, assetIds: ['asset-1'] }),
).rejects.toBeInstanceOf(BadRequestException);
@ -140,7 +138,7 @@ describe(SharedLinkService.name, () => {
});
it('should create an individual shared link', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
shareMock.create.mockResolvedValue(sharedLinkStub.individual);
await sut.create(authStub.admin, {
@ -151,7 +149,7 @@ describe(SharedLinkService.name, () => {
allowUpload: true,
});
expect(accessMock.asset.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, assetStub.image.id);
expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set([assetStub.image.id]));
expect(shareMock.create).toHaveBeenCalledWith({
type: SharedLinkType.INDIVIDUAL,
userId: authStub.admin.id,
@ -215,9 +213,7 @@ describe(SharedLinkService.name, () => {
it('should add assets to a shared link', async () => {
shareMock.get.mockResolvedValue(_.cloneDeep(sharedLinkStub.individual));
shareMock.create.mockResolvedValue(sharedLinkStub.individual);
when(accessMock.asset.hasOwnerAccess).calledWith(authStub.admin.id, 'asset-2').mockResolvedValue(false);
when(accessMock.asset.hasOwnerAccess).calledWith(authStub.admin.id, 'asset-3').mockResolvedValue(true);
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-3']));
await expect(
sut.addAssets(authStub.admin, 'link-1', { assetIds: [assetStub.image.id, 'asset-2', 'asset-3'] }),
@ -227,7 +223,7 @@ describe(SharedLinkService.name, () => {
{ assetId: 'asset-3', success: true },
]);
expect(accessMock.asset.hasOwnerAccess).toHaveBeenCalledTimes(2);
expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledTimes(1);
expect(shareMock.update).toHaveBeenCalledWith({
...sharedLinkStub.individual,
assets: [assetStub.image, { id: 'asset-3' }],

View file

@ -232,54 +232,49 @@ describe('AssetService', () => {
describe('getAssetById', () => {
it('should allow owner access', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
assetRepositoryMock.getById.mockResolvedValue(assetStub.image);
await sut.getAssetById(authStub.admin, assetStub.image.id);
expect(accessMock.asset.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, assetStub.image.id);
expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set([assetStub.image.id]));
});
it('should allow shared link access', async () => {
accessMock.asset.hasSharedLinkAccess.mockResolvedValue(true);
accessMock.asset.checkSharedLinkAccess.mockResolvedValue(new Set([assetStub.image.id]));
assetRepositoryMock.getById.mockResolvedValue(assetStub.image);
await sut.getAssetById(authStub.adminSharedLink, assetStub.image.id);
expect(accessMock.asset.hasSharedLinkAccess).toHaveBeenCalledWith(
expect(accessMock.asset.checkSharedLinkAccess).toHaveBeenCalledWith(
authStub.adminSharedLink.sharedLinkId,
assetStub.image.id,
new Set([assetStub.image.id]),
);
});
it('should allow partner sharing access', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(false);
accessMock.asset.hasPartnerAccess.mockResolvedValue(true);
accessMock.asset.checkPartnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
assetRepositoryMock.getById.mockResolvedValue(assetStub.image);
await sut.getAssetById(authStub.admin, assetStub.image.id);
expect(accessMock.asset.hasPartnerAccess).toHaveBeenCalledWith(authStub.admin.id, assetStub.image.id);
expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith(
authStub.admin.id,
new Set([assetStub.image.id]),
);
});
it('should allow shared album access', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(false);
accessMock.asset.hasPartnerAccess.mockResolvedValue(false);
accessMock.asset.hasAlbumAccess.mockResolvedValue(true);
accessMock.asset.checkAlbumAccess.mockResolvedValue(new Set([assetStub.image.id]));
assetRepositoryMock.getById.mockResolvedValue(assetStub.image);
await sut.getAssetById(authStub.admin, assetStub.image.id);
expect(accessMock.asset.hasAlbumAccess).toHaveBeenCalledWith(authStub.admin.id, assetStub.image.id);
expect(accessMock.asset.checkAlbumAccess).toHaveBeenCalledWith(authStub.admin.id, new Set([assetStub.image.id]));
});
it('should throw an error for no access', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(false);
accessMock.asset.hasPartnerAccess.mockResolvedValue(false);
accessMock.asset.hasSharedLinkAccess.mockResolvedValue(false);
accessMock.asset.hasAlbumAccess.mockResolvedValue(false);
await expect(sut.getAssetById(authStub.admin, assetStub.image.id)).rejects.toBeInstanceOf(BadRequestException);
expect(assetRepositoryMock.getById).not.toHaveBeenCalled();
});
it('should throw an error for an invalid shared link', async () => {
accessMock.asset.hasSharedLinkAccess.mockResolvedValue(false);
await expect(sut.getAssetById(authStub.adminSharedLink, assetStub.image.id)).rejects.toBeInstanceOf(
BadRequestException,
);
expect(accessMock.asset.hasOwnerAccess).not.toHaveBeenCalled();
expect(accessMock.asset.checkOwnerAccess).not.toHaveBeenCalled();
expect(assetRepositoryMock.getById).not.toHaveBeenCalled();
});
});

View file

@ -36,10 +36,10 @@ import { databaseConfig } from './database.config';
import { databaseEntities } from './entities';
import { bullConfig, bullQueues } from './infra.config';
import {
APIKeyRepository,
AccessRepository,
ActivityRepository,
AlbumRepository,
ApiKeyRepository,
AssetRepository,
AuditRepository,
CommunicationRepository,
@ -74,7 +74,7 @@ const providers: Provider[] = [
{ provide: ICryptoRepository, useClass: CryptoRepository },
{ provide: IJobRepository, useClass: JobRepository },
{ provide: ILibraryRepository, useClass: LibraryRepository },
{ provide: IKeyRepository, useClass: APIKeyRepository },
{ provide: IKeyRepository, useClass: ApiKeyRepository },
{ provide: IMachineLearningRepository, useClass: MachineLearningRepository },
{ provide: IMetadataRepository, useClass: MetadataRepository },
{ provide: IMoveRepository, useClass: MoveRepository },

View file

@ -0,0 +1,20 @@
import { SetMetadata } from '@nestjs/common';
export const GENERATE_SQL_KEY = 'generate-sql-key';
export interface GenerateSqlQueries {
name?: string;
params?: any[];
}
/** Decorator to enable versioning/tracking of generated Sql */
export const GenerateSql = (...options: GenerateSqlQueries[]) => SetMetadata(GENERATE_SQL_KEY, options);
export const DummyValue = {
UUID: '00000000-0000-4000-a000-000000000000',
PAGINATION: { take: 10, skip: 0 },
EMAIL: 'user@immich.app',
STRING: 'abcdefghi',
BUFFER: Buffer.from('abcdefghi'),
DATE: new Date(),
};

View file

@ -1,6 +1,6 @@
import { IAccessRepository } from '@app/domain';
import { InjectRepository } from '@nestjs/typeorm';
import { In, Repository } from 'typeorm';
import { Brackets, In, Repository } from 'typeorm';
import {
ActivityEntity,
AlbumEntity,
@ -112,107 +112,120 @@ export class AccessRepository implements IAccessRepository {
};
asset = {
hasAlbumAccess: (userId: string, assetId: string): Promise<boolean> => {
return this.albumRepository.exist({
where: [
{
checkAlbumAccess: async (userId: string, assetIds: Set<string>): Promise<Set<string>> => {
if (assetIds.size === 0) {
return new Set();
}
return this.albumRepository
.createQueryBuilder('album')
.innerJoin('album.assets', 'asset')
.leftJoin('album.sharedUsers', 'sharedUsers')
.select('asset.id', 'assetId')
.addSelect('asset.livePhotoVideoId', 'livePhotoVideoId')
.where(
new Brackets((qb) => {
qb.where('album.ownerId = :userId', { userId }).orWhere('sharedUsers.id = :userId', { userId });
}),
)
.andWhere(
new Brackets((qb) => {
qb.where('asset.id IN (:...assetIds)', { assetIds: [...assetIds] })
// still part of a live photo is in an album
.orWhere('asset.livePhotoVideoId IN (:...assetIds)', { assetIds: [...assetIds] });
}),
)
.getRawMany()
.then((rows) => {
const allowedIds = new Set<string>();
for (const row of rows) {
if (row.assetId && assetIds.has(row.assetId)) {
allowedIds.add(row.assetId);
}
if (row.livePhotoVideoId && assetIds.has(row.livePhotoVideoId)) {
allowedIds.add(row.livePhotoVideoId);
}
}
return allowedIds;
});
},
checkOwnerAccess: async (userId: string, assetIds: Set<string>): Promise<Set<string>> => {
if (assetIds.size === 0) {
return new Set();
}
return this.assetRepository
.find({
select: { id: true },
where: {
id: In([...assetIds]),
ownerId: userId,
assets: {
id: assetId,
},
},
{
sharedUsers: {
id: userId,
},
assets: {
id: assetId,
},
},
// still part of a live photo is in an album
{
ownerId: userId,
assets: {
livePhotoVideoId: assetId,
},
},
{
sharedUsers: {
id: userId,
},
assets: {
livePhotoVideoId: assetId,
},
},
],
});
withDeleted: true,
})
.then((assets) => new Set(assets.map((asset) => asset.id)));
},
hasOwnerAccess: (userId: string, assetId: string): Promise<boolean> => {
return this.assetRepository.exist({
where: {
id: assetId,
ownerId: userId,
},
withDeleted: true,
});
checkPartnerAccess: async (userId: string, assetIds: Set<string>): Promise<Set<string>> => {
if (assetIds.size === 0) {
return new Set();
}
return this.partnerRepository
.createQueryBuilder('partner')
.innerJoin('partner.sharedBy', 'sharedBy')
.innerJoin('sharedBy.assets', 'asset')
.select('asset.id', 'assetId')
.where('partner.sharedWithId = :userId', { userId })
.andWhere('asset.id IN (:...assetIds)', { assetIds: [...assetIds] })
.getRawMany()
.then((rows) => new Set(rows.map((row) => row.assetId)));
},
hasPartnerAccess: (userId: string, assetId: string): Promise<boolean> => {
return this.partnerRepository.exist({
where: {
sharedWith: {
id: userId,
},
sharedBy: {
assets: {
id: assetId,
},
},
},
relations: {
sharedWith: true,
sharedBy: {
assets: true,
},
},
});
},
checkSharedLinkAccess: async (sharedLinkId: string, assetIds: Set<string>): Promise<Set<string>> => {
if (assetIds.size === 0) {
return new Set();
}
hasSharedLinkAccess: async (sharedLinkId: string, assetId: string): Promise<boolean> => {
return this.sharedLinkRepository.exist({
where: [
{
id: sharedLinkId,
album: {
assets: {
id: assetId,
},
},
},
{
id: sharedLinkId,
assets: {
id: assetId,
},
},
// still part of a live photo is in a shared link
{
id: sharedLinkId,
album: {
assets: {
livePhotoVideoId: assetId,
},
},
},
{
id: sharedLinkId,
assets: {
livePhotoVideoId: assetId,
},
},
],
});
return this.sharedLinkRepository
.createQueryBuilder('sharedLink')
.leftJoin('sharedLink.album', 'album')
.leftJoin('sharedLink.assets', 'assets')
.leftJoin('album.assets', 'albumAssets')
.select('assets.id', 'assetId')
.addSelect('albumAssets.id', 'albumAssetId')
.addSelect('assets.livePhotoVideoId', 'assetLivePhotoVideoId')
.addSelect('albumAssets.livePhotoVideoId', 'albumAssetLivePhotoVideoId')
.where('sharedLink.id = :sharedLinkId', { sharedLinkId })
.andWhere(
new Brackets((qb) => {
qb.where('assets.id IN (:...assetIds)', { assetIds: [...assetIds] })
.orWhere('albumAssets.id IN (:...assetIds)', { assetIds: [...assetIds] })
// still part of a live photo is in a shared link
.orWhere('assets.livePhotoVideoId IN (:...assetIds)', { assetIds: [...assetIds] })
.orWhere('albumAssets.livePhotoVideoId IN (:...assetIds)', { assetIds: [...assetIds] });
}),
)
.getRawMany()
.then((rows) => {
const allowedIds = new Set<string>();
for (const row of rows) {
if (row.assetId && assetIds.has(row.assetId)) {
allowedIds.add(row.assetId);
}
if (row.assetLivePhotoVideoId && assetIds.has(row.assetLivePhotoVideoId)) {
allowedIds.add(row.assetLivePhotoVideoId);
}
if (row.albumAssetId && assetIds.has(row.albumAssetId)) {
allowedIds.add(row.albumAssetId);
}
if (row.albumAssetLivePhotoVideoId && assetIds.has(row.albumAssetLivePhotoVideoId)) {
allowedIds.add(row.albumAssetLivePhotoVideoId);
}
}
return allowedIds;
});
},
};

View file

@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { IsNull, Repository } from 'typeorm';
import { ActivityEntity } from '../entities/activity.entity';
import { DummyValue, GenerateSql } from '../infra.util';
export interface ActivitySearch {
albumId?: string;
@ -15,6 +16,7 @@ export interface ActivitySearch {
export class ActivityRepository implements IActivityRepository {
constructor(@InjectRepository(ActivityEntity) private repository: Repository<ActivityEntity>) {}
@GenerateSql({ params: [{ albumId: DummyValue.UUID }] })
search(options: ActivitySearch): Promise<ActivityEntity[]> {
const { userId, assetId, albumId, isLiked } = options;
return this.repository.find({
@ -41,6 +43,7 @@ export class ActivityRepository implements IActivityRepository {
await this.repository.delete(id);
}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
getStatistics(assetId: string, albumId: string): Promise<number> {
return this.repository.count({
where: { assetId, albumId, isLiked: false },

View file

@ -4,6 +4,7 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { DataSource, FindOptionsOrder, FindOptionsRelations, In, IsNull, Not, Repository } from 'typeorm';
import { dataSource } from '../database.config';
import { AlbumEntity, AssetEntity } from '../entities';
import { DummyValue, GenerateSql } from '../infra.util';
@Injectable()
export class AlbumRepository implements IAlbumRepository {
@ -13,6 +14,7 @@ export class AlbumRepository implements IAlbumRepository {
@InjectDataSource() private dataSource: DataSource,
) {}
@GenerateSql({ params: [DummyValue.UUID, {}] })
getById(id: string, options: AlbumInfoOptions): Promise<AlbumEntity | null> {
const relations: FindOptionsRelations<AlbumEntity> = {
owner: true,
@ -36,6 +38,7 @@ export class AlbumRepository implements IAlbumRepository {
return this.repository.findOne({ where: { id }, relations, order });
}
@GenerateSql({ params: [[DummyValue.UUID]] })
getByIds(ids: string[]): Promise<AlbumEntity[]> {
return this.repository.find({
where: {
@ -48,6 +51,7 @@ export class AlbumRepository implements IAlbumRepository {
});
}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
getByAssetId(ownerId: string, assetId: string): Promise<AlbumEntity[]> {
return this.repository.find({
where: [
@ -59,6 +63,7 @@ export class AlbumRepository implements IAlbumRepository {
});
}
@GenerateSql({ params: [[DummyValue.UUID]] })
async getMetadataForIds(ids: string[]): Promise<AlbumAssetCount[]> {
// Guard against running invalid query when ids list is empty.
if (!ids.length) {
@ -91,6 +96,7 @@ export class AlbumRepository implements IAlbumRepository {
* - Thumbnail references an asset outside the album
* - Empty album still has a thumbnail set
*/
@GenerateSql()
async getInvalidThumbnail(): Promise<string[]> {
// Using dataSource, because there is no direct access to albums_assets_assets.
const albumHasAssets = this.dataSource
@ -113,6 +119,7 @@ export class AlbumRepository implements IAlbumRepository {
return albums.map((album) => album.id);
}
@GenerateSql({ params: [DummyValue.UUID] })
getOwned(ownerId: string): Promise<AlbumEntity[]> {
return this.repository.find({
relations: { sharedUsers: true, sharedLinks: true, owner: true },
@ -124,6 +131,7 @@ export class AlbumRepository implements IAlbumRepository {
/**
* Get albums shared with and shared by owner.
*/
@GenerateSql({ params: [DummyValue.UUID] })
getShared(ownerId: string): Promise<AlbumEntity[]> {
return this.repository.find({
relations: { sharedUsers: true, sharedLinks: true, owner: true },
@ -139,6 +147,7 @@ export class AlbumRepository implements IAlbumRepository {
/**
* Get albums of owner that are _not_ shared
*/
@GenerateSql({ params: [DummyValue.UUID] })
getNotShared(ownerId: string): Promise<AlbumEntity[]> {
return this.repository.find({
relations: { sharedUsers: true, sharedLinks: true, owner: true },
@ -159,6 +168,7 @@ export class AlbumRepository implements IAlbumRepository {
await this.repository.delete({ ownerId: userId });
}
@GenerateSql()
getAll(): Promise<AlbumEntity[]> {
return this.repository.find({
relations: {
@ -167,6 +177,7 @@ export class AlbumRepository implements IAlbumRepository {
});
}
// @GenerateSql({ params: [DummyValue.UUID] })
async removeAsset(assetId: string): Promise<void> {
// Using dataSource, because there is no direct access to albums_assets_assets.
await this.dataSource
@ -176,6 +187,7 @@ export class AlbumRepository implements IAlbumRepository {
.where('"albums_assets_assets"."assetsId" = :assetId', { assetId });
}
@GenerateSql({ params: [{ albumId: DummyValue.UUID, assetIds: [DummyValue.UUID] }] })
async removeAssets(asset: AlbumAssets): Promise<void> {
await this.dataSource
.createQueryBuilder()
@ -195,6 +207,7 @@ export class AlbumRepository implements IAlbumRepository {
* @param assetIds Optional list of asset IDs to filter on.
* @returns Set of Asset IDs for the given album ID.
*/
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] }, { name: 'no assets', params: [DummyValue.UUID] })
async getAssetIds(albumId: string, assetIds?: string[]): Promise<Set<string>> {
const query = this.dataSource
.createQueryBuilder()
@ -210,6 +223,7 @@ export class AlbumRepository implements IAlbumRepository {
return new Set(result.map((row) => row['assetId']));
}
@GenerateSql({ params: [{ albumId: DummyValue.UUID, assetId: DummyValue.UUID }] })
hasAsset(asset: AlbumAsset): Promise<boolean> {
return this.repository.exist({
where: {
@ -224,6 +238,7 @@ export class AlbumRepository implements IAlbumRepository {
});
}
@GenerateSql({ params: [{ albumId: DummyValue.UUID, assetIds: [DummyValue.UUID] }] })
async addAssets({ albumId, assetIds }: AlbumAssets): Promise<void> {
await this.dataSource
.createQueryBuilder()
@ -266,6 +281,7 @@ export class AlbumRepository implements IAlbumRepository {
*
* @returns Amount of updated album thumbnails or undefined when unknown
*/
@GenerateSql()
async updateThumbnails(): Promise<number | undefined> {
// Subquery for getting a new thumbnail.
const newThumbnail = this.assetRepository

View file

@ -3,9 +3,10 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { APIKeyEntity } from '../entities';
import { DummyValue, GenerateSql } from '../infra.util';
@Injectable()
export class APIKeyRepository implements IKeyRepository {
export class ApiKeyRepository implements IKeyRepository {
constructor(@InjectRepository(APIKeyEntity) private repository: Repository<APIKeyEntity>) {}
async create(dto: Partial<APIKeyEntity>): Promise<APIKeyEntity> {
@ -21,6 +22,7 @@ export class APIKeyRepository implements IKeyRepository {
await this.repository.delete({ userId, id });
}
@GenerateSql({ params: [DummyValue.STRING] })
getKey(hashedToken: string): Promise<APIKeyEntity | null> {
return this.repository.findOne({
select: {
@ -35,10 +37,12 @@ export class APIKeyRepository implements IKeyRepository {
});
}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
getById(userId: string, id: string): Promise<APIKeyEntity | null> {
return this.repository.findOne({ where: { userId, id } });
}
@GenerateSql({ params: [DummyValue.STRING] })
getByUserId(userId: string): Promise<APIKeyEntity[]> {
return this.repository.find({ where: { userId }, order: { createdAt: 'DESC' } });
}

View file

@ -22,6 +22,7 @@ import _ from 'lodash';
import { DateTime } from 'luxon';
import { And, FindOptionsRelations, FindOptionsWhere, In, IsNull, LessThan, Not, Repository } from 'typeorm';
import { AssetEntity, AssetJobStatusEntity, AssetType, ExifEntity } from '../entities';
import { DummyValue, GenerateSql } from '../infra.util';
import OptionalBetween from '../utils/optional-between.util';
import { paginate } from '../utils/pagination.util';
@ -185,6 +186,7 @@ export class AssetRepository implements IAssetRepository {
return this.repository.save(asset);
}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.DATE] })
getByDate(ownerId: string, date: Date): Promise<AssetEntity[]> {
// For reference of a correct approach although slower
@ -219,6 +221,7 @@ export class AssetRepository implements IAssetRepository {
});
}
@GenerateSql({ params: [DummyValue.UUID, { day: 1, month: 1 }] })
getByDayOfYear(ownerId: string, { day, month }: MonthDay): Promise<AssetEntity[]> {
return this.repository
.createQueryBuilder('entity')
@ -240,6 +243,7 @@ export class AssetRepository implements IAssetRepository {
.getMany();
}
@GenerateSql({ params: [[DummyValue.UUID]] })
getByIds(ids: string[], relations?: FindOptionsRelations<AssetEntity>): Promise<AssetEntity[]> {
if (!relations) {
relations = {
@ -259,6 +263,7 @@ export class AssetRepository implements IAssetRepository {
});
}
@GenerateSql({ params: [DummyValue.UUID] })
async deleteAll(ownerId: string): Promise<void> {
await this.repository.delete({ ownerId });
}
@ -291,12 +296,14 @@ export class AssetRepository implements IAssetRepository {
});
}
@GenerateSql({ params: [[DummyValue.UUID]] })
getByLibraryId(libraryIds: string[]): Promise<AssetEntity[]> {
return this.repository.find({
where: { library: { id: In(libraryIds) } },
});
}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] })
getByLibraryIdAndOriginalPath(libraryId: string, originalPath: string): Promise<AssetEntity | null> {
return this.repository.findOne({
where: { library: { id: libraryId }, originalPath: originalPath },
@ -333,6 +340,7 @@ export class AssetRepository implements IAssetRepository {
*
* @returns Promise<string[]> - Array of assetIds belong to the device
*/
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] })
async getAllByDeviceId(ownerId: string, deviceId: string): Promise<string[]> {
const items = await this.repository.find({
select: { deviceAssetId: true },
@ -347,6 +355,7 @@ export class AssetRepository implements IAssetRepository {
return items.map((asset) => asset.deviceAssetId);
}
@GenerateSql({ params: [DummyValue.UUID] })
getById(id: string): Promise<AssetEntity | null> {
return this.repository.findOne({
where: { id },
@ -362,6 +371,7 @@ export class AssetRepository implements IAssetRepository {
});
}
@GenerateSql({ params: [[DummyValue.UUID], { deviceId: DummyValue.STRING }] })
async updateAll(ids: string[], options: Partial<AssetEntity>): Promise<void> {
await this.repository.update({ id: In(ids) }, options);
}
@ -395,6 +405,7 @@ export class AssetRepository implements IAssetRepository {
await this.repository.remove(asset);
}
@GenerateSql({ params: [[DummyValue.UUID], DummyValue.BUFFER] })
getByChecksum(userId: string, checksum: Buffer): Promise<AssetEntity | null> {
return this.repository.findOne({ where: { ownerId: userId, checksum } });
}
@ -417,6 +428,14 @@ export class AssetRepository implements IAssetRepository {
});
}
@GenerateSql(
...Object.values(WithProperty)
.filter((property) => property !== WithProperty.IS_OFFLINE)
.map((property) => ({
name: property,
params: [DummyValue.PAGINATION, property],
})),
)
getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated<AssetEntity> {
let relations: FindOptionsRelations<AssetEntity> = {};
let where: FindOptionsWhere<AssetEntity> | FindOptionsWhere<AssetEntity>[] = {};

View file

@ -4,11 +4,13 @@ import { InjectRepository } from '@nestjs/typeorm';
import { IsNull, Not } from 'typeorm';
import { Repository } from 'typeorm/repository/Repository';
import { LibraryEntity, LibraryType } from '../entities';
import { DummyValue, GenerateSql } from '../infra.util';
@Injectable()
export class LibraryRepository implements ILibraryRepository {
constructor(@InjectRepository(LibraryEntity) private repository: Repository<LibraryEntity>) {}
@GenerateSql({ params: [DummyValue.UUID] })
get(id: string, withDeleted = false): Promise<LibraryEntity | null> {
return this.repository.findOneOrFail({
where: {
@ -19,6 +21,7 @@ export class LibraryRepository implements ILibraryRepository {
});
}
@GenerateSql({ params: [DummyValue.STRING] })
existsByName(name: string, withDeleted = false): Promise<boolean> {
return this.repository.exist({
where: {
@ -28,10 +31,12 @@ export class LibraryRepository implements ILibraryRepository {
});
}
@GenerateSql({ params: [DummyValue.UUID] })
getCountForUser(ownerId: string): Promise<number> {
return this.repository.countBy({ ownerId });
}
@GenerateSql({ params: [DummyValue.UUID] })
getDefaultUploadLibrary(ownerId: string): Promise<LibraryEntity | null> {
return this.repository.findOne({
where: {
@ -44,6 +49,7 @@ export class LibraryRepository implements ILibraryRepository {
});
}
@GenerateSql({ params: [DummyValue.UUID] })
getUploadLibraryCount(ownerId: string): Promise<number> {
return this.repository.count({
where: {
@ -53,6 +59,7 @@ export class LibraryRepository implements ILibraryRepository {
});
}
@GenerateSql({ params: [DummyValue.UUID] })
getAllByUserId(ownerId: string, type?: LibraryType): Promise<LibraryEntity[]> {
return this.repository.find({
where: {
@ -69,6 +76,7 @@ export class LibraryRepository implements ILibraryRepository {
});
}
@GenerateSql({ params: [] })
getAll(withDeleted = false, type?: LibraryType): Promise<LibraryEntity[]> {
return this.repository.find({
where: { type },
@ -82,6 +90,7 @@ export class LibraryRepository implements ILibraryRepository {
});
}
@GenerateSql()
getAllDeleted(): Promise<LibraryEntity[]> {
return this.repository.find({
where: {
@ -114,6 +123,7 @@ export class LibraryRepository implements ILibraryRepository {
return this.save(library);
}
@GenerateSql({ params: [DummyValue.UUID] })
async getStatistics(id: string): Promise<LibraryStatsResponseDto> {
const stats = await this.repository
.createQueryBuilder('libraries')
@ -134,6 +144,7 @@ export class LibraryRepository implements ILibraryRepository {
};
}
@GenerateSql({ params: [DummyValue.UUID] })
async getOnlineAssetPaths(libraryId: string): Promise<string[]> {
// Return all non-offline asset paths for a given library
const rawResults = await this.repository
@ -153,6 +164,7 @@ export class LibraryRepository implements ILibraryRepository {
return results;
}
@GenerateSql({ params: [DummyValue.UUID] })
async getAssetIds(libraryId: string, withDeleted = false): Promise<string[]> {
let query = await this.repository
.createQueryBuilder('library')

View file

@ -9,7 +9,7 @@ import { GeodataAdmin1Entity, GeodataAdmin2Entity, GeodataPlacesEntity, SystemMe
import { DatabaseLock } from '@app/infra/utils/database-locks';
import { Inject, Logger } from '@nestjs/common';
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { DefaultReadTaskOptions, exiftool } from 'exiftool-vendored';
import { DefaultReadTaskOptions, exiftool, Tags } from 'exiftool-vendored';
import { createReadStream, existsSync } from 'fs';
import { readFile } from 'fs/promises';
import * as geotz from 'geo-tz';
@ -181,7 +181,7 @@ export class MetadataRepository implements IMetadataRepository {
return { country, state, city };
}
getExifTags(path: string): Promise<ImmichTags | null> {
readTags(path: string): Promise<ImmichTags | null> {
return exiftool
.read(path, undefined, {
...DefaultReadTaskOptions,
@ -198,4 +198,12 @@ export class MetadataRepository implements IMetadataRepository {
return null;
}) as Promise<ImmichTags | null>;
}
async writeTags(path: string, tags: Partial<Tags>): Promise<void> {
try {
await exiftool.write(path, tags, ['-overwrite_original']);
} catch (error) {
this.logger.warn(`Error writing exif data (${path}): ${error}`);
}
}
}

View file

@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { MoveEntity, PathType } from '../entities';
import { DummyValue, GenerateSql } from '../infra.util';
@Injectable()
export class MoveRepository implements IMoveRepository {
@ -12,6 +13,7 @@ export class MoveRepository implements IMoveRepository {
return this.repository.save(entity);
}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] })
getByEntity(entityId: string, pathType: PathType): Promise<MoveEntity | null> {
return this.repository.findOne({ where: { entityId, pathType } });
}

View file

@ -9,6 +9,7 @@ import {
import { InjectRepository } from '@nestjs/typeorm';
import { In, Repository } from 'typeorm';
import { AssetEntity, AssetFaceEntity, PersonEntity } from '../entities';
import { DummyValue, GenerateSql } from '../infra.util';
export class PersonRepository implements IPersonRepository {
constructor(
@ -36,6 +37,7 @@ export class PersonRepository implements IPersonRepository {
return assetIds;
}
@GenerateSql({ params: [{ oldPersonId: DummyValue.UUID, newPersonId: DummyValue.UUID }] })
async reassignFaces({ oldPersonId, newPersonId }: UpdateFacesData): Promise<number> {
const result = await this.assetFaceRepository
.createQueryBuilder()
@ -57,18 +59,22 @@ export class PersonRepository implements IPersonRepository {
return people.length;
}
@GenerateSql()
getAllFaces(): Promise<AssetFaceEntity[]> {
return this.assetFaceRepository.find({ relations: { asset: true }, withDeleted: true });
}
@GenerateSql()
getAll(): Promise<PersonEntity[]> {
return this.personRepository.find();
}
@GenerateSql()
getAllWithoutThumbnail(): Promise<PersonEntity[]> {
return this.personRepository.findBy({ thumbnailPath: '' });
}
@GenerateSql({ params: [DummyValue.UUID] })
getAllForUser(userId: string, options?: PersonSearchOptions): Promise<PersonEntity[]> {
const queryBuilder = this.personRepository
.createQueryBuilder('person')
@ -89,6 +95,7 @@ export class PersonRepository implements IPersonRepository {
return queryBuilder.getMany();
}
@GenerateSql()
getAllWithoutFaces(): Promise<PersonEntity[]> {
return this.personRepository
.createQueryBuilder('person')
@ -99,10 +106,12 @@ export class PersonRepository implements IPersonRepository {
.getMany();
}
@GenerateSql({ params: [DummyValue.UUID] })
getById(personId: string): Promise<PersonEntity | null> {
return this.personRepository.findOne({ where: { id: personId } });
}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING, { withHidden: true }] })
getByName(userId: string, personName: string, { withHidden }: PersonNameSearchOptions): Promise<PersonEntity[]> {
const queryBuilder = this.personRepository
.createQueryBuilder('person')
@ -121,6 +130,7 @@ export class PersonRepository implements IPersonRepository {
return queryBuilder.getMany();
}
@GenerateSql({ params: [DummyValue.UUID] })
async getStatistics(personId: string): Promise<PersonStatistics> {
return {
assets: await this.assetFaceRepository
@ -135,6 +145,7 @@ export class PersonRepository implements IPersonRepository {
};
}
@GenerateSql({ params: [DummyValue.UUID] })
getAssets(personId: string): Promise<AssetEntity[]> {
return this.assetRepository.find({
where: {
@ -171,10 +182,12 @@ export class PersonRepository implements IPersonRepository {
return this.personRepository.findOneByOrFail({ id });
}
@GenerateSql({ params: [[{ assetId: DummyValue.UUID, personId: DummyValue.UUID }]] })
async getFacesByIds(ids: AssetFaceId[]): Promise<AssetFaceEntity[]> {
return this.assetFaceRepository.find({ where: ids, relations: { asset: true }, withDeleted: true });
}
@GenerateSql({ params: [DummyValue.UUID] })
async getRandomFace(personId: string): Promise<AssetFaceEntity | null> {
return this.assetFaceRepository.findOneBy({ personId });
}

View file

@ -3,11 +3,13 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { SharedLinkEntity } from '../entities';
import { DummyValue, GenerateSql } from '../infra.util';
@Injectable()
export class SharedLinkRepository implements ISharedLinkRepository {
constructor(@InjectRepository(SharedLinkEntity) private repository: Repository<SharedLinkEntity>) {}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
get(userId: string, id: string): Promise<SharedLinkEntity | null> {
return this.repository.findOne({
where: {
@ -39,6 +41,7 @@ export class SharedLinkRepository implements ISharedLinkRepository {
});
}
@GenerateSql({ params: [DummyValue.UUID] })
getAll(userId: string): Promise<SharedLinkEntity[]> {
return this.repository.find({
where: {
@ -56,6 +59,7 @@ export class SharedLinkRepository implements ISharedLinkRepository {
});
}
@GenerateSql({ params: [DummyValue.BUFFER] })
async getByKey(key: Buffer): Promise<SharedLinkEntity | null> {
return await this.repository.findOne({
where: {

View file

@ -4,6 +4,7 @@ import axios from 'axios';
import { readFile } from 'fs/promises';
import { In, Repository } from 'typeorm';
import { SystemConfigEntity } from '../entities';
import { DummyValue, GenerateSql } from '../infra.util';
export class SystemConfigRepository implements ISystemConfigRepository {
constructor(
@ -14,6 +15,7 @@ export class SystemConfigRepository implements ISystemConfigRepository {
return axios.get(url).then((response) => response.data);
}
@GenerateSql()
load(): Promise<SystemConfigEntity[]> {
return this.repository.find();
}
@ -26,6 +28,7 @@ export class SystemConfigRepository implements ISystemConfigRepository {
return this.repository.save(items);
}
@GenerateSql({ params: [DummyValue.STRING] })
async deleteKeys(keys: string[]): Promise<void> {
await this.repository.delete({ key: In(keys) });
}

View file

@ -3,11 +3,13 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UserTokenEntity } from '../entities';
import { DummyValue, GenerateSql } from '../infra.util';
@Injectable()
export class UserTokenRepository implements IUserTokenRepository {
constructor(@InjectRepository(UserTokenEntity) private repository: Repository<UserTokenEntity>) {}
@GenerateSql({ params: [DummyValue.STRING] })
getByToken(token: string): Promise<UserTokenEntity | null> {
return this.repository.findOne({ where: { token }, relations: { user: true } });
}
@ -35,6 +37,7 @@ export class UserTokenRepository implements IUserTokenRepository {
return this.repository.save(userToken);
}
@GenerateSql({ params: [DummyValue.UUID] })
async delete(id: string): Promise<void> {
await this.repository.delete({ id });
}

View file

@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { IsNull, Not, Repository } from 'typeorm';
import { UserEntity } from '../entities';
import { DummyValue, GenerateSql } from '../infra.util';
@Injectable()
export class UserRepository implements IUserRepository {
@ -16,14 +17,17 @@ export class UserRepository implements IUserRepository {
});
}
@GenerateSql()
async getAdmin(): Promise<UserEntity | null> {
return this.userRepository.findOne({ where: { isAdmin: true } });
}
@GenerateSql()
async hasAdmin(): Promise<boolean> {
return this.userRepository.exist({ where: { isAdmin: true } });
}
@GenerateSql({ params: [DummyValue.EMAIL] })
async getByEmail(email: string, withPassword?: boolean): Promise<UserEntity | null> {
let builder = this.userRepository.createQueryBuilder('user').where({ email });
@ -34,10 +38,12 @@ export class UserRepository implements IUserRepository {
return builder.getOne();
}
@GenerateSql({ params: [DummyValue.STRING] })
async getByStorageLabel(storageLabel: string): Promise<UserEntity | null> {
return this.userRepository.findOne({ where: { storageLabel } });
}
@GenerateSql({ params: [DummyValue.STRING] })
async getByOAuthId(oauthId: string): Promise<UserEntity | null> {
return this.userRepository.findOne({ where: { oauthId } });
}
@ -76,6 +82,7 @@ export class UserRepository implements IUserRepository {
return this.userRepository.recover(user);
}
@GenerateSql()
async getUserStats(): Promise<UserStatsQueryResponse[]> {
const stats = await this.userRepository
.createQueryBuilder('users')

View file

@ -0,0 +1,168 @@
import { INestApplication } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Test } from '@nestjs/testing';
import { TypeOrmModule } from '@nestjs/typeorm';
import { mkdir, rm, writeFile } from 'fs/promises';
import { join } from 'path';
import { databaseConfig } from '../database.config';
import { databaseEntities } from '../entities';
import { GENERATE_SQL_KEY, GenerateSqlQueries } from '../infra.util';
import {
AccessRepository,
AlbumRepository,
ApiKeyRepository,
AssetRepository,
AuditRepository,
LibraryRepository,
MoveRepository,
PartnerRepository,
PersonRepository,
SharedLinkRepository,
SystemConfigRepository,
SystemMetadataRepository,
TagRepository,
UserRepository,
UserTokenRepository,
} from '../repositories';
import { SqlLogger } from './sql.logger';
const reflector = new Reflector();
const repositories = [
AccessRepository,
AlbumRepository,
ApiKeyRepository,
AssetRepository,
AuditRepository,
LibraryRepository,
MoveRepository,
PartnerRepository,
PersonRepository,
SharedLinkRepository,
SystemConfigRepository,
SystemMetadataRepository,
TagRepository,
UserTokenRepository,
UserRepository,
];
type Repository = (typeof repositories)[0];
type SqlGeneratorOptions = { targetDir: string };
class SqlGenerator {
private app: INestApplication | null = null;
private sqlLogger = new SqlLogger();
private results: Record<string, string[]> = {};
constructor(private options: SqlGeneratorOptions) {}
async run() {
try {
await this.setup();
for (const Repository of repositories) {
await this.process(Repository);
}
await this.write();
this.stats();
} finally {
await this.close();
}
}
private async setup() {
await rm(this.options.targetDir, { force: true, recursive: true });
await mkdir(this.options.targetDir);
const moduleFixture = await Test.createTestingModule({
imports: [
TypeOrmModule.forRoot({
...databaseConfig,
entities: databaseEntities,
logging: ['query'],
logger: this.sqlLogger,
}),
TypeOrmModule.forFeature(databaseEntities),
],
providers: repositories,
}).compile();
this.app = await moduleFixture.createNestApplication().init();
}
async process(Repository: Repository) {
if (!this.app) {
throw new Error('Not initialized');
}
const data: string[] = [`-- NOTE: This file is auto generated by ./sql-generator`];
const instance = this.app.get<Repository>(Repository);
const properties = Object.getOwnPropertyNames(Repository.prototype) as Array<keyof typeof Repository>;
for (const key of properties) {
const target = instance[key];
if (!(target instanceof Function)) {
continue;
}
const queries = reflector.get<GenerateSqlQueries[] | undefined>(GENERATE_SQL_KEY, target);
if (!queries) {
continue;
}
// empty decorator implies calling with no arguments
if (queries.length === 0) {
queries.push({ params: [] });
}
for (const { name, params } of queries) {
let queryLabel = `${Repository.name}.${key}`;
if (name) {
queryLabel += ` (${name})`;
}
this.sqlLogger.clear();
// errors still generate sql, which is all we care about
await target.apply(instance, params).catch(() => null);
if (this.sqlLogger.queries.length === 0) {
console.warn(`No queries recorded for ${queryLabel}`);
continue;
}
data.push([`-- ${queryLabel}`, ...this.sqlLogger.queries].join('\n'));
}
}
this.results[Repository.name] = data;
}
private async write() {
for (const [repoName, data] of Object.entries(this.results)) {
const filename = repoName.replace(/[A-Z]/g, (letter) => `.${letter.toLowerCase()}`).replace('.', '');
const file = join(this.options.targetDir, `${filename}.sql`);
await writeFile(file, data.join('\n\n') + '\n');
}
}
private stats() {
console.log(`Wrote ${Object.keys(this.results).length} files`);
console.log(`Generated ${Object.values(this.results).flat().length} queries`);
}
private async close() {
if (this.app) {
await this.app.close();
}
}
}
new SqlGenerator({ targetDir: './src/infra/sql' })
.run()
.then(() => {
console.log('Done');
process.exit(0);
})
.catch((error) => {
console.error(error);
console.log('Something went wrong');
process.exit(1);
});

View file

@ -0,0 +1,27 @@
import { Logger } from 'typeorm';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { format } = require('sql-formatter');
export class SqlLogger implements Logger {
queries: string[] = [];
errors: Array<{ error: string | Error; query: string }> = [];
clear() {
this.queries = [];
this.errors = [];
}
logQuery(query: string) {
this.queries.push(format(query, { language: 'postgresql' }));
}
logQueryError(error: string | Error, query: string) {
this.errors.push({ error, query });
}
logQuerySlow() {}
logSchemaBuild() {}
logMigration() {}
log() {}
}

View file

@ -0,0 +1 @@
-- NOTE: This file is auto generated by ./sql-generator

View file

@ -0,0 +1,613 @@
-- NOTE: This file is auto generated by ./sql-generator
-- AlbumRepository.getById
SELECT DISTINCT
"distinctAlias"."AlbumEntity_id" AS "ids_AlbumEntity_id"
FROM
(
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__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
"AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
"AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor",
"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"."externalPath" AS "AlbumEntity__AlbumEntity_owner_externalPath",
"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"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
"AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
"AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id",
"AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name",
"AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor",
"AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin",
"AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email",
"AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel",
"AlbumEntity__AlbumEntity_sharedUsers"."externalPath" AS "AlbumEntity__AlbumEntity_sharedUsers_externalPath",
"AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId",
"AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath",
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
"AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt",
"AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
"AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
"AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled",
"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__AlbumEntity_sharedUsers" ON "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."albumsId" = "AlbumEntity"."id"
LEFT JOIN "users" "AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity__AlbumEntity_sharedUsers"."id" = "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."usersId"
AND (
"AlbumEntity__AlbumEntity_sharedUsers"."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
-- AlbumRepository.getByIds
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__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
"AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
"AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor",
"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"."externalPath" AS "AlbumEntity__AlbumEntity_owner_externalPath",
"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"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
"AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
"AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id",
"AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name",
"AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor",
"AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin",
"AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email",
"AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel",
"AlbumEntity__AlbumEntity_sharedUsers"."externalPath" AS "AlbumEntity__AlbumEntity_sharedUsers_externalPath",
"AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId",
"AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath",
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
"AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt",
"AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
"AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
"AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled"
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__AlbumEntity_sharedUsers" ON "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."albumsId" = "AlbumEntity"."id"
LEFT JOIN "users" "AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity__AlbumEntity_sharedUsers"."id" = "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."usersId"
AND (
"AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" IS NULL
)
WHERE
(("AlbumEntity"."id" IN ($1)))
AND ("AlbumEntity"."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__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
"AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
"AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor",
"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"."externalPath" AS "AlbumEntity__AlbumEntity_owner_externalPath",
"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"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
"AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
"AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id",
"AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name",
"AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor",
"AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin",
"AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email",
"AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel",
"AlbumEntity__AlbumEntity_sharedUsers"."externalPath" AS "AlbumEntity__AlbumEntity_sharedUsers_externalPath",
"AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId",
"AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath",
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
"AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt",
"AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
"AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
"AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled"
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__AlbumEntity_sharedUsers" ON "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."albumsId" = "AlbumEntity"."id"
LEFT JOIN "users" "AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity__AlbumEntity_sharedUsers"."id" = "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."usersId"
AND (
"AlbumEntity__AlbumEntity_sharedUsers"."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
(
(
(
"AlbumEntity"."ownerId" = $1
AND "AlbumEntity__AlbumEntity_assets"."id" = $2
)
OR (
"AlbumEntity__AlbumEntity_sharedUsers"."id" = $3
AND "AlbumEntity__AlbumEntity_assets"."id" = $4
)
)
)
AND ("AlbumEntity"."deletedAt" IS NULL)
ORDER BY
"AlbumEntity"."createdAt" DESC
-- AlbumRepository.getMetadataForIds
SELECT
"album"."id" AS "album_id",
MIN("assets"."fileCreatedAt") AS "start_date",
MAX("assets"."fileCreatedAt") AS "end_date",
COUNT("album_assets"."assetsId") 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"
-- AlbumRepository.getInvalidThumbnail
SELECT
"albums"."id" AS "albums_id"
FROM
"albums" "albums"
WHERE
(
"albums"."albumThumbnailAssetId" IS NULL
AND EXISTS (
SELECT
1
FROM
"albums_assets_assets" "albums_assets"
WHERE
"albums"."id" = "albums_assets"."albumsId"
)
OR "albums"."albumThumbnailAssetId" IS NOT NULL
AND NOT EXISTS (
SELECT
1
FROM
"albums_assets_assets" "albums_assets"
WHERE
"albums"."id" = "albums_assets"."albumsId"
AND "albums"."albumThumbnailAssetId" = "albums_assets"."assetsId"
)
)
AND ("albums"."deletedAt" IS NULL)
-- 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__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id",
"AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name",
"AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor",
"AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin",
"AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email",
"AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel",
"AlbumEntity__AlbumEntity_sharedUsers"."externalPath" AS "AlbumEntity__AlbumEntity_sharedUsers_externalPath",
"AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId",
"AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath",
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
"AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt",
"AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
"AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
"AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled",
"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"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor",
"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"."externalPath" AS "AlbumEntity__AlbumEntity_owner_externalPath",
"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"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
"AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled"
FROM
"albums" "AlbumEntity"
LEFT JOIN "albums_shared_users_users" "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."albumsId" = "AlbumEntity"."id"
LEFT JOIN "users" "AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity__AlbumEntity_sharedUsers"."id" = "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."usersId"
AND (
"AlbumEntity__AlbumEntity_sharedUsers"."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
-- 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__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id",
"AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name",
"AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor",
"AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin",
"AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email",
"AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel",
"AlbumEntity__AlbumEntity_sharedUsers"."externalPath" AS "AlbumEntity__AlbumEntity_sharedUsers_externalPath",
"AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId",
"AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath",
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
"AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt",
"AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
"AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
"AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled",
"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"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor",
"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"."externalPath" AS "AlbumEntity__AlbumEntity_owner_externalPath",
"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"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
"AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled"
FROM
"albums" "AlbumEntity"
LEFT JOIN "albums_shared_users_users" "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."albumsId" = "AlbumEntity"."id"
LEFT JOIN "users" "AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity__AlbumEntity_sharedUsers"."id" = "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."usersId"
AND (
"AlbumEntity__AlbumEntity_sharedUsers"."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__AlbumEntity_sharedUsers"."id" = $1)
OR (
"AlbumEntity__AlbumEntity_sharedLinks"."userId" = $2
)
OR (
"AlbumEntity"."ownerId" = $3
AND NOT (
"AlbumEntity__AlbumEntity_sharedUsers"."id" IS NULL
)
)
)
)
AND ("AlbumEntity"."deletedAt" IS NULL)
ORDER BY
"AlbumEntity"."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__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id",
"AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name",
"AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor",
"AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin",
"AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email",
"AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel",
"AlbumEntity__AlbumEntity_sharedUsers"."externalPath" AS "AlbumEntity__AlbumEntity_sharedUsers_externalPath",
"AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId",
"AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath",
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
"AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt",
"AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
"AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
"AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled",
"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"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor",
"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"."externalPath" AS "AlbumEntity__AlbumEntity_owner_externalPath",
"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"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
"AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled"
FROM
"albums" "AlbumEntity"
LEFT JOIN "albums_shared_users_users" "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."albumsId" = "AlbumEntity"."id"
LEFT JOIN "users" "AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity__AlbumEntity_sharedUsers"."id" = "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."usersId"
AND (
"AlbumEntity__AlbumEntity_sharedUsers"."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__AlbumEntity_sharedUsers"."id" IS NULL
AND "AlbumEntity__AlbumEntity_sharedLinks"."id" IS NULL
)
)
AND ("AlbumEntity"."deletedAt" IS NULL)
ORDER BY
"AlbumEntity"."createdAt" DESC
-- AlbumRepository.getAll
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__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
"AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
"AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor",
"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"."externalPath" AS "AlbumEntity__AlbumEntity_owner_externalPath",
"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"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
"AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled"
FROM
"albums" "AlbumEntity"
LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId"
AND (
"AlbumEntity__AlbumEntity_owner"."deletedAt" IS NULL
)
WHERE
"AlbumEntity"."deletedAt" IS NULL
-- AlbumRepository.removeAssets
DELETE FROM "albums_assets_assets"
WHERE
(
"albumsId" = $1
AND "assetsId" IN ($2)
)
-- 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)
-- AlbumRepository.getAssetIds (no assets)
SELECT
"albums_assets"."assetsId" AS "assetId"
FROM
"albums_assets_assets" "albums_assets"
WHERE
"albums_assets"."albumsId" = $1
-- AlbumRepository.hasAsset
SELECT
1 AS "row_exists"
FROM
(
SELECT
1 AS dummy_column
) "dummy_table"
WHERE
EXISTS (
SELECT
1
FROM
"albums" "AlbumEntity"
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
(
(
"AlbumEntity"."id" = $1
AND "AlbumEntity__AlbumEntity_assets"."id" = $2
)
)
AND ("AlbumEntity"."deletedAt" IS NULL)
)
LIMIT
1
-- AlbumRepository.addAssets
INSERT INTO
"albums_assets_assets" ("albumsId", "assetsId")
VALUES
($1, $2)
-- AlbumRepository.updateThumbnails
UPDATE "albums"
SET
"albumThumbnailAssetId" = (
SELECT
"albums_assets2"."assetsId"
FROM
"assets" "assets",
"albums_assets_assets" "albums_assets2"
WHERE
(
"albums_assets2"."assetsId" = "assets"."id"
AND "albums_assets2"."albumsId" = "albums"."id"
)
AND ("assets"."deletedAt" IS NULL)
ORDER BY
"assets"."fileCreatedAt" DESC
LIMIT
1
),
"updatedAt" = CURRENT_TIMESTAMP
WHERE
"albums"."albumThumbnailAssetId" IS NULL
AND EXISTS (
SELECT
1
FROM
"albums_assets_assets" "albums_assets"
WHERE
"albums"."id" = "albums_assets"."albumsId"
)
OR "albums"."albumThumbnailAssetId" IS NOT NULL
AND NOT EXISTS (
SELECT
1
FROM
"albums_assets_assets" "albums_assets"
WHERE
"albums"."id" = "albums_assets"."albumsId"
AND "albums"."albumThumbnailAssetId" = "albums_assets"."assetsId"
)

View file

@ -0,0 +1,69 @@
-- NOTE: This file is auto generated by ./sql-generator
-- ApiKeyRepository.getKey
SELECT DISTINCT
"distinctAlias"."APIKeyEntity_id" AS "ids_APIKeyEntity_id"
FROM
(
SELECT
"APIKeyEntity"."id" AS "APIKeyEntity_id",
"APIKeyEntity"."key" AS "APIKeyEntity_key",
"APIKeyEntity"."userId" AS "APIKeyEntity_userId",
"APIKeyEntity__APIKeyEntity_user"."id" AS "APIKeyEntity__APIKeyEntity_user_id",
"APIKeyEntity__APIKeyEntity_user"."name" AS "APIKeyEntity__APIKeyEntity_user_name",
"APIKeyEntity__APIKeyEntity_user"."avatarColor" AS "APIKeyEntity__APIKeyEntity_user_avatarColor",
"APIKeyEntity__APIKeyEntity_user"."isAdmin" AS "APIKeyEntity__APIKeyEntity_user_isAdmin",
"APIKeyEntity__APIKeyEntity_user"."email" AS "APIKeyEntity__APIKeyEntity_user_email",
"APIKeyEntity__APIKeyEntity_user"."storageLabel" AS "APIKeyEntity__APIKeyEntity_user_storageLabel",
"APIKeyEntity__APIKeyEntity_user"."externalPath" AS "APIKeyEntity__APIKeyEntity_user_externalPath",
"APIKeyEntity__APIKeyEntity_user"."oauthId" AS "APIKeyEntity__APIKeyEntity_user_oauthId",
"APIKeyEntity__APIKeyEntity_user"."profileImagePath" AS "APIKeyEntity__APIKeyEntity_user_profileImagePath",
"APIKeyEntity__APIKeyEntity_user"."shouldChangePassword" AS "APIKeyEntity__APIKeyEntity_user_shouldChangePassword",
"APIKeyEntity__APIKeyEntity_user"."createdAt" AS "APIKeyEntity__APIKeyEntity_user_createdAt",
"APIKeyEntity__APIKeyEntity_user"."deletedAt" AS "APIKeyEntity__APIKeyEntity_user_deletedAt",
"APIKeyEntity__APIKeyEntity_user"."updatedAt" AS "APIKeyEntity__APIKeyEntity_user_updatedAt",
"APIKeyEntity__APIKeyEntity_user"."memoriesEnabled" AS "APIKeyEntity__APIKeyEntity_user_memoriesEnabled"
FROM
"api_keys" "APIKeyEntity"
LEFT JOIN "users" "APIKeyEntity__APIKeyEntity_user" ON "APIKeyEntity__APIKeyEntity_user"."id" = "APIKeyEntity"."userId"
AND (
"APIKeyEntity__APIKeyEntity_user"."deletedAt" IS NULL
)
WHERE
("APIKeyEntity"."key" = $1)
) "distinctAlias"
ORDER BY
"APIKeyEntity_id" ASC
LIMIT
1
-- ApiKeyRepository.getById
SELECT
"APIKeyEntity"."id" AS "APIKeyEntity_id",
"APIKeyEntity"."name" AS "APIKeyEntity_name",
"APIKeyEntity"."userId" AS "APIKeyEntity_userId",
"APIKeyEntity"."createdAt" AS "APIKeyEntity_createdAt",
"APIKeyEntity"."updatedAt" AS "APIKeyEntity_updatedAt"
FROM
"api_keys" "APIKeyEntity"
WHERE
(
"APIKeyEntity"."userId" = $1
AND "APIKeyEntity"."id" = $2
)
LIMIT
1
-- ApiKeyRepository.getByUserId
SELECT
"APIKeyEntity"."id" AS "APIKeyEntity_id",
"APIKeyEntity"."name" AS "APIKeyEntity_name",
"APIKeyEntity"."userId" AS "APIKeyEntity_userId",
"APIKeyEntity"."createdAt" AS "APIKeyEntity_createdAt",
"APIKeyEntity"."updatedAt" AS "APIKeyEntity_updatedAt"
FROM
"api_keys" "APIKeyEntity"
WHERE
("APIKeyEntity"."userId" = $1)
ORDER BY
"APIKeyEntity"."createdAt" DESC

View file

@ -0,0 +1,614 @@
-- NOTE: This file is auto generated by ./sql-generator
-- AssetRepository.getByDate
SELECT
"AssetEntity"."id" AS "AssetEntity_id",
"AssetEntity"."deviceAssetId" AS "AssetEntity_deviceAssetId",
"AssetEntity"."ownerId" AS "AssetEntity_ownerId",
"AssetEntity"."libraryId" AS "AssetEntity_libraryId",
"AssetEntity"."deviceId" AS "AssetEntity_deviceId",
"AssetEntity"."type" AS "AssetEntity_type",
"AssetEntity"."originalPath" AS "AssetEntity_originalPath",
"AssetEntity"."resizePath" AS "AssetEntity_resizePath",
"AssetEntity"."webpPath" AS "AssetEntity_webpPath",
"AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
"AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
"AssetEntity"."createdAt" AS "AssetEntity_createdAt",
"AssetEntity"."updatedAt" AS "AssetEntity_updatedAt",
"AssetEntity"."deletedAt" AS "AssetEntity_deletedAt",
"AssetEntity"."fileCreatedAt" AS "AssetEntity_fileCreatedAt",
"AssetEntity"."localDateTime" AS "AssetEntity_localDateTime",
"AssetEntity"."fileModifiedAt" AS "AssetEntity_fileModifiedAt",
"AssetEntity"."isFavorite" AS "AssetEntity_isFavorite",
"AssetEntity"."isArchived" AS "AssetEntity_isArchived",
"AssetEntity"."isExternal" AS "AssetEntity_isExternal",
"AssetEntity"."isReadOnly" AS "AssetEntity_isReadOnly",
"AssetEntity"."isOffline" AS "AssetEntity_isOffline",
"AssetEntity"."checksum" AS "AssetEntity_checksum",
"AssetEntity"."duration" AS "AssetEntity_duration",
"AssetEntity"."isVisible" AS "AssetEntity_isVisible",
"AssetEntity"."livePhotoVideoId" AS "AssetEntity_livePhotoVideoId",
"AssetEntity"."originalFileName" AS "AssetEntity_originalFileName",
"AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath",
"AssetEntity"."stackParentId" AS "AssetEntity_stackParentId",
"AssetEntity__AssetEntity_exifInfo"."assetId" AS "AssetEntity__AssetEntity_exifInfo_assetId",
"AssetEntity__AssetEntity_exifInfo"."description" AS "AssetEntity__AssetEntity_exifInfo_description",
"AssetEntity__AssetEntity_exifInfo"."exifImageWidth" AS "AssetEntity__AssetEntity_exifInfo_exifImageWidth",
"AssetEntity__AssetEntity_exifInfo"."exifImageHeight" AS "AssetEntity__AssetEntity_exifInfo_exifImageHeight",
"AssetEntity__AssetEntity_exifInfo"."fileSizeInByte" AS "AssetEntity__AssetEntity_exifInfo_fileSizeInByte",
"AssetEntity__AssetEntity_exifInfo"."orientation" AS "AssetEntity__AssetEntity_exifInfo_orientation",
"AssetEntity__AssetEntity_exifInfo"."dateTimeOriginal" AS "AssetEntity__AssetEntity_exifInfo_dateTimeOriginal",
"AssetEntity__AssetEntity_exifInfo"."modifyDate" AS "AssetEntity__AssetEntity_exifInfo_modifyDate",
"AssetEntity__AssetEntity_exifInfo"."timeZone" AS "AssetEntity__AssetEntity_exifInfo_timeZone",
"AssetEntity__AssetEntity_exifInfo"."latitude" AS "AssetEntity__AssetEntity_exifInfo_latitude",
"AssetEntity__AssetEntity_exifInfo"."longitude" AS "AssetEntity__AssetEntity_exifInfo_longitude",
"AssetEntity__AssetEntity_exifInfo"."projectionType" AS "AssetEntity__AssetEntity_exifInfo_projectionType",
"AssetEntity__AssetEntity_exifInfo"."city" AS "AssetEntity__AssetEntity_exifInfo_city",
"AssetEntity__AssetEntity_exifInfo"."livePhotoCID" AS "AssetEntity__AssetEntity_exifInfo_livePhotoCID",
"AssetEntity__AssetEntity_exifInfo"."state" AS "AssetEntity__AssetEntity_exifInfo_state",
"AssetEntity__AssetEntity_exifInfo"."country" AS "AssetEntity__AssetEntity_exifInfo_country",
"AssetEntity__AssetEntity_exifInfo"."make" AS "AssetEntity__AssetEntity_exifInfo_make",
"AssetEntity__AssetEntity_exifInfo"."model" AS "AssetEntity__AssetEntity_exifInfo_model",
"AssetEntity__AssetEntity_exifInfo"."lensModel" AS "AssetEntity__AssetEntity_exifInfo_lensModel",
"AssetEntity__AssetEntity_exifInfo"."fNumber" AS "AssetEntity__AssetEntity_exifInfo_fNumber",
"AssetEntity__AssetEntity_exifInfo"."focalLength" AS "AssetEntity__AssetEntity_exifInfo_focalLength",
"AssetEntity__AssetEntity_exifInfo"."iso" AS "AssetEntity__AssetEntity_exifInfo_iso",
"AssetEntity__AssetEntity_exifInfo"."exposureTime" AS "AssetEntity__AssetEntity_exifInfo_exposureTime",
"AssetEntity__AssetEntity_exifInfo"."profileDescription" AS "AssetEntity__AssetEntity_exifInfo_profileDescription",
"AssetEntity__AssetEntity_exifInfo"."colorspace" AS "AssetEntity__AssetEntity_exifInfo_colorspace",
"AssetEntity__AssetEntity_exifInfo"."bitsPerSample" AS "AssetEntity__AssetEntity_exifInfo_bitsPerSample",
"AssetEntity__AssetEntity_exifInfo"."fps" AS "AssetEntity__AssetEntity_exifInfo_fps",
"AssetEntity__AssetEntity_exifInfo"."exifTextSearchableColumn" AS "AssetEntity__AssetEntity_exifInfo_exifTextSearchableColumn"
FROM
"assets" "AssetEntity"
LEFT JOIN "exif" "AssetEntity__AssetEntity_exifInfo" ON "AssetEntity__AssetEntity_exifInfo"."assetId" = "AssetEntity"."id"
WHERE
(
(
"AssetEntity"."ownerId" = $1
AND "AssetEntity"."isVisible" = $2
AND "AssetEntity"."isArchived" = $3
AND NOT ("AssetEntity"."resizePath" IS NULL)
AND "AssetEntity"."fileCreatedAt" BETWEEN $4 AND $5
)
)
AND ("AssetEntity"."deletedAt" IS NULL)
ORDER BY
"AssetEntity"."fileCreatedAt" DESC
-- AssetRepository.getByDayOfYear
SELECT
"entity"."id" AS "entity_id",
"entity"."deviceAssetId" AS "entity_deviceAssetId",
"entity"."ownerId" AS "entity_ownerId",
"entity"."libraryId" AS "entity_libraryId",
"entity"."deviceId" AS "entity_deviceId",
"entity"."type" AS "entity_type",
"entity"."originalPath" AS "entity_originalPath",
"entity"."resizePath" AS "entity_resizePath",
"entity"."webpPath" AS "entity_webpPath",
"entity"."thumbhash" AS "entity_thumbhash",
"entity"."encodedVideoPath" AS "entity_encodedVideoPath",
"entity"."createdAt" AS "entity_createdAt",
"entity"."updatedAt" AS "entity_updatedAt",
"entity"."deletedAt" AS "entity_deletedAt",
"entity"."fileCreatedAt" AS "entity_fileCreatedAt",
"entity"."localDateTime" AS "entity_localDateTime",
"entity"."fileModifiedAt" AS "entity_fileModifiedAt",
"entity"."isFavorite" AS "entity_isFavorite",
"entity"."isArchived" AS "entity_isArchived",
"entity"."isExternal" AS "entity_isExternal",
"entity"."isReadOnly" AS "entity_isReadOnly",
"entity"."isOffline" AS "entity_isOffline",
"entity"."checksum" AS "entity_checksum",
"entity"."duration" AS "entity_duration",
"entity"."isVisible" AS "entity_isVisible",
"entity"."livePhotoVideoId" AS "entity_livePhotoVideoId",
"entity"."originalFileName" AS "entity_originalFileName",
"entity"."sidecarPath" AS "entity_sidecarPath",
"entity"."stackParentId" AS "entity_stackParentId",
"exifInfo"."assetId" AS "exifInfo_assetId",
"exifInfo"."description" AS "exifInfo_description",
"exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth",
"exifInfo"."exifImageHeight" AS "exifInfo_exifImageHeight",
"exifInfo"."fileSizeInByte" AS "exifInfo_fileSizeInByte",
"exifInfo"."orientation" AS "exifInfo_orientation",
"exifInfo"."dateTimeOriginal" AS "exifInfo_dateTimeOriginal",
"exifInfo"."modifyDate" AS "exifInfo_modifyDate",
"exifInfo"."timeZone" AS "exifInfo_timeZone",
"exifInfo"."latitude" AS "exifInfo_latitude",
"exifInfo"."longitude" AS "exifInfo_longitude",
"exifInfo"."projectionType" AS "exifInfo_projectionType",
"exifInfo"."city" AS "exifInfo_city",
"exifInfo"."livePhotoCID" AS "exifInfo_livePhotoCID",
"exifInfo"."state" AS "exifInfo_state",
"exifInfo"."country" AS "exifInfo_country",
"exifInfo"."make" AS "exifInfo_make",
"exifInfo"."model" AS "exifInfo_model",
"exifInfo"."lensModel" AS "exifInfo_lensModel",
"exifInfo"."fNumber" AS "exifInfo_fNumber",
"exifInfo"."focalLength" AS "exifInfo_focalLength",
"exifInfo"."iso" AS "exifInfo_iso",
"exifInfo"."exposureTime" AS "exifInfo_exposureTime",
"exifInfo"."profileDescription" AS "exifInfo_profileDescription",
"exifInfo"."colorspace" AS "exifInfo_colorspace",
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
"exifInfo"."fps" AS "exifInfo_fps",
"exifInfo"."exifTextSearchableColumn" AS "exifInfo_exifTextSearchableColumn"
FROM
"assets" "entity"
LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "entity"."id"
WHERE
(
"entity"."ownerId" = $1
AND "entity"."isVisible" = true
AND "entity"."isArchived" = false
AND "entity"."resizePath" IS NOT NULL
AND EXTRACT(
DAY
FROM
"entity"."localDateTime" AT TIME ZONE 'UTC'
) = $2
AND EXTRACT(
MONTH
FROM
"entity"."localDateTime" AT TIME ZONE 'UTC'
) = $3
)
AND ("entity"."deletedAt" IS NULL)
ORDER BY
"entity"."localDateTime" DESC
-- AssetRepository.getByIds
SELECT
"AssetEntity"."id" AS "AssetEntity_id",
"AssetEntity"."deviceAssetId" AS "AssetEntity_deviceAssetId",
"AssetEntity"."ownerId" AS "AssetEntity_ownerId",
"AssetEntity"."libraryId" AS "AssetEntity_libraryId",
"AssetEntity"."deviceId" AS "AssetEntity_deviceId",
"AssetEntity"."type" AS "AssetEntity_type",
"AssetEntity"."originalPath" AS "AssetEntity_originalPath",
"AssetEntity"."resizePath" AS "AssetEntity_resizePath",
"AssetEntity"."webpPath" AS "AssetEntity_webpPath",
"AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
"AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
"AssetEntity"."createdAt" AS "AssetEntity_createdAt",
"AssetEntity"."updatedAt" AS "AssetEntity_updatedAt",
"AssetEntity"."deletedAt" AS "AssetEntity_deletedAt",
"AssetEntity"."fileCreatedAt" AS "AssetEntity_fileCreatedAt",
"AssetEntity"."localDateTime" AS "AssetEntity_localDateTime",
"AssetEntity"."fileModifiedAt" AS "AssetEntity_fileModifiedAt",
"AssetEntity"."isFavorite" AS "AssetEntity_isFavorite",
"AssetEntity"."isArchived" AS "AssetEntity_isArchived",
"AssetEntity"."isExternal" AS "AssetEntity_isExternal",
"AssetEntity"."isReadOnly" AS "AssetEntity_isReadOnly",
"AssetEntity"."isOffline" AS "AssetEntity_isOffline",
"AssetEntity"."checksum" AS "AssetEntity_checksum",
"AssetEntity"."duration" AS "AssetEntity_duration",
"AssetEntity"."isVisible" AS "AssetEntity_isVisible",
"AssetEntity"."livePhotoVideoId" AS "AssetEntity_livePhotoVideoId",
"AssetEntity"."originalFileName" AS "AssetEntity_originalFileName",
"AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath",
"AssetEntity"."stackParentId" AS "AssetEntity_stackParentId",
"AssetEntity__AssetEntity_exifInfo"."assetId" AS "AssetEntity__AssetEntity_exifInfo_assetId",
"AssetEntity__AssetEntity_exifInfo"."description" AS "AssetEntity__AssetEntity_exifInfo_description",
"AssetEntity__AssetEntity_exifInfo"."exifImageWidth" AS "AssetEntity__AssetEntity_exifInfo_exifImageWidth",
"AssetEntity__AssetEntity_exifInfo"."exifImageHeight" AS "AssetEntity__AssetEntity_exifInfo_exifImageHeight",
"AssetEntity__AssetEntity_exifInfo"."fileSizeInByte" AS "AssetEntity__AssetEntity_exifInfo_fileSizeInByte",
"AssetEntity__AssetEntity_exifInfo"."orientation" AS "AssetEntity__AssetEntity_exifInfo_orientation",
"AssetEntity__AssetEntity_exifInfo"."dateTimeOriginal" AS "AssetEntity__AssetEntity_exifInfo_dateTimeOriginal",
"AssetEntity__AssetEntity_exifInfo"."modifyDate" AS "AssetEntity__AssetEntity_exifInfo_modifyDate",
"AssetEntity__AssetEntity_exifInfo"."timeZone" AS "AssetEntity__AssetEntity_exifInfo_timeZone",
"AssetEntity__AssetEntity_exifInfo"."latitude" AS "AssetEntity__AssetEntity_exifInfo_latitude",
"AssetEntity__AssetEntity_exifInfo"."longitude" AS "AssetEntity__AssetEntity_exifInfo_longitude",
"AssetEntity__AssetEntity_exifInfo"."projectionType" AS "AssetEntity__AssetEntity_exifInfo_projectionType",
"AssetEntity__AssetEntity_exifInfo"."city" AS "AssetEntity__AssetEntity_exifInfo_city",
"AssetEntity__AssetEntity_exifInfo"."livePhotoCID" AS "AssetEntity__AssetEntity_exifInfo_livePhotoCID",
"AssetEntity__AssetEntity_exifInfo"."state" AS "AssetEntity__AssetEntity_exifInfo_state",
"AssetEntity__AssetEntity_exifInfo"."country" AS "AssetEntity__AssetEntity_exifInfo_country",
"AssetEntity__AssetEntity_exifInfo"."make" AS "AssetEntity__AssetEntity_exifInfo_make",
"AssetEntity__AssetEntity_exifInfo"."model" AS "AssetEntity__AssetEntity_exifInfo_model",
"AssetEntity__AssetEntity_exifInfo"."lensModel" AS "AssetEntity__AssetEntity_exifInfo_lensModel",
"AssetEntity__AssetEntity_exifInfo"."fNumber" AS "AssetEntity__AssetEntity_exifInfo_fNumber",
"AssetEntity__AssetEntity_exifInfo"."focalLength" AS "AssetEntity__AssetEntity_exifInfo_focalLength",
"AssetEntity__AssetEntity_exifInfo"."iso" AS "AssetEntity__AssetEntity_exifInfo_iso",
"AssetEntity__AssetEntity_exifInfo"."exposureTime" AS "AssetEntity__AssetEntity_exifInfo_exposureTime",
"AssetEntity__AssetEntity_exifInfo"."profileDescription" AS "AssetEntity__AssetEntity_exifInfo_profileDescription",
"AssetEntity__AssetEntity_exifInfo"."colorspace" AS "AssetEntity__AssetEntity_exifInfo_colorspace",
"AssetEntity__AssetEntity_exifInfo"."bitsPerSample" AS "AssetEntity__AssetEntity_exifInfo_bitsPerSample",
"AssetEntity__AssetEntity_exifInfo"."fps" AS "AssetEntity__AssetEntity_exifInfo_fps",
"AssetEntity__AssetEntity_exifInfo"."exifTextSearchableColumn" AS "AssetEntity__AssetEntity_exifInfo_exifTextSearchableColumn",
"AssetEntity__AssetEntity_smartInfo"."assetId" AS "AssetEntity__AssetEntity_smartInfo_assetId",
"AssetEntity__AssetEntity_smartInfo"."tags" AS "AssetEntity__AssetEntity_smartInfo_tags",
"AssetEntity__AssetEntity_smartInfo"."objects" AS "AssetEntity__AssetEntity_smartInfo_objects",
"AssetEntity__AssetEntity_smartInfo"."clipEmbedding" AS "AssetEntity__AssetEntity_smartInfo_clipEmbedding",
"AssetEntity__AssetEntity_tags"."id" AS "AssetEntity__AssetEntity_tags_id",
"AssetEntity__AssetEntity_tags"."type" AS "AssetEntity__AssetEntity_tags_type",
"AssetEntity__AssetEntity_tags"."name" AS "AssetEntity__AssetEntity_tags_name",
"AssetEntity__AssetEntity_tags"."userId" AS "AssetEntity__AssetEntity_tags_userId",
"AssetEntity__AssetEntity_tags"."renameTagId" AS "AssetEntity__AssetEntity_tags_renameTagId",
"AssetEntity__AssetEntity_faces"."id" AS "AssetEntity__AssetEntity_faces_id",
"AssetEntity__AssetEntity_faces"."assetId" AS "AssetEntity__AssetEntity_faces_assetId",
"AssetEntity__AssetEntity_faces"."personId" AS "AssetEntity__AssetEntity_faces_personId",
"AssetEntity__AssetEntity_faces"."embedding" AS "AssetEntity__AssetEntity_faces_embedding",
"AssetEntity__AssetEntity_faces"."imageWidth" AS "AssetEntity__AssetEntity_faces_imageWidth",
"AssetEntity__AssetEntity_faces"."imageHeight" AS "AssetEntity__AssetEntity_faces_imageHeight",
"AssetEntity__AssetEntity_faces"."boundingBoxX1" AS "AssetEntity__AssetEntity_faces_boundingBoxX1",
"AssetEntity__AssetEntity_faces"."boundingBoxY1" AS "AssetEntity__AssetEntity_faces_boundingBoxY1",
"AssetEntity__AssetEntity_faces"."boundingBoxX2" AS "AssetEntity__AssetEntity_faces_boundingBoxX2",
"AssetEntity__AssetEntity_faces"."boundingBoxY2" AS "AssetEntity__AssetEntity_faces_boundingBoxY2",
"8258e303a73a72cf6abb13d73fb592dde0d68280"."id" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_id",
"8258e303a73a72cf6abb13d73fb592dde0d68280"."createdAt" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_createdAt",
"8258e303a73a72cf6abb13d73fb592dde0d68280"."updatedAt" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_updatedAt",
"8258e303a73a72cf6abb13d73fb592dde0d68280"."ownerId" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_ownerId",
"8258e303a73a72cf6abb13d73fb592dde0d68280"."name" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_name",
"8258e303a73a72cf6abb13d73fb592dde0d68280"."birthDate" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_birthDate",
"8258e303a73a72cf6abb13d73fb592dde0d68280"."thumbnailPath" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_thumbnailPath",
"8258e303a73a72cf6abb13d73fb592dde0d68280"."faceAssetId" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_faceAssetId",
"8258e303a73a72cf6abb13d73fb592dde0d68280"."isHidden" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_isHidden",
"AssetEntity__AssetEntity_stack"."id" AS "AssetEntity__AssetEntity_stack_id",
"AssetEntity__AssetEntity_stack"."deviceAssetId" AS "AssetEntity__AssetEntity_stack_deviceAssetId",
"AssetEntity__AssetEntity_stack"."ownerId" AS "AssetEntity__AssetEntity_stack_ownerId",
"AssetEntity__AssetEntity_stack"."libraryId" AS "AssetEntity__AssetEntity_stack_libraryId",
"AssetEntity__AssetEntity_stack"."deviceId" AS "AssetEntity__AssetEntity_stack_deviceId",
"AssetEntity__AssetEntity_stack"."type" AS "AssetEntity__AssetEntity_stack_type",
"AssetEntity__AssetEntity_stack"."originalPath" AS "AssetEntity__AssetEntity_stack_originalPath",
"AssetEntity__AssetEntity_stack"."resizePath" AS "AssetEntity__AssetEntity_stack_resizePath",
"AssetEntity__AssetEntity_stack"."webpPath" AS "AssetEntity__AssetEntity_stack_webpPath",
"AssetEntity__AssetEntity_stack"."thumbhash" AS "AssetEntity__AssetEntity_stack_thumbhash",
"AssetEntity__AssetEntity_stack"."encodedVideoPath" AS "AssetEntity__AssetEntity_stack_encodedVideoPath",
"AssetEntity__AssetEntity_stack"."createdAt" AS "AssetEntity__AssetEntity_stack_createdAt",
"AssetEntity__AssetEntity_stack"."updatedAt" AS "AssetEntity__AssetEntity_stack_updatedAt",
"AssetEntity__AssetEntity_stack"."deletedAt" AS "AssetEntity__AssetEntity_stack_deletedAt",
"AssetEntity__AssetEntity_stack"."fileCreatedAt" AS "AssetEntity__AssetEntity_stack_fileCreatedAt",
"AssetEntity__AssetEntity_stack"."localDateTime" AS "AssetEntity__AssetEntity_stack_localDateTime",
"AssetEntity__AssetEntity_stack"."fileModifiedAt" AS "AssetEntity__AssetEntity_stack_fileModifiedAt",
"AssetEntity__AssetEntity_stack"."isFavorite" AS "AssetEntity__AssetEntity_stack_isFavorite",
"AssetEntity__AssetEntity_stack"."isArchived" AS "AssetEntity__AssetEntity_stack_isArchived",
"AssetEntity__AssetEntity_stack"."isExternal" AS "AssetEntity__AssetEntity_stack_isExternal",
"AssetEntity__AssetEntity_stack"."isReadOnly" AS "AssetEntity__AssetEntity_stack_isReadOnly",
"AssetEntity__AssetEntity_stack"."isOffline" AS "AssetEntity__AssetEntity_stack_isOffline",
"AssetEntity__AssetEntity_stack"."checksum" AS "AssetEntity__AssetEntity_stack_checksum",
"AssetEntity__AssetEntity_stack"."duration" AS "AssetEntity__AssetEntity_stack_duration",
"AssetEntity__AssetEntity_stack"."isVisible" AS "AssetEntity__AssetEntity_stack_isVisible",
"AssetEntity__AssetEntity_stack"."livePhotoVideoId" AS "AssetEntity__AssetEntity_stack_livePhotoVideoId",
"AssetEntity__AssetEntity_stack"."originalFileName" AS "AssetEntity__AssetEntity_stack_originalFileName",
"AssetEntity__AssetEntity_stack"."sidecarPath" AS "AssetEntity__AssetEntity_stack_sidecarPath",
"AssetEntity__AssetEntity_stack"."stackParentId" AS "AssetEntity__AssetEntity_stack_stackParentId"
FROM
"assets" "AssetEntity"
LEFT JOIN "exif" "AssetEntity__AssetEntity_exifInfo" ON "AssetEntity__AssetEntity_exifInfo"."assetId" = "AssetEntity"."id"
LEFT JOIN "smart_info" "AssetEntity__AssetEntity_smartInfo" ON "AssetEntity__AssetEntity_smartInfo"."assetId" = "AssetEntity"."id"
LEFT JOIN "tag_asset" "AssetEntity_AssetEntity__AssetEntity_tags" ON "AssetEntity_AssetEntity__AssetEntity_tags"."assetsId" = "AssetEntity"."id"
LEFT JOIN "tags" "AssetEntity__AssetEntity_tags" ON "AssetEntity__AssetEntity_tags"."id" = "AssetEntity_AssetEntity__AssetEntity_tags"."tagsId"
LEFT JOIN "asset_faces" "AssetEntity__AssetEntity_faces" ON "AssetEntity__AssetEntity_faces"."assetId" = "AssetEntity"."id"
LEFT JOIN "person" "8258e303a73a72cf6abb13d73fb592dde0d68280" ON "8258e303a73a72cf6abb13d73fb592dde0d68280"."id" = "AssetEntity__AssetEntity_faces"."personId"
LEFT JOIN "assets" "AssetEntity__AssetEntity_stack" ON "AssetEntity__AssetEntity_stack"."stackParentId" = "AssetEntity"."id"
WHERE
("AssetEntity"."id" IN ($1))
-- AssetRepository.deleteAll
DELETE FROM "assets"
WHERE
"ownerId" = $1
-- AssetRepository.getByLibraryId
SELECT
"AssetEntity"."id" AS "AssetEntity_id",
"AssetEntity"."deviceAssetId" AS "AssetEntity_deviceAssetId",
"AssetEntity"."ownerId" AS "AssetEntity_ownerId",
"AssetEntity"."libraryId" AS "AssetEntity_libraryId",
"AssetEntity"."deviceId" AS "AssetEntity_deviceId",
"AssetEntity"."type" AS "AssetEntity_type",
"AssetEntity"."originalPath" AS "AssetEntity_originalPath",
"AssetEntity"."resizePath" AS "AssetEntity_resizePath",
"AssetEntity"."webpPath" AS "AssetEntity_webpPath",
"AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
"AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
"AssetEntity"."createdAt" AS "AssetEntity_createdAt",
"AssetEntity"."updatedAt" AS "AssetEntity_updatedAt",
"AssetEntity"."deletedAt" AS "AssetEntity_deletedAt",
"AssetEntity"."fileCreatedAt" AS "AssetEntity_fileCreatedAt",
"AssetEntity"."localDateTime" AS "AssetEntity_localDateTime",
"AssetEntity"."fileModifiedAt" AS "AssetEntity_fileModifiedAt",
"AssetEntity"."isFavorite" AS "AssetEntity_isFavorite",
"AssetEntity"."isArchived" AS "AssetEntity_isArchived",
"AssetEntity"."isExternal" AS "AssetEntity_isExternal",
"AssetEntity"."isReadOnly" AS "AssetEntity_isReadOnly",
"AssetEntity"."isOffline" AS "AssetEntity_isOffline",
"AssetEntity"."checksum" AS "AssetEntity_checksum",
"AssetEntity"."duration" AS "AssetEntity_duration",
"AssetEntity"."isVisible" AS "AssetEntity_isVisible",
"AssetEntity"."livePhotoVideoId" AS "AssetEntity_livePhotoVideoId",
"AssetEntity"."originalFileName" AS "AssetEntity_originalFileName",
"AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath",
"AssetEntity"."stackParentId" AS "AssetEntity_stackParentId"
FROM
"assets" "AssetEntity"
LEFT JOIN "libraries" "AssetEntity__AssetEntity_library" ON "AssetEntity__AssetEntity_library"."id" = "AssetEntity"."libraryId"
AND (
"AssetEntity__AssetEntity_library"."deletedAt" IS NULL
)
WHERE
(("AssetEntity__AssetEntity_library"."id" IN ($1)))
AND ("AssetEntity"."deletedAt" IS NULL)
-- AssetRepository.getByLibraryIdAndOriginalPath
SELECT DISTINCT
"distinctAlias"."AssetEntity_id" AS "ids_AssetEntity_id"
FROM
(
SELECT
"AssetEntity"."id" AS "AssetEntity_id",
"AssetEntity"."deviceAssetId" AS "AssetEntity_deviceAssetId",
"AssetEntity"."ownerId" AS "AssetEntity_ownerId",
"AssetEntity"."libraryId" AS "AssetEntity_libraryId",
"AssetEntity"."deviceId" AS "AssetEntity_deviceId",
"AssetEntity"."type" AS "AssetEntity_type",
"AssetEntity"."originalPath" AS "AssetEntity_originalPath",
"AssetEntity"."resizePath" AS "AssetEntity_resizePath",
"AssetEntity"."webpPath" AS "AssetEntity_webpPath",
"AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
"AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
"AssetEntity"."createdAt" AS "AssetEntity_createdAt",
"AssetEntity"."updatedAt" AS "AssetEntity_updatedAt",
"AssetEntity"."deletedAt" AS "AssetEntity_deletedAt",
"AssetEntity"."fileCreatedAt" AS "AssetEntity_fileCreatedAt",
"AssetEntity"."localDateTime" AS "AssetEntity_localDateTime",
"AssetEntity"."fileModifiedAt" AS "AssetEntity_fileModifiedAt",
"AssetEntity"."isFavorite" AS "AssetEntity_isFavorite",
"AssetEntity"."isArchived" AS "AssetEntity_isArchived",
"AssetEntity"."isExternal" AS "AssetEntity_isExternal",
"AssetEntity"."isReadOnly" AS "AssetEntity_isReadOnly",
"AssetEntity"."isOffline" AS "AssetEntity_isOffline",
"AssetEntity"."checksum" AS "AssetEntity_checksum",
"AssetEntity"."duration" AS "AssetEntity_duration",
"AssetEntity"."isVisible" AS "AssetEntity_isVisible",
"AssetEntity"."livePhotoVideoId" AS "AssetEntity_livePhotoVideoId",
"AssetEntity"."originalFileName" AS "AssetEntity_originalFileName",
"AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath",
"AssetEntity"."stackParentId" AS "AssetEntity_stackParentId"
FROM
"assets" "AssetEntity"
LEFT JOIN "libraries" "AssetEntity__AssetEntity_library" ON "AssetEntity__AssetEntity_library"."id" = "AssetEntity"."libraryId"
AND (
"AssetEntity__AssetEntity_library"."deletedAt" IS NULL
)
WHERE
(
(
"AssetEntity__AssetEntity_library"."id" = $1
AND "AssetEntity"."originalPath" = $2
)
)
AND ("AssetEntity"."deletedAt" IS NULL)
) "distinctAlias"
ORDER BY
"AssetEntity_id" ASC
LIMIT
1
-- AssetRepository.getAllByDeviceId
SELECT
"AssetEntity"."deviceAssetId" AS "AssetEntity_deviceAssetId",
"AssetEntity"."id" AS "AssetEntity_id"
FROM
"assets" "AssetEntity"
WHERE
(
"AssetEntity"."ownerId" = $1
AND "AssetEntity"."deviceId" = $2
AND "AssetEntity"."isVisible" = $3
)
-- AssetRepository.getById
SELECT DISTINCT
"distinctAlias"."AssetEntity_id" AS "ids_AssetEntity_id"
FROM
(
SELECT
"AssetEntity"."id" AS "AssetEntity_id",
"AssetEntity"."deviceAssetId" AS "AssetEntity_deviceAssetId",
"AssetEntity"."ownerId" AS "AssetEntity_ownerId",
"AssetEntity"."libraryId" AS "AssetEntity_libraryId",
"AssetEntity"."deviceId" AS "AssetEntity_deviceId",
"AssetEntity"."type" AS "AssetEntity_type",
"AssetEntity"."originalPath" AS "AssetEntity_originalPath",
"AssetEntity"."resizePath" AS "AssetEntity_resizePath",
"AssetEntity"."webpPath" AS "AssetEntity_webpPath",
"AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
"AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
"AssetEntity"."createdAt" AS "AssetEntity_createdAt",
"AssetEntity"."updatedAt" AS "AssetEntity_updatedAt",
"AssetEntity"."deletedAt" AS "AssetEntity_deletedAt",
"AssetEntity"."fileCreatedAt" AS "AssetEntity_fileCreatedAt",
"AssetEntity"."localDateTime" AS "AssetEntity_localDateTime",
"AssetEntity"."fileModifiedAt" AS "AssetEntity_fileModifiedAt",
"AssetEntity"."isFavorite" AS "AssetEntity_isFavorite",
"AssetEntity"."isArchived" AS "AssetEntity_isArchived",
"AssetEntity"."isExternal" AS "AssetEntity_isExternal",
"AssetEntity"."isReadOnly" AS "AssetEntity_isReadOnly",
"AssetEntity"."isOffline" AS "AssetEntity_isOffline",
"AssetEntity"."checksum" AS "AssetEntity_checksum",
"AssetEntity"."duration" AS "AssetEntity_duration",
"AssetEntity"."isVisible" AS "AssetEntity_isVisible",
"AssetEntity"."livePhotoVideoId" AS "AssetEntity_livePhotoVideoId",
"AssetEntity"."originalFileName" AS "AssetEntity_originalFileName",
"AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath",
"AssetEntity"."stackParentId" AS "AssetEntity_stackParentId",
"AssetEntity__AssetEntity_faces"."id" AS "AssetEntity__AssetEntity_faces_id",
"AssetEntity__AssetEntity_faces"."assetId" AS "AssetEntity__AssetEntity_faces_assetId",
"AssetEntity__AssetEntity_faces"."personId" AS "AssetEntity__AssetEntity_faces_personId",
"AssetEntity__AssetEntity_faces"."embedding" AS "AssetEntity__AssetEntity_faces_embedding",
"AssetEntity__AssetEntity_faces"."imageWidth" AS "AssetEntity__AssetEntity_faces_imageWidth",
"AssetEntity__AssetEntity_faces"."imageHeight" AS "AssetEntity__AssetEntity_faces_imageHeight",
"AssetEntity__AssetEntity_faces"."boundingBoxX1" AS "AssetEntity__AssetEntity_faces_boundingBoxX1",
"AssetEntity__AssetEntity_faces"."boundingBoxY1" AS "AssetEntity__AssetEntity_faces_boundingBoxY1",
"AssetEntity__AssetEntity_faces"."boundingBoxX2" AS "AssetEntity__AssetEntity_faces_boundingBoxX2",
"AssetEntity__AssetEntity_faces"."boundingBoxY2" AS "AssetEntity__AssetEntity_faces_boundingBoxY2",
"8258e303a73a72cf6abb13d73fb592dde0d68280"."id" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_id",
"8258e303a73a72cf6abb13d73fb592dde0d68280"."createdAt" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_createdAt",
"8258e303a73a72cf6abb13d73fb592dde0d68280"."updatedAt" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_updatedAt",
"8258e303a73a72cf6abb13d73fb592dde0d68280"."ownerId" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_ownerId",
"8258e303a73a72cf6abb13d73fb592dde0d68280"."name" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_name",
"8258e303a73a72cf6abb13d73fb592dde0d68280"."birthDate" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_birthDate",
"8258e303a73a72cf6abb13d73fb592dde0d68280"."thumbnailPath" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_thumbnailPath",
"8258e303a73a72cf6abb13d73fb592dde0d68280"."faceAssetId" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_faceAssetId",
"8258e303a73a72cf6abb13d73fb592dde0d68280"."isHidden" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_isHidden",
"AssetEntity__AssetEntity_library"."id" AS "AssetEntity__AssetEntity_library_id",
"AssetEntity__AssetEntity_library"."name" AS "AssetEntity__AssetEntity_library_name",
"AssetEntity__AssetEntity_library"."ownerId" AS "AssetEntity__AssetEntity_library_ownerId",
"AssetEntity__AssetEntity_library"."type" AS "AssetEntity__AssetEntity_library_type",
"AssetEntity__AssetEntity_library"."importPaths" AS "AssetEntity__AssetEntity_library_importPaths",
"AssetEntity__AssetEntity_library"."exclusionPatterns" AS "AssetEntity__AssetEntity_library_exclusionPatterns",
"AssetEntity__AssetEntity_library"."createdAt" AS "AssetEntity__AssetEntity_library_createdAt",
"AssetEntity__AssetEntity_library"."updatedAt" AS "AssetEntity__AssetEntity_library_updatedAt",
"AssetEntity__AssetEntity_library"."deletedAt" AS "AssetEntity__AssetEntity_library_deletedAt",
"AssetEntity__AssetEntity_library"."refreshedAt" AS "AssetEntity__AssetEntity_library_refreshedAt",
"AssetEntity__AssetEntity_library"."isVisible" AS "AssetEntity__AssetEntity_library_isVisible",
"AssetEntity__AssetEntity_stack"."id" AS "AssetEntity__AssetEntity_stack_id",
"AssetEntity__AssetEntity_stack"."deviceAssetId" AS "AssetEntity__AssetEntity_stack_deviceAssetId",
"AssetEntity__AssetEntity_stack"."ownerId" AS "AssetEntity__AssetEntity_stack_ownerId",
"AssetEntity__AssetEntity_stack"."libraryId" AS "AssetEntity__AssetEntity_stack_libraryId",
"AssetEntity__AssetEntity_stack"."deviceId" AS "AssetEntity__AssetEntity_stack_deviceId",
"AssetEntity__AssetEntity_stack"."type" AS "AssetEntity__AssetEntity_stack_type",
"AssetEntity__AssetEntity_stack"."originalPath" AS "AssetEntity__AssetEntity_stack_originalPath",
"AssetEntity__AssetEntity_stack"."resizePath" AS "AssetEntity__AssetEntity_stack_resizePath",
"AssetEntity__AssetEntity_stack"."webpPath" AS "AssetEntity__AssetEntity_stack_webpPath",
"AssetEntity__AssetEntity_stack"."thumbhash" AS "AssetEntity__AssetEntity_stack_thumbhash",
"AssetEntity__AssetEntity_stack"."encodedVideoPath" AS "AssetEntity__AssetEntity_stack_encodedVideoPath",
"AssetEntity__AssetEntity_stack"."createdAt" AS "AssetEntity__AssetEntity_stack_createdAt",
"AssetEntity__AssetEntity_stack"."updatedAt" AS "AssetEntity__AssetEntity_stack_updatedAt",
"AssetEntity__AssetEntity_stack"."deletedAt" AS "AssetEntity__AssetEntity_stack_deletedAt",
"AssetEntity__AssetEntity_stack"."fileCreatedAt" AS "AssetEntity__AssetEntity_stack_fileCreatedAt",
"AssetEntity__AssetEntity_stack"."localDateTime" AS "AssetEntity__AssetEntity_stack_localDateTime",
"AssetEntity__AssetEntity_stack"."fileModifiedAt" AS "AssetEntity__AssetEntity_stack_fileModifiedAt",
"AssetEntity__AssetEntity_stack"."isFavorite" AS "AssetEntity__AssetEntity_stack_isFavorite",
"AssetEntity__AssetEntity_stack"."isArchived" AS "AssetEntity__AssetEntity_stack_isArchived",
"AssetEntity__AssetEntity_stack"."isExternal" AS "AssetEntity__AssetEntity_stack_isExternal",
"AssetEntity__AssetEntity_stack"."isReadOnly" AS "AssetEntity__AssetEntity_stack_isReadOnly",
"AssetEntity__AssetEntity_stack"."isOffline" AS "AssetEntity__AssetEntity_stack_isOffline",
"AssetEntity__AssetEntity_stack"."checksum" AS "AssetEntity__AssetEntity_stack_checksum",
"AssetEntity__AssetEntity_stack"."duration" AS "AssetEntity__AssetEntity_stack_duration",
"AssetEntity__AssetEntity_stack"."isVisible" AS "AssetEntity__AssetEntity_stack_isVisible",
"AssetEntity__AssetEntity_stack"."livePhotoVideoId" AS "AssetEntity__AssetEntity_stack_livePhotoVideoId",
"AssetEntity__AssetEntity_stack"."originalFileName" AS "AssetEntity__AssetEntity_stack_originalFileName",
"AssetEntity__AssetEntity_stack"."sidecarPath" AS "AssetEntity__AssetEntity_stack_sidecarPath",
"AssetEntity__AssetEntity_stack"."stackParentId" AS "AssetEntity__AssetEntity_stack_stackParentId"
FROM
"assets" "AssetEntity"
LEFT JOIN "asset_faces" "AssetEntity__AssetEntity_faces" ON "AssetEntity__AssetEntity_faces"."assetId" = "AssetEntity"."id"
LEFT JOIN "person" "8258e303a73a72cf6abb13d73fb592dde0d68280" ON "8258e303a73a72cf6abb13d73fb592dde0d68280"."id" = "AssetEntity__AssetEntity_faces"."personId"
LEFT JOIN "libraries" "AssetEntity__AssetEntity_library" ON "AssetEntity__AssetEntity_library"."id" = "AssetEntity"."libraryId"
LEFT JOIN "assets" "AssetEntity__AssetEntity_stack" ON "AssetEntity__AssetEntity_stack"."stackParentId" = "AssetEntity"."id"
WHERE
("AssetEntity"."id" = $1)
) "distinctAlias"
ORDER BY
"AssetEntity_id" ASC
LIMIT
1
-- AssetRepository.updateAll
UPDATE "assets"
SET
"deviceId" = $1,
"updatedAt" = CURRENT_TIMESTAMP
WHERE
"id" IN ($2)
-- AssetRepository.getByChecksum
SELECT
"AssetEntity"."id" AS "AssetEntity_id",
"AssetEntity"."deviceAssetId" AS "AssetEntity_deviceAssetId",
"AssetEntity"."ownerId" AS "AssetEntity_ownerId",
"AssetEntity"."libraryId" AS "AssetEntity_libraryId",
"AssetEntity"."deviceId" AS "AssetEntity_deviceId",
"AssetEntity"."type" AS "AssetEntity_type",
"AssetEntity"."originalPath" AS "AssetEntity_originalPath",
"AssetEntity"."resizePath" AS "AssetEntity_resizePath",
"AssetEntity"."webpPath" AS "AssetEntity_webpPath",
"AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
"AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
"AssetEntity"."createdAt" AS "AssetEntity_createdAt",
"AssetEntity"."updatedAt" AS "AssetEntity_updatedAt",
"AssetEntity"."deletedAt" AS "AssetEntity_deletedAt",
"AssetEntity"."fileCreatedAt" AS "AssetEntity_fileCreatedAt",
"AssetEntity"."localDateTime" AS "AssetEntity_localDateTime",
"AssetEntity"."fileModifiedAt" AS "AssetEntity_fileModifiedAt",
"AssetEntity"."isFavorite" AS "AssetEntity_isFavorite",
"AssetEntity"."isArchived" AS "AssetEntity_isArchived",
"AssetEntity"."isExternal" AS "AssetEntity_isExternal",
"AssetEntity"."isReadOnly" AS "AssetEntity_isReadOnly",
"AssetEntity"."isOffline" AS "AssetEntity_isOffline",
"AssetEntity"."checksum" AS "AssetEntity_checksum",
"AssetEntity"."duration" AS "AssetEntity_duration",
"AssetEntity"."isVisible" AS "AssetEntity_isVisible",
"AssetEntity"."livePhotoVideoId" AS "AssetEntity_livePhotoVideoId",
"AssetEntity"."originalFileName" AS "AssetEntity_originalFileName",
"AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath",
"AssetEntity"."stackParentId" AS "AssetEntity_stackParentId"
FROM
"assets" "AssetEntity"
WHERE
(
(
"AssetEntity"."ownerId" = $1
AND "AssetEntity"."checksum" = $2
)
)
AND ("AssetEntity"."deletedAt" IS NULL)
LIMIT
1
-- AssetRepository.getWithout (sidecar)
SELECT
"AssetEntity"."id" AS "AssetEntity_id",
"AssetEntity"."deviceAssetId" AS "AssetEntity_deviceAssetId",
"AssetEntity"."ownerId" AS "AssetEntity_ownerId",
"AssetEntity"."libraryId" AS "AssetEntity_libraryId",
"AssetEntity"."deviceId" AS "AssetEntity_deviceId",
"AssetEntity"."type" AS "AssetEntity_type",
"AssetEntity"."originalPath" AS "AssetEntity_originalPath",
"AssetEntity"."resizePath" AS "AssetEntity_resizePath",
"AssetEntity"."webpPath" AS "AssetEntity_webpPath",
"AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
"AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
"AssetEntity"."createdAt" AS "AssetEntity_createdAt",
"AssetEntity"."updatedAt" AS "AssetEntity_updatedAt",
"AssetEntity"."deletedAt" AS "AssetEntity_deletedAt",
"AssetEntity"."fileCreatedAt" AS "AssetEntity_fileCreatedAt",
"AssetEntity"."localDateTime" AS "AssetEntity_localDateTime",
"AssetEntity"."fileModifiedAt" AS "AssetEntity_fileModifiedAt",
"AssetEntity"."isFavorite" AS "AssetEntity_isFavorite",
"AssetEntity"."isArchived" AS "AssetEntity_isArchived",
"AssetEntity"."isExternal" AS "AssetEntity_isExternal",
"AssetEntity"."isReadOnly" AS "AssetEntity_isReadOnly",
"AssetEntity"."isOffline" AS "AssetEntity_isOffline",
"AssetEntity"."checksum" AS "AssetEntity_checksum",
"AssetEntity"."duration" AS "AssetEntity_duration",
"AssetEntity"."isVisible" AS "AssetEntity_isVisible",
"AssetEntity"."livePhotoVideoId" AS "AssetEntity_livePhotoVideoId",
"AssetEntity"."originalFileName" AS "AssetEntity_originalFileName",
"AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath",
"AssetEntity"."stackParentId" AS "AssetEntity_stackParentId"
FROM
"assets" "AssetEntity"
WHERE
(
(
(
"AssetEntity"."sidecarPath" IS NULL
AND "AssetEntity"."isVisible" = $1
)
OR (
"AssetEntity"."sidecarPath" = $2
AND "AssetEntity"."isVisible" = $3
)
)
)
AND ("AssetEntity"."deletedAt" IS NULL)
ORDER BY
"AssetEntity"."createdAt" ASC
LIMIT
11

View file

@ -0,0 +1 @@
-- NOTE: This file is auto generated by ./sql-generator

View file

@ -0,0 +1,299 @@
-- NOTE: This file is auto generated by ./sql-generator
-- LibraryRepository.get
SELECT DISTINCT
"distinctAlias"."LibraryEntity_id" AS "ids_LibraryEntity_id"
FROM
(
SELECT
"LibraryEntity"."id" AS "LibraryEntity_id",
"LibraryEntity"."name" AS "LibraryEntity_name",
"LibraryEntity"."ownerId" AS "LibraryEntity_ownerId",
"LibraryEntity"."type" AS "LibraryEntity_type",
"LibraryEntity"."importPaths" AS "LibraryEntity_importPaths",
"LibraryEntity"."exclusionPatterns" AS "LibraryEntity_exclusionPatterns",
"LibraryEntity"."createdAt" AS "LibraryEntity_createdAt",
"LibraryEntity"."updatedAt" AS "LibraryEntity_updatedAt",
"LibraryEntity"."deletedAt" AS "LibraryEntity_deletedAt",
"LibraryEntity"."refreshedAt" AS "LibraryEntity_refreshedAt",
"LibraryEntity"."isVisible" AS "LibraryEntity_isVisible",
"LibraryEntity__LibraryEntity_owner"."id" AS "LibraryEntity__LibraryEntity_owner_id",
"LibraryEntity__LibraryEntity_owner"."name" AS "LibraryEntity__LibraryEntity_owner_name",
"LibraryEntity__LibraryEntity_owner"."avatarColor" AS "LibraryEntity__LibraryEntity_owner_avatarColor",
"LibraryEntity__LibraryEntity_owner"."isAdmin" AS "LibraryEntity__LibraryEntity_owner_isAdmin",
"LibraryEntity__LibraryEntity_owner"."email" AS "LibraryEntity__LibraryEntity_owner_email",
"LibraryEntity__LibraryEntity_owner"."storageLabel" AS "LibraryEntity__LibraryEntity_owner_storageLabel",
"LibraryEntity__LibraryEntity_owner"."externalPath" AS "LibraryEntity__LibraryEntity_owner_externalPath",
"LibraryEntity__LibraryEntity_owner"."oauthId" AS "LibraryEntity__LibraryEntity_owner_oauthId",
"LibraryEntity__LibraryEntity_owner"."profileImagePath" AS "LibraryEntity__LibraryEntity_owner_profileImagePath",
"LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword",
"LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt",
"LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt",
"LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt",
"LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled"
FROM
"libraries" "LibraryEntity"
LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId"
AND (
"LibraryEntity__LibraryEntity_owner"."deletedAt" IS NULL
)
WHERE
(("LibraryEntity"."id" = $1))
AND ("LibraryEntity"."deletedAt" IS NULL)
) "distinctAlias"
ORDER BY
"LibraryEntity_id" ASC
LIMIT
1
-- LibraryRepository.existsByName
SELECT
1 AS "row_exists"
FROM
(
SELECT
1 AS dummy_column
) "dummy_table"
WHERE
EXISTS (
SELECT
1
FROM
"libraries" "LibraryEntity"
WHERE
(("LibraryEntity"."name" = $1))
AND ("LibraryEntity"."deletedAt" IS NULL)
)
LIMIT
1
-- LibraryRepository.getCountForUser
SELECT
COUNT(1) AS "cnt"
FROM
"libraries" "LibraryEntity"
WHERE
(("LibraryEntity"."ownerId" = $1))
AND ("LibraryEntity"."deletedAt" IS NULL)
-- LibraryRepository.getDefaultUploadLibrary
SELECT
"LibraryEntity"."id" AS "LibraryEntity_id",
"LibraryEntity"."name" AS "LibraryEntity_name",
"LibraryEntity"."ownerId" AS "LibraryEntity_ownerId",
"LibraryEntity"."type" AS "LibraryEntity_type",
"LibraryEntity"."importPaths" AS "LibraryEntity_importPaths",
"LibraryEntity"."exclusionPatterns" AS "LibraryEntity_exclusionPatterns",
"LibraryEntity"."createdAt" AS "LibraryEntity_createdAt",
"LibraryEntity"."updatedAt" AS "LibraryEntity_updatedAt",
"LibraryEntity"."deletedAt" AS "LibraryEntity_deletedAt",
"LibraryEntity"."refreshedAt" AS "LibraryEntity_refreshedAt",
"LibraryEntity"."isVisible" AS "LibraryEntity_isVisible"
FROM
"libraries" "LibraryEntity"
WHERE
(
(
"LibraryEntity"."ownerId" = $1
AND "LibraryEntity"."type" = $2
)
)
AND ("LibraryEntity"."deletedAt" IS NULL)
ORDER BY
"LibraryEntity"."createdAt" ASC
LIMIT
1
-- LibraryRepository.getUploadLibraryCount
SELECT
COUNT(1) AS "cnt"
FROM
"libraries" "LibraryEntity"
WHERE
(
(
"LibraryEntity"."ownerId" = $1
AND "LibraryEntity"."type" = $2
)
)
AND ("LibraryEntity"."deletedAt" IS NULL)
-- LibraryRepository.getAllByUserId
SELECT
"LibraryEntity"."id" AS "LibraryEntity_id",
"LibraryEntity"."name" AS "LibraryEntity_name",
"LibraryEntity"."ownerId" AS "LibraryEntity_ownerId",
"LibraryEntity"."type" AS "LibraryEntity_type",
"LibraryEntity"."importPaths" AS "LibraryEntity_importPaths",
"LibraryEntity"."exclusionPatterns" AS "LibraryEntity_exclusionPatterns",
"LibraryEntity"."createdAt" AS "LibraryEntity_createdAt",
"LibraryEntity"."updatedAt" AS "LibraryEntity_updatedAt",
"LibraryEntity"."deletedAt" AS "LibraryEntity_deletedAt",
"LibraryEntity"."refreshedAt" AS "LibraryEntity_refreshedAt",
"LibraryEntity"."isVisible" AS "LibraryEntity_isVisible",
"LibraryEntity__LibraryEntity_owner"."id" AS "LibraryEntity__LibraryEntity_owner_id",
"LibraryEntity__LibraryEntity_owner"."name" AS "LibraryEntity__LibraryEntity_owner_name",
"LibraryEntity__LibraryEntity_owner"."avatarColor" AS "LibraryEntity__LibraryEntity_owner_avatarColor",
"LibraryEntity__LibraryEntity_owner"."isAdmin" AS "LibraryEntity__LibraryEntity_owner_isAdmin",
"LibraryEntity__LibraryEntity_owner"."email" AS "LibraryEntity__LibraryEntity_owner_email",
"LibraryEntity__LibraryEntity_owner"."storageLabel" AS "LibraryEntity__LibraryEntity_owner_storageLabel",
"LibraryEntity__LibraryEntity_owner"."externalPath" AS "LibraryEntity__LibraryEntity_owner_externalPath",
"LibraryEntity__LibraryEntity_owner"."oauthId" AS "LibraryEntity__LibraryEntity_owner_oauthId",
"LibraryEntity__LibraryEntity_owner"."profileImagePath" AS "LibraryEntity__LibraryEntity_owner_profileImagePath",
"LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword",
"LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt",
"LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt",
"LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt",
"LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled"
FROM
"libraries" "LibraryEntity"
LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId"
AND (
"LibraryEntity__LibraryEntity_owner"."deletedAt" IS NULL
)
WHERE
(
(
"LibraryEntity"."ownerId" = $1
AND "LibraryEntity"."isVisible" = $2
)
)
AND ("LibraryEntity"."deletedAt" IS NULL)
ORDER BY
"LibraryEntity"."createdAt" ASC
-- LibraryRepository.getAll
SELECT
"LibraryEntity"."id" AS "LibraryEntity_id",
"LibraryEntity"."name" AS "LibraryEntity_name",
"LibraryEntity"."ownerId" AS "LibraryEntity_ownerId",
"LibraryEntity"."type" AS "LibraryEntity_type",
"LibraryEntity"."importPaths" AS "LibraryEntity_importPaths",
"LibraryEntity"."exclusionPatterns" AS "LibraryEntity_exclusionPatterns",
"LibraryEntity"."createdAt" AS "LibraryEntity_createdAt",
"LibraryEntity"."updatedAt" AS "LibraryEntity_updatedAt",
"LibraryEntity"."deletedAt" AS "LibraryEntity_deletedAt",
"LibraryEntity"."refreshedAt" AS "LibraryEntity_refreshedAt",
"LibraryEntity"."isVisible" AS "LibraryEntity_isVisible",
"LibraryEntity__LibraryEntity_owner"."id" AS "LibraryEntity__LibraryEntity_owner_id",
"LibraryEntity__LibraryEntity_owner"."name" AS "LibraryEntity__LibraryEntity_owner_name",
"LibraryEntity__LibraryEntity_owner"."avatarColor" AS "LibraryEntity__LibraryEntity_owner_avatarColor",
"LibraryEntity__LibraryEntity_owner"."isAdmin" AS "LibraryEntity__LibraryEntity_owner_isAdmin",
"LibraryEntity__LibraryEntity_owner"."email" AS "LibraryEntity__LibraryEntity_owner_email",
"LibraryEntity__LibraryEntity_owner"."storageLabel" AS "LibraryEntity__LibraryEntity_owner_storageLabel",
"LibraryEntity__LibraryEntity_owner"."externalPath" AS "LibraryEntity__LibraryEntity_owner_externalPath",
"LibraryEntity__LibraryEntity_owner"."oauthId" AS "LibraryEntity__LibraryEntity_owner_oauthId",
"LibraryEntity__LibraryEntity_owner"."profileImagePath" AS "LibraryEntity__LibraryEntity_owner_profileImagePath",
"LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword",
"LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt",
"LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt",
"LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt",
"LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled"
FROM
"libraries" "LibraryEntity"
LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId"
AND (
"LibraryEntity__LibraryEntity_owner"."deletedAt" IS NULL
)
WHERE
"LibraryEntity"."deletedAt" IS NULL
ORDER BY
"LibraryEntity"."createdAt" ASC
-- LibraryRepository.getAllDeleted
SELECT
"LibraryEntity"."id" AS "LibraryEntity_id",
"LibraryEntity"."name" AS "LibraryEntity_name",
"LibraryEntity"."ownerId" AS "LibraryEntity_ownerId",
"LibraryEntity"."type" AS "LibraryEntity_type",
"LibraryEntity"."importPaths" AS "LibraryEntity_importPaths",
"LibraryEntity"."exclusionPatterns" AS "LibraryEntity_exclusionPatterns",
"LibraryEntity"."createdAt" AS "LibraryEntity_createdAt",
"LibraryEntity"."updatedAt" AS "LibraryEntity_updatedAt",
"LibraryEntity"."deletedAt" AS "LibraryEntity_deletedAt",
"LibraryEntity"."refreshedAt" AS "LibraryEntity_refreshedAt",
"LibraryEntity"."isVisible" AS "LibraryEntity_isVisible",
"LibraryEntity__LibraryEntity_owner"."id" AS "LibraryEntity__LibraryEntity_owner_id",
"LibraryEntity__LibraryEntity_owner"."name" AS "LibraryEntity__LibraryEntity_owner_name",
"LibraryEntity__LibraryEntity_owner"."avatarColor" AS "LibraryEntity__LibraryEntity_owner_avatarColor",
"LibraryEntity__LibraryEntity_owner"."isAdmin" AS "LibraryEntity__LibraryEntity_owner_isAdmin",
"LibraryEntity__LibraryEntity_owner"."email" AS "LibraryEntity__LibraryEntity_owner_email",
"LibraryEntity__LibraryEntity_owner"."storageLabel" AS "LibraryEntity__LibraryEntity_owner_storageLabel",
"LibraryEntity__LibraryEntity_owner"."externalPath" AS "LibraryEntity__LibraryEntity_owner_externalPath",
"LibraryEntity__LibraryEntity_owner"."oauthId" AS "LibraryEntity__LibraryEntity_owner_oauthId",
"LibraryEntity__LibraryEntity_owner"."profileImagePath" AS "LibraryEntity__LibraryEntity_owner_profileImagePath",
"LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword",
"LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt",
"LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt",
"LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt",
"LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled"
FROM
"libraries" "LibraryEntity"
LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId"
WHERE
(
"LibraryEntity"."isVisible" = $1
AND NOT ("LibraryEntity"."deletedAt" IS NULL)
)
ORDER BY
"LibraryEntity"."createdAt" ASC
-- LibraryRepository.getStatistics
SELECT
"libraries"."id" AS "libraries_id",
"libraries"."name" AS "libraries_name",
"libraries"."ownerId" AS "libraries_ownerId",
"libraries"."type" AS "libraries_type",
"libraries"."importPaths" AS "libraries_importPaths",
"libraries"."exclusionPatterns" AS "libraries_exclusionPatterns",
"libraries"."createdAt" AS "libraries_createdAt",
"libraries"."updatedAt" AS "libraries_updatedAt",
"libraries"."deletedAt" AS "libraries_deletedAt",
"libraries"."refreshedAt" AS "libraries_refreshedAt",
"libraries"."isVisible" AS "libraries_isVisible",
COUNT("assets"."id") FILTER (
WHERE
"assets"."type" = 'IMAGE'
AND "assets"."isVisible"
) AS "photos",
COUNT("assets"."id") FILTER (
WHERE
"assets"."type" = 'VIDEO'
AND "assets"."isVisible"
) AS "videos",
COALESCE(SUM("exif"."fileSizeInByte"), 0) AS "usage"
FROM
"libraries" "libraries"
LEFT JOIN "assets" "assets" ON "assets"."libraryId" = "libraries"."id"
AND ("assets"."deletedAt" IS NULL)
LEFT JOIN "exif" "exif" ON "exif"."assetId" = "assets"."id"
WHERE
("libraries"."id" = $1)
AND ("libraries"."deletedAt" IS NULL)
GROUP BY
"libraries"."id"
-- LibraryRepository.getOnlineAssetPaths
SELECT
"assets"."originalPath" AS "assets_originalPath"
FROM
"libraries" "library"
INNER JOIN "assets" "assets" ON "assets"."libraryId" = "library"."id"
AND ("assets"."deletedAt" IS NULL)
WHERE
(
"library"."id" = $1
AND "assets"."isOffline" = false
)
AND ("library"."deletedAt" IS NULL)
-- LibraryRepository.getAssetIds
SELECT
"assets"."id" AS "assets_id"
FROM
"libraries" "library"
INNER JOIN "assets" "assets" ON "assets"."libraryId" = "library"."id"
AND ("assets"."deletedAt" IS NULL)
WHERE
("library"."id" = $1)
AND ("library"."deletedAt" IS NULL)

View file

@ -0,0 +1,18 @@
-- NOTE: This file is auto generated by ./sql-generator
-- MoveRepository.getByEntity
SELECT
"MoveEntity"."id" AS "MoveEntity_id",
"MoveEntity"."entityId" AS "MoveEntity_entityId",
"MoveEntity"."pathType" AS "MoveEntity_pathType",
"MoveEntity"."oldPath" AS "MoveEntity_oldPath",
"MoveEntity"."newPath" AS "MoveEntity_newPath"
FROM
"move_history" "MoveEntity"
WHERE
(
"MoveEntity"."entityId" = $1
AND "MoveEntity"."pathType" = $2
)
LIMIT
1

View file

@ -0,0 +1 @@
-- NOTE: This file is auto generated by ./sql-generator

View file

@ -0,0 +1,367 @@
-- NOTE: This file is auto generated by ./sql-generator
-- PersonRepository.reassignFaces
UPDATE "asset_faces"
SET
"personId" = $1
WHERE
"personId" = $2
-- PersonRepository.getAllFaces
SELECT
"AssetFaceEntity"."id" AS "AssetFaceEntity_id",
"AssetFaceEntity"."assetId" AS "AssetFaceEntity_assetId",
"AssetFaceEntity"."personId" AS "AssetFaceEntity_personId",
"AssetFaceEntity"."embedding" AS "AssetFaceEntity_embedding",
"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__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"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath",
"AssetFaceEntity__AssetFaceEntity_asset"."resizePath" AS "AssetFaceEntity__AssetFaceEntity_asset_resizePath",
"AssetFaceEntity__AssetFaceEntity_asset"."webpPath" AS "AssetFaceEntity__AssetFaceEntity_asset_webpPath",
"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"."isReadOnly" AS "AssetFaceEntity__AssetFaceEntity_asset_isReadOnly",
"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"."stackParentId" AS "AssetFaceEntity__AssetFaceEntity_asset_stackParentId"
FROM
"asset_faces" "AssetFaceEntity"
LEFT JOIN "assets" "AssetFaceEntity__AssetFaceEntity_asset" ON "AssetFaceEntity__AssetFaceEntity_asset"."id" = "AssetFaceEntity"."assetId"
-- PersonRepository.getAll
SELECT
"PersonEntity"."id" AS "PersonEntity_id",
"PersonEntity"."createdAt" AS "PersonEntity_createdAt",
"PersonEntity"."updatedAt" AS "PersonEntity_updatedAt",
"PersonEntity"."ownerId" AS "PersonEntity_ownerId",
"PersonEntity"."name" AS "PersonEntity_name",
"PersonEntity"."birthDate" AS "PersonEntity_birthDate",
"PersonEntity"."thumbnailPath" AS "PersonEntity_thumbnailPath",
"PersonEntity"."faceAssetId" AS "PersonEntity_faceAssetId",
"PersonEntity"."isHidden" AS "PersonEntity_isHidden"
FROM
"person" "PersonEntity"
-- PersonRepository.getAllWithoutThumbnail
SELECT
"PersonEntity"."id" AS "PersonEntity_id",
"PersonEntity"."createdAt" AS "PersonEntity_createdAt",
"PersonEntity"."updatedAt" AS "PersonEntity_updatedAt",
"PersonEntity"."ownerId" AS "PersonEntity_ownerId",
"PersonEntity"."name" AS "PersonEntity_name",
"PersonEntity"."birthDate" AS "PersonEntity_birthDate",
"PersonEntity"."thumbnailPath" AS "PersonEntity_thumbnailPath",
"PersonEntity"."faceAssetId" AS "PersonEntity_faceAssetId",
"PersonEntity"."isHidden" AS "PersonEntity_isHidden"
FROM
"person" "PersonEntity"
WHERE
("PersonEntity"."thumbnailPath" = $1)
-- 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"
LEFT 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 "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
LIMIT
500
-- 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
"person"."id"
HAVING
COUNT("face"."assetId") = 0
-- PersonRepository.getById
SELECT
"PersonEntity"."id" AS "PersonEntity_id",
"PersonEntity"."createdAt" AS "PersonEntity_createdAt",
"PersonEntity"."updatedAt" AS "PersonEntity_updatedAt",
"PersonEntity"."ownerId" AS "PersonEntity_ownerId",
"PersonEntity"."name" AS "PersonEntity_name",
"PersonEntity"."birthDate" AS "PersonEntity_birthDate",
"PersonEntity"."thumbnailPath" AS "PersonEntity_thumbnailPath",
"PersonEntity"."faceAssetId" AS "PersonEntity_faceAssetId",
"PersonEntity"."isHidden" AS "PersonEntity_isHidden"
FROM
"person" "PersonEntity"
WHERE
("PersonEntity"."id" = $1)
LIMIT
1
-- 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"
LEFT JOIN "asset_faces" "face" ON "face"."personId" = "person"."id"
WHERE
"person"."ownerId" = $1
AND (
LOWER("person"."name") LIKE $2
OR LOWER("person"."name") LIKE $3
)
GROUP BY
"person"."id"
ORDER BY
COUNT("face"."assetId") DESC
LIMIT
20
-- PersonRepository.getStatistics
SELECT DISTINCT
COUNT(DISTINCT ("face"."id")) AS "cnt"
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.getAssets
SELECT DISTINCT
"distinctAlias"."AssetEntity_id" AS "ids_AssetEntity_id",
"distinctAlias"."AssetEntity_fileCreatedAt"
FROM
(
SELECT
"AssetEntity"."id" AS "AssetEntity_id",
"AssetEntity"."deviceAssetId" AS "AssetEntity_deviceAssetId",
"AssetEntity"."ownerId" AS "AssetEntity_ownerId",
"AssetEntity"."libraryId" AS "AssetEntity_libraryId",
"AssetEntity"."deviceId" AS "AssetEntity_deviceId",
"AssetEntity"."type" AS "AssetEntity_type",
"AssetEntity"."originalPath" AS "AssetEntity_originalPath",
"AssetEntity"."resizePath" AS "AssetEntity_resizePath",
"AssetEntity"."webpPath" AS "AssetEntity_webpPath",
"AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
"AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
"AssetEntity"."createdAt" AS "AssetEntity_createdAt",
"AssetEntity"."updatedAt" AS "AssetEntity_updatedAt",
"AssetEntity"."deletedAt" AS "AssetEntity_deletedAt",
"AssetEntity"."fileCreatedAt" AS "AssetEntity_fileCreatedAt",
"AssetEntity"."localDateTime" AS "AssetEntity_localDateTime",
"AssetEntity"."fileModifiedAt" AS "AssetEntity_fileModifiedAt",
"AssetEntity"."isFavorite" AS "AssetEntity_isFavorite",
"AssetEntity"."isArchived" AS "AssetEntity_isArchived",
"AssetEntity"."isExternal" AS "AssetEntity_isExternal",
"AssetEntity"."isReadOnly" AS "AssetEntity_isReadOnly",
"AssetEntity"."isOffline" AS "AssetEntity_isOffline",
"AssetEntity"."checksum" AS "AssetEntity_checksum",
"AssetEntity"."duration" AS "AssetEntity_duration",
"AssetEntity"."isVisible" AS "AssetEntity_isVisible",
"AssetEntity"."livePhotoVideoId" AS "AssetEntity_livePhotoVideoId",
"AssetEntity"."originalFileName" AS "AssetEntity_originalFileName",
"AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath",
"AssetEntity"."stackParentId" AS "AssetEntity_stackParentId",
"AssetEntity__AssetEntity_faces"."id" AS "AssetEntity__AssetEntity_faces_id",
"AssetEntity__AssetEntity_faces"."assetId" AS "AssetEntity__AssetEntity_faces_assetId",
"AssetEntity__AssetEntity_faces"."personId" AS "AssetEntity__AssetEntity_faces_personId",
"AssetEntity__AssetEntity_faces"."embedding" AS "AssetEntity__AssetEntity_faces_embedding",
"AssetEntity__AssetEntity_faces"."imageWidth" AS "AssetEntity__AssetEntity_faces_imageWidth",
"AssetEntity__AssetEntity_faces"."imageHeight" AS "AssetEntity__AssetEntity_faces_imageHeight",
"AssetEntity__AssetEntity_faces"."boundingBoxX1" AS "AssetEntity__AssetEntity_faces_boundingBoxX1",
"AssetEntity__AssetEntity_faces"."boundingBoxY1" AS "AssetEntity__AssetEntity_faces_boundingBoxY1",
"AssetEntity__AssetEntity_faces"."boundingBoxX2" AS "AssetEntity__AssetEntity_faces_boundingBoxX2",
"AssetEntity__AssetEntity_faces"."boundingBoxY2" AS "AssetEntity__AssetEntity_faces_boundingBoxY2",
"8258e303a73a72cf6abb13d73fb592dde0d68280"."id" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_id",
"8258e303a73a72cf6abb13d73fb592dde0d68280"."createdAt" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_createdAt",
"8258e303a73a72cf6abb13d73fb592dde0d68280"."updatedAt" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_updatedAt",
"8258e303a73a72cf6abb13d73fb592dde0d68280"."ownerId" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_ownerId",
"8258e303a73a72cf6abb13d73fb592dde0d68280"."name" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_name",
"8258e303a73a72cf6abb13d73fb592dde0d68280"."birthDate" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_birthDate",
"8258e303a73a72cf6abb13d73fb592dde0d68280"."thumbnailPath" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_thumbnailPath",
"8258e303a73a72cf6abb13d73fb592dde0d68280"."faceAssetId" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_faceAssetId",
"8258e303a73a72cf6abb13d73fb592dde0d68280"."isHidden" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_isHidden",
"AssetEntity__AssetEntity_exifInfo"."assetId" AS "AssetEntity__AssetEntity_exifInfo_assetId",
"AssetEntity__AssetEntity_exifInfo"."description" AS "AssetEntity__AssetEntity_exifInfo_description",
"AssetEntity__AssetEntity_exifInfo"."exifImageWidth" AS "AssetEntity__AssetEntity_exifInfo_exifImageWidth",
"AssetEntity__AssetEntity_exifInfo"."exifImageHeight" AS "AssetEntity__AssetEntity_exifInfo_exifImageHeight",
"AssetEntity__AssetEntity_exifInfo"."fileSizeInByte" AS "AssetEntity__AssetEntity_exifInfo_fileSizeInByte",
"AssetEntity__AssetEntity_exifInfo"."orientation" AS "AssetEntity__AssetEntity_exifInfo_orientation",
"AssetEntity__AssetEntity_exifInfo"."dateTimeOriginal" AS "AssetEntity__AssetEntity_exifInfo_dateTimeOriginal",
"AssetEntity__AssetEntity_exifInfo"."modifyDate" AS "AssetEntity__AssetEntity_exifInfo_modifyDate",
"AssetEntity__AssetEntity_exifInfo"."timeZone" AS "AssetEntity__AssetEntity_exifInfo_timeZone",
"AssetEntity__AssetEntity_exifInfo"."latitude" AS "AssetEntity__AssetEntity_exifInfo_latitude",
"AssetEntity__AssetEntity_exifInfo"."longitude" AS "AssetEntity__AssetEntity_exifInfo_longitude",
"AssetEntity__AssetEntity_exifInfo"."projectionType" AS "AssetEntity__AssetEntity_exifInfo_projectionType",
"AssetEntity__AssetEntity_exifInfo"."city" AS "AssetEntity__AssetEntity_exifInfo_city",
"AssetEntity__AssetEntity_exifInfo"."livePhotoCID" AS "AssetEntity__AssetEntity_exifInfo_livePhotoCID",
"AssetEntity__AssetEntity_exifInfo"."state" AS "AssetEntity__AssetEntity_exifInfo_state",
"AssetEntity__AssetEntity_exifInfo"."country" AS "AssetEntity__AssetEntity_exifInfo_country",
"AssetEntity__AssetEntity_exifInfo"."make" AS "AssetEntity__AssetEntity_exifInfo_make",
"AssetEntity__AssetEntity_exifInfo"."model" AS "AssetEntity__AssetEntity_exifInfo_model",
"AssetEntity__AssetEntity_exifInfo"."lensModel" AS "AssetEntity__AssetEntity_exifInfo_lensModel",
"AssetEntity__AssetEntity_exifInfo"."fNumber" AS "AssetEntity__AssetEntity_exifInfo_fNumber",
"AssetEntity__AssetEntity_exifInfo"."focalLength" AS "AssetEntity__AssetEntity_exifInfo_focalLength",
"AssetEntity__AssetEntity_exifInfo"."iso" AS "AssetEntity__AssetEntity_exifInfo_iso",
"AssetEntity__AssetEntity_exifInfo"."exposureTime" AS "AssetEntity__AssetEntity_exifInfo_exposureTime",
"AssetEntity__AssetEntity_exifInfo"."profileDescription" AS "AssetEntity__AssetEntity_exifInfo_profileDescription",
"AssetEntity__AssetEntity_exifInfo"."colorspace" AS "AssetEntity__AssetEntity_exifInfo_colorspace",
"AssetEntity__AssetEntity_exifInfo"."bitsPerSample" AS "AssetEntity__AssetEntity_exifInfo_bitsPerSample",
"AssetEntity__AssetEntity_exifInfo"."fps" AS "AssetEntity__AssetEntity_exifInfo_fps",
"AssetEntity__AssetEntity_exifInfo"."exifTextSearchableColumn" AS "AssetEntity__AssetEntity_exifInfo_exifTextSearchableColumn"
FROM
"assets" "AssetEntity"
LEFT JOIN "asset_faces" "AssetEntity__AssetEntity_faces" ON "AssetEntity__AssetEntity_faces"."assetId" = "AssetEntity"."id"
LEFT JOIN "person" "8258e303a73a72cf6abb13d73fb592dde0d68280" ON "8258e303a73a72cf6abb13d73fb592dde0d68280"."id" = "AssetEntity__AssetEntity_faces"."personId"
LEFT JOIN "exif" "AssetEntity__AssetEntity_exifInfo" ON "AssetEntity__AssetEntity_exifInfo"."assetId" = "AssetEntity"."id"
WHERE
(
(
"AssetEntity__AssetEntity_faces"."personId" = $1
AND "AssetEntity"."isVisible" = $2
AND "AssetEntity"."isArchived" = $3
)
)
AND ("AssetEntity"."deletedAt" IS NULL)
) "distinctAlias"
ORDER BY
"distinctAlias"."AssetEntity_fileCreatedAt" DESC,
"AssetEntity_id" ASC
LIMIT
1000
-- PersonRepository.getFacesByIds
SELECT
"AssetFaceEntity"."id" AS "AssetFaceEntity_id",
"AssetFaceEntity"."assetId" AS "AssetFaceEntity_assetId",
"AssetFaceEntity"."personId" AS "AssetFaceEntity_personId",
"AssetFaceEntity"."embedding" AS "AssetFaceEntity_embedding",
"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__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"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath",
"AssetFaceEntity__AssetFaceEntity_asset"."resizePath" AS "AssetFaceEntity__AssetFaceEntity_asset_resizePath",
"AssetFaceEntity__AssetFaceEntity_asset"."webpPath" AS "AssetFaceEntity__AssetFaceEntity_asset_webpPath",
"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"."isReadOnly" AS "AssetFaceEntity__AssetFaceEntity_asset_isReadOnly",
"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"."stackParentId" AS "AssetFaceEntity__AssetFaceEntity_asset_stackParentId"
FROM
"asset_faces" "AssetFaceEntity"
LEFT JOIN "assets" "AssetFaceEntity__AssetFaceEntity_asset" ON "AssetFaceEntity__AssetFaceEntity_asset"."id" = "AssetFaceEntity"."assetId"
WHERE
(
(
"AssetFaceEntity"."assetId" = $1
AND "AssetFaceEntity"."personId" = $2
)
)
-- PersonRepository.getRandomFace
SELECT
"AssetFaceEntity"."id" AS "AssetFaceEntity_id",
"AssetFaceEntity"."assetId" AS "AssetFaceEntity_assetId",
"AssetFaceEntity"."personId" AS "AssetFaceEntity_personId",
"AssetFaceEntity"."embedding" AS "AssetFaceEntity_embedding",
"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"
FROM
"asset_faces" "AssetFaceEntity"
WHERE
("AssetFaceEntity"."personId" = $1)
LIMIT
1

View file

@ -0,0 +1,327 @@
-- NOTE: This file is auto generated by ./sql-generator
-- SharedLinkRepository.get
SELECT DISTINCT
"distinctAlias"."SharedLinkEntity_id" AS "ids_SharedLinkEntity_id",
"distinctAlias"."SharedLinkEntity_createdAt",
"distinctAlias"."SharedLinkEntity__SharedLinkEntity_assets_fileCreatedAt",
"distinctAlias"."4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_fileCreatedAt"
FROM
(
SELECT
"SharedLinkEntity"."id" AS "SharedLinkEntity_id",
"SharedLinkEntity"."description" AS "SharedLinkEntity_description",
"SharedLinkEntity"."password" AS "SharedLinkEntity_password",
"SharedLinkEntity"."userId" AS "SharedLinkEntity_userId",
"SharedLinkEntity"."key" AS "SharedLinkEntity_key",
"SharedLinkEntity"."type" AS "SharedLinkEntity_type",
"SharedLinkEntity"."createdAt" AS "SharedLinkEntity_createdAt",
"SharedLinkEntity"."expiresAt" AS "SharedLinkEntity_expiresAt",
"SharedLinkEntity"."allowUpload" AS "SharedLinkEntity_allowUpload",
"SharedLinkEntity"."allowDownload" AS "SharedLinkEntity_allowDownload",
"SharedLinkEntity"."showExif" AS "SharedLinkEntity_showExif",
"SharedLinkEntity"."albumId" AS "SharedLinkEntity_albumId",
"SharedLinkEntity__SharedLinkEntity_assets"."id" AS "SharedLinkEntity__SharedLinkEntity_assets_id",
"SharedLinkEntity__SharedLinkEntity_assets"."deviceAssetId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceAssetId",
"SharedLinkEntity__SharedLinkEntity_assets"."ownerId" AS "SharedLinkEntity__SharedLinkEntity_assets_ownerId",
"SharedLinkEntity__SharedLinkEntity_assets"."libraryId" AS "SharedLinkEntity__SharedLinkEntity_assets_libraryId",
"SharedLinkEntity__SharedLinkEntity_assets"."deviceId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceId",
"SharedLinkEntity__SharedLinkEntity_assets"."type" AS "SharedLinkEntity__SharedLinkEntity_assets_type",
"SharedLinkEntity__SharedLinkEntity_assets"."originalPath" AS "SharedLinkEntity__SharedLinkEntity_assets_originalPath",
"SharedLinkEntity__SharedLinkEntity_assets"."resizePath" AS "SharedLinkEntity__SharedLinkEntity_assets_resizePath",
"SharedLinkEntity__SharedLinkEntity_assets"."webpPath" AS "SharedLinkEntity__SharedLinkEntity_assets_webpPath",
"SharedLinkEntity__SharedLinkEntity_assets"."thumbhash" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbhash",
"SharedLinkEntity__SharedLinkEntity_assets"."encodedVideoPath" AS "SharedLinkEntity__SharedLinkEntity_assets_encodedVideoPath",
"SharedLinkEntity__SharedLinkEntity_assets"."createdAt" AS "SharedLinkEntity__SharedLinkEntity_assets_createdAt",
"SharedLinkEntity__SharedLinkEntity_assets"."updatedAt" AS "SharedLinkEntity__SharedLinkEntity_assets_updatedAt",
"SharedLinkEntity__SharedLinkEntity_assets"."deletedAt" AS "SharedLinkEntity__SharedLinkEntity_assets_deletedAt",
"SharedLinkEntity__SharedLinkEntity_assets"."fileCreatedAt" AS "SharedLinkEntity__SharedLinkEntity_assets_fileCreatedAt",
"SharedLinkEntity__SharedLinkEntity_assets"."localDateTime" AS "SharedLinkEntity__SharedLinkEntity_assets_localDateTime",
"SharedLinkEntity__SharedLinkEntity_assets"."fileModifiedAt" AS "SharedLinkEntity__SharedLinkEntity_assets_fileModifiedAt",
"SharedLinkEntity__SharedLinkEntity_assets"."isFavorite" AS "SharedLinkEntity__SharedLinkEntity_assets_isFavorite",
"SharedLinkEntity__SharedLinkEntity_assets"."isArchived" AS "SharedLinkEntity__SharedLinkEntity_assets_isArchived",
"SharedLinkEntity__SharedLinkEntity_assets"."isExternal" AS "SharedLinkEntity__SharedLinkEntity_assets_isExternal",
"SharedLinkEntity__SharedLinkEntity_assets"."isReadOnly" AS "SharedLinkEntity__SharedLinkEntity_assets_isReadOnly",
"SharedLinkEntity__SharedLinkEntity_assets"."isOffline" AS "SharedLinkEntity__SharedLinkEntity_assets_isOffline",
"SharedLinkEntity__SharedLinkEntity_assets"."checksum" AS "SharedLinkEntity__SharedLinkEntity_assets_checksum",
"SharedLinkEntity__SharedLinkEntity_assets"."duration" AS "SharedLinkEntity__SharedLinkEntity_assets_duration",
"SharedLinkEntity__SharedLinkEntity_assets"."isVisible" AS "SharedLinkEntity__SharedLinkEntity_assets_isVisible",
"SharedLinkEntity__SharedLinkEntity_assets"."livePhotoVideoId" AS "SharedLinkEntity__SharedLinkEntity_assets_livePhotoVideoId",
"SharedLinkEntity__SharedLinkEntity_assets"."originalFileName" AS "SharedLinkEntity__SharedLinkEntity_assets_originalFileName",
"SharedLinkEntity__SharedLinkEntity_assets"."sidecarPath" AS "SharedLinkEntity__SharedLinkEntity_assets_sidecarPath",
"SharedLinkEntity__SharedLinkEntity_assets"."stackParentId" AS "SharedLinkEntity__SharedLinkEntity_assets_stackParentId",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."assetId" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_assetId",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."description" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_description",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."exifImageWidth" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_exifImageWidth",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."exifImageHeight" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_exifImageHeight",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."fileSizeInByte" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_fileSizeInByte",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."orientation" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_orientation",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."dateTimeOriginal" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_dateTimeOriginal",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."modifyDate" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_modifyDate",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."timeZone" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_timeZone",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."latitude" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_latitude",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."longitude" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_longitude",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."projectionType" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_projectionType",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."city" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_city",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."livePhotoCID" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_livePhotoCID",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."state" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_state",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."country" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_country",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."make" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_make",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."model" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_model",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."lensModel" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_lensModel",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."fNumber" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_fNumber",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."focalLength" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_focalLength",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."iso" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_iso",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."exposureTime" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_exposureTime",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."profileDescription" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_profileDescription",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."colorspace" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_colorspace",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."bitsPerSample" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_bitsPerSample",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."fps" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_fps",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."exifTextSearchableColumn" AS "e18de9deffa83f81ac3c43b5e8c2f08dba727bf8",
"SharedLinkEntity__SharedLinkEntity_album"."id" AS "SharedLinkEntity__SharedLinkEntity_album_id",
"SharedLinkEntity__SharedLinkEntity_album"."ownerId" AS "SharedLinkEntity__SharedLinkEntity_album_ownerId",
"SharedLinkEntity__SharedLinkEntity_album"."albumName" AS "SharedLinkEntity__SharedLinkEntity_album_albumName",
"SharedLinkEntity__SharedLinkEntity_album"."description" AS "SharedLinkEntity__SharedLinkEntity_album_description",
"SharedLinkEntity__SharedLinkEntity_album"."createdAt" AS "SharedLinkEntity__SharedLinkEntity_album_createdAt",
"SharedLinkEntity__SharedLinkEntity_album"."updatedAt" AS "SharedLinkEntity__SharedLinkEntity_album_updatedAt",
"SharedLinkEntity__SharedLinkEntity_album"."deletedAt" AS "SharedLinkEntity__SharedLinkEntity_album_deletedAt",
"SharedLinkEntity__SharedLinkEntity_album"."albumThumbnailAssetId" AS "SharedLinkEntity__SharedLinkEntity_album_albumThumbnailAssetId",
"SharedLinkEntity__SharedLinkEntity_album"."isActivityEnabled" AS "SharedLinkEntity__SharedLinkEntity_album_isActivityEnabled",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."id" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_id",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."deviceAssetId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_deviceAssetId",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."ownerId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_ownerId",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."libraryId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_libraryId",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."deviceId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_deviceId",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."type" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_type",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."originalPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_originalPath",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."resizePath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_resizePath",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."webpPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_webpPath",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."thumbhash" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_thumbhash",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."encodedVideoPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_encodedVideoPath",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."createdAt" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_createdAt",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."updatedAt" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_updatedAt",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."deletedAt" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_deletedAt",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."fileCreatedAt" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_fileCreatedAt",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."localDateTime" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_localDateTime",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."fileModifiedAt" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_fileModifiedAt",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."isFavorite" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_isFavorite",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."isArchived" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_isArchived",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."isExternal" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_isExternal",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."isReadOnly" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_isReadOnly",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."isOffline" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_isOffline",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."checksum" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_checksum",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."duration" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_duration",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."isVisible" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_isVisible",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."livePhotoVideoId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_livePhotoVideoId",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."originalFileName" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_originalFileName",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."sidecarPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_sidecarPath",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."stackParentId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_stackParentId",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."assetId" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_assetId",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."description" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_description",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."exifImageWidth" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_exifImageWidth",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."exifImageHeight" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_exifImageHeight",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."fileSizeInByte" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_fileSizeInByte",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."orientation" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_orientation",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."dateTimeOriginal" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_dateTimeOriginal",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."modifyDate" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_modifyDate",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."timeZone" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_timeZone",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."latitude" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_latitude",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."longitude" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_longitude",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."projectionType" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_projectionType",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."city" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_city",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."livePhotoCID" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_livePhotoCID",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."state" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_state",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."country" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_country",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."make" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_make",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."model" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_model",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."lensModel" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_lensModel",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."fNumber" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_fNumber",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."focalLength" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_focalLength",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."iso" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_iso",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."exposureTime" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_exposureTime",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."profileDescription" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_profileDescription",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."colorspace" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_colorspace",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."bitsPerSample" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_bitsPerSample",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."fps" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_fps",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."exifTextSearchableColumn" AS "96535c8046de591cca9b8c5825e6c5db502b0e6a",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."id" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_id",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."name" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_name",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."avatarColor" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_avatarColor",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."isAdmin" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_isAdmin",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."email" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_email",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."storageLabel" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_storageLabel",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."externalPath" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_externalPath",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."oauthId" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_oauthId",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."profileImagePath" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_profileImagePath",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."shouldChangePassword" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_shouldChangePassword",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."createdAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_createdAt",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."deletedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_deletedAt",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."updatedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_updatedAt",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."memoriesEnabled" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_memoriesEnabled"
FROM
"shared_links" "SharedLinkEntity"
LEFT JOIN "shared_link__asset" "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity" ON "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity"."sharedLinksId" = "SharedLinkEntity"."id"
LEFT JOIN "assets" "SharedLinkEntity__SharedLinkEntity_assets" ON "SharedLinkEntity__SharedLinkEntity_assets"."id" = "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity"."assetsId"
AND (
"SharedLinkEntity__SharedLinkEntity_assets"."deletedAt" IS NULL
)
LEFT JOIN "exif" "9b1d35b344d838023994a3233afd6ffe098be6d8" ON "9b1d35b344d838023994a3233afd6ffe098be6d8"."assetId" = "SharedLinkEntity__SharedLinkEntity_assets"."id"
LEFT JOIN "albums" "SharedLinkEntity__SharedLinkEntity_album" ON "SharedLinkEntity__SharedLinkEntity_album"."id" = "SharedLinkEntity"."albumId"
AND (
"SharedLinkEntity__SharedLinkEntity_album"."deletedAt" IS NULL
)
LEFT JOIN "albums_assets_assets" "760f12c00d97bdcec1ce224d1e3bf449859942b6" ON "760f12c00d97bdcec1ce224d1e3bf449859942b6"."albumsId" = "SharedLinkEntity__SharedLinkEntity_album"."id"
LEFT JOIN "assets" "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6" ON "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."id" = "760f12c00d97bdcec1ce224d1e3bf449859942b6"."assetsId"
AND (
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."deletedAt" IS NULL
)
LEFT JOIN "exif" "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f" ON "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."assetId" = "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."id"
LEFT JOIN "users" "6d7fd45329a05fd86b3dbcacde87fe76e33a422d" ON "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."id" = "SharedLinkEntity__SharedLinkEntity_album"."ownerId"
AND (
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."deletedAt" IS NULL
)
WHERE
(
"SharedLinkEntity"."id" = $1
AND "SharedLinkEntity"."userId" = $2
)
) "distinctAlias"
ORDER BY
"distinctAlias"."SharedLinkEntity_createdAt" DESC,
"distinctAlias"."SharedLinkEntity__SharedLinkEntity_assets_fileCreatedAt" ASC,
"distinctAlias"."4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_fileCreatedAt" ASC,
"SharedLinkEntity_id" ASC
LIMIT
1
-- SharedLinkRepository.getAll
SELECT
"SharedLinkEntity"."id" AS "SharedLinkEntity_id",
"SharedLinkEntity"."description" AS "SharedLinkEntity_description",
"SharedLinkEntity"."password" AS "SharedLinkEntity_password",
"SharedLinkEntity"."userId" AS "SharedLinkEntity_userId",
"SharedLinkEntity"."key" AS "SharedLinkEntity_key",
"SharedLinkEntity"."type" AS "SharedLinkEntity_type",
"SharedLinkEntity"."createdAt" AS "SharedLinkEntity_createdAt",
"SharedLinkEntity"."expiresAt" AS "SharedLinkEntity_expiresAt",
"SharedLinkEntity"."allowUpload" AS "SharedLinkEntity_allowUpload",
"SharedLinkEntity"."allowDownload" AS "SharedLinkEntity_allowDownload",
"SharedLinkEntity"."showExif" AS "SharedLinkEntity_showExif",
"SharedLinkEntity"."albumId" AS "SharedLinkEntity_albumId",
"SharedLinkEntity__SharedLinkEntity_assets"."id" AS "SharedLinkEntity__SharedLinkEntity_assets_id",
"SharedLinkEntity__SharedLinkEntity_assets"."deviceAssetId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceAssetId",
"SharedLinkEntity__SharedLinkEntity_assets"."ownerId" AS "SharedLinkEntity__SharedLinkEntity_assets_ownerId",
"SharedLinkEntity__SharedLinkEntity_assets"."libraryId" AS "SharedLinkEntity__SharedLinkEntity_assets_libraryId",
"SharedLinkEntity__SharedLinkEntity_assets"."deviceId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceId",
"SharedLinkEntity__SharedLinkEntity_assets"."type" AS "SharedLinkEntity__SharedLinkEntity_assets_type",
"SharedLinkEntity__SharedLinkEntity_assets"."originalPath" AS "SharedLinkEntity__SharedLinkEntity_assets_originalPath",
"SharedLinkEntity__SharedLinkEntity_assets"."resizePath" AS "SharedLinkEntity__SharedLinkEntity_assets_resizePath",
"SharedLinkEntity__SharedLinkEntity_assets"."webpPath" AS "SharedLinkEntity__SharedLinkEntity_assets_webpPath",
"SharedLinkEntity__SharedLinkEntity_assets"."thumbhash" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbhash",
"SharedLinkEntity__SharedLinkEntity_assets"."encodedVideoPath" AS "SharedLinkEntity__SharedLinkEntity_assets_encodedVideoPath",
"SharedLinkEntity__SharedLinkEntity_assets"."createdAt" AS "SharedLinkEntity__SharedLinkEntity_assets_createdAt",
"SharedLinkEntity__SharedLinkEntity_assets"."updatedAt" AS "SharedLinkEntity__SharedLinkEntity_assets_updatedAt",
"SharedLinkEntity__SharedLinkEntity_assets"."deletedAt" AS "SharedLinkEntity__SharedLinkEntity_assets_deletedAt",
"SharedLinkEntity__SharedLinkEntity_assets"."fileCreatedAt" AS "SharedLinkEntity__SharedLinkEntity_assets_fileCreatedAt",
"SharedLinkEntity__SharedLinkEntity_assets"."localDateTime" AS "SharedLinkEntity__SharedLinkEntity_assets_localDateTime",
"SharedLinkEntity__SharedLinkEntity_assets"."fileModifiedAt" AS "SharedLinkEntity__SharedLinkEntity_assets_fileModifiedAt",
"SharedLinkEntity__SharedLinkEntity_assets"."isFavorite" AS "SharedLinkEntity__SharedLinkEntity_assets_isFavorite",
"SharedLinkEntity__SharedLinkEntity_assets"."isArchived" AS "SharedLinkEntity__SharedLinkEntity_assets_isArchived",
"SharedLinkEntity__SharedLinkEntity_assets"."isExternal" AS "SharedLinkEntity__SharedLinkEntity_assets_isExternal",
"SharedLinkEntity__SharedLinkEntity_assets"."isReadOnly" AS "SharedLinkEntity__SharedLinkEntity_assets_isReadOnly",
"SharedLinkEntity__SharedLinkEntity_assets"."isOffline" AS "SharedLinkEntity__SharedLinkEntity_assets_isOffline",
"SharedLinkEntity__SharedLinkEntity_assets"."checksum" AS "SharedLinkEntity__SharedLinkEntity_assets_checksum",
"SharedLinkEntity__SharedLinkEntity_assets"."duration" AS "SharedLinkEntity__SharedLinkEntity_assets_duration",
"SharedLinkEntity__SharedLinkEntity_assets"."isVisible" AS "SharedLinkEntity__SharedLinkEntity_assets_isVisible",
"SharedLinkEntity__SharedLinkEntity_assets"."livePhotoVideoId" AS "SharedLinkEntity__SharedLinkEntity_assets_livePhotoVideoId",
"SharedLinkEntity__SharedLinkEntity_assets"."originalFileName" AS "SharedLinkEntity__SharedLinkEntity_assets_originalFileName",
"SharedLinkEntity__SharedLinkEntity_assets"."sidecarPath" AS "SharedLinkEntity__SharedLinkEntity_assets_sidecarPath",
"SharedLinkEntity__SharedLinkEntity_assets"."stackParentId" AS "SharedLinkEntity__SharedLinkEntity_assets_stackParentId",
"SharedLinkEntity__SharedLinkEntity_album"."id" AS "SharedLinkEntity__SharedLinkEntity_album_id",
"SharedLinkEntity__SharedLinkEntity_album"."ownerId" AS "SharedLinkEntity__SharedLinkEntity_album_ownerId",
"SharedLinkEntity__SharedLinkEntity_album"."albumName" AS "SharedLinkEntity__SharedLinkEntity_album_albumName",
"SharedLinkEntity__SharedLinkEntity_album"."description" AS "SharedLinkEntity__SharedLinkEntity_album_description",
"SharedLinkEntity__SharedLinkEntity_album"."createdAt" AS "SharedLinkEntity__SharedLinkEntity_album_createdAt",
"SharedLinkEntity__SharedLinkEntity_album"."updatedAt" AS "SharedLinkEntity__SharedLinkEntity_album_updatedAt",
"SharedLinkEntity__SharedLinkEntity_album"."deletedAt" AS "SharedLinkEntity__SharedLinkEntity_album_deletedAt",
"SharedLinkEntity__SharedLinkEntity_album"."albumThumbnailAssetId" AS "SharedLinkEntity__SharedLinkEntity_album_albumThumbnailAssetId",
"SharedLinkEntity__SharedLinkEntity_album"."isActivityEnabled" AS "SharedLinkEntity__SharedLinkEntity_album_isActivityEnabled",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."id" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_id",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."name" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_name",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."avatarColor" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_avatarColor",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."isAdmin" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_isAdmin",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."email" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_email",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."storageLabel" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_storageLabel",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."externalPath" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_externalPath",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."oauthId" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_oauthId",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."profileImagePath" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_profileImagePath",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."shouldChangePassword" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_shouldChangePassword",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."createdAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_createdAt",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."deletedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_deletedAt",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."updatedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_updatedAt",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."memoriesEnabled" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_memoriesEnabled"
FROM
"shared_links" "SharedLinkEntity"
LEFT JOIN "shared_link__asset" "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity" ON "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity"."sharedLinksId" = "SharedLinkEntity"."id"
LEFT JOIN "assets" "SharedLinkEntity__SharedLinkEntity_assets" ON "SharedLinkEntity__SharedLinkEntity_assets"."id" = "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity"."assetsId"
AND (
"SharedLinkEntity__SharedLinkEntity_assets"."deletedAt" IS NULL
)
LEFT JOIN "albums" "SharedLinkEntity__SharedLinkEntity_album" ON "SharedLinkEntity__SharedLinkEntity_album"."id" = "SharedLinkEntity"."albumId"
AND (
"SharedLinkEntity__SharedLinkEntity_album"."deletedAt" IS NULL
)
LEFT JOIN "users" "6d7fd45329a05fd86b3dbcacde87fe76e33a422d" ON "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."id" = "SharedLinkEntity__SharedLinkEntity_album"."ownerId"
AND (
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."deletedAt" IS NULL
)
WHERE
("SharedLinkEntity"."userId" = $1)
ORDER BY
"SharedLinkEntity"."createdAt" DESC
-- SharedLinkRepository.getByKey
SELECT DISTINCT
"distinctAlias"."SharedLinkEntity_id" AS "ids_SharedLinkEntity_id"
FROM
(
SELECT
"SharedLinkEntity"."id" AS "SharedLinkEntity_id",
"SharedLinkEntity"."description" AS "SharedLinkEntity_description",
"SharedLinkEntity"."password" AS "SharedLinkEntity_password",
"SharedLinkEntity"."userId" AS "SharedLinkEntity_userId",
"SharedLinkEntity"."key" AS "SharedLinkEntity_key",
"SharedLinkEntity"."type" AS "SharedLinkEntity_type",
"SharedLinkEntity"."createdAt" AS "SharedLinkEntity_createdAt",
"SharedLinkEntity"."expiresAt" AS "SharedLinkEntity_expiresAt",
"SharedLinkEntity"."allowUpload" AS "SharedLinkEntity_allowUpload",
"SharedLinkEntity"."allowDownload" AS "SharedLinkEntity_allowDownload",
"SharedLinkEntity"."showExif" AS "SharedLinkEntity_showExif",
"SharedLinkEntity"."albumId" AS "SharedLinkEntity_albumId",
"SharedLinkEntity__SharedLinkEntity_user"."id" AS "SharedLinkEntity__SharedLinkEntity_user_id",
"SharedLinkEntity__SharedLinkEntity_user"."name" AS "SharedLinkEntity__SharedLinkEntity_user_name",
"SharedLinkEntity__SharedLinkEntity_user"."avatarColor" AS "SharedLinkEntity__SharedLinkEntity_user_avatarColor",
"SharedLinkEntity__SharedLinkEntity_user"."isAdmin" AS "SharedLinkEntity__SharedLinkEntity_user_isAdmin",
"SharedLinkEntity__SharedLinkEntity_user"."email" AS "SharedLinkEntity__SharedLinkEntity_user_email",
"SharedLinkEntity__SharedLinkEntity_user"."storageLabel" AS "SharedLinkEntity__SharedLinkEntity_user_storageLabel",
"SharedLinkEntity__SharedLinkEntity_user"."externalPath" AS "SharedLinkEntity__SharedLinkEntity_user_externalPath",
"SharedLinkEntity__SharedLinkEntity_user"."oauthId" AS "SharedLinkEntity__SharedLinkEntity_user_oauthId",
"SharedLinkEntity__SharedLinkEntity_user"."profileImagePath" AS "SharedLinkEntity__SharedLinkEntity_user_profileImagePath",
"SharedLinkEntity__SharedLinkEntity_user"."shouldChangePassword" AS "SharedLinkEntity__SharedLinkEntity_user_shouldChangePassword",
"SharedLinkEntity__SharedLinkEntity_user"."createdAt" AS "SharedLinkEntity__SharedLinkEntity_user_createdAt",
"SharedLinkEntity__SharedLinkEntity_user"."deletedAt" AS "SharedLinkEntity__SharedLinkEntity_user_deletedAt",
"SharedLinkEntity__SharedLinkEntity_user"."updatedAt" AS "SharedLinkEntity__SharedLinkEntity_user_updatedAt",
"SharedLinkEntity__SharedLinkEntity_user"."memoriesEnabled" AS "SharedLinkEntity__SharedLinkEntity_user_memoriesEnabled"
FROM
"shared_links" "SharedLinkEntity"
LEFT JOIN "users" "SharedLinkEntity__SharedLinkEntity_user" ON "SharedLinkEntity__SharedLinkEntity_user"."id" = "SharedLinkEntity"."userId"
AND (
"SharedLinkEntity__SharedLinkEntity_user"."deletedAt" IS NULL
)
WHERE
("SharedLinkEntity"."key" = $1)
) "distinctAlias"
ORDER BY
"SharedLinkEntity_id" ASC
LIMIT
1

View file

@ -0,0 +1,13 @@
-- NOTE: This file is auto generated by ./sql-generator
-- SystemConfigRepository.load
SELECT
"SystemConfigEntity"."key" AS "SystemConfigEntity_key",
"SystemConfigEntity"."value" AS "SystemConfigEntity_value"
FROM
"system_config" "SystemConfigEntity"
-- SystemConfigRepository.deleteKeys
DELETE FROM "system_config"
WHERE
"key" IN ($1, $2, $3, $4, $5, $6, $7, $8, $9)

View file

@ -0,0 +1 @@
-- NOTE: This file is auto generated by ./sql-generator

View file

@ -0,0 +1 @@
-- NOTE: This file is auto generated by ./sql-generator

View file

@ -0,0 +1,143 @@
-- NOTE: This file is auto generated by ./sql-generator
-- UserRepository.getAdmin
SELECT
"UserEntity"."id" AS "UserEntity_id",
"UserEntity"."name" AS "UserEntity_name",
"UserEntity"."avatarColor" AS "UserEntity_avatarColor",
"UserEntity"."isAdmin" AS "UserEntity_isAdmin",
"UserEntity"."email" AS "UserEntity_email",
"UserEntity"."storageLabel" AS "UserEntity_storageLabel",
"UserEntity"."externalPath" AS "UserEntity_externalPath",
"UserEntity"."oauthId" AS "UserEntity_oauthId",
"UserEntity"."profileImagePath" AS "UserEntity_profileImagePath",
"UserEntity"."shouldChangePassword" AS "UserEntity_shouldChangePassword",
"UserEntity"."createdAt" AS "UserEntity_createdAt",
"UserEntity"."deletedAt" AS "UserEntity_deletedAt",
"UserEntity"."updatedAt" AS "UserEntity_updatedAt",
"UserEntity"."memoriesEnabled" AS "UserEntity_memoriesEnabled"
FROM
"users" "UserEntity"
WHERE
(("UserEntity"."isAdmin" = $1))
AND ("UserEntity"."deletedAt" IS NULL)
LIMIT
1
-- UserRepository.hasAdmin
SELECT
1 AS "row_exists"
FROM
(
SELECT
1 AS dummy_column
) "dummy_table"
WHERE
EXISTS (
SELECT
1
FROM
"users" "UserEntity"
WHERE
(("UserEntity"."isAdmin" = $1))
AND ("UserEntity"."deletedAt" IS NULL)
)
LIMIT
1
-- UserRepository.getByEmail
SELECT
"user"."id" AS "user_id",
"user"."name" AS "user_name",
"user"."avatarColor" AS "user_avatarColor",
"user"."isAdmin" AS "user_isAdmin",
"user"."email" AS "user_email",
"user"."storageLabel" AS "user_storageLabel",
"user"."externalPath" AS "user_externalPath",
"user"."oauthId" AS "user_oauthId",
"user"."profileImagePath" AS "user_profileImagePath",
"user"."shouldChangePassword" AS "user_shouldChangePassword",
"user"."createdAt" AS "user_createdAt",
"user"."deletedAt" AS "user_deletedAt",
"user"."updatedAt" AS "user_updatedAt",
"user"."memoriesEnabled" AS "user_memoriesEnabled"
FROM
"users" "user"
WHERE
("user"."email" = $1)
AND ("user"."deletedAt" IS NULL)
-- UserRepository.getByStorageLabel
SELECT
"UserEntity"."id" AS "UserEntity_id",
"UserEntity"."name" AS "UserEntity_name",
"UserEntity"."avatarColor" AS "UserEntity_avatarColor",
"UserEntity"."isAdmin" AS "UserEntity_isAdmin",
"UserEntity"."email" AS "UserEntity_email",
"UserEntity"."storageLabel" AS "UserEntity_storageLabel",
"UserEntity"."externalPath" AS "UserEntity_externalPath",
"UserEntity"."oauthId" AS "UserEntity_oauthId",
"UserEntity"."profileImagePath" AS "UserEntity_profileImagePath",
"UserEntity"."shouldChangePassword" AS "UserEntity_shouldChangePassword",
"UserEntity"."createdAt" AS "UserEntity_createdAt",
"UserEntity"."deletedAt" AS "UserEntity_deletedAt",
"UserEntity"."updatedAt" AS "UserEntity_updatedAt",
"UserEntity"."memoriesEnabled" AS "UserEntity_memoriesEnabled"
FROM
"users" "UserEntity"
WHERE
(("UserEntity"."storageLabel" = $1))
AND ("UserEntity"."deletedAt" IS NULL)
LIMIT
1
-- UserRepository.getByOAuthId
SELECT
"UserEntity"."id" AS "UserEntity_id",
"UserEntity"."name" AS "UserEntity_name",
"UserEntity"."avatarColor" AS "UserEntity_avatarColor",
"UserEntity"."isAdmin" AS "UserEntity_isAdmin",
"UserEntity"."email" AS "UserEntity_email",
"UserEntity"."storageLabel" AS "UserEntity_storageLabel",
"UserEntity"."externalPath" AS "UserEntity_externalPath",
"UserEntity"."oauthId" AS "UserEntity_oauthId",
"UserEntity"."profileImagePath" AS "UserEntity_profileImagePath",
"UserEntity"."shouldChangePassword" AS "UserEntity_shouldChangePassword",
"UserEntity"."createdAt" AS "UserEntity_createdAt",
"UserEntity"."deletedAt" AS "UserEntity_deletedAt",
"UserEntity"."updatedAt" AS "UserEntity_updatedAt",
"UserEntity"."memoriesEnabled" AS "UserEntity_memoriesEnabled"
FROM
"users" "UserEntity"
WHERE
(("UserEntity"."oauthId" = $1))
AND ("UserEntity"."deletedAt" IS NULL)
LIMIT
1
-- UserRepository.getUserStats
SELECT
"users"."id" AS "userId",
"users"."name" AS "userName",
COUNT("assets"."id") FILTER (
WHERE
"assets"."type" = 'IMAGE'
AND "assets"."isVisible"
) AS "photos",
COUNT("assets"."id") FILTER (
WHERE
"assets"."type" = 'VIDEO'
AND "assets"."isVisible"
) AS "videos",
COALESCE(SUM("exif"."fileSizeInByte"), 0) AS "usage"
FROM
"users" "users"
LEFT JOIN "assets" "assets" ON "assets"."ownerId" = "users"."id"
AND ("assets"."deletedAt" IS NULL)
LEFT JOIN "exif" "exif" ON "exif"."assetId" = "assets"."id"
WHERE
"users"."deletedAt" IS NULL
GROUP BY
"users"."id"
ORDER BY
"users"."createdAt" ASC

View file

@ -0,0 +1,46 @@
-- NOTE: This file is auto generated by ./sql-generator
-- UserTokenRepository.getByToken
SELECT DISTINCT
"distinctAlias"."UserTokenEntity_id" AS "ids_UserTokenEntity_id"
FROM
(
SELECT
"UserTokenEntity"."id" AS "UserTokenEntity_id",
"UserTokenEntity"."userId" AS "UserTokenEntity_userId",
"UserTokenEntity"."createdAt" AS "UserTokenEntity_createdAt",
"UserTokenEntity"."updatedAt" AS "UserTokenEntity_updatedAt",
"UserTokenEntity"."deviceType" AS "UserTokenEntity_deviceType",
"UserTokenEntity"."deviceOS" AS "UserTokenEntity_deviceOS",
"UserTokenEntity__UserTokenEntity_user"."id" AS "UserTokenEntity__UserTokenEntity_user_id",
"UserTokenEntity__UserTokenEntity_user"."name" AS "UserTokenEntity__UserTokenEntity_user_name",
"UserTokenEntity__UserTokenEntity_user"."avatarColor" AS "UserTokenEntity__UserTokenEntity_user_avatarColor",
"UserTokenEntity__UserTokenEntity_user"."isAdmin" AS "UserTokenEntity__UserTokenEntity_user_isAdmin",
"UserTokenEntity__UserTokenEntity_user"."email" AS "UserTokenEntity__UserTokenEntity_user_email",
"UserTokenEntity__UserTokenEntity_user"."storageLabel" AS "UserTokenEntity__UserTokenEntity_user_storageLabel",
"UserTokenEntity__UserTokenEntity_user"."externalPath" AS "UserTokenEntity__UserTokenEntity_user_externalPath",
"UserTokenEntity__UserTokenEntity_user"."oauthId" AS "UserTokenEntity__UserTokenEntity_user_oauthId",
"UserTokenEntity__UserTokenEntity_user"."profileImagePath" AS "UserTokenEntity__UserTokenEntity_user_profileImagePath",
"UserTokenEntity__UserTokenEntity_user"."shouldChangePassword" AS "UserTokenEntity__UserTokenEntity_user_shouldChangePassword",
"UserTokenEntity__UserTokenEntity_user"."createdAt" AS "UserTokenEntity__UserTokenEntity_user_createdAt",
"UserTokenEntity__UserTokenEntity_user"."deletedAt" AS "UserTokenEntity__UserTokenEntity_user_deletedAt",
"UserTokenEntity__UserTokenEntity_user"."updatedAt" AS "UserTokenEntity__UserTokenEntity_user_updatedAt",
"UserTokenEntity__UserTokenEntity_user"."memoriesEnabled" AS "UserTokenEntity__UserTokenEntity_user_memoriesEnabled"
FROM
"user_token" "UserTokenEntity"
LEFT JOIN "users" "UserTokenEntity__UserTokenEntity_user" ON "UserTokenEntity__UserTokenEntity_user"."id" = "UserTokenEntity"."userId"
AND (
"UserTokenEntity__UserTokenEntity_user"."deletedAt" IS NULL
)
WHERE
("UserTokenEntity"."token" = $1)
) "distinctAlias"
ORDER BY
"UserTokenEntity_id" ASC
LIMIT
1
-- UserTokenRepository.delete
DELETE FROM "user_token"
WHERE
"id" = $1

View file

@ -1,5 +1,5 @@
import { bootstrap as adminCli } from './admin-cli/main';
import { bootstrap as immich } from './immich/main';
import { bootstrap as admin } from './immich-admin/main';
import { bootstrap as server } from './immich/main';
import { bootstrap as microservices } from './microservices/main';
const immichApp = process.argv[2] || process.env.IMMICH_APP;
@ -12,13 +12,13 @@ function bootstrap() {
switch (immichApp) {
case 'immich':
process.title = 'immich_server';
return immich();
return server();
case 'microservices':
process.title = 'immich_microservices';
return microservices();
case 'admin-cli':
case 'immich-admin':
process.title = 'immich_admin_cli';
return adminCli();
return admin();
default:
console.log(`Invalid app name: ${immichApp}. Expected one of immich|microservices|cli`);
process.exit(1);

View file

@ -84,6 +84,7 @@ export class AppService {
[JobName.QUEUE_SIDECAR]: (data) => this.metadataService.handleQueueSidecar(data),
[JobName.SIDECAR_DISCOVERY]: (data) => this.metadataService.handleSidecarDiscovery(data),
[JobName.SIDECAR_SYNC]: () => this.metadataService.handleSidecarSync(),
[JobName.SIDECAR_WRITE]: (data) => this.metadataService.handleSidecarWrite(data),
[JobName.LIBRARY_SCAN_ASSET]: (data) => this.libraryService.handleAssetRefresh(data),
[JobName.LIBRARY_SCAN]: (data) => this.libraryService.handleQueueAssetRefresh(data),
[JobName.LIBRARY_DELETE]: (data) => this.libraryService.handleDeleteLibrary(data),

View file

@ -32,4 +32,4 @@ if [ "$REDIS_PASSWORD_FILE" ]; then
unset REDIS_PASSWORD_FILE
fi
exec node dist/main $@
exec node /usr/src/app/dist/main $@

View file

@ -700,6 +700,54 @@ describe(`${AssetController.name} (e2e)`, () => {
expect(status).toEqual(200);
});
it('should update date time original', async () => {
const { status, body } = await request(server)
.put(`/asset/${asset1.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ dateTimeOriginal: '2023-11-19T18:11:00.000-07:00' });
expect(body).toMatchObject({
id: asset1.id,
exifInfo: expect.objectContaining({ dateTimeOriginal: '2023-11-20T01:11:00.000Z' }),
});
expect(status).toEqual(200);
});
it('should reject invalid gps coordinates', async () => {
for (const test of [
{ latitude: 12 },
{ longitude: 12 },
{ latitude: 12, longitude: 'abc' },
{ latitude: 'abc', longitude: 12 },
{ latitude: null, longitude: 12 },
{ latitude: 12, longitude: null },
{ latitude: 91, longitude: 12 },
{ latitude: -91, longitude: 12 },
{ latitude: 12, longitude: -181 },
{ latitude: 12, longitude: 181 },
]) {
const { status, body } = await request(server)
.put(`/asset/${asset1.id}`)
.send(test)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorStub.badRequest());
}
});
it('should update gps data', async () => {
const { status, body } = await request(server)
.put(`/asset/${asset1.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ latitude: 12, longitude: 12 });
expect(body).toMatchObject({
id: asset1.id,
exifInfo: expect.objectContaining({ latitude: 12, longitude: 12 }),
});
expect(status).toEqual(200);
});
it('should set the description', async () => {
const { status, body } = await request(server)
.put(`/asset/${asset1.id}`)

View file

@ -22,11 +22,12 @@ export const newAccessRepositoryMock = (reset = true): IAccessRepositoryMock =>
hasAlbumOwnerAccess: jest.fn(),
hasCreateAccess: jest.fn(),
},
asset: {
hasOwnerAccess: jest.fn(),
hasAlbumAccess: jest.fn(),
hasPartnerAccess: jest.fn(),
hasSharedLinkAccess: jest.fn(),
checkOwnerAccess: jest.fn().mockResolvedValue(new Set()),
checkAlbumAccess: jest.fn().mockResolvedValue(new Set()),
checkPartnerAccess: jest.fn().mockResolvedValue(new Set()),
checkSharedLinkAccess: jest.fn().mockResolvedValue(new Set()),
},
album: {

View file

@ -2,9 +2,10 @@ import { IMetadataRepository } from '@app/domain';
export const newMetadataRepositoryMock = (): jest.Mocked<IMetadataRepository> => {
return {
getExifTags: jest.fn(),
init: jest.fn(),
teardown: jest.fn(),
reverseGeocode: jest.fn(),
readTags: jest.fn(),
writeTags: jest.fn(),
};
};

View file

@ -447,6 +447,12 @@ export interface AssetBulkDeleteDto {
* @interface AssetBulkUpdateDto
*/
export interface AssetBulkUpdateDto {
/**
*
* @type {string}
* @memberof AssetBulkUpdateDto
*/
'dateTimeOriginal'?: string;
/**
*
* @type {Array<string>}
@ -465,6 +471,18 @@ export interface AssetBulkUpdateDto {
* @memberof AssetBulkUpdateDto
*/
'isFavorite'?: boolean;
/**
*
* @type {number}
* @memberof AssetBulkUpdateDto
*/
'latitude'?: number;
/**
*
* @type {number}
* @memberof AssetBulkUpdateDto
*/
'longitude'?: number;
/**
*
* @type {boolean}
@ -4137,6 +4155,12 @@ export interface UpdateAlbumDto {
* @interface UpdateAssetDto
*/
export interface UpdateAssetDto {
/**
*
* @type {string}
* @memberof UpdateAssetDto
*/
'dateTimeOriginal'?: string;
/**
*
* @type {string}
@ -4155,6 +4179,18 @@ export interface UpdateAssetDto {
* @memberof UpdateAssetDto
*/
'isFavorite'?: boolean;
/**
*
* @type {number}
* @memberof UpdateAssetDto
*/
'latitude'?: number;
/**
*
* @type {number}
* @memberof UpdateAssetDto
*/
'longitude'?: number;
}
/**
*

View file

@ -20,6 +20,7 @@
import ThemeButton from '../shared-components/theme-button.svelte';
import { shouldIgnoreShortcut } from '$lib/utils/shortcut';
import { mdiFileImagePlusOutline, mdiFolderDownloadOutline } from '@mdi/js';
import UpdatePanel from '../shared-components/update-panel.svelte';
export let sharedLink: SharedLinkResponseDto;
export let user: UserResponseDto | undefined = undefined;
@ -143,7 +144,7 @@
<main
class="relative h-screen overflow-hidden bg-immich-bg px-6 pt-[var(--navbar-height)] dark:bg-immich-dark-bg sm:px-12 md:px-24 lg:px-40"
>
<AssetGrid {album} {user} {assetStore} {assetInteractionStore}>
<AssetGrid {album} {assetStore} {assetInteractionStore}>
<section class="pt-24">
<!-- ALBUM TITLE -->
<p
@ -167,4 +168,5 @@
</p>
</section>
</AssetGrid>
<UpdatePanel {assetStore} />
</main>

View file

@ -9,7 +9,6 @@
AssetTypeEnum,
ReactionType,
SharedLinkResponseDto,
UserResponseDto,
} from '@api';
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
import { fly } from 'svelte/transition';
@ -42,6 +41,7 @@
import { updateNumberOfComments } from '$lib/stores/activity.store';
import { SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
import SlideshowBar from './slideshow-bar.svelte';
import { user } from '$lib/stores/user.store';
export let assetStore: AssetStore | null = null;
export let asset: AssetResponseDto;
@ -51,7 +51,6 @@
export let force = false;
export let withStacked = false;
export let isShared = false;
export let user: UserResponseDto | null = null;
export let album: AlbumResponseDto | null = null;
let reactions: ActivityResponseDto[] = [];
@ -143,10 +142,10 @@
};
const getFavorite = async () => {
if (album && user) {
if (album && $user) {
try {
const { data } = await api.activityApi.getActivities({
userId: user.id,
userId: $user.id,
assetId: asset.id,
albumId: album.id,
type: ReactionType.Like,
@ -743,7 +742,7 @@
</div>
{/if}
{#if isShared && album && isShowActivity && user}
{#if isShared && album && isShowActivity && $user}
<div
transition:fly={{ duration: 150 }}
id="activity-panel"
@ -751,7 +750,7 @@
translate="yes"
>
<ActivityViewer
{user}
user={$user}
disabled={!album.isActivityEnabled}
assetType={asset.type}
albumOwnerId={album.ownerId}

View file

@ -5,22 +5,31 @@
import { getAssetFilename } from '$lib/utils/asset-utils';
import { AlbumResponseDto, AssetResponseDto, ThumbnailFormat, api } from '@api';
import { DateTime } from 'luxon';
import { createEventDispatcher } from 'svelte';
import { createEventDispatcher, onDestroy } from 'svelte';
import { slide } from 'svelte/transition';
import { asByteUnitString } from '../../utils/byte-units';
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
import UserAvatar from '../shared-components/user-avatar.svelte';
import ChangeDate from '$lib/components/shared-components/change-date.svelte';
import {
mdiCalendar,
mdiCameraIris,
mdiClose,
mdiPencil,
mdiImageOutline,
mdiMapMarkerOutline,
mdiInformationOutline,
mdiEye,
mdiEyeOff,
} from '@mdi/js';
import Icon from '$lib/components/elements/icon.svelte';
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
import Map from '../shared-components/map/map.svelte';
import { websocketStore } from '$lib/stores/websocket';
import { AppRoute } from '$lib/constants';
import ChangeLocation from '../shared-components/change-location.svelte';
import { handleError } from '../../utils/handle-error';
import { user } from '$lib/stores/user.store';
export let asset: AssetResponseDto;
export let albums: AlbumResponseDto[] = [];
@ -51,6 +60,17 @@
})();
$: people = asset.people || [];
$: showingHiddenPeople = false;
const unsubscribe = websocketStore.onAssetUpdate.subscribe((assetUpdate) => {
if (assetUpdate && assetUpdate.id === asset.id) {
asset = assetUpdate;
}
});
onDestroy(() => {
unsubscribe();
});
const dispatch = createEventDispatcher();
@ -79,9 +99,7 @@
try {
await api.assetApi.updateAsset({
id: asset.id,
updateAssetDto: {
description: description,
},
updateAssetDto: { description },
});
} catch (error) {
console.error(error);
@ -90,6 +108,35 @@
let showAssetPath = false;
const toggleAssetPath = () => (showAssetPath = !showAssetPath);
let isShowChangeDate = false;
async function handleConfirmChangeDate(dateTimeOriginal: string) {
isShowChangeDate = false;
try {
await api.assetApi.updateAsset({ id: asset.id, updateAssetDto: { dateTimeOriginal } });
} catch (error) {
handleError(error, 'Unable to change date');
}
}
let isShowChangeLocation = false;
async function handleConfirmChangeLocation(gps: { lng: number; lat: number }) {
isShowChangeLocation = false;
try {
await api.assetApi.updateAsset({
id: asset.id,
updateAssetDto: {
latitude: gps.lat,
longitude: gps.lng,
},
});
} catch (error) {
handleError(error, 'Unable to change location');
}
}
</script>
<section class="p-2 dark:bg-immich-dark-bg dark:text-immich-dark-fg">
@ -134,25 +181,38 @@
{#if !api.isSharedLink && people.length > 0}
<section class="px-4 py-4 text-sm">
<h2>PEOPLE</h2>
<div class="flex h-10 w-full items-center justify-between">
<h2>PEOPLE</h2>
{#if people.some((person) => person.isHidden)}
<CircleIconButton
title="Show hidden people"
icon={showingHiddenPeople ? mdiEyeOff : mdiEye}
padding="1"
on:click={() => (showingHiddenPeople = !showingHiddenPeople)}
/>
{/if}
</div>
<div class="mt-4 flex flex-wrap gap-2">
<div class="mt-2 flex flex-wrap gap-2">
{#each people as person (person.id)}
<a
href="/people/{person.id}?previousRoute={albumId ? `${AppRoute.ALBUMS}/${albumId}` : AppRoute.PHOTOS}"
class="w-[90px]"
class="w-[90px] {!showingHiddenPeople && person.isHidden ? 'hidden' : ''}"
on:click={() => dispatch('close-viewer')}
>
<ImageThumbnail
curve
shadow
url={api.getPeopleThumbnailUrl(person.id)}
altText={person.name}
title={person.name}
widthStyle="90px"
heightStyle="90px"
thumbhash={null}
/>
<div class="relative">
<ImageThumbnail
curve
shadow
url={api.getPeopleThumbnailUrl(person.id)}
altText={person.name}
title={person.name}
widthStyle="90px"
heightStyle="90px"
thumbhash={null}
hidden={person.isHidden}
/>
</div>
<p class="mt-1 truncate font-medium" title={person.name}>{person.name}</p>
{#if person.birthDate}
{@const personBirthDate = DateTime.fromISO(person.birthDate)}
@ -191,41 +251,120 @@
<p class="text-sm">DETAILS</p>
{/if}
{#if asset.exifInfo?.dateTimeOriginal}
{#if asset.exifInfo?.dateTimeOriginal && !asset.isReadOnly}
{@const assetDateTimeOriginal = DateTime.fromISO(asset.exifInfo.dateTimeOriginal, {
zone: asset.exifInfo.timeZone ?? undefined,
})}
<div class="flex gap-4 py-4">
<div>
<Icon path={mdiCalendar} size="24" />
</div>
<div
class="flex justify-between place-items-start gap-4 py-4"
tabindex="0"
role="button"
on:click={() => (isOwner ? (isShowChangeDate = true) : null)}
on:keydown={(event) => (isOwner ? event.key === 'Enter' && (isShowChangeDate = true) : null)}
title={isOwner ? 'Edit date' : ''}
class:hover:dark:text-immich-dark-primary={isOwner}
class:hover:text-immich-primary={isOwner}
>
<div class="flex gap-4">
<div>
<Icon path={mdiCalendar} size="24" />
</div>
<div>
<p>
{assetDateTimeOriginal.toLocaleString(
{
month: 'short',
day: 'numeric',
year: 'numeric',
},
{ locale: $locale },
)}
</p>
<div class="flex gap-2 text-sm">
<div>
<p>
{assetDateTimeOriginal.toLocaleString(
{
weekday: 'short',
hour: 'numeric',
minute: '2-digit',
timeZoneName: 'longOffset',
month: 'short',
day: 'numeric',
year: 'numeric',
},
{ locale: $locale },
)}
</p>
<div class="flex gap-2 text-sm">
<p>
{assetDateTimeOriginal.toLocaleString(
{
weekday: 'short',
hour: 'numeric',
minute: '2-digit',
timeZoneName: 'longOffset',
},
{ locale: $locale },
)}
</p>
</div>
</div>
</div>
</div>{/if}
{#if isOwner}
<button class="focus:outline-none">
<Icon path={mdiPencil} size="20" />
</button>
{/if}
</div>
{:else if !asset.exifInfo?.dateTimeOriginal && !asset.isReadOnly && $user && asset.ownerId === $user.id}
<div class="flex justify-between place-items-start gap-4 py-4">
<div class="flex gap-4">
<div>
<Icon path={mdiCalendar} size="24" />
</div>
</div>
<button class="focus:outline-none">
<Icon path={mdiPencil} size="20" />
</button>
</div>
{:else if asset.exifInfo?.dateTimeOriginal && asset.isReadOnly}
{@const assetDateTimeOriginal = DateTime.fromISO(asset.exifInfo.dateTimeOriginal, {
zone: asset.exifInfo.timeZone ?? undefined,
})}
<div class="flex justify-between place-items-start gap-4 py-4">
<div class="flex gap-4">
<div>
<Icon path={mdiCalendar} size="24" />
</div>
<div>
<p>
{assetDateTimeOriginal.toLocaleString(
{
month: 'short',
day: 'numeric',
year: 'numeric',
},
{ locale: $locale },
)}
</p>
<div class="flex gap-2 text-sm">
<p>
{assetDateTimeOriginal.toLocaleString(
{
weekday: 'short',
hour: 'numeric',
minute: '2-digit',
timeZoneName: 'longOffset',
},
{ locale: $locale },
)}
</p>
</div>
</div>
</div>
</div>
{/if}
{#if isShowChangeDate}
{@const assetDateTimeOriginal = asset.exifInfo?.dateTimeOriginal
? DateTime.fromISO(asset.exifInfo.dateTimeOriginal, {
zone: asset.exifInfo.timeZone ?? undefined,
})
: DateTime.now()}
<ChangeDate
initialDate={assetDateTimeOriginal}
on:confirm={({ detail: date }) => handleConfirmChangeDate(date)}
on:cancel={() => (isShowChangeDate = false)}
/>
{/if}
{#if asset.exifInfo?.fileSizeInByte}
<div class="flex gap-4 py-4">
@ -292,24 +431,88 @@
</div>
{/if}
{#if asset.exifInfo?.city}
<div class="flex gap-4 py-4">
<div><Icon path={mdiMapMarkerOutline} size="24" /></div>
{#if asset.exifInfo?.city && !asset.isReadOnly}
<div
class="flex justify-between place-items-start gap-4 py-4"
on:click={() => (isOwner ? (isShowChangeLocation = true) : null)}
on:keydown={(event) => (isOwner ? event.key === 'Enter' && (isShowChangeLocation = true) : null)}
tabindex="0"
title={isOwner ? 'Edit location' : ''}
role="button"
class:hover:dark:text-immich-dark-primary={isOwner}
class:hover:text-immich-primary={isOwner}
>
<div class="flex gap-4">
<div><Icon path={mdiMapMarkerOutline} size="24" /></div>
<div>
<p>{asset.exifInfo.city}</p>
{#if asset.exifInfo?.state}
<div class="flex gap-2 text-sm">
<p>{asset.exifInfo.state}</p>
</div>
{/if}
{#if asset.exifInfo?.country}
<div class="flex gap-2 text-sm">
<p>{asset.exifInfo.country}</p>
</div>
{/if}
<div>
<p>{asset.exifInfo.city}</p>
{#if asset.exifInfo?.state}
<div class="flex gap-2 text-sm">
<p>{asset.exifInfo.state}</p>
</div>
{/if}
{#if asset.exifInfo?.country}
<div class="flex gap-2 text-sm">
<p>{asset.exifInfo.country}</p>
</div>
{/if}
</div>
</div>
{#if isOwner}
<div>
<Icon path={mdiPencil} size="20" />
</div>
{/if}
</div>
{:else if !asset.exifInfo?.city && !asset.isReadOnly && $user && asset.ownerId === $user.id}
<div
class="flex justify-between place-items-start gap-4 py-4 rounded-lg pr-2 hover:dark:text-immich-dark-primary hover:text-immich-primary"
on:click={() => (isShowChangeLocation = true)}
on:keydown={(event) => event.key === 'Enter' && (isShowChangeLocation = true)}
tabindex="0"
role="button"
title="Add location"
>
<div class="flex gap-4">
<div>
<div><Icon path={mdiMapMarkerOutline} size="24" /></div>
</div>
<p>Add a location</p>
</div>
<div class="focus:outline-none">
<Icon path={mdiPencil} size="20" />
</div>
</div>
{:else if asset.exifInfo?.city && asset.isReadOnly}
<div class="flex justify-between place-items-start gap-4 py-4">
<div class="flex gap-4">
<div><Icon path={mdiMapMarkerOutline} size="24" /></div>
<div>
<p>{asset.exifInfo.city}</p>
{#if asset.exifInfo?.state}
<div class="flex gap-2 text-sm">
<p>{asset.exifInfo.state}</p>
</div>
{/if}
{#if asset.exifInfo?.country}
<div class="flex gap-2 text-sm">
<p>{asset.exifInfo.country}</p>
</div>
{/if}
</div>
</div>
</div>
{/if}
{#if isShowChangeLocation}
<ChangeLocation
{asset}
on:confirm={({ detail: gps }) => handleConfirmChangeLocation(gps)}
on:cancel={() => (isShowChangeLocation = false)}
/>
{/if}
</div>
</section>

View file

@ -7,8 +7,9 @@
export let color: Color = 'transparent-gray';
export let disabled = false;
export let fullwidth = false;
</script>
<Button size="link" {color} shadow={false} rounded="lg" {disabled} on:click>
<Button size="link" {color} shadow={false} rounded="lg" {disabled} on:click {fullwidth}>
<slot />
</Button>

View file

@ -29,10 +29,13 @@
icon?: string;
};
let showMenu = false;
export let showMenu = false;
export let controlable = false;
const handleClickOutside = () => {
showMenu = false;
if (!controlable) {
showMenu = false;
}
};
const handleSelectOption = (option: T) => {
@ -60,7 +63,7 @@
<div id="dropdown-button" use:clickOutside on:outclick={handleClickOutside} on:escape={handleClickOutside}>
<!-- BUTTON TITLE -->
<LinkButton on:click={() => (showMenu = true)}>
<LinkButton on:click={() => (showMenu = true)} fullwidth>
<div class="flex place-items-center gap-2 text-sm">
{#if renderedSelectedOption?.icon}
<Icon path={renderedSelectedOption.icon} size="18" />
@ -72,13 +75,13 @@
<!-- DROP DOWN MENU -->
{#if showMenu}
<div
transition:fly={{ y: -30, x: 30, duration: 200 }}
class="text-md absolute right-0 top-5 z-50 flex min-w-[250px] flex-col rounded-2xl bg-gray-100 py-4 text-black shadow-lg dark:bg-gray-700 dark:text-white"
transition:fly={{ y: -30, x: 30, duration: 100 }}
class="text-md fixed z-50 flex min-w-[250px] max-h-[70vh] overflow-y-scroll immich-scrollbar flex-col rounded-2xl bg-gray-100 py-2 text-black shadow-lg dark:bg-gray-700 dark:text-white"
>
{#each options as option (option)}
{@const renderedOption = renderOption(option)}
<button
class="grid grid-cols-[20px,1fr] place-items-center gap-2 p-4 transition-all hover:bg-gray-300 dark:hover:bg-gray-800"
class="grid grid-cols-[20px,1fr] place-items-center p-2 transition-all hover:bg-gray-300 dark:hover:bg-gray-800"
on:click={() => handleSelectOption(option)}
>
{#if _.isEqual(selectedOption, option)}

View file

@ -17,6 +17,7 @@
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
import { cloneDeep } from 'lodash-es';
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
import { searchNameLocal } from '$lib/utils/person';
export let person: PersonResponseDto;
let people: PersonResponseDto[] = [];
@ -56,12 +57,7 @@
}
if (!force) {
if (people.length < 20 && name.startsWith(searchWord)) {
people = peopleCopy
.filter((person: PersonResponseDto) => {
const nameParts = person.name.split(' ');
return nameParts.some((splitName) => splitName.toLowerCase().startsWith(name.toLowerCase()));
})
.slice(0, 10);
people = searchNameLocal(name, peopleCopy, 10);
return;
}
}

View file

@ -0,0 +1,39 @@
<script lang="ts">
import ChangeDate from '$lib/components/shared-components/change-date.svelte';
import { handleError } from '$lib/utils/handle-error';
import { api } from '@api';
import { DateTime } from 'luxon';
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
import { user } from '$lib/stores/user.store';
import { getSelectedAssets } from '$lib/utils/asset-utils';
export let menuItem = false;
const { clearSelect, getOwnedAssets } = getAssetControlContext();
let isShowChangeDate = false;
const handleConfirm = async (dateTimeOriginal: string) => {
isShowChangeDate = false;
const ids = getSelectedAssets(getOwnedAssets(), $user);
try {
await api.assetApi.updateAssets({
assetBulkUpdateDto: { ids, dateTimeOriginal },
});
} catch (error) {
handleError(error, 'Unable to change date');
}
clearSelect();
};
</script>
{#if menuItem}
<MenuOption text="Change date" on:click={() => (isShowChangeDate = true)} />
{/if}
{#if isShowChangeDate}
<ChangeDate
initialDate={DateTime.now()}
on:confirm={({ detail: date }) => handleConfirm(date)}
on:cancel={() => (isShowChangeDate = false)}
/>
{/if}

View file

@ -0,0 +1,42 @@
<script lang="ts">
import { api } from '@api';
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
import ChangeLocation from '$lib/components/shared-components/change-location.svelte';
import { handleError } from '../../../utils/handle-error';
import { user } from '$lib/stores/user.store';
import { getSelectedAssets } from '$lib/utils/asset-utils';
export let menuItem = false;
const { clearSelect, getOwnedAssets } = getAssetControlContext();
let isShowChangeLocation = false;
async function handleConfirm(point: { lng: number; lat: number }) {
isShowChangeLocation = false;
const ids = getSelectedAssets(getOwnedAssets(), $user);
try {
await api.assetApi.updateAssets({
assetBulkUpdateDto: {
ids,
latitude: point.lat,
longitude: point.lng,
},
});
} catch (error) {
handleError(error, 'Unable to update location');
}
clearSelect();
}
</script>
{#if menuItem}
<MenuOption text="Change location" on:click={() => (isShowChangeLocation = true)} />
{/if}
{#if isShowChangeLocation}
<ChangeLocation
on:confirm={({ detail: point }) => handleConfirm(point)}
on:cancel={() => (isShowChangeLocation = false)}
/>
{/if}

Some files were not shown because too many files have changed in this diff Show more