Merge branch 'main' of https://github.com/immich-app/immich into feat/crawl-wrapper

This commit is contained in:
Jonathan Jogenfors
2026-02-13 22:54:58 +01:00
236 changed files with 15905 additions and 7319 deletions
@@ -0,0 +1,47 @@
import { Selectable } from 'kysely';
import { SourceType } from 'src/enum';
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
import { build } from 'test/factories/builder.factory';
import { PersonFactory } from 'test/factories/person.factory';
import { AssetFaceLike, FactoryBuilder, PersonLike } from 'test/factories/types';
import { newDate, newUuid, newUuidV7 } from 'test/small.factory';
export class AssetFaceFactory {
#person: PersonFactory | null = null;
private constructor(private readonly value: Selectable<AssetFaceTable>) {}
static create(dto: AssetFaceLike = {}) {
return AssetFaceFactory.from(dto).build();
}
static from(dto: AssetFaceLike = {}) {
return new AssetFaceFactory({
assetId: newUuid(),
boundingBoxX1: 11,
boundingBoxX2: 12,
boundingBoxY1: 21,
boundingBoxY2: 22,
deletedAt: null,
id: newUuid(),
imageHeight: 42,
imageWidth: 420,
isVisible: true,
personId: null,
sourceType: SourceType.MachineLearning,
updatedAt: newDate(),
updateId: newUuidV7(),
...dto,
});
}
person(dto: PersonLike = {}, builder?: FactoryBuilder<PersonFactory>) {
this.#person = build(PersonFactory.from(dto), builder);
this.value.personId = this.#person.build().id;
return this;
}
build() {
return { ...this.value, person: this.#person?.build() ?? null };
}
}
+30 -4
View File
@@ -1,12 +1,23 @@
import { Selectable } from 'kysely';
import { AssetFileType, AssetStatus, AssetType, AssetVisibility } from 'src/enum';
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
import { AssetTable } from 'src/schema/tables/asset.table';
import { StackTable } from 'src/schema/tables/stack.table';
import { AssetEditFactory } from 'test/factories/asset-edit.factory';
import { AssetExifFactory } from 'test/factories/asset-exif.factory';
import { AssetFaceFactory } from 'test/factories/asset-face.factory';
import { AssetFileFactory } from 'test/factories/asset-file.factory';
import { build } from 'test/factories/builder.factory';
import { AssetEditLike, AssetExifLike, AssetFileLike, AssetLike, FactoryBuilder, UserLike } from 'test/factories/types';
import { StackFactory } from 'test/factories/stack.factory';
import {
AssetEditLike,
AssetExifLike,
AssetFaceLike,
AssetFileLike,
AssetLike,
FactoryBuilder,
StackLike,
UserLike,
} from 'test/factories/types';
import { UserFactory } from 'test/factories/user.factory';
import { newDate, newSha1, newUuid, newUuidV7 } from 'test/small.factory';
@@ -15,6 +26,8 @@ export class AssetFactory {
#assetExif?: AssetExifFactory;
#files: AssetFileFactory[] = [];
#edits: AssetEditFactory[] = [];
#faces: AssetFaceFactory[] = [];
#stack?: Selectable<StackTable> & { assets: Selectable<AssetTable>[]; primaryAsset: Selectable<AssetTable> };
private constructor(private readonly value: Selectable<AssetTable>) {
value.ownerId ??= newUuid();
@@ -28,7 +41,7 @@ export class AssetFactory {
static from(dto: AssetLike = {}) {
const id = dto.id ?? newUuid();
const originalFileName = dto.originalFileName ?? `IMG_${id}.jpg`;
const originalFileName = dto.originalFileName ?? (dto.type === AssetType.Video ? `MOV_${id}.mp4` : `IMG_${id}.jpg`);
return new AssetFactory({
id,
@@ -82,6 +95,11 @@ export class AssetFactory {
return this;
}
face(dto: AssetFaceLike = {}, builder?: FactoryBuilder<AssetFaceFactory>) {
this.#faces.push(build(AssetFaceFactory.from(dto), builder));
return this;
}
file(dto: AssetFileLike = {}, builder?: FactoryBuilder<AssetFileFactory>) {
this.#files.push(build(AssetFileFactory.from(dto).asset(this.value), builder));
return this;
@@ -111,6 +129,12 @@ export class AssetFactory {
return this;
}
stack(dto: StackLike = {}, builder?: FactoryBuilder<StackFactory>) {
this.#stack = build(StackFactory.from(dto).primaryAsset(this.value), builder).build();
this.value.stackId = this.#stack.id;
return this;
}
build() {
const exif = this.#assetExif?.build();
@@ -120,7 +144,9 @@ export class AssetFactory {
exifInfo: exif as NonNullable<typeof exif>,
files: this.#files.map((file) => file.build()),
edits: this.#edits.map((edit) => edit.build()),
faces: [] as Selectable<AssetFaceTable>[],
faces: this.#faces.map((face) => face.build()),
stack: this.#stack ?? null,
tags: [],
};
}
}
+34
View File
@@ -0,0 +1,34 @@
import { Selectable } from 'kysely';
import { PersonTable } from 'src/schema/tables/person.table';
import { PersonLike } from 'test/factories/types';
import { newDate, newUuid, newUuidV7 } from 'test/small.factory';
export class PersonFactory {
private constructor(private readonly value: Selectable<PersonTable>) {}
static create(dto: PersonLike = {}) {
return PersonFactory.from(dto).build();
}
static from(dto: PersonLike = {}) {
return new PersonFactory({
birthDate: null,
color: null,
createdAt: newDate(),
faceAssetId: null,
id: newUuid(),
isFavorite: false,
isHidden: false,
name: 'person',
ownerId: newUuid(),
thumbnailPath: '/data/thumbs/person-thumbnail.jpg',
updatedAt: newDate(),
updateId: newUuidV7(),
...dto,
});
}
build() {
return { ...this.value };
}
}
+11 -3
View File
@@ -2,14 +2,16 @@ import { Selectable } from 'kysely';
import { SharedLinkType } from 'src/enum';
import { SharedLinkTable } from 'src/schema/tables/shared-link.table';
import { AlbumFactory } from 'test/factories/album.factory';
import { AssetFactory } from 'test/factories/asset.factory';
import { build } from 'test/factories/builder.factory';
import { AlbumLike, FactoryBuilder, SharedLinkLike, UserLike } from 'test/factories/types';
import { AlbumLike, AssetLike, FactoryBuilder, SharedLinkLike, UserLike } from 'test/factories/types';
import { UserFactory } from 'test/factories/user.factory';
import { factory, newDate, newUuid } from 'test/small.factory';
export class SharedLinkFactory {
#owner: UserFactory;
#album?: AlbumFactory;
#assets: AssetFactory[] = [];
private constructor(private readonly value: Selectable<SharedLinkTable>) {
value.userId ??= newUuid();
@@ -52,12 +54,18 @@ export class SharedLinkFactory {
return this;
}
asset(dto: AssetLike = {}, builder?: FactoryBuilder<AssetFactory>) {
const asset = build(AssetFactory.from(dto), builder);
this.#assets.push(asset);
return this;
}
build() {
return {
...this.value,
owner: this.#owner.build(),
album: this.#album?.build(),
assets: [],
album: this.#album?.build() ?? null,
assets: this.#assets.map((asset) => asset.build()),
};
}
}
+52
View File
@@ -0,0 +1,52 @@
import { Selectable } from 'kysely';
import { StackTable } from 'src/schema/tables/stack.table';
import { AssetFactory } from 'test/factories/asset.factory';
import { build } from 'test/factories/builder.factory';
import { AssetLike, FactoryBuilder, StackLike } from 'test/factories/types';
import { newDate, newUuid, newUuidV7 } from 'test/small.factory';
export class StackFactory {
#assets: AssetFactory[] = [];
#primaryAsset: AssetFactory;
private constructor(private readonly value: Selectable<StackTable>) {
this.#primaryAsset = AssetFactory.from();
this.value.primaryAssetId = this.#primaryAsset.build().id;
}
static create(dto: StackLike = {}) {
return StackFactory.from(dto).build();
}
static from(dto: StackLike = {}) {
return new StackFactory({
createdAt: newDate(),
id: newUuid(),
ownerId: newUuid(),
primaryAssetId: newUuid(),
updatedAt: newDate(),
updateId: newUuidV7(),
...dto,
});
}
asset(dto: AssetLike = {}, builder?: FactoryBuilder<AssetFactory>) {
this.#assets.push(build(AssetFactory.from(dto), builder));
return this;
}
primaryAsset(dto: AssetLike = {}, builder?: FactoryBuilder<AssetFactory>) {
this.#primaryAsset = build(AssetFactory.from(dto), builder);
this.value.primaryAssetId = this.#primaryAsset.build().id;
this.#assets.push(this.#primaryAsset);
return this;
}
build() {
return {
...this.value,
assets: this.#assets.map((asset) => asset.build()),
primaryAsset: this.#primaryAsset.build(),
};
}
}
+6
View File
@@ -3,9 +3,12 @@ import { AlbumUserTable } from 'src/schema/tables/album-user.table';
import { AlbumTable } from 'src/schema/tables/album.table';
import { AssetEditTable } from 'src/schema/tables/asset-edit.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 { 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 { UserTable } from 'src/schema/tables/user.table';
export type FactoryBuilder<T, R extends T = T> = (builder: T) => R;
@@ -18,3 +21,6 @@ export type AlbumLike = Partial<Selectable<AlbumTable>>;
export type AlbumUserLike = Partial<Selectable<AlbumUserTable>>;
export type SharedLinkLike = Partial<Selectable<SharedLinkTable>>;
export type UserLike = Partial<Selectable<UserTable>>;
export type AssetFaceLike = Partial<Selectable<AssetFaceTable>>;
export type PersonLike = Partial<Selectable<PersonTable>>;
export type StackLike = Partial<Selectable<StackTable>>;
+14 -1
View File
@@ -6,6 +6,8 @@ import { UserLike } from 'test/factories/types';
import { newDate, newUuid, newUuidV7 } from 'test/small.factory';
export class UserFactory {
#metadata: Selectable<UserMetadataTable>[] = [];
private constructor(private value: Selectable<UserTable>) {}
static create(dto: UserLike = {}) {
@@ -37,10 +39,21 @@ export class UserFactory {
});
}
metadata(dto: Partial<Selectable<UserMetadataTable>> & Pick<Selectable<UserMetadataTable>, 'key' | 'value'>) {
this.#metadata.push({
updatedAt: newDate(),
updateId: newUuid(),
userId: newUuid(),
...dto,
});
return this;
}
build() {
return {
...this.value,
metadata: [] as UserMetadataTable[],
metadata: this.#metadata,
};
}
}
-84
View File
@@ -1,84 +0,0 @@
import { AlbumUserRole, AssetOrder } from 'src/enum';
import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub';
import { userStub } from 'test/fixtures/user.stub';
export const albumStub = {
empty: Object.freeze({
id: 'album-1',
albumName: 'Empty album',
description: '',
ownerId: authStub.admin.user.id,
owner: userStub.admin,
assets: [],
albumThumbnailAsset: null,
albumThumbnailAssetId: null,
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: null,
sharedLinks: [],
albumUsers: [],
isActivityEnabled: true,
order: AssetOrder.Desc,
updateId: '42',
}),
sharedWithUser: Object.freeze({
id: 'album-2',
albumName: 'Empty album shared with user',
description: '',
ownerId: authStub.admin.user.id,
owner: userStub.admin,
assets: [],
albumThumbnailAsset: null,
albumThumbnailAssetId: null,
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: null,
sharedLinks: [],
albumUsers: [
{
user: userStub.user1,
role: AlbumUserRole.Editor,
},
],
isActivityEnabled: true,
order: AssetOrder.Desc,
updateId: '42',
}),
oneAsset: Object.freeze({
id: 'album-4',
albumName: 'Album with one asset',
description: '',
ownerId: authStub.admin.user.id,
owner: userStub.admin,
assets: [assetStub.image],
albumThumbnailAsset: null,
albumThumbnailAssetId: null,
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: null,
sharedLinks: [],
albumUsers: [],
isActivityEnabled: true,
order: AssetOrder.Desc,
updateId: '42',
}),
emptyWithValidThumbnail: Object.freeze({
id: 'album-5',
albumName: 'Empty album with valid thumbnail',
description: '',
ownerId: authStub.admin.user.id,
owner: userStub.admin,
assets: [],
albumThumbnailAsset: assetStub.image,
albumThumbnailAssetId: assetStub.image.id,
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: null,
sharedLinks: [],
albumUsers: [],
isActivityEnabled: true,
order: AssetOrder.Desc,
updateId: '42',
}),
};
-723
View File
@@ -1,723 +0,0 @@
import { AssetFace, AssetFile, Exif } from 'src/database';
import { MapAsset } from 'src/dtos/asset-response.dto';
import { AssetEditAction, AssetEditActionItem } from 'src/dtos/editing.dto';
import { AssetFileType, AssetStatus, AssetType, AssetVisibility } from 'src/enum';
import { StorageAsset } from 'src/types';
import { authStub } from 'test/fixtures/auth.stub';
import { fileStub } from 'test/fixtures/file.stub';
import { userStub } from 'test/fixtures/user.stub';
import { factory } from 'test/small.factory';
export const previewFile = factory.assetFile({ type: AssetFileType.Preview });
const thumbnailFile = factory.assetFile({
type: AssetFileType.Thumbnail,
path: '/uploads/user-id/webp/path.ext',
});
const fullsizeFile = factory.assetFile({
type: AssetFileType.FullSize,
path: '/uploads/user-id/fullsize/path.webp',
});
const files = [fullsizeFile, previewFile, thumbnailFile];
export const stackStub = (stackId: string, assets: (MapAsset & { exifInfo: Exif })[]) => {
return {
id: stackId,
assets,
ownerId: assets[0].ownerId,
primaryAsset: assets[0],
primaryAssetId: assets[0].id,
createdAt: new Date('2023-02-23T05:06:29.716Z'),
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
updateId: expect.any(String),
};
};
export const assetStub = {
storageAsset: (asset: Partial<StorageAsset> = {}) => ({
id: 'asset-id',
ownerId: 'user-id',
livePhotoVideoId: null,
type: AssetType.Image,
isExternal: false,
checksum: Buffer.from('file hash'),
timeZone: null,
fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
originalPath: '/original/path.jpg',
originalFileName: 'IMG_123.jpg',
fileSizeInByte: 12_345,
files: [],
make: 'FUJIFILM',
model: 'X-T50',
lensModel: 'XF27mm F2.8 R WR',
isEdited: false,
...asset,
}),
noResizePath: Object.freeze({
id: 'asset-id',
status: AssetStatus.Active,
originalFileName: 'IMG_123.jpg',
deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
owner: userStub.user1,
ownerId: 'user-id',
deviceId: 'device-id',
originalPath: '/data/library/IMG_123.jpg',
files: [thumbnailFile],
checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.Image,
thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'),
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: true,
duration: null,
livePhotoVideo: null,
livePhotoVideoId: null,
sharedLinks: [],
faces: [],
exifInfo: {} as Exif,
deletedAt: null,
isExternal: false,
duplicateId: null,
isOffline: false,
libraryId: null,
stackId: null,
updateId: '42',
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
isEdited: false,
}),
primaryImage: Object.freeze({
id: 'primary-asset-id',
status: AssetStatus.Active,
deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
owner: userStub.admin,
ownerId: 'admin-id',
deviceId: 'device-id',
originalPath: '/original/path.jpg',
checksum: Buffer.from('file hash', 'utf8'),
files,
type: AssetType.Image,
thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'),
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: true,
duration: null,
isExternal: false,
livePhotoVideo: null,
livePhotoVideoId: null,
sharedLinks: [],
originalFileName: 'asset-id.jpg',
faces: [],
deletedAt: null,
exifInfo: {
fileSizeInByte: 5000,
exifImageHeight: 1000,
exifImageWidth: 1000,
} as Exif,
stackId: 'stack-1',
stack: stackStub('stack-1', [
{ id: 'primary-asset-id' } as MapAsset & { exifInfo: Exif },
{ id: 'stack-child-asset-1' } as MapAsset & { exifInfo: Exif },
{ id: 'stack-child-asset-2' } as MapAsset & { exifInfo: Exif },
]),
duplicateId: null,
isOffline: false,
updateId: '42',
libraryId: null,
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
isEdited: false,
}),
image: Object.freeze({
id: 'asset-id',
status: AssetStatus.Active,
deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
owner: userStub.user1,
ownerId: 'user-id',
deviceId: 'device-id',
originalPath: '/original/path.jpg',
files,
checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.Image,
thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'),
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
localDateTime: new Date('2025-01-01T01:02:03.456Z'),
isFavorite: true,
duration: null,
isExternal: false,
livePhotoVideo: null,
livePhotoVideoId: null,
updateId: 'foo',
libraryId: null,
stackId: null,
sharedLinks: [],
originalFileName: 'asset-id.jpg',
faces: [],
deletedAt: null,
exifInfo: {
fileSizeInByte: 5000,
exifImageHeight: 3840,
exifImageWidth: 2160,
} as Exif,
duplicateId: null,
isOffline: false,
stack: null,
orientation: '',
projectionType: null,
height: null,
width: null,
visibility: AssetVisibility.Timeline,
edits: [],
isEdited: false,
}),
trashed: Object.freeze({
id: 'asset-id',
deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
owner: userStub.user1,
ownerId: 'user-id',
deviceId: 'device-id',
originalPath: '/original/path.jpg',
checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.Image,
files,
thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'),
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
deletedAt: new Date('2023-02-24T05:06:29.716Z'),
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: false,
duration: null,
isExternal: false,
livePhotoVideo: null,
livePhotoVideoId: null,
sharedLinks: [],
originalFileName: 'asset-id.jpg',
faces: [],
exifInfo: {
fileSizeInByte: 5000,
exifImageHeight: 3840,
exifImageWidth: 2160,
} as Exif,
duplicateId: null,
isOffline: false,
status: AssetStatus.Trashed,
libraryId: null,
stackId: null,
updateId: '42',
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
isEdited: false,
}),
trashedOffline: Object.freeze({
id: 'asset-id',
status: AssetStatus.Active,
deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
owner: userStub.user1,
ownerId: 'user-id',
deviceId: 'device-id',
originalPath: '/original/path.jpg',
checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.Image,
files,
thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'),
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
deletedAt: new Date('2023-02-24T05:06:29.716Z'),
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: false,
duration: null,
libraryId: 'library-id',
isExternal: false,
livePhotoVideo: null,
livePhotoVideoId: null,
sharedLinks: [],
originalFileName: 'asset-id.jpg',
faces: [],
exifInfo: {
fileSizeInByte: 5000,
exifImageHeight: 3840,
exifImageWidth: 2160,
} as Exif,
duplicateId: null,
isOffline: true,
stackId: null,
updateId: '42',
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
isEdited: false,
}),
archived: Object.freeze({
id: 'asset-id',
status: AssetStatus.Active,
deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
owner: userStub.user1,
ownerId: 'user-id',
deviceId: 'device-id',
originalPath: '/original/path.jpg',
checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.Image,
files,
thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'),
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: true,
duration: null,
isExternal: false,
livePhotoVideo: null,
livePhotoVideoId: null,
sharedLinks: [],
originalFileName: 'asset-id.jpg',
faces: [],
deletedAt: null,
exifInfo: {
fileSizeInByte: 5000,
exifImageHeight: 3840,
exifImageWidth: 2160,
} as Exif,
duplicateId: null,
isOffline: false,
libraryId: null,
stackId: null,
updateId: '42',
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
isEdited: false,
}),
external: Object.freeze({
id: 'asset-id',
status: AssetStatus.Active,
deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
owner: userStub.user1,
ownerId: 'user-id',
deviceId: 'device-id',
originalPath: '/data/user1/photo.jpg',
checksum: Buffer.from('path hash', 'utf8'),
type: AssetType.Image,
files,
thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'),
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: true,
isExternal: true,
duration: null,
livePhotoVideo: null,
livePhotoVideoId: null,
libraryId: 'library-id',
sharedLinks: [],
originalFileName: 'asset-id.jpg',
faces: [],
deletedAt: null,
exifInfo: {
fileSizeInByte: 5000,
} as Exif,
duplicateId: null,
isOffline: false,
updateId: '42',
stackId: null,
stack: null,
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
isEdited: false,
}),
image1: Object.freeze({
id: 'asset-id-1',
status: AssetStatus.Active,
deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
owner: userStub.user1,
ownerId: 'user-id',
deviceId: 'device-id',
originalPath: '/original/path.ext',
checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.Image,
files,
thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'),
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
deletedAt: null,
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: true,
duration: null,
livePhotoVideo: null,
livePhotoVideoId: null,
isExternal: false,
sharedLinks: [],
originalFileName: 'asset-id.ext',
faces: [],
exifInfo: {
fileSizeInByte: 5000,
} as Exif,
duplicateId: null,
isOffline: false,
updateId: '42',
stackId: null,
libraryId: null,
stack: null,
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
isEdited: false,
}),
video: Object.freeze({
id: 'asset-id',
status: AssetStatus.Active,
originalFileName: 'asset-id.ext',
deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
owner: userStub.user1,
ownerId: 'user-id',
deviceId: 'device-id',
originalPath: '/original/path.ext',
checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.Video,
files: [previewFile],
thumbhash: null,
encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'),
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: true,
isExternal: false,
duration: null,
livePhotoVideo: null,
livePhotoVideoId: null,
sharedLinks: [],
faces: [],
exifInfo: {
fileSizeInByte: 100_000,
exifImageHeight: 2160,
exifImageWidth: 3840,
} as Exif,
deletedAt: null,
duplicateId: null,
isOffline: false,
updateId: '42',
libraryId: null,
stackId: null,
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
isEdited: false,
}),
livePhotoMotionAsset: Object.freeze({
status: AssetStatus.Active,
id: fileStub.livePhotoMotion.uuid,
originalPath: fileStub.livePhotoMotion.originalPath,
ownerId: authStub.user1.user.id,
type: AssetType.Video,
fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'),
fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
exifInfo: {
fileSizeInByte: 100_000,
timeZone: `America/New_York`,
},
files: [],
libraryId: null,
visibility: AssetVisibility.Hidden,
width: null,
height: null,
edits: [] as AssetEditActionItem[],
isEdited: false,
} as unknown as MapAsset & {
faces: AssetFace[];
files: (AssetFile & { isProgressive: boolean })[];
exifInfo: Exif;
edits: AssetEditActionItem[];
}),
livePhotoStillAsset: Object.freeze({
id: 'live-photo-still-asset',
status: AssetStatus.Active,
originalPath: fileStub.livePhotoStill.originalPath,
ownerId: authStub.user1.user.id,
type: AssetType.Image,
livePhotoVideoId: 'live-photo-motion-asset',
fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'),
fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
exifInfo: {
fileSizeInByte: 25_000,
timeZone: `America/New_York`,
},
files,
faces: [] as AssetFace[],
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [] as AssetEditActionItem[],
isEdited: false,
} as unknown as MapAsset & {
faces: AssetFace[];
files: (AssetFile & { isProgressive: boolean })[];
edits: AssetEditActionItem[];
}),
livePhotoWithOriginalFileName: Object.freeze({
id: 'live-photo-still-asset',
status: AssetStatus.Active,
originalPath: fileStub.livePhotoStill.originalPath,
originalFileName: fileStub.livePhotoStill.originalName,
ownerId: authStub.user1.user.id,
type: AssetType.Image,
livePhotoVideoId: 'live-photo-motion-asset',
fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'),
fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
exifInfo: {
fileSizeInByte: 25_000,
timeZone: `America/New_York`,
},
files: [] as AssetFile[],
libraryId: null,
faces: [] as AssetFace[],
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [] as AssetEditActionItem[],
isEdited: false,
} as MapAsset & { faces: AssetFace[]; files: AssetFile[]; edits: AssetEditActionItem[] }),
withLocation: Object.freeze({
id: 'asset-with-favorite-id',
status: AssetStatus.Active,
deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-22T05:06:29.716Z'),
fileCreatedAt: new Date('2023-02-22T05:06:29.716Z'),
owner: userStub.user1,
ownerId: 'user-id',
deviceId: 'device-id',
checksum: Buffer.from('file hash', 'utf8'),
originalPath: '/original/path.ext',
type: AssetType.Image,
files: [previewFile],
thumbhash: null,
encodedVideoPath: null,
createdAt: new Date('2023-02-22T05:06:29.716Z'),
updatedAt: new Date('2023-02-22T05:06:29.716Z'),
localDateTime: new Date('2020-12-31T23:59:00.000Z'),
isFavorite: false,
isExternal: false,
duration: null,
livePhotoVideo: null,
livePhotoVideoId: null,
updateId: 'foo',
libraryId: null,
stackId: null,
sharedLinks: [],
originalFileName: 'asset-id.ext',
faces: [],
exifInfo: {
latitude: 100,
longitude: 100,
fileSizeInByte: 23_456,
city: 'test-city',
state: 'test-state',
country: 'test-country',
} as Exif,
deletedAt: null,
duplicateId: null,
isOffline: false,
tags: [],
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
isEdited: false,
}),
hasEncodedVideo: Object.freeze({
id: 'asset-id',
status: AssetStatus.Active,
originalFileName: 'asset-id.ext',
deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
owner: userStub.user1,
ownerId: 'user-id',
deviceId: 'device-id',
originalPath: '/original/path.ext',
checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.Video,
files: [previewFile],
thumbhash: null,
encodedVideoPath: '/encoded/video/path.mp4',
createdAt: new Date('2023-02-23T05:06:29.716Z'),
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: true,
isExternal: false,
duration: null,
livePhotoVideo: null,
livePhotoVideoId: null,
sharedLinks: [],
faces: [],
exifInfo: {
fileSizeInByte: 100_000,
} as Exif,
deletedAt: null,
duplicateId: null,
isOffline: false,
updateId: '42',
libraryId: null,
stackId: null,
stack: null,
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
isEdited: false,
}),
imageDng: Object.freeze({
id: 'asset-id',
status: AssetStatus.Active,
deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
owner: userStub.user1,
ownerId: 'user-id',
deviceId: 'device-id',
originalPath: '/original/path.dng',
checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.Image,
files,
thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'),
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: true,
duration: null,
isExternal: false,
livePhotoVideo: null,
livePhotoVideoId: null,
sharedLinks: [],
originalFileName: 'asset-id.dng',
faces: [],
deletedAt: null,
exifInfo: {
fileSizeInByte: 5000,
profileDescription: 'Adobe RGB',
bitsPerSample: 14,
} as Exif,
duplicateId: null,
isOffline: false,
updateId: '42',
libraryId: null,
stackId: null,
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
isEdited: false,
}),
withCropEdit: Object.freeze({
id: 'asset-id',
status: AssetStatus.Active,
deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
owner: userStub.user1,
ownerId: 'user-id',
deviceId: 'device-id',
originalPath: '/original/path.jpg',
files,
checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.Image,
thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'),
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
localDateTime: new Date('2025-01-01T01:02:03.456Z'),
isFavorite: true,
duration: null,
isExternal: false,
livePhotoVideo: null,
livePhotoVideoId: null,
updateId: 'foo',
libraryId: null,
stackId: null,
sharedLinks: [],
originalFileName: 'asset-id.jpg',
faces: [],
deletedAt: null,
sidecarPath: null,
exifInfo: {
fileSizeInByte: 5000,
exifImageHeight: 3840,
exifImageWidth: 2160,
} as Exif,
duplicateId: null,
isOffline: false,
stack: null,
orientation: '',
projectionType: null,
height: 3840,
width: 2160,
visibility: AssetVisibility.Timeline,
edits: [
{
action: AssetEditAction.Crop,
parameters: {
width: 1512,
height: 1152,
x: 216,
y: 1512,
},
},
] as AssetEditActionItem[],
isEdited: true,
}),
};
+17 -17
View File
@@ -1,13 +1,13 @@
import { SourceType } from 'src/enum';
import { assetStub } from 'test/fixtures/asset.stub';
import { AssetFactory } from 'test/factories/asset.factory';
import { personStub } from 'test/fixtures/person.stub';
export const faceStub = {
face1: Object.freeze({
id: 'assetFaceId1',
assetId: assetStub.image.id,
assetId: 'asset-id',
asset: {
...assetStub.image,
...AssetFactory.create({ id: 'asset-id' }),
libraryId: null,
updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125',
stackId: null,
@@ -29,8 +29,8 @@ export const faceStub = {
}),
primaryFace1: Object.freeze({
id: 'assetFaceId2',
assetId: assetStub.image.id,
asset: assetStub.image,
assetId: 'asset-id',
asset: AssetFactory.create({ id: 'asset-id' }),
personId: personStub.primaryPerson.id,
person: personStub.primaryPerson,
boundingBoxX1: 0,
@@ -48,8 +48,8 @@ export const faceStub = {
}),
mergeFace1: Object.freeze({
id: 'assetFaceId3',
assetId: assetStub.image.id,
asset: assetStub.image,
assetId: 'asset-id',
asset: AssetFactory.create({ id: 'asset-id' }),
personId: personStub.mergePerson.id,
person: personStub.mergePerson,
boundingBoxX1: 0,
@@ -67,8 +67,8 @@ export const faceStub = {
}),
noPerson1: Object.freeze({
id: 'assetFaceId8',
assetId: assetStub.image.id,
asset: assetStub.image,
assetId: 'asset-id',
asset: AssetFactory.create({ id: 'asset-id' }),
personId: null,
person: null,
boundingBoxX1: 0,
@@ -86,8 +86,8 @@ export const faceStub = {
}),
noPerson2: Object.freeze({
id: 'assetFaceId9',
assetId: assetStub.image.id,
asset: assetStub.image,
assetId: 'asset-id',
asset: AssetFactory.create({ id: 'asset-id' }),
personId: null,
person: null,
boundingBoxX1: 0,
@@ -105,8 +105,8 @@ export const faceStub = {
}),
fromExif1: Object.freeze({
id: 'assetFaceId9',
assetId: assetStub.image.id,
asset: assetStub.image,
assetId: 'asset-id',
asset: AssetFactory.create({ id: 'asset-id' }),
personId: personStub.randomPerson.id,
person: personStub.randomPerson,
boundingBoxX1: 100,
@@ -123,8 +123,8 @@ export const faceStub = {
}),
fromExif2: Object.freeze({
id: 'assetFaceId9',
assetId: assetStub.image.id,
asset: assetStub.image,
assetId: 'asset-id',
asset: AssetFactory.create({ id: 'asset-id' }),
personId: personStub.randomPerson.id,
person: personStub.randomPerson,
boundingBoxX1: 0,
@@ -141,8 +141,8 @@ export const faceStub = {
}),
withBirthDate: Object.freeze({
id: 'assetFaceId10',
assetId: assetStub.image.id,
asset: assetStub.image,
assetId: 'asset-id',
asset: AssetFactory.create({ id: 'asset-id' }),
personId: personStub.withBirthDate.id,
person: personStub.withBirthDate,
boundingBoxX1: 0,
+9 -9
View File
@@ -1,5 +1,5 @@
import { AssetType } from 'src/enum';
import { previewFile } from 'test/fixtures/asset.stub';
import { AssetFileType, AssetType } from 'src/enum';
import { AssetFileFactory } from 'test/factories/asset-file.factory';
import { userStub } from 'test/fixtures/user.stub';
const updateId = '0d1173e3-4d80-4d76-b41e-57d56de21125';
@@ -179,7 +179,7 @@ export const personThumbnailStub = {
type: AssetType.Image,
originalPath: '/original/path.jpg',
exifOrientation: '1',
previewPath: previewFile.path,
previewPath: AssetFileFactory.create({ type: AssetFileType.Preview }).path,
}),
newThumbnailMiddle: Object.freeze({
ownerId: userStub.admin.id,
@@ -192,7 +192,7 @@ export const personThumbnailStub = {
type: AssetType.Image,
originalPath: '/original/path.jpg',
exifOrientation: '1',
previewPath: previewFile.path,
previewPath: AssetFileFactory.create({ type: AssetFileType.Preview }).path,
}),
newThumbnailEnd: Object.freeze({
ownerId: userStub.admin.id,
@@ -205,7 +205,7 @@ export const personThumbnailStub = {
type: AssetType.Image,
originalPath: '/original/path.jpg',
exifOrientation: '1',
previewPath: previewFile.path,
previewPath: AssetFileFactory.create({ type: AssetFileType.Preview }).path,
}),
rawEmbeddedThumbnail: Object.freeze({
ownerId: userStub.admin.id,
@@ -218,7 +218,7 @@ export const personThumbnailStub = {
type: AssetType.Image,
originalPath: '/original/path.dng',
exifOrientation: '1',
previewPath: previewFile.path,
previewPath: AssetFileFactory.create({ type: AssetFileType.Preview }).path,
}),
negativeCoordinate: Object.freeze({
ownerId: userStub.admin.id,
@@ -231,7 +231,7 @@ export const personThumbnailStub = {
type: AssetType.Image,
originalPath: '/original/path.jpg',
exifOrientation: '1',
previewPath: previewFile.path,
previewPath: AssetFileFactory.create({ type: AssetFileType.Preview }).path,
}),
overflowingCoordinate: Object.freeze({
ownerId: userStub.admin.id,
@@ -244,7 +244,7 @@ export const personThumbnailStub = {
type: AssetType.Image,
originalPath: '/original/path.jpg',
exifOrientation: '1',
previewPath: previewFile.path,
previewPath: AssetFileFactory.create({ type: AssetFileType.Preview }).path,
}),
videoThumbnail: Object.freeze({
ownerId: userStub.admin.id,
@@ -257,6 +257,6 @@ export const personThumbnailStub = {
type: AssetType.Video,
originalPath: '/original/path.mp4',
exifOrientation: '1',
previewPath: previewFile.path,
previewPath: AssetFileFactory.create({ type: AssetFileType.Preview }).path,
}),
};
+2 -2
View File
@@ -2,7 +2,7 @@ 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 { assetStub } from 'test/fixtures/asset.stub';
import { AssetFactory } from 'test/factories/asset.factory';
import { authStub } from 'test/fixtures/auth.stub';
import { userStub } from 'test/fixtures/user.stub';
@@ -31,7 +31,7 @@ export const sharedLinkStub = {
albumId: null,
album: null,
description: null,
assets: [assetStub.image],
assets: [AssetFactory.create()],
password: 'password',
slug: null,
}),
+22
View File
@@ -0,0 +1,22 @@
import { AssetFactory } from 'test/factories/asset.factory';
export const getForStorageTemplate = (asset: ReturnType<AssetFactory['build']>) => {
return {
id: asset.id,
ownerId: asset.ownerId,
livePhotoVideoId: asset.livePhotoVideoId,
type: asset.type,
isExternal: asset.isExternal,
checksum: asset.checksum,
timeZone: asset.exifInfo.timeZone,
fileCreatedAt: asset.fileCreatedAt,
originalPath: asset.originalPath,
originalFileName: asset.originalFileName,
fileSizeInByte: asset.exifInfo.fileSizeInByte,
files: asset.files,
make: asset.exifInfo.make,
model: asset.exifInfo.model,
lensModel: asset.exifInfo.lensModel,
isEdited: asset.isEdited,
};
};
@@ -90,7 +90,7 @@ describe(SharedLinkService.name, () => {
assetIds: assets.map(({ asset }) => asset.id),
});
await expect(sut.getMine({ user, sharedLink }, {})).resolves.toMatchObject({
await expect(sut.getMine({ user, sharedLink }, [])).resolves.toMatchObject({
assets: assets.map(({ asset }) => expect.objectContaining({ id: asset.id })),
});
});
@@ -114,7 +114,7 @@ describe(SharedLinkService.name, () => {
assetIds: [asset.id],
});
await expect(sut.getMine({ user, sharedLink }, {})).resolves.toMatchObject({
await expect(sut.getMine({ user, sharedLink }, [])).resolves.toMatchObject({
assets: [expect.objectContaining({ id: asset.id })],
});
@@ -122,6 +122,6 @@ describe(SharedLinkService.name, () => {
assetIds: [asset.id],
});
await expect(sut.getMine({ user, sharedLink }, {})).resolves.toHaveProperty('assets', []);
await expect(sut.getMine({ user, sharedLink }, [])).resolves.toHaveProperty('assets', []);
});
});
@@ -1,29 +0,0 @@
import { DatabaseRepository } from 'src/repositories/database.repository';
import { RepositoryInterface } from 'src/types';
import { Mocked, vitest } from 'vitest';
export const newDatabaseRepositoryMock = (): Mocked<RepositoryInterface<DatabaseRepository>> => {
return {
shutdown: vitest.fn(),
getExtensionVersions: vitest.fn(),
getVectorExtension: vitest.fn(),
getExtensionVersionRange: vitest.fn(),
getPostgresVersion: vitest.fn().mockResolvedValue('14.10 (Debian 14.10-1.pgdg120+1)'),
getPostgresVersionRange: vitest.fn().mockReturnValue('>=14.0.0'),
createExtension: vitest.fn().mockResolvedValue(void 0),
dropExtension: vitest.fn(),
updateVectorExtension: vitest.fn(),
reindexVectorsIfNeeded: vitest.fn(),
getDimensionSize: vitest.fn(),
setDimensionSize: vitest.fn(),
deleteAllSearchEmbeddings: vitest.fn(),
prewarm: vitest.fn(),
runMigrations: vitest.fn(),
revertLastMigration: vitest.fn(),
withLock: vitest.fn().mockImplementation((_, function_: <R>() => Promise<R>) => function_()),
tryLock: vitest.fn(),
isBusy: vitest.fn(),
wait: vitest.fn(),
migrateFilePaths: vitest.fn(),
};
};
+9 -2
View File
@@ -75,7 +75,6 @@ import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositorie
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
import { newMediaRepositoryMock } from 'test/repositories/media.repository.mock';
import { newMetadataRepositoryMock } from 'test/repositories/metadata.repository.mock';
@@ -279,6 +278,14 @@ export const getMocks = () => {
const loggerMock = { setContext: () => {} };
const configMock = { getEnv: () => ({}) };
// eslint-disable-next-line no-sparse-arrays
const databaseMock = automock(DatabaseRepository, { args: [, loggerMock], strict: false });
databaseMock.withLock.mockImplementation((_type, fn) => fn());
databaseMock.getPostgresVersion = vitest.fn().mockResolvedValue('14.10 (Debian 14.10-1.pgdg120+1)');
databaseMock.getPostgresVersionRange = vitest.fn().mockReturnValue('>=14.0.0');
databaseMock.createExtension = vitest.fn().mockResolvedValue(void 0);
const mocks: ServiceMocks = {
access: newAccessRepositoryMock(),
// eslint-disable-next-line no-sparse-arrays
@@ -295,7 +302,7 @@ export const getMocks = () => {
assetJob: automock(AssetJobRepository),
app: automock(AppRepository, { strict: false }),
config: newConfigRepositoryMock(),
database: newDatabaseRepositoryMock(),
database: databaseMock,
downloadRepository: automock(DownloadRepository, { strict: false }),
duplicateRepository: automock(DuplicateRepository),
email: automock(EmailRepository, { args: [loggerMock] }),