mirror of
https://github.com/immich-app/immich.git
synced 2026-03-09 11:23:46 -04:00
chore: upgrade to kysely 0.28.11
This commit is contained in:
parent
c259fee309
commit
592364a217
24
pnpm-lock.yaml
generated
24
pnpm-lock.yaml
generated
@ -488,11 +488,11 @@ importers:
|
||||
specifier: ^9.0.2
|
||||
version: 9.0.3
|
||||
kysely:
|
||||
specifier: 0.28.2
|
||||
version: 0.28.2
|
||||
specifier: 0.28.11
|
||||
version: 0.28.11
|
||||
kysely-postgres-js:
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0(kysely@0.28.2)(postgres@3.4.8)
|
||||
version: 3.0.0(kysely@0.28.11)(postgres@3.4.8)
|
||||
lodash:
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.23
|
||||
@ -513,7 +513,7 @@ importers:
|
||||
version: 5.4.3(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||
nestjs-kysely:
|
||||
specifier: 3.1.2
|
||||
version: 3.1.2(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(kysely@0.28.2)(reflect-metadata@0.2.2)
|
||||
version: 3.1.2(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(kysely@0.28.11)(reflect-metadata@0.2.2)
|
||||
nestjs-otel:
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.1(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)
|
||||
@ -8379,10 +8379,6 @@ packages:
|
||||
resolution: {integrity: sha512-zpGIFg0HuoC893rIjYX1BETkVWdDnzTzF5e0kWXJFg5lE0k1/LfNWBejrcnOFu8Q2Rfq/hTDTU7XLUM8QOrpzg==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
kysely@0.28.2:
|
||||
resolution: {integrity: sha512-4YAVLoF0Sf0UTqlhgQMFU9iQECdah7n+13ANkiuVfRvlK+uI0Etbgd7bVP36dKlG+NXWbhGua8vnGt+sdhvT7A==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
langium@3.3.1:
|
||||
resolution: {integrity: sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
@ -21040,16 +21036,8 @@ snapshots:
|
||||
optionalDependencies:
|
||||
postgres: 3.4.8
|
||||
|
||||
kysely-postgres-js@3.0.0(kysely@0.28.2)(postgres@3.4.8):
|
||||
dependencies:
|
||||
kysely: 0.28.2
|
||||
optionalDependencies:
|
||||
postgres: 3.4.8
|
||||
|
||||
kysely@0.28.11: {}
|
||||
|
||||
kysely@0.28.2: {}
|
||||
|
||||
langium@3.3.1:
|
||||
dependencies:
|
||||
chevrotain: 11.0.3
|
||||
@ -22128,11 +22116,11 @@ snapshots:
|
||||
reflect-metadata: 0.2.2
|
||||
rxjs: 7.8.2
|
||||
|
||||
nestjs-kysely@3.1.2(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(kysely@0.28.2)(reflect-metadata@0.2.2):
|
||||
nestjs-kysely@3.1.2(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(kysely@0.28.11)(reflect-metadata@0.2.2):
|
||||
dependencies:
|
||||
'@nestjs/common': 11.1.14(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||
'@nestjs/core': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(@nestjs/websockets@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||
kysely: 0.28.2
|
||||
kysely: 0.28.11
|
||||
reflect-metadata: 0.2.2
|
||||
tslib: 2.8.1
|
||||
|
||||
|
||||
@ -82,7 +82,7 @@
|
||||
"jose": "^5.10.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"kysely": "0.28.2",
|
||||
"kysely": "0.28.11",
|
||||
"kysely-postgres-js": "^3.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"luxon": "^3.4.2",
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Selectable } from 'kysely';
|
||||
import { Selectable, ShallowDehydrateObject } from 'kysely';
|
||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||
import {
|
||||
AlbumUserRole,
|
||||
@ -16,6 +16,7 @@ import {
|
||||
} from 'src/enum';
|
||||
import { AlbumTable } from 'src/schema/tables/album.table';
|
||||
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
|
||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||
import { PluginActionTable, PluginFilterTable, PluginTable } from 'src/schema/tables/plugin.table';
|
||||
import { WorkflowActionTable, WorkflowFilterTable, WorkflowTable } from 'src/schema/tables/workflow.table';
|
||||
import { UserMetadataItem } from 'src/types';
|
||||
@ -31,7 +32,7 @@ export type AuthUser = {
|
||||
};
|
||||
|
||||
export type AlbumUser = {
|
||||
user: User;
|
||||
user: ShallowDehydrateObject<User>;
|
||||
role: AlbumUserRole;
|
||||
};
|
||||
|
||||
@ -67,7 +68,7 @@ export type Activity = {
|
||||
updatedAt: Date;
|
||||
albumId: string;
|
||||
userId: string;
|
||||
user: User;
|
||||
user: ShallowDehydrateObject<User>;
|
||||
assetId: string | null;
|
||||
comment: string | null;
|
||||
isLiked: boolean;
|
||||
@ -105,7 +106,7 @@ export type Memory = {
|
||||
data: object;
|
||||
ownerId: string;
|
||||
isSaved: boolean;
|
||||
assets: MapAsset[];
|
||||
assets: ShallowDehydrateObject<MapAsset>[];
|
||||
};
|
||||
|
||||
export type Asset = {
|
||||
@ -159,9 +160,9 @@ export type StorageAsset = {
|
||||
export type Stack = {
|
||||
id: string;
|
||||
primaryAssetId: string;
|
||||
owner?: User;
|
||||
owner?: ShallowDehydrateObject<User>;
|
||||
ownerId: string;
|
||||
assets: MapAsset[];
|
||||
assets: ShallowDehydrateObject<MapAsset>[];
|
||||
assetCount?: number;
|
||||
};
|
||||
|
||||
@ -177,11 +178,11 @@ export type AuthSharedLink = {
|
||||
|
||||
export type SharedLink = {
|
||||
id: string;
|
||||
album?: Album | null;
|
||||
album?: ShallowDehydrateObject<Album> | null;
|
||||
albumId: string | null;
|
||||
allowDownload: boolean;
|
||||
allowUpload: boolean;
|
||||
assets: MapAsset[];
|
||||
assets: ShallowDehydrateObject<MapAsset>[];
|
||||
createdAt: Date;
|
||||
description: string | null;
|
||||
expiresAt: Date | null;
|
||||
@ -194,8 +195,8 @@ export type SharedLink = {
|
||||
};
|
||||
|
||||
export type Album = Selectable<AlbumTable> & {
|
||||
owner: User;
|
||||
assets: MapAsset[];
|
||||
owner: ShallowDehydrateObject<User>;
|
||||
assets: ShallowDehydrateObject<Selectable<AssetTable>>[];
|
||||
};
|
||||
|
||||
export type AuthSession = {
|
||||
@ -205,9 +206,9 @@ export type AuthSession = {
|
||||
|
||||
export type Partner = {
|
||||
sharedById: string;
|
||||
sharedBy: User;
|
||||
sharedBy: ShallowDehydrateObject<User>;
|
||||
sharedWithId: string;
|
||||
sharedWith: User;
|
||||
sharedWith: ShallowDehydrateObject<User>;
|
||||
createdAt: Date;
|
||||
createId: string;
|
||||
updatedAt: Date;
|
||||
@ -270,7 +271,7 @@ export type AssetFace = {
|
||||
imageWidth: number;
|
||||
personId: string | null;
|
||||
sourceType: SourceType;
|
||||
person?: Person | null;
|
||||
person?: ShallowDehydrateObject<Person> | null;
|
||||
updatedAt: Date;
|
||||
updateId: string;
|
||||
isVisible: boolean;
|
||||
|
||||
@ -79,6 +79,6 @@ export const mapActivity = (activity: Activity): ActivityResponseDto => {
|
||||
createdAt: activity.createdAt,
|
||||
comment: activity.comment,
|
||||
type: activity.isLiked ? ReactionType.LIKE : ReactionType.COMMENT,
|
||||
user: mapUser(activity.user),
|
||||
user: mapUser({ ...activity.user, profileChangedAt: new Date(activity.user.profileChangedAt) }),
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,18 +1,22 @@
|
||||
import { mapAlbum } from 'src/dtos/album.dto';
|
||||
import { AlbumFactory } from 'test/factories/album.factory';
|
||||
import { getForAlbum } from 'test/mappers';
|
||||
|
||||
describe('mapAlbum', () => {
|
||||
it('should set start and end dates', () => {
|
||||
const startDate = new Date('2023-02-22T05:06:29.716Z');
|
||||
const endDate = new Date('2025-01-01T01:02:03.456Z');
|
||||
const album = AlbumFactory.from().asset({ localDateTime: endDate }).asset({ localDateTime: startDate }).build();
|
||||
const dto = mapAlbum(album, false);
|
||||
const album = AlbumFactory.from()
|
||||
.asset({ localDateTime: endDate }, (builder) => builder.exif())
|
||||
.asset({ localDateTime: startDate }, (builder) => builder.exif())
|
||||
.build();
|
||||
const dto = mapAlbum(getForAlbum(album), false);
|
||||
expect(dto.startDate).toEqual(startDate);
|
||||
expect(dto.endDate).toEqual(endDate);
|
||||
});
|
||||
|
||||
it('should not set start and end dates for empty assets', () => {
|
||||
const dto = mapAlbum(AlbumFactory.create(), false);
|
||||
const dto = mapAlbum(getForAlbum(AlbumFactory.create()), false);
|
||||
expect(dto.startDate).toBeUndefined();
|
||||
expect(dto.endDate).toBeUndefined();
|
||||
});
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ArrayNotEmpty, IsArray, IsString, ValidateNested } from 'class-validator';
|
||||
import { ShallowDehydrateObject } from 'kysely';
|
||||
import _ from 'lodash';
|
||||
import { AlbumUser, AuthSharedLink, User } from 'src/database';
|
||||
import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AssetResponseDto, MapAsset, mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AssetResponseDto, hydrateAsset, MapAsset, mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
|
||||
import { mapUser, UserResponseDto } from 'src/dtos/user.dto';
|
||||
import { AlbumUserRole, AssetOrder } from 'src/enum';
|
||||
import { Optional, ValidateBoolean, ValidateEnum, ValidateUUID } from 'src/validation';
|
||||
|
||||
@ -191,8 +192,8 @@ export class AlbumResponseDto {
|
||||
|
||||
export type MapAlbumDto = {
|
||||
albumUsers?: AlbumUser[];
|
||||
assets?: MapAsset[];
|
||||
sharedLinks?: AuthSharedLink[];
|
||||
assets?: ShallowDehydrateObject<MapAsset>[];
|
||||
sharedLinks?: ShallowDehydrateObject<AuthSharedLink>[];
|
||||
albumName: string;
|
||||
description: string;
|
||||
albumThumbnailAssetId: string | null;
|
||||
@ -200,7 +201,7 @@ export type MapAlbumDto = {
|
||||
updatedAt: Date;
|
||||
id: string;
|
||||
ownerId: string;
|
||||
owner: User;
|
||||
owner: ShallowDehydrateObject<User>;
|
||||
isActivityEnabled: boolean;
|
||||
order: AssetOrder;
|
||||
};
|
||||
@ -210,7 +211,7 @@ export const mapAlbum = (entity: MapAlbumDto, withAssets: boolean, auth?: AuthDt
|
||||
|
||||
if (entity.albumUsers) {
|
||||
for (const albumUser of entity.albumUsers) {
|
||||
const user = mapUser(albumUser.user);
|
||||
const user = mapUser({ ...albumUser.user, profileChangedAt: new Date(albumUser.user.profileChangedAt) });
|
||||
albumUsers.push({
|
||||
user,
|
||||
role: albumUser.role,
|
||||
@ -240,13 +241,13 @@ export const mapAlbum = (entity: MapAlbumDto, withAssets: boolean, auth?: AuthDt
|
||||
updatedAt: entity.updatedAt,
|
||||
id: entity.id,
|
||||
ownerId: entity.ownerId,
|
||||
owner: mapUser(entity.owner),
|
||||
owner: mapUser({ ...entity.owner, profileChangedAt: new Date(entity.owner.profileChangedAt) }),
|
||||
albumUsers: albumUsersSorted,
|
||||
shared: hasSharedUser || hasSharedLink,
|
||||
hasSharedLink,
|
||||
startDate,
|
||||
endDate,
|
||||
assets: (withAssets ? assets : []).map((asset) => mapAsset(asset, { auth })),
|
||||
startDate: startDate ? new Date(startDate) : undefined,
|
||||
endDate: endDate ? new Date(endDate) : undefined,
|
||||
assets: (withAssets ? assets : []).map((asset) => mapAsset(hydrateAsset(asset), { auth })),
|
||||
assetCount: entity.assets?.length || 0,
|
||||
isActivityEnabled: entity.isActivityEnabled,
|
||||
order: entity.order,
|
||||
|
||||
@ -3,6 +3,7 @@ import { AssetEditAction } from 'src/dtos/editing.dto';
|
||||
import { AssetFaceFactory } from 'test/factories/asset-face.factory';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { PersonFactory } from 'test/factories/person.factory';
|
||||
import { getForAsset } from 'test/mappers';
|
||||
|
||||
describe('mapAsset', () => {
|
||||
describe('peopleWithFaces', () => {
|
||||
@ -41,7 +42,7 @@ describe('mapAsset', () => {
|
||||
})
|
||||
.build();
|
||||
|
||||
const result = mapAsset(asset);
|
||||
const result = mapAsset(getForAsset(asset));
|
||||
|
||||
expect(result.people).toBeDefined();
|
||||
expect(result.people).toHaveLength(1);
|
||||
@ -80,7 +81,7 @@ describe('mapAsset', () => {
|
||||
.edit({ action: AssetEditAction.Crop, parameters: { x: 50, y: 50, width: 500, height: 400 } })
|
||||
.build();
|
||||
|
||||
const result = mapAsset(asset);
|
||||
const result = mapAsset(getForAsset(asset));
|
||||
|
||||
expect(result.unassignedFaces).toBeDefined();
|
||||
expect(result.unassignedFaces).toHaveLength(1);
|
||||
@ -130,7 +131,7 @@ describe('mapAsset', () => {
|
||||
.exif({ exifImageWidth: 1000, exifImageHeight: 800 })
|
||||
.build();
|
||||
|
||||
const result = mapAsset(asset);
|
||||
const result = mapAsset(getForAsset(asset));
|
||||
|
||||
expect(result.people).toBeDefined();
|
||||
expect(result.people).toHaveLength(2);
|
||||
@ -179,7 +180,7 @@ describe('mapAsset', () => {
|
||||
.exif({ exifImageWidth: 1000, exifImageHeight: 800 })
|
||||
.build();
|
||||
|
||||
const result = mapAsset(asset);
|
||||
const result = mapAsset(getForAsset(asset));
|
||||
|
||||
expect(result.people).toBeDefined();
|
||||
expect(result.people).toHaveLength(1);
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Selectable } from 'kysely';
|
||||
import { Selectable, ShallowDehydrateObject } from 'kysely';
|
||||
import { AssetFace, AssetFile, Exif, Stack, Tag, User } from 'src/database';
|
||||
import { HistoryBuilder, Property } from 'src/decorators';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
@ -151,13 +151,13 @@ export type MapAsset = {
|
||||
deviceId: string;
|
||||
duplicateId: string | null;
|
||||
duration: string | null;
|
||||
edits?: AssetEditActionItem[];
|
||||
edits?: ShallowDehydrateObject<AssetEditActionItem>[];
|
||||
encodedVideoPath: string | null;
|
||||
exifInfo?: Selectable<Exif> | null;
|
||||
faces?: AssetFace[];
|
||||
exifInfo?: ShallowDehydrateObject<Selectable<Exif>> | null;
|
||||
faces?: ShallowDehydrateObject<AssetFace>[];
|
||||
fileCreatedAt: Date;
|
||||
fileModifiedAt: Date;
|
||||
files?: AssetFile[];
|
||||
files?: ShallowDehydrateObject<AssetFile>[];
|
||||
isExternal: boolean;
|
||||
isFavorite: boolean;
|
||||
isOffline: boolean;
|
||||
@ -167,11 +167,11 @@ export type MapAsset = {
|
||||
localDateTime: Date;
|
||||
originalFileName: string;
|
||||
originalPath: string;
|
||||
owner?: User | null;
|
||||
owner?: ShallowDehydrateObject<User> | null;
|
||||
ownerId: string;
|
||||
stack?: Stack | null;
|
||||
stack?: (ShallowDehydrateObject<Stack> & { assets: Stack['assets'] }) | null;
|
||||
stackId: string | null;
|
||||
tags?: Tag[];
|
||||
tags?: ShallowDehydrateObject<Tag>[];
|
||||
thumbhash: Buffer<ArrayBufferLike> | null;
|
||||
type: AssetType;
|
||||
width: number | null;
|
||||
@ -213,7 +213,15 @@ const peopleWithFaces = (
|
||||
}
|
||||
|
||||
if (!peopleFaces.has(face.person.id)) {
|
||||
peopleFaces.set(face.person.id, { ...mapPerson(face.person), faces: [] });
|
||||
peopleFaces.set(face.person.id, {
|
||||
...mapPerson({
|
||||
...face.person,
|
||||
birthDate: face.person.birthDate ? new Date(face.person.birthDate) : null,
|
||||
createdAt: new Date(face.person.createdAt),
|
||||
updatedAt: new Date(face.person.updatedAt),
|
||||
}),
|
||||
faces: [],
|
||||
});
|
||||
}
|
||||
const mappedFace = mapFacesWithoutPerson(face, edits, assetDimensions);
|
||||
peopleFaces.get(face.person.id)!.faces.push(mappedFace);
|
||||
@ -260,7 +268,9 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset
|
||||
createdAt: entity.createdAt,
|
||||
deviceAssetId: entity.deviceAssetId,
|
||||
ownerId: entity.ownerId,
|
||||
owner: entity.owner ? mapUser(entity.owner) : undefined,
|
||||
owner: entity.owner
|
||||
? mapUser({ ...entity.owner, profileChangedAt: new Date(entity.owner.profileChangedAt) })
|
||||
: undefined,
|
||||
deviceId: entity.deviceId,
|
||||
libraryId: entity.libraryId,
|
||||
type: entity.type,
|
||||
@ -277,13 +287,35 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset
|
||||
isTrashed: !!entity.deletedAt,
|
||||
visibility: entity.visibility,
|
||||
duration: entity.duration ?? '0:00:00.00000',
|
||||
exifInfo: entity.exifInfo ? mapExif(entity.exifInfo) : undefined,
|
||||
exifInfo: entity.exifInfo
|
||||
? mapExif({
|
||||
...entity.exifInfo,
|
||||
dateTimeOriginal: entity.exifInfo.dateTimeOriginal ? new Date(entity.exifInfo.dateTimeOriginal) : null,
|
||||
modifyDate: entity.exifInfo.modifyDate ? new Date(entity.exifInfo.modifyDate) : null,
|
||||
})
|
||||
: undefined,
|
||||
livePhotoVideoId: entity.livePhotoVideoId,
|
||||
tags: entity.tags?.map((tag) => mapTag(tag)),
|
||||
people: peopleWithFaces(entity.faces, entity.edits, assetDimensions),
|
||||
tags: entity.tags?.map((tag) =>
|
||||
mapTag({ ...tag, createdAt: new Date(tag.createdAt), updatedAt: new Date(tag.updatedAt) }),
|
||||
),
|
||||
people: peopleWithFaces(
|
||||
entity.faces?.map((face) => ({
|
||||
...face,
|
||||
deletedAt: face.deletedAt ? new Date(face.deletedAt) : null,
|
||||
updatedAt: new Date(face.updatedAt),
|
||||
})),
|
||||
entity.edits,
|
||||
assetDimensions,
|
||||
),
|
||||
unassignedFaces: entity.faces
|
||||
?.filter((face) => !face.person)
|
||||
.map((a) => mapFacesWithoutPerson(a, entity.edits, assetDimensions)),
|
||||
.map((face) =>
|
||||
mapFacesWithoutPerson(
|
||||
{ ...face, deletedAt: face.deletedAt ? new Date(face.deletedAt) : null, updatedAt: new Date(face.updatedAt) },
|
||||
entity.edits,
|
||||
assetDimensions,
|
||||
),
|
||||
),
|
||||
checksum: hexOrBufferToBase64(entity.checksum)!,
|
||||
stack: withStack ? mapStack(entity) : undefined,
|
||||
isOffline: entity.isOffline,
|
||||
@ -295,3 +327,15 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset
|
||||
isEdited: entity.isEdited,
|
||||
};
|
||||
}
|
||||
|
||||
export const hydrateAsset = (asset: ShallowDehydrateObject<MapAsset>) => ({
|
||||
...asset,
|
||||
checksum: Buffer.from(asset.checksum),
|
||||
createdAt: new Date(asset.createdAt),
|
||||
deletedAt: asset.deletedAt ? new Date(asset.deletedAt) : null,
|
||||
fileCreatedAt: new Date(asset.fileCreatedAt),
|
||||
fileModifiedAt: new Date(asset.fileModifiedAt),
|
||||
localDateTime: new Date(asset.localDateTime),
|
||||
thumbhash: asset.thumbhash ? Buffer.from(asset.thumbhash) : null,
|
||||
updatedAt: new Date(asset.updatedAt),
|
||||
});
|
||||
|
||||
@ -3,7 +3,7 @@ import { Type } from 'class-transformer';
|
||||
import { IsInt, IsObject, IsPositive, ValidateNested } from 'class-validator';
|
||||
import { Memory } from 'src/database';
|
||||
import { HistoryBuilder } from 'src/decorators';
|
||||
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AssetResponseDto, hydrateAsset, mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { AssetOrderWithRandom, MemoryType } from 'src/enum';
|
||||
import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation';
|
||||
@ -146,6 +146,6 @@ export const mapMemory = (entity: Memory, auth: AuthDto): MemoryResponseDto => {
|
||||
type: entity.type as MemoryType,
|
||||
data: entity.data as unknown as MemoryData,
|
||||
isSaved: entity.isSaved,
|
||||
assets: ('assets' in entity ? entity.assets : []).map((asset) => mapAsset(asset, { auth })),
|
||||
assets: ('assets' in entity ? entity.assets : []).map((asset) => mapAsset(hydrateAsset(asset), { auth })),
|
||||
};
|
||||
};
|
||||
|
||||
@ -266,6 +266,14 @@ export function mapFaces(
|
||||
): AssetFaceResponseDto {
|
||||
return {
|
||||
...mapFacesWithoutPerson(face, edits, assetDimensions),
|
||||
person: face.person?.ownerId === auth.user.id ? mapPerson(face.person) : null,
|
||||
person:
|
||||
face.person?.ownerId === auth.user.id
|
||||
? mapPerson({
|
||||
...face.person,
|
||||
birthDate: face.person.birthDate ? new Date(face.person.birthDate) : null,
|
||||
createdAt: new Date(face.person.createdAt),
|
||||
updatedAt: new Date(face.person.updatedAt),
|
||||
})
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
@ -163,8 +163,29 @@ export function mapSharedLink(sharedLink: SharedLink, options: { stripAssetMetad
|
||||
type: sharedLink.type,
|
||||
createdAt: sharedLink.createdAt,
|
||||
expiresAt: sharedLink.expiresAt,
|
||||
assets: assets.map((asset) => mapAsset(asset, { stripMetadata: options.stripAssetMetadata })),
|
||||
album: sharedLink.album ? mapAlbumWithoutAssets(sharedLink.album) : undefined,
|
||||
assets: assets.map((asset) =>
|
||||
mapAsset(
|
||||
{
|
||||
...asset,
|
||||
checksum: Buffer.from(asset.checksum),
|
||||
createdAt: new Date(asset.createdAt),
|
||||
deletedAt: asset.deletedAt ? new Date(asset.deletedAt) : null,
|
||||
localDateTime: new Date(asset.localDateTime),
|
||||
fileCreatedAt: new Date(asset.fileCreatedAt),
|
||||
fileModifiedAt: new Date(asset.fileModifiedAt),
|
||||
thumbhash: asset.thumbhash ? Buffer.from(asset.thumbhash) : null,
|
||||
updatedAt: new Date(asset.updatedAt),
|
||||
},
|
||||
{ stripMetadata: options.stripAssetMetadata },
|
||||
),
|
||||
),
|
||||
album: sharedLink.album
|
||||
? mapAlbumWithoutAssets({
|
||||
...sharedLink.album,
|
||||
createdAt: new Date(sharedLink.album.createdAt),
|
||||
updatedAt: new Date(sharedLink.album.updatedAt),
|
||||
})
|
||||
: undefined,
|
||||
allowUpload: sharedLink.allowUpload,
|
||||
allowDownload: sharedLink.allowDownload,
|
||||
showMetadata: sharedLink.showExif,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ArrayMinSize } from 'class-validator';
|
||||
import { Stack } from 'src/database';
|
||||
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AssetResponseDto, hydrateAsset, mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { ValidateUUID } from 'src/validation';
|
||||
|
||||
@ -37,6 +37,6 @@ export const mapStack = (stack: Stack, { auth }: { auth?: AuthDto }) => {
|
||||
return {
|
||||
id: stack.id,
|
||||
primaryAssetId: stack.primaryAssetId,
|
||||
assets: [...primary, ...others].map((asset) => mapAsset(asset, { auth })),
|
||||
assets: [...primary, ...others].map((asset) => mapAsset(hydrateAsset(asset), { auth })),
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,12 +1,22 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ExpressionBuilder, Insertable, Kysely, NotNull, sql, Updateable } from 'kysely';
|
||||
import {
|
||||
ExpressionBuilder,
|
||||
Insertable,
|
||||
Kysely,
|
||||
NotNull,
|
||||
Selectable,
|
||||
ShallowDehydrateObject,
|
||||
sql,
|
||||
Updateable,
|
||||
} from 'kysely';
|
||||
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { columns, Exif } from 'src/database';
|
||||
import { columns } from 'src/database';
|
||||
import { Chunked, ChunkedArray, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { AlbumUserCreateDto } from 'src/dtos/album.dto';
|
||||
import { DB } from 'src/schema';
|
||||
import { AlbumTable } from 'src/schema/tables/album.table';
|
||||
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
|
||||
import { withDefaultVisibility } from 'src/utils/database';
|
||||
|
||||
export interface AlbumAssetCount {
|
||||
@ -56,7 +66,11 @@ const withAssets = (eb: ExpressionBuilder<DB, 'album'>) => {
|
||||
.selectFrom('asset')
|
||||
.selectAll('asset')
|
||||
.leftJoin('asset_exif', 'asset.id', 'asset_exif.assetId')
|
||||
.select((eb) => eb.table('asset_exif').$castTo<Exif>().as('exifInfo'))
|
||||
.select((eb) =>
|
||||
jsonObjectFrom(eb.table('asset_exif'))
|
||||
.$castTo<ShallowDehydrateObject<Selectable<AssetExifTable>>>()
|
||||
.as('exifInfo'),
|
||||
)
|
||||
.innerJoin('album_asset', 'album_asset.assetId', 'asset.id')
|
||||
.whereRef('album_asset.albumId', '=', 'album.id')
|
||||
.where('asset.deletedAt', 'is', null)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Kysely, sql } from 'kysely';
|
||||
import { jsonArrayFrom } from 'kysely/helpers/postgres';
|
||||
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { columns } from 'src/database';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
@ -9,7 +9,6 @@ import { DB } from 'src/schema';
|
||||
import {
|
||||
anyUuid,
|
||||
asUuid,
|
||||
toJson,
|
||||
withDefaultVisibility,
|
||||
withEdits,
|
||||
withExif,
|
||||
@ -296,7 +295,11 @@ export class AssetJobRepository {
|
||||
.as('stack_result'),
|
||||
(join) => join.onTrue(),
|
||||
)
|
||||
.select((eb) => toJson(eb, 'stack_result').as('stack'))
|
||||
.select((eb) =>
|
||||
jsonObjectFrom(eb.table('stack_result'))
|
||||
.$castTo<{ id: string; primaryAssetId: string; assets: { id: string }[] } | null>()
|
||||
.as('stack'),
|
||||
)
|
||||
.where('asset.id', '=', id)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
Updateable,
|
||||
UpdateResult,
|
||||
} from 'kysely';
|
||||
import { jsonArrayFrom } from 'kysely/helpers/postgres';
|
||||
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { isEmpty, isUndefined, omitBy } from 'lodash';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { LockableProperty, Stack } from 'src/database';
|
||||
@ -545,7 +545,7 @@ export class AssetRepository {
|
||||
qb
|
||||
.leftJoin('stack', 'stack.id', 'asset.stackId')
|
||||
.$if(!stack!.assets, (qb) =>
|
||||
qb.select((eb) => eb.fn.toJson(eb.table('stack')).$castTo<Stack | null>().as('stack')),
|
||||
qb.select((eb) => jsonObjectFrom(eb.table('stack')).$castTo<Stack | null>().as('stack')),
|
||||
)
|
||||
.$if(!!stack!.assets, (qb) =>
|
||||
qb
|
||||
@ -554,7 +554,7 @@ export class AssetRepository {
|
||||
eb
|
||||
.selectFrom('asset as stacked')
|
||||
.selectAll('stack')
|
||||
.select((eb) => eb.fn('array_agg', [eb.table('stacked')]).as('assets'))
|
||||
.select((eb) => jsonArrayFrom(eb.table('stacked')).as('assets'))
|
||||
.whereRef('stacked.stackId', '=', 'stack.id')
|
||||
.whereRef('stacked.id', '!=', 'stack.primaryAssetId')
|
||||
.where('stacked.deletedAt', 'is', null)
|
||||
@ -563,7 +563,7 @@ export class AssetRepository {
|
||||
.as('stacked_assets'),
|
||||
(join) => join.on('stack.id', 'is not', null),
|
||||
)
|
||||
.select((eb) => eb.fn.toJson(eb.table('stacked_assets')).$castTo<Stack | null>().as('stack')),
|
||||
.select((eb) => jsonObjectFrom(eb.table('stacked_assets')).as('stack')),
|
||||
),
|
||||
)
|
||||
.$if(!!files, (qb) => qb.select(withFiles))
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Kysely, NotNull, sql } from 'kysely';
|
||||
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { Chunked, DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AssetType, VectorIndex } from 'src/enum';
|
||||
import { probes } from 'src/repositories/database.repository';
|
||||
import { DB } from 'src/schema';
|
||||
@ -39,14 +39,14 @@ export class DuplicateRepository {
|
||||
qb
|
||||
.selectFrom('asset_exif')
|
||||
.selectAll('asset')
|
||||
.select((eb) => eb.table('asset_exif').as('exifInfo'))
|
||||
.select((eb) => jsonObjectFrom(eb.table('asset_exif')).as('exifInfo'))
|
||||
.whereRef('asset_exif.assetId', '=', 'asset.id')
|
||||
.as('asset2'),
|
||||
(join) => join.onTrue(),
|
||||
)
|
||||
.select('asset.duplicateId')
|
||||
.select((eb) =>
|
||||
eb.fn.jsonAgg('asset2').orderBy('asset.localDateTime', 'asc').$castTo<MapAsset[]>().as('assets'),
|
||||
jsonArrayFrom(eb.selectFrom('asset2').selectAll().orderBy('asset.localDateTime', 'asc')).as('assets'),
|
||||
)
|
||||
.where('asset.ownerId', '=', asUuid(userId))
|
||||
.where('asset.duplicateId', 'is not', null)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Kysely, OrderByDirection, Selectable, sql } from 'kysely';
|
||||
import { Kysely, OrderByDirection, Selectable, ShallowDehydrateObject, sql } from 'kysely';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
@ -433,7 +433,7 @@ export class SearchRepository {
|
||||
.select((eb) =>
|
||||
eb
|
||||
.fn('to_jsonb', [eb.table('asset_exif')])
|
||||
.$castTo<Selectable<AssetExifTable>>()
|
||||
.$castTo<ShallowDehydrateObject<Selectable<AssetExifTable>>>()
|
||||
.as('exifInfo'),
|
||||
)
|
||||
.orderBy('asset_exif.city')
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Insertable, Kysely, sql, Updateable } from 'kysely';
|
||||
import { Insertable, Kysely, Selectable, ShallowDehydrateObject, sql, Updateable } from 'kysely';
|
||||
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import _ from 'lodash';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { Album, columns } from 'src/database';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { SharedLinkType } from 'src/enum';
|
||||
import { DB } from 'src/schema';
|
||||
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
|
||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||
import { SharedLinkTable } from 'src/schema/tables/shared-link.table';
|
||||
|
||||
export type SharedLinkSearchOptions = {
|
||||
@ -71,7 +72,7 @@ export class SharedLinkRepository {
|
||||
.as('exifInfo'),
|
||||
(join) => join.onTrue(),
|
||||
)
|
||||
.select((eb) => eb.fn.toJson(eb.table('exifInfo')).as('exifInfo'))
|
||||
.select((eb) => jsonObjectFrom(eb.table('exifInfo')).as('exifInfo'))
|
||||
.orderBy('asset.fileCreatedAt', 'asc')
|
||||
.as('assets'),
|
||||
(join) => join.onTrue(),
|
||||
@ -106,11 +107,15 @@ export class SharedLinkRepository {
|
||||
.select((eb) =>
|
||||
eb.fn
|
||||
.coalesce(eb.fn.jsonAgg('a').filterWhere('a.id', 'is not', null), sql`'[]'`)
|
||||
.$castTo<MapAsset[]>()
|
||||
.$castTo<
|
||||
(ShallowDehydrateObject<Selectable<AssetTable>> & {
|
||||
exifInfo: ShallowDehydrateObject<Selectable<AssetExifTable>>;
|
||||
})[]
|
||||
>()
|
||||
.as('assets'),
|
||||
)
|
||||
.groupBy(['shared_link.id', sql`"album".*`])
|
||||
.select((eb) => eb.fn.toJson('album').$castTo<Album | null>().as('album'))
|
||||
.select((eb) => jsonObjectFrom(eb.table('album')).$castTo<ShallowDehydrateObject<Album> | null>().as('album'))
|
||||
.where('shared_link.id', '=', id)
|
||||
.where('shared_link.userId', '=', userId)
|
||||
.where((eb) => eb.or([eb('shared_link.type', '=', SharedLinkType.Individual), eb('album.id', 'is not', null)]))
|
||||
@ -134,9 +139,7 @@ export class SharedLinkRepository {
|
||||
.selectAll('asset')
|
||||
.orderBy('asset.fileCreatedAt', 'asc')
|
||||
.limit(1),
|
||||
)
|
||||
.$castTo<MapAsset[]>()
|
||||
.as('assets'),
|
||||
).as('assets'),
|
||||
)
|
||||
.leftJoinLateral(
|
||||
(eb) =>
|
||||
@ -175,7 +178,7 @@ export class SharedLinkRepository {
|
||||
.as('album'),
|
||||
(join) => join.onTrue(),
|
||||
)
|
||||
.select((eb) => eb.fn.toJson('album').$castTo<Album | null>().as('album'))
|
||||
.select((eb) => eb.fn.toJson('album').$castTo<ShallowDehydrateObject<Album> | null>().as('album'))
|
||||
.where((eb) => eb.or([eb('shared_link.type', '=', SharedLinkType.Individual), eb('album.id', 'is not', null)]))
|
||||
.$if(!!albumId, (eb) => eb.where('shared_link.albumId', '=', albumId!))
|
||||
.$if(!!id, (eb) => eb.where('shared_link.id', '=', id!))
|
||||
@ -268,8 +271,12 @@ export class SharedLinkRepository {
|
||||
)
|
||||
.select((eb) =>
|
||||
eb.fn
|
||||
.coalesce(eb.fn.jsonAgg('assets').filterWhere('assets.id', 'is not', null), sql`'[]'`)
|
||||
.$castTo<MapAsset[]>()
|
||||
.coalesce(jsonArrayFrom(eb.selectFrom('assets').selectAll().where('assets.id', 'is not', null)), sql`'[]'`)
|
||||
.$castTo<
|
||||
(ShallowDehydrateObject<Selectable<AssetTable>> & {
|
||||
exifInfo: ShallowDehydrateObject<Selectable<AssetExifTable>>;
|
||||
})[]
|
||||
>()
|
||||
.as('assets'),
|
||||
)
|
||||
.groupBy('shared_link.id')
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { ReactionType } from 'src/dtos/activity.dto';
|
||||
import { ActivityService } from 'src/services/activity.service';
|
||||
import { getForActivity } from 'test/mappers';
|
||||
import { factory, newUuid, newUuids } from 'test/small.factory';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
@ -78,7 +79,7 @@ describe(ActivityService.name, () => {
|
||||
const activity = factory.activity({ albumId, assetId, userId });
|
||||
|
||||
mocks.access.activity.checkCreateAccess.mockResolvedValue(new Set([albumId]));
|
||||
mocks.activity.create.mockResolvedValue(activity);
|
||||
mocks.activity.create.mockResolvedValue(getForActivity(activity));
|
||||
|
||||
await sut.create(factory.auth({ user: { id: userId } }), {
|
||||
albumId,
|
||||
@ -101,7 +102,7 @@ describe(ActivityService.name, () => {
|
||||
const activity = factory.activity({ albumId, assetId });
|
||||
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumId]));
|
||||
mocks.activity.create.mockResolvedValue(activity);
|
||||
mocks.activity.create.mockResolvedValue(getForActivity(activity));
|
||||
|
||||
await expect(
|
||||
sut.create(factory.auth(), { albumId, assetId, type: ReactionType.COMMENT, comment: 'comment' }),
|
||||
@ -113,7 +114,7 @@ describe(ActivityService.name, () => {
|
||||
const activity = factory.activity({ userId, albumId, assetId, isLiked: true });
|
||||
|
||||
mocks.access.activity.checkCreateAccess.mockResolvedValue(new Set([albumId]));
|
||||
mocks.activity.create.mockResolvedValue(activity);
|
||||
mocks.activity.create.mockResolvedValue(getForActivity(activity));
|
||||
mocks.activity.search.mockResolvedValue([]);
|
||||
|
||||
await sut.create(factory.auth({ user: { id: userId } }), { albumId, assetId, type: ReactionType.LIKE });
|
||||
@ -127,7 +128,7 @@ describe(ActivityService.name, () => {
|
||||
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumId]));
|
||||
mocks.access.activity.checkCreateAccess.mockResolvedValue(new Set([albumId]));
|
||||
mocks.activity.search.mockResolvedValue([activity]);
|
||||
mocks.activity.search.mockResolvedValue([getForActivity(activity)]);
|
||||
|
||||
await sut.create(factory.auth(), { albumId, assetId, type: ReactionType.LIKE });
|
||||
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import _ from 'lodash';
|
||||
import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AlbumUserRole, AssetOrder, UserMetadataKey } from 'src/enum';
|
||||
import { AlbumService } from 'src/services/album.service';
|
||||
@ -9,6 +8,7 @@ import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { AuthFactory } from 'test/factories/auth.factory';
|
||||
import { UserFactory } from 'test/factories/user.factory';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { getForAlbum } from 'test/mappers';
|
||||
import { newUuid } from 'test/small.factory';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
@ -45,7 +45,7 @@ describe(AlbumService.name, () => {
|
||||
it('gets list of albums for auth user', async () => {
|
||||
const album = AlbumFactory.from().albumUser().build();
|
||||
const sharedWithUserAlbum = AlbumFactory.from().owner(album.owner).albumUser().build();
|
||||
mocks.album.getOwned.mockResolvedValue([album, sharedWithUserAlbum]);
|
||||
mocks.album.getOwned.mockResolvedValue([getForAlbum(album), getForAlbum(sharedWithUserAlbum)]);
|
||||
mocks.album.getMetadataForIds.mockResolvedValue([
|
||||
{
|
||||
albumId: album.id,
|
||||
@ -70,8 +70,13 @@ describe(AlbumService.name, () => {
|
||||
});
|
||||
|
||||
it('gets list of albums that have a specific asset', async () => {
|
||||
const album = AlbumFactory.from().owner({ isAdmin: true }).albumUser().asset().asset().build();
|
||||
mocks.album.getByAssetId.mockResolvedValue([album]);
|
||||
const album = AlbumFactory.from()
|
||||
.owner({ isAdmin: true })
|
||||
.albumUser()
|
||||
.asset({}, (builder) => builder.exif())
|
||||
.asset({}, (builder) => builder.exif())
|
||||
.build();
|
||||
mocks.album.getByAssetId.mockResolvedValue([getForAlbum(album)]);
|
||||
mocks.album.getMetadataForIds.mockResolvedValue([
|
||||
{
|
||||
albumId: album.id,
|
||||
@ -90,7 +95,7 @@ describe(AlbumService.name, () => {
|
||||
|
||||
it('gets list of albums that are shared', async () => {
|
||||
const album = AlbumFactory.from().albumUser().build();
|
||||
mocks.album.getShared.mockResolvedValue([album]);
|
||||
mocks.album.getShared.mockResolvedValue([getForAlbum(album)]);
|
||||
mocks.album.getMetadataForIds.mockResolvedValue([
|
||||
{
|
||||
albumId: album.id,
|
||||
@ -109,7 +114,7 @@ describe(AlbumService.name, () => {
|
||||
|
||||
it('gets list of albums that are NOT shared', async () => {
|
||||
const album = AlbumFactory.create();
|
||||
mocks.album.getNotShared.mockResolvedValue([album]);
|
||||
mocks.album.getNotShared.mockResolvedValue([getForAlbum(album)]);
|
||||
mocks.album.getMetadataForIds.mockResolvedValue([
|
||||
{
|
||||
albumId: album.id,
|
||||
@ -129,7 +134,7 @@ describe(AlbumService.name, () => {
|
||||
|
||||
it('counts assets correctly', async () => {
|
||||
const album = AlbumFactory.create();
|
||||
mocks.album.getOwned.mockResolvedValue([album]);
|
||||
mocks.album.getOwned.mockResolvedValue([getForAlbum(album)]);
|
||||
mocks.album.getMetadataForIds.mockResolvedValue([
|
||||
{
|
||||
albumId: album.id,
|
||||
@ -155,7 +160,7 @@ describe(AlbumService.name, () => {
|
||||
.albumUser(albumUser)
|
||||
.build();
|
||||
|
||||
mocks.album.create.mockResolvedValue(album);
|
||||
mocks.album.create.mockResolvedValue(getForAlbum(album));
|
||||
mocks.user.get.mockResolvedValue(UserFactory.create(album.albumUsers[0].user));
|
||||
mocks.user.getMetadata.mockResolvedValue([]);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId]));
|
||||
@ -192,7 +197,7 @@ describe(AlbumService.name, () => {
|
||||
.asset({ id: assetId }, (asset) => asset.exif())
|
||||
.albumUser(albumUser)
|
||||
.build();
|
||||
mocks.album.create.mockResolvedValue(album);
|
||||
mocks.album.create.mockResolvedValue(getForAlbum(album));
|
||||
mocks.user.get.mockResolvedValue(album.albumUsers[0].user);
|
||||
mocks.user.getMetadata.mockResolvedValue([
|
||||
{
|
||||
@ -250,7 +255,7 @@ describe(AlbumService.name, () => {
|
||||
.albumUser()
|
||||
.build();
|
||||
mocks.user.get.mockResolvedValue(album.albumUsers[0].user);
|
||||
mocks.album.create.mockResolvedValue(album);
|
||||
mocks.album.create.mockResolvedValue(getForAlbum(album));
|
||||
mocks.user.getMetadata.mockResolvedValue([]);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId]));
|
||||
|
||||
@ -316,7 +321,7 @@ describe(AlbumService.name, () => {
|
||||
it('should require a valid thumbnail asset id', async () => {
|
||||
const album = AlbumFactory.create();
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.album.getById.mockResolvedValue(album);
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.album.getAssetIds.mockResolvedValue(new Set());
|
||||
|
||||
await expect(
|
||||
@ -330,8 +335,8 @@ describe(AlbumService.name, () => {
|
||||
it('should allow the owner to update the album', async () => {
|
||||
const album = AlbumFactory.create();
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.album.getById.mockResolvedValue(album);
|
||||
mocks.album.update.mockResolvedValue(album);
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.album.update.mockResolvedValue(getForAlbum(album));
|
||||
|
||||
await sut.update(AuthFactory.create(album.owner), album.id, { albumName: 'new album name' });
|
||||
|
||||
@ -352,7 +357,7 @@ describe(AlbumService.name, () => {
|
||||
|
||||
it('should not let a shared user delete the album', async () => {
|
||||
const album = AlbumFactory.create();
|
||||
mocks.album.getById.mockResolvedValue(album);
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set());
|
||||
|
||||
await expect(sut.delete(AuthFactory.create(album.owner), album.id)).rejects.toBeInstanceOf(BadRequestException);
|
||||
@ -363,7 +368,7 @@ describe(AlbumService.name, () => {
|
||||
it('should let the owner delete an album', async () => {
|
||||
const album = AlbumFactory.create();
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.album.getById.mockResolvedValue(album);
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
|
||||
await sut.delete(AuthFactory.create(album.owner), album.id);
|
||||
|
||||
@ -387,7 +392,7 @@ describe(AlbumService.name, () => {
|
||||
const userId = newUuid();
|
||||
const album = AlbumFactory.from().albumUser({ userId }).build();
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.album.getById.mockResolvedValue(album);
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
await expect(
|
||||
sut.addUsers(AuthFactory.create(album.owner), album.id, { albumUsers: [{ userId }] }),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
@ -398,7 +403,7 @@ describe(AlbumService.name, () => {
|
||||
it('should throw an error if the userId does not exist', async () => {
|
||||
const album = AlbumFactory.create();
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.album.getById.mockResolvedValue(album);
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.user.get.mockResolvedValue(void 0);
|
||||
await expect(
|
||||
sut.addUsers(AuthFactory.create(album.owner), album.id, { albumUsers: [{ userId: 'unknown-user' }] }),
|
||||
@ -410,7 +415,7 @@ describe(AlbumService.name, () => {
|
||||
it('should throw an error if the userId is the ownerId', async () => {
|
||||
const album = AlbumFactory.create();
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.album.getById.mockResolvedValue(album);
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
await expect(
|
||||
sut.addUsers(AuthFactory.create(album.owner), album.id, {
|
||||
albumUsers: [{ userId: album.owner.id }],
|
||||
@ -424,8 +429,8 @@ describe(AlbumService.name, () => {
|
||||
const album = AlbumFactory.create();
|
||||
const user = UserFactory.create();
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.album.getById.mockResolvedValue(album);
|
||||
mocks.album.update.mockResolvedValue(album);
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.album.update.mockResolvedValue(getForAlbum(album));
|
||||
mocks.user.get.mockResolvedValue(user);
|
||||
mocks.albumUser.create.mockResolvedValue(AlbumUserFactory.from().album(album).user(user).build());
|
||||
|
||||
@ -456,7 +461,7 @@ describe(AlbumService.name, () => {
|
||||
const userId = newUuid();
|
||||
const album = AlbumFactory.from().albumUser({ userId }).build();
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.album.getById.mockResolvedValue(album);
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.albumUser.delete.mockResolvedValue();
|
||||
|
||||
await expect(sut.removeUser(AuthFactory.create(album.owner), album.id, userId)).resolves.toBeUndefined();
|
||||
@ -470,7 +475,7 @@ describe(AlbumService.name, () => {
|
||||
const user1 = UserFactory.create();
|
||||
const user2 = UserFactory.create();
|
||||
const album = AlbumFactory.from().albumUser({ userId: user1.id }).albumUser({ userId: user2.id }).build();
|
||||
mocks.album.getById.mockResolvedValue(album);
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
|
||||
await expect(sut.removeUser(AuthFactory.create(user1), album.id, user2.id)).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
@ -483,7 +488,7 @@ describe(AlbumService.name, () => {
|
||||
it('should allow a shared user to remove themselves', async () => {
|
||||
const user1 = UserFactory.create();
|
||||
const album = AlbumFactory.from().albumUser({ userId: user1.id }).build();
|
||||
mocks.album.getById.mockResolvedValue(album);
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.albumUser.delete.mockResolvedValue();
|
||||
|
||||
await sut.removeUser(AuthFactory.create(user1), album.id, user1.id);
|
||||
@ -495,7 +500,7 @@ describe(AlbumService.name, () => {
|
||||
it('should allow a shared user to remove themselves using "me"', async () => {
|
||||
const user = UserFactory.create();
|
||||
const album = AlbumFactory.from().albumUser({ userId: user.id }).build();
|
||||
mocks.album.getById.mockResolvedValue(album);
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.albumUser.delete.mockResolvedValue();
|
||||
|
||||
await sut.removeUser(AuthFactory.create(user), album.id, 'me');
|
||||
@ -506,7 +511,7 @@ describe(AlbumService.name, () => {
|
||||
|
||||
it('should not allow the owner to be removed', async () => {
|
||||
const album = AlbumFactory.from().albumUser().build();
|
||||
mocks.album.getById.mockResolvedValue(album);
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
|
||||
await expect(sut.removeUser(AuthFactory.create(album.owner), album.id, album.owner.id)).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
@ -517,7 +522,7 @@ describe(AlbumService.name, () => {
|
||||
|
||||
it('should throw an error for a user not in the album', async () => {
|
||||
const album = AlbumFactory.from().albumUser().build();
|
||||
mocks.album.getById.mockResolvedValue(album);
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
|
||||
await expect(sut.removeUser(AuthFactory.create(album.owner), album.id, 'user-3')).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
@ -546,7 +551,7 @@ describe(AlbumService.name, () => {
|
||||
describe('getAlbumInfo', () => {
|
||||
it('should get a shared album', async () => {
|
||||
const album = AlbumFactory.from().albumUser().build();
|
||||
mocks.album.getById.mockResolvedValue(album);
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.album.getMetadataForIds.mockResolvedValue([
|
||||
{
|
||||
@ -566,7 +571,7 @@ describe(AlbumService.name, () => {
|
||||
|
||||
it('should get a shared album via a shared link', async () => {
|
||||
const album = AlbumFactory.from().albumUser().build();
|
||||
mocks.album.getById.mockResolvedValue(album);
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.access.album.checkSharedLinkAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.album.getMetadataForIds.mockResolvedValue([
|
||||
{
|
||||
@ -588,7 +593,7 @@ describe(AlbumService.name, () => {
|
||||
it('should get a shared album via shared with user', async () => {
|
||||
const user = UserFactory.create();
|
||||
const album = AlbumFactory.from().albumUser({ userId: user.id }).build();
|
||||
mocks.album.getById.mockResolvedValue(album);
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.access.album.checkSharedAlbumAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.album.getMetadataForIds.mockResolvedValue([
|
||||
{
|
||||
@ -630,7 +635,7 @@ describe(AlbumService.name, () => {
|
||||
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id]));
|
||||
mocks.album.getById.mockResolvedValue(album);
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.album.getAssetIds.mockResolvedValueOnce(new Set());
|
||||
|
||||
await expect(
|
||||
@ -654,7 +659,7 @@ describe(AlbumService.name, () => {
|
||||
const album = AlbumFactory.from({ albumThumbnailAssetId: asset1.id }).build();
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset2.id]));
|
||||
mocks.album.getById.mockResolvedValue(album);
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.album.getAssetIds.mockResolvedValueOnce(new Set());
|
||||
|
||||
await expect(sut.addAssets(AuthFactory.create(album.owner), album.id, { ids: [asset2.id] })).resolves.toEqual([
|
||||
@ -675,7 +680,7 @@ describe(AlbumService.name, () => {
|
||||
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
|
||||
mocks.access.album.checkSharedAlbumAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id]));
|
||||
mocks.album.getById.mockResolvedValue(album);
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.album.getAssetIds.mockResolvedValueOnce(new Set());
|
||||
|
||||
await expect(
|
||||
@ -703,7 +708,7 @@ describe(AlbumService.name, () => {
|
||||
const album = AlbumFactory.from().albumUser({ userId: user.id, role: AlbumUserRole.Viewer }).build();
|
||||
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
|
||||
mocks.access.album.checkSharedAlbumAccess.mockResolvedValue(new Set());
|
||||
mocks.album.getById.mockResolvedValue(album);
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
|
||||
await expect(
|
||||
sut.addAssets(AuthFactory.create(user), album.id, { ids: [asset1.id, asset2.id, asset3.id] }),
|
||||
@ -718,7 +723,7 @@ describe(AlbumService.name, () => {
|
||||
const auth = AuthFactory.from(album.owner).sharedLink({ allowUpload: true, userId: album.ownerId }).build();
|
||||
mocks.access.album.checkSharedLinkAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id]));
|
||||
mocks.album.getById.mockResolvedValue(album);
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.album.getAssetIds.mockResolvedValueOnce(new Set());
|
||||
|
||||
await expect(sut.addAssets(auth, album.id, { ids: [asset1.id, asset2.id, asset3.id] })).resolves.toEqual([
|
||||
@ -742,7 +747,7 @@ describe(AlbumService.name, () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.access.asset.checkPartnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.album.getById.mockResolvedValue(album);
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.album.getAssetIds.mockResolvedValueOnce(new Set());
|
||||
|
||||
await expect(sut.addAssets(AuthFactory.create(album.owner), album.id, { ids: [asset.id] })).resolves.toEqual([
|
||||
@ -762,7 +767,7 @@ describe(AlbumService.name, () => {
|
||||
const album = AlbumFactory.create();
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.album.getById.mockResolvedValue(album);
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.album.getAssetIds.mockResolvedValueOnce(new Set([asset.id]));
|
||||
|
||||
await expect(sut.addAssets(AuthFactory.create(album.owner), album.id, { ids: [asset.id] })).resolves.toEqual([
|
||||
@ -776,7 +781,7 @@ describe(AlbumService.name, () => {
|
||||
const asset = AssetFactory.create();
|
||||
const album = AlbumFactory.create();
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.album.getById.mockResolvedValue(album);
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.album.getAssetIds.mockResolvedValueOnce(new Set());
|
||||
|
||||
await expect(sut.addAssets(AuthFactory.create(album.owner), album.id, { ids: [asset.id] })).resolves.toEqual([
|
||||
@ -791,7 +796,7 @@ describe(AlbumService.name, () => {
|
||||
const user = UserFactory.create();
|
||||
const album = AlbumFactory.create();
|
||||
const asset = AssetFactory.create({ ownerId: user.id });
|
||||
mocks.album.getById.mockResolvedValue(album);
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
|
||||
await expect(sut.addAssets(AuthFactory.create(user), album.id, { ids: [asset.id] })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
@ -804,7 +809,7 @@ describe(AlbumService.name, () => {
|
||||
it('should not allow unauthorized shared link access to the album', async () => {
|
||||
const album = AlbumFactory.create();
|
||||
const asset = AssetFactory.create();
|
||||
mocks.album.getById.mockResolvedValue(album);
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
|
||||
await expect(
|
||||
sut.addAssets(AuthFactory.from().sharedLink({ allowUpload: true }).build(), album.id, { ids: [asset.id] }),
|
||||
@ -821,7 +826,7 @@ describe(AlbumService.name, () => {
|
||||
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValueOnce(new Set([album1.id, album2.id]));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id]));
|
||||
mocks.album.getById.mockResolvedValueOnce(album1).mockResolvedValueOnce(album2);
|
||||
mocks.album.getById.mockResolvedValueOnce(getForAlbum(album1)).mockResolvedValueOnce(getForAlbum(album2));
|
||||
mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set());
|
||||
|
||||
await expect(
|
||||
@ -859,7 +864,7 @@ describe(AlbumService.name, () => {
|
||||
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValueOnce(new Set([album1.id, album2.id]));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id]));
|
||||
mocks.album.getById.mockResolvedValueOnce(album1).mockResolvedValueOnce(album2);
|
||||
mocks.album.getById.mockResolvedValueOnce(getForAlbum(album1)).mockResolvedValueOnce(getForAlbum(album2));
|
||||
mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set());
|
||||
|
||||
await expect(
|
||||
@ -897,7 +902,7 @@ describe(AlbumService.name, () => {
|
||||
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
|
||||
mocks.access.album.checkSharedAlbumAccess.mockResolvedValueOnce(new Set([album1.id, album2.id]));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id]));
|
||||
mocks.album.getById.mockResolvedValueOnce(album1).mockResolvedValueOnce(album2);
|
||||
mocks.album.getById.mockResolvedValueOnce(getForAlbum(album1)).mockResolvedValueOnce(getForAlbum(album2));
|
||||
mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set());
|
||||
|
||||
await expect(
|
||||
@ -943,7 +948,7 @@ describe(AlbumService.name, () => {
|
||||
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
|
||||
mocks.access.album.checkSharedAlbumAccess.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set());
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id]));
|
||||
mocks.album.getById.mockResolvedValueOnce(album1).mockResolvedValueOnce(album2);
|
||||
mocks.album.getById.mockResolvedValueOnce(getForAlbum(album1)).mockResolvedValueOnce(getForAlbum(album2));
|
||||
mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set());
|
||||
|
||||
await expect(
|
||||
@ -965,7 +970,7 @@ describe(AlbumService.name, () => {
|
||||
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
|
||||
mocks.access.album.checkSharedLinkAccess.mockResolvedValueOnce(new Set([album1.id]));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id]));
|
||||
mocks.album.getById.mockResolvedValueOnce(album1).mockResolvedValueOnce(album2);
|
||||
mocks.album.getById.mockResolvedValueOnce(getForAlbum(album1)).mockResolvedValueOnce(getForAlbum(album2));
|
||||
mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set());
|
||||
|
||||
const auth = AuthFactory.from(album1.owner).sharedLink({ allowUpload: true }).build();
|
||||
@ -1004,7 +1009,7 @@ describe(AlbumService.name, () => {
|
||||
];
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValueOnce(new Set([album1.id, album2.id]));
|
||||
mocks.access.asset.checkPartnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id]));
|
||||
mocks.album.getById.mockResolvedValueOnce(album1).mockResolvedValueOnce(album2);
|
||||
mocks.album.getById.mockResolvedValueOnce(getForAlbum(album1)).mockResolvedValueOnce(getForAlbum(album2));
|
||||
mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set());
|
||||
|
||||
await expect(
|
||||
@ -1048,7 +1053,7 @@ describe(AlbumService.name, () => {
|
||||
mocks.album.getAssetIds
|
||||
.mockResolvedValueOnce(new Set([asset1.id, asset2.id, asset3.id]))
|
||||
.mockResolvedValueOnce(new Set());
|
||||
mocks.album.getById.mockResolvedValueOnce(album1).mockResolvedValueOnce(album2);
|
||||
mocks.album.getById.mockResolvedValueOnce(getForAlbum(album1)).mockResolvedValueOnce(getForAlbum(album2));
|
||||
|
||||
await expect(
|
||||
sut.addAssetsToAlbums(AuthFactory.create(album1.owner), {
|
||||
@ -1078,7 +1083,7 @@ describe(AlbumService.name, () => {
|
||||
.mockResolvedValueOnce(new Set([album1.id]))
|
||||
.mockResolvedValueOnce(new Set([album2.id]));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id]));
|
||||
mocks.album.getById.mockResolvedValueOnce(album1).mockResolvedValueOnce(album2);
|
||||
mocks.album.getById.mockResolvedValueOnce(getForAlbum(album1)).mockResolvedValueOnce(getForAlbum(album2));
|
||||
mocks.album.getAssetIds.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id]));
|
||||
|
||||
await expect(
|
||||
@ -1107,7 +1112,7 @@ describe(AlbumService.name, () => {
|
||||
mocks.access.album.checkSharedAlbumAccess
|
||||
.mockResolvedValueOnce(new Set([album1.id]))
|
||||
.mockResolvedValueOnce(new Set([album2.id]));
|
||||
mocks.album.getById.mockResolvedValueOnce(_.cloneDeep(album1)).mockResolvedValueOnce(_.cloneDeep(album2));
|
||||
mocks.album.getById.mockResolvedValueOnce(getForAlbum(album1)).mockResolvedValueOnce(getForAlbum(album2));
|
||||
mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set());
|
||||
|
||||
await expect(
|
||||
@ -1138,7 +1143,7 @@ describe(AlbumService.name, () => {
|
||||
const album1 = AlbumFactory.create();
|
||||
const album2 = AlbumFactory.create();
|
||||
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
|
||||
mocks.album.getById.mockResolvedValueOnce(album1).mockResolvedValueOnce(album2);
|
||||
mocks.album.getById.mockResolvedValueOnce(getForAlbum(album1)).mockResolvedValueOnce(getForAlbum(album2));
|
||||
|
||||
await expect(
|
||||
sut.addAssetsToAlbums(AuthFactory.create(user), {
|
||||
@ -1160,7 +1165,7 @@ describe(AlbumService.name, () => {
|
||||
const album1 = AlbumFactory.create();
|
||||
const album2 = AlbumFactory.create();
|
||||
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
|
||||
mocks.album.getById.mockResolvedValueOnce(album1).mockResolvedValueOnce(album2);
|
||||
mocks.album.getById.mockResolvedValueOnce(getForAlbum(album1)).mockResolvedValueOnce(getForAlbum(album2));
|
||||
|
||||
await expect(
|
||||
sut.addAssetsToAlbums(AuthFactory.from().sharedLink({ allowUpload: true }).build(), {
|
||||
@ -1182,7 +1187,7 @@ describe(AlbumService.name, () => {
|
||||
const album = AlbumFactory.create();
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.album.getById.mockResolvedValue(album);
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.album.getAssetIds.mockResolvedValue(new Set([asset.id]));
|
||||
|
||||
await expect(sut.removeAssets(AuthFactory.create(album.owner), album.id, { ids: [asset.id] })).resolves.toEqual([
|
||||
@ -1196,7 +1201,7 @@ describe(AlbumService.name, () => {
|
||||
const asset = AssetFactory.create();
|
||||
const album = AlbumFactory.create();
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.album.getById.mockResolvedValue(album);
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.album.getAssetIds.mockResolvedValue(new Set());
|
||||
|
||||
await expect(sut.removeAssets(AuthFactory.create(album.owner), album.id, { ids: [asset.id] })).resolves.toEqual([
|
||||
@ -1210,7 +1215,7 @@ describe(AlbumService.name, () => {
|
||||
const asset = AssetFactory.create();
|
||||
const album = AlbumFactory.create();
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.album.getById.mockResolvedValue(album);
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.album.getAssetIds.mockResolvedValue(new Set([asset.id]));
|
||||
|
||||
await expect(sut.removeAssets(AuthFactory.create(album.owner), album.id, { ids: [asset.id] })).resolves.toEqual([
|
||||
@ -1224,7 +1229,7 @@ describe(AlbumService.name, () => {
|
||||
const album = AlbumFactory.from({ albumThumbnailAssetId: asset1.id }).build();
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id]));
|
||||
mocks.album.getById.mockResolvedValue(album);
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.album.getAssetIds.mockResolvedValue(new Set([asset1.id, asset2.id]));
|
||||
|
||||
await expect(sut.removeAssets(AuthFactory.create(album.owner), album.id, { ids: [asset1.id] })).resolves.toEqual([
|
||||
|
||||
@ -4,13 +4,12 @@ import {
|
||||
NotFoundException,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { Stats } from 'node:fs';
|
||||
import { AssetFile } from 'src/database';
|
||||
import { AssetMediaStatus, AssetRejectReason, AssetUploadAction } from 'src/dtos/asset-media-response.dto';
|
||||
import { AssetMediaCreateDto, AssetMediaReplaceDto, AssetMediaSize, UploadFieldName } from 'src/dtos/asset-media.dto';
|
||||
import { AssetMediaCreateDto, AssetMediaSize, UploadFieldName } from 'src/dtos/asset-media.dto';
|
||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AssetEditAction } from 'src/dtos/editing.dto';
|
||||
import { AssetFileType, AssetStatus, AssetType, AssetVisibility, CacheControl, JobName } from 'src/enum';
|
||||
import { AssetFileType, AssetType, AssetVisibility, CacheControl, JobName } from 'src/enum';
|
||||
import { AuthRequest } from 'src/middleware/auth.guard';
|
||||
import { AssetMediaService } from 'src/services/asset-media.service';
|
||||
import { UploadBody } from 'src/types';
|
||||
@ -22,6 +21,7 @@ import { AuthFactory } from 'test/factories/auth.factory';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { fileStub } from 'test/fixtures/file.stub';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
import { getForAsset } from 'test/mappers';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
const file1 = Buffer.from('d2947b871a706081be194569951b7db246907957', 'hex');
|
||||
@ -152,13 +152,6 @@ const createDto = Object.freeze({
|
||||
duration: '0:00:00.000000',
|
||||
}) as AssetMediaCreateDto;
|
||||
|
||||
const replaceDto = Object.freeze({
|
||||
deviceAssetId: 'deviceAssetId',
|
||||
deviceId: 'deviceId',
|
||||
fileModifiedAt: new Date('2024-04-15T23:41:36.910Z'),
|
||||
fileCreatedAt: new Date('2024-04-15T23:41:36.910Z'),
|
||||
}) as AssetMediaReplaceDto;
|
||||
|
||||
const assetEntity = Object.freeze({
|
||||
id: 'id_1',
|
||||
ownerId: 'user_id_1',
|
||||
@ -180,25 +173,6 @@ const assetEntity = Object.freeze({
|
||||
livePhotoVideoId: null,
|
||||
} as MapAsset);
|
||||
|
||||
const existingAsset = Object.freeze({
|
||||
...assetEntity,
|
||||
duration: null,
|
||||
type: AssetType.Image,
|
||||
checksum: Buffer.from('_getExistingAsset', 'utf8'),
|
||||
libraryId: 'libraryId',
|
||||
originalFileName: 'existing-filename.jpeg',
|
||||
}) as MapAsset;
|
||||
|
||||
const sidecarAsset = Object.freeze({
|
||||
...existingAsset,
|
||||
checksum: Buffer.from('_getExistingAssetWithSideCar', 'utf8'),
|
||||
}) as MapAsset;
|
||||
|
||||
const copiedAsset = Object.freeze({
|
||||
id: 'copied-asset',
|
||||
originalPath: 'copied-path',
|
||||
}) as MapAsset;
|
||||
|
||||
describe(AssetMediaService.name, () => {
|
||||
let sut: AssetMediaService;
|
||||
let mocks: ServiceMocks;
|
||||
@ -434,7 +408,7 @@ describe(AssetMediaService.name, () => {
|
||||
.owner(authStub.user1.user)
|
||||
.build();
|
||||
const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id });
|
||||
mocks.asset.getById.mockResolvedValueOnce(motionAsset);
|
||||
mocks.asset.getById.mockResolvedValueOnce(getForAsset(motionAsset));
|
||||
mocks.asset.create.mockResolvedValueOnce(asset);
|
||||
|
||||
await expect(
|
||||
@ -451,7 +425,7 @@ describe(AssetMediaService.name, () => {
|
||||
it('should hide the linked motion asset', async () => {
|
||||
const motionAsset = AssetFactory.from({ type: AssetType.Video }).owner(authStub.user1.user).build();
|
||||
const asset = AssetFactory.create();
|
||||
mocks.asset.getById.mockResolvedValueOnce(motionAsset);
|
||||
mocks.asset.getById.mockResolvedValueOnce(getForAsset(motionAsset));
|
||||
mocks.asset.create.mockResolvedValueOnce(asset);
|
||||
|
||||
await expect(
|
||||
@ -470,7 +444,7 @@ describe(AssetMediaService.name, () => {
|
||||
|
||||
it('should handle a sidecar file', async () => {
|
||||
const asset = AssetFactory.from().file({ type: AssetFileType.Sidecar }).build();
|
||||
mocks.asset.getById.mockResolvedValueOnce(asset);
|
||||
mocks.asset.getById.mockResolvedValueOnce(getForAsset(asset));
|
||||
mocks.asset.create.mockResolvedValueOnce(asset);
|
||||
|
||||
await expect(sut.uploadAsset(authStub.user1, createDto, fileStub.photo, fileStub.photoSidecar)).resolves.toEqual({
|
||||
@ -776,177 +750,6 @@ describe(AssetMediaService.name, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('replaceAsset', () => {
|
||||
it('should fail the auth check when update photo does not exist', async () => {
|
||||
await expect(sut.replaceAsset(authStub.user1, 'id', replaceDto, fileStub.photo)).rejects.toThrow(
|
||||
'Not found or no asset.update access',
|
||||
);
|
||||
|
||||
expect(mocks.asset.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fail if asset cannot be fetched', async () => {
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([existingAsset.id]));
|
||||
await expect(sut.replaceAsset(authStub.user1, existingAsset.id, replaceDto, fileStub.photo)).rejects.toThrow(
|
||||
'Asset not found',
|
||||
);
|
||||
|
||||
expect(mocks.asset.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update a photo with no sidecar to photo with no sidecar', async () => {
|
||||
const updatedFile = fileStub.photo;
|
||||
const updatedAsset = { ...existingAsset, ...updatedFile };
|
||||
mocks.asset.getById.mockResolvedValueOnce(existingAsset);
|
||||
mocks.asset.getById.mockResolvedValueOnce(updatedAsset);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([existingAsset.id]));
|
||||
// this is the original file size
|
||||
mocks.storage.stat.mockResolvedValue({ size: 0 } as Stats);
|
||||
// this is for the clone call
|
||||
mocks.asset.create.mockResolvedValue(copiedAsset);
|
||||
|
||||
await expect(sut.replaceAsset(authStub.user1, existingAsset.id, replaceDto, updatedFile)).resolves.toEqual({
|
||||
status: AssetMediaStatus.REPLACED,
|
||||
id: 'copied-asset',
|
||||
});
|
||||
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: existingAsset.id,
|
||||
originalFileName: 'photo1.jpeg',
|
||||
originalPath: 'fake_path/photo1.jpeg',
|
||||
}),
|
||||
);
|
||||
expect(mocks.asset.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
originalFileName: 'existing-filename.jpeg',
|
||||
originalPath: 'fake_path/asset_1.jpeg',
|
||||
}),
|
||||
);
|
||||
expect(mocks.asset.deleteFile).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
assetId: existingAsset.id,
|
||||
type: AssetFileType.Sidecar,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mocks.asset.updateAll).toHaveBeenCalledWith([copiedAsset.id], {
|
||||
deletedAt: expect.any(Date),
|
||||
status: AssetStatus.Trashed,
|
||||
});
|
||||
expect(mocks.user.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, updatedFile.size);
|
||||
expect(mocks.storage.utimes).toHaveBeenCalledWith(
|
||||
updatedFile.originalPath,
|
||||
expect.any(Date),
|
||||
new Date(replaceDto.fileModifiedAt),
|
||||
);
|
||||
});
|
||||
|
||||
it('should update a photo with sidecar to photo with sidecar', async () => {
|
||||
const updatedFile = fileStub.photo;
|
||||
const sidecarFile = fileStub.photoSidecar;
|
||||
const updatedAsset = { ...sidecarAsset, ...updatedFile };
|
||||
mocks.asset.getById.mockResolvedValueOnce(existingAsset);
|
||||
mocks.asset.getById.mockResolvedValueOnce(updatedAsset);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([sidecarAsset.id]));
|
||||
// this is the original file size
|
||||
mocks.storage.stat.mockResolvedValue({ size: 0 } as Stats);
|
||||
// this is for the clone call
|
||||
mocks.asset.create.mockResolvedValue(copiedAsset);
|
||||
|
||||
await expect(
|
||||
sut.replaceAsset(authStub.user1, sidecarAsset.id, replaceDto, updatedFile, sidecarFile),
|
||||
).resolves.toEqual({
|
||||
status: AssetMediaStatus.REPLACED,
|
||||
id: 'copied-asset',
|
||||
});
|
||||
|
||||
expect(mocks.asset.updateAll).toHaveBeenCalledWith([copiedAsset.id], {
|
||||
deletedAt: expect.any(Date),
|
||||
status: AssetStatus.Trashed,
|
||||
});
|
||||
expect(mocks.asset.upsertFile).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
assetId: existingAsset.id,
|
||||
path: sidecarFile.originalPath,
|
||||
type: AssetFileType.Sidecar,
|
||||
}),
|
||||
);
|
||||
expect(mocks.user.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, updatedFile.size);
|
||||
expect(mocks.storage.utimes).toHaveBeenCalledWith(
|
||||
updatedFile.originalPath,
|
||||
expect.any(Date),
|
||||
new Date(replaceDto.fileModifiedAt),
|
||||
);
|
||||
});
|
||||
|
||||
it('should update a photo with a sidecar to photo with no sidecar', async () => {
|
||||
const updatedFile = fileStub.photo;
|
||||
|
||||
const updatedAsset = { ...sidecarAsset, ...updatedFile };
|
||||
mocks.asset.getById.mockResolvedValueOnce(sidecarAsset);
|
||||
mocks.asset.getById.mockResolvedValueOnce(updatedAsset);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([sidecarAsset.id]));
|
||||
// this is the original file size
|
||||
mocks.storage.stat.mockResolvedValue({ size: 0 } as Stats);
|
||||
// this is for the copy call
|
||||
mocks.asset.create.mockResolvedValue(copiedAsset);
|
||||
|
||||
await expect(sut.replaceAsset(authStub.user1, existingAsset.id, replaceDto, updatedFile)).resolves.toEqual({
|
||||
status: AssetMediaStatus.REPLACED,
|
||||
id: 'copied-asset',
|
||||
});
|
||||
|
||||
expect(mocks.asset.updateAll).toHaveBeenCalledWith([copiedAsset.id], {
|
||||
deletedAt: expect.any(Date),
|
||||
status: AssetStatus.Trashed,
|
||||
});
|
||||
expect(mocks.asset.deleteFile).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
assetId: existingAsset.id,
|
||||
type: AssetFileType.Sidecar,
|
||||
}),
|
||||
);
|
||||
expect(mocks.user.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, updatedFile.size);
|
||||
expect(mocks.storage.utimes).toHaveBeenCalledWith(
|
||||
updatedFile.originalPath,
|
||||
expect.any(Date),
|
||||
new Date(replaceDto.fileModifiedAt),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle a photo with sidecar to duplicate photo ', async () => {
|
||||
const updatedFile = fileStub.photo;
|
||||
const error = new Error('unique key violation');
|
||||
(error as any).constraint_name = ASSET_CHECKSUM_CONSTRAINT;
|
||||
|
||||
mocks.asset.update.mockRejectedValue(error);
|
||||
mocks.asset.getById.mockResolvedValueOnce(sidecarAsset);
|
||||
mocks.asset.getUploadAssetIdByChecksum.mockResolvedValue(sidecarAsset.id);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([sidecarAsset.id]));
|
||||
// this is the original file size
|
||||
mocks.storage.stat.mockResolvedValue({ size: 0 } as Stats);
|
||||
// this is for the clone call
|
||||
mocks.asset.create.mockResolvedValue(copiedAsset);
|
||||
|
||||
await expect(sut.replaceAsset(authStub.user1, sidecarAsset.id, replaceDto, updatedFile)).resolves.toEqual({
|
||||
status: AssetMediaStatus.DUPLICATE,
|
||||
id: sidecarAsset.id,
|
||||
});
|
||||
|
||||
expect(mocks.asset.create).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.updateAll).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.upsertFile).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.deleteFile).not.toHaveBeenCalled();
|
||||
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({
|
||||
name: JobName.FileDelete,
|
||||
data: { files: [updatedFile.originalPath, undefined] },
|
||||
});
|
||||
expect(mocks.user.updateUsage).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('bulkUploadCheck', () => {
|
||||
it('should accept hex and base64 checksums', async () => {
|
||||
const file1 = Buffer.from('d2947b871a706081be194569951b7db246907957', 'hex');
|
||||
|
||||
@ -8,6 +8,7 @@ import { AssetService } from 'src/services/asset.service';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { AuthFactory } from 'test/factories/auth.factory';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { getForAsset, getForAssetDeletion, getForPartner } from 'test/mappers';
|
||||
import { factory, newUuid } from 'test/small.factory';
|
||||
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
@ -71,7 +72,7 @@ describe(AssetService.name, () => {
|
||||
describe('getRandom', () => {
|
||||
it('should get own random assets', async () => {
|
||||
mocks.partner.getAll.mockResolvedValue([]);
|
||||
mocks.asset.getRandom.mockResolvedValue([AssetFactory.create()]);
|
||||
mocks.asset.getRandom.mockResolvedValue([getForAsset(AssetFactory.create())]);
|
||||
|
||||
await sut.getRandom(authStub.admin, 1);
|
||||
|
||||
@ -82,8 +83,8 @@ describe(AssetService.name, () => {
|
||||
const partner = factory.partner({ inTimeline: false });
|
||||
const auth = factory.auth({ user: { id: partner.sharedWithId } });
|
||||
|
||||
mocks.asset.getRandom.mockResolvedValue([AssetFactory.create()]);
|
||||
mocks.partner.getAll.mockResolvedValue([partner]);
|
||||
mocks.asset.getRandom.mockResolvedValue([getForAsset(AssetFactory.create())]);
|
||||
mocks.partner.getAll.mockResolvedValue([getForPartner(partner)]);
|
||||
|
||||
await sut.getRandom(auth, 1);
|
||||
|
||||
@ -94,8 +95,8 @@ describe(AssetService.name, () => {
|
||||
const partner = factory.partner({ inTimeline: true });
|
||||
const auth = factory.auth({ user: { id: partner.sharedWithId } });
|
||||
|
||||
mocks.asset.getRandom.mockResolvedValue([AssetFactory.create()]);
|
||||
mocks.partner.getAll.mockResolvedValue([partner]);
|
||||
mocks.asset.getRandom.mockResolvedValue([getForAsset(AssetFactory.create())]);
|
||||
mocks.partner.getAll.mockResolvedValue([getForPartner(partner)]);
|
||||
|
||||
await sut.getRandom(auth, 1);
|
||||
|
||||
@ -107,7 +108,7 @@ describe(AssetService.name, () => {
|
||||
it('should allow owner access', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.asset.getById.mockResolvedValue(asset);
|
||||
mocks.asset.getById.mockResolvedValue(getForAsset(asset));
|
||||
|
||||
await sut.get(authStub.admin, asset.id);
|
||||
|
||||
@ -121,7 +122,7 @@ describe(AssetService.name, () => {
|
||||
it('should allow shared link access', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.asset.getById.mockResolvedValue(asset);
|
||||
mocks.asset.getById.mockResolvedValue(getForAsset(asset));
|
||||
|
||||
await sut.get(authStub.adminSharedLink, asset.id);
|
||||
|
||||
@ -134,7 +135,7 @@ describe(AssetService.name, () => {
|
||||
it('should strip metadata for shared link if exif is disabled', async () => {
|
||||
const asset = AssetFactory.from().exif({ description: 'foo' }).build();
|
||||
mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.asset.getById.mockResolvedValue(asset);
|
||||
mocks.asset.getById.mockResolvedValue(getForAsset(asset));
|
||||
|
||||
const result = await sut.get(
|
||||
{ ...authStub.adminSharedLink, sharedLink: { ...authStub.adminSharedLink.sharedLink!, showExif: false } },
|
||||
@ -152,7 +153,7 @@ describe(AssetService.name, () => {
|
||||
it('should allow partner sharing access', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.access.asset.checkPartnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.asset.getById.mockResolvedValue(asset);
|
||||
mocks.asset.getById.mockResolvedValue(getForAsset(asset));
|
||||
|
||||
await sut.get(authStub.admin, asset.id);
|
||||
|
||||
@ -162,7 +163,7 @@ describe(AssetService.name, () => {
|
||||
it('should allow shared album access', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.access.asset.checkAlbumAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.asset.getById.mockResolvedValue(asset);
|
||||
mocks.asset.getById.mockResolvedValue(getForAsset(asset));
|
||||
|
||||
await sut.get(authStub.admin, asset.id);
|
||||
|
||||
@ -204,8 +205,8 @@ describe(AssetService.name, () => {
|
||||
it('should update the asset', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.asset.getById.mockResolvedValue(asset);
|
||||
mocks.asset.update.mockResolvedValue(asset);
|
||||
mocks.asset.getById.mockResolvedValue(getForAsset(asset));
|
||||
mocks.asset.update.mockResolvedValue(getForAsset(asset));
|
||||
|
||||
await sut.update(authStub.admin, asset.id, { isFavorite: true });
|
||||
|
||||
@ -215,8 +216,8 @@ describe(AssetService.name, () => {
|
||||
it('should update the exif description', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.asset.getById.mockResolvedValue(asset);
|
||||
mocks.asset.update.mockResolvedValue(asset);
|
||||
mocks.asset.getById.mockResolvedValue(getForAsset(asset));
|
||||
mocks.asset.update.mockResolvedValue(getForAsset(asset));
|
||||
|
||||
await sut.update(authStub.admin, asset.id, { description: 'Test description' });
|
||||
|
||||
@ -229,8 +230,8 @@ describe(AssetService.name, () => {
|
||||
it('should update the exif rating', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.asset.getById.mockResolvedValueOnce(asset);
|
||||
mocks.asset.update.mockResolvedValueOnce(asset);
|
||||
mocks.asset.getById.mockResolvedValueOnce(getForAsset(asset));
|
||||
mocks.asset.update.mockResolvedValueOnce(getForAsset(asset));
|
||||
|
||||
await sut.update(authStub.admin, asset.id, { rating: 3 });
|
||||
|
||||
@ -274,7 +275,7 @@ describe(AssetService.name, () => {
|
||||
const motionAsset = AssetFactory.from().owner(auth.user).build();
|
||||
const asset = AssetFactory.create();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.asset.getById.mockResolvedValue(asset);
|
||||
mocks.asset.getById.mockResolvedValue(getForAsset(asset));
|
||||
|
||||
await expect(
|
||||
sut.update(authStub.admin, asset.id, {
|
||||
@ -301,7 +302,7 @@ describe(AssetService.name, () => {
|
||||
const motionAsset = AssetFactory.create({ type: AssetType.Video });
|
||||
const asset = AssetFactory.create();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.asset.getById.mockResolvedValue(motionAsset);
|
||||
mocks.asset.getById.mockResolvedValue(getForAsset(motionAsset));
|
||||
|
||||
await expect(
|
||||
sut.update(auth, asset.id, {
|
||||
@ -327,9 +328,9 @@ describe(AssetService.name, () => {
|
||||
const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Timeline });
|
||||
const stillAsset = AssetFactory.create();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([stillAsset.id]));
|
||||
mocks.asset.getById.mockResolvedValueOnce(motionAsset);
|
||||
mocks.asset.getById.mockResolvedValueOnce(stillAsset);
|
||||
mocks.asset.update.mockResolvedValue(stillAsset);
|
||||
mocks.asset.getById.mockResolvedValueOnce(getForAsset(motionAsset));
|
||||
mocks.asset.getById.mockResolvedValueOnce(getForAsset(stillAsset));
|
||||
mocks.asset.update.mockResolvedValue(getForAsset(stillAsset));
|
||||
const auth = AuthFactory.from(motionAsset.owner).build();
|
||||
|
||||
await sut.update(auth, stillAsset.id, { livePhotoVideoId: motionAsset.id });
|
||||
@ -354,9 +355,9 @@ describe(AssetService.name, () => {
|
||||
const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id });
|
||||
const unlinkedAsset = AssetFactory.create();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.asset.getById.mockResolvedValueOnce(asset);
|
||||
mocks.asset.getById.mockResolvedValueOnce(motionAsset);
|
||||
mocks.asset.update.mockResolvedValueOnce(unlinkedAsset);
|
||||
mocks.asset.getById.mockResolvedValueOnce(getForAsset(asset));
|
||||
mocks.asset.getById.mockResolvedValueOnce(getForAsset(motionAsset));
|
||||
mocks.asset.update.mockResolvedValueOnce(getForAsset(unlinkedAsset));
|
||||
|
||||
await sut.update(auth, asset.id, { livePhotoVideoId: null });
|
||||
|
||||
@ -569,7 +570,7 @@ describe(AssetService.name, () => {
|
||||
.file({ type: AssetFileType.Preview, isEdited: true })
|
||||
.file({ type: AssetFileType.Thumbnail, isEdited: true })
|
||||
.build();
|
||||
mocks.assetJob.getForAssetDeletion.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForAssetDeletion.mockResolvedValue(getForAssetDeletion(asset));
|
||||
|
||||
await sut.handleAssetDeletion({ id: asset.id, deleteOnDisk: true });
|
||||
|
||||
@ -583,7 +584,7 @@ describe(AssetService.name, () => {
|
||||
},
|
||||
],
|
||||
]);
|
||||
expect(mocks.asset.remove).toHaveBeenCalledWith(asset);
|
||||
expect(mocks.asset.remove).toHaveBeenCalledWith(getForAssetDeletion(asset));
|
||||
});
|
||||
|
||||
it('should delete the entire stack if deleted asset was the primary asset and the stack would only contain one asset afterwards', async () => {
|
||||
@ -591,11 +592,7 @@ describe(AssetService.name, () => {
|
||||
.stack({}, (builder) => builder.asset())
|
||||
.build();
|
||||
mocks.stack.delete.mockResolvedValue();
|
||||
mocks.assetJob.getForAssetDeletion.mockResolvedValue({
|
||||
...asset,
|
||||
// TODO the specific query filters out the primary asset from `stack.assets`. This should be in a mapper eventually
|
||||
stack: { ...asset.stack!, assets: asset.stack!.assets.filter(({ id }) => id !== asset.stack!.primaryAssetId) },
|
||||
});
|
||||
mocks.assetJob.getForAssetDeletion.mockResolvedValue(getForAssetDeletion(asset));
|
||||
|
||||
await sut.handleAssetDeletion({ id: asset.id, deleteOnDisk: true });
|
||||
|
||||
@ -605,7 +602,7 @@ describe(AssetService.name, () => {
|
||||
it('should delete a live photo', async () => {
|
||||
const motionAsset = AssetFactory.from({ type: AssetType.Video, visibility: AssetVisibility.Hidden }).build();
|
||||
const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id });
|
||||
mocks.assetJob.getForAssetDeletion.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForAssetDeletion.mockResolvedValue(getForAssetDeletion(asset));
|
||||
mocks.asset.getLivePhotoCount.mockResolvedValue(0);
|
||||
|
||||
await sut.handleAssetDeletion({
|
||||
@ -622,7 +619,7 @@ describe(AssetService.name, () => {
|
||||
it('should not delete a live motion part if it is being used by another asset', async () => {
|
||||
const asset = AssetFactory.create({ livePhotoVideoId: newUuid() });
|
||||
mocks.asset.getLivePhotoCount.mockResolvedValue(2);
|
||||
mocks.assetJob.getForAssetDeletion.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForAssetDeletion.mockResolvedValue(getForAssetDeletion(asset));
|
||||
|
||||
await sut.handleAssetDeletion({ id: asset.id, deleteOnDisk: true });
|
||||
|
||||
@ -633,7 +630,7 @@ describe(AssetService.name, () => {
|
||||
|
||||
it('should update usage', async () => {
|
||||
const asset = AssetFactory.from().exif({ fileSizeInByte: 5000 }).build();
|
||||
mocks.assetJob.getForAssetDeletion.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForAssetDeletion.mockResolvedValue(getForAssetDeletion(asset));
|
||||
await sut.handleAssetDeletion({ id: asset.id, deleteOnDisk: true });
|
||||
expect(mocks.user.updateUsage).toHaveBeenCalledWith(asset.ownerId, -5000);
|
||||
});
|
||||
|
||||
@ -3,6 +3,7 @@ import { DuplicateService } from 'src/services/duplicate.service';
|
||||
import { SearchService } from 'src/services/search.service';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { getForDuplicate } from 'test/mappers';
|
||||
import { newUuid } from 'test/small.factory';
|
||||
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
||||
import { beforeEach, vitest } from 'vitest';
|
||||
@ -39,11 +40,11 @@ describe(SearchService.name, () => {
|
||||
|
||||
describe('getDuplicates', () => {
|
||||
it('should get duplicates', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
const asset = AssetFactory.from().exif().build();
|
||||
mocks.duplicateRepository.getAll.mockResolvedValue([
|
||||
{
|
||||
duplicateId: 'duplicate-id',
|
||||
assets: [asset, asset],
|
||||
assets: [getForDuplicate(asset), getForDuplicate(asset)],
|
||||
},
|
||||
]);
|
||||
await expect(sut.getDuplicates(authStub.admin)).resolves.toEqual([
|
||||
|
||||
@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants';
|
||||
import { OnJob } from 'src/decorators';
|
||||
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { hydrateAsset, mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { DuplicateResponseDto } from 'src/dtos/duplicate.dto';
|
||||
import { AssetVisibility, JobName, JobStatus, QueueName } from 'src/enum';
|
||||
@ -17,7 +17,7 @@ export class DuplicateService extends BaseService {
|
||||
const duplicates = await this.duplicateRepository.getAll(auth.user.id);
|
||||
return duplicates.map(({ duplicateId, assets }) => ({
|
||||
duplicateId,
|
||||
assets: assets.map((asset) => mapAsset(asset, { auth })),
|
||||
assets: assets.map((asset) => mapAsset(hydrateAsset(asset), { auth })),
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@ -186,8 +186,8 @@ export class JobService extends BaseService {
|
||||
exifImageHeight: exif.exifImageHeight,
|
||||
fileSizeInByte: exif.fileSizeInByte,
|
||||
orientation: exif.orientation,
|
||||
dateTimeOriginal: exif.dateTimeOriginal,
|
||||
modifyDate: exif.modifyDate,
|
||||
dateTimeOriginal: exif.dateTimeOriginal ? new Date(exif.dateTimeOriginal) : null,
|
||||
modifyDate: exif.modifyDate ? new Date(exif.modifyDate) : null,
|
||||
timeZone: exif.timeZone,
|
||||
latitude: exif.latitude,
|
||||
longitude: exif.longitude,
|
||||
|
||||
@ -3,6 +3,7 @@ import { AlbumFactory } from 'test/factories/album.factory';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { AuthFactory } from 'test/factories/auth.factory';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
import { getForAlbum, getForPartner } from 'test/mappers';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
@ -52,7 +53,7 @@ describe(MapService.name, () => {
|
||||
state: asset.exifInfo.state,
|
||||
country: asset.exifInfo.country,
|
||||
};
|
||||
mocks.partner.getAll.mockResolvedValue([partner]);
|
||||
mocks.partner.getAll.mockResolvedValue([getForPartner(partner)]);
|
||||
mocks.map.getMapMarkers.mockResolvedValue([marker]);
|
||||
|
||||
const markers = await sut.getMapMarkers(auth, { withPartners: true });
|
||||
@ -81,8 +82,10 @@ describe(MapService.name, () => {
|
||||
};
|
||||
mocks.partner.getAll.mockResolvedValue([]);
|
||||
mocks.map.getMapMarkers.mockResolvedValue([marker]);
|
||||
mocks.album.getOwned.mockResolvedValue([AlbumFactory.create()]);
|
||||
mocks.album.getShared.mockResolvedValue([AlbumFactory.from().albumUser({ userId: userStub.user1.id }).build()]);
|
||||
mocks.album.getOwned.mockResolvedValue([getForAlbum(AlbumFactory.create())]);
|
||||
mocks.album.getShared.mockResolvedValue([
|
||||
getForAlbum(AlbumFactory.from().albumUser({ userId: userStub.user1.id }).build()),
|
||||
]);
|
||||
|
||||
const markers = await sut.getMapMarkers(auth, { withSharedAlbums: true });
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { ShallowDehydrateObject } from 'kysely';
|
||||
import { OutputInfo } from 'sharp';
|
||||
import { SystemConfig } from 'src/config';
|
||||
import { Exif } from 'src/database';
|
||||
@ -27,6 +28,7 @@ import { PersonFactory } from 'test/factories/person.factory';
|
||||
import { probeStub } from 'test/fixtures/media.stub';
|
||||
import { personThumbnailStub } from 'test/fixtures/person.stub';
|
||||
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
||||
import { getForGenerateThumbnail } from 'test/mappers';
|
||||
import { factory, newUuid } from 'test/small.factory';
|
||||
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
@ -367,8 +369,10 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should skip thumbnail generation if asset type is unknown', async () => {
|
||||
const asset = AssetFactory.create({ type: 'foo' as AssetType });
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
const asset = AssetFactory.from({ type: 'foo' as AssetType })
|
||||
.exif()
|
||||
.build();
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||
|
||||
await expect(sut.handleGenerateThumbnails({ id: asset.id })).resolves.toBe(JobStatus.Skipped);
|
||||
expect(mocks.media.probe).not.toHaveBeenCalled();
|
||||
@ -377,17 +381,17 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should skip video thumbnail generation if no video stream', async () => {
|
||||
const asset = AssetFactory.create({ type: AssetType.Video });
|
||||
const asset = AssetFactory.from({ type: AssetType.Video }).exif().build();
|
||||
mocks.media.probe.mockResolvedValue(probeStub.noVideoStreams);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||
await expect(sut.handleGenerateThumbnails({ id: asset.id })).rejects.toThrowError();
|
||||
expect(mocks.media.generateThumbnail).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.update).not.toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
it('should skip invisible assets', async () => {
|
||||
const asset = AssetFactory.create({ visibility: AssetVisibility.Hidden });
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
const asset = AssetFactory.from({ visibility: AssetVisibility.Hidden }).exif().build();
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||
|
||||
expect(await sut.handleGenerateThumbnails({ id: asset.id })).toEqual(JobStatus.Skipped);
|
||||
|
||||
@ -398,7 +402,7 @@ describe(MediaService.name, () => {
|
||||
it('should delete previous preview if different path', async () => {
|
||||
const asset = AssetFactory.from().file({ type: AssetFileType.Preview }).exif().build();
|
||||
mocks.systemMetadata.get.mockResolvedValue({ image: { thumbnail: { format: ImageFormat.Webp } } });
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
@ -415,7 +419,7 @@ describe(MediaService.name, () => {
|
||||
.exif({ profileDescription: 'Adobe RGB', bitsPerSample: 14 })
|
||||
.files([AssetFileType.Preview, AssetFileType.Thumbnail])
|
||||
.build();
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||
const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8');
|
||||
mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer);
|
||||
|
||||
@ -490,9 +494,9 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should generate a thumbnail for a video', async () => {
|
||||
const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' });
|
||||
const asset = AssetFactory.from({ type: AssetType.Video, originalPath: '/original/path.ext' }).exif().build();
|
||||
mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.any(String));
|
||||
@ -532,9 +536,9 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should tonemap thumbnail for hdr video', async () => {
|
||||
const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' });
|
||||
const asset = AssetFactory.from({ type: AssetType.Video, originalPath: '/original/path.ext' }).exif().build();
|
||||
mocks.media.probe.mockResolvedValue(probeStub.videoStreamHDR);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.any(String));
|
||||
@ -574,12 +578,12 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should always generate video thumbnail in one pass', async () => {
|
||||
const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' });
|
||||
const asset = AssetFactory.from({ type: AssetType.Video, originalPath: '/original/path.ext' }).exif().build();
|
||||
mocks.media.probe.mockResolvedValue(probeStub.videoStreamHDR);
|
||||
mocks.systemMetadata.get.mockResolvedValue({
|
||||
ffmpeg: { twoPass: true, maxBitrate: '5000k' },
|
||||
});
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||
@ -600,9 +604,9 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should not skip intra frames for MTS file', async () => {
|
||||
const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' });
|
||||
const asset = AssetFactory.from({ type: AssetType.Video, originalPath: '/original/path.ext' }).exif().build();
|
||||
mocks.media.probe.mockResolvedValue(probeStub.videoStreamMTS);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||
@ -618,9 +622,9 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should override reserved color metadata', async () => {
|
||||
const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' });
|
||||
const asset = AssetFactory.from({ type: AssetType.Video, originalPath: '/original/path.ext' }).exif().build();
|
||||
mocks.media.probe.mockResolvedValue(probeStub.videoStreamReserved);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||
@ -638,10 +642,10 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should use scaling divisible by 2 even when using quick sync', async () => {
|
||||
const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' });
|
||||
const asset = AssetFactory.from({ type: AssetType.Video, originalPath: '/original/path.ext' }).exif().build();
|
||||
mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p);
|
||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv } });
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||
@ -658,7 +662,7 @@ describe(MediaService.name, () => {
|
||||
it.each(Object.values(ImageFormat))('should generate an image preview in %s format', async (format) => {
|
||||
const asset = AssetFactory.from().exif().build();
|
||||
mocks.systemMetadata.get.mockResolvedValue({ image: { preview: { format } } });
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||
const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8');
|
||||
mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer);
|
||||
const previewPath = `/data/thumbs/${asset.ownerId}/${asset.id.slice(0, 2)}/${asset.id.slice(2, 4)}/${asset.id}_preview.${format}`;
|
||||
@ -708,7 +712,7 @@ describe(MediaService.name, () => {
|
||||
it.each(Object.values(ImageFormat))('should generate an image thumbnail in %s format', async (format) => {
|
||||
const asset = AssetFactory.from().exif().build();
|
||||
mocks.systemMetadata.get.mockResolvedValue({ image: { thumbnail: { format } } });
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||
const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8');
|
||||
mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer);
|
||||
const previewPath = `/data/thumbs/${asset.ownerId}/${asset.id.slice(0, 2)}/${asset.id.slice(2, 4)}/${asset.id}_preview.jpeg`;
|
||||
@ -760,7 +764,7 @@ describe(MediaService.name, () => {
|
||||
mocks.systemMetadata.get.mockResolvedValue({
|
||||
image: { preview: { progressive: true }, thumbnail: { progressive: false } },
|
||||
});
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
@ -799,7 +803,7 @@ describe(MediaService.name, () => {
|
||||
mocks.systemMetadata.get.mockResolvedValue({
|
||||
image: { preview: { progressive: false }, thumbnail: { format: ImageFormat.Jpeg, progressive: true } },
|
||||
});
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
@ -834,12 +838,12 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should never set isProgressive for videos', async () => {
|
||||
const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' });
|
||||
const asset = AssetFactory.from({ type: AssetType.Video, originalPath: '/original/path.ext' }).exif().build();
|
||||
mocks.media.probe.mockResolvedValue(probeStub.videoStreamHDR);
|
||||
mocks.systemMetadata.get.mockResolvedValue({
|
||||
image: { preview: { progressive: true }, thumbnail: { progressive: true } },
|
||||
});
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
@ -860,7 +864,7 @@ describe(MediaService.name, () => {
|
||||
it('should delete previous thumbnail if different path', async () => {
|
||||
const asset = AssetFactory.from().exif().file({ type: AssetFileType.Preview }).build();
|
||||
mocks.systemMetadata.get.mockResolvedValue({ image: { thumbnail: { format: ImageFormat.Webp } } });
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
@ -879,7 +883,7 @@ describe(MediaService.name, () => {
|
||||
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
||||
mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false });
|
||||
mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } });
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
@ -896,7 +900,7 @@ describe(MediaService.name, () => {
|
||||
.exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined })
|
||||
.build();
|
||||
mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: false } });
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
@ -910,7 +914,7 @@ describe(MediaService.name, () => {
|
||||
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
||||
mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false });
|
||||
mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } });
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
@ -925,7 +929,7 @@ describe(MediaService.name, () => {
|
||||
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
||||
mocks.media.getImageMetadata.mockResolvedValue({ width: 1000, height: 1000, isTransparent: false });
|
||||
mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } });
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
@ -941,7 +945,7 @@ describe(MediaService.name, () => {
|
||||
.exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined })
|
||||
.build();
|
||||
mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } });
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
@ -958,7 +962,7 @@ describe(MediaService.name, () => {
|
||||
.exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined })
|
||||
.build();
|
||||
mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: false } });
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
@ -977,7 +981,7 @@ describe(MediaService.name, () => {
|
||||
.exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined })
|
||||
.build();
|
||||
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
@ -1018,7 +1022,7 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
||||
mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false });
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
@ -1056,7 +1060,7 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jxl });
|
||||
mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false });
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
@ -1104,7 +1108,7 @@ describe(MediaService.name, () => {
|
||||
mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true }, extractEmbedded: false } });
|
||||
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
||||
mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false });
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
@ -1156,7 +1160,7 @@ describe(MediaService.name, () => {
|
||||
bitsPerSample: 14,
|
||||
})
|
||||
.build();
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
@ -1187,7 +1191,7 @@ describe(MediaService.name, () => {
|
||||
mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true } } });
|
||||
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
||||
mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false });
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
@ -1219,7 +1223,7 @@ describe(MediaService.name, () => {
|
||||
})
|
||||
.build();
|
||||
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
@ -1264,7 +1268,7 @@ describe(MediaService.name, () => {
|
||||
bitsPerSample: 14,
|
||||
})
|
||||
.build();
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
@ -1303,7 +1307,7 @@ describe(MediaService.name, () => {
|
||||
bitsPerSample: 14,
|
||||
})
|
||||
.build();
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||
|
||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||
|
||||
@ -1338,7 +1342,7 @@ describe(MediaService.name, () => {
|
||||
|
||||
it('should skip videos', async () => {
|
||||
const asset = AssetFactory.from({ type: AssetType.Video }).exif().build();
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||
|
||||
await expect(sut.handleAssetEditThumbnailGeneration({ id: asset.id })).resolves.toBe(JobStatus.Success);
|
||||
expect(mocks.media.generateThumbnail).not.toHaveBeenCalled();
|
||||
@ -1355,7 +1359,7 @@ describe(MediaService.name, () => {
|
||||
])
|
||||
.build();
|
||||
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||
const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8');
|
||||
mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer);
|
||||
mocks.person.getFaces.mockResolvedValue([]);
|
||||
@ -1377,7 +1381,7 @@ describe(MediaService.name, () => {
|
||||
.exif()
|
||||
.edit({ action: AssetEditAction.Crop, parameters: { height: 1152, width: 1512, x: 216, y: 1512 } })
|
||||
.build();
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||
mocks.person.getFaces.mockResolvedValue([]);
|
||||
mocks.ocr.getByAssetId.mockResolvedValue([]);
|
||||
|
||||
@ -1405,7 +1409,7 @@ describe(MediaService.name, () => {
|
||||
{ type: AssetFileType.FullSize, path: 'edited3.jpg', isEdited: true },
|
||||
])
|
||||
.build();
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||
|
||||
const status = await sut.handleAssetEditThumbnailGeneration({ id: asset.id });
|
||||
|
||||
@ -1423,7 +1427,7 @@ describe(MediaService.name, () => {
|
||||
|
||||
it('should generate all 3 edited files if an asset has edits', async () => {
|
||||
const asset = AssetFactory.from().exif().edit().build();
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||
mocks.person.getFaces.mockResolvedValue([]);
|
||||
mocks.ocr.getByAssetId.mockResolvedValue([]);
|
||||
|
||||
@ -1449,7 +1453,7 @@ describe(MediaService.name, () => {
|
||||
|
||||
it('should generate the original thumbhash if no edits exist', async () => {
|
||||
const asset = AssetFactory.from().exif().build();
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||
mocks.media.generateThumbhash.mockResolvedValue(factory.buffer());
|
||||
|
||||
await sut.handleAssetEditThumbnailGeneration({ id: asset.id, source: 'upload' });
|
||||
@ -1459,7 +1463,7 @@ describe(MediaService.name, () => {
|
||||
|
||||
it('should apply thumbhash if job source is edit and edits exist', async () => {
|
||||
const asset = AssetFactory.from().exif().edit().build();
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||
const thumbhashBuffer = factory.buffer();
|
||||
mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer);
|
||||
mocks.person.getFaces.mockResolvedValue([]);
|
||||
@ -3553,15 +3557,15 @@ describe(MediaService.name, () => {
|
||||
|
||||
describe('isSRGB', () => {
|
||||
it('should return true for srgb colorspace', () => {
|
||||
expect(sut.isSRGB({ colorspace: 'sRGB' } as Exif)).toEqual(true);
|
||||
expect(sut.isSRGB({ colorspace: 'sRGB' } as ShallowDehydrateObject<Exif>)).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return true for srgb profile description', () => {
|
||||
expect(sut.isSRGB({ profileDescription: 'sRGB v1.31' } as Exif)).toEqual(true);
|
||||
expect(sut.isSRGB({ profileDescription: 'sRGB v1.31' } as ShallowDehydrateObject<Exif>)).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return true for 8-bit image with no colorspace metadata', () => {
|
||||
expect(sut.isSRGB({ bitsPerSample: 8 } as Exif)).toEqual(true);
|
||||
expect(sut.isSRGB({ bitsPerSample: 8 } as ShallowDehydrateObject<Exif>)).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return true for image with no colorspace or bit depth metadata', () => {
|
||||
@ -3569,23 +3573,25 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should return false for non-srgb colorspace', () => {
|
||||
expect(sut.isSRGB({ colorspace: 'Adobe RGB' } as Exif)).toEqual(false);
|
||||
expect(sut.isSRGB({ colorspace: 'Adobe RGB' } as ShallowDehydrateObject<Exif>)).toEqual(false);
|
||||
});
|
||||
|
||||
it('should return false for non-srgb profile description', () => {
|
||||
expect(sut.isSRGB({ profileDescription: 'sP3C' } as Exif)).toEqual(false);
|
||||
expect(sut.isSRGB({ profileDescription: 'sP3C' } as ShallowDehydrateObject<Exif>)).toEqual(false);
|
||||
});
|
||||
|
||||
it('should return false for 16-bit image with no colorspace metadata', () => {
|
||||
expect(sut.isSRGB({ bitsPerSample: 16 } as Exif)).toEqual(false);
|
||||
expect(sut.isSRGB({ bitsPerSample: 16 } as ShallowDehydrateObject<Exif>)).toEqual(false);
|
||||
});
|
||||
|
||||
it('should return true for 16-bit image with sRGB colorspace', () => {
|
||||
expect(sut.isSRGB({ colorspace: 'sRGB', bitsPerSample: 16 } as Exif)).toEqual(true);
|
||||
expect(sut.isSRGB({ colorspace: 'sRGB', bitsPerSample: 16 } as ShallowDehydrateObject<Exif>)).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return true for 16-bit image with sRGB profile', () => {
|
||||
expect(sut.isSRGB({ profileDescription: 'sRGB', bitsPerSample: 16 } as Exif)).toEqual(true);
|
||||
expect(sut.isSRGB({ profileDescription: 'sRGB', bitsPerSample: 16 } as ShallowDehydrateObject<Exif>)).toEqual(
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { SystemConfig } from 'src/config';
|
||||
import { FACE_THUMBNAIL_SIZE, JOBS_ASSET_PAGINATION_SIZE } from 'src/constants';
|
||||
import { ImagePathOptions, StorageCore, ThumbnailPathEntity } from 'src/cores/storage.core';
|
||||
import { AssetFile, Exif } from 'src/database';
|
||||
import { AssetFile } from 'src/database';
|
||||
import { OnEvent, OnJob } from 'src/decorators';
|
||||
import { AssetEditAction, CropParameters } from 'src/dtos/editing.dto';
|
||||
import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
|
||||
@ -258,7 +258,7 @@ export class MediaService extends BaseService {
|
||||
return extracted;
|
||||
}
|
||||
|
||||
private async decodeImage(thumbSource: string | Buffer, exifInfo: Exif, targetSize?: number) {
|
||||
private async decodeImage(thumbSource: string | Buffer, exifInfo: ThumbnailAsset['exifInfo'], targetSize?: number) {
|
||||
const { image } = await this.getConfig({ withCache: true });
|
||||
const colorspace = this.isSRGB(exifInfo) ? Colorspace.Srgb : image.colorspace;
|
||||
const decodeOptions: DecodeToBufferOptions = {
|
||||
@ -753,7 +753,15 @@ export class MediaService extends BaseService {
|
||||
return name !== VideoContainer.Mp4 && !ffmpegConfig.acceptedContainers.includes(name);
|
||||
}
|
||||
|
||||
isSRGB({ colorspace, profileDescription, bitsPerSample }: Exif): boolean {
|
||||
isSRGB({
|
||||
colorspace,
|
||||
profileDescription,
|
||||
bitsPerSample,
|
||||
}: {
|
||||
colorspace: string | null;
|
||||
profileDescription: string | null;
|
||||
bitsPerSample: number | null;
|
||||
}): boolean {
|
||||
if (colorspace || profileDescription) {
|
||||
return [colorspace, profileDescription].some((s) => s?.toLowerCase().includes('srgb'));
|
||||
} else if (bitsPerSample) {
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { MemoryService } from 'src/services/memory.service';
|
||||
import { OnThisDayData } from 'src/types';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { MemoryFactory } from 'test/factories/memory.factory';
|
||||
import { getForMemory } from 'test/mappers';
|
||||
import { factory, newUuid, newUuids } from 'test/small.factory';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
@ -27,11 +30,11 @@ describe(MemoryService.name, () => {
|
||||
describe('search', () => {
|
||||
it('should search memories', async () => {
|
||||
const [userId] = newUuids();
|
||||
const asset = factory.asset();
|
||||
const memory1 = factory.memory({ ownerId: userId, assets: [asset] });
|
||||
const memory2 = factory.memory({ ownerId: userId });
|
||||
const asset = AssetFactory.create();
|
||||
const memory1 = MemoryFactory.from({ ownerId: userId }).asset(asset).build();
|
||||
const memory2 = MemoryFactory.create({ ownerId: userId });
|
||||
|
||||
mocks.memory.search.mockResolvedValue([memory1, memory2]);
|
||||
mocks.memory.search.mockResolvedValue([getForMemory(memory1), getForMemory(memory2)]);
|
||||
|
||||
await expect(sut.search(factory.auth({ user: { id: userId } }), {})).resolves.toEqual(
|
||||
expect.arrayContaining([
|
||||
@ -64,9 +67,9 @@ describe(MemoryService.name, () => {
|
||||
|
||||
it('should get a memory by id', async () => {
|
||||
const userId = newUuid();
|
||||
const memory = factory.memory({ ownerId: userId });
|
||||
const memory = MemoryFactory.create({ ownerId: userId });
|
||||
|
||||
mocks.memory.get.mockResolvedValue(memory);
|
||||
mocks.memory.get.mockResolvedValue(getForMemory(memory));
|
||||
mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set([memory.id]));
|
||||
|
||||
await expect(sut.get(factory.auth({ user: { id: userId } }), memory.id)).resolves.toMatchObject({
|
||||
@ -81,9 +84,9 @@ describe(MemoryService.name, () => {
|
||||
describe('create', () => {
|
||||
it('should skip assets the user does not have access to', async () => {
|
||||
const [assetId, userId] = newUuids();
|
||||
const memory = factory.memory({ ownerId: userId });
|
||||
const memory = MemoryFactory.create({ ownerId: userId });
|
||||
|
||||
mocks.memory.create.mockResolvedValue(memory);
|
||||
mocks.memory.create.mockResolvedValue(getForMemory(memory));
|
||||
|
||||
await expect(
|
||||
sut.create(factory.auth({ user: { id: userId } }), {
|
||||
@ -109,11 +112,11 @@ describe(MemoryService.name, () => {
|
||||
|
||||
it('should create a memory', async () => {
|
||||
const [assetId, userId] = newUuids();
|
||||
const asset = factory.asset({ id: assetId, ownerId: userId });
|
||||
const memory = factory.memory({ assets: [asset] });
|
||||
const asset = AssetFactory.create({ id: assetId, ownerId: userId });
|
||||
const memory = MemoryFactory.from().asset(asset).build();
|
||||
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.memory.create.mockResolvedValue(memory);
|
||||
mocks.memory.create.mockResolvedValue(getForMemory(memory));
|
||||
|
||||
await expect(
|
||||
sut.create(factory.auth({ user: { id: userId } }), {
|
||||
@ -131,9 +134,9 @@ describe(MemoryService.name, () => {
|
||||
});
|
||||
|
||||
it('should create a memory without assets', async () => {
|
||||
const memory = factory.memory();
|
||||
const memory = MemoryFactory.create();
|
||||
|
||||
mocks.memory.create.mockResolvedValue(memory);
|
||||
mocks.memory.create.mockResolvedValue(getForMemory(memory));
|
||||
|
||||
await expect(
|
||||
sut.create(factory.auth(), {
|
||||
@ -155,10 +158,10 @@ describe(MemoryService.name, () => {
|
||||
});
|
||||
|
||||
it('should update a memory', async () => {
|
||||
const memory = factory.memory();
|
||||
const memory = MemoryFactory.create();
|
||||
|
||||
mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set([memory.id]));
|
||||
mocks.memory.update.mockResolvedValue(memory);
|
||||
mocks.memory.update.mockResolvedValue(getForMemory(memory));
|
||||
|
||||
await expect(sut.update(factory.auth(), memory.id, { isSaved: true })).resolves.toBeDefined();
|
||||
|
||||
@ -198,10 +201,10 @@ describe(MemoryService.name, () => {
|
||||
|
||||
it('should require asset access', async () => {
|
||||
const assetId = newUuid();
|
||||
const memory = factory.memory();
|
||||
const memory = MemoryFactory.create();
|
||||
|
||||
mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set([memory.id]));
|
||||
mocks.memory.get.mockResolvedValue(memory);
|
||||
mocks.memory.get.mockResolvedValue(getForMemory(memory));
|
||||
mocks.memory.getAssetIds.mockResolvedValue(new Set());
|
||||
|
||||
await expect(sut.addAssets(factory.auth(), memory.id, { ids: [assetId] })).resolves.toEqual([
|
||||
@ -212,11 +215,11 @@ describe(MemoryService.name, () => {
|
||||
});
|
||||
|
||||
it('should skip assets already in the memory', async () => {
|
||||
const asset = factory.asset();
|
||||
const memory = factory.memory({ assets: [asset] });
|
||||
const asset = AssetFactory.create();
|
||||
const memory = MemoryFactory.from().asset(asset).build();
|
||||
|
||||
mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set([memory.id]));
|
||||
mocks.memory.get.mockResolvedValue(memory);
|
||||
mocks.memory.get.mockResolvedValue(getForMemory(memory));
|
||||
mocks.memory.getAssetIds.mockResolvedValue(new Set([asset.id]));
|
||||
|
||||
await expect(sut.addAssets(factory.auth(), memory.id, { ids: [asset.id] })).resolves.toEqual([
|
||||
@ -228,12 +231,12 @@ describe(MemoryService.name, () => {
|
||||
|
||||
it('should add assets', async () => {
|
||||
const assetId = newUuid();
|
||||
const memory = factory.memory();
|
||||
const memory = MemoryFactory.create();
|
||||
|
||||
mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set([memory.id]));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId]));
|
||||
mocks.memory.get.mockResolvedValue(memory);
|
||||
mocks.memory.update.mockResolvedValue(memory);
|
||||
mocks.memory.get.mockResolvedValue(getForMemory(memory));
|
||||
mocks.memory.update.mockResolvedValue(getForMemory(memory));
|
||||
mocks.memory.getAssetIds.mockResolvedValue(new Set());
|
||||
mocks.memory.addAssetIds.mockResolvedValue();
|
||||
|
||||
@ -266,14 +269,14 @@ describe(MemoryService.name, () => {
|
||||
});
|
||||
|
||||
it('should remove assets', async () => {
|
||||
const memory = factory.memory();
|
||||
const asset = factory.asset();
|
||||
const memory = MemoryFactory.create();
|
||||
const asset = AssetFactory.create();
|
||||
|
||||
mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set([memory.id]));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.memory.getAssetIds.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.memory.removeAssetIds.mockResolvedValue();
|
||||
mocks.memory.update.mockResolvedValue(memory);
|
||||
mocks.memory.update.mockResolvedValue(getForMemory(memory));
|
||||
|
||||
await expect(sut.removeAssets(factory.auth(), memory.id, { ids: [asset.id] })).resolves.toEqual([
|
||||
{ id: asset.id, success: true },
|
||||
|
||||
@ -19,6 +19,7 @@ import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { PersonFactory } from 'test/factories/person.factory';
|
||||
import { probeStub } from 'test/fixtures/media.stub';
|
||||
import { tagStub } from 'test/fixtures/tag.stub';
|
||||
import { getForMetadataExtraction, getForSidecarWrite } from 'test/mappers';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
@ -176,7 +177,7 @@ describe(MetadataService.name, () => {
|
||||
const originalDate = new Date('2023-11-21T16:13:17.517Z');
|
||||
const sidecarDate = new Date('2022-01-01T00:00:00.000Z');
|
||||
const asset = AssetFactory.from().file({ type: AssetFileType.Sidecar }).build();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mockReadTags({ CreationDate: originalDate.toISOString() }, { CreationDate: sidecarDate.toISOString() });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
@ -198,7 +199,7 @@ describe(MetadataService.name, () => {
|
||||
const fileCreatedAt = new Date('2022-01-01T00:00:00.000Z');
|
||||
const fileModifiedAt = new Date('2021-01-01T00:00:00.000Z');
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mocks.storage.stat.mockResolvedValue({
|
||||
size: 123_456,
|
||||
mtime: fileModifiedAt,
|
||||
@ -228,7 +229,7 @@ describe(MetadataService.name, () => {
|
||||
const fileCreatedAt = new Date('2021-01-01T00:00:00.000Z');
|
||||
const fileModifiedAt = new Date('2022-01-01T00:00:00.000Z');
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mocks.storage.stat.mockResolvedValue({
|
||||
size: 123_456,
|
||||
mtime: fileModifiedAt,
|
||||
@ -257,7 +258,7 @@ describe(MetadataService.name, () => {
|
||||
it('should determine dateTimeOriginal regardless of the server time zone', async () => {
|
||||
process.env.TZ = 'America/Los_Angeles';
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mockReadTags({ DateTimeOriginal: '2022:01:01 00:00:00' });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
@ -277,7 +278,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should handle lists of numbers', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mocks.storage.stat.mockResolvedValue({
|
||||
size: 123_456,
|
||||
mtime: asset.fileModifiedAt,
|
||||
@ -305,7 +306,7 @@ describe(MetadataService.name, () => {
|
||||
it('should not delete latituide and longitude without reverse geocode', async () => {
|
||||
// regression test for issue 17511
|
||||
const asset = AssetFactory.from().exif().build();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mocks.systemMetadata.get.mockResolvedValue({ reverseGeocoding: { enabled: false } });
|
||||
mocks.storage.stat.mockResolvedValue({
|
||||
size: 123_456,
|
||||
@ -337,7 +338,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should apply reverse geocoding', async () => {
|
||||
const asset = AssetFactory.from().exif({ latitude: 10, longitude: 20 }).build();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mocks.systemMetadata.get.mockResolvedValue({ reverseGeocoding: { enabled: true } });
|
||||
mocks.map.reverseGeocode.mockResolvedValue({ city: 'City', state: 'State', country: 'Country' });
|
||||
mocks.storage.stat.mockResolvedValue({
|
||||
@ -367,7 +368,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should discard latitude and longitude on null island', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mockReadTags({
|
||||
GPSLatitude: 0,
|
||||
GPSLongitude: 0,
|
||||
@ -383,7 +384,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should extract tags from TagsList', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent'] });
|
||||
mockReadTags({ TagsList: ['Parent'] });
|
||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||
@ -395,7 +396,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should extract hierarchy from TagsList', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent/Child'] });
|
||||
mockReadTags({ TagsList: ['Parent/Child'] });
|
||||
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert);
|
||||
@ -417,7 +418,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should extract tags from Keywords as a string', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent'] });
|
||||
mockReadTags({ Keywords: 'Parent' });
|
||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||
@ -429,7 +430,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should extract tags from Keywords as a list', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent'] });
|
||||
mockReadTags({ Keywords: ['Parent'] });
|
||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||
@ -441,7 +442,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should extract tags from Keywords as a list with a number', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent', '2024'] });
|
||||
mockReadTags({ Keywords: ['Parent', 2024] });
|
||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||
@ -454,7 +455,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should extract hierarchal tags from Keywords', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent/Child'] });
|
||||
mockReadTags({ Keywords: 'Parent/Child' });
|
||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||
@ -474,7 +475,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should ignore Keywords when TagsList is present', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent/Child', 'Child'] });
|
||||
mockReadTags({ Keywords: 'Child', TagsList: ['Parent/Child'] });
|
||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||
@ -495,7 +496,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should extract hierarchy from HierarchicalSubject', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent/Child', 'TagA'] });
|
||||
mockReadTags({ HierarchicalSubject: ['Parent|Child', 'TagA'] });
|
||||
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert);
|
||||
@ -522,7 +523,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should extract tags from HierarchicalSubject as a list with a number', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent', '2024'] });
|
||||
mockReadTags({ HierarchicalSubject: ['Parent', 2024] });
|
||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||
@ -535,7 +536,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should extract ignore / characters in a HierarchicalSubject tag', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Mom|Dad'] });
|
||||
mockReadTags({ HierarchicalSubject: ['Mom/Dad'] });
|
||||
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert);
|
||||
@ -551,7 +552,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should ignore HierarchicalSubject when TagsList is present', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent/Child', 'Parent2/Child2'] });
|
||||
mockReadTags({ HierarchicalSubject: ['Parent2|Child2'], TagsList: ['Parent/Child'] });
|
||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||
@ -572,7 +573,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should remove existing tags', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mockReadTags({});
|
||||
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
@ -582,7 +583,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should not apply motion photos if asset is video', async () => {
|
||||
const asset = AssetFactory.create({ type: AssetType.Video });
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
@ -597,7 +598,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should handle an invalid Directory Item', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mockReadTags({
|
||||
MotionPhoto: 1,
|
||||
ContainerDirectory: [{ Foo: 100 }],
|
||||
@ -608,7 +609,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should extract the correct video orientation', async () => {
|
||||
const asset = AssetFactory.create({ type: AssetType.Video });
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mocks.media.probe.mockResolvedValue(probeStub.videoStreamVertical2160p);
|
||||
mockReadTags({});
|
||||
|
||||
@ -624,7 +625,7 @@ describe(MetadataService.name, () => {
|
||||
it('should extract the MotionPhotoVideo tag from Samsung HEIC motion photos', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden });
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mocks.storage.stat.mockResolvedValue({
|
||||
size: 123_456,
|
||||
mtime: asset.fileModifiedAt,
|
||||
@ -686,7 +687,7 @@ describe(MetadataService.name, () => {
|
||||
mtimeMs: asset.fileModifiedAt.valueOf(),
|
||||
birthtimeMs: asset.fileCreatedAt.valueOf(),
|
||||
} as Stats);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mockReadTags({
|
||||
Directory: 'foo/bar/',
|
||||
EmbeddedVideoFile: new BinaryField(0, ''),
|
||||
@ -733,7 +734,7 @@ describe(MetadataService.name, () => {
|
||||
it('should extract the motion photo video from the XMP directory entry ', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden });
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mocks.storage.stat.mockResolvedValue({
|
||||
size: 123_456,
|
||||
mtime: asset.fileModifiedAt,
|
||||
@ -786,7 +787,7 @@ describe(MetadataService.name, () => {
|
||||
it('should delete old motion photo video assets if they do not match what is extracted', async () => {
|
||||
const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden });
|
||||
const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id });
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mockReadTags({
|
||||
Directory: 'foo/bar/',
|
||||
MotionPhoto: 1,
|
||||
@ -808,7 +809,7 @@ describe(MetadataService.name, () => {
|
||||
it('should not create a new motion photo video asset if the hash of the extracted video matches an existing asset', async () => {
|
||||
const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden });
|
||||
const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id });
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mockReadTags({
|
||||
Directory: 'foo/bar/',
|
||||
MotionPhoto: 1,
|
||||
@ -832,7 +833,7 @@ describe(MetadataService.name, () => {
|
||||
it('should link and hide motion video asset to still asset if the hash of the extracted video matches an existing asset', async () => {
|
||||
const motionAsset = AssetFactory.create({ type: AssetType.Video });
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mockReadTags({
|
||||
Directory: 'foo/bar/',
|
||||
MotionPhoto: 1,
|
||||
@ -859,7 +860,7 @@ describe(MetadataService.name, () => {
|
||||
it('should not update storage usage if motion photo is external', async () => {
|
||||
const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden });
|
||||
const asset = AssetFactory.create({ isExternal: true });
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mockReadTags({
|
||||
Directory: 'foo/bar/',
|
||||
MotionPhoto: 1,
|
||||
@ -904,7 +905,7 @@ describe(MetadataService.name, () => {
|
||||
Rating: 3,
|
||||
};
|
||||
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mockReadTags(tags);
|
||||
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
@ -969,7 +970,7 @@ describe(MetadataService.name, () => {
|
||||
DateTimeOriginal: ExifDateTime.fromISO(someDate + '+00:00'),
|
||||
zone: undefined,
|
||||
};
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mockReadTags(tags);
|
||||
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
@ -984,7 +985,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should extract duration', async () => {
|
||||
const asset = AssetFactory.create({ type: AssetType.Video });
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mocks.media.probe.mockResolvedValue({
|
||||
...probeStub.videoStreamH264,
|
||||
format: {
|
||||
@ -1007,7 +1008,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should only extract duration for videos', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mocks.media.probe.mockResolvedValue({
|
||||
...probeStub.videoStreamH264,
|
||||
format: {
|
||||
@ -1029,7 +1030,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should omit duration of zero', async () => {
|
||||
const asset = AssetFactory.create({ type: AssetType.Video });
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mocks.media.probe.mockResolvedValue({
|
||||
...probeStub.videoStreamH264,
|
||||
format: {
|
||||
@ -1052,7 +1053,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should a handle duration of 1 week', async () => {
|
||||
const asset = AssetFactory.create({ type: AssetType.Video });
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mocks.media.probe.mockResolvedValue({
|
||||
...probeStub.videoStreamH264,
|
||||
format: {
|
||||
@ -1075,7 +1076,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should use Duration from exif', async () => {
|
||||
const asset = AssetFactory.create({ originalFileName: 'file.webp' });
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mockReadTags({ Duration: 123 }, {});
|
||||
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
@ -1086,7 +1087,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should prefer Duration from exif over sidecar', async () => {
|
||||
const asset = AssetFactory.from({ originalFileName: 'file.webp' }).file({ type: AssetFileType.Sidecar }).build();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
|
||||
mockReadTags({ Duration: 123 }, { Duration: 456 });
|
||||
|
||||
@ -1098,7 +1099,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should ignore all Duration tags for definitely static images', async () => {
|
||||
const asset = AssetFactory.from({ originalFileName: 'file.dng' }).build();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mockReadTags({ Duration: 123 }, { Duration: 456 });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
@ -1109,7 +1110,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should ignore Duration from exif for videos', async () => {
|
||||
const asset = AssetFactory.create({ type: AssetType.Video });
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mockReadTags({ Duration: 123 }, {});
|
||||
mocks.media.probe.mockResolvedValue({
|
||||
...probeStub.videoStreamH264,
|
||||
@ -1127,7 +1128,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should trim whitespace from description', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mockReadTags({ Description: '\t \v \f \n \r' });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
@ -1150,7 +1151,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should handle a numeric description', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mockReadTags({ Description: 1000 });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
@ -1164,7 +1165,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should skip importing metadata when the feature is disabled', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: false } } });
|
||||
mockReadTags(makeFaceTags({ Name: 'Person 1' }));
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
@ -1173,7 +1174,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should skip importing metadata face for assets without tags.RegionInfo', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
||||
mockReadTags();
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
@ -1182,7 +1183,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should skip importing faces without name', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
||||
mockReadTags(makeFaceTags());
|
||||
mocks.person.getDistinctNames.mockResolvedValue([]);
|
||||
@ -1195,7 +1196,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should skip importing faces with empty name', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
||||
mockReadTags(makeFaceTags({ Name: '' }));
|
||||
mocks.person.getDistinctNames.mockResolvedValue([]);
|
||||
@ -1210,7 +1211,7 @@ describe(MetadataService.name, () => {
|
||||
const asset = AssetFactory.create();
|
||||
const person = PersonFactory.create();
|
||||
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
||||
mockReadTags(makeFaceTags({ Name: person.name }));
|
||||
mocks.person.getDistinctNames.mockResolvedValue([]);
|
||||
@ -1252,7 +1253,7 @@ describe(MetadataService.name, () => {
|
||||
const asset = AssetFactory.create();
|
||||
const person = PersonFactory.create();
|
||||
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
||||
mockReadTags(makeFaceTags({ Name: person.name }));
|
||||
mocks.person.getDistinctNames.mockResolvedValue([{ id: person.id, name: person.name }]);
|
||||
@ -1339,7 +1340,7 @@ describe(MetadataService.name, () => {
|
||||
const asset = AssetFactory.create();
|
||||
const person = PersonFactory.create();
|
||||
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
||||
mockReadTags(makeFaceTags({ Name: person.name }, orientation));
|
||||
mocks.person.getDistinctNames.mockResolvedValue([]);
|
||||
@ -1383,7 +1384,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should handle invalid modify date', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mockReadTags({ ModifyDate: '00:00:00.000' });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
@ -1397,7 +1398,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should handle invalid rating value', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mockReadTags({ Rating: 6 });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
@ -1411,7 +1412,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should handle valid rating value', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mockReadTags({ Rating: 5 });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
@ -1425,7 +1426,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should handle 0 as unrated -> null', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mockReadTags({ Rating: 0 });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
@ -1439,7 +1440,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should handle valid negative rating value', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mockReadTags({ Rating: -1 });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
@ -1453,7 +1454,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should handle livePhotoCID not set', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
|
||||
@ -1468,7 +1469,7 @@ describe(MetadataService.name, () => {
|
||||
it('should handle not finding a match', async () => {
|
||||
const asset = AssetFactory.create({ type: AssetType.Video });
|
||||
mocks.media.probe.mockResolvedValue(probeStub.videoStreamVertical2160p);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mockReadTags({ ContentIdentifier: 'CID' });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
@ -1490,7 +1491,7 @@ describe(MetadataService.name, () => {
|
||||
it('should link photo and video', async () => {
|
||||
const motionAsset = AssetFactory.create({ type: AssetType.Video });
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mocks.asset.findLivePhotoMatch.mockResolvedValue(motionAsset);
|
||||
mockReadTags({ ContentIdentifier: 'CID' });
|
||||
|
||||
@ -1518,7 +1519,7 @@ describe(MetadataService.name, () => {
|
||||
it('should notify clients on live photo link', async () => {
|
||||
const motionAsset = AssetFactory.create({ type: AssetType.Video });
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mocks.asset.findLivePhotoMatch.mockResolvedValue(motionAsset);
|
||||
mockReadTags({ ContentIdentifier: 'CID' });
|
||||
|
||||
@ -1533,7 +1534,7 @@ describe(MetadataService.name, () => {
|
||||
it('should search by libraryId', async () => {
|
||||
const motionAsset = AssetFactory.create({ type: AssetType.Video, libraryId: 'library-id' });
|
||||
const asset = AssetFactory.create({ libraryId: 'library-id' });
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mocks.asset.findLivePhotoMatch.mockResolvedValue(motionAsset);
|
||||
mockReadTags({ ContentIdentifier: 'CID' });
|
||||
|
||||
@ -1570,7 +1571,7 @@ describe(MetadataService.name, () => {
|
||||
{ exif: { AndroidMake: '1', AndroidModel: '2' }, expected: { make: '1', model: '2' } },
|
||||
])('should read camera make and model $exif -> $expected', async ({ exif, expected }) => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mockReadTags(exif);
|
||||
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
@ -1595,7 +1596,7 @@ describe(MetadataService.name, () => {
|
||||
{ exif: { LensID: '' }, expected: null },
|
||||
])('should read camera lens information $exif -> $expected', async ({ exif, expected }) => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mockReadTags(exif);
|
||||
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
@ -1609,7 +1610,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should properly set width/height for normal images', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mockReadTags({ ImageWidth: 1000, ImageHeight: 2000 });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
@ -1623,7 +1624,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should properly swap asset width/height for rotated images', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mockReadTags({ ImageWidth: 1000, ImageHeight: 2000, Orientation: 6 });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
@ -1637,7 +1638,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should not overwrite existing width/height if they already exist', async () => {
|
||||
const asset = AssetFactory.create({ width: 1920, height: 1080 });
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mockReadTags({ ImageWidth: 1280, ImageHeight: 720 });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
@ -1754,17 +1755,20 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should skip jobs with no metadata', async () => {
|
||||
mocks.assetJob.getLockedPropertiesForMetadataExtraction.mockResolvedValue([]);
|
||||
const asset = factory.jobAssets.sidecarWrite();
|
||||
mocks.assetJob.getForSidecarWriteJob.mockResolvedValue(asset);
|
||||
const asset = AssetFactory.from().file({ type: AssetFileType.Sidecar }).exif().build();
|
||||
mocks.assetJob.getForSidecarWriteJob.mockResolvedValue(getForSidecarWrite(asset));
|
||||
await expect(sut.handleSidecarWrite({ id: asset.id })).resolves.toBe(JobStatus.Skipped);
|
||||
expect(mocks.metadata.writeTags).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should write tags', async () => {
|
||||
const asset = factory.jobAssets.sidecarWrite();
|
||||
const description = 'this is a description';
|
||||
const gps = 12;
|
||||
const date = '2023-11-21T22:56:12.196-06:00';
|
||||
const asset = AssetFactory.from()
|
||||
.file({ type: AssetFileType.Sidecar })
|
||||
.exif({ description, dateTimeOriginal: new Date(date), latitude: gps, longitude: gps })
|
||||
.build();
|
||||
|
||||
mocks.assetJob.getLockedPropertiesForMetadataExtraction.mockResolvedValue([
|
||||
'description',
|
||||
@ -1773,7 +1777,7 @@ describe(MetadataService.name, () => {
|
||||
'dateTimeOriginal',
|
||||
'timeZone',
|
||||
]);
|
||||
mocks.assetJob.getForSidecarWriteJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForSidecarWriteJob.mockResolvedValue(getForSidecarWrite(asset));
|
||||
await expect(
|
||||
sut.handleSidecarWrite({
|
||||
id: asset.id,
|
||||
@ -1796,22 +1800,22 @@ describe(MetadataService.name, () => {
|
||||
});
|
||||
|
||||
it('should write rating', async () => {
|
||||
const asset = factory.jobAssets.sidecarWrite();
|
||||
const asset = AssetFactory.from().file({ type: AssetFileType.Sidecar }).exif().build();
|
||||
asset.exifInfo.rating = 4;
|
||||
|
||||
mocks.assetJob.getLockedPropertiesForMetadataExtraction.mockResolvedValue(['rating']);
|
||||
mocks.assetJob.getForSidecarWriteJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForSidecarWriteJob.mockResolvedValue(getForSidecarWrite(asset));
|
||||
await expect(sut.handleSidecarWrite({ id: asset.id })).resolves.toBe(JobStatus.Success);
|
||||
expect(mocks.metadata.writeTags).toHaveBeenCalledWith(asset.files[0].path, { Rating: 4 });
|
||||
expect(mocks.asset.unlockProperties).toHaveBeenCalledWith(asset.id, ['rating']);
|
||||
});
|
||||
|
||||
it('should write null rating as 0', async () => {
|
||||
const asset = factory.jobAssets.sidecarWrite();
|
||||
const asset = AssetFactory.from().file({ type: AssetFileType.Sidecar }).exif().build();
|
||||
asset.exifInfo.rating = null;
|
||||
|
||||
mocks.assetJob.getLockedPropertiesForMetadataExtraction.mockResolvedValue(['rating']);
|
||||
mocks.assetJob.getForSidecarWriteJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForSidecarWriteJob.mockResolvedValue(getForSidecarWrite(asset));
|
||||
await expect(sut.handleSidecarWrite({ id: asset.id })).resolves.toBe(JobStatus.Success);
|
||||
expect(mocks.metadata.writeTags).toHaveBeenCalledWith(asset.files[0].path, { Rating: 0 });
|
||||
expect(mocks.asset.unlockProperties).toHaveBeenCalledWith(asset.id, ['rating']);
|
||||
|
||||
@ -8,7 +8,7 @@ import { constants } from 'node:fs/promises';
|
||||
import { join, parse } from 'node:path';
|
||||
import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants';
|
||||
import { StorageCore } from 'src/cores/storage.core';
|
||||
import { Asset, AssetFace, AssetFile } from 'src/database';
|
||||
import { Asset, AssetFile } from 'src/database';
|
||||
import { OnEvent, OnJob } from 'src/decorators';
|
||||
import {
|
||||
AssetFileType,
|
||||
@ -829,7 +829,7 @@ export class MetadataService extends BaseService {
|
||||
}
|
||||
|
||||
private async applyTaggedFaces(
|
||||
asset: { id: string; ownerId: string; faces: AssetFace[]; originalPath: string },
|
||||
asset: { id: string; ownerId: string; faces: { id: string; sourceType: SourceType }[]; originalPath: string },
|
||||
tags: ImmichTags,
|
||||
) {
|
||||
if (!tags.RegionInfo?.AppliedToDimensions || tags.RegionInfo.RegionList.length === 0) {
|
||||
|
||||
@ -10,6 +10,7 @@ import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { UserFactory } from 'test/factories/user.factory';
|
||||
import { notificationStub } from 'test/fixtures/notification.stub';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
import { getForAlbum } from 'test/mappers';
|
||||
import { newUuid } from 'test/small.factory';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
@ -269,14 +270,14 @@ describe(NotificationService.name, () => {
|
||||
});
|
||||
|
||||
it('should skip if recipient could not be found', async () => {
|
||||
mocks.album.getById.mockResolvedValue(AlbumFactory.create());
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(AlbumFactory.create()));
|
||||
|
||||
await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Skipped);
|
||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip if the recipient has email notifications disabled', async () => {
|
||||
mocks.album.getById.mockResolvedValue(AlbumFactory.create());
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(AlbumFactory.create()));
|
||||
mocks.user.get.mockResolvedValue({
|
||||
...userStub.user1,
|
||||
metadata: [
|
||||
@ -292,7 +293,7 @@ describe(NotificationService.name, () => {
|
||||
});
|
||||
|
||||
it('should skip if the recipient has email notifications for album invite disabled', async () => {
|
||||
mocks.album.getById.mockResolvedValue(AlbumFactory.create());
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(AlbumFactory.create()));
|
||||
mocks.user.get.mockResolvedValue({
|
||||
...userStub.user1,
|
||||
metadata: [
|
||||
@ -308,7 +309,7 @@ describe(NotificationService.name, () => {
|
||||
});
|
||||
|
||||
it('should send invite email', async () => {
|
||||
mocks.album.getById.mockResolvedValue(AlbumFactory.create());
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(AlbumFactory.create()));
|
||||
mocks.user.get.mockResolvedValue({
|
||||
...userStub.user1,
|
||||
metadata: [
|
||||
@ -331,7 +332,7 @@ describe(NotificationService.name, () => {
|
||||
|
||||
it('should send invite email without album thumbnail if thumbnail asset does not exist', async () => {
|
||||
const album = AlbumFactory.create({ albumThumbnailAssetId: newUuid() });
|
||||
mocks.album.getById.mockResolvedValue(album);
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.user.get.mockResolvedValue({
|
||||
...userStub.user1,
|
||||
metadata: [
|
||||
@ -363,7 +364,7 @@ describe(NotificationService.name, () => {
|
||||
it('should send invite email with album thumbnail as jpeg', async () => {
|
||||
const assetFile = AssetFileFactory.create({ type: AssetFileType.Thumbnail });
|
||||
const album = AlbumFactory.create({ albumThumbnailAssetId: assetFile.assetId });
|
||||
mocks.album.getById.mockResolvedValue(album);
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.user.get.mockResolvedValue({
|
||||
...userStub.user1,
|
||||
metadata: [
|
||||
@ -394,8 +395,10 @@ describe(NotificationService.name, () => {
|
||||
|
||||
it('should send invite email with album thumbnail and arbitrary extension', async () => {
|
||||
const asset = AssetFactory.from().file({ type: AssetFileType.Thumbnail }).build();
|
||||
const album = AlbumFactory.from({ albumThumbnailAssetId: asset.id }).asset(asset).build();
|
||||
mocks.album.getById.mockResolvedValue(album);
|
||||
const album = AlbumFactory.from({ albumThumbnailAssetId: asset.id })
|
||||
.asset(asset, (builder) => builder.exif())
|
||||
.build();
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.user.get.mockResolvedValue({
|
||||
...userStub.user1,
|
||||
metadata: [
|
||||
@ -432,7 +435,7 @@ describe(NotificationService.name, () => {
|
||||
});
|
||||
|
||||
it('should skip if owner could not be found', async () => {
|
||||
mocks.album.getById.mockResolvedValue(AlbumFactory.create({ ownerId: 'non-existent' }));
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(AlbumFactory.create({ ownerId: 'non-existent' })));
|
||||
|
||||
await expect(sut.handleAlbumUpdate({ id: '', recipientId: '1' })).resolves.toBe(JobStatus.Skipped);
|
||||
expect(mocks.systemMetadata.get).not.toHaveBeenCalled();
|
||||
@ -440,7 +443,7 @@ describe(NotificationService.name, () => {
|
||||
|
||||
it('should skip recipient that could not be looked up', async () => {
|
||||
const album = AlbumFactory.from().albumUser({ userId: 'non-existent' }).build();
|
||||
mocks.album.getById.mockResolvedValue(album);
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.user.get.mockResolvedValueOnce(album.owner);
|
||||
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
|
||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
@ -459,7 +462,7 @@ describe(NotificationService.name, () => {
|
||||
})
|
||||
.build();
|
||||
const album = AlbumFactory.from().albumUser({ userId: user.id }).build();
|
||||
mocks.album.getById.mockResolvedValue(album);
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.user.get.mockResolvedValue(user);
|
||||
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
|
||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
@ -478,7 +481,7 @@ describe(NotificationService.name, () => {
|
||||
})
|
||||
.build();
|
||||
const album = AlbumFactory.from().albumUser({ userId: user.id }).build();
|
||||
mocks.album.getById.mockResolvedValue(album);
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.user.get.mockResolvedValue(user);
|
||||
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
|
||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
@ -492,7 +495,7 @@ describe(NotificationService.name, () => {
|
||||
it('should send email', async () => {
|
||||
const user = UserFactory.create();
|
||||
const album = AlbumFactory.from().albumUser({ userId: user.id }).build();
|
||||
mocks.album.getById.mockResolvedValue(album);
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.user.get.mockResolvedValue(user);
|
||||
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
|
||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { PartnerDirection } from 'src/repositories/partner.repository';
|
||||
import { PartnerService } from 'src/services/partner.service';
|
||||
import { UserFactory } from 'test/factories/user.factory';
|
||||
import { getDehydratedUser, getForPartner } from 'test/mappers';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
@ -18,26 +20,38 @@ describe(PartnerService.name, () => {
|
||||
|
||||
describe('search', () => {
|
||||
it("should return a list of partners with whom I've shared my library", async () => {
|
||||
const user1 = factory.user();
|
||||
const user2 = factory.user();
|
||||
const sharedWithUser2 = factory.partner({ sharedBy: user1, sharedWith: user2 });
|
||||
const sharedWithUser1 = factory.partner({ sharedBy: user2, sharedWith: user1 });
|
||||
const user1 = UserFactory.create();
|
||||
const user2 = UserFactory.create();
|
||||
const sharedWithUser2 = factory.partner({
|
||||
sharedBy: getDehydratedUser(user1),
|
||||
sharedWith: getDehydratedUser(user2),
|
||||
});
|
||||
const sharedWithUser1 = factory.partner({
|
||||
sharedBy: getDehydratedUser(user2),
|
||||
sharedWith: getDehydratedUser(user1),
|
||||
});
|
||||
const auth = factory.auth({ user: { id: user1.id } });
|
||||
|
||||
mocks.partner.getAll.mockResolvedValue([sharedWithUser1, sharedWithUser2]);
|
||||
mocks.partner.getAll.mockResolvedValue([getForPartner(sharedWithUser1), getForPartner(sharedWithUser2)]);
|
||||
|
||||
await expect(sut.search(auth, { direction: PartnerDirection.SharedBy })).resolves.toBeDefined();
|
||||
expect(mocks.partner.getAll).toHaveBeenCalledWith(user1.id);
|
||||
});
|
||||
|
||||
it('should return a list of partners who have shared their libraries with me', async () => {
|
||||
const user1 = factory.user();
|
||||
const user2 = factory.user();
|
||||
const sharedWithUser2 = factory.partner({ sharedBy: user1, sharedWith: user2 });
|
||||
const sharedWithUser1 = factory.partner({ sharedBy: user2, sharedWith: user1 });
|
||||
const user1 = UserFactory.create();
|
||||
const user2 = UserFactory.create();
|
||||
const sharedWithUser2 = factory.partner({
|
||||
sharedBy: getDehydratedUser(user1),
|
||||
sharedWith: getDehydratedUser(user2),
|
||||
});
|
||||
const sharedWithUser1 = factory.partner({
|
||||
sharedBy: getDehydratedUser(user2),
|
||||
sharedWith: getDehydratedUser(user1),
|
||||
});
|
||||
const auth = factory.auth({ user: { id: user1.id } });
|
||||
|
||||
mocks.partner.getAll.mockResolvedValue([sharedWithUser1, sharedWithUser2]);
|
||||
mocks.partner.getAll.mockResolvedValue([getForPartner(sharedWithUser1), getForPartner(sharedWithUser2)]);
|
||||
await expect(sut.search(auth, { direction: PartnerDirection.SharedWith })).resolves.toBeDefined();
|
||||
expect(mocks.partner.getAll).toHaveBeenCalledWith(user1.id);
|
||||
});
|
||||
@ -45,13 +59,13 @@ describe(PartnerService.name, () => {
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a new partner', async () => {
|
||||
const user1 = factory.user();
|
||||
const user2 = factory.user();
|
||||
const partner = factory.partner({ sharedBy: user1, sharedWith: user2 });
|
||||
const user1 = UserFactory.create();
|
||||
const user2 = UserFactory.create();
|
||||
const partner = factory.partner({ sharedBy: getDehydratedUser(user1), sharedWith: getDehydratedUser(user2) });
|
||||
const auth = factory.auth({ user: { id: user1.id } });
|
||||
|
||||
mocks.partner.get.mockResolvedValue(void 0);
|
||||
mocks.partner.create.mockResolvedValue(partner);
|
||||
mocks.partner.create.mockResolvedValue(getForPartner(partner));
|
||||
|
||||
await expect(sut.create(auth, { sharedWithId: user2.id })).resolves.toBeDefined();
|
||||
|
||||
@ -62,12 +76,12 @@ describe(PartnerService.name, () => {
|
||||
});
|
||||
|
||||
it('should throw an error when the partner already exists', async () => {
|
||||
const user1 = factory.user();
|
||||
const user2 = factory.user();
|
||||
const partner = factory.partner({ sharedBy: user1, sharedWith: user2 });
|
||||
const user1 = UserFactory.create();
|
||||
const user2 = UserFactory.create();
|
||||
const partner = factory.partner({ sharedBy: getDehydratedUser(user1), sharedWith: getDehydratedUser(user2) });
|
||||
const auth = factory.auth({ user: { id: user1.id } });
|
||||
|
||||
mocks.partner.get.mockResolvedValue(partner);
|
||||
mocks.partner.get.mockResolvedValue(getForPartner(partner));
|
||||
|
||||
await expect(sut.create(auth, { sharedWithId: user2.id })).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
@ -77,12 +91,12 @@ describe(PartnerService.name, () => {
|
||||
|
||||
describe('remove', () => {
|
||||
it('should remove a partner', async () => {
|
||||
const user1 = factory.user();
|
||||
const user2 = factory.user();
|
||||
const partner = factory.partner({ sharedBy: user1, sharedWith: user2 });
|
||||
const user1 = UserFactory.create();
|
||||
const user2 = UserFactory.create();
|
||||
const partner = factory.partner({ sharedBy: getDehydratedUser(user1), sharedWith: getDehydratedUser(user2) });
|
||||
const auth = factory.auth({ user: { id: user1.id } });
|
||||
|
||||
mocks.partner.get.mockResolvedValue(partner);
|
||||
mocks.partner.get.mockResolvedValue(getForPartner(partner));
|
||||
|
||||
await sut.remove(auth, user2.id);
|
||||
|
||||
@ -110,13 +124,13 @@ describe(PartnerService.name, () => {
|
||||
});
|
||||
|
||||
it('should update partner', async () => {
|
||||
const user1 = factory.user();
|
||||
const user2 = factory.user();
|
||||
const partner = factory.partner({ sharedBy: user1, sharedWith: user2 });
|
||||
const user1 = UserFactory.create();
|
||||
const user2 = UserFactory.create();
|
||||
const partner = factory.partner({ sharedBy: getDehydratedUser(user1), sharedWith: getDehydratedUser(user2) });
|
||||
const auth = factory.auth({ user: { id: user1.id } });
|
||||
|
||||
mocks.access.partner.checkUpdateAccess.mockResolvedValue(new Set([user2.id]));
|
||||
mocks.partner.update.mockResolvedValue(partner);
|
||||
mocks.partner.update.mockResolvedValue(getForPartner(partner));
|
||||
|
||||
await expect(sut.update(auth, user2.id, { inTimeline: true })).resolves.toBeDefined();
|
||||
expect(mocks.partner.update).toHaveBeenCalledWith(
|
||||
|
||||
@ -49,9 +49,11 @@ export class PartnerService extends BaseService {
|
||||
|
||||
private mapPartner(partner: Partner, direction: PartnerDirection): PartnerResponseDto {
|
||||
// this is opposite to return the non-me user of the "partner"
|
||||
const user = mapUser(
|
||||
direction === PartnerDirection.SharedBy ? partner.sharedWith : partner.sharedBy,
|
||||
) as PartnerResponseDto;
|
||||
const sharedUser = direction === PartnerDirection.SharedBy ? partner.sharedWith : partner.sharedBy;
|
||||
const user = mapUser({
|
||||
...sharedUser,
|
||||
profileChangedAt: new Date(sharedUser.profileChangedAt),
|
||||
});
|
||||
|
||||
return { ...user, inTimeline: partner.inTimeline };
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ import { PersonFactory } from 'test/factories/person.factory';
|
||||
import { UserFactory } from 'test/factories/user.factory';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
||||
import { getAsDetectedFace, getForFacialRecognitionJob } from 'test/mappers';
|
||||
import { getAsDetectedFace, getForAssetFace, getForDetectedFaces, getForFacialRecognitionJob } from 'test/mappers';
|
||||
import { newDate, newUuid } from 'test/small.factory';
|
||||
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
@ -319,7 +319,7 @@ describe(PersonService.name, () => {
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id]));
|
||||
mocks.person.getById.mockResolvedValue(person);
|
||||
mocks.access.person.checkFaceOwnerAccess.mockResolvedValue(new Set([face.id]));
|
||||
mocks.person.getFacesByIds.mockResolvedValue([face]);
|
||||
mocks.person.getFacesByIds.mockResolvedValue([getForAssetFace(face)]);
|
||||
mocks.person.reassignFace.mockResolvedValue(1);
|
||||
mocks.person.getRandomFace.mockResolvedValue(AssetFaceFactory.create());
|
||||
mocks.person.refreshFaces.mockResolvedValue();
|
||||
@ -353,15 +353,17 @@ describe(PersonService.name, () => {
|
||||
const face = AssetFaceFactory.create();
|
||||
const asset = AssetFactory.from({ id: face.assetId }).exif().build();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.person.getFaces.mockResolvedValue([face]);
|
||||
mocks.person.getFaces.mockResolvedValue([getForAssetFace(face)]);
|
||||
mocks.asset.getForFaces.mockResolvedValue({ edits: [], ...asset.exifInfo });
|
||||
await expect(sut.getFacesById(auth, { id: face.assetId })).resolves.toStrictEqual([mapFaces(face, auth)]);
|
||||
await expect(sut.getFacesById(auth, { id: face.assetId })).resolves.toStrictEqual([
|
||||
mapFaces(getForAssetFace(face), auth),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should reject if the user has not access to the asset', async () => {
|
||||
const face = AssetFaceFactory.create();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set());
|
||||
mocks.person.getFaces.mockResolvedValue([face]);
|
||||
mocks.person.getFaces.mockResolvedValue([getForAssetFace(face)]);
|
||||
await expect(sut.getFacesById(AuthFactory.create(), { id: face.assetId })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
@ -390,7 +392,7 @@ describe(PersonService.name, () => {
|
||||
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id]));
|
||||
mocks.access.person.checkFaceOwnerAccess.mockResolvedValue(new Set([face.id]));
|
||||
mocks.person.getFaceById.mockResolvedValue(face);
|
||||
mocks.person.getFaceById.mockResolvedValue(getForAssetFace(face));
|
||||
mocks.person.reassignFace.mockResolvedValue(1);
|
||||
mocks.person.getById.mockResolvedValue(person);
|
||||
await expect(sut.reassignFacesById(AuthFactory.create(), person.id, { id: face.id })).resolves.toEqual({
|
||||
@ -412,7 +414,7 @@ describe(PersonService.name, () => {
|
||||
const person = PersonFactory.create();
|
||||
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id]));
|
||||
mocks.person.getFaceById.mockResolvedValue(face);
|
||||
mocks.person.getFaceById.mockResolvedValue(getForAssetFace(face));
|
||||
mocks.person.reassignFace.mockResolvedValue(1);
|
||||
mocks.person.getById.mockResolvedValue(person);
|
||||
await expect(
|
||||
@ -735,18 +737,18 @@ describe(PersonService.name, () => {
|
||||
});
|
||||
|
||||
it('should skip when no resize path', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset);
|
||||
const asset = AssetFactory.from().exif().build();
|
||||
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(getForDetectedFaces(asset));
|
||||
await sut.handleDetectFaces({ id: asset.id });
|
||||
expect(mocks.machineLearning.detectFaces).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle no results', async () => {
|
||||
const start = Date.now();
|
||||
const asset = AssetFactory.from().file({ type: AssetFileType.Preview }).build();
|
||||
const asset = AssetFactory.from().file({ type: AssetFileType.Preview }).exif().build();
|
||||
|
||||
mocks.machineLearning.detectFaces.mockResolvedValue({ imageHeight: 500, imageWidth: 400, faces: [] });
|
||||
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(getForDetectedFaces(asset));
|
||||
await sut.handleDetectFaces({ id: asset.id });
|
||||
expect(mocks.machineLearning.detectFaces).toHaveBeenCalledWith(
|
||||
asset.files[0].path,
|
||||
@ -764,12 +766,12 @@ describe(PersonService.name, () => {
|
||||
});
|
||||
|
||||
it('should create a face with no person and queue recognition job', async () => {
|
||||
const asset = AssetFactory.from().file({ type: AssetFileType.Preview }).build();
|
||||
const asset = AssetFactory.from().file({ type: AssetFileType.Preview }).exif().build();
|
||||
const face = AssetFaceFactory.create({ assetId: asset.id });
|
||||
mocks.crypto.randomUUID.mockReturnValue(face.id);
|
||||
mocks.machineLearning.detectFaces.mockResolvedValue(getAsDetectedFace(face));
|
||||
mocks.search.searchFaces.mockResolvedValue([{ ...face, distance: 0.7 }]);
|
||||
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(getForDetectedFaces(asset));
|
||||
mocks.person.refreshFaces.mockResolvedValue();
|
||||
|
||||
await sut.handleDetectFaces({ id: asset.id });
|
||||
@ -788,9 +790,9 @@ describe(PersonService.name, () => {
|
||||
});
|
||||
|
||||
it('should delete an existing face not among the new detected faces', async () => {
|
||||
const asset = AssetFactory.from().face().file({ type: AssetFileType.Preview }).build();
|
||||
const asset = AssetFactory.from().face().file({ type: AssetFileType.Preview }).exif().build();
|
||||
mocks.machineLearning.detectFaces.mockResolvedValue({ faces: [], imageHeight: 500, imageWidth: 400 });
|
||||
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(getForDetectedFaces(asset));
|
||||
|
||||
await sut.handleDetectFaces({ id: asset.id });
|
||||
|
||||
@ -809,9 +811,9 @@ describe(PersonService.name, () => {
|
||||
boundingBoxY1: 200,
|
||||
boundingBoxY2: 300,
|
||||
});
|
||||
const asset = AssetFactory.from({ id: assetId }).face().file({ type: AssetFileType.Preview }).build();
|
||||
const asset = AssetFactory.from({ id: assetId }).face().file({ type: AssetFileType.Preview }).exif().build();
|
||||
mocks.machineLearning.detectFaces.mockResolvedValue(getAsDetectedFace(face));
|
||||
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(getForDetectedFaces(asset));
|
||||
mocks.crypto.randomUUID.mockReturnValue(face.id);
|
||||
mocks.person.refreshFaces.mockResolvedValue();
|
||||
|
||||
@ -832,9 +834,9 @@ describe(PersonService.name, () => {
|
||||
|
||||
it('should add embedding to matching metadata face', async () => {
|
||||
const face = AssetFaceFactory.create({ sourceType: SourceType.Exif });
|
||||
const asset = AssetFactory.from().face(face).file({ type: AssetFileType.Preview }).build();
|
||||
const asset = AssetFactory.from().face(face).file({ type: AssetFileType.Preview }).exif().build();
|
||||
mocks.machineLearning.detectFaces.mockResolvedValue(getAsDetectedFace(face));
|
||||
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(getForDetectedFaces(asset));
|
||||
mocks.person.refreshFaces.mockResolvedValue();
|
||||
|
||||
await sut.handleDetectFaces({ id: asset.id });
|
||||
@ -848,9 +850,9 @@ describe(PersonService.name, () => {
|
||||
it('should not add embedding to non-matching metadata face', async () => {
|
||||
const assetId = newUuid();
|
||||
const face = AssetFaceFactory.create({ assetId, sourceType: SourceType.Exif });
|
||||
const asset = AssetFactory.from({ id: assetId }).file({ type: AssetFileType.Preview }).build();
|
||||
const asset = AssetFactory.from({ id: assetId }).file({ type: AssetFileType.Preview }).exif().build();
|
||||
mocks.machineLearning.detectFaces.mockResolvedValue(getAsDetectedFace(face));
|
||||
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(getForDetectedFaces(asset));
|
||||
mocks.crypto.randomUUID.mockReturnValue(face.id);
|
||||
|
||||
await sut.handleDetectFaces({ id: asset.id });
|
||||
@ -1237,7 +1239,7 @@ describe(PersonService.name, () => {
|
||||
const person = PersonFactory.create({ ownerId: user.id });
|
||||
const face = AssetFaceFactory.from().person(person).build();
|
||||
|
||||
expect(mapFaces(face, auth)).toEqual({
|
||||
expect(mapFaces(getForAssetFace(face), auth)).toEqual({
|
||||
boundingBoxX1: 100,
|
||||
boundingBoxX2: 200,
|
||||
boundingBoxY1: 100,
|
||||
@ -1251,11 +1253,13 @@ describe(PersonService.name, () => {
|
||||
});
|
||||
|
||||
it('should not map person if person is null', () => {
|
||||
expect(mapFaces(AssetFaceFactory.create(), AuthFactory.create()).person).toBeNull();
|
||||
expect(mapFaces(getForAssetFace(AssetFaceFactory.create()), AuthFactory.create()).person).toBeNull();
|
||||
});
|
||||
|
||||
it('should not map person if person does not match auth user id', () => {
|
||||
expect(mapFaces(AssetFaceFactory.from().person().build(), AuthFactory.create()).person).toBeNull();
|
||||
expect(
|
||||
mapFaces(getForAssetFace(AssetFaceFactory.from().person().build()), AuthFactory.create()).person,
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -491,7 +491,7 @@ export class PersonService extends BaseService {
|
||||
embedding: face.faceSearch.embedding,
|
||||
maxDistance: machineLearning.facialRecognition.maxDistance,
|
||||
numResults: machineLearning.facialRecognition.minFaces,
|
||||
minBirthDate: face.asset.fileCreatedAt ?? undefined,
|
||||
minBirthDate: new Date(face.asset.fileCreatedAt),
|
||||
});
|
||||
|
||||
// `matches` also includes the face itself
|
||||
@ -519,7 +519,7 @@ export class PersonService extends BaseService {
|
||||
maxDistance: machineLearning.facialRecognition.maxDistance,
|
||||
numResults: 1,
|
||||
hasPerson: true,
|
||||
minBirthDate: face.asset.fileCreatedAt ?? undefined,
|
||||
minBirthDate: new Date(face.asset.fileCreatedAt),
|
||||
});
|
||||
|
||||
if (matchWithPerson.length > 0) {
|
||||
|
||||
@ -5,6 +5,7 @@ import { SearchService } from 'src/services/search.service';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { AuthFactory } from 'test/factories/auth.factory';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { getForAsset } from 'test/mappers';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
import { beforeEach, vitest } from 'vitest';
|
||||
|
||||
@ -74,7 +75,9 @@ describe(SearchService.name, () => {
|
||||
items: [{ value: 'city', data: asset.id }],
|
||||
});
|
||||
mocks.asset.getByIdsWithAllRelationsButStacks.mockResolvedValue([asset as never]);
|
||||
const expectedResponse = [{ fieldName: 'exifInfo.city', items: [{ value: 'city', data: mapAsset(asset) }] }];
|
||||
const expectedResponse = [
|
||||
{ fieldName: 'exifInfo.city', items: [{ value: 'city', data: mapAsset(getForAsset(asset)) }] },
|
||||
];
|
||||
|
||||
const result = await sut.getExploreData(auth);
|
||||
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
import { BadRequestException, ForbiddenException, UnauthorizedException } from '@nestjs/common';
|
||||
import { AssetIdErrorReason } from 'src/dtos/asset-ids.response.dto';
|
||||
import { mapSharedLink } from 'src/dtos/shared-link.dto';
|
||||
import { SharedLinkType } from 'src/enum';
|
||||
import { SharedLinkService } from 'src/services/shared-link.service';
|
||||
import { AlbumFactory } from 'test/factories/album.factory';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { SharedLinkFactory } from 'test/factories/shared-link.factory';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { sharedLinkResponseStub, sharedLinkStub } from 'test/fixtures/shared-link.stub';
|
||||
import { sharedLinkStub } from 'test/fixtures/shared-link.stub';
|
||||
import { getForSharedLink } from 'test/mappers';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
@ -24,11 +26,13 @@ describe(SharedLinkService.name, () => {
|
||||
|
||||
describe('getAll', () => {
|
||||
it('should return all shared links for a user', async () => {
|
||||
mocks.sharedLink.getAll.mockResolvedValue([sharedLinkStub.expired, sharedLinkStub.valid]);
|
||||
await expect(sut.getAll(authStub.user1, {})).resolves.toEqual([
|
||||
sharedLinkResponseStub.expired,
|
||||
sharedLinkResponseStub.valid,
|
||||
]);
|
||||
const [sharedLink1, sharedLink2] = [SharedLinkFactory.create(), SharedLinkFactory.create()];
|
||||
mocks.sharedLink.getAll.mockResolvedValue([getForSharedLink(sharedLink1), getForSharedLink(sharedLink2)]);
|
||||
await expect(sut.getAll(authStub.user1, {})).resolves.toEqual(
|
||||
[getForSharedLink(sharedLink1), getForSharedLink(sharedLink2)].map((link) =>
|
||||
mapSharedLink(link, { stripAssetMetadata: false }),
|
||||
),
|
||||
);
|
||||
expect(mocks.sharedLink.getAll).toHaveBeenCalledWith({ userId: authStub.user1.user.id });
|
||||
});
|
||||
});
|
||||
@ -41,8 +45,11 @@ describe(SharedLinkService.name, () => {
|
||||
|
||||
it('should return the shared link for the public user', async () => {
|
||||
const authDto = authStub.adminSharedLink;
|
||||
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.valid);
|
||||
await expect(sut.getMine(authDto, [])).resolves.toEqual(sharedLinkResponseStub.valid);
|
||||
const sharedLink = SharedLinkFactory.create();
|
||||
mocks.sharedLink.get.mockResolvedValue(getForSharedLink(sharedLink));
|
||||
await expect(sut.getMine(authDto, [])).resolves.toEqual(
|
||||
mapSharedLink(getForSharedLink(sharedLink), { stripAssetMetadata: false }),
|
||||
);
|
||||
expect(mocks.sharedLink.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id);
|
||||
});
|
||||
|
||||
@ -54,7 +61,13 @@ describe(SharedLinkService.name, () => {
|
||||
allowUpload: true,
|
||||
},
|
||||
});
|
||||
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.readonlyNoExif);
|
||||
mocks.sharedLink.get.mockResolvedValue(
|
||||
getForSharedLink(
|
||||
SharedLinkFactory.from({ showExif: false })
|
||||
.asset({}, (builder) => builder.exif())
|
||||
.build(),
|
||||
),
|
||||
);
|
||||
const response = await sut.getMine(authDto, []);
|
||||
expect(response.assets[0]).toMatchObject({ hasMetadata: false });
|
||||
expect(mocks.sharedLink.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id);
|
||||
@ -68,7 +81,8 @@ describe(SharedLinkService.name, () => {
|
||||
});
|
||||
|
||||
it('should accept a valid shared link auth token', async () => {
|
||||
mocks.sharedLink.get.mockResolvedValue({ ...sharedLinkStub.individual, password: '123' });
|
||||
const sharedLink = SharedLinkFactory.create({ password: '123' });
|
||||
mocks.sharedLink.get.mockResolvedValue(getForSharedLink(sharedLink));
|
||||
const secret = Buffer.from('auth-token-123');
|
||||
mocks.crypto.hashSha256.mockReturnValue(secret);
|
||||
await expect(sut.getMine(authStub.adminSharedLink, [secret.toString('base64')])).resolves.toBeDefined();
|
||||
@ -90,9 +104,12 @@ describe(SharedLinkService.name, () => {
|
||||
});
|
||||
|
||||
it('should get a shared link by id', async () => {
|
||||
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.valid);
|
||||
await expect(sut.get(authStub.user1, sharedLinkStub.valid.id)).resolves.toEqual(sharedLinkResponseStub.valid);
|
||||
expect(mocks.sharedLink.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id);
|
||||
const sharedLink = SharedLinkFactory.create();
|
||||
mocks.sharedLink.get.mockResolvedValue(getForSharedLink(sharedLink));
|
||||
await expect(sut.get(authStub.user1, sharedLink.id)).resolves.toEqual(
|
||||
mapSharedLink(getForSharedLink(sharedLink), { stripAssetMetadata: true }),
|
||||
);
|
||||
expect(mocks.sharedLink.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLink.id);
|
||||
});
|
||||
});
|
||||
|
||||
@ -123,8 +140,9 @@ describe(SharedLinkService.name, () => {
|
||||
|
||||
it('should create an album shared link', async () => {
|
||||
const album = AlbumFactory.from().asset().build();
|
||||
const sharedLink = SharedLinkFactory.from().album(album).build();
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.sharedLink.create.mockResolvedValue(sharedLinkStub.valid);
|
||||
mocks.sharedLink.create.mockResolvedValue(getForSharedLink(sharedLink));
|
||||
|
||||
await sut.create(authStub.admin, { type: SharedLinkType.Album, albumId: album.id });
|
||||
|
||||
@ -145,8 +163,11 @@ describe(SharedLinkService.name, () => {
|
||||
|
||||
it('should create an individual shared link', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
const sharedLink = SharedLinkFactory.from()
|
||||
.asset(asset, (builder) => builder.exif())
|
||||
.build();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.sharedLink.create.mockResolvedValue(sharedLinkStub.individual);
|
||||
mocks.sharedLink.create.mockResolvedValue(getForSharedLink(sharedLink));
|
||||
|
||||
await sut.create(authStub.admin, {
|
||||
type: SharedLinkType.Individual,
|
||||
@ -178,8 +199,11 @@ describe(SharedLinkService.name, () => {
|
||||
|
||||
it('should create a shared link with allowDownload set to false when showMetadata is false', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
const sharedLink = SharedLinkFactory.from({ allowDownload: false })
|
||||
.asset(asset, (builder) => builder.exif())
|
||||
.build();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.sharedLink.create.mockResolvedValue(sharedLinkStub.individual);
|
||||
mocks.sharedLink.create.mockResolvedValue(getForSharedLink(sharedLink));
|
||||
|
||||
await sut.create(authStub.admin, {
|
||||
type: SharedLinkType.Individual,
|
||||
@ -221,8 +245,9 @@ describe(SharedLinkService.name, () => {
|
||||
});
|
||||
|
||||
it('should update a shared link', async () => {
|
||||
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.valid);
|
||||
mocks.sharedLink.update.mockResolvedValue(sharedLinkStub.valid);
|
||||
const sharedLink = SharedLinkFactory.create();
|
||||
mocks.sharedLink.get.mockResolvedValue(getForSharedLink(sharedLink));
|
||||
mocks.sharedLink.update.mockResolvedValue(getForSharedLink(sharedLink));
|
||||
|
||||
await sut.update(authStub.user1, sharedLinkStub.valid.id, { allowDownload: false });
|
||||
|
||||
@ -247,19 +272,21 @@ describe(SharedLinkService.name, () => {
|
||||
});
|
||||
|
||||
it('should remove a key', async () => {
|
||||
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.valid);
|
||||
const sharedLink = SharedLinkFactory.create();
|
||||
mocks.sharedLink.get.mockResolvedValue(getForSharedLink(sharedLink));
|
||||
mocks.sharedLink.remove.mockResolvedValue();
|
||||
|
||||
await sut.remove(authStub.user1, sharedLinkStub.valid.id);
|
||||
await sut.remove(authStub.user1, sharedLink.id);
|
||||
|
||||
expect(mocks.sharedLink.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id);
|
||||
expect(mocks.sharedLink.remove).toHaveBeenCalledWith(sharedLinkStub.valid.id);
|
||||
expect(mocks.sharedLink.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLink.id);
|
||||
expect(mocks.sharedLink.remove).toHaveBeenCalledWith(sharedLink.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addAssets', () => {
|
||||
it('should not work on album shared links', async () => {
|
||||
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.valid);
|
||||
const sharedLink = SharedLinkFactory.from().album().build();
|
||||
mocks.sharedLink.get.mockResolvedValue(getForSharedLink(sharedLink));
|
||||
|
||||
await expect(sut.addAssets(authStub.admin, 'link-1', { assetIds: ['asset-1'] })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
@ -268,11 +295,13 @@ describe(SharedLinkService.name, () => {
|
||||
|
||||
it('should add assets to a shared link', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
const sharedLink = SharedLinkFactory.from().asset(asset).build();
|
||||
const sharedLink = SharedLinkFactory.from()
|
||||
.asset(asset, (builder) => builder.exif())
|
||||
.build();
|
||||
const newAsset = AssetFactory.create();
|
||||
mocks.sharedLink.get.mockResolvedValue(sharedLink);
|
||||
mocks.sharedLink.create.mockResolvedValue(sharedLink);
|
||||
mocks.sharedLink.update.mockResolvedValue(sharedLink);
|
||||
mocks.sharedLink.get.mockResolvedValue(getForSharedLink(sharedLink));
|
||||
mocks.sharedLink.create.mockResolvedValue(getForSharedLink(sharedLink));
|
||||
mocks.sharedLink.update.mockResolvedValue(getForSharedLink(sharedLink));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([newAsset.id]));
|
||||
|
||||
await expect(
|
||||
@ -286,7 +315,7 @@ describe(SharedLinkService.name, () => {
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.sharedLink.update).toHaveBeenCalled();
|
||||
expect(mocks.sharedLink.update).toHaveBeenCalledWith({
|
||||
...sharedLink,
|
||||
...getForSharedLink(sharedLink),
|
||||
slug: null,
|
||||
assetIds: [newAsset.id],
|
||||
});
|
||||
@ -295,19 +324,22 @@ describe(SharedLinkService.name, () => {
|
||||
|
||||
describe('removeAssets', () => {
|
||||
it('should not work on album shared links', async () => {
|
||||
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.valid);
|
||||
const sharedLink = SharedLinkFactory.from().album().build();
|
||||
mocks.sharedLink.get.mockResolvedValue(getForSharedLink(sharedLink));
|
||||
|
||||
await expect(sut.removeAssets(authStub.admin, 'link-1', { assetIds: ['asset-1'] })).rejects.toBeInstanceOf(
|
||||
await expect(sut.removeAssets(authStub.admin, sharedLink.id, { assetIds: ['asset-1'] })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should remove assets from a shared link', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
const sharedLink = SharedLinkFactory.from().asset(asset).build();
|
||||
mocks.sharedLink.get.mockResolvedValue(sharedLink);
|
||||
mocks.sharedLink.create.mockResolvedValue(sharedLink);
|
||||
mocks.sharedLink.update.mockResolvedValue(sharedLink);
|
||||
const sharedLink = SharedLinkFactory.from()
|
||||
.asset(asset, (builder) => builder.exif())
|
||||
.build();
|
||||
mocks.sharedLink.get.mockResolvedValue(getForSharedLink(sharedLink));
|
||||
mocks.sharedLink.create.mockResolvedValue(getForSharedLink(sharedLink));
|
||||
mocks.sharedLink.update.mockResolvedValue(getForSharedLink(sharedLink));
|
||||
mocks.sharedLinkAsset.remove.mockResolvedValue([asset.id]);
|
||||
|
||||
await expect(
|
||||
@ -338,11 +370,14 @@ describe(SharedLinkService.name, () => {
|
||||
});
|
||||
|
||||
it('should return metadata tags', async () => {
|
||||
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.individual);
|
||||
const sharedLink = SharedLinkFactory.from({ description: null })
|
||||
.asset({}, (builder) => builder.exif())
|
||||
.build();
|
||||
mocks.sharedLink.get.mockResolvedValue(getForSharedLink(sharedLink));
|
||||
|
||||
await expect(sut.getMetadataTags(authStub.adminSharedLink)).resolves.toEqual({
|
||||
description: '1 shared photos & videos',
|
||||
imageUrl: `https://my.immich.app/api/assets/${sharedLinkStub.individual.assets[0].id}/thumbnail?key=LCtkaJX4R1O_9D-2lq0STzsPryoL1UdAbyb6Sna1xxmQCSuqU2J1ZUsqt6GR-yGm1s0`,
|
||||
imageUrl: `https://my.immich.app/api/assets/${sharedLink.assets[0].id}/thumbnail?key=${sharedLink.key.toString('base64url')}`,
|
||||
title: 'Public Share',
|
||||
});
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { AuthFactory } from 'test/factories/auth.factory';
|
||||
import { StackFactory } from 'test/factories/stack.factory';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { getForStack } from 'test/mappers';
|
||||
import { newUuid } from 'test/small.factory';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
@ -22,9 +23,11 @@ describe(StackService.name, () => {
|
||||
describe('search', () => {
|
||||
it('should search stacks', async () => {
|
||||
const auth = AuthFactory.create();
|
||||
const asset = AssetFactory.create();
|
||||
const stack = StackFactory.from().primaryAsset(asset).build();
|
||||
mocks.stack.search.mockResolvedValue([stack]);
|
||||
const asset = AssetFactory.from().exif().build();
|
||||
const stack = StackFactory.from()
|
||||
.primaryAsset(asset, (builder) => builder.exif())
|
||||
.build();
|
||||
mocks.stack.search.mockResolvedValue([getForStack(stack)]);
|
||||
|
||||
await sut.search(auth, { primaryAssetId: asset.id });
|
||||
expect(mocks.stack.search).toHaveBeenCalledWith({
|
||||
@ -49,11 +52,14 @@ describe(StackService.name, () => {
|
||||
|
||||
it('should create a stack', async () => {
|
||||
const auth = AuthFactory.create();
|
||||
const [primaryAsset, asset] = [AssetFactory.create(), AssetFactory.create()];
|
||||
const stack = StackFactory.from().primaryAsset(primaryAsset).asset(asset).build();
|
||||
const [primaryAsset, asset] = [AssetFactory.from().exif().build(), AssetFactory.from().exif().build()];
|
||||
const stack = StackFactory.from()
|
||||
.primaryAsset(primaryAsset, (builder) => builder.exif())
|
||||
.asset(asset, (builder) => builder.exif())
|
||||
.build();
|
||||
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([primaryAsset.id, asset.id]));
|
||||
mocks.stack.create.mockResolvedValue(stack);
|
||||
mocks.stack.create.mockResolvedValue(getForStack(stack));
|
||||
|
||||
await expect(sut.create(auth, { assetIds: [primaryAsset.id, asset.id] })).resolves.toEqual({
|
||||
id: stack.id,
|
||||
@ -88,11 +94,14 @@ describe(StackService.name, () => {
|
||||
|
||||
it('should get stack', async () => {
|
||||
const auth = AuthFactory.create();
|
||||
const [primaryAsset, asset] = [AssetFactory.create(), AssetFactory.create()];
|
||||
const stack = StackFactory.from().primaryAsset(primaryAsset).asset(asset).build();
|
||||
const [primaryAsset, asset] = [AssetFactory.from().exif().build(), AssetFactory.from().exif().build()];
|
||||
const stack = StackFactory.from()
|
||||
.primaryAsset(primaryAsset, (builder) => builder.exif())
|
||||
.asset(asset, (builder) => builder.exif())
|
||||
.build();
|
||||
|
||||
mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set([stack.id]));
|
||||
mocks.stack.getById.mockResolvedValue(stack);
|
||||
mocks.stack.getById.mockResolvedValue(getForStack(stack));
|
||||
|
||||
await expect(sut.get(auth, stack.id)).resolves.toEqual({
|
||||
id: stack.id,
|
||||
@ -125,10 +134,13 @@ describe(StackService.name, () => {
|
||||
|
||||
it('should fail if the provided primary asset id is not in the stack', async () => {
|
||||
const auth = AuthFactory.create();
|
||||
const stack = StackFactory.from().primaryAsset().asset().build();
|
||||
const stack = StackFactory.from()
|
||||
.primaryAsset({}, (builder) => builder.exif())
|
||||
.asset({}, (builder) => builder.exif())
|
||||
.build();
|
||||
|
||||
mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set([stack.id]));
|
||||
mocks.stack.getById.mockResolvedValue(stack);
|
||||
mocks.stack.getById.mockResolvedValue(getForStack(stack));
|
||||
|
||||
await expect(sut.update(auth, stack.id, { primaryAssetId: 'unknown-asset' })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
@ -141,12 +153,15 @@ describe(StackService.name, () => {
|
||||
|
||||
it('should update stack', async () => {
|
||||
const auth = AuthFactory.create();
|
||||
const [primaryAsset, asset] = [AssetFactory.create(), AssetFactory.create()];
|
||||
const stack = StackFactory.from().primaryAsset(primaryAsset).asset(asset).build();
|
||||
const [primaryAsset, asset] = [AssetFactory.from().exif().build(), AssetFactory.from().exif().build()];
|
||||
const stack = StackFactory.from()
|
||||
.primaryAsset(primaryAsset, (builder) => builder.exif())
|
||||
.asset(asset, (builder) => builder.exif())
|
||||
.build();
|
||||
|
||||
mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set([stack.id]));
|
||||
mocks.stack.getById.mockResolvedValue(stack);
|
||||
mocks.stack.update.mockResolvedValue(stack);
|
||||
mocks.stack.getById.mockResolvedValue(getForStack(stack));
|
||||
mocks.stack.update.mockResolvedValue(getForStack(stack));
|
||||
|
||||
await sut.update(auth, stack.id, { primaryAssetId: asset.id });
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ import { AlbumFactory } from 'test/factories/album.factory';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { UserFactory } from 'test/factories/user.factory';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
import { getForStorageTemplate } from 'test/mappers';
|
||||
import { getForAlbum, getForStorageTemplate } from 'test/mappers';
|
||||
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
const motionAsset = AssetFactory.from({ type: AssetType.Video }).exif().build();
|
||||
@ -170,7 +170,9 @@ describe(StorageTemplateService.name, () => {
|
||||
.exif()
|
||||
.build();
|
||||
|
||||
const album = AlbumFactory.from().asset().build();
|
||||
const album = AlbumFactory.from()
|
||||
.asset({}, (builder) => builder.exif())
|
||||
.build();
|
||||
const config = structuredClone(defaults);
|
||||
config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other/{{MM}}{{/if}}/{{filename}}';
|
||||
sut.onConfigInit({ newConfig: config });
|
||||
@ -182,7 +184,7 @@ describe(StorageTemplateService.name, () => {
|
||||
|
||||
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(stillAsset));
|
||||
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(motionAsset));
|
||||
mocks.album.getByAssetId.mockResolvedValue([album]);
|
||||
mocks.album.getByAssetId.mockResolvedValue([getForAlbum(album)]);
|
||||
|
||||
mocks.move.create.mockResolvedValueOnce({
|
||||
id: '123',
|
||||
@ -211,7 +213,9 @@ describe(StorageTemplateService.name, () => {
|
||||
it('should use handlebar if condition for album', async () => {
|
||||
const user = UserFactory.create();
|
||||
const asset = AssetFactory.from().owner(user).exif().build();
|
||||
const album = AlbumFactory.from().asset().build();
|
||||
const album = AlbumFactory.from()
|
||||
.asset({}, (builder) => builder.exif())
|
||||
.build();
|
||||
const config = structuredClone(defaults);
|
||||
config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other/{{MM}}{{/if}}/{{filename}}';
|
||||
|
||||
@ -219,7 +223,7 @@ describe(StorageTemplateService.name, () => {
|
||||
|
||||
mocks.user.get.mockResolvedValue(user);
|
||||
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(asset));
|
||||
mocks.album.getByAssetId.mockResolvedValueOnce([album]);
|
||||
mocks.album.getByAssetId.mockResolvedValueOnce([getForAlbum(album)]);
|
||||
|
||||
expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.Success);
|
||||
|
||||
@ -259,7 +263,9 @@ describe(StorageTemplateService.name, () => {
|
||||
it('should handle album startDate', async () => {
|
||||
const user = UserFactory.create();
|
||||
const asset = AssetFactory.from().owner(user).exif().build();
|
||||
const album = AlbumFactory.from().asset().build();
|
||||
const album = AlbumFactory.from()
|
||||
.asset({}, (builder) => builder.exif())
|
||||
.build();
|
||||
const config = structuredClone(defaults);
|
||||
config.storageTemplate.template =
|
||||
'{{#if album}}{{album-startDate-y}}/{{album-startDate-MM}} - {{album}}{{else}}{{y}}/{{MM}}/{{/if}}/{{filename}}';
|
||||
@ -268,7 +274,7 @@ describe(StorageTemplateService.name, () => {
|
||||
|
||||
mocks.user.get.mockResolvedValue(user);
|
||||
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(asset));
|
||||
mocks.album.getByAssetId.mockResolvedValueOnce([album]);
|
||||
mocks.album.getByAssetId.mockResolvedValueOnce([getForAlbum(album)]);
|
||||
mocks.album.getMetadataForIds.mockResolvedValueOnce([
|
||||
{
|
||||
startDate: asset.fileCreatedAt,
|
||||
@ -764,7 +770,9 @@ describe(StorageTemplateService.name, () => {
|
||||
})
|
||||
.exif()
|
||||
.build();
|
||||
const album = AlbumFactory.from().asset().build();
|
||||
const album = AlbumFactory.from()
|
||||
.asset({}, (builder) => builder.exif())
|
||||
.build();
|
||||
const config = structuredClone(defaults);
|
||||
config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other/{{MM}}{{/if}}/{{filename}}';
|
||||
sut.onConfigInit({ newConfig: config });
|
||||
@ -775,7 +783,7 @@ describe(StorageTemplateService.name, () => {
|
||||
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(stillAsset)]));
|
||||
mocks.user.getList.mockResolvedValue([userStub.user1]);
|
||||
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(motionAsset));
|
||||
mocks.album.getByAssetId.mockResolvedValue([album]);
|
||||
mocks.album.getByAssetId.mockResolvedValue([getForAlbum(album)]);
|
||||
|
||||
mocks.move.create.mockResolvedValueOnce({
|
||||
id: '123',
|
||||
@ -803,7 +811,9 @@ describe(StorageTemplateService.name, () => {
|
||||
|
||||
it('should use still photo album info when migrating live photo motion video', async () => {
|
||||
const user = userStub.user1;
|
||||
const album = AlbumFactory.from().asset().build();
|
||||
const album = AlbumFactory.from()
|
||||
.asset({}, (builder) => builder.exif())
|
||||
.build();
|
||||
const config = structuredClone(defaults);
|
||||
config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other{{/if}}/{{filename}}';
|
||||
|
||||
@ -812,7 +822,7 @@ describe(StorageTemplateService.name, () => {
|
||||
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(stillAsset)]));
|
||||
mocks.user.getList.mockResolvedValue([user]);
|
||||
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(motionAsset));
|
||||
mocks.album.getByAssetId.mockResolvedValue([album]);
|
||||
mocks.album.getByAssetId.mockResolvedValue([getForAlbum(album)]);
|
||||
|
||||
mocks.move.create.mockResolvedValueOnce({
|
||||
id: '123',
|
||||
|
||||
@ -2,6 +2,7 @@ import { mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { SyncService } from 'src/services/sync.service';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { getForAsset, getForPartner } from 'test/mappers';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
@ -26,10 +27,10 @@ describe(SyncService.name, () => {
|
||||
AssetFactory.from({ libraryId: 'library-id', isExternal: true }).owner(authStub.user1.user).build(),
|
||||
AssetFactory.from().owner(authStub.user1.user).build(),
|
||||
];
|
||||
mocks.asset.getAllForUserFullSync.mockResolvedValue([asset1, asset2]);
|
||||
mocks.asset.getAllForUserFullSync.mockResolvedValue([getForAsset(asset1), getForAsset(asset2)]);
|
||||
await expect(sut.getFullSync(authStub.user1, { limit: 2, updatedUntil: untilDate })).resolves.toEqual([
|
||||
mapAsset(asset1, mapAssetOpts),
|
||||
mapAsset(asset2, mapAssetOpts),
|
||||
mapAsset(getForAsset(asset1), mapAssetOpts),
|
||||
mapAsset(getForAsset(asset2), mapAssetOpts),
|
||||
]);
|
||||
expect(mocks.asset.getAllForUserFullSync).toHaveBeenCalledWith({
|
||||
ownerId: authStub.user1.user.id,
|
||||
@ -44,7 +45,7 @@ describe(SyncService.name, () => {
|
||||
const partner = factory.partner();
|
||||
const auth = factory.auth({ user: { id: partner.sharedWithId } });
|
||||
|
||||
mocks.partner.getAll.mockResolvedValue([partner]);
|
||||
mocks.partner.getAll.mockResolvedValue([getForPartner(partner)]);
|
||||
|
||||
await expect(
|
||||
sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [auth.user.id] }),
|
||||
@ -66,7 +67,9 @@ describe(SyncService.name, () => {
|
||||
it('should return a response requiring a full sync when there are too many changes', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.partner.getAll.mockResolvedValue([]);
|
||||
mocks.asset.getChangedDeltaSync.mockResolvedValue(Array.from<typeof asset>({ length: 10_000 }).fill(asset));
|
||||
mocks.asset.getChangedDeltaSync.mockResolvedValue(
|
||||
Array.from<ReturnType<typeof getForAsset>>({ length: 10_000 }).fill(getForAsset(asset)),
|
||||
);
|
||||
await expect(
|
||||
sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }),
|
||||
).resolves.toEqual({ needsFullSync: true, upserted: [], deleted: [] });
|
||||
@ -78,13 +81,13 @@ describe(SyncService.name, () => {
|
||||
const asset = AssetFactory.create({ ownerId: authStub.user1.user.id });
|
||||
const deletedAsset = AssetFactory.create({ libraryId: 'library-id', isExternal: true });
|
||||
mocks.partner.getAll.mockResolvedValue([]);
|
||||
mocks.asset.getChangedDeltaSync.mockResolvedValue([asset]);
|
||||
mocks.asset.getChangedDeltaSync.mockResolvedValue([getForAsset(asset)]);
|
||||
mocks.audit.getAfter.mockResolvedValue([deletedAsset.id]);
|
||||
await expect(
|
||||
sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }),
|
||||
).resolves.toEqual({
|
||||
needsFullSync: false,
|
||||
upserted: [mapAsset(asset, mapAssetOpts)],
|
||||
upserted: [mapAsset(getForAsset(asset), mapAssetOpts)],
|
||||
deleted: [deletedAsset.id],
|
||||
});
|
||||
expect(mocks.asset.getChangedDeltaSync).toHaveBeenCalledTimes(1);
|
||||
|
||||
@ -2,6 +2,7 @@ import { mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { ViewService } from 'src/services/view.service';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { getForAsset } from 'test/mappers';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
describe(ViewService.name, () => {
|
||||
@ -37,7 +38,7 @@ describe(ViewService.name, () => {
|
||||
|
||||
const mockAssets = [asset1, asset2];
|
||||
|
||||
const mockAssetReponseDto = mockAssets.map((a) => mapAsset(a, { auth: authStub.admin }));
|
||||
const mockAssetReponseDto = mockAssets.map((asset) => mapAsset(getForAsset(asset), { auth: authStub.admin }));
|
||||
|
||||
mocks.view.getAssetsByOriginalPath.mockResolvedValue(mockAssets as any);
|
||||
|
||||
|
||||
@ -4,23 +4,23 @@ import {
|
||||
DeduplicateJoinsPlugin,
|
||||
Expression,
|
||||
ExpressionBuilder,
|
||||
ExpressionWrapper,
|
||||
Kysely,
|
||||
KyselyConfig,
|
||||
Nullable,
|
||||
NotNull,
|
||||
Selectable,
|
||||
SelectQueryBuilder,
|
||||
Simplify,
|
||||
ShallowDehydrateObject,
|
||||
sql,
|
||||
} from 'kysely';
|
||||
import { PostgresJSDialect } from 'kysely-postgres-js';
|
||||
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { Notice, PostgresError } from 'postgres';
|
||||
import { columns, Exif, lockableProperties, LockableProperty, Person } from 'src/database';
|
||||
import { columns, lockableProperties, LockableProperty, Person } from 'src/database';
|
||||
import { AssetEditActionItem } from 'src/dtos/editing.dto';
|
||||
import { AssetFileType, AssetVisibility, DatabaseExtension } from 'src/enum';
|
||||
import { AssetSearchBuilderOptions } from 'src/repositories/search.repository';
|
||||
import { DB } from 'src/schema';
|
||||
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
|
||||
import { VectorExtension } from 'src/types';
|
||||
|
||||
export const getKyselyConfig = (connection: DatabaseConnectionParams): KyselyConfig => {
|
||||
@ -70,28 +70,6 @@ export const removeUndefinedKeys = <T extends object>(update: T, template: unkno
|
||||
return update;
|
||||
};
|
||||
|
||||
/** Modifies toJson return type to not set all properties as nullable */
|
||||
export function toJson<DB, TB extends keyof DB & string, T extends TB | Expression<unknown>>(
|
||||
eb: ExpressionBuilder<DB, TB>,
|
||||
table: T,
|
||||
) {
|
||||
return eb.fn.toJson<T>(table) as ExpressionWrapper<
|
||||
DB,
|
||||
TB,
|
||||
Simplify<
|
||||
T extends TB
|
||||
? Selectable<DB[T]> extends Nullable<infer N>
|
||||
? N | null
|
||||
: Selectable<DB[T]>
|
||||
: T extends Expression<infer O>
|
||||
? O extends Nullable<infer N>
|
||||
? N | null
|
||||
: O
|
||||
: never
|
||||
>
|
||||
>;
|
||||
}
|
||||
|
||||
export const ASSET_CHECKSUM_CONSTRAINT = 'UQ_assets_owner_checksum';
|
||||
|
||||
export const isAssetChecksumConstraint = (error: unknown) => {
|
||||
@ -106,19 +84,24 @@ export function withDefaultVisibility<O>(qb: SelectQueryBuilder<DB, 'asset', O>)
|
||||
export function withExif<O>(qb: SelectQueryBuilder<DB, 'asset', O>) {
|
||||
return qb
|
||||
.leftJoin('asset_exif', 'asset.id', 'asset_exif.assetId')
|
||||
.select((eb) => eb.fn.toJson(eb.table('asset_exif')).$castTo<Exif | null>().as('exifInfo'));
|
||||
.select((eb) =>
|
||||
jsonObjectFrom(eb.table('asset_exif'))
|
||||
.$castTo<ShallowDehydrateObject<Selectable<AssetExifTable>> | null>()
|
||||
.as('exifInfo'),
|
||||
);
|
||||
}
|
||||
|
||||
export function withExifInner<O>(qb: SelectQueryBuilder<DB, 'asset', O>) {
|
||||
return qb
|
||||
.innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId')
|
||||
.select((eb) => eb.fn.toJson(eb.table('asset_exif')).$castTo<Exif>().as('exifInfo'));
|
||||
.select((eb) => jsonObjectFrom(eb.table('asset_exif')).as('exifInfo'))
|
||||
.$narrowType<{ exifInfo: NotNull }>();
|
||||
}
|
||||
|
||||
export function withSmartSearch<O>(qb: SelectQueryBuilder<DB, 'asset', O>) {
|
||||
return qb
|
||||
.leftJoin('smart_search', 'asset.id', 'smart_search.assetId')
|
||||
.select((eb) => toJson(eb, 'smart_search').as('smartSearch'));
|
||||
.select((eb) => jsonObjectFrom(eb.table('smart_search')).as('smartSearch'));
|
||||
}
|
||||
|
||||
export function withFaces(eb: ExpressionBuilder<DB, 'asset'>, withHidden?: boolean, withDeletedFace?: boolean) {
|
||||
@ -164,7 +147,7 @@ export function withFacesAndPeople(
|
||||
(join) => join.onTrue(),
|
||||
)
|
||||
.selectAll('asset_face')
|
||||
.select((eb) => eb.table('person').$castTo<Person>().as('person'))
|
||||
.select((eb) => eb.table('person').$castTo<ShallowDehydrateObject<Person>>().as('person'))
|
||||
.whereRef('asset_face.assetId', '=', 'asset.id')
|
||||
.$if(!withDeletedFace, (qb) => qb.where('asset_face.deletedAt', 'is', null))
|
||||
.$if(!withHidden, (qb) => qb.where('asset_face.isVisible', 'is', true)),
|
||||
|
||||
45
server/test/factories/memory.factory.ts
Normal file
45
server/test/factories/memory.factory.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { Selectable } from 'kysely';
|
||||
import { MemoryType } from 'src/enum';
|
||||
import { MemoryTable } from 'src/schema/tables/memory.table';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { build } from 'test/factories/builder.factory';
|
||||
import { AssetLike, FactoryBuilder, MemoryLike } from 'test/factories/types';
|
||||
import { newDate, newUuid, newUuidV7 } from 'test/small.factory';
|
||||
|
||||
export class MemoryFactory {
|
||||
#assets: AssetFactory[] = [];
|
||||
|
||||
private constructor(private readonly value: Selectable<MemoryTable>) {}
|
||||
|
||||
static create(dto: MemoryLike = {}) {
|
||||
return MemoryFactory.from(dto).build();
|
||||
}
|
||||
|
||||
static from(dto: MemoryLike = {}) {
|
||||
return new MemoryFactory({
|
||||
id: newUuid(),
|
||||
createdAt: newDate(),
|
||||
updatedAt: newDate(),
|
||||
updateId: newUuidV7(),
|
||||
deletedAt: null,
|
||||
ownerId: newUuid(),
|
||||
type: MemoryType.OnThisDay,
|
||||
data: { year: 2024 },
|
||||
isSaved: false,
|
||||
memoryAt: newDate(),
|
||||
seenAt: null,
|
||||
showAt: newDate(),
|
||||
hideAt: newDate(),
|
||||
...dto,
|
||||
});
|
||||
}
|
||||
|
||||
asset(asset: AssetLike, builder?: FactoryBuilder<AssetFactory>) {
|
||||
this.#assets.push(build(AssetFactory.from(asset), builder));
|
||||
return this;
|
||||
}
|
||||
|
||||
build() {
|
||||
return { ...this.value, assets: this.#assets.map((asset) => asset.build()) };
|
||||
}
|
||||
}
|
||||
@ -51,12 +51,14 @@ export class SharedLinkFactory {
|
||||
|
||||
album(dto: AlbumLike = {}, builder?: FactoryBuilder<AlbumFactory>) {
|
||||
this.#album = build(AlbumFactory.from(dto), builder);
|
||||
this.value.type = SharedLinkType.Album;
|
||||
return this;
|
||||
}
|
||||
|
||||
asset(dto: AssetLike = {}, builder?: FactoryBuilder<AssetFactory>) {
|
||||
const asset = build(AssetFactory.from(dto), builder);
|
||||
this.#assets.push(asset);
|
||||
this.value.type = SharedLinkType.Individual;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
|
||||
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
|
||||
import { AssetFileTable } from 'src/schema/tables/asset-file.table';
|
||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||
import { MemoryTable } from 'src/schema/tables/memory.table';
|
||||
import { PersonTable } from 'src/schema/tables/person.table';
|
||||
import { SharedLinkTable } from 'src/schema/tables/shared-link.table';
|
||||
import { StackTable } from 'src/schema/tables/stack.table';
|
||||
@ -24,3 +25,4 @@ export type UserLike = Partial<Selectable<UserTable>>;
|
||||
export type AssetFaceLike = Partial<Selectable<AssetFaceTable>>;
|
||||
export type PersonLike = Partial<Selectable<PersonTable>>;
|
||||
export type StackLike = Partial<Selectable<StackTable>>;
|
||||
export type MemoryLike = Partial<Selectable<MemoryTable>>;
|
||||
|
||||
84
server/test/fixtures/shared-link.stub.ts
vendored
84
server/test/fixtures/shared-link.stub.ts
vendored
@ -1,7 +1,6 @@
|
||||
import { UserAdmin } from 'src/database';
|
||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { SharedLinkResponseDto } from 'src/dtos/shared-link.dto';
|
||||
import { AssetStatus, AssetType, AssetVisibility, SharedLinkType } from 'src/enum';
|
||||
import { SharedLinkType } from 'src/enum';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
@ -83,86 +82,7 @@ export const sharedLinkStub = {
|
||||
showExif: false,
|
||||
description: null,
|
||||
password: null,
|
||||
assets: [
|
||||
{
|
||||
id: 'id_1',
|
||||
status: AssetStatus.Active,
|
||||
owner: undefined as unknown as UserAdmin,
|
||||
ownerId: 'user_id_1',
|
||||
deviceAssetId: 'device_asset_id_1',
|
||||
deviceId: 'device_id_1',
|
||||
type: AssetType.Video,
|
||||
originalPath: 'fake_path/jpeg',
|
||||
checksum: Buffer.from('file hash', 'utf8'),
|
||||
fileModifiedAt: today,
|
||||
fileCreatedAt: today,
|
||||
localDateTime: today,
|
||||
createdAt: today,
|
||||
updatedAt: today,
|
||||
isFavorite: false,
|
||||
isArchived: false,
|
||||
isExternal: false,
|
||||
isOffline: false,
|
||||
files: [],
|
||||
thumbhash: null,
|
||||
encodedVideoPath: '',
|
||||
duration: null,
|
||||
livePhotoVideo: null,
|
||||
livePhotoVideoId: null,
|
||||
originalFileName: 'asset_1.jpeg',
|
||||
exifInfo: {
|
||||
projectionType: null,
|
||||
livePhotoCID: null,
|
||||
assetId: 'id_1',
|
||||
description: 'description',
|
||||
exifImageWidth: 500,
|
||||
exifImageHeight: 500,
|
||||
fileSizeInByte: 100,
|
||||
orientation: 'orientation',
|
||||
dateTimeOriginal: today,
|
||||
modifyDate: today,
|
||||
timeZone: 'America/Los_Angeles',
|
||||
latitude: 100,
|
||||
longitude: 100,
|
||||
city: 'city',
|
||||
state: 'state',
|
||||
country: 'country',
|
||||
make: 'camera-make',
|
||||
model: 'camera-model',
|
||||
lensModel: 'fancy',
|
||||
fNumber: 100,
|
||||
focalLength: 100,
|
||||
iso: 100,
|
||||
exposureTime: '1/16',
|
||||
fps: 100,
|
||||
profileDescription: 'sRGB',
|
||||
bitsPerSample: 8,
|
||||
colorspace: 'sRGB',
|
||||
autoStackId: null,
|
||||
rating: 3,
|
||||
updatedAt: today,
|
||||
updateId: '42',
|
||||
libraryId: null,
|
||||
stackId: null,
|
||||
visibility: AssetVisibility.Timeline,
|
||||
width: 500,
|
||||
height: 500,
|
||||
tags: [],
|
||||
},
|
||||
sharedLinks: [],
|
||||
faces: [],
|
||||
sidecarPath: null,
|
||||
deletedAt: null,
|
||||
duplicateId: null,
|
||||
updateId: '42',
|
||||
libraryId: null,
|
||||
stackId: null,
|
||||
visibility: AssetVisibility.Timeline,
|
||||
width: 500,
|
||||
height: 500,
|
||||
isEdited: false,
|
||||
},
|
||||
],
|
||||
assets: [],
|
||||
albumId: null,
|
||||
album: null,
|
||||
slug: null,
|
||||
|
||||
@ -1,7 +1,22 @@
|
||||
import { Selectable } from 'kysely';
|
||||
import { Selectable, ShallowDehydrateObject } from 'kysely';
|
||||
import { AssetEditActionItem } from 'src/dtos/editing.dto';
|
||||
import { ActivityTable } from 'src/schema/tables/activity.table';
|
||||
import { AlbumTable } from 'src/schema/tables/album.table';
|
||||
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
|
||||
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
|
||||
import { AssetFileTable } from 'src/schema/tables/asset-file.table';
|
||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||
import { PartnerTable } from 'src/schema/tables/partner.table';
|
||||
import { PersonTable } from 'src/schema/tables/person.table';
|
||||
import { SharedLinkTable } from 'src/schema/tables/shared-link.table';
|
||||
import { StackTable } from 'src/schema/tables/stack.table';
|
||||
import { AlbumFactory } from 'test/factories/album.factory';
|
||||
import { AssetFaceFactory } from 'test/factories/asset-face.factory';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { MemoryFactory } from 'test/factories/memory.factory';
|
||||
import { SharedLinkFactory } from 'test/factories/shared-link.factory';
|
||||
import { StackFactory } from 'test/factories/stack.factory';
|
||||
import { UserFactory } from 'test/factories/user.factory';
|
||||
|
||||
export const getForStorageTemplate = (asset: ReturnType<AssetFactory['build']>) => {
|
||||
return {
|
||||
@ -47,6 +62,222 @@ export const getForFacialRecognitionJob = (
|
||||
asset: Pick<Selectable<AssetTable>, 'ownerId' | 'visibility' | 'fileCreatedAt'> | null,
|
||||
) => ({
|
||||
...face,
|
||||
asset,
|
||||
asset: asset
|
||||
? { ownerId: asset.ownerId, visibility: asset.visibility, fileCreatedAt: asset.fileCreatedAt.toISOString() }
|
||||
: null,
|
||||
faceSearch: { faceId: face.id, embedding: '[1, 2, 3, 4]' },
|
||||
});
|
||||
|
||||
export const getDehydratedUser = (user: ReturnType<UserFactory['build']>) => ({
|
||||
...user,
|
||||
profileChangedAt: user.profileChangedAt.toISOString(),
|
||||
});
|
||||
|
||||
export const getDehydratedAsset = (asset: Selectable<AssetTable>): ShallowDehydrateObject<Selectable<AssetTable>> => ({
|
||||
...asset,
|
||||
checksum: asset.checksum.toString(),
|
||||
createdAt: asset.createdAt.toISOString(),
|
||||
deletedAt: asset.deletedAt?.toISOString() ?? null,
|
||||
fileCreatedAt: asset.fileCreatedAt.toISOString(),
|
||||
fileModifiedAt: asset.fileModifiedAt.toISOString(),
|
||||
localDateTime: asset.localDateTime.toISOString(),
|
||||
thumbhash: asset.thumbhash?.toString() ?? null,
|
||||
updatedAt: asset.updatedAt.toISOString(),
|
||||
});
|
||||
|
||||
const getDehydratedExif = (exif: Selectable<AssetExifTable>): ShallowDehydrateObject<Selectable<AssetExifTable>> => ({
|
||||
...exif,
|
||||
dateTimeOriginal: exif.dateTimeOriginal?.toISOString() ?? null,
|
||||
modifyDate: exif.modifyDate?.toISOString() ?? null,
|
||||
updatedAt: exif.updatedAt.toISOString(),
|
||||
});
|
||||
|
||||
const getDehydratedSharedLink = (
|
||||
sharedLink: Selectable<SharedLinkTable>,
|
||||
): ShallowDehydrateObject<Selectable<SharedLinkTable>> => ({
|
||||
...sharedLink,
|
||||
createdAt: sharedLink.createdAt.toISOString(),
|
||||
expiresAt: sharedLink.expiresAt?.toISOString() ?? null,
|
||||
key: sharedLink.key.toString(),
|
||||
});
|
||||
|
||||
const getDehydratedPerson = (person: Selectable<PersonTable>): ShallowDehydrateObject<Selectable<PersonTable>> => ({
|
||||
...person,
|
||||
birthDate: person.birthDate?.toISOString() ?? null,
|
||||
createdAt: person.createdAt.toISOString(),
|
||||
updatedAt: person.updatedAt.toISOString(),
|
||||
});
|
||||
|
||||
const getDehydratedFace = (face: Selectable<AssetFaceTable>): ShallowDehydrateObject<Selectable<AssetFaceTable>> => ({
|
||||
...face,
|
||||
deletedAt: face.deletedAt?.toISOString() ?? null,
|
||||
updatedAt: face.updatedAt.toISOString(),
|
||||
});
|
||||
|
||||
const getDehydratedStack = (stack: Selectable<StackTable>): ShallowDehydrateObject<Selectable<StackTable>> => ({
|
||||
...stack,
|
||||
createdAt: stack.createdAt.toISOString(),
|
||||
updatedAt: stack.updatedAt.toISOString(),
|
||||
});
|
||||
|
||||
const getDehydratedFile = (file: Selectable<AssetFileTable>): ShallowDehydrateObject<Selectable<AssetFileTable>> => ({
|
||||
...file,
|
||||
createdAt: file.createdAt.toISOString(),
|
||||
updatedAt: file.updatedAt.toISOString(),
|
||||
});
|
||||
|
||||
const getDehydratedAlbum = (album: Selectable<AlbumTable>): ShallowDehydrateObject<Selectable<AlbumTable>> => ({
|
||||
...album,
|
||||
createdAt: album.createdAt.toISOString(),
|
||||
deletedAt: album.deletedAt ? album.deletedAt.toISOString() : null,
|
||||
updatedAt: album.updatedAt.toISOString(),
|
||||
});
|
||||
|
||||
export const getForAlbum = (album: ReturnType<AlbumFactory['build']>) => ({
|
||||
...album,
|
||||
assets: album.assets.map((asset) => ({ ...getDehydratedAsset(asset), exifInfo: getDehydratedExif(asset.exifInfo) })),
|
||||
albumUsers: album.albumUsers.map((albumUser) => ({
|
||||
...albumUser,
|
||||
createdAt: albumUser.createdAt.toISOString(),
|
||||
user: getDehydratedUser(albumUser.user),
|
||||
})),
|
||||
owner: getDehydratedUser(album.owner),
|
||||
sharedLinks: album.sharedLinks.map((sharedLink) => getDehydratedSharedLink(sharedLink)),
|
||||
});
|
||||
|
||||
export const getForActivity = (activity: Selectable<ActivityTable> & { user: ReturnType<UserFactory['build']> }) => ({
|
||||
...activity,
|
||||
user: getDehydratedUser(activity.user),
|
||||
});
|
||||
|
||||
export const getForAsset = (asset: ReturnType<AssetFactory['build']>) => {
|
||||
return {
|
||||
...asset,
|
||||
faces: asset.faces.map((face) => ({
|
||||
...getDehydratedFace(face),
|
||||
person: face.person ? getDehydratedPerson(face.person) : null,
|
||||
})),
|
||||
owner: getDehydratedUser(asset.owner),
|
||||
stack: asset.stack
|
||||
? { ...getDehydratedStack(asset.stack), assets: asset.stack.assets.map((asset) => getDehydratedAsset(asset)) }
|
||||
: null,
|
||||
files: asset.files.map((file) => getDehydratedFile(file)),
|
||||
exifInfo: asset.exifInfo ? getDehydratedExif(asset.exifInfo) : null,
|
||||
edits: asset.edits.map(({ action, parameters }) => ({ action, parameters })) as AssetEditActionItem[],
|
||||
};
|
||||
};
|
||||
|
||||
export const getForPartner = (
|
||||
partner: Selectable<PartnerTable> & Record<'sharedWith' | 'sharedBy', ReturnType<UserFactory['build']>>,
|
||||
) => ({
|
||||
...partner,
|
||||
sharedBy: getDehydratedUser(partner.sharedBy),
|
||||
sharedWith: getDehydratedUser(partner.sharedWith),
|
||||
});
|
||||
|
||||
export const getForMemory = (memory: ReturnType<MemoryFactory['build']>) => ({
|
||||
...memory,
|
||||
assets: memory.assets.map((asset) => getDehydratedAsset(asset)),
|
||||
});
|
||||
|
||||
export const getForMetadataExtraction = (asset: ReturnType<AssetFactory['build']>) => ({
|
||||
id: asset.id,
|
||||
checksum: asset.checksum,
|
||||
deviceAssetId: asset.deviceAssetId,
|
||||
deviceId: asset.deviceId,
|
||||
fileCreatedAt: asset.fileCreatedAt,
|
||||
fileModifiedAt: asset.fileModifiedAt,
|
||||
isExternal: asset.isExternal,
|
||||
visibility: asset.visibility,
|
||||
libraryId: asset.libraryId,
|
||||
livePhotoVideoId: asset.livePhotoVideoId,
|
||||
localDateTime: asset.localDateTime,
|
||||
originalFileName: asset.originalFileName,
|
||||
originalPath: asset.originalPath,
|
||||
ownerId: asset.ownerId,
|
||||
type: asset.type,
|
||||
width: asset.width,
|
||||
height: asset.height,
|
||||
faces: asset.faces.map((face) => getDehydratedFace(face)),
|
||||
files: asset.files.map((file) => getDehydratedFile(file)),
|
||||
});
|
||||
|
||||
export const getForGenerateThumbnail = (asset: ReturnType<AssetFactory['build']>) => ({
|
||||
id: asset.id,
|
||||
visibility: asset.visibility,
|
||||
originalFileName: asset.originalFileName,
|
||||
originalPath: asset.originalPath,
|
||||
ownerId: asset.ownerId,
|
||||
thumbhash: asset.thumbhash,
|
||||
type: asset.type,
|
||||
files: asset.files.map((file) => getDehydratedFile(file)),
|
||||
exifInfo: getDehydratedExif(asset.exifInfo),
|
||||
edits: asset.edits.map(({ action, parameters }) => ({ action, parameters })) as AssetEditActionItem[],
|
||||
});
|
||||
|
||||
export const getForAssetFace = (face: ReturnType<AssetFaceFactory['build']>) => ({
|
||||
...face,
|
||||
person: face.person ? getDehydratedPerson(face.person) : null,
|
||||
});
|
||||
|
||||
export const getForDetectedFaces = (asset: ReturnType<AssetFactory['build']>) => ({
|
||||
id: asset.id,
|
||||
visibility: asset.visibility,
|
||||
exifInfo: getDehydratedExif(asset.exifInfo),
|
||||
faces: asset.faces.map((face) => getDehydratedFace(face)),
|
||||
files: asset.files.map((file) => getDehydratedFile(file)),
|
||||
});
|
||||
|
||||
export const getForSidecarWrite = (asset: ReturnType<AssetFactory['build']>) => ({
|
||||
id: asset.id,
|
||||
originalPath: asset.originalPath,
|
||||
files: asset.files.map((file) => getDehydratedFile(file)),
|
||||
exifInfo: getDehydratedExif(asset.exifInfo),
|
||||
});
|
||||
|
||||
export const getForAssetDeletion = (asset: ReturnType<AssetFactory['build']>) => ({
|
||||
id: asset.id,
|
||||
visibility: asset.visibility,
|
||||
libraryId: asset.libraryId,
|
||||
ownerId: asset.ownerId,
|
||||
livePhotoVideoId: asset.livePhotoVideoId,
|
||||
encodedVideoPath: asset.encodedVideoPath,
|
||||
originalPath: asset.originalPath,
|
||||
isOffline: asset.isOffline,
|
||||
exifInfo: asset.exifInfo ? getDehydratedExif(asset.exifInfo) : null,
|
||||
files: asset.files.map((file) => getDehydratedFile(file)),
|
||||
stack: asset.stack
|
||||
? {
|
||||
...getDehydratedStack(asset.stack),
|
||||
assets: asset.stack.assets.filter(({ id }) => id !== asset.stack?.primaryAssetId).map(({ id }) => ({ id })),
|
||||
}
|
||||
: null,
|
||||
});
|
||||
|
||||
export const getForStack = (stack: ReturnType<StackFactory['build']>) => ({
|
||||
...stack,
|
||||
assets: stack.assets.map((asset) => ({
|
||||
...getDehydratedAsset(asset),
|
||||
exifInfo: getDehydratedExif(asset.exifInfo),
|
||||
})),
|
||||
});
|
||||
|
||||
export const getForDuplicate = (asset: ReturnType<AssetFactory['build']>) => ({
|
||||
...getDehydratedAsset(asset),
|
||||
exifInfo: getDehydratedExif(asset.exifInfo),
|
||||
});
|
||||
|
||||
export const getForSharedLink = (sharedLink: ReturnType<SharedLinkFactory['build']>) => ({
|
||||
...sharedLink,
|
||||
assets: sharedLink.assets.map((asset) => ({
|
||||
...getDehydratedAsset(asset),
|
||||
exifInfo: getDehydratedExif(asset.exifInfo),
|
||||
})),
|
||||
album: sharedLink.album
|
||||
? {
|
||||
...getDehydratedAlbum(sharedLink.album),
|
||||
owner: getDehydratedUser(sharedLink.album.owner),
|
||||
assets: sharedLink.album.assets.map((asset) => getDehydratedAsset(asset)),
|
||||
}
|
||||
: null,
|
||||
});
|
||||
|
||||
@ -1,19 +1,17 @@
|
||||
import { ShallowDehydrateObject } from 'kysely';
|
||||
import {
|
||||
Activity,
|
||||
Album,
|
||||
ApiKey,
|
||||
AssetFace,
|
||||
AssetFile,
|
||||
AuthApiKey,
|
||||
AuthSharedLink,
|
||||
AuthUser,
|
||||
Exif,
|
||||
Library,
|
||||
Memory,
|
||||
Partner,
|
||||
Person,
|
||||
Session,
|
||||
Stack,
|
||||
Tag,
|
||||
User,
|
||||
UserAdmin,
|
||||
@ -28,13 +26,12 @@ import {
|
||||
AssetStatus,
|
||||
AssetType,
|
||||
AssetVisibility,
|
||||
MemoryType,
|
||||
Permission,
|
||||
SourceType,
|
||||
UserMetadataKey,
|
||||
UserStatus,
|
||||
} from 'src/enum';
|
||||
import { DeepPartial, OnThisDayData, UserMetadataItem } from 'src/types';
|
||||
import { DeepPartial, UserMetadataItem } from 'src/types';
|
||||
import { UserFactory } from 'test/factories/user.factory';
|
||||
import { v4, v7 } from 'uuid';
|
||||
|
||||
export const newUuid = () => v4();
|
||||
@ -123,9 +120,17 @@ const authUserFactory = (authUser: Partial<AuthUser> = {}) => {
|
||||
return { id, isAdmin, name, email, quotaUsageInBytes, quotaSizeInBytes };
|
||||
};
|
||||
|
||||
const partnerFactory = (partner: Partial<Partner> = {}) => {
|
||||
const sharedBy = userFactory(partner.sharedBy || {});
|
||||
const sharedWith = userFactory(partner.sharedWith || {});
|
||||
const partnerFactory = ({
|
||||
sharedBy: sharedByProvided,
|
||||
sharedWith: sharedWithProvided,
|
||||
...partner
|
||||
}: Partial<Partner> = {}) => {
|
||||
const hydrateUser = (user: Partial<ShallowDehydrateObject<User>>) => ({
|
||||
...user,
|
||||
profileChangedAt: user.profileChangedAt ? new Date(user.profileChangedAt) : undefined,
|
||||
});
|
||||
const sharedBy = UserFactory.create(sharedByProvided ? hydrateUser(sharedByProvided) : {});
|
||||
const sharedWith = UserFactory.create(sharedWithProvided ? hydrateUser(sharedWithProvided) : {});
|
||||
|
||||
return {
|
||||
sharedById: sharedBy.id,
|
||||
@ -168,19 +173,6 @@ const queueStatisticsFactory = (dto?: Partial<QueueStatisticsDto>) => ({
|
||||
...dto,
|
||||
});
|
||||
|
||||
const stackFactory = ({ owner, assets, ...stack }: DeepPartial<Stack> = {}): Stack => {
|
||||
const ownerId = newUuid();
|
||||
|
||||
return {
|
||||
id: newUuid(),
|
||||
primaryAssetId: assets?.[0].id ?? newUuid(),
|
||||
ownerId,
|
||||
owner: userFactory(owner ?? { id: ownerId }),
|
||||
assets: assets?.map((asset) => assetFactory(asset)) ?? [],
|
||||
...stack,
|
||||
};
|
||||
};
|
||||
|
||||
const userFactory = (user: Partial<User> = {}) => ({
|
||||
id: newUuid(),
|
||||
name: 'Test User',
|
||||
@ -276,14 +268,14 @@ const assetFactory = (
|
||||
};
|
||||
};
|
||||
|
||||
const activityFactory = (activity: Partial<Activity> = {}) => {
|
||||
const activityFactory = (activity: Partial<Omit<Activity, 'user'>> = {}) => {
|
||||
const userId = activity.userId || newUuid();
|
||||
return {
|
||||
id: newUuid(),
|
||||
comment: null,
|
||||
isLiked: false,
|
||||
userId,
|
||||
user: userFactory({ id: userId }),
|
||||
user: UserFactory.create({ id: userId }),
|
||||
assetId: newUuid(),
|
||||
albumId: newUuid(),
|
||||
createdAt: newDate(),
|
||||
@ -319,24 +311,6 @@ const libraryFactory = (library: Partial<Library> = {}) => ({
|
||||
...library,
|
||||
});
|
||||
|
||||
const memoryFactory = (memory: Partial<Memory> = {}) => ({
|
||||
id: newUuid(),
|
||||
createdAt: newDate(),
|
||||
updatedAt: newDate(),
|
||||
updateId: newUuidV7(),
|
||||
deletedAt: null,
|
||||
ownerId: newUuid(),
|
||||
type: MemoryType.OnThisDay,
|
||||
data: { year: 2024 } as OnThisDayData,
|
||||
isSaved: false,
|
||||
memoryAt: newDate(),
|
||||
seenAt: null,
|
||||
showAt: newDate(),
|
||||
hideAt: newDate(),
|
||||
assets: [],
|
||||
...memory,
|
||||
});
|
||||
|
||||
const versionHistoryFactory = () => ({
|
||||
id: newUuid(),
|
||||
createdAt: newDate(),
|
||||
@ -456,25 +430,6 @@ const tagFactory = (tag: Partial<Tag>): Tag => ({
|
||||
...tag,
|
||||
});
|
||||
|
||||
const faceFactory = ({ person, ...face }: DeepPartial<AssetFace> = {}): AssetFace => ({
|
||||
assetId: newUuid(),
|
||||
boundingBoxX1: 1,
|
||||
boundingBoxX2: 2,
|
||||
boundingBoxY1: 1,
|
||||
boundingBoxY2: 2,
|
||||
deletedAt: null,
|
||||
id: newUuid(),
|
||||
imageHeight: 420,
|
||||
imageWidth: 42,
|
||||
isVisible: true,
|
||||
personId: null,
|
||||
sourceType: SourceType.MachineLearning,
|
||||
updatedAt: newDate(),
|
||||
updateId: newUuidV7(),
|
||||
person: person === null ? null : personFactory(person),
|
||||
...face,
|
||||
});
|
||||
|
||||
const assetEditFactory = (edit?: Partial<AssetEditActionItem>): AssetEditActionItem => {
|
||||
switch (edit?.action) {
|
||||
case AssetEditAction.Crop: {
|
||||
@ -536,11 +491,9 @@ export const factory = {
|
||||
authApiKey: authApiKeyFactory,
|
||||
authUser: authUserFactory,
|
||||
library: libraryFactory,
|
||||
memory: memoryFactory,
|
||||
partner: partnerFactory,
|
||||
queueStatistics: queueStatisticsFactory,
|
||||
session: sessionFactory,
|
||||
stack: stackFactory,
|
||||
user: userFactory,
|
||||
userAdmin: userAdminFactory,
|
||||
versionHistory: versionHistoryFactory,
|
||||
@ -548,7 +501,6 @@ export const factory = {
|
||||
sidecarWrite: assetSidecarWriteFactory,
|
||||
},
|
||||
exif: exifFactory,
|
||||
face: faceFactory,
|
||||
person: personFactory,
|
||||
assetEdit: assetEditFactory,
|
||||
tag: tagFactory,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user