mirror of
https://github.com/immich-app/immich.git
synced 2026-01-29 23:23:45 -05:00
* - add component - update server's StackCreateDto for merge parameter - Update stackRepo to only merge stacks when merge=true (default) - update web action handlers to show stack changes * - make open-api * lint & format * - Add proper icon to 'remove from stack' - change web unstack icon to image-off-outline * - cleanup * - format & lint * - make open-api: StackCreateDto merge optional * initial addition of new endpoint * remove stack endpoint * - fix up remove stack endpoint - open-api * - Undo stackCreate merge parameter * - open-api typescript * open-api dart * Tests: - add tests - update assetStub.imageFrom2015 to have required stack attributes to include it with tests * update event name * Fix event name in test * remove asset_update check * - merge stack.removeAsset params into one object - refactor asset existence check (no need for asset fetch) - fix tests * Don't return updated stack * Create specialized stack id & primary asset fetch for asset removal checks * Correct new permission names * make sql * - fix open-api * - cleanup
166 lines
4.8 KiB
TypeScript
166 lines
4.8 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
import { ExpressionBuilder, Insertable, Kysely, Updateable } from 'kysely';
|
|
import { jsonArrayFrom } from 'kysely/helpers/postgres';
|
|
import { InjectKysely } from 'nestjs-kysely';
|
|
import { columns } from 'src/database';
|
|
import { DummyValue, GenerateSql } from 'src/decorators';
|
|
import { DB } from 'src/schema';
|
|
import { StackTable } from 'src/schema/tables/stack.table';
|
|
import { asUuid, withDefaultVisibility } from 'src/utils/database';
|
|
|
|
export interface StackSearch {
|
|
ownerId: string;
|
|
primaryAssetId?: string;
|
|
}
|
|
|
|
const withAssets = (eb: ExpressionBuilder<DB, 'stack'>, withTags = false) => {
|
|
return jsonArrayFrom(
|
|
eb
|
|
.selectFrom('asset')
|
|
.selectAll('asset')
|
|
.innerJoinLateral(
|
|
(eb) =>
|
|
eb
|
|
.selectFrom('asset_exif')
|
|
.select(columns.exif)
|
|
.whereRef('asset_exif.assetId', '=', 'asset.id')
|
|
.as('exifInfo'),
|
|
(join) => join.onTrue(),
|
|
)
|
|
.$if(withTags, (eb) =>
|
|
eb.select((eb) =>
|
|
jsonArrayFrom(
|
|
eb
|
|
.selectFrom('tag')
|
|
.select(columns.tag)
|
|
.innerJoin('tag_asset', 'tag.id', 'tag_asset.tagsId')
|
|
.whereRef('tag_asset.assetsId', '=', 'asset.id'),
|
|
).as('tags'),
|
|
),
|
|
)
|
|
.select((eb) => eb.fn.toJson('exifInfo').as('exifInfo'))
|
|
.where('asset.deletedAt', 'is', null)
|
|
.whereRef('asset.stackId', '=', 'stack.id')
|
|
.$call(withDefaultVisibility),
|
|
).as('assets');
|
|
};
|
|
|
|
@Injectable()
|
|
export class StackRepository {
|
|
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
|
|
|
@GenerateSql({ params: [{ ownerId: DummyValue.UUID }] })
|
|
search(query: StackSearch) {
|
|
return this.db
|
|
.selectFrom('stack')
|
|
.selectAll('stack')
|
|
.select(withAssets)
|
|
.where('stack.ownerId', '=', query.ownerId)
|
|
.$if(!!query.primaryAssetId, (eb) => eb.where('stack.primaryAssetId', '=', query.primaryAssetId!))
|
|
.execute();
|
|
}
|
|
|
|
async create(entity: Omit<Insertable<StackTable>, 'primaryAssetId'>, assetIds: string[]) {
|
|
return this.db.transaction().execute(async (tx) => {
|
|
const stacks = await tx
|
|
.selectFrom('stack')
|
|
.where('stack.ownerId', '=', entity.ownerId)
|
|
.where('stack.primaryAssetId', 'in', assetIds)
|
|
.select('stack.id')
|
|
.select((eb) =>
|
|
jsonArrayFrom(
|
|
eb
|
|
.selectFrom('asset')
|
|
.select('asset.id')
|
|
.whereRef('asset.stackId', '=', 'stack.id')
|
|
.where('asset.deletedAt', 'is', null),
|
|
).as('assets'),
|
|
)
|
|
.execute();
|
|
|
|
const uniqueIds = new Set<string>(assetIds);
|
|
|
|
// children
|
|
for (const stack of stacks) {
|
|
if (stack.assets && stack.assets.length > 0) {
|
|
for (const asset of stack.assets) {
|
|
uniqueIds.add(asset.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (stacks.length > 0) {
|
|
await tx
|
|
.deleteFrom('stack')
|
|
.where(
|
|
'id',
|
|
'in',
|
|
stacks.map((stack) => stack.id),
|
|
)
|
|
.execute();
|
|
}
|
|
|
|
const newRecord = await tx
|
|
.insertInto('stack')
|
|
.values({ ...entity, primaryAssetId: assetIds[0] })
|
|
.returning('id')
|
|
.executeTakeFirstOrThrow();
|
|
|
|
await tx
|
|
.updateTable('asset')
|
|
.set({
|
|
stackId: newRecord.id,
|
|
updatedAt: new Date(),
|
|
})
|
|
.where('id', 'in', [...uniqueIds])
|
|
.execute();
|
|
|
|
return tx
|
|
.selectFrom('stack')
|
|
.selectAll('stack')
|
|
.select(withAssets)
|
|
.where('id', '=', newRecord.id)
|
|
.executeTakeFirstOrThrow();
|
|
});
|
|
}
|
|
|
|
@GenerateSql({ params: [DummyValue.UUID] })
|
|
async delete(id: string): Promise<void> {
|
|
await this.db.deleteFrom('stack').where('id', '=', asUuid(id)).execute();
|
|
}
|
|
|
|
async deleteAll(ids: string[]): Promise<void> {
|
|
await this.db.deleteFrom('stack').where('id', 'in', ids).execute();
|
|
}
|
|
|
|
update(id: string, entity: Updateable<StackTable>) {
|
|
return this.db
|
|
.updateTable('stack')
|
|
.set(entity)
|
|
.where('id', '=', asUuid(id))
|
|
.returningAll('stack')
|
|
.returning((eb) => withAssets(eb, true))
|
|
.executeTakeFirstOrThrow();
|
|
}
|
|
|
|
@GenerateSql({ params: [DummyValue.UUID] })
|
|
getById(id: string) {
|
|
return this.db
|
|
.selectFrom('stack')
|
|
.selectAll()
|
|
.select((eb) => withAssets(eb, true))
|
|
.where('id', '=', asUuid(id))
|
|
.executeTakeFirst();
|
|
}
|
|
|
|
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
|
|
getForAssetRemoval(assetId: string) {
|
|
return this.db
|
|
.selectFrom('asset')
|
|
.leftJoin('stack', 'stack.id', 'asset.stackId')
|
|
.select(['stackId as id', 'stack.primaryAssetId'])
|
|
.where('asset.id', '=', assetId)
|
|
.executeTakeFirst();
|
|
}
|
|
}
|