From 4164bcfd0db966c321bc6d3399e2a9af212c84f1 Mon Sep 17 00:00:00 2001 From: Michael Manganiello Date: Tue, 6 Feb 2024 17:03:25 -0500 Subject: [PATCH] chore(server): Use ChunkedSet in Access repository (#6943) This change simplifies the Access repository methods, by using the `ChunkedSet` decorator. As the methods expect sets, the `chunks` util needed to be fixed, so it returns chunks of the same type it received. Now `chunks` is overloaded, to have proper typing based on the input parameter. --- server/src/domain/domain.util.ts | 16 +- .../infra/repositories/access.repository.ts | 499 ++++++++---------- 2 files changed, 233 insertions(+), 282 deletions(-) diff --git a/server/src/domain/domain.util.ts b/server/src/domain/domain.util.ts index ba5942cea4..43efbce255 100644 --- a/server/src/domain/domain.util.ts +++ b/server/src/domain/domain.util.ts @@ -178,23 +178,25 @@ export function Optional({ nullable, ...validationOptions }: OptionalOptions = { } /** - * Chunks an array or set into smaller arrays of the specified size. + * Chunks an array or set into smaller collections of the same type and specified size. * * @param collection The collection to chunk. * @param size The size of each chunk. */ -export function chunks(collection: Array | Set, size: number): T[][] { +export function chunks(collection: Array, size: number): Array>; +export function chunks(collection: Set, size: number): Array>; +export function chunks(collection: Array | Set, size: number): Array> | Array> { if (collection instanceof Set) { const result = []; - let chunk = []; + let chunk = new Set(); for (const element of collection) { - chunk.push(element); - if (chunk.length === size) { + chunk.add(element); + if (chunk.size === size) { result.push(chunk); - chunk = []; + chunk = new Set(); } } - if (chunk.length > 0) { + if (chunk.size > 0) { result.push(chunk); } return result; diff --git a/server/src/infra/repositories/access.repository.ts b/server/src/infra/repositories/access.repository.ts index cb6469195e..fea1985ced 100644 --- a/server/src/infra/repositories/access.repository.ts +++ b/server/src/infra/repositories/access.repository.ts @@ -1,4 +1,4 @@ -import { IAccessRepository, chunks, setUnion } from '@app/domain'; +import { IAccessRepository } from '@app/domain'; import { InjectRepository } from '@nestjs/typeorm'; import { Brackets, In, Repository } from 'typeorm'; import { @@ -12,7 +12,8 @@ import { SharedLinkEntity, UserTokenEntity, } from '../entities'; -import { DATABASE_PARAMETER_CHUNK_SIZE, DummyValue, GenerateSql } from '../infra.util'; +import { DummyValue, GenerateSql } from '../infra.util'; +import { ChunkedSet } from '../infra.utils'; type IActivityAccess = IAccessRepository['activity']; type IAlbumAccess = IAccessRepository['album']; @@ -30,72 +31,63 @@ class ActivityAccess implements IActivityAccess { ) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) async checkOwnerAccess(userId: string, activityIds: Set): Promise> { if (activityIds.size === 0) { return new Set(); } - return Promise.all( - chunks(activityIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.activityRepository - .find({ - select: { id: true }, - where: { - id: In(idChunk), - userId, - }, - }) - .then((activities) => new Set(activities.map((activity) => activity.id))), - ), - ).then((results) => setUnion(...results)); + return this.activityRepository + .find({ + select: { id: true }, + where: { + id: In([...activityIds]), + userId, + }, + }) + .then((activities) => new Set(activities.map((activity) => activity.id))); } @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) async checkAlbumOwnerAccess(userId: string, activityIds: Set): Promise> { if (activityIds.size === 0) { return new Set(); } - return Promise.all( - chunks(activityIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.activityRepository - .find({ - select: { id: true }, - where: { - id: In(idChunk), - album: { - ownerId: userId, - }, - }, - }) - .then((activities) => new Set(activities.map((activity) => activity.id))), - ), - ).then((results) => setUnion(...results)); + return this.activityRepository + .find({ + select: { id: true }, + where: { + id: In([...activityIds]), + album: { + ownerId: userId, + }, + }, + }) + .then((activities) => new Set(activities.map((activity) => activity.id))); } @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) async checkCreateAccess(userId: string, albumIds: Set): Promise> { if (albumIds.size === 0) { return new Set(); } - return Promise.all( - chunks(albumIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.albumRepository - .createQueryBuilder('album') - .select('album.id') - .leftJoin('album.sharedUsers', 'sharedUsers') - .where('album.id IN (:...albumIds)', { albumIds: idChunk }) - .andWhere('album.isActivityEnabled = true') - .andWhere( - new Brackets((qb) => { - qb.where('album.ownerId = :userId', { userId }).orWhere('sharedUsers.id = :userId', { userId }); - }), - ) - .getMany() - .then((albums) => new Set(albums.map((album) => album.id))), - ), - ).then((results) => setUnion(...results)); + return this.albumRepository + .createQueryBuilder('album') + .select('album.id') + .leftJoin('album.sharedUsers', 'sharedUsers') + .where('album.id IN (:...albumIds)', { albumIds: [...albumIds] }) + .andWhere('album.isActivityEnabled = true') + .andWhere( + new Brackets((qb) => { + qb.where('album.ownerId = :userId', { userId }).orWhere('sharedUsers.id = :userId', { userId }); + }), + ) + .getMany() + .then((albums) => new Set(albums.map((album) => album.id))); } } @@ -106,71 +98,61 @@ class AlbumAccess implements IAlbumAccess { ) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) async checkOwnerAccess(userId: string, albumIds: Set): Promise> { if (albumIds.size === 0) { return new Set(); } - return Promise.all( - chunks(albumIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.albumRepository - .find({ - select: { id: true }, - where: { - id: In(idChunk), - ownerId: userId, - }, - }) - .then((albums) => new Set(albums.map((album) => album.id))), - ), - ).then((results) => setUnion(...results)); + return this.albumRepository + .find({ + select: { id: true }, + where: { + id: In([...albumIds]), + ownerId: userId, + }, + }) + .then((albums) => new Set(albums.map((album) => album.id))); } @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) async checkSharedAlbumAccess(userId: string, albumIds: Set): Promise> { if (albumIds.size === 0) { return new Set(); } - return Promise.all( - chunks(albumIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.albumRepository - .find({ - select: { id: true }, - where: { - id: In(idChunk), - sharedUsers: { - id: userId, - }, - }, - }) - .then((albums) => new Set(albums.map((album) => album.id))), - ), - ).then((results) => setUnion(...results)); + return this.albumRepository + .find({ + select: { id: true }, + where: { + id: In([...albumIds]), + sharedUsers: { + id: userId, + }, + }, + }) + .then((albums) => new Set(albums.map((album) => album.id))); } @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) async checkSharedLinkAccess(sharedLinkId: string, albumIds: Set): Promise> { if (albumIds.size === 0) { return new Set(); } - return Promise.all( - chunks(albumIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.sharedLinkRepository - .find({ - select: { albumId: true }, - where: { - id: sharedLinkId, - albumId: In(idChunk), - }, - }) - .then( - (sharedLinks) => - new Set(sharedLinks.flatMap((sharedLink) => (sharedLink.albumId ? [sharedLink.albumId] : []))), - ), - ), - ).then((results) => setUnion(...results)); + return this.sharedLinkRepository + .find({ + select: { albumId: true }, + where: { + id: sharedLinkId, + albumId: In([...albumIds]), + }, + }) + .then( + (sharedLinks) => new Set(sharedLinks.flatMap((sharedLink) => (sharedLink.albumId ? [sharedLink.albumId] : []))), + ); } } @@ -183,132 +165,120 @@ class AssetAccess implements IAssetAccess { ) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) async checkAlbumAccess(userId: string, assetIds: Set): Promise> { if (assetIds.size === 0) { return new Set(); } - return Promise.all( - chunks(assetIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.albumRepository - .createQueryBuilder('album') - .innerJoin('album.assets', 'asset') - .leftJoin('album.sharedUsers', 'sharedUsers') - .select('asset.id', 'assetId') - .addSelect('asset.livePhotoVideoId', 'livePhotoVideoId') - .where('array["asset"."id", "asset"."livePhotoVideoId"] && array[:...assetIds]::uuid[]', { - assetIds: idChunk, - }) - .andWhere( - new Brackets((qb) => { - qb.where('album.ownerId = :userId', { userId }).orWhere('sharedUsers.id = :userId', { userId }); - }), - ) - .getRawMany() - .then((rows) => { - const allowedIds = new Set(); - 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; - }), - ), - ).then((results) => setUnion(...results)); + return this.albumRepository + .createQueryBuilder('album') + .innerJoin('album.assets', 'asset') + .leftJoin('album.sharedUsers', 'sharedUsers') + .select('asset.id', 'assetId') + .addSelect('asset.livePhotoVideoId', 'livePhotoVideoId') + .where('array["asset"."id", "asset"."livePhotoVideoId"] && array[:...assetIds]::uuid[]', { + assetIds: [...assetIds], + }) + .andWhere( + new Brackets((qb) => { + qb.where('album.ownerId = :userId', { userId }).orWhere('sharedUsers.id = :userId', { userId }); + }), + ) + .getRawMany() + .then((rows) => { + const allowedIds = new Set(); + 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; + }); } @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) async checkOwnerAccess(userId: string, assetIds: Set): Promise> { if (assetIds.size === 0) { return new Set(); } - return Promise.all( - chunks(assetIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.assetRepository - .find({ - select: { id: true }, - where: { - id: In(idChunk), - ownerId: userId, - }, - withDeleted: true, - }) - .then((assets) => new Set(assets.map((asset) => asset.id))), - ), - ).then((results) => setUnion(...results)); + return this.assetRepository + .find({ + select: { id: true }, + where: { + id: In([...assetIds]), + ownerId: userId, + }, + withDeleted: true, + }) + .then((assets) => new Set(assets.map((asset) => asset.id))); } @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) async checkPartnerAccess(userId: string, assetIds: Set): Promise> { if (assetIds.size === 0) { return new Set(); } - return Promise.all( - chunks(assetIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - 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: idChunk }) - .getRawMany() - .then((rows) => new Set(rows.map((row) => row.assetId))), - ), - ).then((results) => setUnion(...results)); + 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))); } @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) async checkSharedLinkAccess(sharedLinkId: string, assetIds: Set): Promise> { if (assetIds.size === 0) { return new Set(); } - return Promise.all( - chunks(assetIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - 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( - 'array["assets"."id", "assets"."livePhotoVideoId", "albumAssets"."id", "albumAssets"."livePhotoVideoId"] && array[:...assetIds]::uuid[]', - { - assetIds: idChunk, - }, - ) - .getRawMany() - .then((rows) => { - const allowedIds = new Set(); - 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; - }), - ), - ).then((results) => setUnion(...results)); + 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( + 'array["assets"."id", "assets"."livePhotoVideoId", "albumAssets"."id", "albumAssets"."livePhotoVideoId"] && array[:...assetIds]::uuid[]', + { + assetIds: [...assetIds], + }, + ) + .getRawMany() + .then((rows) => { + const allowedIds = new Set(); + 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; + }); } } @@ -316,24 +286,21 @@ class AuthDeviceAccess implements IAuthDeviceAccess { constructor(private tokenRepository: Repository) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) async checkOwnerAccess(userId: string, deviceIds: Set): Promise> { if (deviceIds.size === 0) { return new Set(); } - return Promise.all( - chunks(deviceIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.tokenRepository - .find({ - select: { id: true }, - where: { - userId, - id: In(idChunk), - }, - }) - .then((tokens) => new Set(tokens.map((token) => token.id))), - ), - ).then((results) => setUnion(...results)); + return this.tokenRepository + .find({ + select: { id: true }, + where: { + userId, + id: In([...deviceIds]), + }, + }) + .then((tokens) => new Set(tokens.map((token) => token.id))); } } @@ -344,43 +311,37 @@ class LibraryAccess implements ILibraryAccess { ) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) async checkOwnerAccess(userId: string, libraryIds: Set): Promise> { if (libraryIds.size === 0) { return new Set(); } - return Promise.all( - chunks(libraryIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.libraryRepository - .find({ - select: { id: true }, - where: { - id: In(idChunk), - ownerId: userId, - }, - }) - .then((libraries) => new Set(libraries.map((library) => library.id))), - ), - ).then((results) => setUnion(...results)); + return this.libraryRepository + .find({ + select: { id: true }, + where: { + id: In([...libraryIds]), + ownerId: userId, + }, + }) + .then((libraries) => new Set(libraries.map((library) => library.id))); } @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) async checkPartnerAccess(userId: string, partnerIds: Set): Promise> { if (partnerIds.size === 0) { return new Set(); } - return Promise.all( - chunks(partnerIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.partnerRepository - .createQueryBuilder('partner') - .select('partner.sharedById') - .where('partner.sharedById IN (:...partnerIds)', { partnerIds: idChunk }) - .andWhere('partner.sharedWithId = :userId', { userId }) - .getMany() - .then((partners) => new Set(partners.map((partner) => partner.sharedById))), - ), - ).then((results) => setUnion(...results)); + return this.partnerRepository + .createQueryBuilder('partner') + .select('partner.sharedById') + .where('partner.sharedById IN (:...partnerIds)', { partnerIds: [...partnerIds] }) + .andWhere('partner.sharedWithId = :userId', { userId }) + .getMany() + .then((partners) => new Set(partners.map((partner) => partner.sharedById))); } } @@ -388,22 +349,19 @@ class TimelineAccess implements ITimelineAccess { constructor(private partnerRepository: Repository) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) async checkPartnerAccess(userId: string, partnerIds: Set): Promise> { if (partnerIds.size === 0) { return new Set(); } - return Promise.all( - chunks(partnerIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.partnerRepository - .createQueryBuilder('partner') - .select('partner.sharedById') - .where('partner.sharedById IN (:...partnerIds)', { partnerIds: idChunk }) - .andWhere('partner.sharedWithId = :userId', { userId }) - .getMany() - .then((partners) => new Set(partners.map((partner) => partner.sharedById))), - ), - ).then((results) => setUnion(...results)); + return this.partnerRepository + .createQueryBuilder('partner') + .select('partner.sharedById') + .where('partner.sharedById IN (:...partnerIds)', { partnerIds: [...partnerIds] }) + .andWhere('partner.sharedWithId = :userId', { userId }) + .getMany() + .then((partners) => new Set(partners.map((partner) => partner.sharedById))); } } @@ -414,47 +372,41 @@ class PersonAccess implements IPersonAccess { ) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) async checkOwnerAccess(userId: string, personIds: Set): Promise> { if (personIds.size === 0) { return new Set(); } - return Promise.all( - chunks(personIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.personRepository - .find({ - select: { id: true }, - where: { - id: In(idChunk), - ownerId: userId, - }, - }) - .then((persons) => new Set(persons.map((person) => person.id))), - ), - ).then((results) => setUnion(...results)); + return this.personRepository + .find({ + select: { id: true }, + where: { + id: In([...personIds]), + ownerId: userId, + }, + }) + .then((persons) => new Set(persons.map((person) => person.id))); } @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) async checkFaceOwnerAccess(userId: string, assetFaceIds: Set): Promise> { if (assetFaceIds.size === 0) { return new Set(); } - return Promise.all( - chunks(assetFaceIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.assetFaceRepository - .find({ - select: { id: true }, - where: { - id: In(idChunk), - asset: { - ownerId: userId, - }, - }, - }) - .then((faces) => new Set(faces.map((face) => face.id))), - ), - ).then((results) => setUnion(...results)); + return this.assetFaceRepository + .find({ + select: { id: true }, + where: { + id: In([...assetFaceIds]), + asset: { + ownerId: userId, + }, + }, + }) + .then((faces) => new Set(faces.map((face) => face.id))); } } @@ -462,22 +414,19 @@ class PartnerAccess implements IPartnerAccess { constructor(private partnerRepository: Repository) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) async checkUpdateAccess(userId: string, partnerIds: Set): Promise> { if (partnerIds.size === 0) { return new Set(); } - return Promise.all( - chunks(partnerIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) => - this.partnerRepository - .createQueryBuilder('partner') - .select('partner.sharedById') - .where('partner.sharedById IN (:...partnerIds)', { partnerIds: idChunk }) - .andWhere('partner.sharedWithId = :userId', { userId }) - .getMany() - .then((partners) => new Set(partners.map((partner) => partner.sharedById))), - ), - ).then((results) => setUnion(...results)); + return this.partnerRepository + .createQueryBuilder('partner') + .select('partner.sharedById') + .where('partner.sharedById IN (:...partnerIds)', { partnerIds: [...partnerIds] }) + .andWhere('partner.sharedWithId = :userId', { userId }) + .getMany() + .then((partners) => new Set(partners.map((partner) => partner.sharedById))); } }