Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 15 additions & 9 deletions docs/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20721,9 +20721,11 @@ components:
minLength: 1
maxLength: 80
severity:
type: integer
minimum: 1
maximum: 4
anyOf:
- type: integer
minimum: 1
maximum: 4
- type: "null"
required:
- key
note:
Expand Down Expand Up @@ -20859,9 +20861,11 @@ components:
minLength: 1
maxLength: 80
severity:
type: integer
minimum: 1
maximum: 4
anyOf:
- type: integer
minimum: 1
maximum: 4
- type: "null"
required:
- key
note:
Expand Down Expand Up @@ -20975,9 +20979,11 @@ components:
minLength: 1
maxLength: 80
severity:
type: integer
minimum: 1
maximum: 4
anyOf:
- type: integer
minimum: 1
maximum: 4
- type: "null"
required:
- key
note:
Expand Down
65 changes: 65 additions & 0 deletions src/lib/validations/__tests__/cycle-day-log-symptoms.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { describe, expect, it } from "vitest";

import {
cycleDayLogInputSchema,
cycleDayLogPatchSchema,
} from "@/lib/validations/cycle";

/**
* A symptom picked without an intensity is the ordinary way to log one.
*
* The sheet holds the picker as key to intensity and sends `severity: null`
* for every symptom the user did not rate, which is most of them. The schema
* took a number or nothing at all, so those entries came back 422 and the
* sheet showed "Could not save. Try again." with no way to tell why. The link
* row has always stored an unrated symptom as a plain presence; only the
* gate at the door disagreed, and nothing tested it.
*/
const BASE_INPUT = {
date: "2026-09-17",
loggedAt: new Date().toISOString(),
} as const;

describe("cycle day-log symptoms accept an unrated selection", () => {
for (const [name, schema] of [
["create", cycleDayLogInputSchema],
["patch", cycleDayLogPatchSchema],
] as const) {
it(`${name}: takes a symptom with no intensity at all`, () => {
const parsed = schema.safeParse({
...BASE_INPUT,
symptoms: [{ key: "cramps" }],
});
expect(parsed.success, JSON.stringify(parsed.error?.issues)).toBe(true);
});

it(`${name}: takes an explicit null intensity, which is what the sheet sends`, () => {
const parsed = schema.safeParse({
...BASE_INPUT,
symptoms: [
{ key: "cramps", severity: null },
{ key: "back_pain", severity: null },
],
});
expect(parsed.success, JSON.stringify(parsed.error?.issues)).toBe(true);
});

it(`${name}: still takes a rated symptom`, () => {
const parsed = schema.safeParse({
...BASE_INPUT,
symptoms: [{ key: "cramps", severity: 3 }],
});
expect(parsed.success).toBe(true);
});

it(`${name}: still refuses an intensity outside 1 to 4`, () => {
for (const severity of [0, 5, 2.5]) {
const parsed = schema.safeParse({
...BASE_INPUT,
symptoms: [{ key: "cramps", severity }],
});
expect(parsed.success, `severity ${severity}`).toBe(false);
}
});
}
});
14 changes: 12 additions & 2 deletions src/lib/validations/cycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,12 @@ export const cycleDayLogInputSchema = z.object({
.array(
z.object({
key: z.string().min(1).max(80),
severity: z.number().int().min(1).max(4).optional(),
// NULL is the ordinary case, not an edge: picking a symptom without
// tapping an intensity is how most entries are logged, and the link
// row stores that as a plain presence. Rejecting it refused every
// unrated symptom with a 422 the sheet could only show as
// "Could not save".
severity: z.number().int().min(1).max(4).nullable().optional(),
}),
)
.max(40)
Expand Down Expand Up @@ -181,7 +186,12 @@ export const cycleDayLogPatchSchema = z.object({
.array(
z.object({
key: z.string().min(1).max(80),
severity: z.number().int().min(1).max(4).optional(),
// NULL is the ordinary case, not an edge: picking a symptom without
// tapping an intensity is how most entries are logged, and the link
// row stores that as a plain presence. Rejecting it refused every
// unrated symptom with a 422 the sheet could only show as
// "Could not save".
severity: z.number().int().min(1).max(4).nullable().optional(),
}),
)
.max(40)
Expand Down
Loading