mirror of
https://github.com/immich-app/immich.git
synced 2026-03-23 01:47:55 -04:00
135 lines
4.7 KiB
TypeScript
135 lines
4.7 KiB
TypeScript
import { createZodDto } from 'nestjs-zod';
|
|
import * as z from 'zod';
|
|
|
|
export enum AssetEditAction {
|
|
Crop = 'crop',
|
|
Rotate = 'rotate',
|
|
Mirror = 'mirror',
|
|
}
|
|
|
|
export const AssetEditActionSchema = z
|
|
.enum(AssetEditAction)
|
|
.describe('Type of edit action to perform')
|
|
.meta({ id: 'AssetEditAction' });
|
|
|
|
export enum MirrorAxis {
|
|
Horizontal = 'horizontal',
|
|
Vertical = 'vertical',
|
|
}
|
|
|
|
const MirrorAxisSchema = z.enum(['horizontal', 'vertical']).describe('Axis to mirror along').meta({ id: 'MirrorAxis' });
|
|
|
|
const CropParametersSchema = z
|
|
.object({
|
|
x: z.number().min(0).describe('Top-Left X coordinate of crop'),
|
|
y: z.number().min(0).describe('Top-Left Y coordinate of crop'),
|
|
width: z.number().min(1).describe('Width of the crop'),
|
|
height: z.number().min(1).describe('Height of the crop'),
|
|
})
|
|
.meta({ id: 'CropParameters' });
|
|
|
|
const RotateParametersSchema = z
|
|
.object({
|
|
angle: z
|
|
.number()
|
|
.refine((v) => [0, 90, 180, 270].includes(v), {
|
|
error: 'Angle must be one of the following values: 0, 90, 180, 270',
|
|
})
|
|
.describe('Rotation angle in degrees'),
|
|
})
|
|
.meta({ id: 'RotateParameters' });
|
|
|
|
const MirrorParametersSchema = z
|
|
.object({
|
|
axis: MirrorAxisSchema,
|
|
})
|
|
.meta({ id: 'MirrorParameters' });
|
|
|
|
// TODO: ideally we would use the discriminated union directly in the future not only for type support but also for validation and openapi generation
|
|
const __AssetEditActionItemSchema = z.discriminatedUnion('action', [
|
|
z.object({ action: AssetEditActionSchema.extract(['Crop']), parameters: CropParametersSchema }),
|
|
z.object({ action: AssetEditActionSchema.extract(['Rotate']), parameters: RotateParametersSchema }),
|
|
z.object({ action: AssetEditActionSchema.extract(['Mirror']), parameters: MirrorParametersSchema }),
|
|
]);
|
|
|
|
const AssetEditParametersSchema = z
|
|
.union([CropParametersSchema, RotateParametersSchema, MirrorParametersSchema], {
|
|
error: getExpectedKeysByActionMessage,
|
|
})
|
|
.describe('List of edit actions to apply (crop, rotate, or mirror)');
|
|
|
|
const actionParameterMap = {
|
|
[AssetEditAction.Crop]: CropParametersSchema,
|
|
[AssetEditAction.Rotate]: RotateParametersSchema,
|
|
[AssetEditAction.Mirror]: MirrorParametersSchema,
|
|
} as const;
|
|
|
|
function getExpectedKeysByActionMessage(): string {
|
|
const expectedByAction = Object.entries(actionParameterMap)
|
|
.map(([action, schema]) => `${action}: [${Object.keys(schema.shape).join(', ')}]`)
|
|
.join('; ');
|
|
|
|
return `Invalid parameters for action, expected keys by action: ${expectedByAction}`;
|
|
}
|
|
|
|
function isParametersValidForAction(edit: z.infer<typeof AssetEditActionItemSchema>): boolean {
|
|
return actionParameterMap[edit.action].safeParse(edit.parameters).success;
|
|
}
|
|
|
|
const AssetEditActionItemSchema = z
|
|
.object({
|
|
action: AssetEditActionSchema,
|
|
parameters: AssetEditParametersSchema,
|
|
})
|
|
.superRefine((edit, ctx) => {
|
|
if (!isParametersValidForAction(edit)) {
|
|
ctx.addIssue({
|
|
code: 'custom',
|
|
path: ['parameters'],
|
|
message: `Invalid parameters for action '${edit.action}', expecting keys: ${Object.keys(actionParameterMap[edit.action].shape).join(', ')}`,
|
|
});
|
|
}
|
|
})
|
|
.meta({ id: 'AssetEditActionItemDto' });
|
|
|
|
export type AssetEditActionItem = z.infer<typeof __AssetEditActionItemSchema>;
|
|
export type AssetEditParameters = AssetEditActionItem['parameters'];
|
|
|
|
function uniqueEditActions(edits: z.infer<typeof AssetEditActionItemSchema>[]): boolean {
|
|
const keys = new Set<string>();
|
|
for (const edit of edits) {
|
|
const key = edit.action === 'mirror' ? `mirror-${JSON.stringify(edit.parameters)}` : edit.action;
|
|
if (keys.has(key)) {
|
|
return false;
|
|
}
|
|
keys.add(key);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
const AssetEditsCreateSchema = z
|
|
.object({
|
|
edits: z
|
|
.array(AssetEditActionItemSchema)
|
|
.min(1)
|
|
.describe('List of edit actions to apply (crop, rotate, or mirror)')
|
|
.refine(uniqueEditActions, { error: 'Duplicate edit actions are not allowed' }),
|
|
})
|
|
.meta({ id: 'AssetEditsCreateDto' });
|
|
|
|
const AssetEditActionItemResponseSchema = AssetEditActionItemSchema.extend({
|
|
id: z.uuidv4().describe('Asset edit ID'),
|
|
}).meta({ id: 'AssetEditActionItemResponseDto' });
|
|
|
|
const AssetEditsResponseSchema = z
|
|
.object({
|
|
assetId: z.uuidv4().describe('Asset ID these edits belong to'),
|
|
edits: z.array(AssetEditActionItemResponseSchema).describe('List of edit actions applied to the asset'),
|
|
})
|
|
.meta({ id: 'AssetEditsResponseDto' });
|
|
|
|
export class AssetEditActionItemResponseDto extends createZodDto(AssetEditActionItemResponseSchema) {}
|
|
export class AssetEditsCreateDto extends createZodDto(AssetEditsCreateSchema) {}
|
|
export class AssetEditsResponseDto extends createZodDto(AssetEditsResponseSchema) {}
|
|
export type CropParameters = z.infer<typeof CropParametersSchema>;
|