Skip to content

fix(api): type day_summary, label and sleep type as nullable - #23

Merged
drakulavich merged 2 commits into
mainfrom
fix/nullable-api-fields
Jul 25, 2026
Merged

fix(api): type day_summary, label and sleep type as nullable#23
drakulavich merged 2 commits into
mainfrom
fix/nullable-api-fields

Conversation

@drakulavich

Copy link
Copy Markdown
Owner

Found by an api-drift-watcher run against the live Oura spec — the first run able to reach cloud.ouraring.com (the egress block recorded in docs/loops/api-drift-watcher-state.md is gone).

Finding

Three fields the CLI reads are nullable in the spec and were declared string:

Field Spec (1.37) Was Now
daily_stress.day_summary anyOf[PublicDailyStressSummary, null] string string | null
workout.label anyOf[string, null] string string | null
sleep.type anyOf[PublicSleepType, null] string string | null

None is in the schema's required list.

Impact

No runtime breakage today. Every backing column is nullable TEXT (daily_stress.day_summary, workouts.label, sleep_model.type), so a null inserts cleanly, and queries.ts already reads day_summary back as string | null. importDaily even writes w.label ?? '' — defensive code that the old type made look dead.

The problem is the contract: the declared types promised a value that the API does not guarantee, so a new reader could reasonably skip a null check and tsc would agree with them.

Change

Types only, no behaviour change. bunx tsc --noEmit passes with no downstream edits needed, which confirms nothing was relying on the non-null claim in a way that breaks.

The new test drives importDaily through a stub client returning nulls in all three fields and asserts what actually lands in SQLite.

One thing worth a decision (not changed here)

importDaily coerces a null workout label to '' rather than storing NULL, which collapses "no label" and "empty label" into the same value. The test pins the current behaviour rather than changing it — happy to switch it to NULL in a separate PR if you'd prefer the distinction preserved.

Not in scope

day_summary and type are enums upstream (restored|normal|stressful and deleted|sleep|long_sleep|late_nap|rest), still typed as open string. Narrowing them is a separate change.

Verification

bun test          129 pass, 0 fail   (was 128)
bunx tsc --noEmit clean

The Oura OpenAPI spec (verified against 1.37) declares three fields the CLI
reads as nullable, none of them required:

  daily_stress.day_summary  anyOf[PublicDailyStressSummary, null]
  workout.label             anyOf[string, null]
  sleep.type                anyOf[PublicSleepType, null]

All three were declared `string`. Nothing breaks at runtime today — every
backing column is nullable TEXT, and importDaily already wrote `w.label ?? ''`
against exactly this case — but the declared contract told callers a value is
always present, so any new reader could reasonably assume one.

Types only; no behaviour change. The added test drives importDaily with nulls
in all three fields and asserts what actually lands in SQLite, including the
existing '' coercion for workout labels.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DRnTxyiiF3XHiyFqZJ3soz
@greptile-apps

greptile-apps Bot commented Jul 25, 2026

Copy link
Copy Markdown

Greptile Summary

The PR aligns three API response fields with the upstream nullable and optional contract.

  • Makes OuraStressDay.day_summary, OuraWorkout.label, and OuraSleepModel.type optional and nullable.
  • Explicitly stores omitted stress summaries and sleep types as SQL NULL.
  • Adds import coverage for explicit nulls and omitted properties.
  • Documents the corrected API contract in the changelog.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the previously reported required-property mismatch is fixed by marking all three fields optional as well as nullable.

Important Files Changed

Filename Overview
src/api/types.ts The three fields are now both optional and nullable, fully addressing the previously reported mismatch with responses that omit them.
src/db/import.ts Optional stress summaries and sleep types are normalized to SQL NULL, while existing workout-label normalization remains unchanged.
src/db/import.test.ts Tests cover explicit null values and omitted properties through the complete import path.
CHANGELOG.md The changelog accurately describes the corrected types and import normalization.

Reviews (2): Last reviewed commit: "fix(api): mark the three fields optional..." | Re-trigger Greptile

Comment thread src/api/types.ts Outdated
Greptile review: the spec leaves day_summary, label and type out of each
schema's `required` list, so the property can be absent, not only null.
`string | null` on a required property does not express that.

Making them optional surfaced a latent gap tsc had no way to see before:
`undefined` is not a member of bun:sqlite's SQLQueryBindings, so the two
insert sites were passing a value the binding type rejects. The runtime
coerces undefined to NULL, so nothing was breaking — but it was leaning on
an undocumented coercion. Both sites now map to null explicitly.

Adds a test driving importDaily with the properties omitted entirely,
alongside the existing explicit-null case.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DRnTxyiiF3XHiyFqZJ3soz
drakulavich added a commit that referenced this pull request Jul 25, 2026
Greptile review: the file described the three nullability findings as
"Fixed in #23" while this branch still carries the old declarations, so a
reader landing on this commit would see the record and the code disagree.
Reworded to "filed", with the open state called out explicitly.

Also records that all three are absent from their schema's `required` list
— they can be omitted, not merely null — and drops the duplicate mention
of that in the paragraph above the table.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DRnTxyiiF3XHiyFqZJ3soz
@drakulavich
drakulavich merged commit c11cb3b into main Jul 25, 2026
3 checks passed
drakulavich added a commit that referenced this pull request Jul 26, 2026
An independent `omc ask grok` pass checked every claim against the
source. All verified before changing anything.

Corrected:
- "each command wires handleError in its own catch" is false: only the
  data-path commands do. healthcheck deliberately swallows into
  { ok: false, error } as a probe, and login/describe/manifest have no
  catch at all. Stated the real shape, including that index.ts has no
  global handler.
- docs/ARCHITECTURE.md is stale on layering too, not just citty — its
  diagram makes api and db peers when db imports api, and it omits the
  root-level format modules. Withdrew the "accurate on layering"
  endorsement.
- npm audit is not a gate: the step swallows failures into a
  ::warning::. Also noted release.yml runs no tsc.
- Dropped the invented "no default exports outside src/index.ts"
  exception; there are no default exports in src/ at all.

Added, all non-obvious enough that an agent would get them wrong:
- Registering a command touches four files. Missing the SUBCOMMANDS set
  in src/lib/argv-normalize.ts silently breaks `--format json <cmd>`,
  because citty does not hoist root flags and that normalizer is what
  moves them. The describe/manifest agent surface is hard-coded too.
- Schema migrations are append-only (`version > current`), so editing a
  shipped migration is a no-op on existing databases.
- Oura API fields should be treated as nullable — #23 retyped three of
  them after upstream drift.
- Both output modes apply to new data commands; the JSON-only and
  interactive commands are deliberate and must not be "fixed".
- The OURA_TOKEN / OURA_TOKEN_PATH / OURA_DB_PATH / OURA_TZ / NO_COLOR
  environment surface, which the file omitted entirely.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DRnTxyiiF3XHiyFqZJ3soz
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant