From 6798d5df328d71bf8d75fe63e267d87b9bd02fb9 Mon Sep 17 00:00:00 2001 From: Freddie Floydd Date: Fri, 17 Apr 2026 21:18:48 +0100 Subject: [PATCH] fix(server): require at least one field to be set when updating memory (#27842) * add zod util to require one field is set in some schemas. appy to update memory endpoint * add test --- server/src/controllers/memory.controller.spec.ts | 6 ++++++ server/src/dtos/memory.dto.ts | 14 ++++++-------- server/src/validation.ts | 16 ++++++++++++++++ 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/server/src/controllers/memory.controller.spec.ts b/server/src/controllers/memory.controller.spec.ts index 4ed32ee271..6a84edce45 100644 --- a/server/src/controllers/memory.controller.spec.ts +++ b/server/src/controllers/memory.controller.spec.ts @@ -96,6 +96,12 @@ describe(MemoryController.name, () => { expect(status).toBe(400); expect(body).toEqual(errorDto.badRequest(['Invalid input: expected object, received undefined'])); }); + + it('should require at least one field', async () => { + const { status, body } = await request(ctx.getHttpServer()).put(`/memories/${factory.uuid()}`).send({}); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['At least one field must be provided'])); + }); }); describe('DELETE /memories/:id', () => { diff --git a/server/src/dtos/memory.dto.ts b/server/src/dtos/memory.dto.ts index 334520dded..ce2e9fda6c 100644 --- a/server/src/dtos/memory.dto.ts +++ b/server/src/dtos/memory.dto.ts @@ -4,7 +4,7 @@ import { HistoryBuilder } from 'src/decorators'; import { AssetResponseSchema, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetOrderWithRandomSchema, MemoryType, MemoryTypeSchema } from 'src/enum'; -import { isoDatetimeToDate, stringToBool } from 'src/validation'; +import { isoDatetimeToDate, nonEmptyPartial, stringToBool } from 'src/validation'; import z from 'zod'; const MemorySearchSchema = z @@ -26,13 +26,11 @@ const OnThisDaySchema = z type MemoryData = z.infer; -const MemoryUpdateSchema = z - .object({ - isSaved: z.boolean().optional().describe('Is memory saved'), - seenAt: isoDatetimeToDate.optional().describe('Date when memory was seen'), - memoryAt: isoDatetimeToDate.optional().describe('Memory date'), - }) - .meta({ id: 'MemoryUpdateDto' }); +const MemoryUpdateSchema = nonEmptyPartial({ + isSaved: z.boolean().describe('Is memory saved'), + seenAt: isoDatetimeToDate.describe('Date when memory was seen'), + memoryAt: isoDatetimeToDate.describe('Memory date'), +}).meta({ id: 'MemoryUpdateDto' }); const MemoryCreateSchema = z .object({ diff --git a/server/src/validation.ts b/server/src/validation.ts index 54e3b1820e..59131b3abe 100644 --- a/server/src/validation.ts +++ b/server/src/validation.ts @@ -32,6 +32,22 @@ export function IsIPRange(options?: IsIPRangeOptions) { .refine((arr) => arr.every((item) => isIPOrRange(item, options)), 'Must be an ip address or ip address range'); } +/** + * Like z.object().partial(), but rejects objects where every field is undefined. + * Use for update/patch DTOs where at least one field must be provided. + * + * @example + * nonEmptyPartial({ name: z.string(), bio: z.string() }).meta({ id: 'UpdateDto' }); + */ +export function nonEmptyPartial(shape: T) { + return z + .object(shape) + .partial() + .refine((data) => Object.values(data as Record).some((value) => value !== undefined), { + message: 'At least one field must be provided', + }); +} + /** * Zod schema that validates sibling-exclusion for object schemas. * Validation passes when the target property is missing, or when none of the sibling properties are present.