mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
chore: upgrade to kysely 0.28.11 (#26744)
This commit is contained in:
Generated
+6
-18
@@ -488,11 +488,11 @@ importers:
|
|||||||
specifier: ^9.0.2
|
specifier: ^9.0.2
|
||||||
version: 9.0.3
|
version: 9.0.3
|
||||||
kysely:
|
kysely:
|
||||||
specifier: 0.28.2
|
specifier: 0.28.11
|
||||||
version: 0.28.2
|
version: 0.28.11
|
||||||
kysely-postgres-js:
|
kysely-postgres-js:
|
||||||
specifier: ^3.0.0
|
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:
|
lodash:
|
||||||
specifier: ^4.17.21
|
specifier: ^4.17.21
|
||||||
version: 4.17.23
|
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)
|
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:
|
nestjs-kysely:
|
||||||
specifier: 3.1.2
|
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:
|
nestjs-otel:
|
||||||
specifier: ^7.0.0
|
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)
|
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==}
|
resolution: {integrity: sha512-zpGIFg0HuoC893rIjYX1BETkVWdDnzTzF5e0kWXJFg5lE0k1/LfNWBejrcnOFu8Q2Rfq/hTDTU7XLUM8QOrpzg==}
|
||||||
engines: {node: '>=20.0.0'}
|
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:
|
langium@3.3.1:
|
||||||
resolution: {integrity: sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==}
|
resolution: {integrity: sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==}
|
||||||
engines: {node: '>=16.0.0'}
|
engines: {node: '>=16.0.0'}
|
||||||
@@ -21040,16 +21036,8 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
postgres: 3.4.8
|
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.11: {}
|
||||||
|
|
||||||
kysely@0.28.2: {}
|
|
||||||
|
|
||||||
langium@3.3.1:
|
langium@3.3.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
chevrotain: 11.0.3
|
chevrotain: 11.0.3
|
||||||
@@ -22128,11 +22116,11 @@ snapshots:
|
|||||||
reflect-metadata: 0.2.2
|
reflect-metadata: 0.2.2
|
||||||
rxjs: 7.8.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:
|
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/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)
|
'@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
|
reflect-metadata: 0.2.2
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -82,7 +82,7 @@
|
|||||||
"jose": "^5.10.0",
|
"jose": "^5.10.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"kysely": "0.28.2",
|
"kysely": "0.28.11",
|
||||||
"kysely-postgres-js": "^3.0.0",
|
"kysely-postgres-js": "^3.0.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"luxon": "^3.4.2",
|
"luxon": "^3.4.2",
|
||||||
|
|||||||
+14
-13
@@ -1,4 +1,4 @@
|
|||||||
import { Selectable } from 'kysely';
|
import { Selectable, ShallowDehydrateObject } from 'kysely';
|
||||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||||
import {
|
import {
|
||||||
AlbumUserRole,
|
AlbumUserRole,
|
||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
} from 'src/enum';
|
} from 'src/enum';
|
||||||
import { AlbumTable } from 'src/schema/tables/album.table';
|
import { AlbumTable } from 'src/schema/tables/album.table';
|
||||||
import { AssetExifTable } from 'src/schema/tables/asset-exif.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 { PluginActionTable, PluginFilterTable, PluginTable } from 'src/schema/tables/plugin.table';
|
||||||
import { WorkflowActionTable, WorkflowFilterTable, WorkflowTable } from 'src/schema/tables/workflow.table';
|
import { WorkflowActionTable, WorkflowFilterTable, WorkflowTable } from 'src/schema/tables/workflow.table';
|
||||||
import { UserMetadataItem } from 'src/types';
|
import { UserMetadataItem } from 'src/types';
|
||||||
@@ -31,7 +32,7 @@ export type AuthUser = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type AlbumUser = {
|
export type AlbumUser = {
|
||||||
user: User;
|
user: ShallowDehydrateObject<User>;
|
||||||
role: AlbumUserRole;
|
role: AlbumUserRole;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -67,7 +68,7 @@ export type Activity = {
|
|||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
albumId: string;
|
albumId: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
user: User;
|
user: ShallowDehydrateObject<User>;
|
||||||
assetId: string | null;
|
assetId: string | null;
|
||||||
comment: string | null;
|
comment: string | null;
|
||||||
isLiked: boolean;
|
isLiked: boolean;
|
||||||
@@ -105,7 +106,7 @@ export type Memory = {
|
|||||||
data: object;
|
data: object;
|
||||||
ownerId: string;
|
ownerId: string;
|
||||||
isSaved: boolean;
|
isSaved: boolean;
|
||||||
assets: MapAsset[];
|
assets: ShallowDehydrateObject<MapAsset>[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Asset = {
|
export type Asset = {
|
||||||
@@ -159,9 +160,9 @@ export type StorageAsset = {
|
|||||||
export type Stack = {
|
export type Stack = {
|
||||||
id: string;
|
id: string;
|
||||||
primaryAssetId: string;
|
primaryAssetId: string;
|
||||||
owner?: User;
|
owner?: ShallowDehydrateObject<User>;
|
||||||
ownerId: string;
|
ownerId: string;
|
||||||
assets: MapAsset[];
|
assets: ShallowDehydrateObject<MapAsset>[];
|
||||||
assetCount?: number;
|
assetCount?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -177,11 +178,11 @@ export type AuthSharedLink = {
|
|||||||
|
|
||||||
export type SharedLink = {
|
export type SharedLink = {
|
||||||
id: string;
|
id: string;
|
||||||
album?: Album | null;
|
album?: ShallowDehydrateObject<Album> | null;
|
||||||
albumId: string | null;
|
albumId: string | null;
|
||||||
allowDownload: boolean;
|
allowDownload: boolean;
|
||||||
allowUpload: boolean;
|
allowUpload: boolean;
|
||||||
assets: MapAsset[];
|
assets: ShallowDehydrateObject<MapAsset>[];
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
expiresAt: Date | null;
|
expiresAt: Date | null;
|
||||||
@@ -194,8 +195,8 @@ export type SharedLink = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type Album = Selectable<AlbumTable> & {
|
export type Album = Selectable<AlbumTable> & {
|
||||||
owner: User;
|
owner: ShallowDehydrateObject<User>;
|
||||||
assets: MapAsset[];
|
assets: ShallowDehydrateObject<Selectable<AssetTable>>[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AuthSession = {
|
export type AuthSession = {
|
||||||
@@ -205,9 +206,9 @@ export type AuthSession = {
|
|||||||
|
|
||||||
export type Partner = {
|
export type Partner = {
|
||||||
sharedById: string;
|
sharedById: string;
|
||||||
sharedBy: User;
|
sharedBy: ShallowDehydrateObject<User>;
|
||||||
sharedWithId: string;
|
sharedWithId: string;
|
||||||
sharedWith: User;
|
sharedWith: ShallowDehydrateObject<User>;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
createId: string;
|
createId: string;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
@@ -270,7 +271,7 @@ export type AssetFace = {
|
|||||||
imageWidth: number;
|
imageWidth: number;
|
||||||
personId: string | null;
|
personId: string | null;
|
||||||
sourceType: SourceType;
|
sourceType: SourceType;
|
||||||
person?: Person | null;
|
person?: ShallowDehydrateObject<Person> | null;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
updateId: string;
|
updateId: string;
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
import { mapAlbum } from 'src/dtos/album.dto';
|
import { mapAlbum } from 'src/dtos/album.dto';
|
||||||
import { AlbumFactory } from 'test/factories/album.factory';
|
import { AlbumFactory } from 'test/factories/album.factory';
|
||||||
|
import { getForAlbum } from 'test/mappers';
|
||||||
|
|
||||||
describe('mapAlbum', () => {
|
describe('mapAlbum', () => {
|
||||||
it('should set start and end dates', () => {
|
it('should set start and end dates', () => {
|
||||||
const startDate = new Date('2023-02-22T05:06:29.716Z');
|
const startDate = new Date('2023-02-22T05:06:29.716Z');
|
||||||
const endDate = new Date('2025-01-01T01:02:03.456Z');
|
const endDate = new Date('2025-01-01T01:02:03.456Z');
|
||||||
const album = AlbumFactory.from().asset({ localDateTime: endDate }).asset({ localDateTime: startDate }).build();
|
const album = AlbumFactory.from()
|
||||||
const dto = mapAlbum(album, false);
|
.asset({ localDateTime: endDate }, (builder) => builder.exif())
|
||||||
expect(dto.startDate).toEqual(startDate);
|
.asset({ localDateTime: startDate }, (builder) => builder.exif())
|
||||||
expect(dto.endDate).toEqual(endDate);
|
.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', () => {
|
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.startDate).toBeUndefined();
|
||||||
expect(dto.endDate).toBeUndefined();
|
expect(dto.endDate).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import { ArrayNotEmpty, IsArray, IsString, ValidateNested } from 'class-validator';
|
import { ArrayNotEmpty, IsArray, IsString, ValidateNested } from 'class-validator';
|
||||||
|
import { ShallowDehydrateObject } from 'kysely';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { AlbumUser, AuthSharedLink, User } from 'src/database';
|
import { AlbumUser, AuthSharedLink, User } from 'src/database';
|
||||||
import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto';
|
import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto';
|
||||||
import { AssetResponseDto, MapAsset, mapAsset } from 'src/dtos/asset-response.dto';
|
import { AssetResponseDto, MapAsset, mapAsset } from 'src/dtos/asset-response.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.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 { AlbumUserRole, AssetOrder } from 'src/enum';
|
||||||
|
import { MaybeDehydrated } from 'src/types';
|
||||||
|
import { asDateString } from 'src/utils/date';
|
||||||
import { Optional, ValidateBoolean, ValidateEnum, ValidateUUID } from 'src/validation';
|
import { Optional, ValidateBoolean, ValidateEnum, ValidateUUID } from 'src/validation';
|
||||||
|
|
||||||
export class AlbumInfoDto {
|
export class AlbumInfoDto {
|
||||||
@@ -151,10 +154,10 @@ export class AlbumResponseDto {
|
|||||||
albumName!: string;
|
albumName!: string;
|
||||||
@ApiProperty({ description: 'Album description' })
|
@ApiProperty({ description: 'Album description' })
|
||||||
description!: string;
|
description!: string;
|
||||||
@ApiProperty({ description: 'Creation date' })
|
@ApiProperty({ description: 'Creation date', format: 'date-time' })
|
||||||
createdAt!: Date;
|
createdAt!: string;
|
||||||
@ApiProperty({ description: 'Last update date' })
|
@ApiProperty({ description: 'Last update date', format: 'date-time' })
|
||||||
updatedAt!: Date;
|
updatedAt!: string;
|
||||||
@ApiProperty({ description: 'Thumbnail asset ID' })
|
@ApiProperty({ description: 'Thumbnail asset ID' })
|
||||||
albumThumbnailAssetId!: string | null;
|
albumThumbnailAssetId!: string | null;
|
||||||
@ApiProperty({ description: 'Is shared album' })
|
@ApiProperty({ description: 'Is shared album' })
|
||||||
@@ -172,12 +175,12 @@ export class AlbumResponseDto {
|
|||||||
owner!: UserResponseDto;
|
owner!: UserResponseDto;
|
||||||
@ApiProperty({ type: 'integer', description: 'Number of assets' })
|
@ApiProperty({ type: 'integer', description: 'Number of assets' })
|
||||||
assetCount!: number;
|
assetCount!: number;
|
||||||
@ApiPropertyOptional({ description: 'Last modified asset timestamp' })
|
@ApiPropertyOptional({ description: 'Last modified asset timestamp', format: 'date-time' })
|
||||||
lastModifiedAssetTimestamp?: Date;
|
lastModifiedAssetTimestamp?: string;
|
||||||
@ApiPropertyOptional({ description: 'Start date (earliest asset)' })
|
@ApiPropertyOptional({ description: 'Start date (earliest asset)', format: 'date-time' })
|
||||||
startDate?: Date;
|
startDate?: string;
|
||||||
@ApiPropertyOptional({ description: 'End date (latest asset)' })
|
@ApiPropertyOptional({ description: 'End date (latest asset)', format: 'date-time' })
|
||||||
endDate?: Date;
|
endDate?: string;
|
||||||
@ApiProperty({ description: 'Activity feed enabled' })
|
@ApiProperty({ description: 'Activity feed enabled' })
|
||||||
isActivityEnabled!: boolean;
|
isActivityEnabled!: boolean;
|
||||||
@ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', description: 'Asset sort order', optional: true })
|
@ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', description: 'Asset sort order', optional: true })
|
||||||
@@ -191,8 +194,8 @@ export class AlbumResponseDto {
|
|||||||
|
|
||||||
export type MapAlbumDto = {
|
export type MapAlbumDto = {
|
||||||
albumUsers?: AlbumUser[];
|
albumUsers?: AlbumUser[];
|
||||||
assets?: MapAsset[];
|
assets?: ShallowDehydrateObject<MapAsset>[];
|
||||||
sharedLinks?: AuthSharedLink[];
|
sharedLinks?: ShallowDehydrateObject<AuthSharedLink>[];
|
||||||
albumName: string;
|
albumName: string;
|
||||||
description: string;
|
description: string;
|
||||||
albumThumbnailAssetId: string | null;
|
albumThumbnailAssetId: string | null;
|
||||||
@@ -200,12 +203,16 @@ export type MapAlbumDto = {
|
|||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
id: string;
|
id: string;
|
||||||
ownerId: string;
|
ownerId: string;
|
||||||
owner: User;
|
owner: ShallowDehydrateObject<User>;
|
||||||
isActivityEnabled: boolean;
|
isActivityEnabled: boolean;
|
||||||
order: AssetOrder;
|
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[] = [];
|
const albumUsers: AlbumUserResponseDto[] = [];
|
||||||
|
|
||||||
if (entity.albumUsers) {
|
if (entity.albumUsers) {
|
||||||
@@ -236,16 +243,16 @@ export const mapAlbum = (entity: MapAlbumDto, withAssets: boolean, auth?: AuthDt
|
|||||||
albumName: entity.albumName,
|
albumName: entity.albumName,
|
||||||
description: entity.description,
|
description: entity.description,
|
||||||
albumThumbnailAssetId: entity.albumThumbnailAssetId,
|
albumThumbnailAssetId: entity.albumThumbnailAssetId,
|
||||||
createdAt: entity.createdAt,
|
createdAt: asDateString(entity.createdAt),
|
||||||
updatedAt: entity.updatedAt,
|
updatedAt: asDateString(entity.updatedAt),
|
||||||
id: entity.id,
|
id: entity.id,
|
||||||
ownerId: entity.ownerId,
|
ownerId: entity.ownerId,
|
||||||
owner: mapUser(entity.owner),
|
owner: mapUser(entity.owner),
|
||||||
albumUsers: albumUsersSorted,
|
albumUsers: albumUsersSorted,
|
||||||
shared: hasSharedUser || hasSharedLink,
|
shared: hasSharedUser || hasSharedLink,
|
||||||
hasSharedLink,
|
hasSharedLink,
|
||||||
startDate,
|
startDate: asDateString(startDate),
|
||||||
endDate,
|
endDate: asDateString(endDate),
|
||||||
assets: (withAssets ? assets : []).map((asset) => mapAsset(asset, { auth })),
|
assets: (withAssets ? assets : []).map((asset) => mapAsset(asset, { auth })),
|
||||||
assetCount: entity.assets?.length || 0,
|
assetCount: entity.assets?.length || 0,
|
||||||
isActivityEnabled: entity.isActivityEnabled,
|
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 mapAlbumWithAssets = (entity: MaybeDehydrated<MapAlbumDto>) => mapAlbum(entity, true);
|
||||||
export const mapAlbumWithoutAssets = (entity: MapAlbumDto) => mapAlbum(entity, false);
|
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 { AssetFaceFactory } from 'test/factories/asset-face.factory';
|
||||||
import { AssetFactory } from 'test/factories/asset.factory';
|
import { AssetFactory } from 'test/factories/asset.factory';
|
||||||
import { PersonFactory } from 'test/factories/person.factory';
|
import { PersonFactory } from 'test/factories/person.factory';
|
||||||
|
import { getForAsset } from 'test/mappers';
|
||||||
|
|
||||||
describe('mapAsset', () => {
|
describe('mapAsset', () => {
|
||||||
describe('peopleWithFaces', () => {
|
describe('peopleWithFaces', () => {
|
||||||
@@ -41,7 +42,7 @@ describe('mapAsset', () => {
|
|||||||
})
|
})
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
const result = mapAsset(asset);
|
const result = mapAsset(getForAsset(asset));
|
||||||
|
|
||||||
expect(result.people).toBeDefined();
|
expect(result.people).toBeDefined();
|
||||||
expect(result.people).toHaveLength(1);
|
expect(result.people).toHaveLength(1);
|
||||||
@@ -80,7 +81,7 @@ describe('mapAsset', () => {
|
|||||||
.edit({ action: AssetEditAction.Crop, parameters: { x: 50, y: 50, width: 500, height: 400 } })
|
.edit({ action: AssetEditAction.Crop, parameters: { x: 50, y: 50, width: 500, height: 400 } })
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
const result = mapAsset(asset);
|
const result = mapAsset(getForAsset(asset));
|
||||||
|
|
||||||
expect(result.unassignedFaces).toBeDefined();
|
expect(result.unassignedFaces).toBeDefined();
|
||||||
expect(result.unassignedFaces).toHaveLength(1);
|
expect(result.unassignedFaces).toHaveLength(1);
|
||||||
@@ -130,7 +131,7 @@ describe('mapAsset', () => {
|
|||||||
.exif({ exifImageWidth: 1000, exifImageHeight: 800 })
|
.exif({ exifImageWidth: 1000, exifImageHeight: 800 })
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
const result = mapAsset(asset);
|
const result = mapAsset(getForAsset(asset));
|
||||||
|
|
||||||
expect(result.people).toBeDefined();
|
expect(result.people).toBeDefined();
|
||||||
expect(result.people).toHaveLength(2);
|
expect(result.people).toHaveLength(2);
|
||||||
@@ -179,7 +180,7 @@ describe('mapAsset', () => {
|
|||||||
.exif({ exifImageWidth: 1000, exifImageHeight: 800 })
|
.exif({ exifImageWidth: 1000, exifImageHeight: 800 })
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
const result = mapAsset(asset);
|
const result = mapAsset(getForAsset(asset));
|
||||||
|
|
||||||
expect(result.people).toBeDefined();
|
expect(result.people).toBeDefined();
|
||||||
expect(result.people).toHaveLength(1);
|
expect(result.people).toHaveLength(1);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
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 { AssetFace, AssetFile, Exif, Stack, Tag, User } from 'src/database';
|
||||||
import { HistoryBuilder, Property } from 'src/decorators';
|
import { HistoryBuilder, Property } from 'src/decorators';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
@@ -14,9 +14,10 @@ import {
|
|||||||
import { TagResponseDto, mapTag } from 'src/dtos/tag.dto';
|
import { TagResponseDto, mapTag } from 'src/dtos/tag.dto';
|
||||||
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
|
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
|
||||||
import { AssetStatus, AssetType, AssetVisibility } from 'src/enum';
|
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 { getDimensions } from 'src/utils/asset.util';
|
||||||
import { hexOrBufferToBase64 } from 'src/utils/bytes';
|
import { hexOrBufferToBase64 } from 'src/utils/bytes';
|
||||||
|
import { asDateString } from 'src/utils/date';
|
||||||
import { mimeTypes } from 'src/utils/mime-types';
|
import { mimeTypes } from 'src/utils/mime-types';
|
||||||
import { ValidateEnum, ValidateUUID } from 'src/validation';
|
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.',
|
'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',
|
example: '2024-01-15T14:30:00.000Z',
|
||||||
})
|
})
|
||||||
localDateTime!: Date;
|
localDateTime!: string;
|
||||||
@ApiProperty({ description: 'Video duration (for videos)' })
|
@ApiProperty({ description: 'Video duration (for videos)' })
|
||||||
duration!: string;
|
duration!: string;
|
||||||
@ApiPropertyOptional({ description: 'Live photo video ID' })
|
@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.',
|
description: 'The UTC timestamp when the asset was originally uploaded to Immich.',
|
||||||
example: '2024-01-15T20:30:00.000Z',
|
example: '2024-01-15T20:30:00.000Z',
|
||||||
})
|
})
|
||||||
createdAt!: Date;
|
createdAt!: string;
|
||||||
@ApiProperty({ description: 'Device asset ID' })
|
@ApiProperty({ description: 'Device asset ID' })
|
||||||
deviceAssetId!: string;
|
deviceAssetId!: string;
|
||||||
@ApiProperty({ description: 'Device ID' })
|
@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.',
|
'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',
|
example: '2024-01-15T19:30:00.000Z',
|
||||||
})
|
})
|
||||||
fileCreatedAt!: Date;
|
fileCreatedAt!: string;
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
type: 'string',
|
type: 'string',
|
||||||
format: 'date-time',
|
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.',
|
'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',
|
example: '2024-01-16T10:15:00.000Z',
|
||||||
})
|
})
|
||||||
fileModifiedAt!: Date;
|
fileModifiedAt!: string;
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
type: 'string',
|
type: 'string',
|
||||||
format: 'date-time',
|
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.',
|
'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',
|
example: '2024-01-16T12:45:30.000Z',
|
||||||
})
|
})
|
||||||
updatedAt!: Date;
|
updatedAt!: string;
|
||||||
@ApiProperty({ description: 'Is favorite' })
|
@ApiProperty({ description: 'Is favorite' })
|
||||||
isFavorite!: boolean;
|
isFavorite!: boolean;
|
||||||
@ApiProperty({ description: 'Is archived' })
|
@ApiProperty({ description: 'Is archived' })
|
||||||
@@ -151,13 +152,13 @@ export type MapAsset = {
|
|||||||
deviceId: string;
|
deviceId: string;
|
||||||
duplicateId: string | null;
|
duplicateId: string | null;
|
||||||
duration: string | null;
|
duration: string | null;
|
||||||
edits?: AssetEditActionItem[];
|
edits?: ShallowDehydrateObject<AssetEditActionItem>[];
|
||||||
encodedVideoPath: string | null;
|
encodedVideoPath: string | null;
|
||||||
exifInfo?: Selectable<Exif> | null;
|
exifInfo?: ShallowDehydrateObject<Selectable<Exif>> | null;
|
||||||
faces?: AssetFace[];
|
faces?: ShallowDehydrateObject<AssetFace>[];
|
||||||
fileCreatedAt: Date;
|
fileCreatedAt: Date;
|
||||||
fileModifiedAt: Date;
|
fileModifiedAt: Date;
|
||||||
files?: AssetFile[];
|
files?: ShallowDehydrateObject<AssetFile>[];
|
||||||
isExternal: boolean;
|
isExternal: boolean;
|
||||||
isFavorite: boolean;
|
isFavorite: boolean;
|
||||||
isOffline: boolean;
|
isOffline: boolean;
|
||||||
@@ -167,11 +168,11 @@ export type MapAsset = {
|
|||||||
localDateTime: Date;
|
localDateTime: Date;
|
||||||
originalFileName: string;
|
originalFileName: string;
|
||||||
originalPath: string;
|
originalPath: string;
|
||||||
owner?: User | null;
|
owner?: ShallowDehydrateObject<User> | null;
|
||||||
ownerId: string;
|
ownerId: string;
|
||||||
stack?: Stack | null;
|
stack?: (ShallowDehydrateObject<Stack> & { assets: Stack['assets'] }) | null;
|
||||||
stackId: string | null;
|
stackId: string | null;
|
||||||
tags?: Tag[];
|
tags?: ShallowDehydrateObject<Tag>[];
|
||||||
thumbhash: Buffer<ArrayBufferLike> | null;
|
thumbhash: Buffer<ArrayBufferLike> | null;
|
||||||
type: AssetType;
|
type: AssetType;
|
||||||
width: number | null;
|
width: number | null;
|
||||||
@@ -197,7 +198,7 @@ export type AssetMapOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const peopleWithFaces = (
|
const peopleWithFaces = (
|
||||||
faces?: AssetFace[],
|
faces?: MaybeDehydrated<AssetFace>[],
|
||||||
edits?: AssetEditActionItem[],
|
edits?: AssetEditActionItem[],
|
||||||
assetDimensions?: ImageDimensions,
|
assetDimensions?: ImageDimensions,
|
||||||
): PersonWithFacesResponseDto[] => {
|
): PersonWithFacesResponseDto[] => {
|
||||||
@@ -213,7 +214,10 @@ const peopleWithFaces = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!peopleFaces.has(face.person.id)) {
|
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);
|
const mappedFace = mapFacesWithoutPerson(face, edits, assetDimensions);
|
||||||
peopleFaces.get(face.person.id)!.faces.push(mappedFace);
|
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;
|
const { stripMetadata = false, withStack = false } = options;
|
||||||
|
|
||||||
if (stripMetadata) {
|
if (stripMetadata) {
|
||||||
@@ -243,7 +247,7 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset
|
|||||||
type: entity.type,
|
type: entity.type,
|
||||||
originalMimeType: mimeTypes.lookup(entity.originalFileName),
|
originalMimeType: mimeTypes.lookup(entity.originalFileName),
|
||||||
thumbhash: entity.thumbhash ? hexOrBufferToBase64(entity.thumbhash) : null,
|
thumbhash: entity.thumbhash ? hexOrBufferToBase64(entity.thumbhash) : null,
|
||||||
localDateTime: entity.localDateTime,
|
localDateTime: asDateString(entity.localDateTime),
|
||||||
duration: entity.duration ?? '0:00:00.00000',
|
duration: entity.duration ?? '0:00:00.00000',
|
||||||
livePhotoVideoId: entity.livePhotoVideoId,
|
livePhotoVideoId: entity.livePhotoVideoId,
|
||||||
hasMetadata: false,
|
hasMetadata: false,
|
||||||
@@ -257,7 +261,7 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: entity.id,
|
id: entity.id,
|
||||||
createdAt: entity.createdAt,
|
createdAt: asDateString(entity.createdAt),
|
||||||
deviceAssetId: entity.deviceAssetId,
|
deviceAssetId: entity.deviceAssetId,
|
||||||
ownerId: entity.ownerId,
|
ownerId: entity.ownerId,
|
||||||
owner: entity.owner ? mapUser(entity.owner) : undefined,
|
owner: entity.owner ? mapUser(entity.owner) : undefined,
|
||||||
@@ -268,10 +272,10 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset
|
|||||||
originalFileName: entity.originalFileName,
|
originalFileName: entity.originalFileName,
|
||||||
originalMimeType: mimeTypes.lookup(entity.originalFileName),
|
originalMimeType: mimeTypes.lookup(entity.originalFileName),
|
||||||
thumbhash: entity.thumbhash ? hexOrBufferToBase64(entity.thumbhash) : null,
|
thumbhash: entity.thumbhash ? hexOrBufferToBase64(entity.thumbhash) : null,
|
||||||
fileCreatedAt: entity.fileCreatedAt,
|
fileCreatedAt: asDateString(entity.fileCreatedAt),
|
||||||
fileModifiedAt: entity.fileModifiedAt,
|
fileModifiedAt: asDateString(entity.fileModifiedAt),
|
||||||
localDateTime: entity.localDateTime,
|
localDateTime: asDateString(entity.localDateTime),
|
||||||
updatedAt: entity.updatedAt,
|
updatedAt: asDateString(entity.updatedAt),
|
||||||
isFavorite: options.auth?.user.id === entity.ownerId && entity.isFavorite,
|
isFavorite: options.auth?.user.id === entity.ownerId && entity.isFavorite,
|
||||||
isArchived: entity.visibility === AssetVisibility.Archive,
|
isArchived: entity.visibility === AssetVisibility.Archive,
|
||||||
isTrashed: !!entity.deletedAt,
|
isTrashed: !!entity.deletedAt,
|
||||||
@@ -283,7 +287,7 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset
|
|||||||
people: peopleWithFaces(entity.faces, entity.edits, assetDimensions),
|
people: peopleWithFaces(entity.faces, entity.edits, assetDimensions),
|
||||||
unassignedFaces: entity.faces
|
unassignedFaces: entity.faces
|
||||||
?.filter((face) => !face.person)
|
?.filter((face) => !face.person)
|
||||||
.map((a) => mapFacesWithoutPerson(a, entity.edits, assetDimensions)),
|
.map((face) => mapFacesWithoutPerson(face, entity.edits, assetDimensions)),
|
||||||
checksum: hexOrBufferToBase64(entity.checksum)!,
|
checksum: hexOrBufferToBase64(entity.checksum)!,
|
||||||
stack: withStack ? mapStack(entity) : undefined,
|
stack: withStack ? mapStack(entity) : undefined,
|
||||||
isOffline: entity.isOffline,
|
isOffline: entity.isOffline,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
import { Exif } from 'src/database';
|
import { Exif } from 'src/database';
|
||||||
|
import { MaybeDehydrated } from 'src/types';
|
||||||
|
import { asDateString } from 'src/utils/date';
|
||||||
|
|
||||||
export class ExifResponseDto {
|
export class ExifResponseDto {
|
||||||
@ApiPropertyOptional({ description: 'Camera make' })
|
@ApiPropertyOptional({ description: 'Camera make' })
|
||||||
@@ -16,9 +18,9 @@ export class ExifResponseDto {
|
|||||||
@ApiPropertyOptional({ description: 'Image orientation' })
|
@ApiPropertyOptional({ description: 'Image orientation' })
|
||||||
orientation?: string | null = null;
|
orientation?: string | null = null;
|
||||||
@ApiPropertyOptional({ description: 'Original date/time', format: 'date-time' })
|
@ApiPropertyOptional({ description: 'Original date/time', format: 'date-time' })
|
||||||
dateTimeOriginal?: Date | null = null;
|
dateTimeOriginal?: string | null = null;
|
||||||
@ApiPropertyOptional({ description: 'Modification date/time', format: 'date-time' })
|
@ApiPropertyOptional({ description: 'Modification date/time', format: 'date-time' })
|
||||||
modifyDate?: Date | null = null;
|
modifyDate?: string | null = null;
|
||||||
@ApiPropertyOptional({ description: 'Time zone' })
|
@ApiPropertyOptional({ description: 'Time zone' })
|
||||||
timeZone?: string | null = null;
|
timeZone?: string | null = null;
|
||||||
@ApiPropertyOptional({ description: 'Lens model' })
|
@ApiPropertyOptional({ description: 'Lens model' })
|
||||||
@@ -49,7 +51,7 @@ export class ExifResponseDto {
|
|||||||
rating?: number | null = null;
|
rating?: number | null = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapExif(entity: Exif): ExifResponseDto {
|
export function mapExif(entity: MaybeDehydrated<Exif>): ExifResponseDto {
|
||||||
return {
|
return {
|
||||||
make: entity.make,
|
make: entity.make,
|
||||||
model: entity.model,
|
model: entity.model,
|
||||||
@@ -57,8 +59,8 @@ export function mapExif(entity: Exif): ExifResponseDto {
|
|||||||
exifImageHeight: entity.exifImageHeight,
|
exifImageHeight: entity.exifImageHeight,
|
||||||
fileSizeInByte: entity.fileSizeInByte ? Number.parseInt(entity.fileSizeInByte.toString()) : null,
|
fileSizeInByte: entity.fileSizeInByte ? Number.parseInt(entity.fileSizeInByte.toString()) : null,
|
||||||
orientation: entity.orientation,
|
orientation: entity.orientation,
|
||||||
dateTimeOriginal: entity.dateTimeOriginal,
|
dateTimeOriginal: asDateString(entity.dateTimeOriginal),
|
||||||
modifyDate: entity.modifyDate,
|
modifyDate: asDateString(entity.modifyDate),
|
||||||
timeZone: entity.timeZone,
|
timeZone: entity.timeZone,
|
||||||
lensModel: entity.lensModel,
|
lensModel: entity.lensModel,
|
||||||
fNumber: entity.fNumber,
|
fNumber: entity.fNumber,
|
||||||
@@ -80,7 +82,7 @@ export function mapSanitizedExif(entity: Exif): ExifResponseDto {
|
|||||||
return {
|
return {
|
||||||
fileSizeInByte: entity.fileSizeInByte ? Number.parseInt(entity.fileSizeInByte.toString()) : null,
|
fileSizeInByte: entity.fileSizeInByte ? Number.parseInt(entity.fileSizeInByte.toString()) : null,
|
||||||
orientation: entity.orientation,
|
orientation: entity.orientation,
|
||||||
dateTimeOriginal: entity.dateTimeOriginal,
|
dateTimeOriginal: asDateString(entity.dateTimeOriginal),
|
||||||
timeZone: entity.timeZone,
|
timeZone: entity.timeZone,
|
||||||
projectionType: entity.projectionType,
|
projectionType: entity.projectionType,
|
||||||
exifImageWidth: entity.exifImageWidth,
|
exifImageWidth: entity.exifImageWidth,
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import { AuthDto } from 'src/dtos/auth.dto';
|
|||||||
import { AssetEditActionItem } from 'src/dtos/editing.dto';
|
import { AssetEditActionItem } from 'src/dtos/editing.dto';
|
||||||
import { SourceType } from 'src/enum';
|
import { SourceType } from 'src/enum';
|
||||||
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
|
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
|
||||||
import { ImageDimensions } from 'src/types';
|
import { ImageDimensions, MaybeDehydrated } from 'src/types';
|
||||||
import { asDateString } from 'src/utils/date';
|
import { asBirthDateString, asDateString } from 'src/utils/date';
|
||||||
import { transformFaceBoundingBox } from 'src/utils/transform';
|
import { transformFaceBoundingBox } from 'src/utils/transform';
|
||||||
import {
|
import {
|
||||||
IsDateStringFormat,
|
IsDateStringFormat,
|
||||||
@@ -33,7 +33,7 @@ export class PersonCreateDto {
|
|||||||
@MaxDateString(() => DateTime.now(), { message: 'Birth date cannot be in the future' })
|
@MaxDateString(() => DateTime.now(), { message: 'Birth date cannot be in the future' })
|
||||||
@IsDateStringFormat('yyyy-MM-dd')
|
@IsDateStringFormat('yyyy-MM-dd')
|
||||||
@Optional({ nullable: true, emptyToNull: true })
|
@Optional({ nullable: true, emptyToNull: true })
|
||||||
birthDate?: Date | null;
|
birthDate?: string | null;
|
||||||
|
|
||||||
@ValidateBoolean({ optional: true, description: 'Person visibility (hidden)' })
|
@ValidateBoolean({ optional: true, description: 'Person visibility (hidden)' })
|
||||||
isHidden?: boolean;
|
isHidden?: boolean;
|
||||||
@@ -105,8 +105,12 @@ export class PersonResponseDto {
|
|||||||
thumbnailPath!: string;
|
thumbnailPath!: string;
|
||||||
@ApiProperty({ description: 'Is hidden' })
|
@ApiProperty({ description: 'Is hidden' })
|
||||||
isHidden!: boolean;
|
isHidden!: boolean;
|
||||||
@Property({ description: 'Last update date', history: new HistoryBuilder().added('v1.107.0').stable('v2') })
|
@Property({
|
||||||
updatedAt?: Date;
|
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') })
|
@Property({ description: 'Is favorite', history: new HistoryBuilder().added('v1.126.0').stable('v2') })
|
||||||
isFavorite?: boolean;
|
isFavorite?: boolean;
|
||||||
@Property({ description: 'Person color (hex)', history: new HistoryBuilder().added('v1.126.0').stable('v2') })
|
@Property({ description: 'Person color (hex)', history: new HistoryBuilder().added('v1.126.0').stable('v2') })
|
||||||
@@ -222,21 +226,21 @@ export class PeopleResponseDto {
|
|||||||
hasNextPage?: boolean;
|
hasNextPage?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapPerson(person: Person): PersonResponseDto {
|
export function mapPerson(person: MaybeDehydrated<Person>): PersonResponseDto {
|
||||||
return {
|
return {
|
||||||
id: person.id,
|
id: person.id,
|
||||||
name: person.name,
|
name: person.name,
|
||||||
birthDate: asDateString(person.birthDate),
|
birthDate: asBirthDateString(person.birthDate),
|
||||||
thumbnailPath: person.thumbnailPath,
|
thumbnailPath: person.thumbnailPath,
|
||||||
isHidden: person.isHidden,
|
isHidden: person.isHidden,
|
||||||
isFavorite: person.isFavorite,
|
isFavorite: person.isFavorite,
|
||||||
color: person.color ?? undefined,
|
color: person.color ?? undefined,
|
||||||
updatedAt: person.updatedAt,
|
updatedAt: asDateString(person.updatedAt),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapFacesWithoutPerson(
|
export function mapFacesWithoutPerson(
|
||||||
face: Selectable<AssetFaceTable>,
|
face: MaybeDehydrated<Selectable<AssetFaceTable>>,
|
||||||
edits?: AssetEditActionItem[],
|
edits?: AssetEditActionItem[],
|
||||||
assetDimensions?: ImageDimensions,
|
assetDimensions?: ImageDimensions,
|
||||||
): AssetFaceWithoutPersonResponseDto {
|
): AssetFaceWithoutPersonResponseDto {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
import { IsHexColor, IsNotEmpty, IsString } from 'class-validator';
|
import { IsHexColor, IsNotEmpty, IsString } from 'class-validator';
|
||||||
import { Tag } from 'src/database';
|
import { Tag } from 'src/database';
|
||||||
|
import { MaybeDehydrated } from 'src/types';
|
||||||
|
import { asDateString } from 'src/utils/date';
|
||||||
import { Optional, ValidateHexColor, ValidateUUID } from 'src/validation';
|
import { Optional, ValidateHexColor, ValidateUUID } from 'src/validation';
|
||||||
|
|
||||||
export class TagCreateDto {
|
export class TagCreateDto {
|
||||||
@@ -54,22 +56,22 @@ export class TagResponseDto {
|
|||||||
name!: string;
|
name!: string;
|
||||||
@ApiProperty({ description: 'Tag value (full path)' })
|
@ApiProperty({ description: 'Tag value (full path)' })
|
||||||
value!: string;
|
value!: string;
|
||||||
@ApiProperty({ description: 'Creation date' })
|
@ApiProperty({ description: 'Creation date', format: 'date-time' })
|
||||||
createdAt!: Date;
|
createdAt!: string;
|
||||||
@ApiProperty({ description: 'Last update date' })
|
@ApiProperty({ description: 'Last update date', format: 'date-time' })
|
||||||
updatedAt!: Date;
|
updatedAt!: string;
|
||||||
@ApiPropertyOptional({ description: 'Tag color (hex)' })
|
@ApiPropertyOptional({ description: 'Tag color (hex)' })
|
||||||
color?: string;
|
color?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapTag(entity: Tag): TagResponseDto {
|
export function mapTag(entity: MaybeDehydrated<Tag>): TagResponseDto {
|
||||||
return {
|
return {
|
||||||
id: entity.id,
|
id: entity.id,
|
||||||
parentId: entity.parentId ?? undefined,
|
parentId: entity.parentId ?? undefined,
|
||||||
name: entity.value.split('/').at(-1) as string,
|
name: entity.value.split('/').at(-1) as string,
|
||||||
value: entity.value,
|
value: entity.value,
|
||||||
createdAt: entity.createdAt,
|
createdAt: asDateString(entity.createdAt),
|
||||||
updatedAt: entity.updatedAt,
|
updatedAt: asDateString(entity.updatedAt),
|
||||||
color: entity.color ?? undefined,
|
color: entity.color ?? undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { Transform } from 'class-transformer';
|
|||||||
import { IsEmail, IsInt, IsNotEmpty, IsString, Min } from 'class-validator';
|
import { IsEmail, IsInt, IsNotEmpty, IsString, Min } from 'class-validator';
|
||||||
import { User, UserAdmin } from 'src/database';
|
import { User, UserAdmin } from 'src/database';
|
||||||
import { UserAvatarColor, UserMetadataKey, UserStatus } from 'src/enum';
|
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';
|
import { Optional, PinCode, ValidateBoolean, ValidateEnum, ValidateUUID, toEmail, toSanitized } from 'src/validation';
|
||||||
|
|
||||||
export class UserUpdateMeDto {
|
export class UserUpdateMeDto {
|
||||||
@@ -47,8 +48,8 @@ export class UserResponseDto {
|
|||||||
profileImagePath!: string;
|
profileImagePath!: string;
|
||||||
@ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', description: 'Avatar color' })
|
@ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', description: 'Avatar color' })
|
||||||
avatarColor!: UserAvatarColor;
|
avatarColor!: UserAvatarColor;
|
||||||
@ApiProperty({ description: 'Profile change date' })
|
@ApiProperty({ description: 'Profile change date', format: 'date-time' })
|
||||||
profileChangedAt!: Date;
|
profileChangedAt!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UserLicense {
|
export class UserLicense {
|
||||||
@@ -68,14 +69,14 @@ const emailToAvatarColor = (email: string): UserAvatarColor => {
|
|||||||
return values[randomIndex];
|
return values[randomIndex];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mapUser = (entity: User | UserAdmin): UserResponseDto => {
|
export const mapUser = (entity: MaybeDehydrated<User | UserAdmin>): UserResponseDto => {
|
||||||
return {
|
return {
|
||||||
id: entity.id,
|
id: entity.id,
|
||||||
email: entity.email,
|
email: entity.email,
|
||||||
name: entity.name,
|
name: entity.name,
|
||||||
profileImagePath: entity.profileImagePath,
|
profileImagePath: entity.profileImagePath,
|
||||||
avatarColor: entity.avatarColor ?? emailToAvatarColor(entity.email),
|
avatarColor: entity.avatarColor ?? emailToAvatarColor(entity.email),
|
||||||
profileChangedAt: entity.profileChangedAt,
|
profileChangedAt: asDateString(entity.profileChangedAt),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -244,3 +244,37 @@ where
|
|||||||
or "album"."id" is not null
|
or "album"."id" is not null
|
||||||
)
|
)
|
||||||
and "shared_link"."slug" = $2
|
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 { 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 { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||||
import { InjectKysely } from 'nestjs-kysely';
|
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 { Chunked, ChunkedArray, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
|
||||||
import { AlbumUserCreateDto } from 'src/dtos/album.dto';
|
import { AlbumUserCreateDto } from 'src/dtos/album.dto';
|
||||||
import { DB } from 'src/schema';
|
import { DB } from 'src/schema';
|
||||||
import { AlbumTable } from 'src/schema/tables/album.table';
|
import { AlbumTable } from 'src/schema/tables/album.table';
|
||||||
|
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
|
||||||
import { withDefaultVisibility } from 'src/utils/database';
|
import { withDefaultVisibility } from 'src/utils/database';
|
||||||
|
|
||||||
export interface AlbumAssetCount {
|
export interface AlbumAssetCount {
|
||||||
@@ -56,7 +66,9 @@ const withAssets = (eb: ExpressionBuilder<DB, 'album'>) => {
|
|||||||
.selectFrom('asset')
|
.selectFrom('asset')
|
||||||
.selectAll('asset')
|
.selectAll('asset')
|
||||||
.leftJoin('asset_exif', 'asset.id', 'asset_exif.assetId')
|
.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')
|
.innerJoin('album_asset', 'album_asset.assetId', 'asset.id')
|
||||||
.whereRef('album_asset.albumId', '=', 'album.id')
|
.whereRef('album_asset.albumId', '=', 'album.id')
|
||||||
.where('asset.deletedAt', 'is', null)
|
.where('asset.deletedAt', 'is', null)
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { DB } from 'src/schema';
|
|||||||
import {
|
import {
|
||||||
anyUuid,
|
anyUuid,
|
||||||
asUuid,
|
asUuid,
|
||||||
toJson,
|
|
||||||
withDefaultVisibility,
|
withDefaultVisibility,
|
||||||
withEdits,
|
withEdits,
|
||||||
withExif,
|
withExif,
|
||||||
@@ -296,7 +295,12 @@ export class AssetJobRepository {
|
|||||||
.as('stack_result'),
|
.as('stack_result'),
|
||||||
(join) => join.onTrue(),
|
(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)
|
.where('asset.id', '=', id)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
NotNull,
|
NotNull,
|
||||||
Selectable,
|
Selectable,
|
||||||
SelectQueryBuilder,
|
SelectQueryBuilder,
|
||||||
|
ShallowDehydrateObject,
|
||||||
sql,
|
sql,
|
||||||
Updateable,
|
Updateable,
|
||||||
UpdateResult,
|
UpdateResult,
|
||||||
@@ -554,7 +555,11 @@ export class AssetRepository {
|
|||||||
eb
|
eb
|
||||||
.selectFrom('asset as stacked')
|
.selectFrom('asset as stacked')
|
||||||
.selectAll('stack')
|
.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.stackId', '=', 'stack.id')
|
||||||
.whereRef('stacked.id', '!=', 'stack.primaryAssetId')
|
.whereRef('stacked.id', '!=', 'stack.primaryAssetId')
|
||||||
.where('stacked.deletedAt', 'is', null)
|
.where('stacked.deletedAt', 'is', null)
|
||||||
@@ -563,7 +568,7 @@ export class AssetRepository {
|
|||||||
.as('stacked_assets'),
|
.as('stacked_assets'),
|
||||||
(join) => join.on('stack.id', 'is not', null),
|
(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))
|
.$if(!!files, (qb) => qb.select(withFiles))
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
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 { InjectKysely } from 'nestjs-kysely';
|
||||||
import { Chunked, DummyValue, GenerateSql } from 'src/decorators';
|
import { Chunked, DummyValue, GenerateSql } from 'src/decorators';
|
||||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
|
||||||
import { AssetType, VectorIndex } from 'src/enum';
|
import { AssetType, VectorIndex } from 'src/enum';
|
||||||
import { probes } from 'src/repositories/database.repository';
|
import { probes } from 'src/repositories/database.repository';
|
||||||
import { DB } from 'src/schema';
|
import { DB } from 'src/schema';
|
||||||
|
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
|
||||||
import { anyUuid, asUuid, withDefaultVisibility } from 'src/utils/database';
|
import { anyUuid, asUuid, withDefaultVisibility } from 'src/utils/database';
|
||||||
|
|
||||||
interface DuplicateSearch {
|
interface DuplicateSearch {
|
||||||
@@ -39,15 +39,15 @@ export class DuplicateRepository {
|
|||||||
qb
|
qb
|
||||||
.selectFrom('asset_exif')
|
.selectFrom('asset_exif')
|
||||||
.selectAll('asset')
|
.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')
|
.whereRef('asset_exif.assetId', '=', 'asset.id')
|
||||||
.as('asset2'),
|
.as('asset2'),
|
||||||
(join) => join.onTrue(),
|
(join) => join.onTrue(),
|
||||||
)
|
)
|
||||||
.select('asset.duplicateId')
|
.select('asset.duplicateId')
|
||||||
.select((eb) =>
|
.select((eb) => eb.fn.jsonAgg('asset2').orderBy('asset.localDateTime', 'asc').as('assets'))
|
||||||
eb.fn.jsonAgg('asset2').orderBy('asset.localDateTime', 'asc').$castTo<MapAsset[]>().as('assets'),
|
|
||||||
)
|
|
||||||
.where('asset.ownerId', '=', asUuid(userId))
|
.where('asset.ownerId', '=', asUuid(userId))
|
||||||
.where('asset.duplicateId', 'is not', null)
|
.where('asset.duplicateId', 'is not', null)
|
||||||
.$narrowType<{ duplicateId: NotNull }>()
|
.$narrowType<{ duplicateId: NotNull }>()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
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 { InjectKysely } from 'nestjs-kysely';
|
||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||||
@@ -433,7 +433,7 @@ export class SearchRepository {
|
|||||||
.select((eb) =>
|
.select((eb) =>
|
||||||
eb
|
eb
|
||||||
.fn('to_jsonb', [eb.table('asset_exif')])
|
.fn('to_jsonb', [eb.table('asset_exif')])
|
||||||
.$castTo<Selectable<AssetExifTable>>()
|
.$castTo<ShallowDehydrateObject<Selectable<AssetExifTable>>>()
|
||||||
.as('exifInfo'),
|
.as('exifInfo'),
|
||||||
)
|
)
|
||||||
.orderBy('asset_exif.city')
|
.orderBy('asset_exif.city')
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
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 { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { InjectKysely } from 'nestjs-kysely';
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
import { Album, columns } from 'src/database';
|
import { Album, columns } from 'src/database';
|
||||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
|
||||||
import { SharedLinkType } from 'src/enum';
|
import { SharedLinkType } from 'src/enum';
|
||||||
import { DB } from 'src/schema';
|
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';
|
import { SharedLinkTable } from 'src/schema/tables/shared-link.table';
|
||||||
|
|
||||||
export type SharedLinkSearchOptions = {
|
export type SharedLinkSearchOptions = {
|
||||||
@@ -106,11 +107,15 @@ export class SharedLinkRepository {
|
|||||||
.select((eb) =>
|
.select((eb) =>
|
||||||
eb.fn
|
eb.fn
|
||||||
.coalesce(eb.fn.jsonAgg('a').filterWhere('a.id', 'is not', null), sql`'[]'`)
|
.coalesce(eb.fn.jsonAgg('a').filterWhere('a.id', 'is not', null), sql`'[]'`)
|
||||||
.$castTo<MapAsset[]>()
|
.$castTo<
|
||||||
|
(ShallowDehydrateObject<Selectable<AssetTable>> & {
|
||||||
|
exifInfo: ShallowDehydrateObject<Selectable<AssetExifTable>>;
|
||||||
|
})[]
|
||||||
|
>()
|
||||||
.as('assets'),
|
.as('assets'),
|
||||||
)
|
)
|
||||||
.groupBy(['shared_link.id', sql`"album".*`])
|
.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.id', '=', id)
|
||||||
.where('shared_link.userId', '=', userId)
|
.where('shared_link.userId', '=', userId)
|
||||||
.where((eb) => eb.or([eb('shared_link.type', '=', SharedLinkType.Individual), eb('album.id', 'is not', null)]))
|
.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')
|
.selectAll('asset')
|
||||||
.orderBy('asset.fileCreatedAt', 'asc')
|
.orderBy('asset.fileCreatedAt', 'asc')
|
||||||
.limit(1),
|
.limit(1),
|
||||||
)
|
).as('assets'),
|
||||||
.$castTo<MapAsset[]>()
|
|
||||||
.as('assets'),
|
|
||||||
)
|
)
|
||||||
.leftJoinLateral(
|
.leftJoinLateral(
|
||||||
(eb) =>
|
(eb) =>
|
||||||
@@ -175,7 +178,7 @@ export class SharedLinkRepository {
|
|||||||
.as('album'),
|
.as('album'),
|
||||||
(join) => join.onTrue(),
|
(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)]))
|
.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(!!albumId, (eb) => eb.where('shared_link.albumId', '=', albumId!))
|
||||||
.$if(!!id, (eb) => eb.where('shared_link.id', '=', id!))
|
.$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();
|
await this.db.deleteFrom('shared_link').where('shared_link.id', '=', id).execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
private getSharedLinks(id: string) {
|
private getSharedLinks(id: string) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('shared_link')
|
.selectFrom('shared_link')
|
||||||
@@ -269,7 +273,11 @@ export class SharedLinkRepository {
|
|||||||
.select((eb) =>
|
.select((eb) =>
|
||||||
eb.fn
|
eb.fn
|
||||||
.coalesce(eb.fn.jsonAgg('assets').filterWhere('assets.id', 'is not', null), sql`'[]'`)
|
.coalesce(eb.fn.jsonAgg('assets').filterWhere('assets.id', 'is not', null), sql`'[]'`)
|
||||||
.$castTo<MapAsset[]>()
|
.$castTo<
|
||||||
|
(ShallowDehydrateObject<Selectable<AssetTable>> & {
|
||||||
|
exifInfo: ShallowDehydrateObject<Selectable<AssetExifTable>>;
|
||||||
|
})[]
|
||||||
|
>()
|
||||||
.as('assets'),
|
.as('assets'),
|
||||||
)
|
)
|
||||||
.groupBy('shared_link.id')
|
.groupBy('shared_link.id')
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { BadRequestException } from '@nestjs/common';
|
import { BadRequestException } from '@nestjs/common';
|
||||||
import { ReactionType } from 'src/dtos/activity.dto';
|
import { ReactionType } from 'src/dtos/activity.dto';
|
||||||
import { ActivityService } from 'src/services/activity.service';
|
import { ActivityService } from 'src/services/activity.service';
|
||||||
|
import { getForActivity } from 'test/mappers';
|
||||||
import { factory, newUuid, newUuids } from 'test/small.factory';
|
import { factory, newUuid, newUuids } from 'test/small.factory';
|
||||||
import { newTestService, ServiceMocks } from 'test/utils';
|
import { newTestService, ServiceMocks } from 'test/utils';
|
||||||
|
|
||||||
@@ -78,7 +79,7 @@ describe(ActivityService.name, () => {
|
|||||||
const activity = factory.activity({ albumId, assetId, userId });
|
const activity = factory.activity({ albumId, assetId, userId });
|
||||||
|
|
||||||
mocks.access.activity.checkCreateAccess.mockResolvedValue(new Set([albumId]));
|
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 } }), {
|
await sut.create(factory.auth({ user: { id: userId } }), {
|
||||||
albumId,
|
albumId,
|
||||||
@@ -101,7 +102,7 @@ describe(ActivityService.name, () => {
|
|||||||
const activity = factory.activity({ albumId, assetId });
|
const activity = factory.activity({ albumId, assetId });
|
||||||
|
|
||||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumId]));
|
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumId]));
|
||||||
mocks.activity.create.mockResolvedValue(activity);
|
mocks.activity.create.mockResolvedValue(getForActivity(activity));
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
sut.create(factory.auth(), { albumId, assetId, type: ReactionType.COMMENT, comment: 'comment' }),
|
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 });
|
const activity = factory.activity({ userId, albumId, assetId, isLiked: true });
|
||||||
|
|
||||||
mocks.access.activity.checkCreateAccess.mockResolvedValue(new Set([albumId]));
|
mocks.access.activity.checkCreateAccess.mockResolvedValue(new Set([albumId]));
|
||||||
mocks.activity.create.mockResolvedValue(activity);
|
mocks.activity.create.mockResolvedValue(getForActivity(activity));
|
||||||
mocks.activity.search.mockResolvedValue([]);
|
mocks.activity.search.mockResolvedValue([]);
|
||||||
|
|
||||||
await sut.create(factory.auth({ user: { id: userId } }), { albumId, assetId, type: ReactionType.LIKE });
|
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.album.checkOwnerAccess.mockResolvedValue(new Set([albumId]));
|
||||||
mocks.access.activity.checkCreateAccess.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 });
|
await sut.create(factory.auth(), { albumId, assetId, type: ReactionType.LIKE });
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { BadRequestException } from '@nestjs/common';
|
import { BadRequestException } from '@nestjs/common';
|
||||||
import _ from 'lodash';
|
|
||||||
import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto';
|
import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto';
|
||||||
import { AlbumUserRole, AssetOrder, UserMetadataKey } from 'src/enum';
|
import { AlbumUserRole, AssetOrder, UserMetadataKey } from 'src/enum';
|
||||||
import { AlbumService } from 'src/services/album.service';
|
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 { AuthFactory } from 'test/factories/auth.factory';
|
||||||
import { UserFactory } from 'test/factories/user.factory';
|
import { UserFactory } from 'test/factories/user.factory';
|
||||||
import { authStub } from 'test/fixtures/auth.stub';
|
import { authStub } from 'test/fixtures/auth.stub';
|
||||||
|
import { getForAlbum } from 'test/mappers';
|
||||||
import { newUuid } from 'test/small.factory';
|
import { newUuid } from 'test/small.factory';
|
||||||
import { newTestService, ServiceMocks } from 'test/utils';
|
import { newTestService, ServiceMocks } from 'test/utils';
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ describe(AlbumService.name, () => {
|
|||||||
it('gets list of albums for auth user', async () => {
|
it('gets list of albums for auth user', async () => {
|
||||||
const album = AlbumFactory.from().albumUser().build();
|
const album = AlbumFactory.from().albumUser().build();
|
||||||
const sharedWithUserAlbum = AlbumFactory.from().owner(album.owner).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([
|
mocks.album.getMetadataForIds.mockResolvedValue([
|
||||||
{
|
{
|
||||||
albumId: album.id,
|
albumId: album.id,
|
||||||
@@ -70,8 +70,13 @@ describe(AlbumService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('gets list of albums that have a specific asset', async () => {
|
it('gets list of albums that have a specific asset', async () => {
|
||||||
const album = AlbumFactory.from().owner({ isAdmin: true }).albumUser().asset().asset().build();
|
const album = AlbumFactory.from()
|
||||||
mocks.album.getByAssetId.mockResolvedValue([album]);
|
.owner({ isAdmin: true })
|
||||||
|
.albumUser()
|
||||||
|
.asset({}, (builder) => builder.exif())
|
||||||
|
.asset({}, (builder) => builder.exif())
|
||||||
|
.build();
|
||||||
|
mocks.album.getByAssetId.mockResolvedValue([getForAlbum(album)]);
|
||||||
mocks.album.getMetadataForIds.mockResolvedValue([
|
mocks.album.getMetadataForIds.mockResolvedValue([
|
||||||
{
|
{
|
||||||
albumId: album.id,
|
albumId: album.id,
|
||||||
@@ -90,7 +95,7 @@ describe(AlbumService.name, () => {
|
|||||||
|
|
||||||
it('gets list of albums that are shared', async () => {
|
it('gets list of albums that are shared', async () => {
|
||||||
const album = AlbumFactory.from().albumUser().build();
|
const album = AlbumFactory.from().albumUser().build();
|
||||||
mocks.album.getShared.mockResolvedValue([album]);
|
mocks.album.getShared.mockResolvedValue([getForAlbum(album)]);
|
||||||
mocks.album.getMetadataForIds.mockResolvedValue([
|
mocks.album.getMetadataForIds.mockResolvedValue([
|
||||||
{
|
{
|
||||||
albumId: album.id,
|
albumId: album.id,
|
||||||
@@ -109,7 +114,7 @@ describe(AlbumService.name, () => {
|
|||||||
|
|
||||||
it('gets list of albums that are NOT shared', async () => {
|
it('gets list of albums that are NOT shared', async () => {
|
||||||
const album = AlbumFactory.create();
|
const album = AlbumFactory.create();
|
||||||
mocks.album.getNotShared.mockResolvedValue([album]);
|
mocks.album.getNotShared.mockResolvedValue([getForAlbum(album)]);
|
||||||
mocks.album.getMetadataForIds.mockResolvedValue([
|
mocks.album.getMetadataForIds.mockResolvedValue([
|
||||||
{
|
{
|
||||||
albumId: album.id,
|
albumId: album.id,
|
||||||
@@ -129,7 +134,7 @@ describe(AlbumService.name, () => {
|
|||||||
|
|
||||||
it('counts assets correctly', async () => {
|
it('counts assets correctly', async () => {
|
||||||
const album = AlbumFactory.create();
|
const album = AlbumFactory.create();
|
||||||
mocks.album.getOwned.mockResolvedValue([album]);
|
mocks.album.getOwned.mockResolvedValue([getForAlbum(album)]);
|
||||||
mocks.album.getMetadataForIds.mockResolvedValue([
|
mocks.album.getMetadataForIds.mockResolvedValue([
|
||||||
{
|
{
|
||||||
albumId: album.id,
|
albumId: album.id,
|
||||||
@@ -155,7 +160,7 @@ describe(AlbumService.name, () => {
|
|||||||
.albumUser(albumUser)
|
.albumUser(albumUser)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
mocks.album.create.mockResolvedValue(album);
|
mocks.album.create.mockResolvedValue(getForAlbum(album));
|
||||||
mocks.user.get.mockResolvedValue(UserFactory.create(album.albumUsers[0].user));
|
mocks.user.get.mockResolvedValue(UserFactory.create(album.albumUsers[0].user));
|
||||||
mocks.user.getMetadata.mockResolvedValue([]);
|
mocks.user.getMetadata.mockResolvedValue([]);
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId]));
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId]));
|
||||||
@@ -192,7 +197,7 @@ describe(AlbumService.name, () => {
|
|||||||
.asset({ id: assetId }, (asset) => asset.exif())
|
.asset({ id: assetId }, (asset) => asset.exif())
|
||||||
.albumUser(albumUser)
|
.albumUser(albumUser)
|
||||||
.build();
|
.build();
|
||||||
mocks.album.create.mockResolvedValue(album);
|
mocks.album.create.mockResolvedValue(getForAlbum(album));
|
||||||
mocks.user.get.mockResolvedValue(album.albumUsers[0].user);
|
mocks.user.get.mockResolvedValue(album.albumUsers[0].user);
|
||||||
mocks.user.getMetadata.mockResolvedValue([
|
mocks.user.getMetadata.mockResolvedValue([
|
||||||
{
|
{
|
||||||
@@ -250,7 +255,7 @@ describe(AlbumService.name, () => {
|
|||||||
.albumUser()
|
.albumUser()
|
||||||
.build();
|
.build();
|
||||||
mocks.user.get.mockResolvedValue(album.albumUsers[0].user);
|
mocks.user.get.mockResolvedValue(album.albumUsers[0].user);
|
||||||
mocks.album.create.mockResolvedValue(album);
|
mocks.album.create.mockResolvedValue(getForAlbum(album));
|
||||||
mocks.user.getMetadata.mockResolvedValue([]);
|
mocks.user.getMetadata.mockResolvedValue([]);
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId]));
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId]));
|
||||||
|
|
||||||
@@ -316,7 +321,7 @@ describe(AlbumService.name, () => {
|
|||||||
it('should require a valid thumbnail asset id', async () => {
|
it('should require a valid thumbnail asset id', async () => {
|
||||||
const album = AlbumFactory.create();
|
const album = AlbumFactory.create();
|
||||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
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());
|
mocks.album.getAssetIds.mockResolvedValue(new Set());
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
@@ -330,8 +335,8 @@ describe(AlbumService.name, () => {
|
|||||||
it('should allow the owner to update the album', async () => {
|
it('should allow the owner to update the album', async () => {
|
||||||
const album = AlbumFactory.create();
|
const album = AlbumFactory.create();
|
||||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||||
mocks.album.getById.mockResolvedValue(album);
|
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||||
mocks.album.update.mockResolvedValue(album);
|
mocks.album.update.mockResolvedValue(getForAlbum(album));
|
||||||
|
|
||||||
await sut.update(AuthFactory.create(album.owner), album.id, { albumName: 'new album name' });
|
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 () => {
|
it('should not let a shared user delete the album', async () => {
|
||||||
const album = AlbumFactory.create();
|
const album = AlbumFactory.create();
|
||||||
mocks.album.getById.mockResolvedValue(album);
|
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set());
|
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set());
|
||||||
|
|
||||||
await expect(sut.delete(AuthFactory.create(album.owner), album.id)).rejects.toBeInstanceOf(BadRequestException);
|
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 () => {
|
it('should let the owner delete an album', async () => {
|
||||||
const album = AlbumFactory.create();
|
const album = AlbumFactory.create();
|
||||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
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);
|
await sut.delete(AuthFactory.create(album.owner), album.id);
|
||||||
|
|
||||||
@@ -387,7 +392,7 @@ describe(AlbumService.name, () => {
|
|||||||
const userId = newUuid();
|
const userId = newUuid();
|
||||||
const album = AlbumFactory.from().albumUser({ userId }).build();
|
const album = AlbumFactory.from().albumUser({ userId }).build();
|
||||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||||
mocks.album.getById.mockResolvedValue(album);
|
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||||
await expect(
|
await expect(
|
||||||
sut.addUsers(AuthFactory.create(album.owner), album.id, { albumUsers: [{ userId }] }),
|
sut.addUsers(AuthFactory.create(album.owner), album.id, { albumUsers: [{ userId }] }),
|
||||||
).rejects.toBeInstanceOf(BadRequestException);
|
).rejects.toBeInstanceOf(BadRequestException);
|
||||||
@@ -398,7 +403,7 @@ describe(AlbumService.name, () => {
|
|||||||
it('should throw an error if the userId does not exist', async () => {
|
it('should throw an error if the userId does not exist', async () => {
|
||||||
const album = AlbumFactory.create();
|
const album = AlbumFactory.create();
|
||||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
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);
|
mocks.user.get.mockResolvedValue(void 0);
|
||||||
await expect(
|
await expect(
|
||||||
sut.addUsers(AuthFactory.create(album.owner), album.id, { albumUsers: [{ userId: 'unknown-user' }] }),
|
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 () => {
|
it('should throw an error if the userId is the ownerId', async () => {
|
||||||
const album = AlbumFactory.create();
|
const album = AlbumFactory.create();
|
||||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||||
mocks.album.getById.mockResolvedValue(album);
|
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||||
await expect(
|
await expect(
|
||||||
sut.addUsers(AuthFactory.create(album.owner), album.id, {
|
sut.addUsers(AuthFactory.create(album.owner), album.id, {
|
||||||
albumUsers: [{ userId: album.owner.id }],
|
albumUsers: [{ userId: album.owner.id }],
|
||||||
@@ -424,8 +429,8 @@ describe(AlbumService.name, () => {
|
|||||||
const album = AlbumFactory.create();
|
const album = AlbumFactory.create();
|
||||||
const user = UserFactory.create();
|
const user = UserFactory.create();
|
||||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||||
mocks.album.getById.mockResolvedValue(album);
|
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||||
mocks.album.update.mockResolvedValue(album);
|
mocks.album.update.mockResolvedValue(getForAlbum(album));
|
||||||
mocks.user.get.mockResolvedValue(user);
|
mocks.user.get.mockResolvedValue(user);
|
||||||
mocks.albumUser.create.mockResolvedValue(AlbumUserFactory.from().album(album).user(user).build());
|
mocks.albumUser.create.mockResolvedValue(AlbumUserFactory.from().album(album).user(user).build());
|
||||||
|
|
||||||
@@ -456,7 +461,7 @@ describe(AlbumService.name, () => {
|
|||||||
const userId = newUuid();
|
const userId = newUuid();
|
||||||
const album = AlbumFactory.from().albumUser({ userId }).build();
|
const album = AlbumFactory.from().albumUser({ userId }).build();
|
||||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||||
mocks.album.getById.mockResolvedValue(album);
|
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||||
mocks.albumUser.delete.mockResolvedValue();
|
mocks.albumUser.delete.mockResolvedValue();
|
||||||
|
|
||||||
await expect(sut.removeUser(AuthFactory.create(album.owner), album.id, userId)).resolves.toBeUndefined();
|
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 user1 = UserFactory.create();
|
||||||
const user2 = UserFactory.create();
|
const user2 = UserFactory.create();
|
||||||
const album = AlbumFactory.from().albumUser({ userId: user1.id }).albumUser({ userId: user2.id }).build();
|
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(
|
await expect(sut.removeUser(AuthFactory.create(user1), album.id, user2.id)).rejects.toBeInstanceOf(
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
@@ -483,7 +488,7 @@ describe(AlbumService.name, () => {
|
|||||||
it('should allow a shared user to remove themselves', async () => {
|
it('should allow a shared user to remove themselves', async () => {
|
||||||
const user1 = UserFactory.create();
|
const user1 = UserFactory.create();
|
||||||
const album = AlbumFactory.from().albumUser({ userId: user1.id }).build();
|
const album = AlbumFactory.from().albumUser({ userId: user1.id }).build();
|
||||||
mocks.album.getById.mockResolvedValue(album);
|
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||||
mocks.albumUser.delete.mockResolvedValue();
|
mocks.albumUser.delete.mockResolvedValue();
|
||||||
|
|
||||||
await sut.removeUser(AuthFactory.create(user1), album.id, user1.id);
|
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 () => {
|
it('should allow a shared user to remove themselves using "me"', async () => {
|
||||||
const user = UserFactory.create();
|
const user = UserFactory.create();
|
||||||
const album = AlbumFactory.from().albumUser({ userId: user.id }).build();
|
const album = AlbumFactory.from().albumUser({ userId: user.id }).build();
|
||||||
mocks.album.getById.mockResolvedValue(album);
|
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||||
mocks.albumUser.delete.mockResolvedValue();
|
mocks.albumUser.delete.mockResolvedValue();
|
||||||
|
|
||||||
await sut.removeUser(AuthFactory.create(user), album.id, 'me');
|
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 () => {
|
it('should not allow the owner to be removed', async () => {
|
||||||
const album = AlbumFactory.from().albumUser().build();
|
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(
|
await expect(sut.removeUser(AuthFactory.create(album.owner), album.id, album.owner.id)).rejects.toBeInstanceOf(
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
@@ -517,7 +522,7 @@ describe(AlbumService.name, () => {
|
|||||||
|
|
||||||
it('should throw an error for a user not in the album', async () => {
|
it('should throw an error for a user not in the album', async () => {
|
||||||
const album = AlbumFactory.from().albumUser().build();
|
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(
|
await expect(sut.removeUser(AuthFactory.create(album.owner), album.id, 'user-3')).rejects.toBeInstanceOf(
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
@@ -546,7 +551,7 @@ describe(AlbumService.name, () => {
|
|||||||
describe('getAlbumInfo', () => {
|
describe('getAlbumInfo', () => {
|
||||||
it('should get a shared album', async () => {
|
it('should get a shared album', async () => {
|
||||||
const album = AlbumFactory.from().albumUser().build();
|
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.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||||
mocks.album.getMetadataForIds.mockResolvedValue([
|
mocks.album.getMetadataForIds.mockResolvedValue([
|
||||||
{
|
{
|
||||||
@@ -566,7 +571,7 @@ describe(AlbumService.name, () => {
|
|||||||
|
|
||||||
it('should get a shared album via a shared link', async () => {
|
it('should get a shared album via a shared link', async () => {
|
||||||
const album = AlbumFactory.from().albumUser().build();
|
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.access.album.checkSharedLinkAccess.mockResolvedValue(new Set([album.id]));
|
||||||
mocks.album.getMetadataForIds.mockResolvedValue([
|
mocks.album.getMetadataForIds.mockResolvedValue([
|
||||||
{
|
{
|
||||||
@@ -588,7 +593,7 @@ describe(AlbumService.name, () => {
|
|||||||
it('should get a shared album via shared with user', async () => {
|
it('should get a shared album via shared with user', async () => {
|
||||||
const user = UserFactory.create();
|
const user = UserFactory.create();
|
||||||
const album = AlbumFactory.from().albumUser({ userId: user.id }).build();
|
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.access.album.checkSharedAlbumAccess.mockResolvedValue(new Set([album.id]));
|
||||||
mocks.album.getMetadataForIds.mockResolvedValue([
|
mocks.album.getMetadataForIds.mockResolvedValue([
|
||||||
{
|
{
|
||||||
@@ -630,7 +635,7 @@ describe(AlbumService.name, () => {
|
|||||||
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
|
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
|
||||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.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());
|
mocks.album.getAssetIds.mockResolvedValueOnce(new Set());
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
@@ -654,7 +659,7 @@ describe(AlbumService.name, () => {
|
|||||||
const album = AlbumFactory.from({ albumThumbnailAssetId: asset1.id }).build();
|
const album = AlbumFactory.from({ albumThumbnailAssetId: asset1.id }).build();
|
||||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset2.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());
|
mocks.album.getAssetIds.mockResolvedValueOnce(new Set());
|
||||||
|
|
||||||
await expect(sut.addAssets(AuthFactory.create(album.owner), album.id, { ids: [asset2.id] })).resolves.toEqual([
|
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()];
|
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
|
||||||
mocks.access.album.checkSharedAlbumAccess.mockResolvedValue(new Set([album.id]));
|
mocks.access.album.checkSharedAlbumAccess.mockResolvedValue(new Set([album.id]));
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.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());
|
mocks.album.getAssetIds.mockResolvedValueOnce(new Set());
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
@@ -703,7 +708,7 @@ describe(AlbumService.name, () => {
|
|||||||
const album = AlbumFactory.from().albumUser({ userId: user.id, role: AlbumUserRole.Viewer }).build();
|
const album = AlbumFactory.from().albumUser({ userId: user.id, role: AlbumUserRole.Viewer }).build();
|
||||||
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
|
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
|
||||||
mocks.access.album.checkSharedAlbumAccess.mockResolvedValue(new Set());
|
mocks.access.album.checkSharedAlbumAccess.mockResolvedValue(new Set());
|
||||||
mocks.album.getById.mockResolvedValue(album);
|
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
sut.addAssets(AuthFactory.create(user), album.id, { ids: [asset1.id, asset2.id, asset3.id] }),
|
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();
|
const auth = AuthFactory.from(album.owner).sharedLink({ allowUpload: true, userId: album.ownerId }).build();
|
||||||
mocks.access.album.checkSharedLinkAccess.mockResolvedValue(new Set([album.id]));
|
mocks.access.album.checkSharedLinkAccess.mockResolvedValue(new Set([album.id]));
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.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());
|
mocks.album.getAssetIds.mockResolvedValueOnce(new Set());
|
||||||
|
|
||||||
await expect(sut.addAssets(auth, album.id, { ids: [asset1.id, asset2.id, asset3.id] })).resolves.toEqual([
|
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();
|
const asset = AssetFactory.create();
|
||||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||||
mocks.access.asset.checkPartnerAccess.mockResolvedValue(new Set([asset.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());
|
mocks.album.getAssetIds.mockResolvedValueOnce(new Set());
|
||||||
|
|
||||||
await expect(sut.addAssets(AuthFactory.create(album.owner), album.id, { ids: [asset.id] })).resolves.toEqual([
|
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();
|
const album = AlbumFactory.create();
|
||||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.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]));
|
mocks.album.getAssetIds.mockResolvedValueOnce(new Set([asset.id]));
|
||||||
|
|
||||||
await expect(sut.addAssets(AuthFactory.create(album.owner), album.id, { ids: [asset.id] })).resolves.toEqual([
|
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 asset = AssetFactory.create();
|
||||||
const album = AlbumFactory.create();
|
const album = AlbumFactory.create();
|
||||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
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());
|
mocks.album.getAssetIds.mockResolvedValueOnce(new Set());
|
||||||
|
|
||||||
await expect(sut.addAssets(AuthFactory.create(album.owner), album.id, { ids: [asset.id] })).resolves.toEqual([
|
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 user = UserFactory.create();
|
||||||
const album = AlbumFactory.create();
|
const album = AlbumFactory.create();
|
||||||
const asset = AssetFactory.create({ ownerId: user.id });
|
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(
|
await expect(sut.addAssets(AuthFactory.create(user), album.id, { ids: [asset.id] })).rejects.toBeInstanceOf(
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
@@ -804,7 +809,7 @@ describe(AlbumService.name, () => {
|
|||||||
it('should not allow unauthorized shared link access to the album', async () => {
|
it('should not allow unauthorized shared link access to the album', async () => {
|
||||||
const album = AlbumFactory.create();
|
const album = AlbumFactory.create();
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.album.getById.mockResolvedValue(album);
|
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
sut.addAssets(AuthFactory.from().sharedLink({ allowUpload: true }).build(), album.id, { ids: [asset.id] }),
|
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()];
|
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
|
||||||
mocks.access.album.checkOwnerAccess.mockResolvedValueOnce(new Set([album1.id, album2.id]));
|
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.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());
|
mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set());
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
@@ -859,7 +864,7 @@ describe(AlbumService.name, () => {
|
|||||||
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
|
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
|
||||||
mocks.access.album.checkOwnerAccess.mockResolvedValueOnce(new Set([album1.id, album2.id]));
|
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.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());
|
mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set());
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
@@ -897,7 +902,7 @@ describe(AlbumService.name, () => {
|
|||||||
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
|
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
|
||||||
mocks.access.album.checkSharedAlbumAccess.mockResolvedValueOnce(new Set([album1.id, album2.id]));
|
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.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());
|
mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set());
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
@@ -943,7 +948,7 @@ describe(AlbumService.name, () => {
|
|||||||
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
|
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
|
||||||
mocks.access.album.checkSharedAlbumAccess.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set());
|
mocks.access.album.checkSharedAlbumAccess.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set());
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.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());
|
mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set());
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
@@ -965,7 +970,7 @@ describe(AlbumService.name, () => {
|
|||||||
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
|
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
|
||||||
mocks.access.album.checkSharedLinkAccess.mockResolvedValueOnce(new Set([album1.id]));
|
mocks.access.album.checkSharedLinkAccess.mockResolvedValueOnce(new Set([album1.id]));
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.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());
|
mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set());
|
||||||
|
|
||||||
const auth = AuthFactory.from(album1.owner).sharedLink({ allowUpload: true }).build();
|
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.album.checkOwnerAccess.mockResolvedValueOnce(new Set([album1.id, album2.id]));
|
||||||
mocks.access.asset.checkPartnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.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());
|
mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set());
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
@@ -1048,7 +1053,7 @@ describe(AlbumService.name, () => {
|
|||||||
mocks.album.getAssetIds
|
mocks.album.getAssetIds
|
||||||
.mockResolvedValueOnce(new Set([asset1.id, asset2.id, asset3.id]))
|
.mockResolvedValueOnce(new Set([asset1.id, asset2.id, asset3.id]))
|
||||||
.mockResolvedValueOnce(new Set());
|
.mockResolvedValueOnce(new Set());
|
||||||
mocks.album.getById.mockResolvedValueOnce(album1).mockResolvedValueOnce(album2);
|
mocks.album.getById.mockResolvedValueOnce(getForAlbum(album1)).mockResolvedValueOnce(getForAlbum(album2));
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
sut.addAssetsToAlbums(AuthFactory.create(album1.owner), {
|
sut.addAssetsToAlbums(AuthFactory.create(album1.owner), {
|
||||||
@@ -1078,7 +1083,7 @@ describe(AlbumService.name, () => {
|
|||||||
.mockResolvedValueOnce(new Set([album1.id]))
|
.mockResolvedValueOnce(new Set([album1.id]))
|
||||||
.mockResolvedValueOnce(new Set([album2.id]));
|
.mockResolvedValueOnce(new Set([album2.id]));
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.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]));
|
mocks.album.getAssetIds.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id]));
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
@@ -1107,7 +1112,7 @@ describe(AlbumService.name, () => {
|
|||||||
mocks.access.album.checkSharedAlbumAccess
|
mocks.access.album.checkSharedAlbumAccess
|
||||||
.mockResolvedValueOnce(new Set([album1.id]))
|
.mockResolvedValueOnce(new Set([album1.id]))
|
||||||
.mockResolvedValueOnce(new Set([album2.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());
|
mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set());
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
@@ -1138,7 +1143,7 @@ describe(AlbumService.name, () => {
|
|||||||
const album1 = AlbumFactory.create();
|
const album1 = AlbumFactory.create();
|
||||||
const album2 = AlbumFactory.create();
|
const album2 = AlbumFactory.create();
|
||||||
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.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(
|
await expect(
|
||||||
sut.addAssetsToAlbums(AuthFactory.create(user), {
|
sut.addAssetsToAlbums(AuthFactory.create(user), {
|
||||||
@@ -1160,7 +1165,7 @@ describe(AlbumService.name, () => {
|
|||||||
const album1 = AlbumFactory.create();
|
const album1 = AlbumFactory.create();
|
||||||
const album2 = AlbumFactory.create();
|
const album2 = AlbumFactory.create();
|
||||||
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.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(
|
await expect(
|
||||||
sut.addAssetsToAlbums(AuthFactory.from().sharedLink({ allowUpload: true }).build(), {
|
sut.addAssetsToAlbums(AuthFactory.from().sharedLink({ allowUpload: true }).build(), {
|
||||||
@@ -1182,7 +1187,7 @@ describe(AlbumService.name, () => {
|
|||||||
const album = AlbumFactory.create();
|
const album = AlbumFactory.create();
|
||||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.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]));
|
mocks.album.getAssetIds.mockResolvedValue(new Set([asset.id]));
|
||||||
|
|
||||||
await expect(sut.removeAssets(AuthFactory.create(album.owner), album.id, { ids: [asset.id] })).resolves.toEqual([
|
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 asset = AssetFactory.create();
|
||||||
const album = AlbumFactory.create();
|
const album = AlbumFactory.create();
|
||||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
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());
|
mocks.album.getAssetIds.mockResolvedValue(new Set());
|
||||||
|
|
||||||
await expect(sut.removeAssets(AuthFactory.create(album.owner), album.id, { ids: [asset.id] })).resolves.toEqual([
|
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 asset = AssetFactory.create();
|
||||||
const album = AlbumFactory.create();
|
const album = AlbumFactory.create();
|
||||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
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]));
|
mocks.album.getAssetIds.mockResolvedValue(new Set([asset.id]));
|
||||||
|
|
||||||
await expect(sut.removeAssets(AuthFactory.create(album.owner), album.id, { ids: [asset.id] })).resolves.toEqual([
|
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();
|
const album = AlbumFactory.from({ albumThumbnailAssetId: asset1.id }).build();
|
||||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.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]));
|
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([
|
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 { AlbumAssetCount, AlbumInfoOptions } from 'src/repositories/album.repository';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { addAssets, removeAssets } from 'src/utils/asset.util';
|
import { addAssets, removeAssets } from 'src/utils/asset.util';
|
||||||
|
import { asDateString } from 'src/utils/date';
|
||||||
import { getPreferences } from 'src/utils/preferences';
|
import { getPreferences } from 'src/utils/preferences';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -64,11 +65,11 @@ export class AlbumService extends BaseService {
|
|||||||
return albums.map((album) => ({
|
return albums.map((album) => ({
|
||||||
...mapAlbumWithoutAssets(album),
|
...mapAlbumWithoutAssets(album),
|
||||||
sharedLinks: undefined,
|
sharedLinks: undefined,
|
||||||
startDate: albumMetadata[album.id]?.startDate ?? undefined,
|
startDate: asDateString(albumMetadata[album.id]?.startDate ?? undefined),
|
||||||
endDate: albumMetadata[album.id]?.endDate ?? undefined,
|
endDate: asDateString(albumMetadata[album.id]?.endDate ?? undefined),
|
||||||
assetCount: albumMetadata[album.id]?.assetCount ?? 0,
|
assetCount: albumMetadata[album.id]?.assetCount ?? 0,
|
||||||
// lastModifiedAssetTimestamp is only used in mobile app, please remove if not need
|
// 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 {
|
return {
|
||||||
...mapAlbum(album, withAssets, auth),
|
...mapAlbum(album, withAssets, auth),
|
||||||
startDate: albumMetadataForIds?.startDate ?? undefined,
|
startDate: asDateString(albumMetadataForIds?.startDate ?? undefined),
|
||||||
endDate: albumMetadataForIds?.endDate ?? undefined,
|
endDate: asDateString(albumMetadataForIds?.endDate ?? undefined),
|
||||||
assetCount: albumMetadataForIds?.assetCount ?? 0,
|
assetCount: albumMetadataForIds?.assetCount ?? 0,
|
||||||
lastModifiedAssetTimestamp: albumMetadataForIds?.lastModifiedAssetTimestamp ?? undefined,
|
lastModifiedAssetTimestamp: asDateString(albumMetadataForIds?.lastModifiedAssetTimestamp ?? undefined),
|
||||||
contributorCounts: isShared ? await this.albumRepository.getContributorCounts(album.id) : undefined,
|
contributorCounts: isShared ? await this.albumRepository.getContributorCounts(album.id) : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,12 @@ import {
|
|||||||
NotFoundException,
|
NotFoundException,
|
||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { Stats } from 'node:fs';
|
|
||||||
import { AssetFile } from 'src/database';
|
import { AssetFile } from 'src/database';
|
||||||
import { AssetMediaStatus, AssetRejectReason, AssetUploadAction } from 'src/dtos/asset-media-response.dto';
|
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 { MapAsset } from 'src/dtos/asset-response.dto';
|
||||||
import { AssetEditAction } from 'src/dtos/editing.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 { AuthRequest } from 'src/middleware/auth.guard';
|
||||||
import { AssetMediaService } from 'src/services/asset-media.service';
|
import { AssetMediaService } from 'src/services/asset-media.service';
|
||||||
import { UploadBody } from 'src/types';
|
import { UploadBody } from 'src/types';
|
||||||
@@ -22,6 +21,7 @@ import { AuthFactory } from 'test/factories/auth.factory';
|
|||||||
import { authStub } from 'test/fixtures/auth.stub';
|
import { authStub } from 'test/fixtures/auth.stub';
|
||||||
import { fileStub } from 'test/fixtures/file.stub';
|
import { fileStub } from 'test/fixtures/file.stub';
|
||||||
import { userStub } from 'test/fixtures/user.stub';
|
import { userStub } from 'test/fixtures/user.stub';
|
||||||
|
import { getForAsset } from 'test/mappers';
|
||||||
import { newTestService, ServiceMocks } from 'test/utils';
|
import { newTestService, ServiceMocks } from 'test/utils';
|
||||||
|
|
||||||
const file1 = Buffer.from('d2947b871a706081be194569951b7db246907957', 'hex');
|
const file1 = Buffer.from('d2947b871a706081be194569951b7db246907957', 'hex');
|
||||||
@@ -152,13 +152,6 @@ const createDto = Object.freeze({
|
|||||||
duration: '0:00:00.000000',
|
duration: '0:00:00.000000',
|
||||||
}) as AssetMediaCreateDto;
|
}) 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({
|
const assetEntity = Object.freeze({
|
||||||
id: 'id_1',
|
id: 'id_1',
|
||||||
ownerId: 'user_id_1',
|
ownerId: 'user_id_1',
|
||||||
@@ -180,25 +173,6 @@ const assetEntity = Object.freeze({
|
|||||||
livePhotoVideoId: null,
|
livePhotoVideoId: null,
|
||||||
} as MapAsset);
|
} 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, () => {
|
describe(AssetMediaService.name, () => {
|
||||||
let sut: AssetMediaService;
|
let sut: AssetMediaService;
|
||||||
let mocks: ServiceMocks;
|
let mocks: ServiceMocks;
|
||||||
@@ -434,7 +408,7 @@ describe(AssetMediaService.name, () => {
|
|||||||
.owner(authStub.user1.user)
|
.owner(authStub.user1.user)
|
||||||
.build();
|
.build();
|
||||||
const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id });
|
const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id });
|
||||||
mocks.asset.getById.mockResolvedValueOnce(motionAsset);
|
mocks.asset.getById.mockResolvedValueOnce(getForAsset(motionAsset));
|
||||||
mocks.asset.create.mockResolvedValueOnce(asset);
|
mocks.asset.create.mockResolvedValueOnce(asset);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
@@ -451,7 +425,7 @@ describe(AssetMediaService.name, () => {
|
|||||||
it('should hide the linked motion asset', async () => {
|
it('should hide the linked motion asset', async () => {
|
||||||
const motionAsset = AssetFactory.from({ type: AssetType.Video }).owner(authStub.user1.user).build();
|
const motionAsset = AssetFactory.from({ type: AssetType.Video }).owner(authStub.user1.user).build();
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.asset.getById.mockResolvedValueOnce(motionAsset);
|
mocks.asset.getById.mockResolvedValueOnce(getForAsset(motionAsset));
|
||||||
mocks.asset.create.mockResolvedValueOnce(asset);
|
mocks.asset.create.mockResolvedValueOnce(asset);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
@@ -470,7 +444,7 @@ describe(AssetMediaService.name, () => {
|
|||||||
|
|
||||||
it('should handle a sidecar file', async () => {
|
it('should handle a sidecar file', async () => {
|
||||||
const asset = AssetFactory.from().file({ type: AssetFileType.Sidecar }).build();
|
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);
|
mocks.asset.create.mockResolvedValueOnce(asset);
|
||||||
|
|
||||||
await expect(sut.uploadAsset(authStub.user1, createDto, fileStub.photo, fileStub.photoSidecar)).resolves.toEqual({
|
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', () => {
|
describe('bulkUploadCheck', () => {
|
||||||
it('should accept hex and base64 checksums', async () => {
|
it('should accept hex and base64 checksums', async () => {
|
||||||
const file1 = Buffer.from('d2947b871a706081be194569951b7db246907957', 'hex');
|
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 { AssetFactory } from 'test/factories/asset.factory';
|
||||||
import { AuthFactory } from 'test/factories/auth.factory';
|
import { AuthFactory } from 'test/factories/auth.factory';
|
||||||
import { authStub } from 'test/fixtures/auth.stub';
|
import { authStub } from 'test/fixtures/auth.stub';
|
||||||
|
import { getForAsset, getForAssetDeletion, getForPartner } from 'test/mappers';
|
||||||
import { factory, newUuid } from 'test/small.factory';
|
import { factory, newUuid } from 'test/small.factory';
|
||||||
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
||||||
|
|
||||||
@@ -71,7 +72,7 @@ describe(AssetService.name, () => {
|
|||||||
describe('getRandom', () => {
|
describe('getRandom', () => {
|
||||||
it('should get own random assets', async () => {
|
it('should get own random assets', async () => {
|
||||||
mocks.partner.getAll.mockResolvedValue([]);
|
mocks.partner.getAll.mockResolvedValue([]);
|
||||||
mocks.asset.getRandom.mockResolvedValue([AssetFactory.create()]);
|
mocks.asset.getRandom.mockResolvedValue([getForAsset(AssetFactory.create())]);
|
||||||
|
|
||||||
await sut.getRandom(authStub.admin, 1);
|
await sut.getRandom(authStub.admin, 1);
|
||||||
|
|
||||||
@@ -82,8 +83,8 @@ describe(AssetService.name, () => {
|
|||||||
const partner = factory.partner({ inTimeline: false });
|
const partner = factory.partner({ inTimeline: false });
|
||||||
const auth = factory.auth({ user: { id: partner.sharedWithId } });
|
const auth = factory.auth({ user: { id: partner.sharedWithId } });
|
||||||
|
|
||||||
mocks.asset.getRandom.mockResolvedValue([AssetFactory.create()]);
|
mocks.asset.getRandom.mockResolvedValue([getForAsset(AssetFactory.create())]);
|
||||||
mocks.partner.getAll.mockResolvedValue([partner]);
|
mocks.partner.getAll.mockResolvedValue([getForPartner(partner)]);
|
||||||
|
|
||||||
await sut.getRandom(auth, 1);
|
await sut.getRandom(auth, 1);
|
||||||
|
|
||||||
@@ -94,8 +95,8 @@ describe(AssetService.name, () => {
|
|||||||
const partner = factory.partner({ inTimeline: true });
|
const partner = factory.partner({ inTimeline: true });
|
||||||
const auth = factory.auth({ user: { id: partner.sharedWithId } });
|
const auth = factory.auth({ user: { id: partner.sharedWithId } });
|
||||||
|
|
||||||
mocks.asset.getRandom.mockResolvedValue([AssetFactory.create()]);
|
mocks.asset.getRandom.mockResolvedValue([getForAsset(AssetFactory.create())]);
|
||||||
mocks.partner.getAll.mockResolvedValue([partner]);
|
mocks.partner.getAll.mockResolvedValue([getForPartner(partner)]);
|
||||||
|
|
||||||
await sut.getRandom(auth, 1);
|
await sut.getRandom(auth, 1);
|
||||||
|
|
||||||
@@ -107,7 +108,7 @@ describe(AssetService.name, () => {
|
|||||||
it('should allow owner access', async () => {
|
it('should allow owner access', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
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);
|
await sut.get(authStub.admin, asset.id);
|
||||||
|
|
||||||
@@ -121,7 +122,7 @@ describe(AssetService.name, () => {
|
|||||||
it('should allow shared link access', async () => {
|
it('should allow shared link access', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set([asset.id]));
|
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);
|
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 () => {
|
it('should strip metadata for shared link if exif is disabled', async () => {
|
||||||
const asset = AssetFactory.from().exif({ description: 'foo' }).build();
|
const asset = AssetFactory.from().exif({ description: 'foo' }).build();
|
||||||
mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set([asset.id]));
|
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(
|
const result = await sut.get(
|
||||||
{ ...authStub.adminSharedLink, sharedLink: { ...authStub.adminSharedLink.sharedLink!, showExif: false } },
|
{ ...authStub.adminSharedLink, sharedLink: { ...authStub.adminSharedLink.sharedLink!, showExif: false } },
|
||||||
@@ -152,7 +153,7 @@ describe(AssetService.name, () => {
|
|||||||
it('should allow partner sharing access', async () => {
|
it('should allow partner sharing access', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.access.asset.checkPartnerAccess.mockResolvedValue(new Set([asset.id]));
|
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);
|
await sut.get(authStub.admin, asset.id);
|
||||||
|
|
||||||
@@ -162,7 +163,7 @@ describe(AssetService.name, () => {
|
|||||||
it('should allow shared album access', async () => {
|
it('should allow shared album access', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.access.asset.checkAlbumAccess.mockResolvedValue(new Set([asset.id]));
|
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);
|
await sut.get(authStub.admin, asset.id);
|
||||||
|
|
||||||
@@ -204,8 +205,8 @@ describe(AssetService.name, () => {
|
|||||||
it('should update the asset', async () => {
|
it('should update the asset', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||||
mocks.asset.getById.mockResolvedValue(asset);
|
mocks.asset.getById.mockResolvedValue(getForAsset(asset));
|
||||||
mocks.asset.update.mockResolvedValue(asset);
|
mocks.asset.update.mockResolvedValue(getForAsset(asset));
|
||||||
|
|
||||||
await sut.update(authStub.admin, asset.id, { isFavorite: true });
|
await sut.update(authStub.admin, asset.id, { isFavorite: true });
|
||||||
|
|
||||||
@@ -215,8 +216,8 @@ describe(AssetService.name, () => {
|
|||||||
it('should update the exif description', async () => {
|
it('should update the exif description', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||||
mocks.asset.getById.mockResolvedValue(asset);
|
mocks.asset.getById.mockResolvedValue(getForAsset(asset));
|
||||||
mocks.asset.update.mockResolvedValue(asset);
|
mocks.asset.update.mockResolvedValue(getForAsset(asset));
|
||||||
|
|
||||||
await sut.update(authStub.admin, asset.id, { description: 'Test description' });
|
await sut.update(authStub.admin, asset.id, { description: 'Test description' });
|
||||||
|
|
||||||
@@ -229,8 +230,8 @@ describe(AssetService.name, () => {
|
|||||||
it('should update the exif rating', async () => {
|
it('should update the exif rating', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||||
mocks.asset.getById.mockResolvedValueOnce(asset);
|
mocks.asset.getById.mockResolvedValueOnce(getForAsset(asset));
|
||||||
mocks.asset.update.mockResolvedValueOnce(asset);
|
mocks.asset.update.mockResolvedValueOnce(getForAsset(asset));
|
||||||
|
|
||||||
await sut.update(authStub.admin, asset.id, { rating: 3 });
|
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 motionAsset = AssetFactory.from().owner(auth.user).build();
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||||
mocks.asset.getById.mockResolvedValue(asset);
|
mocks.asset.getById.mockResolvedValue(getForAsset(asset));
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
sut.update(authStub.admin, asset.id, {
|
sut.update(authStub.admin, asset.id, {
|
||||||
@@ -301,7 +302,7 @@ describe(AssetService.name, () => {
|
|||||||
const motionAsset = AssetFactory.create({ type: AssetType.Video });
|
const motionAsset = AssetFactory.create({ type: AssetType.Video });
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||||
mocks.asset.getById.mockResolvedValue(motionAsset);
|
mocks.asset.getById.mockResolvedValue(getForAsset(motionAsset));
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
sut.update(auth, asset.id, {
|
sut.update(auth, asset.id, {
|
||||||
@@ -327,9 +328,9 @@ describe(AssetService.name, () => {
|
|||||||
const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Timeline });
|
const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Timeline });
|
||||||
const stillAsset = AssetFactory.create();
|
const stillAsset = AssetFactory.create();
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([stillAsset.id]));
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([stillAsset.id]));
|
||||||
mocks.asset.getById.mockResolvedValueOnce(motionAsset);
|
mocks.asset.getById.mockResolvedValueOnce(getForAsset(motionAsset));
|
||||||
mocks.asset.getById.mockResolvedValueOnce(stillAsset);
|
mocks.asset.getById.mockResolvedValueOnce(getForAsset(stillAsset));
|
||||||
mocks.asset.update.mockResolvedValue(stillAsset);
|
mocks.asset.update.mockResolvedValue(getForAsset(stillAsset));
|
||||||
const auth = AuthFactory.from(motionAsset.owner).build();
|
const auth = AuthFactory.from(motionAsset.owner).build();
|
||||||
|
|
||||||
await sut.update(auth, stillAsset.id, { livePhotoVideoId: motionAsset.id });
|
await sut.update(auth, stillAsset.id, { livePhotoVideoId: motionAsset.id });
|
||||||
@@ -354,9 +355,9 @@ describe(AssetService.name, () => {
|
|||||||
const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id });
|
const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id });
|
||||||
const unlinkedAsset = AssetFactory.create();
|
const unlinkedAsset = AssetFactory.create();
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||||
mocks.asset.getById.mockResolvedValueOnce(asset);
|
mocks.asset.getById.mockResolvedValueOnce(getForAsset(asset));
|
||||||
mocks.asset.getById.mockResolvedValueOnce(motionAsset);
|
mocks.asset.getById.mockResolvedValueOnce(getForAsset(motionAsset));
|
||||||
mocks.asset.update.mockResolvedValueOnce(unlinkedAsset);
|
mocks.asset.update.mockResolvedValueOnce(getForAsset(unlinkedAsset));
|
||||||
|
|
||||||
await sut.update(auth, asset.id, { livePhotoVideoId: null });
|
await sut.update(auth, asset.id, { livePhotoVideoId: null });
|
||||||
|
|
||||||
@@ -569,7 +570,7 @@ describe(AssetService.name, () => {
|
|||||||
.file({ type: AssetFileType.Preview, isEdited: true })
|
.file({ type: AssetFileType.Preview, isEdited: true })
|
||||||
.file({ type: AssetFileType.Thumbnail, isEdited: true })
|
.file({ type: AssetFileType.Thumbnail, isEdited: true })
|
||||||
.build();
|
.build();
|
||||||
mocks.assetJob.getForAssetDeletion.mockResolvedValue(asset);
|
mocks.assetJob.getForAssetDeletion.mockResolvedValue(getForAssetDeletion(asset));
|
||||||
|
|
||||||
await sut.handleAssetDeletion({ id: asset.id, deleteOnDisk: true });
|
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 () => {
|
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())
|
.stack({}, (builder) => builder.asset())
|
||||||
.build();
|
.build();
|
||||||
mocks.stack.delete.mockResolvedValue();
|
mocks.stack.delete.mockResolvedValue();
|
||||||
mocks.assetJob.getForAssetDeletion.mockResolvedValue({
|
mocks.assetJob.getForAssetDeletion.mockResolvedValue(getForAssetDeletion(asset));
|
||||||
...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) },
|
|
||||||
});
|
|
||||||
|
|
||||||
await sut.handleAssetDeletion({ id: asset.id, deleteOnDisk: true });
|
await sut.handleAssetDeletion({ id: asset.id, deleteOnDisk: true });
|
||||||
|
|
||||||
@@ -605,7 +602,7 @@ describe(AssetService.name, () => {
|
|||||||
it('should delete a live photo', async () => {
|
it('should delete a live photo', async () => {
|
||||||
const motionAsset = AssetFactory.from({ type: AssetType.Video, visibility: AssetVisibility.Hidden }).build();
|
const motionAsset = AssetFactory.from({ type: AssetType.Video, visibility: AssetVisibility.Hidden }).build();
|
||||||
const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id });
|
const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id });
|
||||||
mocks.assetJob.getForAssetDeletion.mockResolvedValue(asset);
|
mocks.assetJob.getForAssetDeletion.mockResolvedValue(getForAssetDeletion(asset));
|
||||||
mocks.asset.getLivePhotoCount.mockResolvedValue(0);
|
mocks.asset.getLivePhotoCount.mockResolvedValue(0);
|
||||||
|
|
||||||
await sut.handleAssetDeletion({
|
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 () => {
|
it('should not delete a live motion part if it is being used by another asset', async () => {
|
||||||
const asset = AssetFactory.create({ livePhotoVideoId: newUuid() });
|
const asset = AssetFactory.create({ livePhotoVideoId: newUuid() });
|
||||||
mocks.asset.getLivePhotoCount.mockResolvedValue(2);
|
mocks.asset.getLivePhotoCount.mockResolvedValue(2);
|
||||||
mocks.assetJob.getForAssetDeletion.mockResolvedValue(asset);
|
mocks.assetJob.getForAssetDeletion.mockResolvedValue(getForAssetDeletion(asset));
|
||||||
|
|
||||||
await sut.handleAssetDeletion({ id: asset.id, deleteOnDisk: true });
|
await sut.handleAssetDeletion({ id: asset.id, deleteOnDisk: true });
|
||||||
|
|
||||||
@@ -633,7 +630,7 @@ describe(AssetService.name, () => {
|
|||||||
|
|
||||||
it('should update usage', async () => {
|
it('should update usage', async () => {
|
||||||
const asset = AssetFactory.from().exif({ fileSizeInByte: 5000 }).build();
|
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 });
|
await sut.handleAssetDeletion({ id: asset.id, deleteOnDisk: true });
|
||||||
expect(mocks.user.updateUsage).toHaveBeenCalledWith(asset.ownerId, -5000);
|
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 { SearchService } from 'src/services/search.service';
|
||||||
import { AssetFactory } from 'test/factories/asset.factory';
|
import { AssetFactory } from 'test/factories/asset.factory';
|
||||||
import { authStub } from 'test/fixtures/auth.stub';
|
import { authStub } from 'test/fixtures/auth.stub';
|
||||||
|
import { getForDuplicate } from 'test/mappers';
|
||||||
import { newUuid } from 'test/small.factory';
|
import { newUuid } from 'test/small.factory';
|
||||||
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
||||||
import { beforeEach, vitest } from 'vitest';
|
import { beforeEach, vitest } from 'vitest';
|
||||||
@@ -39,11 +40,11 @@ describe(SearchService.name, () => {
|
|||||||
|
|
||||||
describe('getDuplicates', () => {
|
describe('getDuplicates', () => {
|
||||||
it('should get duplicates', async () => {
|
it('should get duplicates', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.from().exif().build();
|
||||||
mocks.duplicateRepository.getAll.mockResolvedValue([
|
mocks.duplicateRepository.getAll.mockResolvedValue([
|
||||||
{
|
{
|
||||||
duplicateId: 'duplicate-id',
|
duplicateId: 'duplicate-id',
|
||||||
assets: [asset, asset],
|
assets: [getForDuplicate(asset), getForDuplicate(asset)],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
await expect(sut.getDuplicates(authStub.admin)).resolves.toEqual([
|
await expect(sut.getDuplicates(authStub.admin)).resolves.toEqual([
|
||||||
|
|||||||
@@ -186,8 +186,8 @@ export class JobService extends BaseService {
|
|||||||
exifImageHeight: exif.exifImageHeight,
|
exifImageHeight: exif.exifImageHeight,
|
||||||
fileSizeInByte: exif.fileSizeInByte,
|
fileSizeInByte: exif.fileSizeInByte,
|
||||||
orientation: exif.orientation,
|
orientation: exif.orientation,
|
||||||
dateTimeOriginal: exif.dateTimeOriginal,
|
dateTimeOriginal: exif.dateTimeOriginal ? new Date(exif.dateTimeOriginal) : null,
|
||||||
modifyDate: exif.modifyDate,
|
modifyDate: exif.modifyDate ? new Date(exif.modifyDate) : null,
|
||||||
timeZone: exif.timeZone,
|
timeZone: exif.timeZone,
|
||||||
latitude: exif.latitude,
|
latitude: exif.latitude,
|
||||||
longitude: exif.longitude,
|
longitude: exif.longitude,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { AlbumFactory } from 'test/factories/album.factory';
|
|||||||
import { AssetFactory } from 'test/factories/asset.factory';
|
import { AssetFactory } from 'test/factories/asset.factory';
|
||||||
import { AuthFactory } from 'test/factories/auth.factory';
|
import { AuthFactory } from 'test/factories/auth.factory';
|
||||||
import { userStub } from 'test/fixtures/user.stub';
|
import { userStub } from 'test/fixtures/user.stub';
|
||||||
|
import { getForAlbum, getForPartner } from 'test/mappers';
|
||||||
import { factory } from 'test/small.factory';
|
import { factory } from 'test/small.factory';
|
||||||
import { newTestService, ServiceMocks } from 'test/utils';
|
import { newTestService, ServiceMocks } from 'test/utils';
|
||||||
|
|
||||||
@@ -52,7 +53,7 @@ describe(MapService.name, () => {
|
|||||||
state: asset.exifInfo.state,
|
state: asset.exifInfo.state,
|
||||||
country: asset.exifInfo.country,
|
country: asset.exifInfo.country,
|
||||||
};
|
};
|
||||||
mocks.partner.getAll.mockResolvedValue([partner]);
|
mocks.partner.getAll.mockResolvedValue([getForPartner(partner)]);
|
||||||
mocks.map.getMapMarkers.mockResolvedValue([marker]);
|
mocks.map.getMapMarkers.mockResolvedValue([marker]);
|
||||||
|
|
||||||
const markers = await sut.getMapMarkers(auth, { withPartners: true });
|
const markers = await sut.getMapMarkers(auth, { withPartners: true });
|
||||||
@@ -81,8 +82,10 @@ describe(MapService.name, () => {
|
|||||||
};
|
};
|
||||||
mocks.partner.getAll.mockResolvedValue([]);
|
mocks.partner.getAll.mockResolvedValue([]);
|
||||||
mocks.map.getMapMarkers.mockResolvedValue([marker]);
|
mocks.map.getMapMarkers.mockResolvedValue([marker]);
|
||||||
mocks.album.getOwned.mockResolvedValue([AlbumFactory.create()]);
|
mocks.album.getOwned.mockResolvedValue([getForAlbum(AlbumFactory.create())]);
|
||||||
mocks.album.getShared.mockResolvedValue([AlbumFactory.from().albumUser({ userId: userStub.user1.id }).build()]);
|
mocks.album.getShared.mockResolvedValue([
|
||||||
|
getForAlbum(AlbumFactory.from().albumUser({ userId: userStub.user1.id }).build()),
|
||||||
|
]);
|
||||||
|
|
||||||
const markers = await sut.getMapMarkers(auth, { withSharedAlbums: true });
|
const markers = await sut.getMapMarkers(auth, { withSharedAlbums: true });
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ShallowDehydrateObject } from 'kysely';
|
||||||
import { OutputInfo } from 'sharp';
|
import { OutputInfo } from 'sharp';
|
||||||
import { SystemConfig } from 'src/config';
|
import { SystemConfig } from 'src/config';
|
||||||
import { Exif } from 'src/database';
|
import { Exif } from 'src/database';
|
||||||
@@ -27,6 +28,7 @@ import { PersonFactory } from 'test/factories/person.factory';
|
|||||||
import { probeStub } from 'test/fixtures/media.stub';
|
import { probeStub } from 'test/fixtures/media.stub';
|
||||||
import { personThumbnailStub } from 'test/fixtures/person.stub';
|
import { personThumbnailStub } from 'test/fixtures/person.stub';
|
||||||
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
||||||
|
import { getForGenerateThumbnail } from 'test/mappers';
|
||||||
import { factory, newUuid } from 'test/small.factory';
|
import { factory, newUuid } from 'test/small.factory';
|
||||||
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
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 () => {
|
it('should skip thumbnail generation if asset type is unknown', async () => {
|
||||||
const asset = AssetFactory.create({ type: 'foo' as AssetType });
|
const asset = AssetFactory.from({ type: 'foo' as AssetType })
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
.exif()
|
||||||
|
.build();
|
||||||
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||||
|
|
||||||
await expect(sut.handleGenerateThumbnails({ id: asset.id })).resolves.toBe(JobStatus.Skipped);
|
await expect(sut.handleGenerateThumbnails({ id: asset.id })).resolves.toBe(JobStatus.Skipped);
|
||||||
expect(mocks.media.probe).not.toHaveBeenCalled();
|
expect(mocks.media.probe).not.toHaveBeenCalled();
|
||||||
@@ -377,17 +381,17 @@ describe(MediaService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should skip video thumbnail generation if no video stream', async () => {
|
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.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();
|
await expect(sut.handleGenerateThumbnails({ id: asset.id })).rejects.toThrowError();
|
||||||
expect(mocks.media.generateThumbnail).not.toHaveBeenCalled();
|
expect(mocks.media.generateThumbnail).not.toHaveBeenCalled();
|
||||||
expect(mocks.asset.update).not.toHaveBeenCalledWith();
|
expect(mocks.asset.update).not.toHaveBeenCalledWith();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should skip invisible assets', async () => {
|
it('should skip invisible assets', async () => {
|
||||||
const asset = AssetFactory.create({ visibility: AssetVisibility.Hidden });
|
const asset = AssetFactory.from({ visibility: AssetVisibility.Hidden }).exif().build();
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||||
|
|
||||||
expect(await sut.handleGenerateThumbnails({ id: asset.id })).toEqual(JobStatus.Skipped);
|
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 () => {
|
it('should delete previous preview if different path', async () => {
|
||||||
const asset = AssetFactory.from().file({ type: AssetFileType.Preview }).exif().build();
|
const asset = AssetFactory.from().file({ type: AssetFileType.Preview }).exif().build();
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ image: { thumbnail: { format: ImageFormat.Webp } } });
|
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 });
|
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||||
|
|
||||||
@@ -415,7 +419,7 @@ describe(MediaService.name, () => {
|
|||||||
.exif({ profileDescription: 'Adobe RGB', bitsPerSample: 14 })
|
.exif({ profileDescription: 'Adobe RGB', bitsPerSample: 14 })
|
||||||
.files([AssetFileType.Preview, AssetFileType.Thumbnail])
|
.files([AssetFileType.Preview, AssetFileType.Thumbnail])
|
||||||
.build();
|
.build();
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||||
const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8');
|
const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8');
|
||||||
mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer);
|
mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer);
|
||||||
|
|
||||||
@@ -490,9 +494,9 @@ describe(MediaService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should generate a thumbnail for a video', async () => {
|
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.media.probe.mockResolvedValue(probeStub.videoStream2160p);
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||||
|
|
||||||
expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.any(String));
|
expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.any(String));
|
||||||
@@ -532,9 +536,9 @@ describe(MediaService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should tonemap thumbnail for hdr video', async () => {
|
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.media.probe.mockResolvedValue(probeStub.videoStreamHDR);
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||||
|
|
||||||
expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.any(String));
|
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 () => {
|
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.media.probe.mockResolvedValue(probeStub.videoStreamHDR);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({
|
mocks.systemMetadata.get.mockResolvedValue({
|
||||||
ffmpeg: { twoPass: true, maxBitrate: '5000k' },
|
ffmpeg: { twoPass: true, maxBitrate: '5000k' },
|
||||||
});
|
});
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||||
|
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
@@ -600,9 +604,9 @@ describe(MediaService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not skip intra frames for MTS file', async () => {
|
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.media.probe.mockResolvedValue(probeStub.videoStreamMTS);
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||||
|
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
@@ -618,9 +622,9 @@ describe(MediaService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should override reserved color metadata', async () => {
|
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.media.probe.mockResolvedValue(probeStub.videoStreamReserved);
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||||
|
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
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 () => {
|
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.media.probe.mockResolvedValue(probeStub.videoStream2160p);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv } });
|
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 });
|
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||||
|
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
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) => {
|
it.each(Object.values(ImageFormat))('should generate an image preview in %s format', async (format) => {
|
||||||
const asset = AssetFactory.from().exif().build();
|
const asset = AssetFactory.from().exif().build();
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ image: { preview: { format } } });
|
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');
|
const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8');
|
||||||
mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer);
|
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}`;
|
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) => {
|
it.each(Object.values(ImageFormat))('should generate an image thumbnail in %s format', async (format) => {
|
||||||
const asset = AssetFactory.from().exif().build();
|
const asset = AssetFactory.from().exif().build();
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ image: { thumbnail: { format } } });
|
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');
|
const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8');
|
||||||
mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer);
|
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`;
|
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({
|
mocks.systemMetadata.get.mockResolvedValue({
|
||||||
image: { preview: { progressive: true }, thumbnail: { progressive: false } },
|
image: { preview: { progressive: true }, thumbnail: { progressive: false } },
|
||||||
});
|
});
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||||
|
|
||||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||||
|
|
||||||
@@ -799,7 +803,7 @@ describe(MediaService.name, () => {
|
|||||||
mocks.systemMetadata.get.mockResolvedValue({
|
mocks.systemMetadata.get.mockResolvedValue({
|
||||||
image: { preview: { progressive: false }, thumbnail: { format: ImageFormat.Jpeg, progressive: true } },
|
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 });
|
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||||
|
|
||||||
@@ -834,12 +838,12 @@ describe(MediaService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should never set isProgressive for videos', async () => {
|
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.media.probe.mockResolvedValue(probeStub.videoStreamHDR);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({
|
mocks.systemMetadata.get.mockResolvedValue({
|
||||||
image: { preview: { progressive: true }, thumbnail: { progressive: true } },
|
image: { preview: { progressive: true }, thumbnail: { progressive: true } },
|
||||||
});
|
});
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||||
|
|
||||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||||
|
|
||||||
@@ -860,7 +864,7 @@ describe(MediaService.name, () => {
|
|||||||
it('should delete previous thumbnail if different path', async () => {
|
it('should delete previous thumbnail if different path', async () => {
|
||||||
const asset = AssetFactory.from().exif().file({ type: AssetFileType.Preview }).build();
|
const asset = AssetFactory.from().exif().file({ type: AssetFileType.Preview }).build();
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ image: { thumbnail: { format: ImageFormat.Webp } } });
|
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 });
|
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||||
|
|
||||||
@@ -879,7 +883,7 @@ describe(MediaService.name, () => {
|
|||||||
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
||||||
mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false });
|
mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false });
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } });
|
mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } });
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||||
|
|
||||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||||
|
|
||||||
@@ -896,7 +900,7 @@ describe(MediaService.name, () => {
|
|||||||
.exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined })
|
.exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined })
|
||||||
.build();
|
.build();
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: false } });
|
mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: false } });
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||||
|
|
||||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||||
|
|
||||||
@@ -910,7 +914,7 @@ describe(MediaService.name, () => {
|
|||||||
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
||||||
mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false });
|
mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false });
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } });
|
mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } });
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||||
|
|
||||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||||
|
|
||||||
@@ -925,7 +929,7 @@ describe(MediaService.name, () => {
|
|||||||
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
||||||
mocks.media.getImageMetadata.mockResolvedValue({ width: 1000, height: 1000, isTransparent: false });
|
mocks.media.getImageMetadata.mockResolvedValue({ width: 1000, height: 1000, isTransparent: false });
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } });
|
mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } });
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||||
|
|
||||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||||
|
|
||||||
@@ -941,7 +945,7 @@ describe(MediaService.name, () => {
|
|||||||
.exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined })
|
.exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined })
|
||||||
.build();
|
.build();
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } });
|
mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } });
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||||
|
|
||||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||||
|
|
||||||
@@ -958,7 +962,7 @@ describe(MediaService.name, () => {
|
|||||||
.exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined })
|
.exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined })
|
||||||
.build();
|
.build();
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: false } });
|
mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: false } });
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||||
|
|
||||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||||
|
|
||||||
@@ -977,7 +981,7 @@ describe(MediaService.name, () => {
|
|||||||
.exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined })
|
.exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined })
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||||
|
|
||||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||||
|
|
||||||
@@ -1018,7 +1022,7 @@ describe(MediaService.name, () => {
|
|||||||
});
|
});
|
||||||
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
||||||
mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false });
|
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 });
|
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||||
|
|
||||||
@@ -1056,7 +1060,7 @@ describe(MediaService.name, () => {
|
|||||||
});
|
});
|
||||||
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jxl });
|
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jxl });
|
||||||
mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false });
|
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 });
|
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||||
|
|
||||||
@@ -1104,7 +1108,7 @@ describe(MediaService.name, () => {
|
|||||||
mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true }, extractEmbedded: false } });
|
mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true }, extractEmbedded: false } });
|
||||||
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
||||||
mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false });
|
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 });
|
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||||
|
|
||||||
@@ -1156,7 +1160,7 @@ describe(MediaService.name, () => {
|
|||||||
bitsPerSample: 14,
|
bitsPerSample: 14,
|
||||||
})
|
})
|
||||||
.build();
|
.build();
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||||
|
|
||||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||||
|
|
||||||
@@ -1187,7 +1191,7 @@ describe(MediaService.name, () => {
|
|||||||
mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true } } });
|
mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true } } });
|
||||||
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
||||||
mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false });
|
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 });
|
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||||
|
|
||||||
@@ -1219,7 +1223,7 @@ describe(MediaService.name, () => {
|
|||||||
})
|
})
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||||
|
|
||||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||||
|
|
||||||
@@ -1264,7 +1268,7 @@ describe(MediaService.name, () => {
|
|||||||
bitsPerSample: 14,
|
bitsPerSample: 14,
|
||||||
})
|
})
|
||||||
.build();
|
.build();
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||||
|
|
||||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||||
|
|
||||||
@@ -1303,7 +1307,7 @@ describe(MediaService.name, () => {
|
|||||||
bitsPerSample: 14,
|
bitsPerSample: 14,
|
||||||
})
|
})
|
||||||
.build();
|
.build();
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||||
|
|
||||||
await sut.handleGenerateThumbnails({ id: asset.id });
|
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||||
|
|
||||||
@@ -1338,7 +1342,7 @@ describe(MediaService.name, () => {
|
|||||||
|
|
||||||
it('should skip videos', async () => {
|
it('should skip videos', async () => {
|
||||||
const asset = AssetFactory.from({ type: AssetType.Video }).exif().build();
|
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);
|
await expect(sut.handleAssetEditThumbnailGeneration({ id: asset.id })).resolves.toBe(JobStatus.Success);
|
||||||
expect(mocks.media.generateThumbnail).not.toHaveBeenCalled();
|
expect(mocks.media.generateThumbnail).not.toHaveBeenCalled();
|
||||||
@@ -1355,7 +1359,7 @@ describe(MediaService.name, () => {
|
|||||||
])
|
])
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||||
const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8');
|
const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8');
|
||||||
mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer);
|
mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer);
|
||||||
mocks.person.getFaces.mockResolvedValue([]);
|
mocks.person.getFaces.mockResolvedValue([]);
|
||||||
@@ -1377,7 +1381,7 @@ describe(MediaService.name, () => {
|
|||||||
.exif()
|
.exif()
|
||||||
.edit({ action: AssetEditAction.Crop, parameters: { height: 1152, width: 1512, x: 216, y: 1512 } })
|
.edit({ action: AssetEditAction.Crop, parameters: { height: 1152, width: 1512, x: 216, y: 1512 } })
|
||||||
.build();
|
.build();
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||||
mocks.person.getFaces.mockResolvedValue([]);
|
mocks.person.getFaces.mockResolvedValue([]);
|
||||||
mocks.ocr.getByAssetId.mockResolvedValue([]);
|
mocks.ocr.getByAssetId.mockResolvedValue([]);
|
||||||
|
|
||||||
@@ -1405,7 +1409,7 @@ describe(MediaService.name, () => {
|
|||||||
{ type: AssetFileType.FullSize, path: 'edited3.jpg', isEdited: true },
|
{ type: AssetFileType.FullSize, path: 'edited3.jpg', isEdited: true },
|
||||||
])
|
])
|
||||||
.build();
|
.build();
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||||
|
|
||||||
const status = await sut.handleAssetEditThumbnailGeneration({ id: asset.id });
|
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 () => {
|
it('should generate all 3 edited files if an asset has edits', async () => {
|
||||||
const asset = AssetFactory.from().exif().edit().build();
|
const asset = AssetFactory.from().exif().edit().build();
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||||
mocks.person.getFaces.mockResolvedValue([]);
|
mocks.person.getFaces.mockResolvedValue([]);
|
||||||
mocks.ocr.getByAssetId.mockResolvedValue([]);
|
mocks.ocr.getByAssetId.mockResolvedValue([]);
|
||||||
|
|
||||||
@@ -1449,7 +1453,7 @@ describe(MediaService.name, () => {
|
|||||||
|
|
||||||
it('should generate the original thumbhash if no edits exist', async () => {
|
it('should generate the original thumbhash if no edits exist', async () => {
|
||||||
const asset = AssetFactory.from().exif().build();
|
const asset = AssetFactory.from().exif().build();
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||||
mocks.media.generateThumbhash.mockResolvedValue(factory.buffer());
|
mocks.media.generateThumbhash.mockResolvedValue(factory.buffer());
|
||||||
|
|
||||||
await sut.handleAssetEditThumbnailGeneration({ id: asset.id, source: 'upload' });
|
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 () => {
|
it('should apply thumbhash if job source is edit and edits exist', async () => {
|
||||||
const asset = AssetFactory.from().exif().edit().build();
|
const asset = AssetFactory.from().exif().edit().build();
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
|
||||||
const thumbhashBuffer = factory.buffer();
|
const thumbhashBuffer = factory.buffer();
|
||||||
mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer);
|
mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer);
|
||||||
mocks.person.getFaces.mockResolvedValue([]);
|
mocks.person.getFaces.mockResolvedValue([]);
|
||||||
@@ -3603,15 +3607,15 @@ describe(MediaService.name, () => {
|
|||||||
|
|
||||||
describe('isSRGB', () => {
|
describe('isSRGB', () => {
|
||||||
it('should return true for srgb colorspace', () => {
|
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', () => {
|
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', () => {
|
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', () => {
|
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', () => {
|
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', () => {
|
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', () => {
|
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', () => {
|
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', () => {
|
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 { SystemConfig } from 'src/config';
|
||||||
import { FACE_THUMBNAIL_SIZE, JOBS_ASSET_PAGINATION_SIZE } from 'src/constants';
|
import { FACE_THUMBNAIL_SIZE, JOBS_ASSET_PAGINATION_SIZE } from 'src/constants';
|
||||||
import { ImagePathOptions, StorageCore, ThumbnailPathEntity } from 'src/cores/storage.core';
|
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 { OnEvent, OnJob } from 'src/decorators';
|
||||||
import { AssetEditAction, CropParameters } from 'src/dtos/editing.dto';
|
import { AssetEditAction, CropParameters } from 'src/dtos/editing.dto';
|
||||||
import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
|
import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
|
||||||
@@ -258,7 +258,7 @@ export class MediaService extends BaseService {
|
|||||||
return extracted;
|
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 { image } = await this.getConfig({ withCache: true });
|
||||||
const colorspace = this.isSRGB(exifInfo) ? Colorspace.Srgb : image.colorspace;
|
const colorspace = this.isSRGB(exifInfo) ? Colorspace.Srgb : image.colorspace;
|
||||||
const decodeOptions: DecodeToBufferOptions = {
|
const decodeOptions: DecodeToBufferOptions = {
|
||||||
@@ -754,7 +754,15 @@ export class MediaService extends BaseService {
|
|||||||
return name !== VideoContainer.Mp4 && !ffmpegConfig.acceptedContainers.includes(name);
|
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) {
|
if (colorspace || profileDescription) {
|
||||||
return [colorspace, profileDescription].some((s) => s?.toLowerCase().includes('srgb'));
|
return [colorspace, profileDescription].some((s) => s?.toLowerCase().includes('srgb'));
|
||||||
} else if (bitsPerSample) {
|
} else if (bitsPerSample) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { MemoryService } from 'src/services/memory.service';
|
|||||||
import { OnThisDayData } from 'src/types';
|
import { OnThisDayData } from 'src/types';
|
||||||
import { AssetFactory } from 'test/factories/asset.factory';
|
import { AssetFactory } from 'test/factories/asset.factory';
|
||||||
import { MemoryFactory } from 'test/factories/memory.factory';
|
import { MemoryFactory } from 'test/factories/memory.factory';
|
||||||
|
import { getForMemory } from 'test/mappers';
|
||||||
import { factory, newUuid, newUuids } from 'test/small.factory';
|
import { factory, newUuid, newUuids } from 'test/small.factory';
|
||||||
import { newTestService, ServiceMocks } from 'test/utils';
|
import { newTestService, ServiceMocks } from 'test/utils';
|
||||||
|
|
||||||
@@ -33,7 +34,7 @@ describe(MemoryService.name, () => {
|
|||||||
const memory1 = MemoryFactory.from({ ownerId: userId }).asset(asset).build();
|
const memory1 = MemoryFactory.from({ ownerId: userId }).asset(asset).build();
|
||||||
const memory2 = MemoryFactory.create({ ownerId: userId });
|
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(
|
await expect(sut.search(factory.auth({ user: { id: userId } }), {})).resolves.toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
@@ -68,7 +69,7 @@ describe(MemoryService.name, () => {
|
|||||||
const userId = newUuid();
|
const userId = newUuid();
|
||||||
const memory = MemoryFactory.create({ ownerId: userId });
|
const memory = MemoryFactory.create({ ownerId: userId });
|
||||||
|
|
||||||
mocks.memory.get.mockResolvedValue(memory);
|
mocks.memory.get.mockResolvedValue(getForMemory(memory));
|
||||||
mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set([memory.id]));
|
mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set([memory.id]));
|
||||||
|
|
||||||
await expect(sut.get(factory.auth({ user: { id: userId } }), memory.id)).resolves.toMatchObject({
|
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 [assetId, userId] = newUuids();
|
||||||
const memory = MemoryFactory.create({ ownerId: userId });
|
const memory = MemoryFactory.create({ ownerId: userId });
|
||||||
|
|
||||||
mocks.memory.create.mockResolvedValue(memory);
|
mocks.memory.create.mockResolvedValue(getForMemory(memory));
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
sut.create(factory.auth({ user: { id: userId } }), {
|
sut.create(factory.auth({ user: { id: userId } }), {
|
||||||
@@ -115,7 +116,7 @@ describe(MemoryService.name, () => {
|
|||||||
const memory = MemoryFactory.from().asset(asset).build();
|
const memory = MemoryFactory.from().asset(asset).build();
|
||||||
|
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||||
mocks.memory.create.mockResolvedValue(memory);
|
mocks.memory.create.mockResolvedValue(getForMemory(memory));
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
sut.create(factory.auth({ user: { id: userId } }), {
|
sut.create(factory.auth({ user: { id: userId } }), {
|
||||||
@@ -135,7 +136,7 @@ describe(MemoryService.name, () => {
|
|||||||
it('should create a memory without assets', async () => {
|
it('should create a memory without assets', async () => {
|
||||||
const memory = MemoryFactory.create();
|
const memory = MemoryFactory.create();
|
||||||
|
|
||||||
mocks.memory.create.mockResolvedValue(memory);
|
mocks.memory.create.mockResolvedValue(getForMemory(memory));
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
sut.create(factory.auth(), {
|
sut.create(factory.auth(), {
|
||||||
@@ -160,7 +161,7 @@ describe(MemoryService.name, () => {
|
|||||||
const memory = MemoryFactory.create();
|
const memory = MemoryFactory.create();
|
||||||
|
|
||||||
mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set([memory.id]));
|
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();
|
await expect(sut.update(factory.auth(), memory.id, { isSaved: true })).resolves.toBeDefined();
|
||||||
|
|
||||||
@@ -203,7 +204,7 @@ describe(MemoryService.name, () => {
|
|||||||
const memory = MemoryFactory.create();
|
const memory = MemoryFactory.create();
|
||||||
|
|
||||||
mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set([memory.id]));
|
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());
|
mocks.memory.getAssetIds.mockResolvedValue(new Set());
|
||||||
|
|
||||||
await expect(sut.addAssets(factory.auth(), memory.id, { ids: [assetId] })).resolves.toEqual([
|
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();
|
const memory = MemoryFactory.from().asset(asset).build();
|
||||||
|
|
||||||
mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set([memory.id]));
|
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]));
|
mocks.memory.getAssetIds.mockResolvedValue(new Set([asset.id]));
|
||||||
|
|
||||||
await expect(sut.addAssets(factory.auth(), memory.id, { ids: [asset.id] })).resolves.toEqual([
|
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.memory.checkOwnerAccess.mockResolvedValue(new Set([memory.id]));
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId]));
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId]));
|
||||||
mocks.memory.get.mockResolvedValue(memory);
|
mocks.memory.get.mockResolvedValue(getForMemory(memory));
|
||||||
mocks.memory.update.mockResolvedValue(memory);
|
mocks.memory.update.mockResolvedValue(getForMemory(memory));
|
||||||
mocks.memory.getAssetIds.mockResolvedValue(new Set());
|
mocks.memory.getAssetIds.mockResolvedValue(new Set());
|
||||||
mocks.memory.addAssetIds.mockResolvedValue();
|
mocks.memory.addAssetIds.mockResolvedValue();
|
||||||
|
|
||||||
@@ -275,7 +276,7 @@ describe(MemoryService.name, () => {
|
|||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||||
mocks.memory.getAssetIds.mockResolvedValue(new Set([asset.id]));
|
mocks.memory.getAssetIds.mockResolvedValue(new Set([asset.id]));
|
||||||
mocks.memory.removeAssetIds.mockResolvedValue();
|
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([
|
await expect(sut.removeAssets(factory.auth(), memory.id, { ids: [asset.id] })).resolves.toEqual([
|
||||||
{ id: asset.id, success: true },
|
{ id: asset.id, success: true },
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { AssetFactory } from 'test/factories/asset.factory';
|
|||||||
import { PersonFactory } from 'test/factories/person.factory';
|
import { PersonFactory } from 'test/factories/person.factory';
|
||||||
import { probeStub } from 'test/fixtures/media.stub';
|
import { probeStub } from 'test/fixtures/media.stub';
|
||||||
import { tagStub } from 'test/fixtures/tag.stub';
|
import { tagStub } from 'test/fixtures/tag.stub';
|
||||||
|
import { getForMetadataExtraction, getForSidecarWrite } from 'test/mappers';
|
||||||
import { factory } from 'test/small.factory';
|
import { factory } from 'test/small.factory';
|
||||||
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
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 originalDate = new Date('2023-11-21T16:13:17.517Z');
|
||||||
const sidecarDate = new Date('2022-01-01T00:00:00.000Z');
|
const sidecarDate = new Date('2022-01-01T00:00:00.000Z');
|
||||||
const asset = AssetFactory.from().file({ type: AssetFileType.Sidecar }).build();
|
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() });
|
mockReadTags({ CreationDate: originalDate.toISOString() }, { CreationDate: sidecarDate.toISOString() });
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: asset.id });
|
await sut.handleMetadataExtraction({ id: asset.id });
|
||||||
@@ -198,7 +199,7 @@ describe(MetadataService.name, () => {
|
|||||||
const fileCreatedAt = new Date('2022-01-01T00:00:00.000Z');
|
const fileCreatedAt = new Date('2022-01-01T00:00:00.000Z');
|
||||||
const fileModifiedAt = new Date('2021-01-01T00:00:00.000Z');
|
const fileModifiedAt = new Date('2021-01-01T00:00:00.000Z');
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mocks.storage.stat.mockResolvedValue({
|
mocks.storage.stat.mockResolvedValue({
|
||||||
size: 123_456,
|
size: 123_456,
|
||||||
mtime: fileModifiedAt,
|
mtime: fileModifiedAt,
|
||||||
@@ -228,7 +229,7 @@ describe(MetadataService.name, () => {
|
|||||||
const fileCreatedAt = new Date('2021-01-01T00:00:00.000Z');
|
const fileCreatedAt = new Date('2021-01-01T00:00:00.000Z');
|
||||||
const fileModifiedAt = new Date('2022-01-01T00:00:00.000Z');
|
const fileModifiedAt = new Date('2022-01-01T00:00:00.000Z');
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mocks.storage.stat.mockResolvedValue({
|
mocks.storage.stat.mockResolvedValue({
|
||||||
size: 123_456,
|
size: 123_456,
|
||||||
mtime: fileModifiedAt,
|
mtime: fileModifiedAt,
|
||||||
@@ -257,7 +258,7 @@ describe(MetadataService.name, () => {
|
|||||||
it('should determine dateTimeOriginal regardless of the server time zone', async () => {
|
it('should determine dateTimeOriginal regardless of the server time zone', async () => {
|
||||||
process.env.TZ = 'America/Los_Angeles';
|
process.env.TZ = 'America/Los_Angeles';
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mockReadTags({ DateTimeOriginal: '2022:01:01 00:00:00' });
|
mockReadTags({ DateTimeOriginal: '2022:01:01 00:00:00' });
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: asset.id });
|
await sut.handleMetadataExtraction({ id: asset.id });
|
||||||
@@ -277,7 +278,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should handle lists of numbers', async () => {
|
it('should handle lists of numbers', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mocks.storage.stat.mockResolvedValue({
|
mocks.storage.stat.mockResolvedValue({
|
||||||
size: 123_456,
|
size: 123_456,
|
||||||
mtime: asset.fileModifiedAt,
|
mtime: asset.fileModifiedAt,
|
||||||
@@ -305,7 +306,7 @@ describe(MetadataService.name, () => {
|
|||||||
it('should not delete latituide and longitude without reverse geocode', async () => {
|
it('should not delete latituide and longitude without reverse geocode', async () => {
|
||||||
// regression test for issue 17511
|
// regression test for issue 17511
|
||||||
const asset = AssetFactory.from().exif().build();
|
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.systemMetadata.get.mockResolvedValue({ reverseGeocoding: { enabled: false } });
|
||||||
mocks.storage.stat.mockResolvedValue({
|
mocks.storage.stat.mockResolvedValue({
|
||||||
size: 123_456,
|
size: 123_456,
|
||||||
@@ -337,7 +338,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should apply reverse geocoding', async () => {
|
it('should apply reverse geocoding', async () => {
|
||||||
const asset = AssetFactory.from().exif({ latitude: 10, longitude: 20 }).build();
|
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.systemMetadata.get.mockResolvedValue({ reverseGeocoding: { enabled: true } });
|
||||||
mocks.map.reverseGeocode.mockResolvedValue({ city: 'City', state: 'State', country: 'Country' });
|
mocks.map.reverseGeocode.mockResolvedValue({ city: 'City', state: 'State', country: 'Country' });
|
||||||
mocks.storage.stat.mockResolvedValue({
|
mocks.storage.stat.mockResolvedValue({
|
||||||
@@ -367,7 +368,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should discard latitude and longitude on null island', async () => {
|
it('should discard latitude and longitude on null island', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mockReadTags({
|
mockReadTags({
|
||||||
GPSLatitude: 0,
|
GPSLatitude: 0,
|
||||||
GPSLongitude: 0,
|
GPSLongitude: 0,
|
||||||
@@ -383,7 +384,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should extract tags from TagsList', async () => {
|
it('should extract tags from TagsList', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent'] });
|
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent'] });
|
||||||
mockReadTags({ TagsList: ['Parent'] });
|
mockReadTags({ TagsList: ['Parent'] });
|
||||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||||
@@ -395,7 +396,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should extract hierarchy from TagsList', async () => {
|
it('should extract hierarchy from TagsList', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent/Child'] });
|
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent/Child'] });
|
||||||
mockReadTags({ TagsList: ['Parent/Child'] });
|
mockReadTags({ TagsList: ['Parent/Child'] });
|
||||||
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert);
|
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert);
|
||||||
@@ -417,7 +418,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should extract tags from Keywords as a string', async () => {
|
it('should extract tags from Keywords as a string', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent'] });
|
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent'] });
|
||||||
mockReadTags({ Keywords: 'Parent' });
|
mockReadTags({ Keywords: 'Parent' });
|
||||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||||
@@ -429,7 +430,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should extract tags from Keywords as a list', async () => {
|
it('should extract tags from Keywords as a list', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent'] });
|
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent'] });
|
||||||
mockReadTags({ Keywords: ['Parent'] });
|
mockReadTags({ Keywords: ['Parent'] });
|
||||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
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 () => {
|
it('should extract tags from Keywords as a list with a number', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent', '2024'] });
|
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent', '2024'] });
|
||||||
mockReadTags({ Keywords: ['Parent', 2024] });
|
mockReadTags({ Keywords: ['Parent', 2024] });
|
||||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||||
@@ -454,7 +455,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should extract hierarchal tags from Keywords', async () => {
|
it('should extract hierarchal tags from Keywords', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent/Child'] });
|
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent/Child'] });
|
||||||
mockReadTags({ Keywords: 'Parent/Child' });
|
mockReadTags({ Keywords: 'Parent/Child' });
|
||||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||||
@@ -474,7 +475,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should ignore Keywords when TagsList is present', async () => {
|
it('should ignore Keywords when TagsList is present', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent/Child', 'Child'] });
|
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent/Child', 'Child'] });
|
||||||
mockReadTags({ Keywords: 'Child', TagsList: ['Parent/Child'] });
|
mockReadTags({ Keywords: 'Child', TagsList: ['Parent/Child'] });
|
||||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||||
@@ -495,7 +496,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should extract hierarchy from HierarchicalSubject', async () => {
|
it('should extract hierarchy from HierarchicalSubject', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent/Child', 'TagA'] });
|
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent/Child', 'TagA'] });
|
||||||
mockReadTags({ HierarchicalSubject: ['Parent|Child', 'TagA'] });
|
mockReadTags({ HierarchicalSubject: ['Parent|Child', 'TagA'] });
|
||||||
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert);
|
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 () => {
|
it('should extract tags from HierarchicalSubject as a list with a number', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent', '2024'] });
|
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent', '2024'] });
|
||||||
mockReadTags({ HierarchicalSubject: ['Parent', 2024] });
|
mockReadTags({ HierarchicalSubject: ['Parent', 2024] });
|
||||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||||
@@ -535,7 +536,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should extract ignore / characters in a HierarchicalSubject tag', async () => {
|
it('should extract ignore / characters in a HierarchicalSubject tag', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Mom|Dad'] });
|
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Mom|Dad'] });
|
||||||
mockReadTags({ HierarchicalSubject: ['Mom/Dad'] });
|
mockReadTags({ HierarchicalSubject: ['Mom/Dad'] });
|
||||||
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert);
|
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert);
|
||||||
@@ -551,7 +552,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should ignore HierarchicalSubject when TagsList is present', async () => {
|
it('should ignore HierarchicalSubject when TagsList is present', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent/Child', 'Parent2/Child2'] });
|
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent/Child', 'Parent2/Child2'] });
|
||||||
mockReadTags({ HierarchicalSubject: ['Parent2|Child2'], TagsList: ['Parent/Child'] });
|
mockReadTags({ HierarchicalSubject: ['Parent2|Child2'], TagsList: ['Parent/Child'] });
|
||||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||||
@@ -572,7 +573,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should remove existing tags', async () => {
|
it('should remove existing tags', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mockReadTags({});
|
mockReadTags({});
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: asset.id });
|
await sut.handleMetadataExtraction({ id: asset.id });
|
||||||
@@ -582,7 +583,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should not apply motion photos if asset is video', async () => {
|
it('should not apply motion photos if asset is video', async () => {
|
||||||
const asset = AssetFactory.create({ type: AssetType.Video });
|
const asset = AssetFactory.create({ type: AssetType.Video });
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: asset.id });
|
await sut.handleMetadataExtraction({ id: asset.id });
|
||||||
@@ -597,7 +598,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should handle an invalid Directory Item', async () => {
|
it('should handle an invalid Directory Item', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mockReadTags({
|
mockReadTags({
|
||||||
MotionPhoto: 1,
|
MotionPhoto: 1,
|
||||||
ContainerDirectory: [{ Foo: 100 }],
|
ContainerDirectory: [{ Foo: 100 }],
|
||||||
@@ -608,7 +609,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should extract the correct video orientation', async () => {
|
it('should extract the correct video orientation', async () => {
|
||||||
const asset = AssetFactory.create({ type: AssetType.Video });
|
const asset = AssetFactory.create({ type: AssetType.Video });
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.videoStreamVertical2160p);
|
mocks.media.probe.mockResolvedValue(probeStub.videoStreamVertical2160p);
|
||||||
mockReadTags({});
|
mockReadTags({});
|
||||||
|
|
||||||
@@ -624,7 +625,7 @@ describe(MetadataService.name, () => {
|
|||||||
it('should extract the MotionPhotoVideo tag from Samsung HEIC motion photos', async () => {
|
it('should extract the MotionPhotoVideo tag from Samsung HEIC motion photos', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden });
|
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({
|
mocks.storage.stat.mockResolvedValue({
|
||||||
size: 123_456,
|
size: 123_456,
|
||||||
mtime: asset.fileModifiedAt,
|
mtime: asset.fileModifiedAt,
|
||||||
@@ -686,7 +687,7 @@ describe(MetadataService.name, () => {
|
|||||||
mtimeMs: asset.fileModifiedAt.valueOf(),
|
mtimeMs: asset.fileModifiedAt.valueOf(),
|
||||||
birthtimeMs: asset.fileCreatedAt.valueOf(),
|
birthtimeMs: asset.fileCreatedAt.valueOf(),
|
||||||
} as Stats);
|
} as Stats);
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mockReadTags({
|
mockReadTags({
|
||||||
Directory: 'foo/bar/',
|
Directory: 'foo/bar/',
|
||||||
EmbeddedVideoFile: new BinaryField(0, ''),
|
EmbeddedVideoFile: new BinaryField(0, ''),
|
||||||
@@ -733,7 +734,7 @@ describe(MetadataService.name, () => {
|
|||||||
it('should extract the motion photo video from the XMP directory entry ', async () => {
|
it('should extract the motion photo video from the XMP directory entry ', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden });
|
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({
|
mocks.storage.stat.mockResolvedValue({
|
||||||
size: 123_456,
|
size: 123_456,
|
||||||
mtime: asset.fileModifiedAt,
|
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 () => {
|
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 motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden });
|
||||||
const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id });
|
const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id });
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mockReadTags({
|
mockReadTags({
|
||||||
Directory: 'foo/bar/',
|
Directory: 'foo/bar/',
|
||||||
MotionPhoto: 1,
|
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 () => {
|
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 motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden });
|
||||||
const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id });
|
const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id });
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mockReadTags({
|
mockReadTags({
|
||||||
Directory: 'foo/bar/',
|
Directory: 'foo/bar/',
|
||||||
MotionPhoto: 1,
|
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 () => {
|
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 motionAsset = AssetFactory.create({ type: AssetType.Video });
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mockReadTags({
|
mockReadTags({
|
||||||
Directory: 'foo/bar/',
|
Directory: 'foo/bar/',
|
||||||
MotionPhoto: 1,
|
MotionPhoto: 1,
|
||||||
@@ -859,7 +860,7 @@ describe(MetadataService.name, () => {
|
|||||||
it('should not update storage usage if motion photo is external', async () => {
|
it('should not update storage usage if motion photo is external', async () => {
|
||||||
const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden });
|
const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden });
|
||||||
const asset = AssetFactory.create({ isExternal: true });
|
const asset = AssetFactory.create({ isExternal: true });
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mockReadTags({
|
mockReadTags({
|
||||||
Directory: 'foo/bar/',
|
Directory: 'foo/bar/',
|
||||||
MotionPhoto: 1,
|
MotionPhoto: 1,
|
||||||
@@ -904,7 +905,7 @@ describe(MetadataService.name, () => {
|
|||||||
Rating: 3,
|
Rating: 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mockReadTags(tags);
|
mockReadTags(tags);
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: asset.id });
|
await sut.handleMetadataExtraction({ id: asset.id });
|
||||||
@@ -969,7 +970,7 @@ describe(MetadataService.name, () => {
|
|||||||
DateTimeOriginal: ExifDateTime.fromISO(someDate + '+00:00'),
|
DateTimeOriginal: ExifDateTime.fromISO(someDate + '+00:00'),
|
||||||
zone: undefined,
|
zone: undefined,
|
||||||
};
|
};
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mockReadTags(tags);
|
mockReadTags(tags);
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: asset.id });
|
await sut.handleMetadataExtraction({ id: asset.id });
|
||||||
@@ -984,7 +985,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should extract duration', async () => {
|
it('should extract duration', async () => {
|
||||||
const asset = AssetFactory.create({ type: AssetType.Video });
|
const asset = AssetFactory.create({ type: AssetType.Video });
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mocks.media.probe.mockResolvedValue({
|
mocks.media.probe.mockResolvedValue({
|
||||||
...probeStub.videoStreamH264,
|
...probeStub.videoStreamH264,
|
||||||
format: {
|
format: {
|
||||||
@@ -1007,7 +1008,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should only extract duration for videos', async () => {
|
it('should only extract duration for videos', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mocks.media.probe.mockResolvedValue({
|
mocks.media.probe.mockResolvedValue({
|
||||||
...probeStub.videoStreamH264,
|
...probeStub.videoStreamH264,
|
||||||
format: {
|
format: {
|
||||||
@@ -1029,7 +1030,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should omit duration of zero', async () => {
|
it('should omit duration of zero', async () => {
|
||||||
const asset = AssetFactory.create({ type: AssetType.Video });
|
const asset = AssetFactory.create({ type: AssetType.Video });
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mocks.media.probe.mockResolvedValue({
|
mocks.media.probe.mockResolvedValue({
|
||||||
...probeStub.videoStreamH264,
|
...probeStub.videoStreamH264,
|
||||||
format: {
|
format: {
|
||||||
@@ -1052,7 +1053,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should a handle duration of 1 week', async () => {
|
it('should a handle duration of 1 week', async () => {
|
||||||
const asset = AssetFactory.create({ type: AssetType.Video });
|
const asset = AssetFactory.create({ type: AssetType.Video });
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mocks.media.probe.mockResolvedValue({
|
mocks.media.probe.mockResolvedValue({
|
||||||
...probeStub.videoStreamH264,
|
...probeStub.videoStreamH264,
|
||||||
format: {
|
format: {
|
||||||
@@ -1075,7 +1076,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should use Duration from exif', async () => {
|
it('should use Duration from exif', async () => {
|
||||||
const asset = AssetFactory.create({ originalFileName: 'file.webp' });
|
const asset = AssetFactory.create({ originalFileName: 'file.webp' });
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mockReadTags({ Duration: 123 }, {});
|
mockReadTags({ Duration: 123 }, {});
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: asset.id });
|
await sut.handleMetadataExtraction({ id: asset.id });
|
||||||
@@ -1086,7 +1087,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should prefer Duration from exif over sidecar', async () => {
|
it('should prefer Duration from exif over sidecar', async () => {
|
||||||
const asset = AssetFactory.from({ originalFileName: 'file.webp' }).file({ type: AssetFileType.Sidecar }).build();
|
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 });
|
mockReadTags({ Duration: 123 }, { Duration: 456 });
|
||||||
|
|
||||||
@@ -1098,7 +1099,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should ignore all Duration tags for definitely static images', async () => {
|
it('should ignore all Duration tags for definitely static images', async () => {
|
||||||
const asset = AssetFactory.from({ originalFileName: 'file.dng' }).build();
|
const asset = AssetFactory.from({ originalFileName: 'file.dng' }).build();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mockReadTags({ Duration: 123 }, { Duration: 456 });
|
mockReadTags({ Duration: 123 }, { Duration: 456 });
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: asset.id });
|
await sut.handleMetadataExtraction({ id: asset.id });
|
||||||
@@ -1109,7 +1110,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should ignore Duration from exif for videos', async () => {
|
it('should ignore Duration from exif for videos', async () => {
|
||||||
const asset = AssetFactory.create({ type: AssetType.Video });
|
const asset = AssetFactory.create({ type: AssetType.Video });
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mockReadTags({ Duration: 123 }, {});
|
mockReadTags({ Duration: 123 }, {});
|
||||||
mocks.media.probe.mockResolvedValue({
|
mocks.media.probe.mockResolvedValue({
|
||||||
...probeStub.videoStreamH264,
|
...probeStub.videoStreamH264,
|
||||||
@@ -1127,7 +1128,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should trim whitespace from description', async () => {
|
it('should trim whitespace from description', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mockReadTags({ Description: '\t \v \f \n \r' });
|
mockReadTags({ Description: '\t \v \f \n \r' });
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: asset.id });
|
await sut.handleMetadataExtraction({ id: asset.id });
|
||||||
@@ -1150,7 +1151,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should handle a numeric description', async () => {
|
it('should handle a numeric description', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mockReadTags({ Description: 1000 });
|
mockReadTags({ Description: 1000 });
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: asset.id });
|
await sut.handleMetadataExtraction({ id: asset.id });
|
||||||
@@ -1164,7 +1165,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should skip importing metadata when the feature is disabled', async () => {
|
it('should skip importing metadata when the feature is disabled', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: false } } });
|
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: false } } });
|
||||||
mockReadTags(makeFaceTags({ Name: 'Person 1' }));
|
mockReadTags(makeFaceTags({ Name: 'Person 1' }));
|
||||||
await sut.handleMetadataExtraction({ id: asset.id });
|
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 () => {
|
it('should skip importing metadata face for assets without tags.RegionInfo', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
||||||
mockReadTags();
|
mockReadTags();
|
||||||
await sut.handleMetadataExtraction({ id: asset.id });
|
await sut.handleMetadataExtraction({ id: asset.id });
|
||||||
@@ -1182,7 +1183,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should skip importing faces without name', async () => {
|
it('should skip importing faces without name', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
||||||
mockReadTags(makeFaceTags());
|
mockReadTags(makeFaceTags());
|
||||||
mocks.person.getDistinctNames.mockResolvedValue([]);
|
mocks.person.getDistinctNames.mockResolvedValue([]);
|
||||||
@@ -1195,7 +1196,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should skip importing faces with empty name', async () => {
|
it('should skip importing faces with empty name', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
||||||
mockReadTags(makeFaceTags({ Name: '' }));
|
mockReadTags(makeFaceTags({ Name: '' }));
|
||||||
mocks.person.getDistinctNames.mockResolvedValue([]);
|
mocks.person.getDistinctNames.mockResolvedValue([]);
|
||||||
@@ -1210,7 +1211,7 @@ describe(MetadataService.name, () => {
|
|||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
const person = PersonFactory.create();
|
const person = PersonFactory.create();
|
||||||
|
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
||||||
mockReadTags(makeFaceTags({ Name: person.name }));
|
mockReadTags(makeFaceTags({ Name: person.name }));
|
||||||
mocks.person.getDistinctNames.mockResolvedValue([]);
|
mocks.person.getDistinctNames.mockResolvedValue([]);
|
||||||
@@ -1252,7 +1253,7 @@ describe(MetadataService.name, () => {
|
|||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
const person = PersonFactory.create();
|
const person = PersonFactory.create();
|
||||||
|
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
||||||
mockReadTags(makeFaceTags({ Name: person.name }));
|
mockReadTags(makeFaceTags({ Name: person.name }));
|
||||||
mocks.person.getDistinctNames.mockResolvedValue([{ id: person.id, 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 asset = AssetFactory.create();
|
||||||
const person = PersonFactory.create();
|
const person = PersonFactory.create();
|
||||||
|
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
||||||
mockReadTags(makeFaceTags({ Name: person.name }, orientation));
|
mockReadTags(makeFaceTags({ Name: person.name }, orientation));
|
||||||
mocks.person.getDistinctNames.mockResolvedValue([]);
|
mocks.person.getDistinctNames.mockResolvedValue([]);
|
||||||
@@ -1383,7 +1384,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should handle invalid modify date', async () => {
|
it('should handle invalid modify date', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mockReadTags({ ModifyDate: '00:00:00.000' });
|
mockReadTags({ ModifyDate: '00:00:00.000' });
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: asset.id });
|
await sut.handleMetadataExtraction({ id: asset.id });
|
||||||
@@ -1397,7 +1398,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should handle invalid rating value', async () => {
|
it('should handle invalid rating value', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mockReadTags({ Rating: 6 });
|
mockReadTags({ Rating: 6 });
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: asset.id });
|
await sut.handleMetadataExtraction({ id: asset.id });
|
||||||
@@ -1411,7 +1412,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should handle valid rating value', async () => {
|
it('should handle valid rating value', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mockReadTags({ Rating: 5 });
|
mockReadTags({ Rating: 5 });
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: asset.id });
|
await sut.handleMetadataExtraction({ id: asset.id });
|
||||||
@@ -1425,7 +1426,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should handle 0 as unrated -> null', async () => {
|
it('should handle 0 as unrated -> null', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mockReadTags({ Rating: 0 });
|
mockReadTags({ Rating: 0 });
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: asset.id });
|
await sut.handleMetadataExtraction({ id: asset.id });
|
||||||
@@ -1439,7 +1440,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should handle valid negative rating value', async () => {
|
it('should handle valid negative rating value', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mockReadTags({ Rating: -1 });
|
mockReadTags({ Rating: -1 });
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: asset.id });
|
await sut.handleMetadataExtraction({ id: asset.id });
|
||||||
@@ -1453,7 +1454,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should handle livePhotoCID not set', async () => {
|
it('should handle livePhotoCID not set', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: asset.id });
|
await sut.handleMetadataExtraction({ id: asset.id });
|
||||||
|
|
||||||
@@ -1468,7 +1469,7 @@ describe(MetadataService.name, () => {
|
|||||||
it('should handle not finding a match', async () => {
|
it('should handle not finding a match', async () => {
|
||||||
const asset = AssetFactory.create({ type: AssetType.Video });
|
const asset = AssetFactory.create({ type: AssetType.Video });
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.videoStreamVertical2160p);
|
mocks.media.probe.mockResolvedValue(probeStub.videoStreamVertical2160p);
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mockReadTags({ ContentIdentifier: 'CID' });
|
mockReadTags({ ContentIdentifier: 'CID' });
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: asset.id });
|
await sut.handleMetadataExtraction({ id: asset.id });
|
||||||
@@ -1490,7 +1491,7 @@ describe(MetadataService.name, () => {
|
|||||||
it('should link photo and video', async () => {
|
it('should link photo and video', async () => {
|
||||||
const motionAsset = AssetFactory.create({ type: AssetType.Video });
|
const motionAsset = AssetFactory.create({ type: AssetType.Video });
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mocks.asset.findLivePhotoMatch.mockResolvedValue(motionAsset);
|
mocks.asset.findLivePhotoMatch.mockResolvedValue(motionAsset);
|
||||||
mockReadTags({ ContentIdentifier: 'CID' });
|
mockReadTags({ ContentIdentifier: 'CID' });
|
||||||
|
|
||||||
@@ -1518,7 +1519,7 @@ describe(MetadataService.name, () => {
|
|||||||
it('should notify clients on live photo link', async () => {
|
it('should notify clients on live photo link', async () => {
|
||||||
const motionAsset = AssetFactory.create({ type: AssetType.Video });
|
const motionAsset = AssetFactory.create({ type: AssetType.Video });
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mocks.asset.findLivePhotoMatch.mockResolvedValue(motionAsset);
|
mocks.asset.findLivePhotoMatch.mockResolvedValue(motionAsset);
|
||||||
mockReadTags({ ContentIdentifier: 'CID' });
|
mockReadTags({ ContentIdentifier: 'CID' });
|
||||||
|
|
||||||
@@ -1533,7 +1534,7 @@ describe(MetadataService.name, () => {
|
|||||||
it('should search by libraryId', async () => {
|
it('should search by libraryId', async () => {
|
||||||
const motionAsset = AssetFactory.create({ type: AssetType.Video, libraryId: 'library-id' });
|
const motionAsset = AssetFactory.create({ type: AssetType.Video, libraryId: 'library-id' });
|
||||||
const asset = AssetFactory.create({ 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);
|
mocks.asset.findLivePhotoMatch.mockResolvedValue(motionAsset);
|
||||||
mockReadTags({ ContentIdentifier: 'CID' });
|
mockReadTags({ ContentIdentifier: 'CID' });
|
||||||
|
|
||||||
@@ -1570,7 +1571,7 @@ describe(MetadataService.name, () => {
|
|||||||
{ exif: { AndroidMake: '1', AndroidModel: '2' }, expected: { make: '1', model: '2' } },
|
{ exif: { AndroidMake: '1', AndroidModel: '2' }, expected: { make: '1', model: '2' } },
|
||||||
])('should read camera make and model $exif -> $expected', async ({ exif, expected }) => {
|
])('should read camera make and model $exif -> $expected', async ({ exif, expected }) => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mockReadTags(exif);
|
mockReadTags(exif);
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: asset.id });
|
await sut.handleMetadataExtraction({ id: asset.id });
|
||||||
@@ -1595,7 +1596,7 @@ describe(MetadataService.name, () => {
|
|||||||
{ exif: { LensID: '' }, expected: null },
|
{ exif: { LensID: '' }, expected: null },
|
||||||
])('should read camera lens information $exif -> $expected', async ({ exif, expected }) => {
|
])('should read camera lens information $exif -> $expected', async ({ exif, expected }) => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mockReadTags(exif);
|
mockReadTags(exif);
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: asset.id });
|
await sut.handleMetadataExtraction({ id: asset.id });
|
||||||
@@ -1609,7 +1610,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should properly set width/height for normal images', async () => {
|
it('should properly set width/height for normal images', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mockReadTags({ ImageWidth: 1000, ImageHeight: 2000 });
|
mockReadTags({ ImageWidth: 1000, ImageHeight: 2000 });
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: asset.id });
|
await sut.handleMetadataExtraction({ id: asset.id });
|
||||||
@@ -1623,7 +1624,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should properly swap asset width/height for rotated images', async () => {
|
it('should properly swap asset width/height for rotated images', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mockReadTags({ ImageWidth: 1000, ImageHeight: 2000, Orientation: 6 });
|
mockReadTags({ ImageWidth: 1000, ImageHeight: 2000, Orientation: 6 });
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: asset.id });
|
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 () => {
|
it('should not overwrite existing width/height if they already exist', async () => {
|
||||||
const asset = AssetFactory.create({ width: 1920, height: 1080 });
|
const asset = AssetFactory.create({ width: 1920, height: 1080 });
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||||
mockReadTags({ ImageWidth: 1280, ImageHeight: 720 });
|
mockReadTags({ ImageWidth: 1280, ImageHeight: 720 });
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: asset.id });
|
await sut.handleMetadataExtraction({ id: asset.id });
|
||||||
@@ -1754,17 +1755,20 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
it('should skip jobs with no metadata', async () => {
|
it('should skip jobs with no metadata', async () => {
|
||||||
mocks.assetJob.getLockedPropertiesForMetadataExtraction.mockResolvedValue([]);
|
mocks.assetJob.getLockedPropertiesForMetadataExtraction.mockResolvedValue([]);
|
||||||
const asset = factory.jobAssets.sidecarWrite();
|
const asset = AssetFactory.from().file({ type: AssetFileType.Sidecar }).exif().build();
|
||||||
mocks.assetJob.getForSidecarWriteJob.mockResolvedValue(asset);
|
mocks.assetJob.getForSidecarWriteJob.mockResolvedValue(getForSidecarWrite(asset));
|
||||||
await expect(sut.handleSidecarWrite({ id: asset.id })).resolves.toBe(JobStatus.Skipped);
|
await expect(sut.handleSidecarWrite({ id: asset.id })).resolves.toBe(JobStatus.Skipped);
|
||||||
expect(mocks.metadata.writeTags).not.toHaveBeenCalled();
|
expect(mocks.metadata.writeTags).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should write tags', async () => {
|
it('should write tags', async () => {
|
||||||
const asset = factory.jobAssets.sidecarWrite();
|
|
||||||
const description = 'this is a description';
|
const description = 'this is a description';
|
||||||
const gps = 12;
|
const gps = 12;
|
||||||
const date = '2023-11-21T22:56:12.196-06:00';
|
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([
|
mocks.assetJob.getLockedPropertiesForMetadataExtraction.mockResolvedValue([
|
||||||
'description',
|
'description',
|
||||||
@@ -1773,7 +1777,7 @@ describe(MetadataService.name, () => {
|
|||||||
'dateTimeOriginal',
|
'dateTimeOriginal',
|
||||||
'timeZone',
|
'timeZone',
|
||||||
]);
|
]);
|
||||||
mocks.assetJob.getForSidecarWriteJob.mockResolvedValue(asset);
|
mocks.assetJob.getForSidecarWriteJob.mockResolvedValue(getForSidecarWrite(asset));
|
||||||
await expect(
|
await expect(
|
||||||
sut.handleSidecarWrite({
|
sut.handleSidecarWrite({
|
||||||
id: asset.id,
|
id: asset.id,
|
||||||
@@ -1796,22 +1800,22 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should write rating', async () => {
|
it('should write rating', async () => {
|
||||||
const asset = factory.jobAssets.sidecarWrite();
|
const asset = AssetFactory.from().file({ type: AssetFileType.Sidecar }).exif().build();
|
||||||
asset.exifInfo.rating = 4;
|
asset.exifInfo.rating = 4;
|
||||||
|
|
||||||
mocks.assetJob.getLockedPropertiesForMetadataExtraction.mockResolvedValue(['rating']);
|
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);
|
await expect(sut.handleSidecarWrite({ id: asset.id })).resolves.toBe(JobStatus.Success);
|
||||||
expect(mocks.metadata.writeTags).toHaveBeenCalledWith(asset.files[0].path, { Rating: 4 });
|
expect(mocks.metadata.writeTags).toHaveBeenCalledWith(asset.files[0].path, { Rating: 4 });
|
||||||
expect(mocks.asset.unlockProperties).toHaveBeenCalledWith(asset.id, ['rating']);
|
expect(mocks.asset.unlockProperties).toHaveBeenCalledWith(asset.id, ['rating']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should write null rating as 0', async () => {
|
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;
|
asset.exifInfo.rating = null;
|
||||||
|
|
||||||
mocks.assetJob.getLockedPropertiesForMetadataExtraction.mockResolvedValue(['rating']);
|
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);
|
await expect(sut.handleSidecarWrite({ id: asset.id })).resolves.toBe(JobStatus.Success);
|
||||||
expect(mocks.metadata.writeTags).toHaveBeenCalledWith(asset.files[0].path, { Rating: 0 });
|
expect(mocks.metadata.writeTags).toHaveBeenCalledWith(asset.files[0].path, { Rating: 0 });
|
||||||
expect(mocks.asset.unlockProperties).toHaveBeenCalledWith(asset.id, ['rating']);
|
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 { join, parse } from 'node:path';
|
||||||
import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants';
|
import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
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 { OnEvent, OnJob } from 'src/decorators';
|
||||||
import {
|
import {
|
||||||
AssetFileType,
|
AssetFileType,
|
||||||
@@ -447,8 +447,7 @@ export class MetadataService extends BaseService {
|
|||||||
const { description, dateTimeOriginal, latitude, longitude, rating, tags, timeZone } = _.pick(
|
const { description, dateTimeOriginal, latitude, longitude, rating, tags, timeZone } = _.pick(
|
||||||
{
|
{
|
||||||
description: asset.exifInfo.description,
|
description: asset.exifInfo.description,
|
||||||
// the kysely type is wrong here; fixed in 0.28.3
|
dateTimeOriginal: asset.exifInfo.dateTimeOriginal,
|
||||||
dateTimeOriginal: asset.exifInfo.dateTimeOriginal as string | null,
|
|
||||||
latitude: asset.exifInfo.latitude,
|
latitude: asset.exifInfo.latitude,
|
||||||
longitude: asset.exifInfo.longitude,
|
longitude: asset.exifInfo.longitude,
|
||||||
rating: asset.exifInfo.rating ?? 0,
|
rating: asset.exifInfo.rating ?? 0,
|
||||||
@@ -829,7 +828,7 @@ export class MetadataService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async applyTaggedFaces(
|
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,
|
tags: ImmichTags,
|
||||||
) {
|
) {
|
||||||
if (!tags.RegionInfo?.AppliedToDimensions || tags.RegionInfo.RegionList.length === 0) {
|
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 { UserFactory } from 'test/factories/user.factory';
|
||||||
import { notificationStub } from 'test/fixtures/notification.stub';
|
import { notificationStub } from 'test/fixtures/notification.stub';
|
||||||
import { userStub } from 'test/fixtures/user.stub';
|
import { userStub } from 'test/fixtures/user.stub';
|
||||||
|
import { getForAlbum } from 'test/mappers';
|
||||||
import { newUuid } from 'test/small.factory';
|
import { newUuid } from 'test/small.factory';
|
||||||
import { newTestService, ServiceMocks } from 'test/utils';
|
import { newTestService, ServiceMocks } from 'test/utils';
|
||||||
|
|
||||||
@@ -269,14 +270,14 @@ describe(NotificationService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should skip if recipient could not be found', async () => {
|
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);
|
await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Skipped);
|
||||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should skip if the recipient has email notifications disabled', async () => {
|
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({
|
mocks.user.get.mockResolvedValue({
|
||||||
...userStub.user1,
|
...userStub.user1,
|
||||||
metadata: [
|
metadata: [
|
||||||
@@ -292,7 +293,7 @@ describe(NotificationService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should skip if the recipient has email notifications for album invite disabled', async () => {
|
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({
|
mocks.user.get.mockResolvedValue({
|
||||||
...userStub.user1,
|
...userStub.user1,
|
||||||
metadata: [
|
metadata: [
|
||||||
@@ -308,7 +309,7 @@ describe(NotificationService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should send invite email', async () => {
|
it('should send invite email', async () => {
|
||||||
mocks.album.getById.mockResolvedValue(AlbumFactory.create());
|
mocks.album.getById.mockResolvedValue(getForAlbum(AlbumFactory.create()));
|
||||||
mocks.user.get.mockResolvedValue({
|
mocks.user.get.mockResolvedValue({
|
||||||
...userStub.user1,
|
...userStub.user1,
|
||||||
metadata: [
|
metadata: [
|
||||||
@@ -331,7 +332,7 @@ describe(NotificationService.name, () => {
|
|||||||
|
|
||||||
it('should send invite email without album thumbnail if thumbnail asset does not exist', async () => {
|
it('should send invite email without album thumbnail if thumbnail asset does not exist', async () => {
|
||||||
const album = AlbumFactory.create({ albumThumbnailAssetId: newUuid() });
|
const album = AlbumFactory.create({ albumThumbnailAssetId: newUuid() });
|
||||||
mocks.album.getById.mockResolvedValue(album);
|
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||||
mocks.user.get.mockResolvedValue({
|
mocks.user.get.mockResolvedValue({
|
||||||
...userStub.user1,
|
...userStub.user1,
|
||||||
metadata: [
|
metadata: [
|
||||||
@@ -363,7 +364,7 @@ describe(NotificationService.name, () => {
|
|||||||
it('should send invite email with album thumbnail as jpeg', async () => {
|
it('should send invite email with album thumbnail as jpeg', async () => {
|
||||||
const assetFile = AssetFileFactory.create({ type: AssetFileType.Thumbnail });
|
const assetFile = AssetFileFactory.create({ type: AssetFileType.Thumbnail });
|
||||||
const album = AlbumFactory.create({ albumThumbnailAssetId: assetFile.assetId });
|
const album = AlbumFactory.create({ albumThumbnailAssetId: assetFile.assetId });
|
||||||
mocks.album.getById.mockResolvedValue(album);
|
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||||
mocks.user.get.mockResolvedValue({
|
mocks.user.get.mockResolvedValue({
|
||||||
...userStub.user1,
|
...userStub.user1,
|
||||||
metadata: [
|
metadata: [
|
||||||
@@ -394,8 +395,10 @@ describe(NotificationService.name, () => {
|
|||||||
|
|
||||||
it('should send invite email with album thumbnail and arbitrary extension', async () => {
|
it('should send invite email with album thumbnail and arbitrary extension', async () => {
|
||||||
const asset = AssetFactory.from().file({ type: AssetFileType.Thumbnail }).build();
|
const asset = AssetFactory.from().file({ type: AssetFileType.Thumbnail }).build();
|
||||||
const album = AlbumFactory.from({ albumThumbnailAssetId: asset.id }).asset(asset).build();
|
const album = AlbumFactory.from({ albumThumbnailAssetId: asset.id })
|
||||||
mocks.album.getById.mockResolvedValue(album);
|
.asset(asset, (builder) => builder.exif())
|
||||||
|
.build();
|
||||||
|
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||||
mocks.user.get.mockResolvedValue({
|
mocks.user.get.mockResolvedValue({
|
||||||
...userStub.user1,
|
...userStub.user1,
|
||||||
metadata: [
|
metadata: [
|
||||||
@@ -432,7 +435,7 @@ describe(NotificationService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should skip if owner could not be found', async () => {
|
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);
|
await expect(sut.handleAlbumUpdate({ id: '', recipientId: '1' })).resolves.toBe(JobStatus.Skipped);
|
||||||
expect(mocks.systemMetadata.get).not.toHaveBeenCalled();
|
expect(mocks.systemMetadata.get).not.toHaveBeenCalled();
|
||||||
@@ -440,7 +443,7 @@ describe(NotificationService.name, () => {
|
|||||||
|
|
||||||
it('should skip recipient that could not be looked up', async () => {
|
it('should skip recipient that could not be looked up', async () => {
|
||||||
const album = AlbumFactory.from().albumUser({ userId: 'non-existent' }).build();
|
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.user.get.mockResolvedValueOnce(album.owner);
|
||||||
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
|
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
|
||||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||||
@@ -459,7 +462,7 @@ describe(NotificationService.name, () => {
|
|||||||
})
|
})
|
||||||
.build();
|
.build();
|
||||||
const album = AlbumFactory.from().albumUser({ userId: user.id }).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.user.get.mockResolvedValue(user);
|
||||||
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
|
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
|
||||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||||
@@ -478,7 +481,7 @@ describe(NotificationService.name, () => {
|
|||||||
})
|
})
|
||||||
.build();
|
.build();
|
||||||
const album = AlbumFactory.from().albumUser({ userId: user.id }).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.user.get.mockResolvedValue(user);
|
||||||
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
|
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
|
||||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||||
@@ -492,7 +495,7 @@ describe(NotificationService.name, () => {
|
|||||||
it('should send email', async () => {
|
it('should send email', async () => {
|
||||||
const user = UserFactory.create();
|
const user = UserFactory.create();
|
||||||
const album = AlbumFactory.from().albumUser({ userId: user.id }).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.user.get.mockResolvedValue(user);
|
||||||
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
|
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
|
||||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { BadRequestException } from '@nestjs/common';
|
import { BadRequestException } from '@nestjs/common';
|
||||||
import { PartnerDirection } from 'src/repositories/partner.repository';
|
import { PartnerDirection } from 'src/repositories/partner.repository';
|
||||||
import { PartnerService } from 'src/services/partner.service';
|
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 { factory } from 'test/small.factory';
|
||||||
import { newTestService, ServiceMocks } from 'test/utils';
|
import { newTestService, ServiceMocks } from 'test/utils';
|
||||||
|
|
||||||
@@ -18,26 +20,38 @@ describe(PartnerService.name, () => {
|
|||||||
|
|
||||||
describe('search', () => {
|
describe('search', () => {
|
||||||
it("should return a list of partners with whom I've shared my library", async () => {
|
it("should return a list of partners with whom I've shared my library", async () => {
|
||||||
const user1 = factory.user();
|
const user1 = UserFactory.create();
|
||||||
const user2 = factory.user();
|
const user2 = UserFactory.create();
|
||||||
const sharedWithUser2 = factory.partner({ sharedBy: user1, sharedWith: user2 });
|
const sharedWithUser2 = factory.partner({
|
||||||
const sharedWithUser1 = factory.partner({ sharedBy: user2, sharedWith: user1 });
|
sharedBy: getDehydrated(user1),
|
||||||
|
sharedWith: getDehydrated(user2),
|
||||||
|
});
|
||||||
|
const sharedWithUser1 = factory.partner({
|
||||||
|
sharedBy: getDehydrated(user2),
|
||||||
|
sharedWith: getDehydrated(user1),
|
||||||
|
});
|
||||||
const auth = factory.auth({ user: { id: user1.id } });
|
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();
|
await expect(sut.search(auth, { direction: PartnerDirection.SharedBy })).resolves.toBeDefined();
|
||||||
expect(mocks.partner.getAll).toHaveBeenCalledWith(user1.id);
|
expect(mocks.partner.getAll).toHaveBeenCalledWith(user1.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return a list of partners who have shared their libraries with me', async () => {
|
it('should return a list of partners who have shared their libraries with me', async () => {
|
||||||
const user1 = factory.user();
|
const user1 = UserFactory.create();
|
||||||
const user2 = factory.user();
|
const user2 = UserFactory.create();
|
||||||
const sharedWithUser2 = factory.partner({ sharedBy: user1, sharedWith: user2 });
|
const sharedWithUser2 = factory.partner({
|
||||||
const sharedWithUser1 = factory.partner({ sharedBy: user2, sharedWith: user1 });
|
sharedBy: getDehydrated(user1),
|
||||||
|
sharedWith: getDehydrated(user2),
|
||||||
|
});
|
||||||
|
const sharedWithUser1 = factory.partner({
|
||||||
|
sharedBy: getDehydrated(user2),
|
||||||
|
sharedWith: getDehydrated(user1),
|
||||||
|
});
|
||||||
const auth = factory.auth({ user: { id: user1.id } });
|
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();
|
await expect(sut.search(auth, { direction: PartnerDirection.SharedWith })).resolves.toBeDefined();
|
||||||
expect(mocks.partner.getAll).toHaveBeenCalledWith(user1.id);
|
expect(mocks.partner.getAll).toHaveBeenCalledWith(user1.id);
|
||||||
});
|
});
|
||||||
@@ -45,13 +59,13 @@ describe(PartnerService.name, () => {
|
|||||||
|
|
||||||
describe('create', () => {
|
describe('create', () => {
|
||||||
it('should create a new partner', async () => {
|
it('should create a new partner', async () => {
|
||||||
const user1 = factory.user();
|
const user1 = UserFactory.create();
|
||||||
const user2 = factory.user();
|
const user2 = UserFactory.create();
|
||||||
const partner = factory.partner({ sharedBy: user1, sharedWith: user2 });
|
const partner = factory.partner({ sharedBy: getDehydrated(user1), sharedWith: getDehydrated(user2) });
|
||||||
const auth = factory.auth({ user: { id: user1.id } });
|
const auth = factory.auth({ user: { id: user1.id } });
|
||||||
|
|
||||||
mocks.partner.get.mockResolvedValue(void 0);
|
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();
|
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 () => {
|
it('should throw an error when the partner already exists', async () => {
|
||||||
const user1 = factory.user();
|
const user1 = UserFactory.create();
|
||||||
const user2 = factory.user();
|
const user2 = UserFactory.create();
|
||||||
const partner = factory.partner({ sharedBy: user1, sharedWith: user2 });
|
const partner = factory.partner({ sharedBy: getDehydrated(user1), sharedWith: getDehydrated(user2) });
|
||||||
const auth = factory.auth({ user: { id: user1.id } });
|
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);
|
await expect(sut.create(auth, { sharedWithId: user2.id })).rejects.toBeInstanceOf(BadRequestException);
|
||||||
|
|
||||||
@@ -77,12 +91,12 @@ describe(PartnerService.name, () => {
|
|||||||
|
|
||||||
describe('remove', () => {
|
describe('remove', () => {
|
||||||
it('should remove a partner', async () => {
|
it('should remove a partner', async () => {
|
||||||
const user1 = factory.user();
|
const user1 = UserFactory.create();
|
||||||
const user2 = factory.user();
|
const user2 = UserFactory.create();
|
||||||
const partner = factory.partner({ sharedBy: user1, sharedWith: user2 });
|
const partner = factory.partner({ sharedBy: getDehydrated(user1), sharedWith: getDehydrated(user2) });
|
||||||
const auth = factory.auth({ user: { id: user1.id } });
|
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);
|
await sut.remove(auth, user2.id);
|
||||||
|
|
||||||
@@ -110,13 +124,13 @@ describe(PartnerService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should update partner', async () => {
|
it('should update partner', async () => {
|
||||||
const user1 = factory.user();
|
const user1 = UserFactory.create();
|
||||||
const user2 = factory.user();
|
const user2 = UserFactory.create();
|
||||||
const partner = factory.partner({ sharedBy: user1, sharedWith: user2 });
|
const partner = factory.partner({ sharedBy: getDehydrated(user1), sharedWith: getDehydrated(user2) });
|
||||||
const auth = factory.auth({ user: { id: user1.id } });
|
const auth = factory.auth({ user: { id: user1.id } });
|
||||||
|
|
||||||
mocks.access.partner.checkUpdateAccess.mockResolvedValue(new Set([user2.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();
|
await expect(sut.update(auth, user2.id, { inTimeline: true })).resolves.toBeDefined();
|
||||||
expect(mocks.partner.update).toHaveBeenCalledWith(
|
expect(mocks.partner.update).toHaveBeenCalledWith(
|
||||||
|
|||||||
@@ -49,9 +49,8 @@ export class PartnerService extends BaseService {
|
|||||||
|
|
||||||
private mapPartner(partner: Partner, direction: PartnerDirection): PartnerResponseDto {
|
private mapPartner(partner: Partner, direction: PartnerDirection): PartnerResponseDto {
|
||||||
// this is opposite to return the non-me user of the "partner"
|
// this is opposite to return the non-me user of the "partner"
|
||||||
const user = mapUser(
|
const sharedUser = direction === PartnerDirection.SharedBy ? partner.sharedWith : partner.sharedBy;
|
||||||
direction === PartnerDirection.SharedBy ? partner.sharedWith : partner.sharedBy,
|
const user = mapUser(sharedUser);
|
||||||
) as PartnerResponseDto;
|
|
||||||
|
|
||||||
return { ...user, inTimeline: partner.inTimeline };
|
return { ...user, inTimeline: partner.inTimeline };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { PersonFactory } from 'test/factories/person.factory';
|
|||||||
import { UserFactory } from 'test/factories/user.factory';
|
import { UserFactory } from 'test/factories/user.factory';
|
||||||
import { authStub } from 'test/fixtures/auth.stub';
|
import { authStub } from 'test/fixtures/auth.stub';
|
||||||
import { systemConfigStub } from 'test/fixtures/system-config.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 { newDate, newUuid } from 'test/small.factory';
|
||||||
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
||||||
|
|
||||||
@@ -202,16 +202,16 @@ describe(PersonService.name, () => {
|
|||||||
mocks.person.update.mockResolvedValue(person);
|
mocks.person.update.mockResolvedValue(person);
|
||||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id]));
|
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,
|
id: person.id,
|
||||||
name: person.name,
|
name: person.name,
|
||||||
birthDate: '1976-06-30',
|
birthDate: '1976-06-30',
|
||||||
thumbnailPath: person.thumbnailPath,
|
thumbnailPath: person.thumbnailPath,
|
||||||
isHidden: false,
|
isHidden: false,
|
||||||
isFavorite: 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.queue).not.toHaveBeenCalled();
|
||||||
expect(mocks.job.queueAll).not.toHaveBeenCalled();
|
expect(mocks.job.queueAll).not.toHaveBeenCalled();
|
||||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id]));
|
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.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id]));
|
||||||
mocks.person.getById.mockResolvedValue(person);
|
mocks.person.getById.mockResolvedValue(person);
|
||||||
mocks.access.person.checkFaceOwnerAccess.mockResolvedValue(new Set([face.id]));
|
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.reassignFace.mockResolvedValue(1);
|
||||||
mocks.person.getRandomFace.mockResolvedValue(AssetFaceFactory.create());
|
mocks.person.getRandomFace.mockResolvedValue(AssetFaceFactory.create());
|
||||||
mocks.person.refreshFaces.mockResolvedValue();
|
mocks.person.refreshFaces.mockResolvedValue();
|
||||||
@@ -353,15 +353,17 @@ describe(PersonService.name, () => {
|
|||||||
const face = AssetFaceFactory.create();
|
const face = AssetFaceFactory.create();
|
||||||
const asset = AssetFactory.from({ id: face.assetId }).exif().build();
|
const asset = AssetFactory.from({ id: face.assetId }).exif().build();
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
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 });
|
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 () => {
|
it('should reject if the user has not access to the asset', async () => {
|
||||||
const face = AssetFaceFactory.create();
|
const face = AssetFaceFactory.create();
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set());
|
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(
|
await expect(sut.getFacesById(AuthFactory.create(), { id: face.assetId })).rejects.toBeInstanceOf(
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
);
|
);
|
||||||
@@ -390,7 +392,7 @@ describe(PersonService.name, () => {
|
|||||||
|
|
||||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id]));
|
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id]));
|
||||||
mocks.access.person.checkFaceOwnerAccess.mockResolvedValue(new Set([face.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.reassignFace.mockResolvedValue(1);
|
||||||
mocks.person.getById.mockResolvedValue(person);
|
mocks.person.getById.mockResolvedValue(person);
|
||||||
await expect(sut.reassignFacesById(AuthFactory.create(), person.id, { id: face.id })).resolves.toEqual({
|
await expect(sut.reassignFacesById(AuthFactory.create(), person.id, { id: face.id })).resolves.toEqual({
|
||||||
@@ -400,7 +402,7 @@ describe(PersonService.name, () => {
|
|||||||
id: person.id,
|
id: person.id,
|
||||||
name: person.name,
|
name: person.name,
|
||||||
thumbnailPath: person.thumbnailPath,
|
thumbnailPath: person.thumbnailPath,
|
||||||
updatedAt: expect.any(Date),
|
updatedAt: expect.any(String),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mocks.job.queue).not.toHaveBeenCalledWith();
|
expect(mocks.job.queue).not.toHaveBeenCalledWith();
|
||||||
@@ -412,7 +414,7 @@ describe(PersonService.name, () => {
|
|||||||
const person = PersonFactory.create();
|
const person = PersonFactory.create();
|
||||||
|
|
||||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id]));
|
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.reassignFace.mockResolvedValue(1);
|
||||||
mocks.person.getById.mockResolvedValue(person);
|
mocks.person.getById.mockResolvedValue(person);
|
||||||
await expect(
|
await expect(
|
||||||
@@ -735,18 +737,18 @@ describe(PersonService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should skip when no resize path', async () => {
|
it('should skip when no resize path', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.from().exif().build();
|
||||||
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset);
|
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(getForDetectedFaces(asset));
|
||||||
await sut.handleDetectFaces({ id: asset.id });
|
await sut.handleDetectFaces({ id: asset.id });
|
||||||
expect(mocks.machineLearning.detectFaces).not.toHaveBeenCalled();
|
expect(mocks.machineLearning.detectFaces).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle no results', async () => {
|
it('should handle no results', async () => {
|
||||||
const start = Date.now();
|
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.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 });
|
await sut.handleDetectFaces({ id: asset.id });
|
||||||
expect(mocks.machineLearning.detectFaces).toHaveBeenCalledWith(
|
expect(mocks.machineLearning.detectFaces).toHaveBeenCalledWith(
|
||||||
asset.files[0].path,
|
asset.files[0].path,
|
||||||
@@ -764,12 +766,12 @@ describe(PersonService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should create a face with no person and queue recognition job', async () => {
|
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 });
|
const face = AssetFaceFactory.create({ assetId: asset.id });
|
||||||
mocks.crypto.randomUUID.mockReturnValue(face.id);
|
mocks.crypto.randomUUID.mockReturnValue(face.id);
|
||||||
mocks.machineLearning.detectFaces.mockResolvedValue(getAsDetectedFace(face));
|
mocks.machineLearning.detectFaces.mockResolvedValue(getAsDetectedFace(face));
|
||||||
mocks.search.searchFaces.mockResolvedValue([{ ...face, distance: 0.7 }]);
|
mocks.search.searchFaces.mockResolvedValue([{ ...face, distance: 0.7 }]);
|
||||||
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset);
|
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(getForDetectedFaces(asset));
|
||||||
mocks.person.refreshFaces.mockResolvedValue();
|
mocks.person.refreshFaces.mockResolvedValue();
|
||||||
|
|
||||||
await sut.handleDetectFaces({ id: asset.id });
|
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 () => {
|
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.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 });
|
await sut.handleDetectFaces({ id: asset.id });
|
||||||
|
|
||||||
@@ -809,9 +811,9 @@ describe(PersonService.name, () => {
|
|||||||
boundingBoxY1: 200,
|
boundingBoxY1: 200,
|
||||||
boundingBoxY2: 300,
|
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.machineLearning.detectFaces.mockResolvedValue(getAsDetectedFace(face));
|
||||||
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset);
|
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(getForDetectedFaces(asset));
|
||||||
mocks.crypto.randomUUID.mockReturnValue(face.id);
|
mocks.crypto.randomUUID.mockReturnValue(face.id);
|
||||||
mocks.person.refreshFaces.mockResolvedValue();
|
mocks.person.refreshFaces.mockResolvedValue();
|
||||||
|
|
||||||
@@ -832,9 +834,9 @@ describe(PersonService.name, () => {
|
|||||||
|
|
||||||
it('should add embedding to matching metadata face', async () => {
|
it('should add embedding to matching metadata face', async () => {
|
||||||
const face = AssetFaceFactory.create({ sourceType: SourceType.Exif });
|
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.machineLearning.detectFaces.mockResolvedValue(getAsDetectedFace(face));
|
||||||
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset);
|
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(getForDetectedFaces(asset));
|
||||||
mocks.person.refreshFaces.mockResolvedValue();
|
mocks.person.refreshFaces.mockResolvedValue();
|
||||||
|
|
||||||
await sut.handleDetectFaces({ id: asset.id });
|
await sut.handleDetectFaces({ id: asset.id });
|
||||||
@@ -848,9 +850,9 @@ describe(PersonService.name, () => {
|
|||||||
it('should not add embedding to non-matching metadata face', async () => {
|
it('should not add embedding to non-matching metadata face', async () => {
|
||||||
const assetId = newUuid();
|
const assetId = newUuid();
|
||||||
const face = AssetFaceFactory.create({ assetId, sourceType: SourceType.Exif });
|
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.machineLearning.detectFaces.mockResolvedValue(getAsDetectedFace(face));
|
||||||
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset);
|
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(getForDetectedFaces(asset));
|
||||||
mocks.crypto.randomUUID.mockReturnValue(face.id);
|
mocks.crypto.randomUUID.mockReturnValue(face.id);
|
||||||
|
|
||||||
await sut.handleDetectFaces({ id: asset.id });
|
await sut.handleDetectFaces({ id: asset.id });
|
||||||
@@ -1237,7 +1239,7 @@ describe(PersonService.name, () => {
|
|||||||
const person = PersonFactory.create({ ownerId: user.id });
|
const person = PersonFactory.create({ ownerId: user.id });
|
||||||
const face = AssetFaceFactory.from().person(person).build();
|
const face = AssetFaceFactory.from().person(person).build();
|
||||||
|
|
||||||
expect(mapFaces(face, auth)).toEqual({
|
expect(mapFaces(getForAssetFace(face), auth)).toEqual({
|
||||||
boundingBoxX1: 100,
|
boundingBoxX1: 100,
|
||||||
boundingBoxX2: 200,
|
boundingBoxX2: 200,
|
||||||
boundingBoxY1: 100,
|
boundingBoxY1: 100,
|
||||||
@@ -1251,11 +1253,13 @@ describe(PersonService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not map person if person is null', () => {
|
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', () => {
|
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,
|
embedding: face.faceSearch.embedding,
|
||||||
maxDistance: machineLearning.facialRecognition.maxDistance,
|
maxDistance: machineLearning.facialRecognition.maxDistance,
|
||||||
numResults: machineLearning.facialRecognition.minFaces,
|
numResults: machineLearning.facialRecognition.minFaces,
|
||||||
minBirthDate: face.asset.fileCreatedAt ?? undefined,
|
minBirthDate: new Date(face.asset.fileCreatedAt),
|
||||||
});
|
});
|
||||||
|
|
||||||
// `matches` also includes the face itself
|
// `matches` also includes the face itself
|
||||||
@@ -519,7 +519,7 @@ export class PersonService extends BaseService {
|
|||||||
maxDistance: machineLearning.facialRecognition.maxDistance,
|
maxDistance: machineLearning.facialRecognition.maxDistance,
|
||||||
numResults: 1,
|
numResults: 1,
|
||||||
hasPerson: true,
|
hasPerson: true,
|
||||||
minBirthDate: face.asset.fileCreatedAt ?? undefined,
|
minBirthDate: new Date(face.asset.fileCreatedAt),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (matchWithPerson.length > 0) {
|
if (matchWithPerson.length > 0) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { SearchService } from 'src/services/search.service';
|
|||||||
import { AssetFactory } from 'test/factories/asset.factory';
|
import { AssetFactory } from 'test/factories/asset.factory';
|
||||||
import { AuthFactory } from 'test/factories/auth.factory';
|
import { AuthFactory } from 'test/factories/auth.factory';
|
||||||
import { authStub } from 'test/fixtures/auth.stub';
|
import { authStub } from 'test/fixtures/auth.stub';
|
||||||
|
import { getForAsset } from 'test/mappers';
|
||||||
import { newTestService, ServiceMocks } from 'test/utils';
|
import { newTestService, ServiceMocks } from 'test/utils';
|
||||||
import { beforeEach, vitest } from 'vitest';
|
import { beforeEach, vitest } from 'vitest';
|
||||||
|
|
||||||
@@ -74,7 +75,9 @@ describe(SearchService.name, () => {
|
|||||||
items: [{ value: 'city', data: asset.id }],
|
items: [{ value: 'city', data: asset.id }],
|
||||||
});
|
});
|
||||||
mocks.asset.getByIdsWithAllRelationsButStacks.mockResolvedValue([asset as never]);
|
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);
|
const result = await sut.getExploreData(auth);
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { BadRequestException, ForbiddenException, UnauthorizedException } from '@nestjs/common';
|
import { BadRequestException, ForbiddenException, UnauthorizedException } from '@nestjs/common';
|
||||||
import { AssetIdErrorReason } from 'src/dtos/asset-ids.response.dto';
|
import { AssetIdErrorReason } from 'src/dtos/asset-ids.response.dto';
|
||||||
|
import { mapSharedLink } from 'src/dtos/shared-link.dto';
|
||||||
import { SharedLinkType } from 'src/enum';
|
import { SharedLinkType } from 'src/enum';
|
||||||
import { SharedLinkService } from 'src/services/shared-link.service';
|
import { SharedLinkService } from 'src/services/shared-link.service';
|
||||||
import { AlbumFactory } from 'test/factories/album.factory';
|
import { AlbumFactory } from 'test/factories/album.factory';
|
||||||
import { AssetFactory } from 'test/factories/asset.factory';
|
import { AssetFactory } from 'test/factories/asset.factory';
|
||||||
import { SharedLinkFactory } from 'test/factories/shared-link.factory';
|
import { SharedLinkFactory } from 'test/factories/shared-link.factory';
|
||||||
import { authStub } from 'test/fixtures/auth.stub';
|
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 { factory } from 'test/small.factory';
|
||||||
import { newTestService, ServiceMocks } from 'test/utils';
|
import { newTestService, ServiceMocks } from 'test/utils';
|
||||||
|
|
||||||
@@ -24,11 +26,13 @@ describe(SharedLinkService.name, () => {
|
|||||||
|
|
||||||
describe('getAll', () => {
|
describe('getAll', () => {
|
||||||
it('should return all shared links for a user', async () => {
|
it('should return all shared links for a user', async () => {
|
||||||
mocks.sharedLink.getAll.mockResolvedValue([sharedLinkStub.expired, sharedLinkStub.valid]);
|
const [sharedLink1, sharedLink2] = [SharedLinkFactory.create(), SharedLinkFactory.create()];
|
||||||
await expect(sut.getAll(authStub.user1, {})).resolves.toEqual([
|
mocks.sharedLink.getAll.mockResolvedValue([getForSharedLink(sharedLink1), getForSharedLink(sharedLink2)]);
|
||||||
sharedLinkResponseStub.expired,
|
await expect(sut.getAll(authStub.user1, {})).resolves.toEqual(
|
||||||
sharedLinkResponseStub.valid,
|
[getForSharedLink(sharedLink1), getForSharedLink(sharedLink2)].map((link) =>
|
||||||
]);
|
mapSharedLink(link, { stripAssetMetadata: false }),
|
||||||
|
),
|
||||||
|
);
|
||||||
expect(mocks.sharedLink.getAll).toHaveBeenCalledWith({ userId: authStub.user1.user.id });
|
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 () => {
|
it('should return the shared link for the public user', async () => {
|
||||||
const authDto = authStub.adminSharedLink;
|
const authDto = authStub.adminSharedLink;
|
||||||
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.valid);
|
const sharedLink = SharedLinkFactory.create();
|
||||||
await expect(sut.getMine(authDto, [])).resolves.toEqual(sharedLinkResponseStub.valid);
|
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);
|
expect(mocks.sharedLink.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -54,7 +61,13 @@ describe(SharedLinkService.name, () => {
|
|||||||
allowUpload: true,
|
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, []);
|
const response = await sut.getMine(authDto, []);
|
||||||
expect(response.assets[0]).toMatchObject({ hasMetadata: false });
|
expect(response.assets[0]).toMatchObject({ hasMetadata: false });
|
||||||
expect(mocks.sharedLink.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id);
|
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 () => {
|
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');
|
const secret = Buffer.from('auth-token-123');
|
||||||
mocks.crypto.hashSha256.mockReturnValue(secret);
|
mocks.crypto.hashSha256.mockReturnValue(secret);
|
||||||
await expect(sut.getMine(authStub.adminSharedLink, [secret.toString('base64')])).resolves.toBeDefined();
|
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 () => {
|
it('should get a shared link by id', async () => {
|
||||||
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.valid);
|
const sharedLink = SharedLinkFactory.create();
|
||||||
await expect(sut.get(authStub.user1, sharedLinkStub.valid.id)).resolves.toEqual(sharedLinkResponseStub.valid);
|
mocks.sharedLink.get.mockResolvedValue(getForSharedLink(sharedLink));
|
||||||
expect(mocks.sharedLink.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id);
|
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 () => {
|
it('should create an album shared link', async () => {
|
||||||
const album = AlbumFactory.from().asset().build();
|
const album = AlbumFactory.from().asset().build();
|
||||||
|
const sharedLink = SharedLinkFactory.from().album(album).build();
|
||||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
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 });
|
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 () => {
|
it('should create an individual shared link', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
|
const sharedLink = SharedLinkFactory.from()
|
||||||
|
.asset(asset, (builder) => builder.exif())
|
||||||
|
.build();
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
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, {
|
await sut.create(authStub.admin, {
|
||||||
type: SharedLinkType.Individual,
|
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 () => {
|
it('should create a shared link with allowDownload set to false when showMetadata is false', async () => {
|
||||||
const asset = AssetFactory.create();
|
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.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, {
|
await sut.create(authStub.admin, {
|
||||||
type: SharedLinkType.Individual,
|
type: SharedLinkType.Individual,
|
||||||
@@ -221,8 +245,9 @@ describe(SharedLinkService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should update a shared link', async () => {
|
it('should update a shared link', async () => {
|
||||||
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.valid);
|
const sharedLink = SharedLinkFactory.create();
|
||||||
mocks.sharedLink.update.mockResolvedValue(sharedLinkStub.valid);
|
mocks.sharedLink.get.mockResolvedValue(getForSharedLink(sharedLink));
|
||||||
|
mocks.sharedLink.update.mockResolvedValue(getForSharedLink(sharedLink));
|
||||||
|
|
||||||
await sut.update(authStub.user1, sharedLinkStub.valid.id, { allowDownload: false });
|
await sut.update(authStub.user1, sharedLinkStub.valid.id, { allowDownload: false });
|
||||||
|
|
||||||
@@ -247,19 +272,21 @@ describe(SharedLinkService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should remove a key', async () => {
|
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();
|
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.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLink.id);
|
||||||
expect(mocks.sharedLink.remove).toHaveBeenCalledWith(sharedLinkStub.valid.id);
|
expect(mocks.sharedLink.remove).toHaveBeenCalledWith(sharedLink.id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('addAssets', () => {
|
describe('addAssets', () => {
|
||||||
it('should not work on album shared links', async () => {
|
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(
|
await expect(sut.addAssets(authStub.admin, 'link-1', { assetIds: ['asset-1'] })).rejects.toBeInstanceOf(
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
@@ -268,11 +295,13 @@ describe(SharedLinkService.name, () => {
|
|||||||
|
|
||||||
it('should add assets to a shared link', async () => {
|
it('should add assets to a shared link', async () => {
|
||||||
const asset = AssetFactory.create();
|
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();
|
const newAsset = AssetFactory.create();
|
||||||
mocks.sharedLink.get.mockResolvedValue(sharedLink);
|
mocks.sharedLink.get.mockResolvedValue(getForSharedLink(sharedLink));
|
||||||
mocks.sharedLink.create.mockResolvedValue(sharedLink);
|
mocks.sharedLink.create.mockResolvedValue(getForSharedLink(sharedLink));
|
||||||
mocks.sharedLink.update.mockResolvedValue(sharedLink);
|
mocks.sharedLink.update.mockResolvedValue(getForSharedLink(sharedLink));
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([newAsset.id]));
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([newAsset.id]));
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
@@ -286,7 +315,7 @@ describe(SharedLinkService.name, () => {
|
|||||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledTimes(1);
|
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledTimes(1);
|
||||||
expect(mocks.sharedLink.update).toHaveBeenCalled();
|
expect(mocks.sharedLink.update).toHaveBeenCalled();
|
||||||
expect(mocks.sharedLink.update).toHaveBeenCalledWith({
|
expect(mocks.sharedLink.update).toHaveBeenCalledWith({
|
||||||
...sharedLink,
|
...getForSharedLink(sharedLink),
|
||||||
slug: null,
|
slug: null,
|
||||||
assetIds: [newAsset.id],
|
assetIds: [newAsset.id],
|
||||||
});
|
});
|
||||||
@@ -295,19 +324,22 @@ describe(SharedLinkService.name, () => {
|
|||||||
|
|
||||||
describe('removeAssets', () => {
|
describe('removeAssets', () => {
|
||||||
it('should not work on album shared links', async () => {
|
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,
|
BadRequestException,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should remove assets from a shared link', async () => {
|
it('should remove assets from a shared link', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
const sharedLink = SharedLinkFactory.from().asset(asset).build();
|
const sharedLink = SharedLinkFactory.from()
|
||||||
mocks.sharedLink.get.mockResolvedValue(sharedLink);
|
.asset(asset, (builder) => builder.exif())
|
||||||
mocks.sharedLink.create.mockResolvedValue(sharedLink);
|
.build();
|
||||||
mocks.sharedLink.update.mockResolvedValue(sharedLink);
|
mocks.sharedLink.get.mockResolvedValue(getForSharedLink(sharedLink));
|
||||||
|
mocks.sharedLink.create.mockResolvedValue(getForSharedLink(sharedLink));
|
||||||
|
mocks.sharedLink.update.mockResolvedValue(getForSharedLink(sharedLink));
|
||||||
mocks.sharedLinkAsset.remove.mockResolvedValue([asset.id]);
|
mocks.sharedLinkAsset.remove.mockResolvedValue([asset.id]);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
@@ -338,11 +370,14 @@ describe(SharedLinkService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return metadata tags', async () => {
|
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({
|
await expect(sut.getMetadataTags(authStub.adminSharedLink)).resolves.toEqual({
|
||||||
description: '1 shared photos & videos',
|
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',
|
title: 'Public Share',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { AssetFactory } from 'test/factories/asset.factory';
|
|||||||
import { AuthFactory } from 'test/factories/auth.factory';
|
import { AuthFactory } from 'test/factories/auth.factory';
|
||||||
import { StackFactory } from 'test/factories/stack.factory';
|
import { StackFactory } from 'test/factories/stack.factory';
|
||||||
import { authStub } from 'test/fixtures/auth.stub';
|
import { authStub } from 'test/fixtures/auth.stub';
|
||||||
|
import { getForStack } from 'test/mappers';
|
||||||
import { newUuid } from 'test/small.factory';
|
import { newUuid } from 'test/small.factory';
|
||||||
import { newTestService, ServiceMocks } from 'test/utils';
|
import { newTestService, ServiceMocks } from 'test/utils';
|
||||||
|
|
||||||
@@ -22,9 +23,11 @@ describe(StackService.name, () => {
|
|||||||
describe('search', () => {
|
describe('search', () => {
|
||||||
it('should search stacks', async () => {
|
it('should search stacks', async () => {
|
||||||
const auth = AuthFactory.create();
|
const auth = AuthFactory.create();
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.from().exif().build();
|
||||||
const stack = StackFactory.from().primaryAsset(asset).build();
|
const stack = StackFactory.from()
|
||||||
mocks.stack.search.mockResolvedValue([stack]);
|
.primaryAsset(asset, (builder) => builder.exif())
|
||||||
|
.build();
|
||||||
|
mocks.stack.search.mockResolvedValue([getForStack(stack)]);
|
||||||
|
|
||||||
await sut.search(auth, { primaryAssetId: asset.id });
|
await sut.search(auth, { primaryAssetId: asset.id });
|
||||||
expect(mocks.stack.search).toHaveBeenCalledWith({
|
expect(mocks.stack.search).toHaveBeenCalledWith({
|
||||||
@@ -49,11 +52,14 @@ describe(StackService.name, () => {
|
|||||||
|
|
||||||
it('should create a stack', async () => {
|
it('should create a stack', async () => {
|
||||||
const auth = AuthFactory.create();
|
const auth = AuthFactory.create();
|
||||||
const [primaryAsset, asset] = [AssetFactory.create(), AssetFactory.create()];
|
const [primaryAsset, asset] = [AssetFactory.from().exif().build(), AssetFactory.from().exif().build()];
|
||||||
const stack = StackFactory.from().primaryAsset(primaryAsset).asset(asset).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.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({
|
await expect(sut.create(auth, { assetIds: [primaryAsset.id, asset.id] })).resolves.toEqual({
|
||||||
id: stack.id,
|
id: stack.id,
|
||||||
@@ -88,11 +94,14 @@ describe(StackService.name, () => {
|
|||||||
|
|
||||||
it('should get stack', async () => {
|
it('should get stack', async () => {
|
||||||
const auth = AuthFactory.create();
|
const auth = AuthFactory.create();
|
||||||
const [primaryAsset, asset] = [AssetFactory.create(), AssetFactory.create()];
|
const [primaryAsset, asset] = [AssetFactory.from().exif().build(), AssetFactory.from().exif().build()];
|
||||||
const stack = StackFactory.from().primaryAsset(primaryAsset).asset(asset).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.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({
|
await expect(sut.get(auth, stack.id)).resolves.toEqual({
|
||||||
id: stack.id,
|
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 () => {
|
it('should fail if the provided primary asset id is not in the stack', async () => {
|
||||||
const auth = AuthFactory.create();
|
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.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(
|
await expect(sut.update(auth, stack.id, { primaryAssetId: 'unknown-asset' })).rejects.toBeInstanceOf(
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
@@ -141,12 +153,15 @@ describe(StackService.name, () => {
|
|||||||
|
|
||||||
it('should update stack', async () => {
|
it('should update stack', async () => {
|
||||||
const auth = AuthFactory.create();
|
const auth = AuthFactory.create();
|
||||||
const [primaryAsset, asset] = [AssetFactory.create(), AssetFactory.create()];
|
const [primaryAsset, asset] = [AssetFactory.from().exif().build(), AssetFactory.from().exif().build()];
|
||||||
const stack = StackFactory.from().primaryAsset(primaryAsset).asset(asset).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.access.stack.checkOwnerAccess.mockResolvedValue(new Set([stack.id]));
|
||||||
mocks.stack.getById.mockResolvedValue(stack);
|
mocks.stack.getById.mockResolvedValue(getForStack(stack));
|
||||||
mocks.stack.update.mockResolvedValue(stack);
|
mocks.stack.update.mockResolvedValue(getForStack(stack));
|
||||||
|
|
||||||
await sut.update(auth, stack.id, { primaryAssetId: asset.id });
|
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 { AssetFactory } from 'test/factories/asset.factory';
|
||||||
import { UserFactory } from 'test/factories/user.factory';
|
import { UserFactory } from 'test/factories/user.factory';
|
||||||
import { userStub } from 'test/fixtures/user.stub';
|
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';
|
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
||||||
|
|
||||||
const motionAsset = AssetFactory.from({ type: AssetType.Video }).exif().build();
|
const motionAsset = AssetFactory.from({ type: AssetType.Video }).exif().build();
|
||||||
@@ -170,7 +170,9 @@ describe(StorageTemplateService.name, () => {
|
|||||||
.exif()
|
.exif()
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
const album = AlbumFactory.from().asset().build();
|
const album = AlbumFactory.from()
|
||||||
|
.asset({}, (builder) => builder.exif())
|
||||||
|
.build();
|
||||||
const config = structuredClone(defaults);
|
const config = structuredClone(defaults);
|
||||||
config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other/{{MM}}{{/if}}/{{filename}}';
|
config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other/{{MM}}{{/if}}/{{filename}}';
|
||||||
sut.onConfigInit({ newConfig: config });
|
sut.onConfigInit({ newConfig: config });
|
||||||
@@ -182,7 +184,7 @@ describe(StorageTemplateService.name, () => {
|
|||||||
|
|
||||||
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(stillAsset));
|
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(stillAsset));
|
||||||
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(motionAsset));
|
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(motionAsset));
|
||||||
mocks.album.getByAssetId.mockResolvedValue([album]);
|
mocks.album.getByAssetId.mockResolvedValue([getForAlbum(album)]);
|
||||||
|
|
||||||
mocks.move.create.mockResolvedValueOnce({
|
mocks.move.create.mockResolvedValueOnce({
|
||||||
id: '123',
|
id: '123',
|
||||||
@@ -211,7 +213,9 @@ describe(StorageTemplateService.name, () => {
|
|||||||
it('should use handlebar if condition for album', async () => {
|
it('should use handlebar if condition for album', async () => {
|
||||||
const user = UserFactory.create();
|
const user = UserFactory.create();
|
||||||
const asset = AssetFactory.from().owner(user).exif().build();
|
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);
|
const config = structuredClone(defaults);
|
||||||
config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other/{{MM}}{{/if}}/{{filename}}';
|
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.user.get.mockResolvedValue(user);
|
||||||
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(asset));
|
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);
|
expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.Success);
|
||||||
|
|
||||||
@@ -259,7 +263,9 @@ describe(StorageTemplateService.name, () => {
|
|||||||
it('should handle album startDate', async () => {
|
it('should handle album startDate', async () => {
|
||||||
const user = UserFactory.create();
|
const user = UserFactory.create();
|
||||||
const asset = AssetFactory.from().owner(user).exif().build();
|
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);
|
const config = structuredClone(defaults);
|
||||||
config.storageTemplate.template =
|
config.storageTemplate.template =
|
||||||
'{{#if album}}{{album-startDate-y}}/{{album-startDate-MM}} - {{album}}{{else}}{{y}}/{{MM}}/{{/if}}/{{filename}}';
|
'{{#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.user.get.mockResolvedValue(user);
|
||||||
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(asset));
|
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(asset));
|
||||||
mocks.album.getByAssetId.mockResolvedValueOnce([album]);
|
mocks.album.getByAssetId.mockResolvedValueOnce([getForAlbum(album)]);
|
||||||
mocks.album.getMetadataForIds.mockResolvedValueOnce([
|
mocks.album.getMetadataForIds.mockResolvedValueOnce([
|
||||||
{
|
{
|
||||||
startDate: asset.fileCreatedAt,
|
startDate: asset.fileCreatedAt,
|
||||||
@@ -764,7 +770,9 @@ describe(StorageTemplateService.name, () => {
|
|||||||
})
|
})
|
||||||
.exif()
|
.exif()
|
||||||
.build();
|
.build();
|
||||||
const album = AlbumFactory.from().asset().build();
|
const album = AlbumFactory.from()
|
||||||
|
.asset({}, (builder) => builder.exif())
|
||||||
|
.build();
|
||||||
const config = structuredClone(defaults);
|
const config = structuredClone(defaults);
|
||||||
config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other/{{MM}}{{/if}}/{{filename}}';
|
config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other/{{MM}}{{/if}}/{{filename}}';
|
||||||
sut.onConfigInit({ newConfig: config });
|
sut.onConfigInit({ newConfig: config });
|
||||||
@@ -775,7 +783,7 @@ describe(StorageTemplateService.name, () => {
|
|||||||
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(stillAsset)]));
|
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(stillAsset)]));
|
||||||
mocks.user.getList.mockResolvedValue([userStub.user1]);
|
mocks.user.getList.mockResolvedValue([userStub.user1]);
|
||||||
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(motionAsset));
|
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(motionAsset));
|
||||||
mocks.album.getByAssetId.mockResolvedValue([album]);
|
mocks.album.getByAssetId.mockResolvedValue([getForAlbum(album)]);
|
||||||
|
|
||||||
mocks.move.create.mockResolvedValueOnce({
|
mocks.move.create.mockResolvedValueOnce({
|
||||||
id: '123',
|
id: '123',
|
||||||
@@ -803,7 +811,9 @@ describe(StorageTemplateService.name, () => {
|
|||||||
|
|
||||||
it('should use still photo album info when migrating live photo motion video', async () => {
|
it('should use still photo album info when migrating live photo motion video', async () => {
|
||||||
const user = userStub.user1;
|
const user = userStub.user1;
|
||||||
const album = AlbumFactory.from().asset().build();
|
const album = AlbumFactory.from()
|
||||||
|
.asset({}, (builder) => builder.exif())
|
||||||
|
.build();
|
||||||
const config = structuredClone(defaults);
|
const config = structuredClone(defaults);
|
||||||
config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other{{/if}}/{{filename}}';
|
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.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(stillAsset)]));
|
||||||
mocks.user.getList.mockResolvedValue([user]);
|
mocks.user.getList.mockResolvedValue([user]);
|
||||||
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(motionAsset));
|
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(motionAsset));
|
||||||
mocks.album.getByAssetId.mockResolvedValue([album]);
|
mocks.album.getByAssetId.mockResolvedValue([getForAlbum(album)]);
|
||||||
|
|
||||||
mocks.move.create.mockResolvedValueOnce({
|
mocks.move.create.mockResolvedValueOnce({
|
||||||
id: '123',
|
id: '123',
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { mapAsset } from 'src/dtos/asset-response.dto';
|
|||||||
import { SyncService } from 'src/services/sync.service';
|
import { SyncService } from 'src/services/sync.service';
|
||||||
import { AssetFactory } from 'test/factories/asset.factory';
|
import { AssetFactory } from 'test/factories/asset.factory';
|
||||||
import { authStub } from 'test/fixtures/auth.stub';
|
import { authStub } from 'test/fixtures/auth.stub';
|
||||||
|
import { getForAsset, getForPartner } from 'test/mappers';
|
||||||
import { factory } from 'test/small.factory';
|
import { factory } from 'test/small.factory';
|
||||||
import { newTestService, ServiceMocks } from 'test/utils';
|
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({ libraryId: 'library-id', isExternal: true }).owner(authStub.user1.user).build(),
|
||||||
AssetFactory.from().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([
|
await expect(sut.getFullSync(authStub.user1, { limit: 2, updatedUntil: untilDate })).resolves.toEqual([
|
||||||
mapAsset(asset1, mapAssetOpts),
|
mapAsset(getForAsset(asset1), mapAssetOpts),
|
||||||
mapAsset(asset2, mapAssetOpts),
|
mapAsset(getForAsset(asset2), mapAssetOpts),
|
||||||
]);
|
]);
|
||||||
expect(mocks.asset.getAllForUserFullSync).toHaveBeenCalledWith({
|
expect(mocks.asset.getAllForUserFullSync).toHaveBeenCalledWith({
|
||||||
ownerId: authStub.user1.user.id,
|
ownerId: authStub.user1.user.id,
|
||||||
@@ -44,7 +45,7 @@ describe(SyncService.name, () => {
|
|||||||
const partner = factory.partner();
|
const partner = factory.partner();
|
||||||
const auth = factory.auth({ user: { id: partner.sharedWithId } });
|
const auth = factory.auth({ user: { id: partner.sharedWithId } });
|
||||||
|
|
||||||
mocks.partner.getAll.mockResolvedValue([partner]);
|
mocks.partner.getAll.mockResolvedValue([getForPartner(partner)]);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [auth.user.id] }),
|
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 () => {
|
it('should return a response requiring a full sync when there are too many changes', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.partner.getAll.mockResolvedValue([]);
|
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(
|
await expect(
|
||||||
sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }),
|
sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }),
|
||||||
).resolves.toEqual({ needsFullSync: true, upserted: [], deleted: [] });
|
).resolves.toEqual({ needsFullSync: true, upserted: [], deleted: [] });
|
||||||
@@ -78,13 +81,13 @@ describe(SyncService.name, () => {
|
|||||||
const asset = AssetFactory.create({ ownerId: authStub.user1.user.id });
|
const asset = AssetFactory.create({ ownerId: authStub.user1.user.id });
|
||||||
const deletedAsset = AssetFactory.create({ libraryId: 'library-id', isExternal: true });
|
const deletedAsset = AssetFactory.create({ libraryId: 'library-id', isExternal: true });
|
||||||
mocks.partner.getAll.mockResolvedValue([]);
|
mocks.partner.getAll.mockResolvedValue([]);
|
||||||
mocks.asset.getChangedDeltaSync.mockResolvedValue([asset]);
|
mocks.asset.getChangedDeltaSync.mockResolvedValue([getForAsset(asset)]);
|
||||||
mocks.audit.getAfter.mockResolvedValue([deletedAsset.id]);
|
mocks.audit.getAfter.mockResolvedValue([deletedAsset.id]);
|
||||||
await expect(
|
await expect(
|
||||||
sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }),
|
sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }),
|
||||||
).resolves.toEqual({
|
).resolves.toEqual({
|
||||||
needsFullSync: false,
|
needsFullSync: false,
|
||||||
upserted: [mapAsset(asset, mapAssetOpts)],
|
upserted: [mapAsset(getForAsset(asset), mapAssetOpts)],
|
||||||
deleted: [deletedAsset.id],
|
deleted: [deletedAsset.id],
|
||||||
});
|
});
|
||||||
expect(mocks.asset.getChangedDeltaSync).toHaveBeenCalledTimes(1);
|
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 { ViewService } from 'src/services/view.service';
|
||||||
import { AssetFactory } from 'test/factories/asset.factory';
|
import { AssetFactory } from 'test/factories/asset.factory';
|
||||||
import { authStub } from 'test/fixtures/auth.stub';
|
import { authStub } from 'test/fixtures/auth.stub';
|
||||||
|
import { getForAsset } from 'test/mappers';
|
||||||
import { newTestService, ServiceMocks } from 'test/utils';
|
import { newTestService, ServiceMocks } from 'test/utils';
|
||||||
|
|
||||||
describe(ViewService.name, () => {
|
describe(ViewService.name, () => {
|
||||||
@@ -37,7 +38,7 @@ describe(ViewService.name, () => {
|
|||||||
|
|
||||||
const mockAssets = [asset1, asset2];
|
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);
|
mocks.view.getAssetsByOriginalPath.mockResolvedValue(mockAssets as any);
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ShallowDehydrateObject } from 'kysely';
|
||||||
import { SystemConfig } from 'src/config';
|
import { SystemConfig } from 'src/config';
|
||||||
import { VECTOR_EXTENSIONS } from 'src/constants';
|
import { VECTOR_EXTENSIONS } from 'src/constants';
|
||||||
import { Asset, AssetFile } from 'src/database';
|
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.License]: { licenseKey: string; activationKey: string; activatedAt: string };
|
||||||
[UserMetadataKey.Onboarding]: { isOnboarded: boolean };
|
[UserMetadataKey.Onboarding]: { isOnboarded: boolean };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MaybeDehydrated<T> = T | ShallowDehydrateObject<T>;
|
||||||
|
|||||||
@@ -4,23 +4,23 @@ import {
|
|||||||
DeduplicateJoinsPlugin,
|
DeduplicateJoinsPlugin,
|
||||||
Expression,
|
Expression,
|
||||||
ExpressionBuilder,
|
ExpressionBuilder,
|
||||||
ExpressionWrapper,
|
|
||||||
Kysely,
|
Kysely,
|
||||||
KyselyConfig,
|
KyselyConfig,
|
||||||
Nullable,
|
NotNull,
|
||||||
Selectable,
|
Selectable,
|
||||||
SelectQueryBuilder,
|
SelectQueryBuilder,
|
||||||
Simplify,
|
ShallowDehydrateObject,
|
||||||
sql,
|
sql,
|
||||||
} from 'kysely';
|
} from 'kysely';
|
||||||
import { PostgresJSDialect } from 'kysely-postgres-js';
|
import { PostgresJSDialect } from 'kysely-postgres-js';
|
||||||
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||||
import { Notice, PostgresError } from '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 { AssetEditActionItem } from 'src/dtos/editing.dto';
|
||||||
import { AssetFileType, AssetVisibility, DatabaseExtension } from 'src/enum';
|
import { AssetFileType, AssetVisibility, DatabaseExtension } from 'src/enum';
|
||||||
import { AssetSearchBuilderOptions } from 'src/repositories/search.repository';
|
import { AssetSearchBuilderOptions } from 'src/repositories/search.repository';
|
||||||
import { DB } from 'src/schema';
|
import { DB } from 'src/schema';
|
||||||
|
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
|
||||||
import { VectorExtension } from 'src/types';
|
import { VectorExtension } from 'src/types';
|
||||||
|
|
||||||
export const getKyselyConfig = (connection: DatabaseConnectionParams): KyselyConfig => {
|
export const getKyselyConfig = (connection: DatabaseConnectionParams): KyselyConfig => {
|
||||||
@@ -70,28 +70,6 @@ export const removeUndefinedKeys = <T extends object>(update: T, template: unkno
|
|||||||
return update;
|
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 ASSET_CHECKSUM_CONSTRAINT = 'UQ_assets_owner_checksum';
|
||||||
|
|
||||||
export const isAssetChecksumConstraint = (error: unknown) => {
|
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>) {
|
export function withExif<O>(qb: SelectQueryBuilder<DB, 'asset', O>) {
|
||||||
return qb
|
return qb
|
||||||
.leftJoin('asset_exif', 'asset.id', 'asset_exif.assetId')
|
.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>) {
|
export function withExifInner<O>(qb: SelectQueryBuilder<DB, 'asset', O>) {
|
||||||
return qb
|
return qb
|
||||||
.innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId')
|
.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>) {
|
export function withSmartSearch<O>(qb: SelectQueryBuilder<DB, 'asset', O>) {
|
||||||
return qb
|
return qb
|
||||||
.leftJoin('smart_search', 'asset.id', 'smart_search.assetId')
|
.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) {
|
export function withFaces(eb: ExpressionBuilder<DB, 'asset'>, withHidden?: boolean, withDeletedFace?: boolean) {
|
||||||
@@ -164,7 +148,7 @@ export function withFacesAndPeople(
|
|||||||
(join) => join.onTrue(),
|
(join) => join.onTrue(),
|
||||||
)
|
)
|
||||||
.selectAll('asset_face')
|
.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')
|
.whereRef('asset_face.assetId', '=', 'asset.id')
|
||||||
.$if(!withDeletedFace, (qb) => qb.where('asset_face.deletedAt', 'is', null))
|
.$if(!withDeletedFace, (qb) => qb.where('asset_face.deletedAt', 'is', null))
|
||||||
.$if(!withHidden, (qb) => qb.where('asset_face.isVisible', 'is', true)),
|
.$if(!withHidden, (qb) => qb.where('asset_face.isVisible', 'is', true)),
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { DateTime } from 'luxon';
|
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;
|
return x instanceof Date ? x.toISOString().split('T')[0] : x;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -51,12 +51,14 @@ export class SharedLinkFactory {
|
|||||||
|
|
||||||
album(dto: AlbumLike = {}, builder?: FactoryBuilder<AlbumFactory>) {
|
album(dto: AlbumLike = {}, builder?: FactoryBuilder<AlbumFactory>) {
|
||||||
this.#album = build(AlbumFactory.from(dto), builder);
|
this.#album = build(AlbumFactory.from(dto), builder);
|
||||||
|
this.value.type = SharedLinkType.Album;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
asset(dto: AssetLike = {}, builder?: FactoryBuilder<AssetFactory>) {
|
asset(dto: AssetLike = {}, builder?: FactoryBuilder<AssetFactory>) {
|
||||||
const asset = build(AssetFactory.from(dto), builder);
|
const asset = build(AssetFactory.from(dto), builder);
|
||||||
this.#assets.push(asset);
|
this.#assets.push(asset);
|
||||||
|
this.value.type = SharedLinkType.Individual;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-82
@@ -1,7 +1,6 @@
|
|||||||
import { UserAdmin } from 'src/database';
|
|
||||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||||
import { SharedLinkResponseDto } from 'src/dtos/shared-link.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 { AssetFactory } from 'test/factories/asset.factory';
|
||||||
import { authStub } from 'test/fixtures/auth.stub';
|
import { authStub } from 'test/fixtures/auth.stub';
|
||||||
import { userStub } from 'test/fixtures/user.stub';
|
import { userStub } from 'test/fixtures/user.stub';
|
||||||
@@ -83,86 +82,7 @@ export const sharedLinkStub = {
|
|||||||
showExif: false,
|
showExif: false,
|
||||||
description: null,
|
description: null,
|
||||||
password: null,
|
password: null,
|
||||||
assets: [
|
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,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
albumId: null,
|
albumId: null,
|
||||||
album: null,
|
album: null,
|
||||||
slug: null,
|
slug: null,
|
||||||
|
|||||||
Vendored
+4
-4
@@ -55,15 +55,15 @@ export const tagStub = {
|
|||||||
export const tagResponseStub = {
|
export const tagResponseStub = {
|
||||||
tag1: Object.freeze<TagResponseDto>({
|
tag1: Object.freeze<TagResponseDto>({
|
||||||
id: 'tag-1',
|
id: 'tag-1',
|
||||||
createdAt: new Date('2021-01-01T00:00:00Z'),
|
createdAt: '2021-01-01T00:00:00.000Z',
|
||||||
updatedAt: new Date('2021-01-01T00:00:00Z'),
|
updatedAt: '2021-01-01T00:00:00.000Z',
|
||||||
name: 'Tag1',
|
name: 'Tag1',
|
||||||
value: 'Tag1',
|
value: 'Tag1',
|
||||||
}),
|
}),
|
||||||
color1: Object.freeze<TagResponseDto>({
|
color1: Object.freeze<TagResponseDto>({
|
||||||
id: 'tag-1',
|
id: 'tag-1',
|
||||||
createdAt: new Date('2021-01-01T00:00:00Z'),
|
createdAt: '2021-01-01T00:00:00.000Z',
|
||||||
updatedAt: new Date('2021-01-01T00:00:00Z'),
|
updatedAt: '2021-01-01T00:00:00.000Z',
|
||||||
color: '#000000',
|
color: '#000000',
|
||||||
name: 'Tag1',
|
name: 'Tag1',
|
||||||
value: 'Tag1',
|
value: 'Tag1',
|
||||||
|
|||||||
+175
-2
@@ -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 { 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 { AssetFaceFactory } from 'test/factories/asset-face.factory';
|
||||||
import { AssetFactory } from 'test/factories/asset.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']>) => {
|
export const getForStorageTemplate = (asset: ReturnType<AssetFactory['build']>) => {
|
||||||
return {
|
return {
|
||||||
@@ -47,6 +55,171 @@ export const getForFacialRecognitionJob = (
|
|||||||
asset: Pick<Selectable<AssetTable>, 'ownerId' | 'visibility' | 'fileCreatedAt'> | null,
|
asset: Pick<Selectable<AssetTable>, 'ownerId' | 'visibility' | 'fileCreatedAt'> | null,
|
||||||
) => ({
|
) => ({
|
||||||
...face,
|
...face,
|
||||||
asset,
|
asset: asset
|
||||||
|
? { ownerId: asset.ownerId, visibility: asset.visibility, fileCreatedAt: asset.fileCreatedAt.toISOString() }
|
||||||
|
: null,
|
||||||
faceSearch: { faceId: face.id, embedding: '[1, 2, 3, 4]' },
|
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 {
|
import {
|
||||||
Activity,
|
Activity,
|
||||||
Album,
|
Album,
|
||||||
@@ -113,8 +114,12 @@ const partnerFactory = ({
|
|||||||
sharedWith: sharedWithProvided,
|
sharedWith: sharedWithProvided,
|
||||||
...partner
|
...partner
|
||||||
}: Partial<Partner> = {}) => {
|
}: Partial<Partner> = {}) => {
|
||||||
const sharedBy = UserFactory.create(sharedByProvided ?? {});
|
const hydrateUser = (user: Partial<ShallowDehydrateObject<User>>) => ({
|
||||||
const sharedWith = UserFactory.create(sharedWithProvided ?? {});
|
...user,
|
||||||
|
profileChangedAt: user.profileChangedAt ? new Date(user.profileChangedAt) : undefined,
|
||||||
|
});
|
||||||
|
const sharedBy = UserFactory.create(sharedByProvided ? hydrateUser(sharedByProvided) : {});
|
||||||
|
const sharedWith = UserFactory.create(sharedWithProvided ? hydrateUser(sharedWithProvided) : {});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sharedById: sharedBy.id,
|
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();
|
const userId = activity.userId || newUuid();
|
||||||
return {
|
return {
|
||||||
id: newUuid(),
|
id: newUuid(),
|
||||||
|
|||||||
Reference in New Issue
Block a user