mirror of
https://github.com/immich-app/immich.git
synced 2025-05-24 01:12:58 -04:00
refactor: migrate library spec to factories (#16711)
This commit is contained in:
parent
fd46d43726
commit
1e127ae3a1
@ -10,6 +10,20 @@ export type AuthUser = {
|
||||
quotaSizeInBytes: number | null;
|
||||
};
|
||||
|
||||
export type Library = {
|
||||
id: string;
|
||||
ownerId: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
updateId: string;
|
||||
name: string;
|
||||
importPaths: string[];
|
||||
exclusionPatterns: string[];
|
||||
deletedAt: Date | null;
|
||||
refreshedAt: Date | null;
|
||||
assets?: Asset[];
|
||||
};
|
||||
|
||||
export type AuthApiKey = {
|
||||
id: string;
|
||||
permissions: Permission[];
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ArrayMaxSize, ArrayUnique, IsNotEmpty, IsString } from 'class-validator';
|
||||
import { LibraryEntity } from 'src/entities/library.entity';
|
||||
import { Library } from 'src/database';
|
||||
import { Optional, ValidateUUID } from 'src/validation';
|
||||
|
||||
export class CreateLibraryDto {
|
||||
@ -120,7 +120,7 @@ export class LibraryStatsResponseDto {
|
||||
usage = 0;
|
||||
}
|
||||
|
||||
export function mapLibrary(entity: LibraryEntity): LibraryResponseDto {
|
||||
export function mapLibrary(entity: Library): LibraryResponseDto {
|
||||
let assetCount = 0;
|
||||
if (entity.assets) {
|
||||
assetCount = entity.assets.length;
|
||||
|
@ -26,7 +26,7 @@ export class LibraryEntity {
|
||||
assets!: AssetEntity[];
|
||||
|
||||
@ManyToOne(() => UserEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
|
||||
owner!: UserEntity;
|
||||
owner?: UserEntity;
|
||||
|
||||
@Column()
|
||||
ownerId!: string;
|
||||
|
@ -2,34 +2,7 @@
|
||||
|
||||
-- LibraryRepository.get
|
||||
select
|
||||
"libraries".*,
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"users"."id",
|
||||
"users"."email",
|
||||
"users"."createdAt",
|
||||
"users"."profileImagePath",
|
||||
"users"."isAdmin",
|
||||
"users"."shouldChangePassword",
|
||||
"users"."deletedAt",
|
||||
"users"."oauthId",
|
||||
"users"."updatedAt",
|
||||
"users"."storageLabel",
|
||||
"users"."name",
|
||||
"users"."quotaSizeInBytes",
|
||||
"users"."quotaUsageInBytes",
|
||||
"users"."status",
|
||||
"users"."profileChangedAt"
|
||||
from
|
||||
"users"
|
||||
where
|
||||
"users"."id" = "libraries"."ownerId"
|
||||
) as obj
|
||||
) as "owner"
|
||||
"libraries".*
|
||||
from
|
||||
"libraries"
|
||||
where
|
||||
@ -38,34 +11,7 @@ where
|
||||
|
||||
-- LibraryRepository.getAll
|
||||
select
|
||||
"libraries".*,
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"users"."id",
|
||||
"users"."email",
|
||||
"users"."createdAt",
|
||||
"users"."profileImagePath",
|
||||
"users"."isAdmin",
|
||||
"users"."shouldChangePassword",
|
||||
"users"."deletedAt",
|
||||
"users"."oauthId",
|
||||
"users"."updatedAt",
|
||||
"users"."storageLabel",
|
||||
"users"."name",
|
||||
"users"."quotaSizeInBytes",
|
||||
"users"."quotaUsageInBytes",
|
||||
"users"."status",
|
||||
"users"."profileChangedAt"
|
||||
from
|
||||
"users"
|
||||
where
|
||||
"users"."id" = "libraries"."ownerId"
|
||||
) as obj
|
||||
) as "owner"
|
||||
"libraries".*
|
||||
from
|
||||
"libraries"
|
||||
where
|
||||
@ -75,34 +21,7 @@ order by
|
||||
|
||||
-- LibraryRepository.getAllDeleted
|
||||
select
|
||||
"libraries".*,
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"users"."id",
|
||||
"users"."email",
|
||||
"users"."createdAt",
|
||||
"users"."profileImagePath",
|
||||
"users"."isAdmin",
|
||||
"users"."shouldChangePassword",
|
||||
"users"."deletedAt",
|
||||
"users"."oauthId",
|
||||
"users"."updatedAt",
|
||||
"users"."storageLabel",
|
||||
"users"."name",
|
||||
"users"."quotaSizeInBytes",
|
||||
"users"."quotaUsageInBytes",
|
||||
"users"."status",
|
||||
"users"."profileChangedAt"
|
||||
from
|
||||
"users"
|
||||
where
|
||||
"users"."id" = "libraries"."ownerId"
|
||||
) as obj
|
||||
) as "owner"
|
||||
"libraries".*
|
||||
from
|
||||
"libraries"
|
||||
where
|
||||
|
@ -1,31 +1,11 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ExpressionBuilder, Insertable, Kysely, sql, Updateable } from 'kysely';
|
||||
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { Insertable, Kysely, sql, Updateable } from 'kysely';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { DB, Libraries } from 'src/db';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { LibraryStatsResponseDto } from 'src/dtos/library.dto';
|
||||
import { LibraryEntity } from 'src/entities/library.entity';
|
||||
import { AssetType } from 'src/enum';
|
||||
|
||||
const userColumns = [
|
||||
'users.id',
|
||||
'users.email',
|
||||
'users.createdAt',
|
||||
'users.profileImagePath',
|
||||
'users.isAdmin',
|
||||
'users.shouldChangePassword',
|
||||
'users.deletedAt',
|
||||
'users.oauthId',
|
||||
'users.updatedAt',
|
||||
'users.storageLabel',
|
||||
'users.name',
|
||||
'users.quotaSizeInBytes',
|
||||
'users.quotaUsageInBytes',
|
||||
'users.status',
|
||||
'users.profileChangedAt',
|
||||
] as const;
|
||||
|
||||
export enum AssetSyncResult {
|
||||
DO_NOTHING,
|
||||
UPDATE,
|
||||
@ -33,72 +13,59 @@ export enum AssetSyncResult {
|
||||
CHECK_OFFLINE,
|
||||
}
|
||||
|
||||
const withOwner = (eb: ExpressionBuilder<DB, 'libraries'>) => {
|
||||
return jsonObjectFrom(eb.selectFrom('users').whereRef('users.id', '=', 'libraries.ownerId').select(userColumns)).as(
|
||||
'owner',
|
||||
);
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class LibraryRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
get(id: string, withDeleted = false): Promise<LibraryEntity | undefined> {
|
||||
get(id: string, withDeleted = false) {
|
||||
return this.db
|
||||
.selectFrom('libraries')
|
||||
.selectAll('libraries')
|
||||
.select(withOwner)
|
||||
.where('libraries.id', '=', id)
|
||||
.$if(!withDeleted, (qb) => qb.where('libraries.deletedAt', 'is', null))
|
||||
.executeTakeFirst() as Promise<LibraryEntity | undefined>;
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [] })
|
||||
getAll(withDeleted = false): Promise<LibraryEntity[]> {
|
||||
getAll(withDeleted = false) {
|
||||
return this.db
|
||||
.selectFrom('libraries')
|
||||
.selectAll('libraries')
|
||||
.select(withOwner)
|
||||
.orderBy('createdAt', 'asc')
|
||||
.$if(!withDeleted, (qb) => qb.where('libraries.deletedAt', 'is', null))
|
||||
.execute() as unknown as Promise<LibraryEntity[]>;
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql()
|
||||
getAllDeleted(): Promise<LibraryEntity[]> {
|
||||
getAllDeleted() {
|
||||
return this.db
|
||||
.selectFrom('libraries')
|
||||
.selectAll('libraries')
|
||||
.select(withOwner)
|
||||
.where('libraries.deletedAt', 'is not', null)
|
||||
.orderBy('createdAt', 'asc')
|
||||
.execute() as unknown as Promise<LibraryEntity[]>;
|
||||
.execute();
|
||||
}
|
||||
|
||||
create(library: Insertable<Libraries>): Promise<LibraryEntity> {
|
||||
return this.db
|
||||
.insertInto('libraries')
|
||||
.values(library)
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow() as Promise<LibraryEntity>;
|
||||
create(library: Insertable<Libraries>) {
|
||||
return this.db.insertInto('libraries').values(library).returningAll().executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
async delete(id: string) {
|
||||
await this.db.deleteFrom('libraries').where('libraries.id', '=', id).execute();
|
||||
}
|
||||
|
||||
async softDelete(id: string): Promise<void> {
|
||||
async softDelete(id: string) {
|
||||
await this.db.updateTable('libraries').set({ deletedAt: new Date() }).where('libraries.id', '=', id).execute();
|
||||
}
|
||||
|
||||
update(id: string, library: Updateable<Libraries>): Promise<LibraryEntity> {
|
||||
update(id: string, library: Updateable<Libraries>) {
|
||||
return this.db
|
||||
.updateTable('libraries')
|
||||
.set(library)
|
||||
.where('libraries.id', '=', id)
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow() as Promise<LibraryEntity>;
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
|
File diff suppressed because it is too large
Load Diff
3
server/test/fixtures/asset.stub.ts
vendored
3
server/test/fixtures/asset.stub.ts
vendored
@ -6,7 +6,6 @@ import { AssetFileType, AssetStatus, AssetType } from 'src/enum';
|
||||
import { StorageAsset } from 'src/types';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { fileStub } from 'test/fixtures/file.stub';
|
||||
import { libraryStub } from 'test/fixtures/library.stub';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
|
||||
const previewFile: AssetFileEntity = {
|
||||
@ -396,7 +395,6 @@ export const assetStub = {
|
||||
livePhotoVideo: null,
|
||||
livePhotoVideoId: null,
|
||||
libraryId: 'library-id',
|
||||
library: libraryStub.externalLibrary1,
|
||||
tags: [],
|
||||
sharedLinks: [],
|
||||
originalFileName: 'asset-id.jpg',
|
||||
@ -751,7 +749,6 @@ export const assetStub = {
|
||||
livePhotoVideo: null,
|
||||
livePhotoVideoId: null,
|
||||
libraryId: 'library-id',
|
||||
library: libraryStub.externalLibrary1,
|
||||
tags: [],
|
||||
sharedLinks: [],
|
||||
originalFileName: 'photo.jpg',
|
||||
|
77
server/test/fixtures/library.stub.ts
vendored
77
server/test/fixtures/library.stub.ts
vendored
@ -1,77 +0,0 @@
|
||||
import { LibraryEntity } from 'src/entities/library.entity';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
|
||||
export const libraryStub = {
|
||||
externalLibrary1: Object.freeze<LibraryEntity>({
|
||||
id: 'library-id',
|
||||
name: 'test_library',
|
||||
assets: [],
|
||||
owner: userStub.admin,
|
||||
ownerId: 'admin_id',
|
||||
importPaths: [],
|
||||
createdAt: new Date('2023-01-01'),
|
||||
updatedAt: new Date('2023-01-01'),
|
||||
refreshedAt: null,
|
||||
exclusionPatterns: [],
|
||||
}),
|
||||
externalLibrary2: Object.freeze<LibraryEntity>({
|
||||
id: 'library-id2',
|
||||
name: 'test_library2',
|
||||
assets: [],
|
||||
owner: userStub.admin,
|
||||
ownerId: 'admin_id',
|
||||
importPaths: [],
|
||||
createdAt: new Date('2021-01-01'),
|
||||
updatedAt: new Date('2022-01-01'),
|
||||
refreshedAt: null,
|
||||
exclusionPatterns: [],
|
||||
}),
|
||||
externalLibraryWithImportPaths1: Object.freeze<LibraryEntity>({
|
||||
id: 'library-id-with-paths1',
|
||||
name: 'library-with-import-paths1',
|
||||
assets: [],
|
||||
owner: userStub.admin,
|
||||
ownerId: 'admin_id',
|
||||
importPaths: ['/foo', '/bar'],
|
||||
createdAt: new Date('2023-01-01'),
|
||||
updatedAt: new Date('2023-01-01'),
|
||||
refreshedAt: null,
|
||||
exclusionPatterns: [],
|
||||
}),
|
||||
externalLibraryWithImportPaths2: Object.freeze<LibraryEntity>({
|
||||
id: 'library-id-with-paths2',
|
||||
name: 'library-with-import-paths2',
|
||||
assets: [],
|
||||
owner: userStub.admin,
|
||||
ownerId: 'admin_id',
|
||||
importPaths: ['/xyz', '/asdf'],
|
||||
createdAt: new Date('2023-01-01'),
|
||||
updatedAt: new Date('2023-01-01'),
|
||||
refreshedAt: null,
|
||||
exclusionPatterns: [],
|
||||
}),
|
||||
patternPath: Object.freeze<LibraryEntity>({
|
||||
id: 'library-id1337',
|
||||
name: 'importpath-exclusion-library1',
|
||||
assets: [],
|
||||
owner: userStub.admin,
|
||||
ownerId: 'user-id',
|
||||
importPaths: ['/xyz', '/asdf'],
|
||||
createdAt: new Date('2023-01-01'),
|
||||
updatedAt: new Date('2023-01-01'),
|
||||
refreshedAt: null,
|
||||
exclusionPatterns: ['**/dir1/**'],
|
||||
}),
|
||||
hasImmichPaths: Object.freeze<LibraryEntity>({
|
||||
id: 'library-id1337',
|
||||
name: 'importpath-exclusion-library1',
|
||||
assets: [],
|
||||
owner: userStub.admin,
|
||||
ownerId: 'user-id',
|
||||
importPaths: ['upload/thumbs', 'xyz', 'upload/library'],
|
||||
createdAt: new Date('2023-01-01'),
|
||||
updatedAt: new Date('2023-01-01'),
|
||||
refreshedAt: null,
|
||||
exclusionPatterns: ['**/dir1/**'],
|
||||
}),
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { Asset, AuthUser, User } from 'src/database';
|
||||
import { Asset, AuthUser, Library, User } from 'src/database';
|
||||
import { OnThisDayData } from 'src/entities/memory.entity';
|
||||
import { AssetStatus, AssetType, MemoryType } from 'src/enum';
|
||||
import { ActivityItem, MemoryItem } from 'src/types';
|
||||
@ -13,7 +13,11 @@ export const newDate = () => new Date();
|
||||
export const newUpdateId = () => 'uuid-v7';
|
||||
export const newSha1 = () => Buffer.from('this is a fake hash');
|
||||
|
||||
const authUser = (authUser: Partial<AuthUser>) => ({
|
||||
const authFactory = (user: Partial<AuthUser> = {}) => ({
|
||||
user: authUserFactory(user),
|
||||
});
|
||||
|
||||
const authUserFactory = (authUser: Partial<AuthUser>) => ({
|
||||
id: newUuid(),
|
||||
isAdmin: false,
|
||||
name: 'Test User',
|
||||
@ -23,7 +27,7 @@ const authUser = (authUser: Partial<AuthUser>) => ({
|
||||
...authUser,
|
||||
});
|
||||
|
||||
const user = (user: Partial<User>) => ({
|
||||
const userFactory = (user: Partial<User> = {}) => ({
|
||||
id: newUuid(),
|
||||
name: 'Test User',
|
||||
email: 'test@immich.cloud',
|
||||
@ -32,75 +36,95 @@ const user = (user: Partial<User>) => ({
|
||||
...user,
|
||||
});
|
||||
|
||||
export const factory = {
|
||||
auth: (user: Partial<AuthUser> = {}) => ({
|
||||
user: authUser(user),
|
||||
}),
|
||||
authUser,
|
||||
user,
|
||||
asset: (asset: Partial<Asset> = {}) => ({
|
||||
id: newUuid(),
|
||||
createdAt: newDate(),
|
||||
updatedAt: newDate(),
|
||||
deletedAt: null,
|
||||
updateId: newUpdateId(),
|
||||
status: AssetStatus.ACTIVE,
|
||||
checksum: newSha1(),
|
||||
deviceAssetId: '',
|
||||
deviceId: '',
|
||||
duplicateId: null,
|
||||
duration: null,
|
||||
encodedVideoPath: null,
|
||||
fileCreatedAt: newDate(),
|
||||
fileModifiedAt: newDate(),
|
||||
isArchived: false,
|
||||
isExternal: false,
|
||||
isFavorite: false,
|
||||
isOffline: false,
|
||||
isVisible: true,
|
||||
libraryId: null,
|
||||
livePhotoVideoId: null,
|
||||
localDateTime: newDate(),
|
||||
originalFileName: 'IMG_123.jpg',
|
||||
originalPath: `upload/12/34/IMG_123.jpg`,
|
||||
ownerId: newUuid(),
|
||||
sidecarPath: null,
|
||||
stackId: null,
|
||||
thumbhash: null,
|
||||
type: AssetType.IMAGE,
|
||||
...asset,
|
||||
}),
|
||||
activity: (activity: Partial<ActivityItem> = {}) => {
|
||||
const userId = activity.userId || newUuid();
|
||||
return {
|
||||
id: newUuid(),
|
||||
comment: null,
|
||||
isLiked: false,
|
||||
userId,
|
||||
user: user({ id: userId }),
|
||||
assetId: newUuid(),
|
||||
albumId: newUuid(),
|
||||
createdAt: newDate(),
|
||||
updatedAt: newDate(),
|
||||
updateId: newUpdateId(),
|
||||
...activity,
|
||||
};
|
||||
},
|
||||
memory: (memory: Partial<MemoryItem> = {}) => ({
|
||||
const assetFactory = (asset: Partial<Asset> = {}) => ({
|
||||
id: newUuid(),
|
||||
createdAt: newDate(),
|
||||
updatedAt: newDate(),
|
||||
deletedAt: null,
|
||||
updateId: newUpdateId(),
|
||||
status: AssetStatus.ACTIVE,
|
||||
checksum: newSha1(),
|
||||
deviceAssetId: '',
|
||||
deviceId: '',
|
||||
duplicateId: null,
|
||||
duration: null,
|
||||
encodedVideoPath: null,
|
||||
fileCreatedAt: newDate(),
|
||||
fileModifiedAt: newDate(),
|
||||
isArchived: false,
|
||||
isExternal: false,
|
||||
isFavorite: false,
|
||||
isOffline: false,
|
||||
isVisible: true,
|
||||
libraryId: null,
|
||||
livePhotoVideoId: null,
|
||||
localDateTime: newDate(),
|
||||
originalFileName: 'IMG_123.jpg',
|
||||
originalPath: `upload/12/34/IMG_123.jpg`,
|
||||
ownerId: newUuid(),
|
||||
sidecarPath: null,
|
||||
stackId: null,
|
||||
thumbhash: null,
|
||||
type: AssetType.IMAGE,
|
||||
...asset,
|
||||
});
|
||||
|
||||
const activityFactory = (activity: Partial<ActivityItem> = {}) => {
|
||||
const userId = activity.userId || newUuid();
|
||||
return {
|
||||
id: newUuid(),
|
||||
comment: null,
|
||||
isLiked: false,
|
||||
userId,
|
||||
user: userFactory({ id: userId }),
|
||||
assetId: newUuid(),
|
||||
albumId: newUuid(),
|
||||
createdAt: newDate(),
|
||||
updatedAt: newDate(),
|
||||
updateId: newUpdateId(),
|
||||
deletedAt: null,
|
||||
ownerId: newUuid(),
|
||||
type: MemoryType.ON_THIS_DAY,
|
||||
data: { year: 2024 } as OnThisDayData,
|
||||
isSaved: false,
|
||||
memoryAt: newDate(),
|
||||
seenAt: null,
|
||||
showAt: newDate(),
|
||||
hideAt: newDate(),
|
||||
assets: [],
|
||||
...memory,
|
||||
}),
|
||||
...activity,
|
||||
};
|
||||
};
|
||||
|
||||
const libraryFactory = (library: Partial<Library> = {}) => ({
|
||||
id: newUuid(),
|
||||
createdAt: newDate(),
|
||||
updatedAt: newDate(),
|
||||
updateId: newUpdateId(),
|
||||
deletedAt: null,
|
||||
refreshedAt: null,
|
||||
name: 'Library',
|
||||
assets: [],
|
||||
ownerId: newUuid(),
|
||||
importPaths: [],
|
||||
exclusionPatterns: [],
|
||||
...library,
|
||||
});
|
||||
|
||||
const memoryFactory = (memory: Partial<MemoryItem> = {}) => ({
|
||||
id: newUuid(),
|
||||
createdAt: newDate(),
|
||||
updatedAt: newDate(),
|
||||
updateId: newUpdateId(),
|
||||
deletedAt: null,
|
||||
ownerId: newUuid(),
|
||||
type: MemoryType.ON_THIS_DAY,
|
||||
data: { year: 2024 } as OnThisDayData,
|
||||
isSaved: false,
|
||||
memoryAt: newDate(),
|
||||
seenAt: null,
|
||||
showAt: newDate(),
|
||||
hideAt: newDate(),
|
||||
assets: [],
|
||||
...memory,
|
||||
});
|
||||
|
||||
export const factory = {
|
||||
activity: activityFactory,
|
||||
asset: assetFactory,
|
||||
auth: authFactory,
|
||||
authUser: authUserFactory,
|
||||
library: libraryFactory,
|
||||
memory: memoryFactory,
|
||||
user: userFactory,
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user