diff --git a/e2e/src/api/specs/album.e2e-spec.ts b/e2e/src/api/specs/album.e2e-spec.ts index 5c101a0793..5b40234e8d 100644 --- a/e2e/src/api/specs/album.e2e-spec.ts +++ b/e2e/src/api/specs/album.e2e-spec.ts @@ -142,6 +142,10 @@ describe('/albums', () => { ...user1Albums[0], assets: [expect.objectContaining({ isFavorite: false })], lastModifiedAssetTimestamp: expect.any(String), + startDate: expect.any(String), + endDate: expect.any(String), + shared: true, + albumUsers: expect.any(Array), }); }); @@ -299,6 +303,10 @@ describe('/albums', () => { ...user1Albums[0], assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })], lastModifiedAssetTimestamp: expect.any(String), + startDate: expect.any(String), + endDate: expect.any(String), + albumUsers: expect.any(Array), + shared: true, }); }); @@ -330,6 +338,10 @@ describe('/albums', () => { ...user1Albums[0], assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })], lastModifiedAssetTimestamp: expect.any(String), + startDate: expect.any(String), + endDate: expect.any(String), + albumUsers: expect.any(Array), + shared: true, }); }); @@ -344,6 +356,10 @@ describe('/albums', () => { assets: [], assetCount: 1, lastModifiedAssetTimestamp: expect.any(String), + endDate: expect.any(String), + startDate: expect.any(String), + albumUsers: expect.any(Array), + shared: true, }); }); }); diff --git a/server/src/dtos/album.dto.ts b/server/src/dtos/album.dto.ts index 76f4fdfc98..2f99b958c4 100644 --- a/server/src/dtos/album.dto.ts +++ b/server/src/dtos/album.dto.ts @@ -29,7 +29,7 @@ export class AddUsersDto { albumUsers!: AlbumUserAddDto[]; } -class AlbumUserCreateDto { +export class AlbumUserCreateDto { @ValidateUUID() userId!: string; diff --git a/server/src/interfaces/album.interface.ts b/server/src/interfaces/album.interface.ts index 24c64bdc9d..7af1bd97e1 100644 --- a/server/src/interfaces/album.interface.ts +++ b/server/src/interfaces/album.interface.ts @@ -1,3 +1,6 @@ +import { Insertable, Updateable } from 'kysely'; +import { Albums } from 'src/db'; +import { AlbumUserCreateDto } from 'src/dtos/album.dto'; import { AlbumEntity } from 'src/entities/album.entity'; import { IBulkAsset } from 'src/utils/asset.util'; @@ -15,7 +18,7 @@ export interface AlbumInfoOptions { } export interface IAlbumRepository extends IBulkAsset { - getById(id: string, options: AlbumInfoOptions): Promise<AlbumEntity | null>; + getById(id: string, options: AlbumInfoOptions): Promise<AlbumEntity | undefined>; getByAssetId(ownerId: string, assetId: string): Promise<AlbumEntity[]>; removeAsset(assetId: string): Promise<void>; getMetadataForIds(ids: string[]): Promise<AlbumAssetCount[]>; @@ -25,8 +28,8 @@ export interface IAlbumRepository extends IBulkAsset { restoreAll(userId: string): Promise<void>; softDeleteAll(userId: string): Promise<void>; deleteAll(userId: string): Promise<void>; - create(album: Partial<AlbumEntity>): Promise<AlbumEntity>; - update(album: Partial<AlbumEntity>): Promise<AlbumEntity>; + create(album: Insertable<Albums>, assetIds: string[], albumUsers: AlbumUserCreateDto[]): Promise<AlbumEntity>; + update(id: string, album: Updateable<Albums>): Promise<AlbumEntity>; delete(id: string): Promise<void>; updateThumbnails(): Promise<number | undefined>; } diff --git a/server/src/queries/album.repository.sql b/server/src/queries/album.repository.sql index 196a1d1609..89c9e3b4a9 100644 --- a/server/src/queries/album.repository.sql +++ b/server/src/queries/album.repository.sql @@ -1,460 +1,490 @@ -- NOTE: This file is auto generated by ./sql-generator -- AlbumRepository.getById -SELECT DISTINCT - "distinctAlias"."AlbumEntity_id" AS "ids_AlbumEntity_id" -FROM +select + "albums".*, ( - SELECT - "AlbumEntity"."id" AS "AlbumEntity_id", - "AlbumEntity"."ownerId" AS "AlbumEntity_ownerId", - "AlbumEntity"."albumName" AS "AlbumEntity_albumName", - "AlbumEntity"."description" AS "AlbumEntity_description", - "AlbumEntity"."createdAt" AS "AlbumEntity_createdAt", - "AlbumEntity"."updatedAt" AS "AlbumEntity_updatedAt", - "AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt", - "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId", - "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled", - "AlbumEntity"."order" AS "AlbumEntity_order", - "AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id", - "AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name", - "AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin", - "AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email", - "AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel", - "AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId", - "AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath", - "AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword", - "AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt", - "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt", - "AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status", - "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", - "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", - "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes", - "AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt", - "AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId", - "AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId", - "AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_id", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."name" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_name", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."isAdmin" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_isAdmin", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."email" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_email", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."storageLabel" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_storageLabel", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."oauthId" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_oauthId", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileImagePath" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileImagePath", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."shouldChangePassword" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_shouldChangePassword", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."createdAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_createdAt", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_deletedAt", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileChangedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileChangedAt", - "AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id", - "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description", - "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password", - "AlbumEntity__AlbumEntity_sharedLinks"."userId" AS "AlbumEntity__AlbumEntity_sharedLinks_userId", - "AlbumEntity__AlbumEntity_sharedLinks"."key" AS "AlbumEntity__AlbumEntity_sharedLinks_key", - "AlbumEntity__AlbumEntity_sharedLinks"."type" AS "AlbumEntity__AlbumEntity_sharedLinks_type", - "AlbumEntity__AlbumEntity_sharedLinks"."createdAt" AS "AlbumEntity__AlbumEntity_sharedLinks_createdAt", - "AlbumEntity__AlbumEntity_sharedLinks"."expiresAt" AS "AlbumEntity__AlbumEntity_sharedLinks_expiresAt", - "AlbumEntity__AlbumEntity_sharedLinks"."allowUpload" AS "AlbumEntity__AlbumEntity_sharedLinks_allowUpload", - "AlbumEntity__AlbumEntity_sharedLinks"."allowDownload" AS "AlbumEntity__AlbumEntity_sharedLinks_allowDownload", - "AlbumEntity__AlbumEntity_sharedLinks"."showExif" AS "AlbumEntity__AlbumEntity_sharedLinks_showExif", - "AlbumEntity__AlbumEntity_sharedLinks"."albumId" AS "AlbumEntity__AlbumEntity_sharedLinks_albumId" - FROM - "albums" "AlbumEntity" - LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId" - AND ( - "AlbumEntity__AlbumEntity_owner"."deletedAt" IS NULL - ) - LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id" - LEFT JOIN "users" "a641d58cf46d4a391ba060ac4dc337665c69ffea" ON "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" = "AlbumEntity__AlbumEntity_albumUsers"."usersId" - AND ( - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" IS NULL - ) - LEFT JOIN "shared_links" "AlbumEntity__AlbumEntity_sharedLinks" ON "AlbumEntity__AlbumEntity_sharedLinks"."albumId" = "AlbumEntity"."id" - WHERE - ((("AlbumEntity"."id" = $1))) - AND ("AlbumEntity"."deletedAt" IS NULL) - ) "distinctAlias" -ORDER BY - "AlbumEntity_id" ASC -LIMIT - 1 + select + to_json(obj) + from + ( + select + "id", + "email", + "createdAt", + "profileImagePath", + "isAdmin", + "shouldChangePassword", + "deletedAt", + "oauthId", + "updatedAt", + "storageLabel", + "name", + "quotaSizeInBytes", + "quotaUsageInBytes", + "status", + "profileChangedAt" + from + "users" + where + "users"."id" = "albums"."ownerId" + ) as obj + ) as "owner", + ( + select + coalesce(json_agg(agg), '[]') + from + ( + select + "album_users".*, + ( + select + to_json(obj) + from + ( + select + "id", + "email", + "createdAt", + "profileImagePath", + "isAdmin", + "shouldChangePassword", + "deletedAt", + "oauthId", + "updatedAt", + "storageLabel", + "name", + "quotaSizeInBytes", + "quotaUsageInBytes", + "status", + "profileChangedAt" + from + "users" + where + "users"."id" = "album_users"."usersId" + ) as obj + ) as "user" + from + "albums_shared_users_users" as "album_users" + where + "album_users"."albumsId" = "albums"."id" + ) as agg + ) as "albumUsers", + ( + select + coalesce(json_agg(agg), '[]') + from + ( + select + * + from + "shared_links" + where + "shared_links"."albumId" = "albums"."id" + ) as agg + ) as "sharedLinks" +from + "albums" +where + "albums"."id" = $1 + and "albums"."deletedAt" is null -- AlbumRepository.getByAssetId -SELECT - "AlbumEntity"."id" AS "AlbumEntity_id", - "AlbumEntity"."ownerId" AS "AlbumEntity_ownerId", - "AlbumEntity"."albumName" AS "AlbumEntity_albumName", - "AlbumEntity"."description" AS "AlbumEntity_description", - "AlbumEntity"."createdAt" AS "AlbumEntity_createdAt", - "AlbumEntity"."updatedAt" AS "AlbumEntity_updatedAt", - "AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt", - "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId", - "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled", - "AlbumEntity"."order" AS "AlbumEntity_order", - "AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id", - "AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name", - "AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin", - "AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email", - "AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel", - "AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId", - "AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath", - "AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword", - "AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt", - "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt", - "AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status", - "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", - "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", - "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes", - "AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt", - "AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId", - "AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId", - "AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_id", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."name" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_name", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."isAdmin" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_isAdmin", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."email" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_email", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."storageLabel" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_storageLabel", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."oauthId" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_oauthId", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileImagePath" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileImagePath", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."shouldChangePassword" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_shouldChangePassword", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."createdAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_createdAt", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_deletedAt", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileChangedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileChangedAt" -FROM - "albums" "AlbumEntity" - LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId" - AND ( - "AlbumEntity__AlbumEntity_owner"."deletedAt" IS NULL - ) - LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id" - LEFT JOIN "users" "a641d58cf46d4a391ba060ac4dc337665c69ffea" ON "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" = "AlbumEntity__AlbumEntity_albumUsers"."usersId" - AND ( - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" IS NULL - ) - LEFT JOIN "albums_assets_assets" "AlbumEntity_AlbumEntity__AlbumEntity_assets" ON "AlbumEntity_AlbumEntity__AlbumEntity_assets"."albumsId" = "AlbumEntity"."id" - LEFT JOIN "assets" "AlbumEntity__AlbumEntity_assets" ON "AlbumEntity__AlbumEntity_assets"."id" = "AlbumEntity_AlbumEntity__AlbumEntity_assets"."assetsId" - AND ( - "AlbumEntity__AlbumEntity_assets"."deletedAt" IS NULL - ) -WHERE +select + "albums".*, + ( + select + to_json(obj) + from + ( + select + "id", + "email", + "createdAt", + "profileImagePath", + "isAdmin", + "shouldChangePassword", + "deletedAt", + "oauthId", + "updatedAt", + "storageLabel", + "name", + "quotaSizeInBytes", + "quotaUsageInBytes", + "status", + "profileChangedAt" + from + "users" + where + "users"."id" = "albums"."ownerId" + ) as obj + ) as "owner", + ( + select + coalesce(json_agg(agg), '[]') + from + ( + select + "album_users".*, + ( + select + to_json(obj) + from + ( + select + "id", + "email", + "createdAt", + "profileImagePath", + "isAdmin", + "shouldChangePassword", + "deletedAt", + "oauthId", + "updatedAt", + "storageLabel", + "name", + "quotaSizeInBytes", + "quotaUsageInBytes", + "status", + "profileChangedAt" + from + "users" + where + "users"."id" = "album_users"."usersId" + ) as obj + ) as "user" + from + "albums_shared_users_users" as "album_users" + where + "album_users"."albumsId" = "albums"."id" + ) as agg + ) as "albumUsers" +from + "albums" + left join "albums_assets_assets" as "album_assets" on "album_assets"."albumsId" = "albums"."id" + left join "albums_shared_users_users" as "album_users" on "album_users"."albumsId" = "albums"."id" +where ( ( - ( - ( - ("AlbumEntity"."ownerId" = $1) - AND ((("AlbumEntity__AlbumEntity_assets"."id" = $2))) - ) - ) - OR ( - ( - ( - ( - ( - "AlbumEntity__AlbumEntity_albumUsers"."usersId" = $3 - ) - ) - ) - AND ((("AlbumEntity__AlbumEntity_assets"."id" = $4))) - ) - ) + "albums"."ownerId" = $1 + and "album_assets"."assetsId" = $2 + ) + or ( + "album_users"."usersId" = $3 + and "album_assets"."assetsId" = $4 ) ) - AND ("AlbumEntity"."deletedAt" IS NULL) -ORDER BY - "AlbumEntity"."createdAt" DESC + and "albums"."deletedAt" is null +order by + "albums"."createdAt" desc, + "albums"."createdAt" desc -- AlbumRepository.getMetadataForIds -SELECT - "album"."id" AS "album_id", - MIN("assets"."fileCreatedAt") AS "start_date", - MAX("assets"."fileCreatedAt") AS "end_date", - COUNT("assets"."id") AS "asset_count" -FROM - "albums" "album" - LEFT JOIN "albums_assets_assets" "album_assets" ON "album_assets"."albumsId" = "album"."id" - LEFT JOIN "assets" "assets" ON "assets"."id" = "album_assets"."assetsId" - AND "assets"."deletedAt" IS NULL -WHERE - ("album"."id" IN ($1)) - AND ("album"."deletedAt" IS NULL) -GROUP BY - "album"."id" +select + "albums"."id", + min("assets"."fileCreatedAt") as "startDate", + max("assets"."fileCreatedAt") as "endDate", + count("assets"."id") as "assetCount" +from + "albums" + left join "albums_assets_assets" as "album_assets" on "album_assets"."albumsId" = "albums"."id" + left join "assets" on "assets"."id" = "album_assets"."assetsId" +where + "albums"."id" in ($1) +group by + "albums"."id" -- AlbumRepository.getOwned -SELECT - "AlbumEntity"."id" AS "AlbumEntity_id", - "AlbumEntity"."ownerId" AS "AlbumEntity_ownerId", - "AlbumEntity"."albumName" AS "AlbumEntity_albumName", - "AlbumEntity"."description" AS "AlbumEntity_description", - "AlbumEntity"."createdAt" AS "AlbumEntity_createdAt", - "AlbumEntity"."updatedAt" AS "AlbumEntity_updatedAt", - "AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt", - "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId", - "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled", - "AlbumEntity"."order" AS "AlbumEntity_order", - "AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId", - "AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId", - "AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_id", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."name" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_name", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."isAdmin" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_isAdmin", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."email" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_email", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."storageLabel" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_storageLabel", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."oauthId" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_oauthId", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileImagePath" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileImagePath", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."shouldChangePassword" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_shouldChangePassword", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."createdAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_createdAt", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_deletedAt", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileChangedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileChangedAt", - "AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id", - "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description", - "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password", - "AlbumEntity__AlbumEntity_sharedLinks"."userId" AS "AlbumEntity__AlbumEntity_sharedLinks_userId", - "AlbumEntity__AlbumEntity_sharedLinks"."key" AS "AlbumEntity__AlbumEntity_sharedLinks_key", - "AlbumEntity__AlbumEntity_sharedLinks"."type" AS "AlbumEntity__AlbumEntity_sharedLinks_type", - "AlbumEntity__AlbumEntity_sharedLinks"."createdAt" AS "AlbumEntity__AlbumEntity_sharedLinks_createdAt", - "AlbumEntity__AlbumEntity_sharedLinks"."expiresAt" AS "AlbumEntity__AlbumEntity_sharedLinks_expiresAt", - "AlbumEntity__AlbumEntity_sharedLinks"."allowUpload" AS "AlbumEntity__AlbumEntity_sharedLinks_allowUpload", - "AlbumEntity__AlbumEntity_sharedLinks"."allowDownload" AS "AlbumEntity__AlbumEntity_sharedLinks_allowDownload", - "AlbumEntity__AlbumEntity_sharedLinks"."showExif" AS "AlbumEntity__AlbumEntity_sharedLinks_showExif", - "AlbumEntity__AlbumEntity_sharedLinks"."albumId" AS "AlbumEntity__AlbumEntity_sharedLinks_albumId", - "AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id", - "AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name", - "AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin", - "AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email", - "AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel", - "AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId", - "AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath", - "AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword", - "AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt", - "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt", - "AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status", - "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", - "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", - "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes", - "AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt" -FROM - "albums" "AlbumEntity" - LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id" - LEFT JOIN "users" "a641d58cf46d4a391ba060ac4dc337665c69ffea" ON "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" = "AlbumEntity__AlbumEntity_albumUsers"."usersId" - AND ( - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" IS NULL - ) - LEFT JOIN "shared_links" "AlbumEntity__AlbumEntity_sharedLinks" ON "AlbumEntity__AlbumEntity_sharedLinks"."albumId" = "AlbumEntity"."id" - LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId" - AND ( - "AlbumEntity__AlbumEntity_owner"."deletedAt" IS NULL - ) -WHERE - ((("AlbumEntity"."ownerId" = $1))) - AND ("AlbumEntity"."deletedAt" IS NULL) -ORDER BY - "AlbumEntity"."createdAt" DESC +select + "albums".*, + ( + select + to_json(obj) + from + ( + select + "id", + "email", + "createdAt", + "profileImagePath", + "isAdmin", + "shouldChangePassword", + "deletedAt", + "oauthId", + "updatedAt", + "storageLabel", + "name", + "quotaSizeInBytes", + "quotaUsageInBytes", + "status", + "profileChangedAt" + from + "users" + where + "users"."id" = "albums"."ownerId" + ) as obj + ) as "owner", + ( + select + coalesce(json_agg(agg), '[]') + from + ( + select + "album_users".*, + ( + select + to_json(obj) + from + ( + select + "id", + "email", + "createdAt", + "profileImagePath", + "isAdmin", + "shouldChangePassword", + "deletedAt", + "oauthId", + "updatedAt", + "storageLabel", + "name", + "quotaSizeInBytes", + "quotaUsageInBytes", + "status", + "profileChangedAt" + from + "users" + where + "users"."id" = "album_users"."usersId" + ) as obj + ) as "user" + from + "albums_shared_users_users" as "album_users" + where + "album_users"."albumsId" = "albums"."id" + ) as agg + ) as "albumUsers", + ( + select + coalesce(json_agg(agg), '[]') + from + ( + select + * + from + "shared_links" + where + "shared_links"."albumId" = "albums"."id" + ) as agg + ) as "sharedLinks" +from + "albums" +where + "albums"."ownerId" = $1 + and "albums"."deletedAt" is null +order by + "albums"."createdAt" desc -- AlbumRepository.getShared -SELECT - "AlbumEntity"."id" AS "AlbumEntity_id", - "AlbumEntity"."ownerId" AS "AlbumEntity_ownerId", - "AlbumEntity"."albumName" AS "AlbumEntity_albumName", - "AlbumEntity"."description" AS "AlbumEntity_description", - "AlbumEntity"."createdAt" AS "AlbumEntity_createdAt", - "AlbumEntity"."updatedAt" AS "AlbumEntity_updatedAt", - "AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt", - "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId", - "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled", - "AlbumEntity"."order" AS "AlbumEntity_order", - "AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId", - "AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId", - "AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_id", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."name" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_name", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."isAdmin" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_isAdmin", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."email" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_email", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."storageLabel" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_storageLabel", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."oauthId" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_oauthId", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileImagePath" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileImagePath", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."shouldChangePassword" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_shouldChangePassword", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."createdAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_createdAt", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_deletedAt", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileChangedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileChangedAt", - "AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id", - "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description", - "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password", - "AlbumEntity__AlbumEntity_sharedLinks"."userId" AS "AlbumEntity__AlbumEntity_sharedLinks_userId", - "AlbumEntity__AlbumEntity_sharedLinks"."key" AS "AlbumEntity__AlbumEntity_sharedLinks_key", - "AlbumEntity__AlbumEntity_sharedLinks"."type" AS "AlbumEntity__AlbumEntity_sharedLinks_type", - "AlbumEntity__AlbumEntity_sharedLinks"."createdAt" AS "AlbumEntity__AlbumEntity_sharedLinks_createdAt", - "AlbumEntity__AlbumEntity_sharedLinks"."expiresAt" AS "AlbumEntity__AlbumEntity_sharedLinks_expiresAt", - "AlbumEntity__AlbumEntity_sharedLinks"."allowUpload" AS "AlbumEntity__AlbumEntity_sharedLinks_allowUpload", - "AlbumEntity__AlbumEntity_sharedLinks"."allowDownload" AS "AlbumEntity__AlbumEntity_sharedLinks_allowDownload", - "AlbumEntity__AlbumEntity_sharedLinks"."showExif" AS "AlbumEntity__AlbumEntity_sharedLinks_showExif", - "AlbumEntity__AlbumEntity_sharedLinks"."albumId" AS "AlbumEntity__AlbumEntity_sharedLinks_albumId", - "AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id", - "AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name", - "AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin", - "AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email", - "AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel", - "AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId", - "AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath", - "AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword", - "AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt", - "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt", - "AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status", - "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", - "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", - "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes", - "AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt" -FROM - "albums" "AlbumEntity" - LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id" - LEFT JOIN "users" "a641d58cf46d4a391ba060ac4dc337665c69ffea" ON "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" = "AlbumEntity__AlbumEntity_albumUsers"."usersId" - AND ( - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" IS NULL - ) - LEFT JOIN "shared_links" "AlbumEntity__AlbumEntity_sharedLinks" ON "AlbumEntity__AlbumEntity_sharedLinks"."albumId" = "AlbumEntity"."id" - LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId" - AND ( - "AlbumEntity__AlbumEntity_owner"."deletedAt" IS NULL - ) -WHERE +select distinct + on ("albums"."createdAt") "albums".*, ( - ( + select + coalesce(json_agg(agg), '[]') + from ( - ( + select + "album_users".*, ( - ( + select + to_json(obj) + from ( - "AlbumEntity__AlbumEntity_albumUsers"."usersId" = $1 - ) - ) - ) - ) - ) - OR ( - ( - ( - ( - ( - "AlbumEntity__AlbumEntity_sharedLinks"."userId" = $2 - ) - ) - ) - ) - ) - OR ( - ( - ("AlbumEntity"."ownerId" = $3) - AND ( - ( - ( - NOT ( - "AlbumEntity__AlbumEntity_albumUsers"."usersId" IS NULL - ) - ) - ) - ) - ) - ) + select + "id", + "email", + "createdAt", + "profileImagePath", + "isAdmin", + "shouldChangePassword", + "deletedAt", + "oauthId", + "updatedAt", + "storageLabel", + "name", + "quotaSizeInBytes", + "quotaUsageInBytes", + "status", + "profileChangedAt" + from + "users" + where + "users"."id" = "album_users"."usersId" + ) as obj + ) as "user" + from + "albums_shared_users_users" as "album_users" + where + "album_users"."albumsId" = "albums"."id" + ) as agg + ) as "albumUsers", + ( + select + to_json(obj) + from + ( + select + "id", + "email", + "createdAt", + "profileImagePath", + "isAdmin", + "shouldChangePassword", + "deletedAt", + "oauthId", + "updatedAt", + "storageLabel", + "name", + "quotaSizeInBytes", + "quotaUsageInBytes", + "status", + "profileChangedAt" + from + "users" + where + "users"."id" = "albums"."ownerId" + ) as obj + ) as "owner", + ( + select + coalesce(json_agg(agg), '[]') + from + ( + select + * + from + "shared_links" + where + "shared_links"."albumId" = "albums"."id" + ) as agg + ) as "sharedLinks" +from + "albums" + left join "albums_shared_users_users" as "shared_albums" on "shared_albums"."albumsId" = "albums"."id" + left join "shared_links" on "shared_links"."albumId" = "albums"."id" +where + ( + "shared_albums"."usersId" = $1 + or "shared_links"."userId" = $2 + or ( + "albums"."ownerId" = $3 + and "shared_albums"."usersId" is not null ) ) - AND ("AlbumEntity"."deletedAt" IS NULL) -ORDER BY - "AlbumEntity"."createdAt" DESC + and "albums"."deletedAt" is null +order by + "albums"."createdAt" desc -- AlbumRepository.getNotShared -SELECT - "AlbumEntity"."id" AS "AlbumEntity_id", - "AlbumEntity"."ownerId" AS "AlbumEntity_ownerId", - "AlbumEntity"."albumName" AS "AlbumEntity_albumName", - "AlbumEntity"."description" AS "AlbumEntity_description", - "AlbumEntity"."createdAt" AS "AlbumEntity_createdAt", - "AlbumEntity"."updatedAt" AS "AlbumEntity_updatedAt", - "AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt", - "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId", - "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled", - "AlbumEntity"."order" AS "AlbumEntity_order", - "AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId", - "AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId", - "AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role", - "AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id", - "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description", - "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password", - "AlbumEntity__AlbumEntity_sharedLinks"."userId" AS "AlbumEntity__AlbumEntity_sharedLinks_userId", - "AlbumEntity__AlbumEntity_sharedLinks"."key" AS "AlbumEntity__AlbumEntity_sharedLinks_key", - "AlbumEntity__AlbumEntity_sharedLinks"."type" AS "AlbumEntity__AlbumEntity_sharedLinks_type", - "AlbumEntity__AlbumEntity_sharedLinks"."createdAt" AS "AlbumEntity__AlbumEntity_sharedLinks_createdAt", - "AlbumEntity__AlbumEntity_sharedLinks"."expiresAt" AS "AlbumEntity__AlbumEntity_sharedLinks_expiresAt", - "AlbumEntity__AlbumEntity_sharedLinks"."allowUpload" AS "AlbumEntity__AlbumEntity_sharedLinks_allowUpload", - "AlbumEntity__AlbumEntity_sharedLinks"."allowDownload" AS "AlbumEntity__AlbumEntity_sharedLinks_allowDownload", - "AlbumEntity__AlbumEntity_sharedLinks"."showExif" AS "AlbumEntity__AlbumEntity_sharedLinks_showExif", - "AlbumEntity__AlbumEntity_sharedLinks"."albumId" AS "AlbumEntity__AlbumEntity_sharedLinks_albumId", - "AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id", - "AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name", - "AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin", - "AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email", - "AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel", - "AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId", - "AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath", - "AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword", - "AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt", - "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt", - "AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status", - "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", - "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", - "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes", - "AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt" -FROM - "albums" "AlbumEntity" - LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id" - LEFT JOIN "shared_links" "AlbumEntity__AlbumEntity_sharedLinks" ON "AlbumEntity__AlbumEntity_sharedLinks"."albumId" = "AlbumEntity"."id" - LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId" - AND ( - "AlbumEntity__AlbumEntity_owner"."deletedAt" IS NULL - ) -WHERE +select distinct + on ("albums"."createdAt") "albums".*, ( - ( - ("AlbumEntity"."ownerId" = $1) - AND ( - ( + select + coalesce(json_agg(agg), '[]') + from + ( + select + "album_users".*, ( - "AlbumEntity__AlbumEntity_albumUsers"."usersId" IS NULL - ) - ) - ) - AND ( - ( - ( - "AlbumEntity__AlbumEntity_sharedLinks"."id" IS NULL - ) - ) - ) - ) - ) - AND ("AlbumEntity"."deletedAt" IS NULL) -ORDER BY - "AlbumEntity"."createdAt" DESC + select + to_json(obj) + from + ( + select + "id", + "email", + "createdAt", + "profileImagePath", + "isAdmin", + "shouldChangePassword", + "deletedAt", + "oauthId", + "updatedAt", + "storageLabel", + "name", + "quotaSizeInBytes", + "quotaUsageInBytes", + "status", + "profileChangedAt" + from + "users" + where + "users"."id" = "album_users"."usersId" + ) as obj + ) as "user" + from + "albums_shared_users_users" as "album_users" + where + "album_users"."albumsId" = "albums"."id" + ) as agg + ) as "albumUsers", + ( + select + to_json(obj) + from + ( + select + "id", + "email", + "createdAt", + "profileImagePath", + "isAdmin", + "shouldChangePassword", + "deletedAt", + "oauthId", + "updatedAt", + "storageLabel", + "name", + "quotaSizeInBytes", + "quotaUsageInBytes", + "status", + "profileChangedAt" + from + "users" + where + "users"."id" = "albums"."ownerId" + ) as obj + ) as "owner", + ( + select + coalesce(json_agg(agg), '[]') + from + ( + select + * + from + "shared_links" + where + "shared_links"."albumId" = "albums"."id" + ) as agg + ) as "sharedLinks" +from + "albums" + left join "albums_shared_users_users" as "shared_albums" on "shared_albums"."albumsId" = "albums"."id" + left join "shared_links" on "shared_links"."albumId" = "albums"."id" +where + "albums"."ownerId" = $1 + and "shared_albums"."usersId" is null + and "shared_links"."userId" is null + and "albums"."deletedAt" is null +order by + "albums"."createdAt" desc -- AlbumRepository.getAssetIds -SELECT - "albums_assets"."assetsId" AS "assetId" -FROM - "albums_assets_assets" "albums_assets" -WHERE - "albums_assets"."albumsId" = $1 - AND "albums_assets"."assetsId" IN ($2) +select + * +from + "albums_assets_assets" +where + "albums_assets_assets"."albumsId" = $1 + and "albums_assets_assets"."assetsId" in ($2) diff --git a/server/src/repositories/album.repository.ts b/server/src/repositories/album.repository.ts index 8ac352e945..bae91349f5 100644 --- a/server/src/repositories/album.repository.ts +++ b/server/src/repositories/album.repository.ts @@ -1,72 +1,116 @@ import { Injectable } from '@nestjs/common'; -import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; +import { InjectRepository } from '@nestjs/typeorm'; +import { ExpressionBuilder, Insertable, Kysely, sql, Updateable } from 'kysely'; +import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres'; +import { InjectKysely } from 'nestjs-kysely'; +import { Albums, DB } from 'src/db'; import { Chunked, ChunkedArray, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators'; +import { AlbumUserCreateDto } from 'src/dtos/album.dto'; import { AlbumEntity } from 'src/entities/album.entity'; -import { AssetEntity } from 'src/entities/asset.entity'; import { AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from 'src/interfaces/album.interface'; -import { - DataSource, - EntityManager, - FindOptionsOrder, - FindOptionsRelations, - In, - IsNull, - Not, - Repository, -} from 'typeorm'; +import { Repository } from 'typeorm'; -const withoutDeletedUsers = <T extends AlbumEntity | null>(album: T) => { - if (album) { - album.albumUsers = album.albumUsers.filter((albumUser) => albumUser.user && !albumUser.user.deletedAt); - } - return album; +const userColumns = [ + 'id', + 'email', + 'createdAt', + 'profileImagePath', + 'isAdmin', + 'shouldChangePassword', + 'deletedAt', + 'oauthId', + 'updatedAt', + 'storageLabel', + 'name', + 'quotaSizeInBytes', + 'quotaUsageInBytes', + 'status', + 'profileChangedAt', +] as const; + +const withOwner = (eb: ExpressionBuilder<DB, 'albums'>) => { + return jsonObjectFrom(eb.selectFrom('users').select(userColumns).whereRef('users.id', '=', 'albums.ownerId')).as( + 'owner', + ); +}; + +const withAlbumUsers = (eb: ExpressionBuilder<DB, 'albums'>) => { + return jsonArrayFrom( + eb + .selectFrom('albums_shared_users_users as album_users') + .selectAll('album_users') + .select((eb) => + jsonObjectFrom(eb.selectFrom('users').select(userColumns).whereRef('users.id', '=', 'album_users.usersId')).as( + 'user', + ), + ) + .whereRef('album_users.albumsId', '=', 'albums.id'), + ).as('albumUsers'); +}; + +const withSharedLink = (eb: ExpressionBuilder<DB, 'albums'>) => { + return jsonArrayFrom(eb.selectFrom('shared_links').selectAll().whereRef('shared_links.albumId', '=', 'albums.id')).as( + 'sharedLinks', + ); +}; + +const withAssets = (eb: ExpressionBuilder<DB, 'albums'>) => { + return eb + .selectFrom((eb) => + eb + .selectFrom('assets') + .selectAll('assets') + .innerJoin('exif', 'assets.id', 'exif.assetId') + .select((eb) => eb.fn.toJson('exif').as('exifInfo')) + .innerJoin('albums_assets_assets', 'albums_assets_assets.assetsId', 'assets.id') + .whereRef('albums_assets_assets.albumsId', '=', 'albums.id') + .orderBy('assets.fileCreatedAt', 'desc') + .as('asset'), + ) + .select((eb) => eb.fn.jsonAgg('asset').as('assets')) + .as('assets'); }; @Injectable() export class AlbumRepository implements IAlbumRepository { constructor( - @InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>, @InjectRepository(AlbumEntity) private repository: Repository<AlbumEntity>, - @InjectDataSource() private dataSource: DataSource, + @InjectKysely() private db: Kysely<DB>, ) {} @GenerateSql({ params: [DummyValue.UUID, {}] }) - async getById(id: string, options: AlbumInfoOptions): Promise<AlbumEntity | null> { - const relations: FindOptionsRelations<AlbumEntity> = { - owner: true, - albumUsers: { user: true }, - assets: false, - sharedLinks: true, - }; - - const order: FindOptionsOrder<AlbumEntity> = {}; - - if (options.withAssets) { - relations.assets = { - exifInfo: true, - }; - - order.assets = { - fileCreatedAt: 'DESC', - }; - } - - const album = await this.repository.findOne({ where: { id }, relations, order }); - return withoutDeletedUsers(album); + async getById(id: string, options: AlbumInfoOptions): Promise<AlbumEntity | undefined> { + return this.db + .selectFrom('albums') + .selectAll('albums') + .where('albums.id', '=', id) + .where('albums.deletedAt', 'is', null) + .select(withOwner) + .select(withAlbumUsers) + .select(withSharedLink) + .$if(options.withAssets, (eb) => eb.select(withAssets)) + .executeTakeFirst() as Promise<AlbumEntity | undefined>; } @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] }) async getByAssetId(ownerId: string, assetId: string): Promise<AlbumEntity[]> { - const albums = await this.repository.find({ - where: [ - { ownerId, assets: { id: assetId } }, - { albumUsers: { userId: ownerId }, assets: { id: assetId } }, - ], - relations: { owner: true, albumUsers: { user: true } }, - order: { createdAt: 'DESC' }, - }); - - return albums.map((album) => withoutDeletedUsers(album)); + return this.db + .selectFrom('albums') + .selectAll('albums') + .leftJoin('albums_assets_assets as album_assets', 'album_assets.albumsId', 'albums.id') + .leftJoin('albums_shared_users_users as album_users', 'album_users.albumsId', 'albums.id') + .where((eb) => + eb.or([ + eb.and([eb('albums.ownerId', '=', ownerId), eb('album_assets.assetsId', '=', assetId)]), + eb.and([eb('album_users.usersId', '=', ownerId), eb('album_assets.assetsId', '=', assetId)]), + ]), + ) + .where('albums.deletedAt', 'is', null) + .orderBy('albums.createdAt', 'desc') + .select(withOwner) + .select(withAlbumUsers) + .orderBy('albums.createdAt', 'desc') + .execute() as unknown as Promise<AlbumEntity[]>; } @GenerateSql({ params: [[DummyValue.UUID]] }) @@ -77,36 +121,38 @@ export class AlbumRepository implements IAlbumRepository { return []; } - // Only possible with query builder because of GROUP BY. - const albumMetadatas = await this.repository - .createQueryBuilder('album') - .select('album.id') - .addSelect('MIN(assets.fileCreatedAt)', 'start_date') - .addSelect('MAX(assets.fileCreatedAt)', 'end_date') - .addSelect('COUNT(assets.id)', 'asset_count') - .leftJoin('albums_assets_assets', 'album_assets', 'album_assets.albumsId = album.id') - .leftJoin('assets', 'assets', 'assets.id = album_assets.assetsId') - .where('album.id IN (:...ids)', { ids }) - .groupBy('album.id') - .getRawMany(); + const metadatas = await this.db + .selectFrom('albums') + .leftJoin('albums_assets_assets as album_assets', 'album_assets.albumsId', 'albums.id') + .leftJoin('assets', 'assets.id', 'album_assets.assetsId') + .select('albums.id') + .select((eb) => eb.fn.min('assets.fileCreatedAt').as('startDate')) + .select((eb) => eb.fn.max('assets.fileCreatedAt').as('endDate')) + .select((eb) => eb.fn.count('assets.id').as('assetCount')) + .where('albums.id', 'in', ids) + .groupBy('albums.id') + .execute(); - return albumMetadatas.map<AlbumAssetCount>((metadatas) => ({ - albumId: metadatas['album_id'], - assetCount: Number(metadatas['asset_count']), - startDate: metadatas['end_date'] ? new Date(metadatas['start_date']) : undefined, - endDate: metadatas['end_date'] ? new Date(metadatas['end_date']) : undefined, + return metadatas.map((metadatas) => ({ + albumId: metadatas.id, + assetCount: Number(metadatas.assetCount), + startDate: metadatas.startDate ? new Date(metadatas.startDate) : undefined, + endDate: metadatas.endDate ? new Date(metadatas.endDate) : undefined, })); } @GenerateSql({ params: [DummyValue.UUID] }) async getOwned(ownerId: string): Promise<AlbumEntity[]> { - const albums = await this.repository.find({ - relations: { albumUsers: { user: true }, sharedLinks: true, owner: true }, - where: { ownerId }, - order: { createdAt: 'DESC' }, - }); - - return albums.map((album) => withoutDeletedUsers(album)); + return this.db + .selectFrom('albums') + .selectAll('albums') + .select(withOwner) + .select(withAlbumUsers) + .select(withSharedLink) + .where('albums.ownerId', '=', ownerId) + .where('albums.deletedAt', 'is', null) + .orderBy('albums.createdAt', 'desc') + .execute() as unknown as Promise<AlbumEntity[]>; } /** @@ -114,17 +160,25 @@ export class AlbumRepository implements IAlbumRepository { */ @GenerateSql({ params: [DummyValue.UUID] }) async getShared(ownerId: string): Promise<AlbumEntity[]> { - const albums = await this.repository.find({ - relations: { albumUsers: { user: true }, sharedLinks: true, owner: true }, - where: [ - { albumUsers: { userId: ownerId } }, - { sharedLinks: { userId: ownerId } }, - { ownerId, albumUsers: { user: Not(IsNull()) } }, - ], - order: { createdAt: 'DESC' }, - }); - - return albums.map((album) => withoutDeletedUsers(album)); + return this.db + .selectFrom('albums') + .selectAll('albums') + .distinctOn('albums.createdAt') + .leftJoin('albums_shared_users_users as shared_albums', 'shared_albums.albumsId', 'albums.id') + .leftJoin('shared_links', 'shared_links.albumId', 'albums.id') + .where((eb) => + eb.or([ + eb('shared_albums.usersId', '=', ownerId), + eb('shared_links.userId', '=', ownerId), + eb.and([eb('albums.ownerId', '=', ownerId), eb('shared_albums.usersId', 'is not', null)]), + ]), + ) + .where('albums.deletedAt', 'is', null) + .select(withAlbumUsers) + .select(withOwner) + .select(withSharedLink) + .orderBy('albums.createdAt', 'desc') + .execute() as unknown as Promise<AlbumEntity[]>; } /** @@ -132,35 +186,37 @@ export class AlbumRepository implements IAlbumRepository { */ @GenerateSql({ params: [DummyValue.UUID] }) async getNotShared(ownerId: string): Promise<AlbumEntity[]> { - const albums = await this.repository.find({ - relations: { albumUsers: true, sharedLinks: true, owner: true }, - where: { ownerId, albumUsers: { user: IsNull() }, sharedLinks: { id: IsNull() } }, - order: { createdAt: 'DESC' }, - }); - - return albums.map((album) => withoutDeletedUsers(album)); + return this.db + .selectFrom('albums') + .selectAll('albums') + .distinctOn('albums.createdAt') + .leftJoin('albums_shared_users_users as shared_albums', 'shared_albums.albumsId', 'albums.id') + .leftJoin('shared_links', 'shared_links.albumId', 'albums.id') + .where('albums.ownerId', '=', ownerId) + .where('shared_albums.usersId', 'is', null) + .where('shared_links.userId', 'is', null) + .where('albums.deletedAt', 'is', null) + .select(withAlbumUsers) + .select(withOwner) + .select(withSharedLink) + .orderBy('albums.createdAt', 'desc') + .execute() as unknown as Promise<AlbumEntity[]>; } async restoreAll(userId: string): Promise<void> { - await this.repository.restore({ ownerId: userId }); + await this.db.updateTable('albums').set({ deletedAt: null }).where('ownerId', '=', userId).execute(); } async softDeleteAll(userId: string): Promise<void> { - await this.repository.softDelete({ ownerId: userId }); + await this.db.updateTable('albums').set({ deletedAt: new Date() }).where('ownerId', '=', userId).execute(); } async deleteAll(userId: string): Promise<void> { - await this.repository.delete({ ownerId: userId }); + await this.db.deleteFrom('albums').where('ownerId', '=', userId).execute(); } async removeAsset(assetId: string): Promise<void> { - // Using dataSource, because there is no direct access to albums_assets_assets. - await this.dataSource - .createQueryBuilder() - .delete() - .from('albums_assets_assets') - .where('"albums_assets_assets"."assetsId" = :assetId', { assetId }) - .execute(); + await this.db.deleteFrom('albums_assets_assets').where('albums_assets_assets.assetsId', '=', assetId).execute(); } @Chunked({ paramIndex: 1 }) @@ -169,14 +225,10 @@ export class AlbumRepository implements IAlbumRepository { return; } - await this.dataSource - .createQueryBuilder() - .delete() - .from('albums_assets_assets') - .where({ - albumsId: albumId, - assetsId: In(assetIds), - }) + await this.db + .deleteFrom('albums_assets_assets') + .where('albums_assets_assets.albumsId', '=', albumId) + .where('albums_assets_assets.assetsId', 'in', assetIds) .execute(); } @@ -194,73 +246,80 @@ export class AlbumRepository implements IAlbumRepository { return new Set(); } - const results = await this.dataSource - .createQueryBuilder() - .select('albums_assets.assetsId', 'assetId') - .from('albums_assets_assets', 'albums_assets') - .where('"albums_assets"."albumsId" = :albumId', { albumId }) - .andWhere('"albums_assets"."assetsId" IN (:...assetIds)', { assetIds }) - .getRawMany<{ assetId: string }>(); - - return new Set(results.map(({ assetId }) => assetId)); + return this.db + .selectFrom('albums_assets_assets') + .selectAll() + .where('albums_assets_assets.albumsId', '=', albumId) + .where('albums_assets_assets.assetsId', 'in', assetIds) + .execute() + .then((results) => new Set(results.map(({ assetsId }) => assetsId))); } async addAssetIds(albumId: string, assetIds: string[]): Promise<void> { - await this.addAssets(this.dataSource.manager, albumId, assetIds); + await this.addAssets(this.db, albumId, assetIds); } - create(album: Partial<AlbumEntity>): Promise<AlbumEntity> { - return this.dataSource.transaction<AlbumEntity>(async (manager) => { - const { id } = await manager.save(AlbumEntity, { ...album, assets: [] }); - const assetIds = (album.assets || []).map((asset) => asset.id); - await this.addAssets(manager, id, assetIds); - return manager.findOneOrFail(AlbumEntity, { - where: { id }, - relations: { - owner: true, - albumUsers: { user: true }, - sharedLinks: true, - assets: true, - }, - }); + create(album: Insertable<Albums>, assetIds: string[], albumUsers: AlbumUserCreateDto[]): Promise<AlbumEntity> { + return this.db.transaction().execute(async (tx) => { + const newAlbum = await tx.insertInto('albums').values(album).returning('albums.id').executeTakeFirst(); + + if (!newAlbum) { + throw new Error('Failed to create album'); + } + + if (assetIds.length > 0) { + await this.addAssets(tx, newAlbum.id, assetIds); + } + + if (albumUsers.length > 0) { + await tx + .insertInto('albums_shared_users_users') + .values( + albumUsers.map((albumUser) => ({ albumsId: newAlbum.id, usersId: albumUser.userId, role: albumUser.role })), + ) + .execute(); + } + + return tx + .selectFrom('albums') + .selectAll() + .where('id', '=', newAlbum.id) + .select(withOwner) + .select(withSharedLink) + .select(withAssets) + .select(withAlbumUsers) + .executeTakeFirst() as unknown as Promise<AlbumEntity>; }); } - update(album: Partial<AlbumEntity>): Promise<AlbumEntity> { - return this.save(album); + update(id: string, album: Updateable<Albums>): Promise<AlbumEntity> { + return this.db + .updateTable('albums') + .set({ ...album, updatedAt: new Date() }) + .where('id', '=', id) + .returningAll('albums') + .returning(withOwner) + .returning(withSharedLink) + .returning(withAlbumUsers) + .executeTakeFirst() as unknown as Promise<AlbumEntity>; } async delete(id: string): Promise<void> { - await this.repository.delete({ id }); + await this.db.deleteFrom('albums').where('id', '=', id).execute(); } @Chunked({ paramIndex: 2, chunkSize: 30_000 }) - private async addAssets(manager: EntityManager, albumId: string, assetIds: string[]): Promise<void> { + private async addAssets(db: Kysely<DB>, albumId: string, assetIds: string[]): Promise<void> { if (assetIds.length === 0) { return; } - await manager - .createQueryBuilder() - .insert() - .into('albums_assets_assets', ['albumsId', 'assetsId']) + await db + .insertInto('albums_assets_assets') .values(assetIds.map((assetId) => ({ albumsId: albumId, assetsId: assetId }))) .execute(); } - private async save(album: Partial<AlbumEntity>) { - const { id } = await this.repository.save(album); - return this.repository.findOneOrFail({ - where: { id }, - relations: { - owner: true, - albumUsers: { user: true }, - sharedLinks: true, - assets: true, - }, - }); - } - /** * Makes sure all thumbnails for albums are updated by: * - Removing thumbnails from albums without assets @@ -272,28 +331,44 @@ export class AlbumRepository implements IAlbumRepository { async updateThumbnails(): Promise<number | undefined> { // Subquery for getting a new thumbnail. - const builder = this.dataSource - .createQueryBuilder('albums_assets_assets', 'album_assets') - .innerJoin('assets', 'assets', '"album_assets"."assetsId" = "assets"."id"') - .where('"album_assets"."albumsId" = "albums"."id"'); + const result = await this.db + .updateTable('albums') + .set((eb) => ({ + albumThumbnailAssetId: this.updateThumbnailBuilder(eb) + .select('album_assets.assetsId') + .orderBy('assets.fileCreatedAt', 'desc') + .limit(1), + updatedAt: new Date(), + })) + .where((eb) => + eb.or([ + eb.and([ + eb('albumThumbnailAssetId', 'is', null), + eb.exists(this.updateThumbnailBuilder(eb).select(sql`1`.as('1'))), // Has assets + ]), + eb.and([ + eb('albumThumbnailAssetId', 'is not', null), + eb.not( + eb.exists( + this.updateThumbnailBuilder(eb) + .select(sql`1`.as('1')) + .whereRef('albums.albumThumbnailAssetId', '=', 'album_assets.assetsId'), // Has invalid assets + ), + ), + ]), + ]), + ) + .execute(); - const newThumbnail = builder - .clone() - .select('"album_assets"."assetsId"') - .orderBy('"assets"."fileCreatedAt"', 'DESC') - .limit(1); - const hasAssets = builder.clone().select('1'); - const hasInvalidAsset = hasAssets.clone().andWhere('"albums"."albumThumbnailAssetId" = "album_assets"."assetsId"'); + return Number(result[0].numUpdatedRows); + } - const updateAlbums = this.repository - .createQueryBuilder('albums') - .update(AlbumEntity) - .set({ albumThumbnailAssetId: () => `(${newThumbnail.getQuery()})` }) - .where(`"albums"."albumThumbnailAssetId" IS NULL AND EXISTS (${hasAssets.getQuery()})`) - .orWhere(`"albums"."albumThumbnailAssetId" IS NOT NULL AND NOT EXISTS (${hasInvalidAsset.getQuery()})`); - - const result = await updateAlbums.execute(); - - return result.affected; + private updateThumbnailBuilder(eb: ExpressionBuilder<DB, 'albums'>) { + return eb + .selectFrom('albums_assets_assets as album_assets') + .innerJoin('assets', (join) => + join.onRef('album_assets.assetsId', '=', 'assets.id').on('assets.deletedAt', 'is', null), + ) + .whereRef('album_assets.albumsId', '=', 'albums.id'); } } diff --git a/server/src/services/album.service.spec.ts b/server/src/services/album.service.spec.ts index ca6b56e085..99c794adc9 100644 --- a/server/src/services/album.service.spec.ts +++ b/server/src/services/album.service.spec.ts @@ -135,14 +135,17 @@ describe(AlbumService.name, () => { assetIds: ['123'], }); - expect(albumMock.create).toHaveBeenCalledWith({ - ownerId: authStub.admin.user.id, - albumName: albumStub.empty.albumName, - description: albumStub.empty.description, - albumUsers: [{ userId: 'user-id', role: AlbumUserRole.EDITOR }], - assets: [{ id: '123' }], - albumThumbnailAssetId: '123', - }); + expect(albumMock.create).toHaveBeenCalledWith( + { + ownerId: authStub.admin.user.id, + albumName: albumStub.empty.albumName, + description: albumStub.empty.description, + + albumThumbnailAssetId: '123', + }, + ['123'], + [{ userId: 'user-id', role: AlbumUserRole.EDITOR }], + ); expect(userMock.get).toHaveBeenCalledWith('user-id', {}); expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['123'])); @@ -175,14 +178,17 @@ describe(AlbumService.name, () => { assetIds: ['asset-1', 'asset-2'], }); - expect(albumMock.create).toHaveBeenCalledWith({ - ownerId: authStub.admin.user.id, - albumName: 'Test album', - description: '', - albumUsers: [], - assets: [{ id: 'asset-1' }], - albumThumbnailAssetId: 'asset-1', - }); + expect(albumMock.create).toHaveBeenCalledWith( + { + ownerId: authStub.admin.user.id, + albumName: 'Test album', + description: '', + + albumThumbnailAssetId: 'asset-1', + }, + ['asset-1'], + [], + ); expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith( authStub.admin.user.id, new Set(['asset-1', 'asset-2']), @@ -192,7 +198,7 @@ describe(AlbumService.name, () => { describe('update', () => { it('should prevent updating an album that does not exist', async () => { - albumMock.getById.mockResolvedValue(null); + albumMock.getById.mockResolvedValue(void 0); await expect( sut.update(authStub.user1, 'invalid-id', { @@ -238,7 +244,7 @@ describe(AlbumService.name, () => { }); expect(albumMock.update).toHaveBeenCalledTimes(1); - expect(albumMock.update).toHaveBeenCalledWith({ + expect(albumMock.update).toHaveBeenCalledWith('album-4', { id: 'album-4', albumName: 'new album name', }); @@ -344,7 +350,7 @@ describe(AlbumService.name, () => { describe('removeUser', () => { it('should require a valid album id', async () => { accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-1'])); - albumMock.getById.mockResolvedValue(null); + albumMock.getById.mockResolvedValue(void 0); await expect(sut.removeUser(authStub.admin, 'album-1', 'user-1')).rejects.toBeInstanceOf(BadRequestException); expect(albumMock.update).not.toHaveBeenCalled(); }); @@ -529,7 +535,7 @@ describe(AlbumService.name, () => { { success: true, id: 'asset-3' }, ]); - expect(albumMock.update).toHaveBeenCalledWith({ + expect(albumMock.update).toHaveBeenCalledWith('album-123', { id: 'album-123', updatedAt: expect.any(Date), albumThumbnailAssetId: 'asset-1', @@ -547,7 +553,7 @@ describe(AlbumService.name, () => { { success: true, id: 'asset-1' }, ]); - expect(albumMock.update).toHaveBeenCalledWith({ + expect(albumMock.update).toHaveBeenCalledWith('album-123', { id: 'album-123', updatedAt: expect.any(Date), albumThumbnailAssetId: 'asset-id', @@ -569,7 +575,7 @@ describe(AlbumService.name, () => { { success: true, id: 'asset-3' }, ]); - expect(albumMock.update).toHaveBeenCalledWith({ + expect(albumMock.update).toHaveBeenCalledWith('album-123', { id: 'album-123', updatedAt: expect.any(Date), albumThumbnailAssetId: 'asset-1', @@ -606,7 +612,7 @@ describe(AlbumService.name, () => { { success: true, id: 'asset-3' }, ]); - expect(albumMock.update).toHaveBeenCalledWith({ + expect(albumMock.update).toHaveBeenCalledWith('album-123', { id: 'album-123', updatedAt: expect.any(Date), albumThumbnailAssetId: 'asset-1', @@ -629,7 +635,7 @@ describe(AlbumService.name, () => { { success: true, id: 'asset-1' }, ]); - expect(albumMock.update).toHaveBeenCalledWith({ + expect(albumMock.update).toHaveBeenCalledWith('album-123', { id: 'album-123', updatedAt: expect.any(Date), albumThumbnailAssetId: 'asset-1', @@ -696,7 +702,6 @@ describe(AlbumService.name, () => { { success: true, id: 'asset-id' }, ]); - expect(albumMock.update).toHaveBeenCalledWith({ id: 'album-123', updatedAt: expect.any(Date) }); expect(albumMock.removeAssetIds).toHaveBeenCalledWith('album-123', ['asset-id']); }); @@ -720,8 +725,6 @@ describe(AlbumService.name, () => { await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([ { success: true, id: 'asset-id' }, ]); - - expect(albumMock.update).toHaveBeenCalledWith({ id: 'album-123', updatedAt: expect.any(Date) }); }); it('should reset the thumbnail if it is removed', async () => { @@ -734,10 +737,6 @@ describe(AlbumService.name, () => { { success: true, id: 'asset-id' }, ]); - expect(albumMock.update).toHaveBeenCalledWith({ - id: 'album-123', - updatedAt: expect.any(Date), - }); expect(albumMock.updateThumbnails).toHaveBeenCalled(); }); }); diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index f5685f84eb..efc71c4c8d 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -15,7 +15,6 @@ import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { AlbumUserEntity } from 'src/entities/album-user.entity'; import { AlbumEntity } from 'src/entities/album.entity'; -import { AssetEntity } from 'src/entities/asset.entity'; import { Permission } from 'src/enum'; import { AlbumAssetCount, AlbumInfoOptions } from 'src/interfaces/album.interface'; import { BaseService } from 'src/services/base.service'; @@ -112,16 +111,18 @@ export class AlbumService extends BaseService { permission: Permission.ASSET_SHARE, ids: dto.assetIds || [], }); - const assets = [...allowedAssetIdsSet].map((id) => ({ id }) as AssetEntity); + const assetIds = [...allowedAssetIdsSet].map((id) => id); - const album = await this.albumRepository.create({ - ownerId: auth.user.id, - albumName: dto.albumName, - description: dto.description, - albumUsers: albumUsers.map((albumUser) => albumUser as AlbumUserEntity) ?? [], - assets, - albumThumbnailAssetId: assets[0]?.id || null, - }); + const album = await this.albumRepository.create( + { + ownerId: auth.user.id, + albumName: dto.albumName, + description: dto.description, + albumThumbnailAssetId: assetIds[0] || null, + }, + assetIds, + albumUsers, + ); for (const { userId } of albumUsers) { await this.eventRepository.emit('album.invite', { id: album.id, userId }); @@ -141,7 +142,7 @@ export class AlbumService extends BaseService { throw new BadRequestException('Invalid album thumbnail'); } } - const updatedAlbum = await this.albumRepository.update({ + const updatedAlbum = await this.albumRepository.update(album.id, { id: album.id, albumName: dto.albumName, description: dto.description, @@ -170,7 +171,7 @@ export class AlbumService extends BaseService { const { id: firstNewAssetId } = results.find(({ success }) => success) || {}; if (firstNewAssetId) { - await this.albumRepository.update({ + await this.albumRepository.update(id, { id, updatedAt: new Date(), albumThumbnailAssetId: album.albumThumbnailAssetId ?? firstNewAssetId, @@ -199,11 +200,8 @@ export class AlbumService extends BaseService { ); const removedIds = results.filter(({ success }) => success).map(({ id }) => id); - if (removedIds.length > 0) { - await this.albumRepository.update({ id, updatedAt: new Date() }); - if (album.albumThumbnailAssetId && removedIds.includes(album.albumThumbnailAssetId)) { - await this.albumRepository.updateThumbnails(); - } + if (removedIds.length > 0 && album.albumThumbnailAssetId && removedIds.includes(album.albumThumbnailAssetId)) { + await this.albumRepository.updateThumbnails(); } return results;