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): 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; export type AssetEditParameters = AssetEditActionItem['parameters']; function uniqueEditActions(edits: z.infer[]): boolean { const keys = new Set(); 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;