mirror of
https://github.com/immich-app/immich.git
synced 2025-12-29 17:25:00 +03:00
refactor: remove album entity, update types (#17450)
This commit is contained in:
@@ -1,12 +1,11 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ExpressionBuilder, Insertable, Kysely, sql, Updateable } from 'kysely';
|
||||
import { ExpressionBuilder, Insertable, Kysely, NotNull, sql, Updateable } from 'kysely';
|
||||
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { columns } from 'src/database';
|
||||
import { columns, Exif } from 'src/database';
|
||||
import { Albums, DB } from 'src/db';
|
||||
import { Chunked, ChunkedArray, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { AlbumUserCreateDto } from 'src/dtos/album.dto';
|
||||
import { AlbumEntity } from 'src/entities/album.entity';
|
||||
|
||||
export interface AlbumAssetCount {
|
||||
albumId: string;
|
||||
@@ -21,9 +20,9 @@ export interface AlbumInfoOptions {
|
||||
}
|
||||
|
||||
const withOwner = (eb: ExpressionBuilder<DB, 'albums'>) => {
|
||||
return jsonObjectFrom(eb.selectFrom('users').select(columns.user).whereRef('users.id', '=', 'albums.ownerId')).as(
|
||||
'owner',
|
||||
);
|
||||
return jsonObjectFrom(eb.selectFrom('users').select(columns.user).whereRef('users.id', '=', 'albums.ownerId'))
|
||||
.$notNull()
|
||||
.as('owner');
|
||||
};
|
||||
|
||||
const withAlbumUsers = (eb: ExpressionBuilder<DB, 'albums'>) => {
|
||||
@@ -32,12 +31,14 @@ const withAlbumUsers = (eb: ExpressionBuilder<DB, 'albums'>) => {
|
||||
.selectFrom('albums_shared_users_users as album_users')
|
||||
.select('album_users.role')
|
||||
.select((eb) =>
|
||||
jsonObjectFrom(eb.selectFrom('users').select(columns.user).whereRef('users.id', '=', 'album_users.usersId')).as(
|
||||
'user',
|
||||
),
|
||||
jsonObjectFrom(eb.selectFrom('users').select(columns.user).whereRef('users.id', '=', 'album_users.usersId'))
|
||||
.$notNull()
|
||||
.as('user'),
|
||||
)
|
||||
.whereRef('album_users.albumsId', '=', 'albums.id'),
|
||||
).as('albumUsers');
|
||||
)
|
||||
.$notNull()
|
||||
.as('albumUsers');
|
||||
};
|
||||
|
||||
const withSharedLink = (eb: ExpressionBuilder<DB, 'albums'>) => {
|
||||
@@ -53,7 +54,7 @@ const withAssets = (eb: ExpressionBuilder<DB, 'albums'>) => {
|
||||
.selectFrom('assets')
|
||||
.selectAll('assets')
|
||||
.leftJoin('exif', 'assets.id', 'exif.assetId')
|
||||
.select((eb) => eb.table('exif').as('exifInfo'))
|
||||
.select((eb) => eb.table('exif').$castTo<Exif>().as('exifInfo'))
|
||||
.innerJoin('albums_assets_assets', 'albums_assets_assets.assetsId', 'assets.id')
|
||||
.whereRef('albums_assets_assets.albumsId', '=', 'albums.id')
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
@@ -69,7 +70,7 @@ export class AlbumRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, { withAssets: true }] })
|
||||
async getById(id: string, options: AlbumInfoOptions): Promise<AlbumEntity | undefined> {
|
||||
async getById(id: string, options: AlbumInfoOptions) {
|
||||
return this.db
|
||||
.selectFrom('albums')
|
||||
.selectAll('albums')
|
||||
@@ -79,11 +80,12 @@ export class AlbumRepository {
|
||||
.select(withAlbumUsers)
|
||||
.select(withSharedLink)
|
||||
.$if(options.withAssets, (eb) => eb.select(withAssets))
|
||||
.executeTakeFirst() as Promise<AlbumEntity | undefined>;
|
||||
.$narrowType<{ assets: NotNull }>()
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
|
||||
async getByAssetId(ownerId: string, assetId: string): Promise<AlbumEntity[]> {
|
||||
async getByAssetId(ownerId: string, assetId: string) {
|
||||
return this.db
|
||||
.selectFrom('albums')
|
||||
.selectAll('albums')
|
||||
@@ -105,7 +107,7 @@ export class AlbumRepository {
|
||||
.select(withOwner)
|
||||
.select(withAlbumUsers)
|
||||
.orderBy('albums.createdAt', 'desc')
|
||||
.execute() as unknown as Promise<AlbumEntity[]>;
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [[DummyValue.UUID]] })
|
||||
@@ -134,7 +136,7 @@ export class AlbumRepository {
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async getOwned(ownerId: string): Promise<AlbumEntity[]> {
|
||||
async getOwned(ownerId: string) {
|
||||
return this.db
|
||||
.selectFrom('albums')
|
||||
.selectAll('albums')
|
||||
@@ -144,14 +146,14 @@ export class AlbumRepository {
|
||||
.where('albums.ownerId', '=', ownerId)
|
||||
.where('albums.deletedAt', 'is', null)
|
||||
.orderBy('albums.createdAt', 'desc')
|
||||
.execute() as unknown as Promise<AlbumEntity[]>;
|
||||
.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get albums shared with and shared by owner.
|
||||
*/
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async getShared(ownerId: string): Promise<AlbumEntity[]> {
|
||||
async getShared(ownerId: string) {
|
||||
return this.db
|
||||
.selectFrom('albums')
|
||||
.selectAll('albums')
|
||||
@@ -176,14 +178,14 @@ export class AlbumRepository {
|
||||
.select(withOwner)
|
||||
.select(withSharedLink)
|
||||
.orderBy('albums.createdAt', 'desc')
|
||||
.execute() as unknown as Promise<AlbumEntity[]>;
|
||||
.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get albums of owner that are _not_ shared
|
||||
*/
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async getNotShared(ownerId: string): Promise<AlbumEntity[]> {
|
||||
async getNotShared(ownerId: string) {
|
||||
return this.db
|
||||
.selectFrom('albums')
|
||||
.selectAll('albums')
|
||||
@@ -203,7 +205,7 @@ export class AlbumRepository {
|
||||
)
|
||||
.select(withOwner)
|
||||
.orderBy('albums.createdAt', 'desc')
|
||||
.execute() as unknown as Promise<AlbumEntity[]>;
|
||||
.execute();
|
||||
}
|
||||
|
||||
async restoreAll(userId: string): Promise<void> {
|
||||
@@ -262,7 +264,7 @@ export class AlbumRepository {
|
||||
await this.addAssets(this.db, albumId, assetIds);
|
||||
}
|
||||
|
||||
create(album: Insertable<Albums>, assetIds: string[], albumUsers: AlbumUserCreateDto[]): Promise<AlbumEntity> {
|
||||
create(album: Insertable<Albums>, assetIds: string[], albumUsers: AlbumUserCreateDto[]) {
|
||||
return this.db.transaction().execute(async (tx) => {
|
||||
const newAlbum = await tx.insertInto('albums').values(album).returning('albums.id').executeTakeFirst();
|
||||
|
||||
@@ -290,11 +292,12 @@ export class AlbumRepository {
|
||||
.select(withOwner)
|
||||
.select(withAssets)
|
||||
.select(withAlbumUsers)
|
||||
.executeTakeFirst() as unknown as Promise<AlbumEntity>;
|
||||
.$narrowType<{ assets: NotNull }>()
|
||||
.executeTakeFirstOrThrow();
|
||||
});
|
||||
}
|
||||
|
||||
update(id: string, album: Updateable<Albums>): Promise<AlbumEntity> {
|
||||
update(id: string, album: Updateable<Albums>) {
|
||||
return this.db
|
||||
.updateTable('albums')
|
||||
.set(album)
|
||||
@@ -303,7 +306,7 @@ export class AlbumRepository {
|
||||
.returning(withOwner)
|
||||
.returning(withSharedLink)
|
||||
.returning(withAlbumUsers)
|
||||
.executeTakeFirst() as unknown as Promise<AlbumEntity>;
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { withExifInner, withFaces, withFiles } from 'src/entities/asset.entity';
|
||||
import { AssetFileType } from 'src/enum';
|
||||
import { StorageAsset } from 'src/types';
|
||||
import { asUuid } from 'src/utils/database';
|
||||
import { anyUuid, asUuid } from 'src/utils/database';
|
||||
|
||||
@Injectable()
|
||||
export class AssetJobRepository {
|
||||
@@ -149,6 +149,21 @@ export class AssetJobRepository {
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
getForSyncAssets(ids: string[]) {
|
||||
return this.db
|
||||
.selectFrom('assets')
|
||||
.select([
|
||||
'assets.id',
|
||||
'assets.isOffline',
|
||||
'assets.libraryId',
|
||||
'assets.originalPath',
|
||||
'assets.status',
|
||||
'assets.fileModifiedAt',
|
||||
])
|
||||
.where('assets.id', '=', anyUuid(ids))
|
||||
.execute();
|
||||
}
|
||||
|
||||
private storageTemplateAssetQuery() {
|
||||
return this.db
|
||||
.selectFrom('assets')
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Insertable, Kysely, Selectable, UpdateResult, Updateable, sql } from 'kysely';
|
||||
import { Insertable, Kysely, NotNull, Selectable, UpdateResult, Updateable, sql } from 'kysely';
|
||||
import { isEmpty, isUndefined, omitBy } from 'lodash';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { Stack } from 'src/database';
|
||||
import { AssetFiles, AssetJobStatus, Assets, DB, Exif } from 'src/db';
|
||||
import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||
import {
|
||||
AssetEntity,
|
||||
hasPeople,
|
||||
@@ -23,7 +25,7 @@ import { AssetFileType, AssetOrder, AssetStatus, AssetType } from 'src/enum';
|
||||
import { AssetSearchOptions, SearchExploreItem, SearchExploreItemSet } from 'src/repositories/search.repository';
|
||||
import { anyUuid, asUuid, removeUndefinedKeys, unnest } from 'src/utils/database';
|
||||
import { globToSqlPattern } from 'src/utils/misc';
|
||||
import { Paginated, PaginationOptions, paginationHelper } from 'src/utils/pagination';
|
||||
import { PaginationOptions, paginationHelper } from 'src/utils/pagination';
|
||||
|
||||
export type AssetStats = Record<AssetType, number>;
|
||||
|
||||
@@ -141,12 +143,12 @@ export interface GetByIdsRelations {
|
||||
|
||||
export interface DuplicateGroup {
|
||||
duplicateId: string;
|
||||
assets: AssetEntity[];
|
||||
assets: MapAsset[];
|
||||
}
|
||||
|
||||
export interface DayOfYearAssets {
|
||||
yearsAgo: number;
|
||||
assets: AssetEntity[];
|
||||
assets: MapAsset[];
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@@ -234,12 +236,12 @@ export class AssetRepository {
|
||||
.execute();
|
||||
}
|
||||
|
||||
create(asset: Insertable<Assets>): Promise<AssetEntity> {
|
||||
return this.db.insertInto('assets').values(asset).returningAll().executeTakeFirst() as any as Promise<AssetEntity>;
|
||||
create(asset: Insertable<Assets>) {
|
||||
return this.db.insertInto('assets').values(asset).returningAll().executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
createAll(assets: Insertable<Assets>[]): Promise<AssetEntity[]> {
|
||||
return this.db.insertInto('assets').values(assets).returningAll().execute() as any as Promise<AssetEntity[]>;
|
||||
createAll(assets: Insertable<Assets>[]) {
|
||||
return this.db.insertInto('assets').values(assets).returningAll().execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, { day: 1, month: 1 }] })
|
||||
@@ -299,20 +301,13 @@ export class AssetRepository {
|
||||
|
||||
@GenerateSql({ params: [[DummyValue.UUID]] })
|
||||
@ChunkedArray()
|
||||
getByIds(ids: string[]): Promise<AssetEntity[]> {
|
||||
return (
|
||||
this.db
|
||||
//
|
||||
.selectFrom('assets')
|
||||
.selectAll('assets')
|
||||
.where('assets.id', '=', anyUuid(ids))
|
||||
.execute() as Promise<AssetEntity[]>
|
||||
);
|
||||
getByIds(ids: string[]) {
|
||||
return this.db.selectFrom('assets').selectAll('assets').where('assets.id', '=', anyUuid(ids)).execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [[DummyValue.UUID]] })
|
||||
@ChunkedArray()
|
||||
getByIdsWithAllRelations(ids: string[]): Promise<AssetEntity[]> {
|
||||
getByIdsWithAllRelationsButStacks(ids: string[]) {
|
||||
return this.db
|
||||
.selectFrom('assets')
|
||||
.selectAll('assets')
|
||||
@@ -320,23 +315,8 @@ export class AssetRepository {
|
||||
.select(withTags)
|
||||
.$call(withExif)
|
||||
.leftJoin('asset_stack', 'asset_stack.id', 'assets.stackId')
|
||||
.leftJoinLateral(
|
||||
(eb) =>
|
||||
eb
|
||||
.selectFrom('assets as stacked')
|
||||
.selectAll('asset_stack')
|
||||
.select((eb) => eb.fn('array_agg', [eb.table('stacked')]).as('assets'))
|
||||
.whereRef('stacked.stackId', '=', 'asset_stack.id')
|
||||
.whereRef('stacked.id', '!=', 'asset_stack.primaryAssetId')
|
||||
.where('stacked.deletedAt', 'is', null)
|
||||
.where('stacked.isArchived', '=', false)
|
||||
.groupBy('asset_stack.id')
|
||||
.as('stacked_assets'),
|
||||
(join) => join.on('asset_stack.id', 'is not', null),
|
||||
)
|
||||
.select((eb) => eb.fn.toJson(eb.table('stacked_assets')).as('stack'))
|
||||
.where('assets.id', '=', anyUuid(ids))
|
||||
.execute() as any as Promise<AssetEntity[]>;
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
@@ -356,36 +336,29 @@ export class AssetRepository {
|
||||
return assets.map((asset) => asset.deviceAssetId);
|
||||
}
|
||||
|
||||
getByUserId(
|
||||
pagination: PaginationOptions,
|
||||
userId: string,
|
||||
options: Omit<AssetSearchOptions, 'userIds'> = {},
|
||||
): Paginated<AssetEntity> {
|
||||
getByUserId(pagination: PaginationOptions, userId: string, options: Omit<AssetSearchOptions, 'userIds'> = {}) {
|
||||
return this.getAll(pagination, { ...options, userIds: [userId] });
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] })
|
||||
getByLibraryIdAndOriginalPath(libraryId: string, originalPath: string): Promise<AssetEntity | undefined> {
|
||||
getByLibraryIdAndOriginalPath(libraryId: string, originalPath: string) {
|
||||
return this.db
|
||||
.selectFrom('assets')
|
||||
.selectAll('assets')
|
||||
.where('libraryId', '=', asUuid(libraryId))
|
||||
.where('originalPath', '=', originalPath)
|
||||
.limit(1)
|
||||
.executeTakeFirst() as any as Promise<AssetEntity | undefined>;
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async getAll(
|
||||
pagination: PaginationOptions,
|
||||
{ orderDirection, ...options }: AssetSearchOptions = {},
|
||||
): Paginated<AssetEntity> {
|
||||
async getAll(pagination: PaginationOptions, { orderDirection, ...options }: AssetSearchOptions = {}) {
|
||||
const builder = searchAssetBuilder(this.db, options)
|
||||
.select(withFiles)
|
||||
.orderBy('assets.createdAt', orderDirection ?? 'asc')
|
||||
.limit(pagination.take + 1)
|
||||
.offset(pagination.skip ?? 0);
|
||||
const items = await builder.execute();
|
||||
return paginationHelper(items as any as AssetEntity[], pagination.take);
|
||||
return paginationHelper(items, pagination.take);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -420,23 +393,22 @@ export class AssetRepository {
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getById(
|
||||
id: string,
|
||||
{ exifInfo, faces, files, library, owner, smartSearch, stack, tags }: GetByIdsRelations = {},
|
||||
): Promise<AssetEntity | undefined> {
|
||||
getById(id: string, { exifInfo, faces, files, library, owner, smartSearch, stack, tags }: GetByIdsRelations = {}) {
|
||||
return this.db
|
||||
.selectFrom('assets')
|
||||
.selectAll('assets')
|
||||
.where('assets.id', '=', asUuid(id))
|
||||
.$if(!!exifInfo, withExif)
|
||||
.$if(!!faces, (qb) => qb.select(faces?.person ? withFacesAndPeople : withFaces))
|
||||
.$if(!!faces, (qb) => qb.select(faces?.person ? withFacesAndPeople : withFaces).$narrowType<{ faces: NotNull }>())
|
||||
.$if(!!library, (qb) => qb.select(withLibrary))
|
||||
.$if(!!owner, (qb) => qb.select(withOwner))
|
||||
.$if(!!smartSearch, withSmartSearch)
|
||||
.$if(!!stack, (qb) =>
|
||||
qb
|
||||
.leftJoin('asset_stack', 'asset_stack.id', 'assets.stackId')
|
||||
.$if(!stack!.assets, (qb) => qb.select((eb) => eb.fn.toJson(eb.table('asset_stack')).as('stack')))
|
||||
.$if(!stack!.assets, (qb) =>
|
||||
qb.select((eb) => eb.fn.toJson(eb.table('asset_stack')).$castTo<Stack | null>().as('stack')),
|
||||
)
|
||||
.$if(!!stack!.assets, (qb) =>
|
||||
qb
|
||||
.leftJoinLateral(
|
||||
@@ -453,13 +425,13 @@ export class AssetRepository {
|
||||
.as('stacked_assets'),
|
||||
(join) => join.on('asset_stack.id', 'is not', null),
|
||||
)
|
||||
.select((eb) => eb.fn.toJson(eb.table('stacked_assets')).as('stack')),
|
||||
.select((eb) => eb.fn.toJson(eb.table('stacked_assets')).$castTo<Stack | null>().as('stack')),
|
||||
),
|
||||
)
|
||||
.$if(!!files, (qb) => qb.select(withFiles))
|
||||
.$if(!!tags, (qb) => qb.select(withTags))
|
||||
.limit(1)
|
||||
.executeTakeFirst() as any as Promise<AssetEntity | undefined>;
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [[DummyValue.UUID], { deviceId: DummyValue.STRING }] })
|
||||
@@ -488,7 +460,7 @@ export class AssetRepository {
|
||||
.execute();
|
||||
}
|
||||
|
||||
async update(asset: Updateable<Assets> & { id: string }): Promise<AssetEntity> {
|
||||
async update(asset: Updateable<Assets> & { id: string }) {
|
||||
const value = omitBy(asset, isUndefined);
|
||||
delete value.id;
|
||||
if (!isEmpty(value)) {
|
||||
@@ -498,10 +470,10 @@ export class AssetRepository {
|
||||
.selectAll('assets')
|
||||
.$call(withExif)
|
||||
.$call((qb) => qb.select(withFacesAndPeople))
|
||||
.executeTakeFirst() as Promise<AssetEntity>;
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
return this.getById(asset.id, { exifInfo: true, faces: { person: true } }) as Promise<AssetEntity>;
|
||||
return this.getById(asset.id, { exifInfo: true, faces: { person: true } });
|
||||
}
|
||||
|
||||
async remove(asset: { id: string }): Promise<void> {
|
||||
@@ -509,7 +481,7 @@ export class AssetRepository {
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [{ ownerId: DummyValue.UUID, libraryId: DummyValue.UUID, checksum: DummyValue.BUFFER }] })
|
||||
getByChecksum({ ownerId, libraryId, checksum }: AssetGetByChecksumOptions): Promise<AssetEntity | undefined> {
|
||||
getByChecksum({ ownerId, libraryId, checksum }: AssetGetByChecksumOptions) {
|
||||
return this.db
|
||||
.selectFrom('assets')
|
||||
.selectAll('assets')
|
||||
@@ -517,7 +489,7 @@ export class AssetRepository {
|
||||
.where('checksum', '=', checksum)
|
||||
.$call((qb) => (libraryId ? qb.where('libraryId', '=', asUuid(libraryId)) : qb.where('libraryId', 'is', null)))
|
||||
.limit(1)
|
||||
.executeTakeFirst() as Promise<AssetEntity | undefined>;
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.BUFFER]] })
|
||||
@@ -544,7 +516,7 @@ export class AssetRepository {
|
||||
return asset?.id;
|
||||
}
|
||||
|
||||
findLivePhotoMatch(options: LivePhotoSearchOptions): Promise<AssetEntity | undefined> {
|
||||
findLivePhotoMatch(options: LivePhotoSearchOptions) {
|
||||
const { ownerId, otherAssetId, livePhotoCID, type } = options;
|
||||
return this.db
|
||||
.selectFrom('assets')
|
||||
@@ -555,7 +527,7 @@ export class AssetRepository {
|
||||
.where('type', '=', type)
|
||||
.where('exif.livePhotoCID', '=', livePhotoCID)
|
||||
.limit(1)
|
||||
.executeTakeFirst() as Promise<AssetEntity | undefined>;
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql(
|
||||
@@ -564,7 +536,7 @@ export class AssetRepository {
|
||||
params: [DummyValue.PAGINATION, property],
|
||||
})),
|
||||
)
|
||||
async getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated<AssetEntity> {
|
||||
async getWithout(pagination: PaginationOptions, property: WithoutProperty) {
|
||||
const items = await this.db
|
||||
.selectFrom('assets')
|
||||
.selectAll('assets')
|
||||
@@ -626,7 +598,7 @@ export class AssetRepository {
|
||||
.orderBy('createdAt')
|
||||
.execute();
|
||||
|
||||
return paginationHelper(items as any as AssetEntity[], pagination.take);
|
||||
return paginationHelper(items, pagination.take);
|
||||
}
|
||||
|
||||
getStatistics(ownerId: string, { isArchived, isFavorite, isTrashed }: AssetStatsOptions): Promise<AssetStats> {
|
||||
@@ -645,7 +617,7 @@ export class AssetRepository {
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
getRandom(userIds: string[], take: number): Promise<AssetEntity[]> {
|
||||
getRandom(userIds: string[], take: number) {
|
||||
return this.db
|
||||
.selectFrom('assets')
|
||||
.selectAll('assets')
|
||||
@@ -655,7 +627,7 @@ export class AssetRepository {
|
||||
.where('deletedAt', 'is', null)
|
||||
.orderBy((eb) => eb.fn('random'))
|
||||
.limit(take)
|
||||
.execute() as any as Promise<AssetEntity[]>;
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [{ size: TimeBucketSize.MONTH }] })
|
||||
@@ -708,7 +680,7 @@ export class AssetRepository {
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.TIME_BUCKET, { size: TimeBucketSize.MONTH, withStacked: true }] })
|
||||
async getTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise<AssetEntity[]> {
|
||||
async getTimeBucket(timeBucket: string, options: TimeBucketOptions) {
|
||||
return this.db
|
||||
.selectFrom('assets')
|
||||
.selectAll('assets')
|
||||
@@ -741,7 +713,7 @@ export class AssetRepository {
|
||||
.as('stacked_assets'),
|
||||
(join) => join.on('asset_stack.id', 'is not', null),
|
||||
)
|
||||
.select((eb) => eb.fn.toJson(eb.table('stacked_assets')).as('stack')),
|
||||
.select((eb) => eb.fn.toJson(eb.table('stacked_assets').$castTo<Stack | null>()).as('stack')),
|
||||
)
|
||||
.$if(!!options.assetType, (qb) => qb.where('assets.type', '=', options.assetType!))
|
||||
.$if(options.isDuplicate !== undefined, (qb) =>
|
||||
@@ -753,11 +725,11 @@ export class AssetRepository {
|
||||
.where('assets.isVisible', '=', true)
|
||||
.where(truncatedDate(options.size), '=', timeBucket.replace(/^[+-]/, ''))
|
||||
.orderBy('assets.localDateTime', options.order ?? 'desc')
|
||||
.execute() as any as Promise<AssetEntity[]>;
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getDuplicates(userId: string): Promise<DuplicateGroup[]> {
|
||||
getDuplicates(userId: string) {
|
||||
return (
|
||||
this.db
|
||||
.with('duplicates', (qb) =>
|
||||
@@ -774,9 +746,15 @@ export class AssetRepository {
|
||||
(join) => join.onTrue(),
|
||||
)
|
||||
.select('assets.duplicateId')
|
||||
.select((eb) => eb.fn('jsonb_agg', [eb.table('asset')]).as('assets'))
|
||||
.select((eb) =>
|
||||
eb
|
||||
.fn('jsonb_agg', [eb.table('asset')])
|
||||
.$castTo<MapAsset[]>()
|
||||
.as('assets'),
|
||||
)
|
||||
.where('assets.ownerId', '=', asUuid(userId))
|
||||
.where('assets.duplicateId', 'is not', null)
|
||||
.$narrowType<{ duplicateId: NotNull }>()
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
.where('assets.isVisible', '=', true)
|
||||
.where('assets.stackId', 'is', null)
|
||||
@@ -801,7 +779,7 @@ export class AssetRepository {
|
||||
.where(({ not, exists }) =>
|
||||
not(exists((eb) => eb.selectFrom('unique').whereRef('unique.duplicateId', '=', 'duplicates.duplicateId'))),
|
||||
)
|
||||
.execute() as any as Promise<DuplicateGroup[]>
|
||||
.execute()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -845,7 +823,7 @@ export class AssetRepository {
|
||||
},
|
||||
],
|
||||
})
|
||||
getAllForUserFullSync(options: AssetFullSyncOptions): Promise<AssetEntity[]> {
|
||||
getAllForUserFullSync(options: AssetFullSyncOptions) {
|
||||
const { ownerId, lastId, updatedUntil, limit } = options;
|
||||
return this.db
|
||||
.selectFrom('assets')
|
||||
@@ -863,18 +841,18 @@ export class AssetRepository {
|
||||
.as('stacked_assets'),
|
||||
(join) => join.on('asset_stack.id', 'is not', null),
|
||||
)
|
||||
.select((eb) => eb.fn.toJson(eb.table('stacked_assets')).as('stack'))
|
||||
.select((eb) => eb.fn.toJson(eb.table('stacked_assets')).$castTo<Stack | null>().as('stack'))
|
||||
.where('assets.ownerId', '=', asUuid(ownerId))
|
||||
.where('assets.isVisible', '=', true)
|
||||
.where('assets.updatedAt', '<=', updatedUntil)
|
||||
.$if(!!lastId, (qb) => qb.where('assets.id', '>', lastId!))
|
||||
.orderBy('assets.id')
|
||||
.limit(limit)
|
||||
.execute() as any as Promise<AssetEntity[]>;
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [{ userIds: [DummyValue.UUID], updatedAfter: DummyValue.DATE, limit: 100 }] })
|
||||
async getChangedDeltaSync(options: AssetDeltaSyncOptions): Promise<AssetEntity[]> {
|
||||
async getChangedDeltaSync(options: AssetDeltaSyncOptions) {
|
||||
return this.db
|
||||
.selectFrom('assets')
|
||||
.selectAll('assets')
|
||||
@@ -891,12 +869,12 @@ export class AssetRepository {
|
||||
.as('stacked_assets'),
|
||||
(join) => join.on('asset_stack.id', 'is not', null),
|
||||
)
|
||||
.select((eb) => eb.fn.toJson(eb.table('stacked_assets')).as('stack'))
|
||||
.select((eb) => eb.fn.toJson(eb.table('stacked_assets').$castTo<Stack | null>()).as('stack'))
|
||||
.where('assets.ownerId', '=', anyUuid(options.userIds))
|
||||
.where('assets.isVisible', '=', true)
|
||||
.where('assets.updatedAt', '>', options.updatedAfter)
|
||||
.limit(options.limit)
|
||||
.execute() as any as Promise<AssetEntity[]>;
|
||||
.execute();
|
||||
}
|
||||
|
||||
async upsertFile(file: Pick<Insertable<AssetFiles>, 'assetId' | 'path' | 'type'>): Promise<void> {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Kysely, OrderByDirection, sql } from 'kysely';
|
||||
import { Kysely, OrderByDirection, Selectable, sql } from 'kysely';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { DB } from 'src/db';
|
||||
import { DB, Exif } from 'src/db';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { AssetEntity, searchAssetBuilder } from 'src/entities/asset.entity';
|
||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { searchAssetBuilder } from 'src/entities/asset.entity';
|
||||
import { AssetStatus, AssetType } from 'src/enum';
|
||||
import { anyUuid, asUuid } from 'src/utils/database';
|
||||
import { Paginated } from 'src/utils/pagination';
|
||||
import { isValidInteger } from 'src/validation';
|
||||
|
||||
export interface SearchResult<T> {
|
||||
@@ -216,7 +216,7 @@ export class SearchRepository {
|
||||
},
|
||||
],
|
||||
})
|
||||
async searchMetadata(pagination: SearchPaginationOptions, options: AssetSearchOptions): Paginated<AssetEntity> {
|
||||
async searchMetadata(pagination: SearchPaginationOptions, options: AssetSearchOptions) {
|
||||
const orderDirection = (options.orderDirection?.toLowerCase() || 'desc') as OrderByDirection;
|
||||
const items = await searchAssetBuilder(this.db, options)
|
||||
.orderBy('assets.fileCreatedAt', orderDirection)
|
||||
@@ -225,7 +225,7 @@ export class SearchRepository {
|
||||
.execute();
|
||||
const hasNextPage = items.length > pagination.size;
|
||||
items.splice(pagination.size);
|
||||
return { items: items as any as AssetEntity[], hasNextPage };
|
||||
return { items, hasNextPage };
|
||||
}
|
||||
|
||||
@GenerateSql({
|
||||
@@ -240,7 +240,7 @@ export class SearchRepository {
|
||||
},
|
||||
],
|
||||
})
|
||||
async searchRandom(size: number, options: AssetSearchOptions): Promise<AssetEntity[]> {
|
||||
async searchRandom(size: number, options: AssetSearchOptions) {
|
||||
const uuid = randomUUID();
|
||||
const builder = searchAssetBuilder(this.db, options);
|
||||
const lessThan = builder
|
||||
@@ -251,8 +251,8 @@ export class SearchRepository {
|
||||
.where('assets.id', '>', uuid)
|
||||
.orderBy(sql`random()`)
|
||||
.limit(size);
|
||||
const { rows } = await sql`${lessThan} union all ${greaterThan} limit ${size}`.execute(this.db);
|
||||
return rows as any as AssetEntity[];
|
||||
const { rows } = await sql<MapAsset>`${lessThan} union all ${greaterThan} limit ${size}`.execute(this.db);
|
||||
return rows;
|
||||
}
|
||||
|
||||
@GenerateSql({
|
||||
@@ -268,17 +268,17 @@ export class SearchRepository {
|
||||
},
|
||||
],
|
||||
})
|
||||
async searchSmart(pagination: SearchPaginationOptions, options: SmartSearchOptions): Paginated<AssetEntity> {
|
||||
async searchSmart(pagination: SearchPaginationOptions, options: SmartSearchOptions) {
|
||||
if (!isValidInteger(pagination.size, { min: 1, max: 1000 })) {
|
||||
throw new Error(`Invalid value for 'size': ${pagination.size}`);
|
||||
}
|
||||
|
||||
const items = (await searchAssetBuilder(this.db, options)
|
||||
const items = await searchAssetBuilder(this.db, options)
|
||||
.innerJoin('smart_search', 'assets.id', 'smart_search.assetId')
|
||||
.orderBy(sql`smart_search.embedding <=> ${options.embedding}`)
|
||||
.limit(pagination.size + 1)
|
||||
.offset((pagination.page - 1) * pagination.size)
|
||||
.execute()) as any as AssetEntity[];
|
||||
.execute();
|
||||
|
||||
const hasNextPage = items.length > pagination.size;
|
||||
items.splice(pagination.size);
|
||||
@@ -392,7 +392,7 @@ export class SearchRepository {
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [[DummyValue.UUID]] })
|
||||
getAssetsByCity(userIds: string[]): Promise<AssetEntity[]> {
|
||||
getAssetsByCity(userIds: string[]) {
|
||||
return this.db
|
||||
.withRecursive('cte', (qb) => {
|
||||
const base = qb
|
||||
@@ -434,9 +434,14 @@ export class SearchRepository {
|
||||
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
||||
.innerJoin('cte', 'assets.id', 'cte.assetId')
|
||||
.selectAll('assets')
|
||||
.select((eb) => eb.fn('to_jsonb', [eb.table('exif')]).as('exifInfo'))
|
||||
.select((eb) =>
|
||||
eb
|
||||
.fn('to_jsonb', [eb.table('exif')])
|
||||
.$castTo<Selectable<Exif>>()
|
||||
.as('exifInfo'),
|
||||
)
|
||||
.orderBy('exif.city')
|
||||
.execute() as any as Promise<AssetEntity[]>;
|
||||
.execute();
|
||||
}
|
||||
|
||||
async upsert(assetId: string, embedding: string): Promise<void> {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Insertable, Kysely, sql, Updateable } from 'kysely';
|
||||
import { Insertable, Kysely, NotNull, sql, Updateable } from 'kysely';
|
||||
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import _ from 'lodash';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { columns } from 'src/database';
|
||||
import { Album, columns } from 'src/database';
|
||||
import { DB, SharedLinks } from 'src/db';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { SharedLinkType } from 'src/enum';
|
||||
|
||||
export type SharedLinkSearchOptions = {
|
||||
@@ -19,7 +19,7 @@ export class SharedLinkRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
|
||||
get(userId: string, id: string): Promise<SharedLinkEntity | undefined> {
|
||||
get(userId: string, id: string) {
|
||||
return this.db
|
||||
.selectFrom('shared_links')
|
||||
.selectAll('shared_links')
|
||||
@@ -87,18 +87,23 @@ export class SharedLinkRepository {
|
||||
.as('album'),
|
||||
(join) => join.onTrue(),
|
||||
)
|
||||
.select((eb) => eb.fn.coalesce(eb.fn.jsonAgg('a').filterWhere('a.id', 'is not', null), sql`'[]'`).as('assets'))
|
||||
.select((eb) =>
|
||||
eb.fn
|
||||
.coalesce(eb.fn.jsonAgg('a').filterWhere('a.id', 'is not', null), sql`'[]'`)
|
||||
.$castTo<MapAsset[]>()
|
||||
.as('assets'),
|
||||
)
|
||||
.groupBy(['shared_links.id', sql`"album".*`])
|
||||
.select((eb) => eb.fn.toJson('album').as('album'))
|
||||
.select((eb) => eb.fn.toJson('album').$castTo<Album | null>().as('album'))
|
||||
.where('shared_links.id', '=', id)
|
||||
.where('shared_links.userId', '=', userId)
|
||||
.where((eb) => eb.or([eb('shared_links.type', '=', SharedLinkType.INDIVIDUAL), eb('album.id', 'is not', null)]))
|
||||
.orderBy('shared_links.createdAt', 'desc')
|
||||
.executeTakeFirst() as Promise<SharedLinkEntity | undefined>;
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [{ userId: DummyValue.UUID, albumId: DummyValue.UUID }] })
|
||||
getAll({ userId, albumId }: SharedLinkSearchOptions): Promise<SharedLinkEntity[]> {
|
||||
getAll({ userId, albumId }: SharedLinkSearchOptions) {
|
||||
return this.db
|
||||
.selectFrom('shared_links')
|
||||
.selectAll('shared_links')
|
||||
@@ -115,6 +120,7 @@ export class SharedLinkRepository {
|
||||
(join) => join.onTrue(),
|
||||
)
|
||||
.select('assets.assets')
|
||||
.$narrowType<{ assets: NotNull }>()
|
||||
.leftJoinLateral(
|
||||
(eb) =>
|
||||
eb
|
||||
@@ -152,12 +158,12 @@ export class SharedLinkRepository {
|
||||
.as('album'),
|
||||
(join) => join.onTrue(),
|
||||
)
|
||||
.select((eb) => eb.fn.toJson('album').as('album'))
|
||||
.select((eb) => eb.fn.toJson('album').$castTo<Album | null>().as('album'))
|
||||
.where((eb) => eb.or([eb('shared_links.type', '=', SharedLinkType.INDIVIDUAL), eb('album.id', 'is not', null)]))
|
||||
.$if(!!albumId, (eb) => eb.where('shared_links.albumId', '=', albumId!))
|
||||
.orderBy('shared_links.createdAt', 'desc')
|
||||
.distinctOn(['shared_links.createdAt'])
|
||||
.execute() as unknown as Promise<SharedLinkEntity[]>;
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.BUFFER] })
|
||||
@@ -177,7 +183,7 @@ export class SharedLinkRepository {
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async create(entity: Insertable<SharedLinks> & { assetIds?: string[] }): Promise<SharedLinkEntity> {
|
||||
async create(entity: Insertable<SharedLinks> & { assetIds?: string[] }) {
|
||||
const { id } = await this.db
|
||||
.insertInto('shared_links')
|
||||
.values(_.omit(entity, 'assetIds'))
|
||||
@@ -194,7 +200,7 @@ export class SharedLinkRepository {
|
||||
return this.getSharedLinks(id);
|
||||
}
|
||||
|
||||
async update(entity: Updateable<SharedLinks> & { id: string; assetIds?: string[] }): Promise<SharedLinkEntity> {
|
||||
async update(entity: Updateable<SharedLinks> & { id: string; assetIds?: string[] }) {
|
||||
const { id } = await this.db
|
||||
.updateTable('shared_links')
|
||||
.set(_.omit(entity, 'assets', 'album', 'assetIds'))
|
||||
@@ -212,8 +218,8 @@ export class SharedLinkRepository {
|
||||
return this.getSharedLinks(id);
|
||||
}
|
||||
|
||||
async remove(entity: SharedLinkEntity): Promise<void> {
|
||||
await this.db.deleteFrom('shared_links').where('shared_links.id', '=', entity.id).execute();
|
||||
async remove(id: string): Promise<void> {
|
||||
await this.db.deleteFrom('shared_links').where('shared_links.id', '=', id).execute();
|
||||
}
|
||||
|
||||
private getSharedLinks(id: string) {
|
||||
@@ -236,9 +242,12 @@ export class SharedLinkRepository {
|
||||
(join) => join.onTrue(),
|
||||
)
|
||||
.select((eb) =>
|
||||
eb.fn.coalesce(eb.fn.jsonAgg('assets').filterWhere('assets.id', 'is not', null), sql`'[]'`).as('assets'),
|
||||
eb.fn
|
||||
.coalesce(eb.fn.jsonAgg('assets').filterWhere('assets.id', 'is not', null), sql`'[]'`)
|
||||
.$castTo<MapAsset[]>()
|
||||
.as('assets'),
|
||||
)
|
||||
.groupBy('shared_links.id')
|
||||
.executeTakeFirstOrThrow() as Promise<SharedLinkEntity>;
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { InjectKysely } from 'nestjs-kysely';
|
||||
import { columns } from 'src/database';
|
||||
import { AssetStack, DB } from 'src/db';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { asUuid } from 'src/utils/database';
|
||||
|
||||
export interface StackSearch {
|
||||
@@ -36,9 +35,7 @@ const withAssets = (eb: ExpressionBuilder<DB, 'asset_stack'>, withTags = false)
|
||||
.select((eb) => eb.fn.toJson('exifInfo').as('exifInfo'))
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
.whereRef('assets.stackId', '=', 'asset_stack.id'),
|
||||
)
|
||||
.$castTo<AssetEntity[]>()
|
||||
.as('assets');
|
||||
).as('assets');
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
|
||||
Reference in New Issue
Block a user