mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
docs(api): declare the two sync domains that were already on the wire
`GET /api/sync/changes` returns five domains — measurements, mood, intakes, cycleDays, cycles — and has since the cycle module landed. The registered schema declared three, closed the object and required only those three, so `cycleDays` and `cycles` did not exist in the contract: full day-log and cycle payloads, both upserts and tombstones, computed and serialised on every single pull, invisible to any client generated from the spec. The body was also invalid against its own schema, since the object was closed against the two keys it always sends. The day-log and cycle DTOs were already published for the cycle routes, so the two upsert arrays reuse them and only the tombstone shapes are new. The module's own description and the operation summary were stale in the same direction and now name all five. This is the two-ended change the contributor notes describe: the writer shipped, the contract half did not, and no gate noticed because every gate proves the other end. No wire change.
This commit is contained in:
1 parent
fd8fc0ec0e
commit
0cd996e7de
3 files changed
+151
-12
No files matched your search
+99
-7
@@ -3331,10 +3331,12 @@ paths:
|
||||
get:
|
||||
tags:
|
||||
- Sync
|
||||
summary: Measurements delta feed
|
||||
description: "Incremental catch-up after the first-pair backfill (never a replacement for it). Pages over an opaque
|
||||
keyset cursor; each page carries `tombstones` (soft-deleted rows, keyed on `externalId`) and `upserts` (live
|
||||
rows). Apply tombstones before upserts within a page. `cursorExpired: true` forces a clean re-init."
|
||||
summary: Multi-domain delta feed
|
||||
description: "Incremental catch-up after the first-pair backfill (never a replacement for it). Pages over one opaque
|
||||
keyset cursor spanning five domains — measurements, mood, medication intakes, cycle day logs and cycles. Each
|
||||
domain carries `tombstones` (soft-deleted rows) and `upserts` (live rows); apply tombstones before upserts
|
||||
within a domain. Tombstone identity is `externalId` for measurements and the server `id` everywhere else.
|
||||
`cursorExpired: true` forces a clean re-init and the page is empty in every domain."
|
||||
parameters:
|
||||
- in: query
|
||||
name: cursor
|
||||
@@ -37327,10 +37329,45 @@ components:
|
||||
- upserts
|
||||
- tombstones
|
||||
additionalProperties: false
|
||||
cycleDays:
|
||||
type: object
|
||||
properties:
|
||||
upserts:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/CycleDayLogDTO"
|
||||
tombstones:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/SyncCycleDayTombstone"
|
||||
required:
|
||||
- upserts
|
||||
- tombstones
|
||||
additionalProperties: false
|
||||
description: Cycle day logs. Present on every page, empty arrays when the account has the cycle module off.
|
||||
cycles:
|
||||
type: object
|
||||
properties:
|
||||
upserts:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/MenstrualCycleDTO"
|
||||
tombstones:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/SyncCycleTombstone"
|
||||
required:
|
||||
- upserts
|
||||
- tombstones
|
||||
additionalProperties: false
|
||||
description: Menstrual cycles, observed and forward-predicted. Present on every page, empty arrays when the account has
|
||||
the cycle module off.
|
||||
required:
|
||||
- measurements
|
||||
- mood
|
||||
- intakes
|
||||
- cycleDays
|
||||
- cycles
|
||||
additionalProperties: false
|
||||
required:
|
||||
- serverNow
|
||||
@@ -37339,9 +37376,10 @@ components:
|
||||
- cursorExpired
|
||||
- changes
|
||||
additionalProperties: false
|
||||
description: "Multi-domain delta page (v1.7.0): measurements + mood + intakes. One opaque multi-domain keyset cursor;
|
||||
tombstones apply before upserts within each domain. Tombstone identity: measurements key on externalId, mood +
|
||||
intakes on server id. The iOS consumer is measurements-only this cycle; mood + intakes are forward-prep."
|
||||
description: "Multi-domain delta page: measurements, mood, intakes, cycle day logs and cycles. One opaque multi-domain
|
||||
keyset cursor; tombstones apply before upserts within each domain. Tombstone identity: measurements key on
|
||||
externalId, every other domain on server id. All five keys are present on every response, including the
|
||||
`cursorExpired` short-circuit — a domain with nothing to report sends empty arrays rather than being omitted."
|
||||
SyncMeasurementUpsert:
|
||||
type: object
|
||||
properties:
|
||||
@@ -37683,6 +37721,60 @@ components:
|
||||
- updatedAt
|
||||
additionalProperties: false
|
||||
description: A soft-deleted medication intake, keyed on server `id`. Apply before upserts within the domain page.
|
||||
SyncCycleDayTombstone:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
externalId:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: "null"
|
||||
description: The client's own key for the day log, when it set one.
|
||||
syncVersion:
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 9007199254740991
|
||||
deletedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
|
||||
required:
|
||||
- id
|
||||
- externalId
|
||||
- syncVersion
|
||||
- deletedAt
|
||||
- updatedAt
|
||||
additionalProperties: false
|
||||
description: A soft-deleted cycle day log, keyed on server `id`. Apply before upserts within the domain page.
|
||||
SyncCycleTombstone:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
syncVersion:
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 9007199254740991
|
||||
deletedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
|
||||
required:
|
||||
- id
|
||||
- syncVersion
|
||||
- deletedAt
|
||||
- updatedAt
|
||||
additionalProperties: false
|
||||
description: A soft-deleted menstrual cycle, keyed on server `id`. Apply before upserts within the domain page.
|
||||
GetMeasurementsSeriesBatchResponse:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -112,7 +112,7 @@ const cycleSymptomDto = z.object({
|
||||
severity: z.number().int().min(1).max(4).nullable(),
|
||||
});
|
||||
|
||||
const cycleDayLogDto = z
|
||||
export const cycleDayLogDto = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
date: z.string(),
|
||||
@@ -145,7 +145,7 @@ const cycleDayLogDto = z
|
||||
"The canonical day-log row iOS mirrors. `note` is decrypted on read. Soft-deleted rows ride `/api/sync/changes` as tombstones.",
|
||||
});
|
||||
|
||||
const menstrualCycleDto = z
|
||||
export const menstrualCycleDto = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
startDate: z.string(),
|
||||
|
||||
@@ -9,6 +9,7 @@ import { z } from "zod/v4";
|
||||
import type { ZodOpenApiObject } from "zod-openapi";
|
||||
import { measurementResource } from "./measurements";
|
||||
import { moodContextWire } from "./mood";
|
||||
import { cycleDayLogDto, menstrualCycleDto } from "./cycle";
|
||||
import { dataEnvelope, stdResponses } from "./shared";
|
||||
|
||||
// ── Sync (v1.7.0 offline / server-optional) ─────────────────────────
|
||||
@@ -185,6 +186,36 @@ const syncIntakeTombstone = z
|
||||
"A soft-deleted medication intake, keyed on server `id`. Apply before upserts within the domain page.",
|
||||
});
|
||||
|
||||
const syncCycleDayTombstone = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
externalId: z
|
||||
.string()
|
||||
.nullable()
|
||||
.describe("The client's own key for the day log, when it set one."),
|
||||
syncVersion: z.number().int().nonnegative(),
|
||||
deletedAt: z.iso.datetime({ offset: true }),
|
||||
updatedAt: z.iso.datetime({ offset: true }),
|
||||
})
|
||||
.meta({
|
||||
id: "SyncCycleDayTombstone",
|
||||
description:
|
||||
"A soft-deleted cycle day log, keyed on server `id`. Apply before upserts within the domain page.",
|
||||
});
|
||||
|
||||
const syncCycleTombstone = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
syncVersion: z.number().int().nonnegative(),
|
||||
deletedAt: z.iso.datetime({ offset: true }),
|
||||
updatedAt: z.iso.datetime({ offset: true }),
|
||||
})
|
||||
.meta({
|
||||
id: "SyncCycleTombstone",
|
||||
description:
|
||||
"A soft-deleted menstrual cycle, keyed on server `id`. Apply before upserts within the domain page.",
|
||||
});
|
||||
|
||||
const syncChangesQuery = z
|
||||
.object({
|
||||
cursor: z
|
||||
@@ -233,12 +264,28 @@ const syncChangesResponse = z
|
||||
upserts: z.array(syncIntakeUpsert),
|
||||
tombstones: z.array(syncIntakeTombstone),
|
||||
}),
|
||||
cycleDays: z
|
||||
.object({
|
||||
upserts: z.array(cycleDayLogDto),
|
||||
tombstones: z.array(syncCycleDayTombstone),
|
||||
})
|
||||
.describe(
|
||||
"Cycle day logs. Present on every page, empty arrays when the account has the cycle module off.",
|
||||
),
|
||||
cycles: z
|
||||
.object({
|
||||
upserts: z.array(menstrualCycleDto),
|
||||
tombstones: z.array(syncCycleTombstone),
|
||||
})
|
||||
.describe(
|
||||
"Menstrual cycles, observed and forward-predicted. Present on every page, empty arrays when the account has the cycle module off.",
|
||||
),
|
||||
}),
|
||||
})
|
||||
.meta({
|
||||
id: "SyncChangesResponse",
|
||||
description:
|
||||
"Multi-domain delta page (v1.7.0): measurements + mood + intakes. One opaque multi-domain keyset cursor; tombstones apply before upserts within each domain. Tombstone identity: measurements key on externalId, mood + intakes on server id. The iOS consumer is measurements-only this cycle; mood + intakes are forward-prep.",
|
||||
"Multi-domain delta page: measurements, mood, intakes, cycle day logs and cycles. One opaque multi-domain keyset cursor; tombstones apply before upserts within each domain. Tombstone identity: measurements key on externalId, every other domain on server id. All five keys are present on every response, including the `cursorExpired` short-circuit — a domain with nothing to report sends empty arrays rather than being omitted.",
|
||||
});
|
||||
|
||||
export const syncPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
@@ -264,9 +311,9 @@ export const syncPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
"/api/sync/changes": {
|
||||
get: {
|
||||
tags: ["Sync"],
|
||||
summary: "Measurements delta feed",
|
||||
summary: "Multi-domain delta feed",
|
||||
description:
|
||||
"Incremental catch-up after the first-pair backfill (never a replacement for it). Pages over an opaque keyset cursor; each page carries `tombstones` (soft-deleted rows, keyed on `externalId`) and `upserts` (live rows). Apply tombstones before upserts within a page. `cursorExpired: true` forces a clean re-init.",
|
||||
"Incremental catch-up after the first-pair backfill (never a replacement for it). Pages over one opaque keyset cursor spanning five domains — measurements, mood, medication intakes, cycle day logs and cycles. Each domain carries `tombstones` (soft-deleted rows) and `upserts` (live rows); apply tombstones before upserts within a domain. Tombstone identity is `externalId` for measurements and the server `id` everywhere else. `cursorExpired: true` forces a clean re-init and the page is empty in every domain.",
|
||||
requestParams: {
|
||||
query: syncChangesQuery,
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user