fix(shared): align todo contracts with the wire format the client sends

The optional metadata fields (category, due_date, description,
assigned_user_id — plus priority on update) now accept explicit null: the
client clears fields by sending null, which the schemas rejected. checked
accepts the legacy 0/1 numeric form alongside boolean (the integration net
pins it). Drops the client-side casts that papered over the too-narrow
types.
This commit is contained in:
jubnl
2026-07-26 14:54:53 +02:00
parent ad7d349c85
commit 2fa075a076
4 changed files with 50 additions and 27 deletions
+31
View File
@@ -17,6 +17,18 @@ describe('todoCreateItemRequestSchema', () => {
// priority is numeric (matches the service), not a string
expect(todoCreateItemRequestSchema.safeParse({ name: 'X', priority: 'high' }).success).toBe(false);
});
it('accepts explicit null on the optional metadata fields (client clears with null)', () => {
expect(
todoCreateItemRequestSchema.safeParse({
name: 'X',
category: null,
due_date: null,
description: null,
assigned_user_id: null,
}).success,
).toBe(true);
});
});
describe('todoUpdateItemRequestSchema', () => {
@@ -24,6 +36,25 @@ describe('todoUpdateItemRequestSchema', () => {
expect(todoUpdateItemRequestSchema.safeParse({}).success).toBe(true);
expect(todoUpdateItemRequestSchema.safeParse({ checked: true }).success).toBe(true);
});
it('accepts checked as 0/1 (legacy numeric form) but rejects other numbers', () => {
expect(todoUpdateItemRequestSchema.safeParse({ checked: 1 }).success).toBe(true);
expect(todoUpdateItemRequestSchema.safeParse({ checked: 0 }).success).toBe(true);
expect(todoUpdateItemRequestSchema.safeParse({ checked: 2 }).success).toBe(false);
});
it('accepts explicit null on the nullable fields (bodyKeys clear protocol)', () => {
expect(
todoUpdateItemRequestSchema.safeParse({
category: null,
due_date: null,
description: null,
assigned_user_id: null,
priority: null,
}).success,
).toBe(true);
expect(todoUpdateItemRequestSchema.safeParse({ name: null }).success).toBe(false);
});
});
describe('todoReorderRequestSchema', () => {
+15 -10
View File
@@ -12,22 +12,27 @@ import { z } from 'zod';
export const todoCreateItemRequestSchema = z.object({
name: z.string().min(1),
category: z.string().optional(),
due_date: z.string().optional(),
description: z.string().optional(),
assigned_user_id: z.number().optional(),
// The client clears optional fields by sending explicit null (the service
// coerces falsy to its defaults), so every optional metadata field is nullable.
category: z.string().nullable().optional(),
due_date: z.string().nullable().optional(),
description: z.string().nullable().optional(),
assigned_user_id: z.number().nullable().optional(),
priority: z.number().optional(),
});
export type TodoCreateItemRequest = z.infer<typeof todoCreateItemRequestSchema>;
export const todoUpdateItemRequestSchema = z.object({
name: z.string().optional(),
checked: z.boolean().optional(),
category: z.string().optional(),
due_date: z.string().optional(),
description: z.string().optional(),
assigned_user_id: z.number().optional(),
priority: z.number().optional(),
// The legacy route accepted both boolean and 0/1 for checked — both stay valid.
checked: z.union([z.boolean(), z.number().int().min(0).max(1)]).optional(),
// Nullable fields follow the bodyKeys protocol: a key present with null
// clears the field, an omitted key leaves it unchanged.
category: z.string().nullable().optional(),
due_date: z.string().nullable().optional(),
description: z.string().nullable().optional(),
assigned_user_id: z.number().nullable().optional(),
priority: z.number().nullable().optional(),
});
export type TodoUpdateItemRequest = z.infer<typeof todoUpdateItemRequestSchema>;