refactor(workflow): map AlbumAssetAdded trigger to AssetV1

Drop the AssetAlbumV1 workflow type and have the AlbumAssetAdded trigger
reuse the existing asset-aware AssetV1 type, so all current AssetV1
plugin methods work with it out of the box.
This commit is contained in:
Alex Tran
2026-06-04 01:51:36 +00:00
parent 57d77ad5da
commit 72eaba6ee2
10 changed files with 39 additions and 96 deletions
-1
View File
@@ -1172,7 +1172,6 @@ export const WorkflowTriggerSchema = z
export enum WorkflowType {
AssetV1 = 'AssetV1',
AssetPersonV1 = 'AssetPersonV1',
AssetAlbumV1 = 'AssetAlbumV1',
}
export const WorkflowTypeSchema = z.enum(WorkflowType).describe('Workflow type').meta({ id: 'WorkflowType' });
@@ -5,7 +5,6 @@ import { InjectKysely } from 'nestjs-kysely';
import { columns } from 'src/database';
import { DummyValue, GenerateSql } from 'src/decorators';
import { WorkflowSearchDto } from 'src/dtos/workflow.dto';
import { AlbumUserRole } from 'src/enum';
import { DB } from 'src/schema';
import { WorkflowStepTable } from 'src/schema/tables/workflow-step.table';
import { WorkflowTable } from 'src/schema/tables/workflow.table';
@@ -183,15 +182,4 @@ export class WorkflowRepository {
.where('id', '=', assetId)
.executeTakeFirstOrThrow();
}
getAlbumForWorkflow(albumId: string) {
return this.db
.selectFrom('album')
.innerJoin('album_user', 'album_user.albumId', 'album.id')
.where('album_user.role', '=', AlbumUserRole.Owner)
.select(['album.id', 'album_user.userId as ownerId', 'album.albumName', 'album.description'])
.where('album.id', '=', albumId)
.where('album.deletedAt', 'is', null)
.executeTakeFirstOrThrow();
}
}
@@ -40,7 +40,7 @@ type ExecuteOptions<T extends WorkflowType> = {
write: (auth: AuthDto, changes: WorkflowChanges<T>) => Promise<void>;
};
type AssetTrigger = { userId: string; assetId: string; albumId?: string; trigger: WorkflowTrigger };
type AssetTrigger = { userId: string; assetId: string; trigger: WorkflowTrigger };
export class WorkflowExecutionService extends BaseService {
private jwtSecret!: string;
@@ -275,56 +275,25 @@ export class WorkflowExecutionService extends BaseService {
}
@OnEvent({ name: 'AlbumAssetAdd' })
onAlbumAssetAdd({ userId, assetId, albumId }: ArgOf<'AlbumAssetAdd'>) {
return this.onAssetTrigger({ userId, assetId, albumId, trigger: WorkflowTrigger.AlbumAssetAdded });
onAlbumAssetAdd({ userId, assetId }: ArgOf<'AlbumAssetAdd'>) {
return this.onAssetTrigger({ userId, assetId, trigger: WorkflowTrigger.AlbumAssetAdded });
}
private async onAssetTrigger({ userId, assetId, albumId, trigger }: AssetTrigger) {
private async onAssetTrigger({ userId, assetId, trigger }: AssetTrigger) {
const items = await this.workflowRepository.search({ userId, trigger });
await this.jobRepository.queueAll(
items.map((workflow) => ({
name: JobName.WorkflowAssetTrigger,
data: { workflowId: workflow.id, assetId, albumId, trigger },
data: { workflowId: workflow.id, assetId, trigger },
})),
);
}
@OnJob({ name: JobName.WorkflowAssetTrigger, queue: QueueName.Workflow })
handleAssetTrigger({ workflowId, assetId, albumId }: JobOf<JobName.WorkflowAssetTrigger>) {
handleAssetTrigger({ workflowId, assetId }: JobOf<JobName.WorkflowAssetTrigger>) {
return this.execute(workflowId, (type) => {
const assetService = BaseService.create(AssetService, this);
const writeAsset: ExecuteOptions<WorkflowType.AssetV1>['write'] = async (auth, changes) => {
const asset = changes.asset;
if (!asset) {
return;
}
await assetService.update(auth, assetId, {
isFavorite: asset.isFavorite,
visibility: asset.visibility,
dateTimeOriginal: asset.exifInfo?.dateTimeOriginal ?? undefined,
// TODO allow setting to null
longitude: asset.exifInfo?.longitude ?? undefined,
// TODO allow setting to null
latitude: asset.exifInfo?.latitude ?? undefined,
// TODO allow setting to null
description: asset.exifInfo?.description ?? undefined,
rating: asset.exifInfo?.rating,
// TODO add to update dto
// make: asset.exifInfo?.make,
// model: asset.exifInfo?.model,
// city: asset.exifInfo?.city,
// state: asset.exifInfo?.state,
// country: asset.exifInfo?.country,
// lensModel: asset.exifInfo?.lensModel,
// fNumber: asset.exifInfo?.fNumber,
// fps: asset.exifInfo?.fps,
// iso: asset.exifInfo?.iso,
});
};
switch (type) {
case WorkflowType.AssetV1: {
return {
@@ -335,28 +304,36 @@ export class WorkflowExecutionService extends BaseService {
authUserId: asset.ownerId,
};
},
write: writeAsset,
} satisfies ExecuteOptions<typeof type>;
}
write: async (auth, changes) => {
const asset = changes.asset;
if (!asset) {
return;
}
case WorkflowType.AssetAlbumV1: {
if (!albumId) {
this.logger.error(`Misconfigured workflow ${workflowId}: missing albumId for type ${type}`);
return;
}
await assetService.update(auth, assetId, {
isFavorite: asset.isFavorite,
visibility: asset.visibility,
dateTimeOriginal: asset.exifInfo?.dateTimeOriginal ?? undefined,
// TODO allow setting to null
longitude: asset.exifInfo?.longitude ?? undefined,
// TODO allow setting to null
latitude: asset.exifInfo?.latitude ?? undefined,
// TODO allow setting to null
description: asset.exifInfo?.description ?? undefined,
rating: asset.exifInfo?.rating,
return {
read: async () => {
const [asset, album] = await Promise.all([
this.workflowRepository.getForAssetV1(assetId),
this.workflowRepository.getAlbumForWorkflow(albumId),
]);
return {
data: { asset, album } as any,
authUserId: asset.ownerId,
};
// TODO add to update dto
// make: asset.exifInfo?.make,
// model: asset.exifInfo?.model,
// city: asset.exifInfo?.city,
// state: asset.exifInfo?.state,
// country: asset.exifInfo?.country,
// lensModel: asset.exifInfo?.lensModel,
// fNumber: asset.exifInfo?.fNumber,
// fps: asset.exifInfo?.fps,
// iso: asset.exifInfo?.iso,
});
},
write: writeAsset,
} satisfies ExecuteOptions<typeof type>;
}
}
+1 -1
View File
@@ -420,7 +420,7 @@ export type JobItem =
| { name: JobName.Ocr; data: IEntityJob }
// Workflow
| { name: JobName.WorkflowAssetTrigger; data: { workflowId: string; assetId: string; albumId?: string } }
| { name: JobName.WorkflowAssetTrigger; data: { workflowId: string; assetId: string } }
// Editor
| { name: JobName.AssetEditThumbnailGeneration; data: IEntityJob };
+2 -7
View File
@@ -30,17 +30,12 @@ const tests: Array<{ trigger: WorkflowTrigger; types: WorkflowType[]; expected:
},
{
trigger: WorkflowTrigger.AlbumAssetAdded,
types: [WorkflowType.AssetAlbumV1],
types: [WorkflowType.AssetV1],
expected: true,
},
{
trigger: WorkflowTrigger.AlbumAssetAdded,
types: [WorkflowType.AssetV1],
expected: false,
},
{
trigger: WorkflowTrigger.AssetCreate,
types: [WorkflowType.AssetAlbumV1],
types: [WorkflowType.AssetPersonV1],
expected: true,
},
];
+1 -2
View File
@@ -6,7 +6,7 @@ export const triggerMap: Record<WorkflowTrigger, WorkflowType[]> = {
[WorkflowTrigger.AssetCreate]: [WorkflowType.AssetV1],
[WorkflowTrigger.PersonRecognized]: [WorkflowType.AssetPersonV1],
[WorkflowTrigger.AssetMetadataExtraction]: [WorkflowType.AssetV1],
[WorkflowTrigger.AlbumAssetAdded]: [WorkflowType.AssetAlbumV1],
[WorkflowTrigger.AlbumAssetAdded]: [WorkflowType.AssetV1],
};
export const getWorkflowTriggers = () =>
@@ -16,7 +16,6 @@ export const getWorkflowTriggers = () =>
const inferredMap: Record<WorkflowType, WorkflowType[]> = {
[WorkflowType.AssetV1]: [],
[WorkflowType.AssetPersonV1]: [WorkflowType.AssetV1],
[WorkflowType.AssetAlbumV1]: [WorkflowType.AssetV1],
};
const withImpliedItems = (type: WorkflowType): WorkflowType[] => {