fix(pets): add extended fields to createPetSchema/updatePetSchema (GRO-2172)
createPetSchema in src/routes/pets.ts was missing temperamentScore,
temperamentFlags, medicalAlerts, and preferredCuts. Migrations 0034/0036
and seed data populate them, but POST/PATCH silently dropped the fields
because Zod validation rejected them. GET worked because seed bypassed
the schema. This is the regression originally reported as GRO-1472 and
blocks the GRO-1178 extended-profile feature rollout (api/#39).
Mirrors the field shape that already lives in apps/api/src/routes/pets.ts
(legacy duplicate) and the test expectations in
apps/api/src/__tests__/petsExtendedFields.test.ts:
- temperamentScore: int 1–5
- temperamentFlags: string[] (max 20, item max 100)
- medicalAlerts: { type, description, severity: low|medium|high }[] (max 50)
- preferredCuts: string[] (max 20, item max 200)
updatePetSchema inherits from createPetSchema.partial().omit({ clientId })
so no separate change is needed there.
POST/PATCH handlers now forward medicalAlerts through to the insert/update
with a localized cast — the @groombook/types MedicalAlert includes a
server-generated id that is not part of the API request shape, and the
jsonb column is schemaless at runtime.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+26
-2
@@ -57,6 +57,23 @@ const createPetSchema = z.object({
|
|||||||
customFields: z.record(z.string(), z.string()).optional(),
|
customFields: z.record(z.string(), z.string()).optional(),
|
||||||
petSizeCategory: z.enum(["small", "medium", "large", "extra_large"]).optional(),
|
petSizeCategory: z.enum(["small", "medium", "large", "extra_large"]).optional(),
|
||||||
coatType: z.enum(["short", "medium", "long", "double", "wire", "silky", "curly", "hairless"]).optional(),
|
coatType: z.enum(["short", "medium", "long", "double", "wire", "silky", "curly", "hairless"]).optional(),
|
||||||
|
// Extended pet profile fields (api/#39, GRO-1178).
|
||||||
|
// GRO-2172: these were missing from the schema, causing POST/PATCH to
|
||||||
|
// silently drop them even though migrations 0034/0036 and seed data
|
||||||
|
// populate them. GRO-1472 was the original UAT regression.
|
||||||
|
temperamentScore: z.number().int().min(1).max(5).optional(),
|
||||||
|
temperamentFlags: z.array(z.string().max(100)).max(20).optional(),
|
||||||
|
medicalAlerts: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
type: z.string().max(100),
|
||||||
|
description: z.string().max(1000),
|
||||||
|
severity: z.enum(["low", "medium", "high"]),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.max(50)
|
||||||
|
.optional(),
|
||||||
|
preferredCuts: z.array(z.string().max(200)).max(20).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const updatePetSchema = createPetSchema.partial().omit({ clientId: true });
|
const updatePetSchema = createPetSchema.partial().omit({ clientId: true });
|
||||||
@@ -333,7 +350,8 @@ petsRouter.get("/:id/profile-summary", async (c) => {
|
|||||||
|
|
||||||
petsRouter.post("/", zValidator("json", createPetSchema), async (c) => {
|
petsRouter.post("/", zValidator("json", createPetSchema), async (c) => {
|
||||||
const db = getDb();
|
const db = getDb();
|
||||||
const { weightKg, dateOfBirth, customFields, ...rest } = c.req.valid("json");
|
const { weightKg, dateOfBirth, customFields, medicalAlerts, ...rest } =
|
||||||
|
c.req.valid("json");
|
||||||
const [row] = await db
|
const [row] = await db
|
||||||
.insert(pets)
|
.insert(pets)
|
||||||
.values({
|
.values({
|
||||||
@@ -341,6 +359,10 @@ petsRouter.post("/", zValidator("json", createPetSchema), async (c) => {
|
|||||||
weightKg: weightKg?.toString(),
|
weightKg: weightKg?.toString(),
|
||||||
dateOfBirth: dateOfBirth ? new Date(dateOfBirth) : undefined,
|
dateOfBirth: dateOfBirth ? new Date(dateOfBirth) : undefined,
|
||||||
customFields: customFields ?? {},
|
customFields: customFields ?? {},
|
||||||
|
// GRO-2172: medicalAlerts shape from the API request is
|
||||||
|
// { type, description, severity } — the @groombook/types MedicalAlert
|
||||||
|
// has an optional server-generated `id`, so cast for the jsonb column.
|
||||||
|
medicalAlerts: medicalAlerts as never,
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
return c.json(row, 201);
|
return c.json(row, 201);
|
||||||
@@ -351,7 +373,8 @@ petsRouter.patch(
|
|||||||
zValidator("json", updatePetSchema),
|
zValidator("json", updatePetSchema),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const db = getDb();
|
const db = getDb();
|
||||||
const { weightKg, dateOfBirth, customFields, ...rest } = c.req.valid("json");
|
const { weightKg, dateOfBirth, customFields, medicalAlerts, ...rest } =
|
||||||
|
c.req.valid("json");
|
||||||
const [row] = await db
|
const [row] = await db
|
||||||
.update(pets)
|
.update(pets)
|
||||||
.set({
|
.set({
|
||||||
@@ -359,6 +382,7 @@ petsRouter.patch(
|
|||||||
weightKg: weightKg?.toString(),
|
weightKg: weightKg?.toString(),
|
||||||
dateOfBirth: dateOfBirth ? new Date(dateOfBirth) : undefined,
|
dateOfBirth: dateOfBirth ? new Date(dateOfBirth) : undefined,
|
||||||
...(customFields !== undefined ? { customFields } : {}),
|
...(customFields !== undefined ? { customFields } : {}),
|
||||||
|
medicalAlerts: medicalAlerts as never,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
})
|
})
|
||||||
.where(eq(pets.id, c.req.param("id")))
|
.where(eq(pets.id, c.req.param("id")))
|
||||||
|
|||||||
Reference in New Issue
Block a user