chore: upgrade to kysely 0.28.11 (#26744)
parent
8764a1894b
commit
34ce68095d
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
expect(dto.startDate).toEqual(startDate);
|
||||
expect(dto.endDate).toEqual(endDate);
|
||||
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.toISOString());
|
||||
expect(dto.endDate).toEqual(endDate.toISOString());
|
||||
});
|
||||
|
||||
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,13 +1,16 @@
|
|||
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 { 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 { MaybeDehydrated } from 'src/types';
|
||||
import { asDateString } from 'src/utils/date';
|
||||
import { Optional, ValidateBoolean, ValidateEnum, ValidateUUID } from 'src/validation';
|
||||
|
||||
export class AlbumInfoDto {
|
||||
|
|
@ -151,10 +154,10 @@ export class AlbumResponseDto {
|
|||
albumName!: string;
|
||||
@ApiProperty({ description: 'Album description' })
|
||||
description!: string;
|
||||
@ApiProperty({ description: 'Creation date' })
|
||||
createdAt!: Date;
|
||||
@ApiProperty({ description: 'Last update date' })
|
||||
updatedAt!: Date;
|
||||
@ApiProperty({ description: 'Creation date', format: 'date-time' })
|
||||
createdAt!: string;
|
||||
@ApiProperty({ description: 'Last update date', format: 'date-time' })
|
||||
updatedAt!: string;
|
||||
@ApiProperty({ description: 'Thumbnail asset ID' })
|
||||
albumThumbnailAssetId!: string | null;
|
||||
@ApiProperty({ description: 'Is shared album' })
|
||||
|
|
@ -172,12 +175,12 @@ export class AlbumResponseDto {
|
|||
owner!: UserResponseDto;
|
||||
@ApiProperty({ type: 'integer', description: 'Number of assets' })
|
||||
assetCount!: number;
|
||||
@ApiPropertyOptional({ description: 'Last modified asset timestamp' })
|
||||
lastModifiedAssetTimestamp?: Date;
|
||||
@ApiPropertyOptional({ description: 'Start date (earliest asset)' })
|
||||
startDate?: Date;
|
||||
@ApiPropertyOptional({ description: 'End date (latest asset)' })
|
||||
endDate?: Date;
|
||||
@ApiPropertyOptional({ description: 'Last modified asset timestamp', format: 'date-time' })
|
||||
lastModifiedAssetTimestamp?: string;
|
||||
@ApiPropertyOptional({ description: 'Start date (earliest asset)', format: 'date-time' })
|
||||
startDate?: string;
|
||||
@ApiPropertyOptional({ description: 'End date (latest asset)', format: 'date-time' })
|
||||
endDate?: string;
|
||||
@ApiProperty({ description: 'Activity feed enabled' })
|
||||
isActivityEnabled!: boolean;
|
||||
@ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', description: 'Asset sort order', optional: true })
|
||||
|
|
@ -191,8 +194,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,12 +203,16 @@ export type MapAlbumDto = {
|
|||
updatedAt: Date;
|
||||
id: string;
|
||||
ownerId: string;
|
||||
owner: User;
|
||||
owner: ShallowDehydrateObject<User>;
|
||||
isActivityEnabled: boolean;
|
||||
order: AssetOrder;
|
||||
};
|
||||
|
||||
export const mapAlbum = (entity: MapAlbumDto, withAssets: boolean, auth?: AuthDto): AlbumResponseDto => {
|
||||
export const mapAlbum = (
|
||||
entity: MaybeDehydrated<MapAlbumDto>,
|
||||
withAssets: boolean,
|
||||
auth?: AuthDto,
|
||||
): AlbumResponseDto => {
|
||||
const albumUsers: AlbumUserResponseDto[] = [];
|
||||
|
||||
if (entity.albumUsers) {
|
||||
|
|
@ -236,16 +243,16 @@ export const mapAlbum = (entity: MapAlbumDto, withAssets: boolean, auth?: AuthDt
|
|||
albumName: entity.albumName,
|
||||
description: entity.description,
|
||||
albumThumbnailAssetId: entity.albumThumbnailAssetId,
|
||||
createdAt: entity.createdAt,
|
||||
updatedAt: entity.updatedAt,
|
||||
createdAt: asDateString(entity.createdAt),
|
||||
updatedAt: asDateString(entity.updatedAt),
|
||||
id: entity.id,
|
||||
ownerId: entity.ownerId,
|
||||
owner: mapUser(entity.owner),
|
||||
albumUsers: albumUsersSorted,
|
||||
shared: hasSharedUser || hasSharedLink,
|
||||
hasSharedLink,
|
||||
startDate,
|
||||
endDate,
|
||||
startDate: asDateString(startDate),
|
||||
endDate: asDateString(endDate),
|
||||
assets: (withAssets ? assets : []).map((asset) => mapAsset(asset, { auth })),
|
||||
assetCount: entity.assets?.length || 0,
|
||||
isActivityEnabled: entity.isActivityEnabled,
|
||||
|
|
@ -253,5 +260,5 @@ export const mapAlbum = (entity: MapAlbumDto, withAssets: boolean, auth?: AuthDt
|
|||
};
|
||||
};
|
||||
|
||||
export const mapAlbumWithAssets = (entity: MapAlbumDto) => mapAlbum(entity, true);
|
||||
export const mapAlbumWithoutAssets = (entity: MapAlbumDto) => mapAlbum(entity, false);
|
||||
export const mapAlbumWithAssets = (entity: MaybeDehydrated<MapAlbumDto>) => mapAlbum(entity, true);
|
||||
export const mapAlbumWithoutAssets = (entity: MaybeDehydrated<MapAlbumDto>) => mapAlbum(entity, false);
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -14,9 +14,10 @@ import {
|
|||
import { TagResponseDto, mapTag } from 'src/dtos/tag.dto';
|
||||
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
|
||||
import { AssetStatus, AssetType, AssetVisibility } from 'src/enum';
|
||||
import { ImageDimensions } from 'src/types';
|
||||
import { ImageDimensions, MaybeDehydrated } from 'src/types';
|
||||
import { getDimensions } from 'src/utils/asset.util';
|
||||
import { hexOrBufferToBase64 } from 'src/utils/bytes';
|
||||
import { asDateString } from 'src/utils/date';
|
||||
import { mimeTypes } from 'src/utils/mime-types';
|
||||
import { ValidateEnum, ValidateUUID } from 'src/validation';
|
||||
|
||||
|
|
@ -39,7 +40,7 @@ export class SanitizedAssetResponseDto {
|
|||
'The local date and time when the photo/video was taken, derived from EXIF metadata. This represents the photographer\'s local time regardless of timezone, stored as a timezone-agnostic timestamp. Used for timeline grouping by "local" days and months.',
|
||||
example: '2024-01-15T14:30:00.000Z',
|
||||
})
|
||||
localDateTime!: Date;
|
||||
localDateTime!: string;
|
||||
@ApiProperty({ description: 'Video duration (for videos)' })
|
||||
duration!: string;
|
||||
@ApiPropertyOptional({ description: 'Live photo video ID' })
|
||||
|
|
@ -59,7 +60,7 @@ export class AssetResponseDto extends SanitizedAssetResponseDto {
|
|||
description: 'The UTC timestamp when the asset was originally uploaded to Immich.',
|
||||
example: '2024-01-15T20:30:00.000Z',
|
||||
})
|
||||
createdAt!: Date;
|
||||
createdAt!: string;
|
||||
@ApiProperty({ description: 'Device asset ID' })
|
||||
deviceAssetId!: string;
|
||||
@ApiProperty({ description: 'Device ID' })
|
||||
|
|
@ -86,7 +87,7 @@ export class AssetResponseDto extends SanitizedAssetResponseDto {
|
|||
'The actual UTC timestamp when the file was created/captured, preserving timezone information. This is the authoritative timestamp for chronological sorting within timeline groups. Combined with timezone data, this can be used to determine the exact moment the photo was taken.',
|
||||
example: '2024-01-15T19:30:00.000Z',
|
||||
})
|
||||
fileCreatedAt!: Date;
|
||||
fileCreatedAt!: string;
|
||||
@ApiProperty({
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
|
|
@ -94,7 +95,7 @@ export class AssetResponseDto extends SanitizedAssetResponseDto {
|
|||
'The UTC timestamp when the file was last modified on the filesystem. This reflects the last time the physical file was changed, which may be different from when the photo was originally taken.',
|
||||
example: '2024-01-16T10:15:00.000Z',
|
||||
})
|
||||
fileModifiedAt!: Date;
|
||||
fileModifiedAt!: string;
|
||||
@ApiProperty({
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
|
|
@ -102,7 +103,7 @@ export class AssetResponseDto extends SanitizedAssetResponseDto {
|
|||
'The UTC timestamp when the asset record was last updated in the database. This is automatically maintained by the database and reflects when any field in the asset was last modified.',
|
||||
example: '2024-01-16T12:45:30.000Z',
|
||||
})
|
||||
updatedAt!: Date;
|
||||
updatedAt!: string;
|
||||
@ApiProperty({ description: 'Is favorite' })
|
||||
isFavorite!: boolean;
|
||||
@ApiProperty({ description: 'Is archived' })
|
||||
|
|
@ -151,13 +152,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 +168,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;
|
||||
|
|
@ -197,7 +198,7 @@ export type AssetMapOptions = {
|
|||
};
|
||||
|
||||
const peopleWithFaces = (
|
||||
faces?: AssetFace[],
|
||||
faces?: MaybeDehydrated<AssetFace>[],
|
||||
edits?: AssetEditActionItem[],
|
||||
assetDimensions?: ImageDimensions,
|
||||
): PersonWithFacesResponseDto[] => {
|
||||
|
|
@ -213,7 +214,10 @@ 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),
|
||||
faces: [],
|
||||
});
|
||||
}
|
||||
const mappedFace = mapFacesWithoutPerson(face, edits, assetDimensions);
|
||||
peopleFaces.get(face.person.id)!.faces.push(mappedFace);
|
||||
|
|
@ -234,7 +238,7 @@ const mapStack = (entity: { stack?: Stack | null }) => {
|
|||
};
|
||||
};
|
||||
|
||||
export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): AssetResponseDto {
|
||||
export function mapAsset(entity: MaybeDehydrated<MapAsset>, options: AssetMapOptions = {}): AssetResponseDto {
|
||||
const { stripMetadata = false, withStack = false } = options;
|
||||
|
||||
if (stripMetadata) {
|
||||
|
|
@ -243,7 +247,7 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset
|
|||
type: entity.type,
|
||||
originalMimeType: mimeTypes.lookup(entity.originalFileName),
|
||||
thumbhash: entity.thumbhash ? hexOrBufferToBase64(entity.thumbhash) : null,
|
||||
localDateTime: entity.localDateTime,
|
||||
localDateTime: asDateString(entity.localDateTime),
|
||||
duration: entity.duration ?? '0:00:00.00000',
|
||||
livePhotoVideoId: entity.livePhotoVideoId,
|
||||
hasMetadata: false,
|
||||
|
|
@ -257,7 +261,7 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset
|
|||
|
||||
return {
|
||||
id: entity.id,
|
||||
createdAt: entity.createdAt,
|
||||
createdAt: asDateString(entity.createdAt),
|
||||
deviceAssetId: entity.deviceAssetId,
|
||||
ownerId: entity.ownerId,
|
||||
owner: entity.owner ? mapUser(entity.owner) : undefined,
|
||||
|
|
@ -268,10 +272,10 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset
|
|||
originalFileName: entity.originalFileName,
|
||||
originalMimeType: mimeTypes.lookup(entity.originalFileName),
|
||||
thumbhash: entity.thumbhash ? hexOrBufferToBase64(entity.thumbhash) : null,
|
||||
fileCreatedAt: entity.fileCreatedAt,
|
||||
fileModifiedAt: entity.fileModifiedAt,
|
||||
localDateTime: entity.localDateTime,
|
||||
updatedAt: entity.updatedAt,
|
||||
fileCreatedAt: asDateString(entity.fileCreatedAt),
|
||||
fileModifiedAt: asDateString(entity.fileModifiedAt),
|
||||
localDateTime: asDateString(entity.localDateTime),
|
||||
updatedAt: asDateString(entity.updatedAt),
|
||||
isFavorite: options.auth?.user.id === entity.ownerId && entity.isFavorite,
|
||||
isArchived: entity.visibility === AssetVisibility.Archive,
|
||||
isTrashed: !!entity.deletedAt,
|
||||
|
|
@ -283,7 +287,7 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset
|
|||
people: peopleWithFaces(entity.faces, entity.edits, assetDimensions),
|
||||
unassignedFaces: entity.faces
|
||||
?.filter((face) => !face.person)
|
||||
.map((a) => mapFacesWithoutPerson(a, entity.edits, assetDimensions)),
|
||||
.map((face) => mapFacesWithoutPerson(face, entity.edits, assetDimensions)),
|
||||
checksum: hexOrBufferToBase64(entity.checksum)!,
|
||||
stack: withStack ? mapStack(entity) : undefined,
|
||||
isOffline: entity.isOffline,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Exif } from 'src/database';
|
||||
import { MaybeDehydrated } from 'src/types';
|
||||
import { asDateString } from 'src/utils/date';
|
||||
|
||||
export class ExifResponseDto {
|
||||
@ApiPropertyOptional({ description: 'Camera make' })
|
||||
|
|
@ -16,9 +18,9 @@ export class ExifResponseDto {
|
|||
@ApiPropertyOptional({ description: 'Image orientation' })
|
||||
orientation?: string | null = null;
|
||||
@ApiPropertyOptional({ description: 'Original date/time', format: 'date-time' })
|
||||
dateTimeOriginal?: Date | null = null;
|
||||
dateTimeOriginal?: string | null = null;
|
||||
@ApiPropertyOptional({ description: 'Modification date/time', format: 'date-time' })
|
||||
modifyDate?: Date | null = null;
|
||||
modifyDate?: string | null = null;
|
||||
@ApiPropertyOptional({ description: 'Time zone' })
|
||||
timeZone?: string | null = null;
|
||||
@ApiPropertyOptional({ description: 'Lens model' })
|
||||
|
|
@ -49,7 +51,7 @@ export class ExifResponseDto {
|
|||
rating?: number | null = null;
|
||||
}
|
||||
|
||||
export function mapExif(entity: Exif): ExifResponseDto {
|
||||
export function mapExif(entity: MaybeDehydrated<Exif>): ExifResponseDto {
|
||||
return {
|
||||
make: entity.make,
|
||||
model: entity.model,
|
||||
|
|
@ -57,8 +59,8 @@ export function mapExif(entity: Exif): ExifResponseDto {
|
|||
exifImageHeight: entity.exifImageHeight,
|
||||
fileSizeInByte: entity.fileSizeInByte ? Number.parseInt(entity.fileSizeInByte.toString()) : null,
|
||||
orientation: entity.orientation,
|
||||
dateTimeOriginal: entity.dateTimeOriginal,
|
||||
modifyDate: entity.modifyDate,
|
||||
dateTimeOriginal: asDateString(entity.dateTimeOriginal),
|
||||
modifyDate: asDateString(entity.modifyDate),
|
||||
timeZone: entity.timeZone,
|
||||
lensModel: entity.lensModel,
|
||||
fNumber: entity.fNumber,
|
||||
|
|
@ -80,7 +82,7 @@ export function mapSanitizedExif(entity: Exif): ExifResponseDto {
|
|||
return {
|
||||
fileSizeInByte: entity.fileSizeInByte ? Number.parseInt(entity.fileSizeInByte.toString()) : null,
|
||||
orientation: entity.orientation,
|
||||
dateTimeOriginal: entity.dateTimeOriginal,
|
||||
dateTimeOriginal: asDateString(entity.dateTimeOriginal),
|
||||
timeZone: entity.timeZone,
|
||||
projectionType: entity.projectionType,
|
||||
exifImageWidth: entity.exifImageWidth,
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ import { AuthDto } from 'src/dtos/auth.dto';
|
|||
import { AssetEditActionItem } from 'src/dtos/editing.dto';
|
||||
import { SourceType } from 'src/enum';
|
||||
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
|
||||
import { ImageDimensions } from 'src/types';
|
||||
import { asDateString } from 'src/utils/date';
|
||||
import { ImageDimensions, MaybeDehydrated } from 'src/types';
|
||||
import { asBirthDateString, asDateString } from 'src/utils/date';
|
||||
import { transformFaceBoundingBox } from 'src/utils/transform';
|
||||
import {
|
||||
IsDateStringFormat,
|
||||
|
|
@ -33,7 +33,7 @@ export class PersonCreateDto {
|
|||
@MaxDateString(() => DateTime.now(), { message: 'Birth date cannot be in the future' })
|
||||
@IsDateStringFormat('yyyy-MM-dd')
|
||||
@Optional({ nullable: true, emptyToNull: true })
|
||||
birthDate?: Date | null;
|
||||
birthDate?: string | null;
|
||||
|
||||
@ValidateBoolean({ optional: true, description: 'Person visibility (hidden)' })
|
||||
isHidden?: boolean;
|
||||
|
|
@ -105,8 +105,12 @@ export class PersonResponseDto {
|
|||
thumbnailPath!: string;
|
||||
@ApiProperty({ description: 'Is hidden' })
|
||||
isHidden!: boolean;
|
||||
@Property({ description: 'Last update date', history: new HistoryBuilder().added('v1.107.0').stable('v2') })
|
||||
updatedAt?: Date;
|
||||
@Property({
|
||||
description: 'Last update date',
|
||||
format: 'date-time',
|
||||
history: new HistoryBuilder().added('v1.107.0').stable('v2'),
|
||||
})
|
||||
updatedAt?: string;
|
||||
@Property({ description: 'Is favorite', history: new HistoryBuilder().added('v1.126.0').stable('v2') })
|
||||
isFavorite?: boolean;
|
||||
@Property({ description: 'Person color (hex)', history: new HistoryBuilder().added('v1.126.0').stable('v2') })
|
||||
|
|
@ -222,21 +226,21 @@ export class PeopleResponseDto {
|
|||
hasNextPage?: boolean;
|
||||
}
|
||||
|
||||
export function mapPerson(person: Person): PersonResponseDto {
|
||||
export function mapPerson(person: MaybeDehydrated<Person>): PersonResponseDto {
|
||||
return {
|
||||
id: person.id,
|
||||
name: person.name,
|
||||
birthDate: asDateString(person.birthDate),
|
||||
birthDate: asBirthDateString(person.birthDate),
|
||||
thumbnailPath: person.thumbnailPath,
|
||||
isHidden: person.isHidden,
|
||||
isFavorite: person.isFavorite,
|
||||
color: person.color ?? undefined,
|
||||
updatedAt: person.updatedAt,
|
||||
updatedAt: asDateString(person.updatedAt),
|
||||
};
|
||||
}
|
||||
|
||||
export function mapFacesWithoutPerson(
|
||||
face: Selectable<AssetFaceTable>,
|
||||
face: MaybeDehydrated<Selectable<AssetFaceTable>>,
|
||||
edits?: AssetEditActionItem[],
|
||||
assetDimensions?: ImageDimensions,
|
||||
): AssetFaceWithoutPersonResponseDto {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsHexColor, IsNotEmpty, IsString } from 'class-validator';
|
||||
import { Tag } from 'src/database';
|
||||
import { MaybeDehydrated } from 'src/types';
|
||||
import { asDateString } from 'src/utils/date';
|
||||
import { Optional, ValidateHexColor, ValidateUUID } from 'src/validation';
|
||||
|
||||
export class TagCreateDto {
|
||||
|
|
@ -54,22 +56,22 @@ export class TagResponseDto {
|
|||
name!: string;
|
||||
@ApiProperty({ description: 'Tag value (full path)' })
|
||||
value!: string;
|
||||
@ApiProperty({ description: 'Creation date' })
|
||||
createdAt!: Date;
|
||||
@ApiProperty({ description: 'Last update date' })
|
||||
updatedAt!: Date;
|
||||
@ApiProperty({ description: 'Creation date', format: 'date-time' })
|
||||
createdAt!: string;
|
||||
@ApiProperty({ description: 'Last update date', format: 'date-time' })
|
||||
updatedAt!: string;
|
||||
@ApiPropertyOptional({ description: 'Tag color (hex)' })
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export function mapTag(entity: Tag): TagResponseDto {
|
||||
export function mapTag(entity: MaybeDehydrated<Tag>): TagResponseDto {
|
||||
return {
|
||||
id: entity.id,
|
||||
parentId: entity.parentId ?? undefined,
|
||||
name: entity.value.split('/').at(-1) as string,
|
||||
value: entity.value,
|
||||
createdAt: entity.createdAt,
|
||||
updatedAt: entity.updatedAt,
|
||||
createdAt: asDateString(entity.createdAt),
|
||||
updatedAt: asDateString(entity.updatedAt),
|
||||
color: entity.color ?? undefined,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import { Transform } from 'class-transformer';
|
|||
import { IsEmail, IsInt, IsNotEmpty, IsString, Min } from 'class-validator';
|
||||
import { User, UserAdmin } from 'src/database';
|
||||
import { UserAvatarColor, UserMetadataKey, UserStatus } from 'src/enum';
|
||||
import { UserMetadataItem } from 'src/types';
|
||||
import { MaybeDehydrated, UserMetadataItem } from 'src/types';
|
||||
import { asDateString } from 'src/utils/date';
|
||||
import { Optional, PinCode, ValidateBoolean, ValidateEnum, ValidateUUID, toEmail, toSanitized } from 'src/validation';
|
||||
|
||||
export class UserUpdateMeDto {
|
||||
|
|
@ -47,8 +48,8 @@ export class UserResponseDto {
|
|||
profileImagePath!: string;
|
||||
@ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', description: 'Avatar color' })
|
||||
avatarColor!: UserAvatarColor;
|
||||
@ApiProperty({ description: 'Profile change date' })
|
||||
profileChangedAt!: Date;
|
||||
@ApiProperty({ description: 'Profile change date', format: 'date-time' })
|
||||
profileChangedAt!: string;
|
||||
}
|
||||
|
||||
export class UserLicense {
|
||||
|
|
@ -68,14 +69,14 @@ const emailToAvatarColor = (email: string): UserAvatarColor => {
|
|||
return values[randomIndex];
|
||||
};
|
||||
|
||||
export const mapUser = (entity: User | UserAdmin): UserResponseDto => {
|
||||
export const mapUser = (entity: MaybeDehydrated<User | UserAdmin>): UserResponseDto => {
|
||||
return {
|
||||
id: entity.id,
|
||||
email: entity.email,
|
||||
name: entity.name,
|
||||
profileImagePath: entity.profileImagePath,
|
||||
avatarColor: entity.avatarColor ?? emailToAvatarColor(entity.email),
|
||||
profileChangedAt: entity.profileChangedAt,
|
||||
profileChangedAt: asDateString(entity.profileChangedAt),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -244,3 +244,37 @@ where
|
|||
or "album"."id" is not null
|
||||
)
|
||||
and "shared_link"."slug" = $2
|
||||
|
||||
-- SharedLinkRepository.getSharedLinks
|
||||
select
|
||||
"shared_link".*,
|
||||
coalesce(
|
||||
json_agg("assets") filter (
|
||||
where
|
||||
"assets"."id" is not null
|
||||
),
|
||||
'[]'
|
||||
) as "assets"
|
||||
from
|
||||
"shared_link"
|
||||
left join "shared_link_asset" on "shared_link_asset"."sharedLinkId" = "shared_link"."id"
|
||||
left join lateral (
|
||||
select
|
||||
"asset".*
|
||||
from
|
||||
"asset"
|
||||
inner join lateral (
|
||||
select
|
||||
*
|
||||
from
|
||||
"asset_exif"
|
||||
where
|
||||
"asset_exif"."assetId" = "asset"."id"
|
||||
) as "exifInfo" on true
|
||||
where
|
||||
"asset"."id" = "shared_link_asset"."assetId"
|
||||
) as "assets" on true
|
||||
where
|
||||
"shared_link"."id" = $1
|
||||
group by
|
||||
"shared_link"."id"
|
||||
|
|
|
|||
|
|
@ -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,9 @@ 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) =>
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import { DB } from 'src/schema';
|
|||
import {
|
||||
anyUuid,
|
||||
asUuid,
|
||||
toJson,
|
||||
withDefaultVisibility,
|
||||
withEdits,
|
||||
withExif,
|
||||
|
|
@ -296,7 +295,12 @@ export class AssetJobRepository {
|
|||
.as('stack_result'),
|
||||
(join) => join.onTrue(),
|
||||
)
|
||||
.select((eb) => toJson(eb, 'stack_result').as('stack'))
|
||||
.select((eb) =>
|
||||
eb.fn
|
||||
.toJson(eb.table('stack_result'))
|
||||
.$castTo<{ id: string; primaryAssetId: string; assets: { id: string }[] } | null>()
|
||||
.as('stack'),
|
||||
)
|
||||
.where('asset.id', '=', id)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
NotNull,
|
||||
Selectable,
|
||||
SelectQueryBuilder,
|
||||
ShallowDehydrateObject,
|
||||
sql,
|
||||
Updateable,
|
||||
UpdateResult,
|
||||
|
|
@ -554,7 +555,11 @@ export class AssetRepository {
|
|||
eb
|
||||
.selectFrom('asset as stacked')
|
||||
.selectAll('stack')
|
||||
.select((eb) => eb.fn('array_agg', [eb.table('stacked')]).as('assets'))
|
||||
.select((eb) =>
|
||||
eb
|
||||
.fn<ShallowDehydrateObject<Selectable<AssetTable>>>('array_agg', [eb.table('stacked')])
|
||||
.as('assets'),
|
||||
)
|
||||
.whereRef('stacked.stackId', '=', 'stack.id')
|
||||
.whereRef('stacked.id', '!=', 'stack.primaryAssetId')
|
||||
.where('stacked.deletedAt', 'is', null)
|
||||
|
|
@ -563,7 +568,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) => eb.fn.toJson(eb.table('stacked_assets')).as('stack')),
|
||||
),
|
||||
)
|
||||
.$if(!!files, (qb) => qb.select(withFiles))
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { Kysely, NotNull, sql } from 'kysely';
|
||||
import { Kysely, NotNull, Selectable, ShallowDehydrateObject, sql } from 'kysely';
|
||||
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';
|
||||
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
|
||||
import { anyUuid, asUuid, withDefaultVisibility } from 'src/utils/database';
|
||||
|
||||
interface DuplicateSearch {
|
||||
|
|
@ -39,15 +39,15 @@ export class DuplicateRepository {
|
|||
qb
|
||||
.selectFrom('asset_exif')
|
||||
.selectAll('asset')
|
||||
.select((eb) => eb.table('asset_exif').as('exifInfo'))
|
||||
.select((eb) =>
|
||||
eb.table('asset_exif').$castTo<ShallowDehydrateObject<Selectable<AssetExifTable>>>().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'),
|
||||
)
|
||||
.select((eb) => eb.fn.jsonAgg('asset2').orderBy('asset.localDateTime', 'asc').as('assets'))
|
||||
.where('asset.ownerId', '=', asUuid(userId))
|
||||
.where('asset.duplicateId', 'is not', null)
|
||||
.$narrowType<{ duplicateId: NotNull }>()
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
@ -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) => eb.fn.toJson(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!))
|
||||
|
|
@ -246,6 +249,7 @@ export class SharedLinkRepository {
|
|||
await this.db.deleteFrom('shared_link').where('shared_link.id', '=', id).execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
private getSharedLinks(id: string) {
|
||||
return this.db
|
||||
.selectFrom('shared_link')
|
||||
|
|
@ -269,7 +273,11 @@ export class SharedLinkRepository {
|
|||
.select((eb) =>
|
||||
eb.fn
|
||||
.coalesce(eb.fn.jsonAgg('assets').filterWhere('assets.id', 'is not', null), sql`'[]'`)
|
||||
.$castTo<MapAsset[]>()
|
||||
.$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([
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { Permission } from 'src/enum';
|
|||
import { AlbumAssetCount, AlbumInfoOptions } from 'src/repositories/album.repository';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { addAssets, removeAssets } from 'src/utils/asset.util';
|
||||
import { asDateString } from 'src/utils/date';
|
||||
import { getPreferences } from 'src/utils/preferences';
|
||||
|
||||
@Injectable()
|
||||
|
|
@ -64,11 +65,11 @@ export class AlbumService extends BaseService {
|
|||
return albums.map((album) => ({
|
||||
...mapAlbumWithoutAssets(album),
|
||||
sharedLinks: undefined,
|
||||
startDate: albumMetadata[album.id]?.startDate ?? undefined,
|
||||
endDate: albumMetadata[album.id]?.endDate ?? undefined,
|
||||
startDate: asDateString(albumMetadata[album.id]?.startDate ?? undefined),
|
||||
endDate: asDateString(albumMetadata[album.id]?.endDate ?? undefined),
|
||||
assetCount: albumMetadata[album.id]?.assetCount ?? 0,
|
||||
// lastModifiedAssetTimestamp is only used in mobile app, please remove if not need
|
||||
lastModifiedAssetTimestamp: albumMetadata[album.id]?.lastModifiedAssetTimestamp ?? undefined,
|
||||
lastModifiedAssetTimestamp: asDateString(albumMetadata[album.id]?.lastModifiedAssetTimestamp ?? undefined),
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
@ -85,10 +86,10 @@ export class AlbumService extends BaseService {
|
|||
|
||||
return {
|
||||
...mapAlbum(album, withAssets, auth),
|
||||
startDate: albumMetadataForIds?.startDate ?? undefined,
|
||||
endDate: albumMetadataForIds?.endDate ?? undefined,
|
||||
startDate: asDateString(albumMetadataForIds?.startDate ?? undefined),
|
||||
endDate: asDateString(albumMetadataForIds?.endDate ?? undefined),
|
||||
assetCount: albumMetadataForIds?.assetCount ?? 0,
|
||||
lastModifiedAssetTimestamp: albumMetadataForIds?.lastModifiedAssetTimestamp ?? undefined,
|
||||
lastModifiedAssetTimestamp: asDateString(albumMetadataForIds?.lastModifiedAssetTimestamp ?? undefined),
|
||||
contributorCounts: isShared ? await this.albumRepository.getContributorCounts(album.id) : undefined,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
|
|
|
|||
|
|
@ -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([]);
|
||||
|
|
@ -3603,15 +3607,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', () => {
|
||||
|
|
@ -3619,23 +3623,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 = {
|
||||
|
|
@ -754,7 +754,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) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ 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';
|
||||
|
||||
|
|
@ -33,7 +34,7 @@ describe(MemoryService.name, () => {
|
|||
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([
|
||||
|
|
@ -68,7 +69,7 @@ describe(MemoryService.name, () => {
|
|||
const userId = newUuid();
|
||||
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({
|
||||
|
|
@ -85,7 +86,7 @@ describe(MemoryService.name, () => {
|
|||
const [assetId, userId] = newUuids();
|
||||
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 } }), {
|
||||
|
|
@ -115,7 +116,7 @@ describe(MemoryService.name, () => {
|
|||
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 } }), {
|
||||
|
|
@ -135,7 +136,7 @@ describe(MemoryService.name, () => {
|
|||
it('should create a memory without assets', async () => {
|
||||
const memory = MemoryFactory.create();
|
||||
|
||||
mocks.memory.create.mockResolvedValue(memory);
|
||||
mocks.memory.create.mockResolvedValue(getForMemory(memory));
|
||||
|
||||
await expect(
|
||||
sut.create(factory.auth(), {
|
||||
|
|
@ -160,7 +161,7 @@ describe(MemoryService.name, () => {
|
|||
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();
|
||||
|
||||
|
|
@ -203,7 +204,7 @@ describe(MemoryService.name, () => {
|
|||
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([
|
||||
|
|
@ -218,7 +219,7 @@ describe(MemoryService.name, () => {
|
|||
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([
|
||||
|
|
@ -234,8 +235,8 @@ describe(MemoryService.name, () => {
|
|||
|
||||
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();
|
||||
|
||||
|
|
@ -275,7 +276,7 @@ describe(MemoryService.name, () => {
|
|||
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,
|
||||
|
|
@ -447,8 +447,7 @@ export class MetadataService extends BaseService {
|
|||
const { description, dateTimeOriginal, latitude, longitude, rating, tags, timeZone } = _.pick(
|
||||
{
|
||||
description: asset.exifInfo.description,
|
||||
// the kysely type is wrong here; fixed in 0.28.3
|
||||
dateTimeOriginal: asset.exifInfo.dateTimeOriginal as string | null,
|
||||
dateTimeOriginal: asset.exifInfo.dateTimeOriginal,
|
||||
latitude: asset.exifInfo.latitude,
|
||||
longitude: asset.exifInfo.longitude,
|
||||
rating: asset.exifInfo.rating ?? 0,
|
||||
|
|
@ -829,7 +828,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 { getDehydrated, 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: getDehydrated(user1),
|
||||
sharedWith: getDehydrated(user2),
|
||||
});
|
||||
const sharedWithUser1 = factory.partner({
|
||||
sharedBy: getDehydrated(user2),
|
||||
sharedWith: getDehydrated(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: getDehydrated(user1),
|
||||
sharedWith: getDehydrated(user2),
|
||||
});
|
||||
const sharedWithUser1 = factory.partner({
|
||||
sharedBy: getDehydrated(user2),
|
||||
sharedWith: getDehydrated(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: getDehydrated(user1), sharedWith: getDehydrated(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: getDehydrated(user1), sharedWith: getDehydrated(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: getDehydrated(user1), sharedWith: getDehydrated(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: getDehydrated(user1), sharedWith: getDehydrated(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,8 @@ 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);
|
||||
|
||||
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';
|
||||
|
||||
|
|
@ -202,16 +202,16 @@ describe(PersonService.name, () => {
|
|||
mocks.person.update.mockResolvedValue(person);
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id]));
|
||||
|
||||
await expect(sut.update(auth, person.id, { birthDate: new Date('1976-06-30') })).resolves.toEqual({
|
||||
await expect(sut.update(auth, person.id, { birthDate: '1976-06-30' })).resolves.toEqual({
|
||||
id: person.id,
|
||||
name: person.name,
|
||||
birthDate: '1976-06-30',
|
||||
thumbnailPath: person.thumbnailPath,
|
||||
isHidden: false,
|
||||
isFavorite: false,
|
||||
updatedAt: expect.any(Date),
|
||||
updatedAt: expect.any(String),
|
||||
});
|
||||
expect(mocks.person.update).toHaveBeenCalledWith({ id: person.id, birthDate: new Date('1976-06-30') });
|
||||
expect(mocks.person.update).toHaveBeenCalledWith({ id: person.id, birthDate: '1976-06-30' });
|
||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||
expect(mocks.job.queueAll).not.toHaveBeenCalled();
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id]));
|
||||
|
|
@ -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({
|
||||
|
|
@ -400,7 +402,7 @@ describe(PersonService.name, () => {
|
|||
id: person.id,
|
||||
name: person.name,
|
||||
thumbnailPath: person.thumbnailPath,
|
||||
updatedAt: expect.any(Date),
|
||||
updatedAt: expect.any(String),
|
||||
});
|
||||
|
||||
expect(mocks.job.queue).not.toHaveBeenCalledWith();
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { ShallowDehydrateObject } from 'kysely';
|
||||
import { SystemConfig } from 'src/config';
|
||||
import { VECTOR_EXTENSIONS } from 'src/constants';
|
||||
import { Asset, AssetFile } from 'src/database';
|
||||
|
|
@ -548,3 +549,5 @@ export interface UserMetadata extends Record<UserMetadataKey, Record<string, any
|
|||
[UserMetadataKey.License]: { licenseKey: string; activationKey: string; activatedAt: string };
|
||||
[UserMetadataKey.Onboarding]: { isOnboarded: boolean };
|
||||
}
|
||||
|
||||
export type MaybeDehydrated<T> = T | ShallowDehydrateObject<T>;
|
||||
|
|
|
|||
|
|
@ -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,25 @@ 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) =>
|
||||
eb.fn
|
||||
.toJson(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) => eb.fn.toJson(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 +148,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)),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import { DateTime } from 'luxon';
|
||||
|
||||
export const asDateString = (x: Date | string | null): string | null => {
|
||||
export const asDateString = <T extends Date | string | undefined | null>(x: T) => {
|
||||
return x instanceof Date ? x.toISOString() : (x as Exclude<T, Date>);
|
||||
};
|
||||
|
||||
export const asBirthDateString = (x: Date | string | null): string | null => {
|
||||
return x instanceof Date ? x.toISOString().split('T')[0] : x;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -55,15 +55,15 @@ export const tagStub = {
|
|||
export const tagResponseStub = {
|
||||
tag1: Object.freeze<TagResponseDto>({
|
||||
id: 'tag-1',
|
||||
createdAt: new Date('2021-01-01T00:00:00Z'),
|
||||
updatedAt: new Date('2021-01-01T00:00:00Z'),
|
||||
createdAt: '2021-01-01T00:00:00.000Z',
|
||||
updatedAt: '2021-01-01T00:00:00.000Z',
|
||||
name: 'Tag1',
|
||||
value: 'Tag1',
|
||||
}),
|
||||
color1: Object.freeze<TagResponseDto>({
|
||||
id: 'tag-1',
|
||||
createdAt: new Date('2021-01-01T00:00:00Z'),
|
||||
updatedAt: new Date('2021-01-01T00:00:00Z'),
|
||||
createdAt: '2021-01-01T00:00:00.000Z',
|
||||
updatedAt: '2021-01-01T00:00:00.000Z',
|
||||
color: '#000000',
|
||||
name: 'Tag1',
|
||||
value: 'Tag1',
|
||||
|
|
|
|||
|
|
@ -1,7 +1,15 @@
|
|||
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 { AssetTable } from 'src/schema/tables/asset.table';
|
||||
import { PartnerTable } from 'src/schema/tables/partner.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 +55,171 @@ 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 getDehydrated = <T extends Record<string, unknown>>(entity: T) => {
|
||||
const copiedEntity = structuredClone(entity);
|
||||
for (const [key, value] of Object.entries(copiedEntity)) {
|
||||
if (value instanceof Date) {
|
||||
Object.assign(copiedEntity, { [key]: value.toISOString() });
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return copiedEntity as ShallowDehydrateObject<T>;
|
||||
};
|
||||
|
||||
export const getForAlbum = (album: ReturnType<AlbumFactory['build']>) => ({
|
||||
...album,
|
||||
assets: album.assets.map((asset) =>
|
||||
getDehydrated({ ...getForAsset(asset), exifInfo: getDehydrated(asset.exifInfo) }),
|
||||
),
|
||||
albumUsers: album.albumUsers.map((albumUser) => ({
|
||||
...albumUser,
|
||||
createdAt: albumUser.createdAt.toISOString(),
|
||||
user: getDehydrated(albumUser.user),
|
||||
})),
|
||||
owner: getDehydrated(album.owner),
|
||||
sharedLinks: album.sharedLinks.map((sharedLink) => getDehydrated(sharedLink)),
|
||||
});
|
||||
|
||||
export const getForActivity = (activity: Selectable<ActivityTable> & { user: ReturnType<UserFactory['build']> }) => ({
|
||||
...activity,
|
||||
user: getDehydrated(activity.user),
|
||||
});
|
||||
|
||||
export const getForAsset = (asset: ReturnType<AssetFactory['build']>) => {
|
||||
return {
|
||||
...asset,
|
||||
faces: asset.faces.map((face) => ({
|
||||
...getDehydrated(face),
|
||||
person: face.person ? getDehydrated(face.person) : null,
|
||||
})),
|
||||
owner: getDehydrated(asset.owner),
|
||||
stack: asset.stack
|
||||
? { ...getDehydrated(asset.stack), assets: asset.stack.assets.map((asset) => getDehydrated(asset)) }
|
||||
: null,
|
||||
files: asset.files.map((file) => getDehydrated(file)),
|
||||
exifInfo: asset.exifInfo ? getDehydrated(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: getDehydrated(partner.sharedBy),
|
||||
sharedWith: getDehydrated(partner.sharedWith),
|
||||
});
|
||||
|
||||
export const getForMemory = (memory: ReturnType<MemoryFactory['build']>) => ({
|
||||
...memory,
|
||||
assets: memory.assets.map((asset) => getDehydrated(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) => getDehydrated(face)),
|
||||
files: asset.files.map((file) => getDehydrated(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) => getDehydrated(file)),
|
||||
exifInfo: getDehydrated(asset.exifInfo),
|
||||
edits: asset.edits.map(({ action, parameters }) => ({ action, parameters })) as AssetEditActionItem[],
|
||||
});
|
||||
|
||||
export const getForAssetFace = (face: ReturnType<AssetFaceFactory['build']>) => ({
|
||||
...face,
|
||||
person: face.person ? getDehydrated(face.person) : null,
|
||||
});
|
||||
|
||||
export const getForDetectedFaces = (asset: ReturnType<AssetFactory['build']>) => ({
|
||||
id: asset.id,
|
||||
visibility: asset.visibility,
|
||||
exifInfo: getDehydrated(asset.exifInfo),
|
||||
faces: asset.faces.map((face) => getDehydrated(face)),
|
||||
files: asset.files.map((file) => getDehydrated(file)),
|
||||
});
|
||||
|
||||
export const getForSidecarWrite = (asset: ReturnType<AssetFactory['build']>) => ({
|
||||
id: asset.id,
|
||||
originalPath: asset.originalPath,
|
||||
files: asset.files.map((file) => getDehydrated(file)),
|
||||
exifInfo: getDehydrated(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 ? getDehydrated(asset.exifInfo) : null,
|
||||
files: asset.files.map((file) => getDehydrated(file)),
|
||||
stack: asset.stack
|
||||
? {
|
||||
...getDehydrated(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) => ({
|
||||
...getDehydrated(asset),
|
||||
exifInfo: getDehydrated(asset.exifInfo),
|
||||
})),
|
||||
});
|
||||
|
||||
export const getForDuplicate = (asset: ReturnType<AssetFactory['build']>) => ({
|
||||
...getDehydrated(asset),
|
||||
exifInfo: getDehydrated(asset.exifInfo),
|
||||
});
|
||||
|
||||
export const getForSharedLink = (sharedLink: ReturnType<SharedLinkFactory['build']>) => ({
|
||||
...sharedLink,
|
||||
assets: sharedLink.assets.map((asset) => ({
|
||||
...getDehydrated({ ...getForAsset(asset) }),
|
||||
exifInfo: getDehydrated(asset.exifInfo),
|
||||
})),
|
||||
album: sharedLink.album
|
||||
? {
|
||||
...getDehydrated(sharedLink.album),
|
||||
owner: getDehydrated(sharedLink.album.owner),
|
||||
assets: sharedLink.album.assets.map((asset) => getDehydrated(asset)),
|
||||
}
|
||||
: null,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { ShallowDehydrateObject } from 'kysely';
|
||||
import {
|
||||
Activity,
|
||||
Album,
|
||||
|
|
@ -113,8 +114,12 @@ const partnerFactory = ({
|
|||
sharedWith: sharedWithProvided,
|
||||
...partner
|
||||
}: Partial<Partner> = {}) => {
|
||||
const sharedBy = UserFactory.create(sharedByProvided ?? {});
|
||||
const sharedWith = UserFactory.create(sharedWithProvided ?? {});
|
||||
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,
|
||||
|
|
@ -214,7 +219,7 @@ const userAdminFactory = (user: Partial<UserAdmin> = {}) => {
|
|||
};
|
||||
};
|
||||
|
||||
const activityFactory = (activity: Partial<Activity> = {}) => {
|
||||
const activityFactory = (activity: Omit<Partial<Activity>, 'user'> = {}) => {
|
||||
const userId = activity.userId || newUuid();
|
||||
return {
|
||||
id: newUuid(),
|
||||
|
|
|
|||
Loading…
Reference in New Issue