mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +00:00
feat(server): improve and refactor get all albums (#2048)
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
@@ -2,8 +2,19 @@ import { AlbumEntity } from '@app/infra/db/entities';
|
||||
|
||||
export const IAlbumRepository = 'IAlbumRepository';
|
||||
|
||||
export interface AlbumAssetCount {
|
||||
albumId: string;
|
||||
assetCount: number;
|
||||
}
|
||||
|
||||
export interface IAlbumRepository {
|
||||
getByIds(ids: string[]): Promise<AlbumEntity[]>;
|
||||
getByAssetId(ownerId: string, assetId: string): Promise<AlbumEntity[]>;
|
||||
getAssetCountForIds(ids: string[]): Promise<AlbumAssetCount[]>;
|
||||
getInvalidThumbnail(): Promise<string[]>;
|
||||
getOwned(ownerId: string): Promise<AlbumEntity[]>;
|
||||
getShared(ownerId: string): Promise<AlbumEntity[]>;
|
||||
getNotShared(ownerId: string): Promise<AlbumEntity[]>;
|
||||
deleteAll(userId: string): Promise<void>;
|
||||
getAll(): Promise<AlbumEntity[]>;
|
||||
save(album: Partial<AlbumEntity>): Promise<AlbumEntity>;
|
||||
|
||||
114
server/libs/domain/src/album/album.service.spec.ts
Normal file
114
server/libs/domain/src/album/album.service.spec.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { albumStub, authStub, newAlbumRepositoryMock, newAssetRepositoryMock } from '../../test';
|
||||
import { IAssetRepository } from '../asset';
|
||||
import { IAlbumRepository } from './album.repository';
|
||||
import { AlbumService } from './album.service';
|
||||
|
||||
describe(AlbumService.name, () => {
|
||||
let sut: AlbumService;
|
||||
let albumMock: jest.Mocked<IAlbumRepository>;
|
||||
let assetMock: jest.Mocked<IAssetRepository>;
|
||||
|
||||
beforeEach(async () => {
|
||||
albumMock = newAlbumRepositoryMock();
|
||||
assetMock = newAssetRepositoryMock();
|
||||
|
||||
sut = new AlbumService(albumMock, assetMock);
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
expect(sut).toBeDefined();
|
||||
});
|
||||
|
||||
describe('get list of albums', () => {
|
||||
it('gets list of albums for auth user', async () => {
|
||||
albumMock.getOwned.mockResolvedValue([albumStub.empty, albumStub.sharedWithUser]);
|
||||
albumMock.getAssetCountForIds.mockResolvedValue([
|
||||
{ albumId: albumStub.empty.id, assetCount: 0 },
|
||||
{ albumId: albumStub.sharedWithUser.id, assetCount: 0 },
|
||||
]);
|
||||
albumMock.getInvalidThumbnail.mockResolvedValue([]);
|
||||
|
||||
const result = await sut.getAllAlbums(authStub.admin, {});
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].id).toEqual(albumStub.empty.id);
|
||||
expect(result[1].id).toEqual(albumStub.sharedWithUser.id);
|
||||
});
|
||||
|
||||
it('gets list of albums that have a specific asset', async () => {
|
||||
albumMock.getByAssetId.mockResolvedValue([albumStub.oneAsset]);
|
||||
albumMock.getAssetCountForIds.mockResolvedValue([{ albumId: albumStub.oneAsset.id, assetCount: 1 }]);
|
||||
albumMock.getInvalidThumbnail.mockResolvedValue([]);
|
||||
|
||||
const result = await sut.getAllAlbums(authStub.admin, { assetId: albumStub.oneAsset.id });
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toEqual(albumStub.oneAsset.id);
|
||||
expect(albumMock.getByAssetId).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('gets list of albums that are shared', async () => {
|
||||
albumMock.getShared.mockResolvedValue([albumStub.sharedWithUser]);
|
||||
albumMock.getAssetCountForIds.mockResolvedValue([{ albumId: albumStub.sharedWithUser.id, assetCount: 0 }]);
|
||||
albumMock.getInvalidThumbnail.mockResolvedValue([]);
|
||||
|
||||
const result = await sut.getAllAlbums(authStub.admin, { shared: true });
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toEqual(albumStub.sharedWithUser.id);
|
||||
expect(albumMock.getShared).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('gets list of albums that are NOT shared', async () => {
|
||||
albumMock.getNotShared.mockResolvedValue([albumStub.empty]);
|
||||
albumMock.getAssetCountForIds.mockResolvedValue([{ albumId: albumStub.empty.id, assetCount: 0 }]);
|
||||
albumMock.getInvalidThumbnail.mockResolvedValue([]);
|
||||
|
||||
const result = await sut.getAllAlbums(authStub.admin, { shared: false });
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toEqual(albumStub.empty.id);
|
||||
expect(albumMock.getNotShared).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('counts assets correctly', async () => {
|
||||
albumMock.getOwned.mockResolvedValue([albumStub.oneAsset]);
|
||||
albumMock.getAssetCountForIds.mockResolvedValue([{ albumId: albumStub.oneAsset.id, assetCount: 1 }]);
|
||||
albumMock.getInvalidThumbnail.mockResolvedValue([]);
|
||||
|
||||
const result = await sut.getAllAlbums(authStub.admin, {});
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].assetCount).toEqual(1);
|
||||
expect(albumMock.getOwned).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('updates the album thumbnail by listing all albums', async () => {
|
||||
albumMock.getOwned.mockResolvedValue([albumStub.oneAssetInvalidThumbnail]);
|
||||
albumMock.getAssetCountForIds.mockResolvedValue([
|
||||
{ albumId: albumStub.oneAssetInvalidThumbnail.id, assetCount: 1 },
|
||||
]);
|
||||
albumMock.getInvalidThumbnail.mockResolvedValue([albumStub.oneAssetInvalidThumbnail.id]);
|
||||
albumMock.save.mockResolvedValue(albumStub.oneAssetValidThumbnail);
|
||||
assetMock.getFirstAssetForAlbumId.mockResolvedValue(albumStub.oneAssetInvalidThumbnail.assets[0]);
|
||||
|
||||
const result = await sut.getAllAlbums(authStub.admin, {});
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(albumMock.getInvalidThumbnail).toHaveBeenCalledTimes(1);
|
||||
expect(albumMock.save).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('removes the thumbnail for an empty album', async () => {
|
||||
albumMock.getOwned.mockResolvedValue([albumStub.emptyWithInvalidThumbnail]);
|
||||
albumMock.getAssetCountForIds.mockResolvedValue([
|
||||
{ albumId: albumStub.emptyWithInvalidThumbnail.id, assetCount: 1 },
|
||||
]);
|
||||
albumMock.getInvalidThumbnail.mockResolvedValue([albumStub.emptyWithInvalidThumbnail.id]);
|
||||
albumMock.save.mockResolvedValue(albumStub.emptyWithValidThumbnail);
|
||||
assetMock.getFirstAssetForAlbumId.mockResolvedValue(null);
|
||||
|
||||
const result = await sut.getAllAlbums(authStub.admin, {});
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(albumMock.getInvalidThumbnail).toHaveBeenCalledTimes(1);
|
||||
expect(albumMock.save).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
58
server/libs/domain/src/album/album.service.ts
Normal file
58
server/libs/domain/src/album/album.service.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { AlbumEntity } from '@app/infra';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { IAssetRepository } from '../asset';
|
||||
import { AuthUserDto } from '../auth';
|
||||
import { IAlbumRepository } from './album.repository';
|
||||
import { GetAlbumsDto } from './dto/get-albums.dto';
|
||||
import { AlbumResponseDto } from './response-dto';
|
||||
|
||||
@Injectable()
|
||||
export class AlbumService {
|
||||
constructor(
|
||||
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||
) {}
|
||||
|
||||
async getAllAlbums({ id: ownerId }: AuthUserDto, { assetId, shared }: GetAlbumsDto): Promise<AlbumResponseDto[]> {
|
||||
await this.updateInvalidThumbnails();
|
||||
|
||||
let albums: AlbumEntity[];
|
||||
if (assetId) {
|
||||
albums = await this.albumRepository.getByAssetId(ownerId, assetId);
|
||||
} else if (shared === true) {
|
||||
albums = await this.albumRepository.getShared(ownerId);
|
||||
} else if (shared === false) {
|
||||
albums = await this.albumRepository.getNotShared(ownerId);
|
||||
} else {
|
||||
albums = await this.albumRepository.getOwned(ownerId);
|
||||
}
|
||||
|
||||
// Get asset count for each album. Then map the result to an object:
|
||||
// { [albumId]: assetCount }
|
||||
const albumsAssetCount = await this.albumRepository.getAssetCountForIds(albums.map((album) => album.id));
|
||||
const albumsAssetCountObj = albumsAssetCount.reduce((obj: Record<string, number>, { albumId, assetCount }) => {
|
||||
obj[albumId] = assetCount;
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
return albums.map((album) => {
|
||||
return {
|
||||
...album,
|
||||
sharedLinks: undefined, // Don't return shared links
|
||||
shared: album.sharedLinks?.length > 0 || album.sharedUsers?.length > 0,
|
||||
assetCount: albumsAssetCountObj[album.id],
|
||||
} as AlbumResponseDto;
|
||||
});
|
||||
}
|
||||
|
||||
async updateInvalidThumbnails(): Promise<number> {
|
||||
const invalidAlbumIds = await this.albumRepository.getInvalidThumbnail();
|
||||
|
||||
for (const albumId of invalidAlbumIds) {
|
||||
const newThumbnail = await this.assetRepository.getFirstAssetForAlbumId(albumId);
|
||||
await this.albumRepository.save({ id: albumId, albumThumbnailAsset: newThumbnail });
|
||||
}
|
||||
|
||||
return invalidAlbumIds.length;
|
||||
}
|
||||
}
|
||||
27
server/libs/domain/src/album/dto/get-albums.dto.ts
Normal file
27
server/libs/domain/src/album/dto/get-albums.dto.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsBoolean, IsOptional, IsUUID } from 'class-validator';
|
||||
import { toBoolean } from 'apps/immich/src/utils/transform.util';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class GetAlbumsDto {
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Transform(toBoolean)
|
||||
@ApiProperty()
|
||||
/**
|
||||
* true: only shared albums
|
||||
* false: only non-shared own albums
|
||||
* undefined: shared and owned albums
|
||||
*/
|
||||
shared?: boolean;
|
||||
|
||||
/**
|
||||
* Only returns albums that contain the asset
|
||||
* Ignores the shared parameter
|
||||
* undefined: get all albums
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsUUID(4)
|
||||
@ApiProperty({ format: 'uuid' })
|
||||
assetId?: string;
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './album.repository';
|
||||
export * from './album.service';
|
||||
export * from './response-dto';
|
||||
|
||||
@@ -18,6 +18,7 @@ export const IAssetRepository = 'IAssetRepository';
|
||||
export interface IAssetRepository {
|
||||
getByIds(ids: string[]): Promise<AssetEntity[]>;
|
||||
getWithout(property: WithoutProperty): Promise<AssetEntity[]>;
|
||||
getFirstAssetForAlbumId(albumId: string): Promise<AssetEntity | null>;
|
||||
deleteAll(ownerId: string): Promise<void>;
|
||||
getAll(options?: AssetSearchOptions): Promise<AssetEntity[]>;
|
||||
save(asset: Partial<AssetEntity>): Promise<AssetEntity>;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { DynamicModule, Global, Module, ModuleMetadata, Provider } from '@nestjs/common';
|
||||
import { AlbumService } from './album';
|
||||
import { APIKeyService } from './api-key';
|
||||
import { AssetService } from './asset';
|
||||
import { AuthService } from './auth';
|
||||
@@ -16,6 +17,7 @@ import { INITIAL_SYSTEM_CONFIG, SystemConfigService } from './system-config';
|
||||
import { UserService } from './user';
|
||||
|
||||
const providers: Provider[] = [
|
||||
AlbumService,
|
||||
AssetService,
|
||||
APIKeyService,
|
||||
AuthService,
|
||||
|
||||
@@ -3,6 +3,12 @@ import { IAlbumRepository } from '../src';
|
||||
export const newAlbumRepositoryMock = (): jest.Mocked<IAlbumRepository> => {
|
||||
return {
|
||||
getByIds: jest.fn(),
|
||||
getByAssetId: jest.fn(),
|
||||
getAssetCountForIds: jest.fn(),
|
||||
getInvalidThumbnail: jest.fn(),
|
||||
getOwned: jest.fn(),
|
||||
getShared: jest.fn(),
|
||||
getNotShared: jest.fn(),
|
||||
deleteAll: jest.fn(),
|
||||
getAll: jest.fn(),
|
||||
save: jest.fn(),
|
||||
|
||||
@@ -4,6 +4,7 @@ export const newAssetRepositoryMock = (): jest.Mocked<IAssetRepository> => {
|
||||
return {
|
||||
getByIds: jest.fn(),
|
||||
getWithout: jest.fn(),
|
||||
getFirstAssetForAlbumId: jest.fn(),
|
||||
getAll: jest.fn(),
|
||||
deleteAll: jest.fn(),
|
||||
save: jest.fn(),
|
||||
|
||||
@@ -219,6 +219,97 @@ export const albumStub = {
|
||||
sharedLinks: [],
|
||||
sharedUsers: [],
|
||||
}),
|
||||
sharedWithUser: Object.freeze<AlbumEntity>({
|
||||
id: 'album-2',
|
||||
albumName: 'Empty album shared with user',
|
||||
ownerId: authStub.admin.id,
|
||||
owner: userEntityStub.admin,
|
||||
assets: [],
|
||||
albumThumbnailAsset: null,
|
||||
albumThumbnailAssetId: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
sharedLinks: [],
|
||||
sharedUsers: [userEntityStub.user1],
|
||||
}),
|
||||
sharedWithAdmin: Object.freeze<AlbumEntity>({
|
||||
id: 'album-3',
|
||||
albumName: 'Empty album shared with admin',
|
||||
ownerId: authStub.user1.id,
|
||||
owner: userEntityStub.user1,
|
||||
assets: [],
|
||||
albumThumbnailAsset: null,
|
||||
albumThumbnailAssetId: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
sharedLinks: [],
|
||||
sharedUsers: [userEntityStub.admin],
|
||||
}),
|
||||
oneAsset: Object.freeze<AlbumEntity>({
|
||||
id: 'album-4',
|
||||
albumName: 'Album with one asset',
|
||||
ownerId: authStub.admin.id,
|
||||
owner: userEntityStub.admin,
|
||||
assets: [assetEntityStub.image],
|
||||
albumThumbnailAsset: null,
|
||||
albumThumbnailAssetId: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
sharedLinks: [],
|
||||
sharedUsers: [],
|
||||
}),
|
||||
emptyWithInvalidThumbnail: Object.freeze<AlbumEntity>({
|
||||
id: 'album-5',
|
||||
albumName: 'Empty album with invalid thumbnail',
|
||||
ownerId: authStub.admin.id,
|
||||
owner: userEntityStub.admin,
|
||||
assets: [],
|
||||
albumThumbnailAsset: assetEntityStub.image,
|
||||
albumThumbnailAssetId: assetEntityStub.image.id,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
sharedLinks: [],
|
||||
sharedUsers: [],
|
||||
}),
|
||||
emptyWithValidThumbnail: Object.freeze<AlbumEntity>({
|
||||
id: 'album-5',
|
||||
albumName: 'Empty album with invalid thumbnail',
|
||||
ownerId: authStub.admin.id,
|
||||
owner: userEntityStub.admin,
|
||||
assets: [],
|
||||
albumThumbnailAsset: null,
|
||||
albumThumbnailAssetId: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
sharedLinks: [],
|
||||
sharedUsers: [],
|
||||
}),
|
||||
oneAssetInvalidThumbnail: Object.freeze<AlbumEntity>({
|
||||
id: 'album-6',
|
||||
albumName: 'Album with one asset and invalid thumbnail',
|
||||
ownerId: authStub.admin.id,
|
||||
owner: userEntityStub.admin,
|
||||
assets: [assetEntityStub.image],
|
||||
albumThumbnailAsset: assetEntityStub.livePhotoMotionAsset,
|
||||
albumThumbnailAssetId: assetEntityStub.livePhotoMotionAsset.id,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
sharedLinks: [],
|
||||
sharedUsers: [],
|
||||
}),
|
||||
oneAssetValidThumbnail: Object.freeze<AlbumEntity>({
|
||||
id: 'album-6',
|
||||
albumName: 'Album with one asset and invalid thumbnail',
|
||||
ownerId: authStub.admin.id,
|
||||
owner: userEntityStub.admin,
|
||||
assets: [assetEntityStub.image],
|
||||
albumThumbnailAsset: assetEntityStub.image,
|
||||
albumThumbnailAssetId: assetEntityStub.image.id,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
sharedLinks: [],
|
||||
sharedUsers: [],
|
||||
}),
|
||||
};
|
||||
|
||||
const assetInfo: ExifResponseDto = {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Unique,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { AlbumEntity } from './album.entity';
|
||||
import { ExifEntity } from './exif.entity';
|
||||
import { SharedLinkEntity } from './shared-link.entity';
|
||||
import { SmartInfoEntity } from './smart-info.entity';
|
||||
@@ -99,6 +100,9 @@ export class AssetEntity {
|
||||
@ManyToMany(() => SharedLinkEntity, (link) => link.assets, { cascade: true })
|
||||
@JoinTable({ name: 'shared_link__asset' })
|
||||
sharedLinks!: SharedLinkEntity[];
|
||||
|
||||
@ManyToMany(() => AlbumEntity, (album) => album.assets)
|
||||
albums?: AlbumEntity[];
|
||||
}
|
||||
|
||||
export enum AssetType {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { IAlbumRepository } from '@app/domain';
|
||||
import { AlbumAssetCount, IAlbumRepository } from '@app/domain';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { In, Repository } from 'typeorm';
|
||||
import { In, IsNull, Not, Repository } from 'typeorm';
|
||||
import { dataSource } from '../config';
|
||||
import { AlbumEntity } from '../entities';
|
||||
|
||||
@Injectable()
|
||||
@@ -19,6 +20,97 @@ export class AlbumRepository implements IAlbumRepository {
|
||||
});
|
||||
}
|
||||
|
||||
getByAssetId(ownerId: string, assetId: string): Promise<AlbumEntity[]> {
|
||||
return this.repository.find({
|
||||
where: { ownerId, assets: { id: assetId } },
|
||||
relations: { owner: true, sharedUsers: true },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
async getAssetCountForIds(ids: string[]): Promise<AlbumAssetCount[]> {
|
||||
// Guard against running invalid query when ids list is empty.
|
||||
if (!ids.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Only possible with query builder because of GROUP BY.
|
||||
const countByAlbums = await this.repository
|
||||
.createQueryBuilder('album')
|
||||
.select('album.id')
|
||||
.addSelect('COUNT(albums_assets.assetsId)', 'asset_count')
|
||||
.leftJoin('albums_assets_assets', 'albums_assets', 'albums_assets.albumsId = album.id')
|
||||
.where('album.id IN (:...ids)', { ids })
|
||||
.groupBy('album.id')
|
||||
.getRawMany();
|
||||
|
||||
return countByAlbums.map<AlbumAssetCount>((albumCount) => ({
|
||||
albumId: albumCount['album_id'],
|
||||
assetCount: Number(albumCount['asset_count']),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the album IDs that have an invalid thumbnail, when:
|
||||
* - Thumbnail references an asset outside the album
|
||||
* - Empty album still has a thumbnail set
|
||||
*/
|
||||
async getInvalidThumbnail(): Promise<string[]> {
|
||||
// Using dataSource, because there is no direct access to albums_assets_assets.
|
||||
const albumHasAssets = dataSource
|
||||
.createQueryBuilder()
|
||||
.select('1')
|
||||
.from('albums_assets_assets', 'albums_assets')
|
||||
.where('"albums"."id" = "albums_assets"."albumsId"');
|
||||
|
||||
const albumContainsThumbnail = albumHasAssets
|
||||
.clone()
|
||||
.andWhere('"albums"."albumThumbnailAssetId" = "albums_assets"."assetsId"');
|
||||
|
||||
const albums = await this.repository
|
||||
.createQueryBuilder('albums')
|
||||
.select('albums.id')
|
||||
.where(`"albums"."albumThumbnailAssetId" IS NULL AND EXISTS (${albumHasAssets.getQuery()})`)
|
||||
.orWhere(`"albums"."albumThumbnailAssetId" IS NOT NULL AND NOT EXISTS (${albumContainsThumbnail.getQuery()})`)
|
||||
.getMany();
|
||||
|
||||
return albums.map((album) => album.id);
|
||||
}
|
||||
|
||||
getOwned(ownerId: string): Promise<AlbumEntity[]> {
|
||||
return this.repository.find({
|
||||
relations: { sharedUsers: true, sharedLinks: true, owner: true },
|
||||
where: { ownerId },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get albums shared with and shared by owner.
|
||||
*/
|
||||
getShared(ownerId: string): Promise<AlbumEntity[]> {
|
||||
return this.repository.find({
|
||||
relations: { sharedUsers: true, sharedLinks: true, owner: true },
|
||||
where: [
|
||||
{ sharedUsers: { id: ownerId } },
|
||||
{ sharedLinks: { userId: ownerId } },
|
||||
{ ownerId, sharedUsers: { id: Not(IsNull()) } },
|
||||
],
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get albums of owner that are _not_ shared
|
||||
*/
|
||||
getNotShared(ownerId: string): Promise<AlbumEntity[]> {
|
||||
return this.repository.find({
|
||||
relations: { sharedUsers: true, sharedLinks: true, owner: true },
|
||||
where: { ownerId, sharedUsers: { id: IsNull() }, sharedLinks: { id: IsNull() } },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
async deleteAll(userId: string): Promise<void> {
|
||||
await this.repository.delete({ ownerId: userId });
|
||||
}
|
||||
|
||||
@@ -134,4 +134,11 @@ export class AssetRepository implements IAssetRepository {
|
||||
where,
|
||||
});
|
||||
}
|
||||
|
||||
getFirstAssetForAlbumId(albumId: string): Promise<AssetEntity | null> {
|
||||
return this.repository.findOne({
|
||||
where: { albums: { id: albumId } },
|
||||
order: { fileCreatedAt: 'DESC' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user