feat(measurements): add EXTERNAL measurement source

Rows pushed in through a `measurements:write` Bearer token are attributed
`MANUAL` today, so afterwards nobody can tell which readings came off the
scale and which were typed by hand. Give that write surface its own
`MeasurementSource` value, the way 0189 did for TELEGRAM and 0207 did for
MCP.

Enum extension only: purely additive, no row touched, `IF NOT EXISTS` makes
a rerun safe, and the value is not USED in the migration so Postgres'
same-transaction restriction does not bite. Declared last in the datamodel
so the Prisma member order and the physical order stay identical.

No backfill, and none is possible — `measurements` carries no actor or
token column, so a bridge-written MANUAL row is byte-identical to a typed
one. Existing rows stay MANUAL.
This commit is contained in:
Antiheld86 authored and Marc-André Bombeck committed 2026-09-25 06:55:17 +02:00
1 parent 9328a4298d
commit c2999c955d
2 files changed
+42

No files matched your search

@@ -0,0 +1,27 @@
-- Ingest-token write provenance.
--
-- The `measurements:write` Bearer scope mints a token a home-automation
-- bridge pastes into a rule; until now the rows it pushed were attributed
-- `MANUAL`, the same label a hand-typed reading gets, so afterwards nobody
-- could tell which readings came off the scale. Rows written under that
-- scope carry their own `MeasurementSource` value instead, so they are
-- distinguishable from MANUAL / TELEGRAM / MCP provenance and get an
-- isolated idempotency namespace `(userId, type, EXTERNAL, externalId)`.
--
-- Purely-additive enum extension; no row touched. `ADD VALUE IF NOT EXISTS`
-- makes a rerun safe. The new value is NOT used elsewhere in this migration,
-- so it is safe to extend the enum in the same step (Postgres only forbids
-- USING a freshly-added value in the same transaction). Mirrors the 0189
-- extension that added TELEGRAM and the 0207 one that added MCP. The enum is
-- `@@map`-ed to the snake_case type name `measurement_source`.
--
-- No backfill, deliberately, and none is possible: `measurements` carries no
-- actor or token column and the audit trail never records the credential, so
-- a bridge-written MANUAL row is byte-identical to a hand-typed one. That
-- indistinguishability is the bug being fixed; any heuristic would relabel
-- real hand entries as machine ones. Existing rows stay MANUAL — still
-- editable, and correctly not badged.
--
-- Reversibility: Postgres cannot remove an enum value, so `EXTERNAL` stays
-- (inert with no rows).
ALTER TYPE "measurement_source" ADD VALUE IF NOT EXISTS 'EXTERNAL';
+15
View File
@@ -1741,6 +1741,21 @@ enum MeasurementSource {
// mirrors this exact spelling so the `/api/sync/changes` delta feed decodes
// the rows. Feeds Workout rows only — Strava exposes no daily metrics.
STRAVA
// v1.38.x — measurements pushed in through a `measurements:write` Bearer
// token: a home-automation bridge, a smart scale's own uploader, any
// third-party sender the user pointed at their own record. Like TELEGRAM
// and MCP this is a user-driven write rather than a server-owned
// integration, but it does not enter through the cookie/Bearer client
// write path, so it carries its own source label — a hand-typed reading
// and a bridged one were otherwise indistinguishable after the fact. Its
// own value also gives those writes an isolated idempotency namespace
// `(userId, type, EXTERNAL, externalId)`. No client may NAME it: the batch
// + single-POST surfaces refuse any body that asserts a source under this
// scope, and the value is resolved server-side. Unlike the server-owned
// sources above the rows stay value-editable, since the hardware is the
// user's own. iOS DTO must mirror this exact spelling so the
// /api/sync/changes delta feed decodes the rows.
EXTERNAL
@@map("measurement_source")
}