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
-3
View File
@@ -25,13 +25,11 @@ class WorkflowType {
static const assetV1 = WorkflowType._(r'AssetV1');
static const assetPersonV1 = WorkflowType._(r'AssetPersonV1');
static const assetAlbumV1 = WorkflowType._(r'AssetAlbumV1');
/// List of all possible values in this [enum][WorkflowType].
static const values = <WorkflowType>[
assetV1,
assetPersonV1,
assetAlbumV1,
];
static WorkflowType? fromJson(dynamic value) => WorkflowTypeTypeTransformer().decode(value);
@@ -72,7 +70,6 @@ class WorkflowTypeTypeTransformer {
switch (data) {
case r'AssetV1': return WorkflowType.assetV1;
case r'AssetPersonV1': return WorkflowType.assetPersonV1;
case r'AssetAlbumV1': return WorkflowType.assetAlbumV1;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
+1 -2
View File
@@ -26840,8 +26840,7 @@
"description": "Workflow type",
"enum": [
"AssetV1",
"AssetPersonV1",
"AssetAlbumV1"
"AssetPersonV1"
],
"type": "string"
},
-10
View File
@@ -11,7 +11,6 @@ type DeepPartial<T> = T extends Date
export type WorkflowEventMap = {
[WorkflowType.AssetV1]: AssetV1;
[WorkflowType.AssetPersonV1]: AssetPersonV1;
[WorkflowType.AssetAlbumV1]: AssetAlbumV1;
};
export type WorkflowEventData<T extends WorkflowType> = WorkflowEventMap[T];
@@ -130,12 +129,3 @@ export type AssetPersonV1 = AssetV1 & {
name: string;
};
};
export type AssetAlbumV1 = AssetV1 & {
album: {
id: string;
ownerId: string;
albumName: string;
description: string;
};
};
+1 -2
View File
@@ -7179,8 +7179,7 @@ export enum PartnerDirection {
}
export enum WorkflowType {
AssetV1 = "AssetV1",
AssetPersonV1 = "AssetPersonV1",
AssetAlbumV1 = "AssetAlbumV1"
AssetPersonV1 = "AssetPersonV1"
}
export enum WorkflowTrigger {
AssetCreate = "AssetCreate",
-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[] => {