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
28 changes: 28 additions & 0 deletions .changeset/tidy-jars-shake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
'@objectstack/metadata-protocol': patch
'@objectstack/rest': patch
---

Correct the out-of-package comments that still described `SqlDriver#formatOutput`'s
two timestamp passes as gated on `if (this.isSqlite)`.

Since ADR-0053 D-F1 (#13973) both passes — the `AUDIT_TIMESTAMP_COLUMNS` pass and the
`normalizeSqliteDatetimeOutput` pass over `datetimeFields` — run on every dialect, so a
declared `Field.datetime` and the builtin audit columns are presented as canonical
ISO-8601-`Z` text on Postgres and MySQL as well as SQLite. The `rest-server.ts` comment
went further than staleness: it warned future authors that "a declared `Field.datetime`
is therefore NOT protected on Postgres/MySQL", inviting exactly the tolerant consumer-side
coercion ADR-0053 forbids.

Comments only — no runtime behaviour, no exported symbol and no public type changes; the
published `.d.ts` of both packages is byte-identical. These two packages are named because
their bundled `dist/index.js` / `dist/index.cjs` carry the amended comment text verbatim,
so the published output does change. `@objectstack/metadata` carries the same correction
in `database-loader.ts` but is deliberately NOT named: its edits are all JSDoc blocks,
which its bundle strips, so its published output is unchanged.

Two carve-outs are preserved rather than flattened: `withPostgresCalendarDayAsText` is
untouched by that ruling (D-F2 — the client library still materialises `timestamptz` /
`DATETIME(3)` as a `Date`; the driver now folds it at its own read boundary), and the
Invalid `Date` residue still stands (D-F3 — the one `Date` shape with no canonical text
leaves the read door unchanged).
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,20 @@
* straight past it into the declared field.
*
* `recorded_at` is a declared `Field.datetime` on `sys_metadata_history`, and
* that does NOT protect it: `SqlDriver#formatOutput` folds declared datetime
* columns (`normalizeSqliteDatetimeOutput`) only inside its
* `if (this.isSqlite)` arm, and `withPostgresCalendarDayAsText` leaves
* `timestamptz` / `timestamp` deliberately untouched. Pinned live in
* `packages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts`.
* when this landed that did NOT protect it: `SqlDriver#formatOutput` folded
* declared datetime columns (`normalizeSqliteDatetimeOutput`) only inside its
* `if (this.isSqlite)` arm. #13973 ([ADR-0053 D-F1]) has since lifted that fold
* out of the gate — it runs on every dialect — and the pin that recorded the
* asymmetry now records the canonical-text contract
* (`packages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts`
* §B, inverted on purpose).
*
* ⚠️ `withPostgresCalendarDayAsText` is untouched by that ruling and still
* leaves `timestamptz` / `timestamp` deliberately alone ([ADR-0053 D-F2]) — the
* client still hands back a `Date`; the driver folds it at its own read
* boundary now. And the `Date` this file plants stays reachable: an INVALID
* `Date` leaves `driver-sql` unchanged ([ADR-0053 D-F3]) and non-SQL drivers
* materialise their own, so what is pinned below is a live adapter arm.
*
* ## Why it matters downstream, not just as a type
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,29 @@
* declaration is an inline return type), and `rows` is cast `as any[]` one line
* above the map, so tsc sees a `string` assignment that never happened.
*
* ## Why the value is a `Date` on the live dialects
* ## Why a `Date` reaches this projection at all
*
* `updated_at` / `created_at` are the BUILTIN audit columns on `sys_metadata`
* (`Field.datetime`, `packages/metadata-core/src/objects/sys-metadata.object.ts`).
* `SqlDriver#formatOutput` repairs the audit columns
* (`repairNaiveUtcAuditTimestamp`) and folds the declared datetime columns
* When this landed, `SqlDriver#formatOutput` repaired the audit columns
* (`repairNaiveUtcAuditTimestamp`) and folded the declared datetime columns
* (`normalizeSqliteDatetimeOutput`) ONLY inside its `if (this.isSqlite)` arm,
* and `withPostgresCalendarDayAsText` leaves `timestamptz` / `timestamp`
* deliberately untouched because those are instants. That dialect fact is
* pinned live in
* `packages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts`;
* this file does not re-derive it and takes on no driver dependency
* (`@objectstack/metadata-protocol` has none, and the layering runs the other
* way) — the `Date` is hand-made here for exactly that reason.
* so both live dialects handed the column through as a JS `Date`. #13973
* ([ADR-0053 D-F1]) has since lifted both passes out of that gate — they run on
* every dialect — and the pin that recorded the asymmetry now records the
* canonical-text contract
* (`packages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts`
* §B, inverted on purpose).
*
* ⚠️ `withPostgresCalendarDayAsText` is untouched by that ruling and still
* leaves `timestamptz` / `timestamp` deliberately alone ([ADR-0053 D-F2]) —
* those are instants and the CLIENT still materialises them as a `Date`; what
* changed is that the driver folds it at its own read boundary. The `Date` this
* file plants therefore still reaches the projection in practice: an INVALID
* `Date` leaves `driver-sql` unchanged ([ADR-0053 D-F3]) and non-SQL drivers
* materialise their own. This file does not re-derive any of it and takes on no
* driver dependency (`@objectstack/metadata-protocol` has none, and the
* layering runs the other way) — the `Date` is hand-made here for that reason.
*
* ## What is asserted, and why it is not a hand-copied shape
*
Expand Down
64 changes: 42 additions & 22 deletions packages/metadata-protocol/src/sys-metadata-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,24 @@ import { isWritablePackage } from './package-writability.js';
*
* [#13997] `sys_metadata`'s `created_at` / `updated_at` are BUILTIN audit
* columns; `sys_metadata_history`'s `recorded_at` is a declared
* `Field.datetime`. On the live dialects BOTH arrive out of the record read
* door as a JS `Date`: `SqlDriver#formatOutput` repairs the audit columns
* (`repairNaiveUtcAuditTimestamp`) and folds the declared datetime columns
* (`normalizeSqliteDatetimeOutput`) ONLY inside its `if (this.isSqlite)` arm,
* and `withPostgresCalendarDayAsText` leaves `timestamptz` / `timestamp`
* deliberately untouched because "those are instants, a `Date` is the right
* materialisation for them, and `Field.datetime` depends on it". Pinned in
* `packages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts`.
* `Field.datetime`. On the live dialects BOTH used to arrive out of the record
* read door as a JS `Date`: `SqlDriver#formatOutput` repaired the audit columns
* (`repairNaiveUtcAuditTimestamp`) and folded the declared datetime columns
* (`normalizeSqliteDatetimeOutput`) ONLY inside its `if (this.isSqlite)` arm.
* #13973 ([ADR-0053 D-F1]) lifted both passes out of that gate — they run on
* EVERY dialect now, so the read door presents the canonical ISO-Z text.
* Pinned in
* `packages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts`
* §B, which was inverted on purpose to record the new contract.
*
* ⚠️ `withPostgresCalendarDayAsText` still leaves `timestamptz` / `timestamp`
* deliberately untouched ([ADR-0053 D-F2]) — those are instants and a `Date`
* remains the right materialisation for them at the CLIENT layer. What changed
* is that the driver no longer lets that `Date` out of its read door. (⛔ The
* clause this comment used to quote alongside it — that `Field.datetime`
* "depends on" the `Date` materialisation — was checked against the tree by
* #13973 and did not hold; it is gone from the driver and must not be quoted
* back.)
*
* `MetadataItem.authoredAt` is declared `z.string()` ('ISO-8601 timestamp',
* `packages/metadata-core/src/types.ts`) and `MetadataItem` is a `z.infer`, so
Expand Down Expand Up @@ -163,10 +173,14 @@ function canonicalIsoInstant(value: unknown): string | undefined {
* [#14037] `rowToEvent` reaches `ts` through `(row.recorded_at as string) ??
* …`, and `row` is `any`, so tsc sees a `string` assignment that never
* happened. `recorded_at` is a declared `Field.datetime` on
* `sys_metadata_history`, which the dialect asymmetry above does NOT protect:
* the `datetimeFields` fold sits inside `formatOutput`'s `if (this.isSqlite)`
* arm, so Postgres and MySQL hand the column out as a JS `Date`.
* `MetadataEventSchema.ts` is `z.string()`
* `sys_metadata_history`, and the dialect asymmetry described above did not
* protect it: the `datetimeFields` fold sat inside `formatOutput`'s
* `if (this.isSqlite)` arm, so Postgres and MySQL handed the column out as a JS
* `Date`. #13973 ([ADR-0053 D-F1]) has since closed that asymmetry — the fold
* runs on every dialect — but the cast is still an assertion rather than a
* measurement, and the `Date` domain is not empty: an INVALID `Date` still
* leaves `driver-sql` unchanged ([ADR-0053 D-F3]) and non-SQL drivers
* materialise their own. `MetadataEventSchema.ts` is `z.string()`
* (`packages/metadata-core/src/types.ts`), and the value's one in-repo reader
* — `MetadataManager.applyRepoEvent`, which forwards it to
* `MetadataWatchEvent.timestamp` — is declared `z.string().datetime()`.
Expand Down Expand Up @@ -1177,16 +1191,22 @@ export class SysMetadataRepository implements MetadataRepository {
organizationId: row.organization_id ?? null,
packageId: row.package_id ?? null,
// [#14938] `updated_at` / `created_at` are the BUILTIN audit columns,
// so on Postgres and MySQL they arrive here as a JS `Date`: the audit
// repair and the declared-datetime fold both sit inside
// `SqlDriver#formatOutput`'s `if (this.isSqlite)` arm, and
// `withPostgresCalendarDayAsText` leaves `timestamptz` / `timestamp`
// alone because those are instants. `rows` is cast `as any[]` above,
// so tsc never saw the `Date` land in a field this signature declares
// `string | null`. Canonicalised at the producer — the same adapter
// boundary `rowToItem` uses, never a tolerant `??` in the console or a
// reshape at the driver's read door (#13973's two standing
// prohibitions).
// and on Postgres and MySQL they used to arrive here as a JS `Date`:
// the audit repair and the declared-datetime fold both sat inside
// `SqlDriver#formatOutput`'s `if (this.isSqlite)` arm. #13973
// ([ADR-0053 D-F1]) lifted both out of that gate, so the read door now
// presents canonical ISO-Z text on every dialect;
// `withPostgresCalendarDayAsText` is untouched by that ruling and still
// leaves `timestamptz` / `timestamp` alone at the CLIENT parser
// ([ADR-0053 D-F2]). `rows` is cast `as any[]` above, so tsc never saw
// the `Date` land in a field this signature declares `string | null`,
// and the shape is still reachable: an INVALID `Date` leaves the driver
// unchanged ([ADR-0053 D-F3]) and non-SQL drivers materialise their own.
// Canonicalised at the producer — the same adapter boundary `rowToItem`
// uses, and ⛔ never a tolerant `??` in the console, which is the #13973
// prohibition that still stands. (Its second one — ⛔ no unilateral
// reshape at the driver's read door — was DISCHARGED by the B1 ruling,
// which made that reshape the central fix rather than a local one.)
//
// The terminal is chosen PER CALL SITE (#14078) and this one is
// `null`, not `rowToItem`'s `?? new Date(...).toISOString()`: this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,24 @@
* casts — an assertion about a driver row, never a measurement of one — which
* is why tsc reported nothing.
*
* On Postgres and MySQL the assertion is false for BOTH column classes:
* `SqlDriver#formatOutput` repairs the builtin audit columns
* (`repairNaiveUtcAuditTimestamp`) and folds declared `Field.datetime` columns
* When this landed, the assertion was false on Postgres and MySQL for BOTH
* column classes: `SqlDriver#formatOutput` repaired the builtin audit columns
* (`repairNaiveUtcAuditTimestamp`) and folded declared `Field.datetime` columns
* (`normalizeSqliteDatetimeOutput`) only inside its `if (this.isSqlite)` arm,
* and `withPostgresCalendarDayAsText` leaves `timestamptz` / `timestamp`
* deliberately untouched. That dialect fact is pinned live in
* `packages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts`.
* `recorded_at` being a declared `Field.datetime` on `sys_metadata_history`
* does NOT protect it — the fold is inside the SQLite arm too.
* so `recorded_at` being a declared `Field.datetime` on `sys_metadata_history`
* did not protect it either. #13973 ([ADR-0053 D-F1]) has since lifted both
* passes out of that gate — they run on EVERY dialect — and the pin that
* recorded the asymmetry now records the canonical-text contract
* (`packages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts`
* §B, inverted on purpose).
*
* ⚠️ `withPostgresCalendarDayAsText` is untouched by that ruling and still
* leaves `timestamptz` / `timestamp` deliberately alone ([ADR-0053 D-F2]) — the
* client still hands back a `Date`; the driver now folds it at its own read
* boundary. And the `Date` domain these cases pin did not close: an INVALID
* `Date` still leaves `driver-sql` unchanged ([ADR-0053 D-F3]), and non-SQL
* drivers materialise their own. So these cases pin a live adapter arm, not a
* historical one — what they own is the adapter's behaviour per input shape.
*
* All three declarations are `z.string().datetime()`
* (`packages/spec/src/system/metadata-persistence.zod.ts`), a refinement a
Expand Down
48 changes: 34 additions & 14 deletions packages/metadata/src/loaders/database-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,23 @@ import { migrateProjectIdToEnvironmentId } from '../migrations/migrate-project-i
* `MetadataStats.mtime` is declared as.
*
* [#13997] `sys_metadata`'s `created_at` / `updated_at` are BUILTIN audit
* columns, so no declared-field coercion reaches them and
* `SqlDriver#formatOutput` repairs them only inside its `if (this.isSqlite)`
* arm. On Postgres and MySQL they arrive out of the record read door as a JS
* `Date` — pinned in
* `packages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts`.
* columns, so no declared-field coercion reaches them; `SqlDriver#formatOutput`
* repairs them through its own `AUDIT_TIMESTAMP_COLUMNS` pass, which used to
* sit inside `if (this.isSqlite)` and so handed Postgres and MySQL rows out of
* the record read door as a JS `Date`. #13973 ([ADR-0053 D-F1]) lifted that
* pass out of the gate — it now runs on EVERY dialect, and the pin that once
* recorded the asymmetry records the canonical-text contract instead
* (`packages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts`
* §B, inverted on purpose).
*
* `MetadataStatsSchema.mtime` is `z.string().datetime()`
* (`packages/spec/src/system/metadata-persistence.zod.ts`), so a `Date` here
* is a silent violation of a declared contract.
* is a silent violation of a declared contract. This adapter stays because the
* `Date` domain did not close: `driver-sql` still hands an INVALID `Date`
* through unchanged ([ADR-0053 D-F3] — the one shape with no canonical text),
* and non-SQL drivers materialise their own `Date`s. Under [ADR-0053 D-F1]'s
* B1 ruling a producer-side arm like this became a NO-OP for the valid-`Date`
* case, ⛔ never a conflict with it.
*
* ⚠️ The call below looks redundant against `MetadataRecord`'s static type and
* is not: `rowToRecord` reaches its `createdAt` / `updatedAt` through an
Expand Down Expand Up @@ -103,18 +112,29 @@ function canonicalIsoInstant(value: unknown): string | undefined {
*
* [#14037] `rowToRecord` and the two history adapters below each assert a
* `string` over a driver row (`row.created_at as string | undefined`, and so
* on). On Postgres and MySQL that assertion is false: `SqlDriver#formatOutput`
* repairs the BUILTIN audit columns (`repairNaiveUtcAuditTimestamp`) and folds
* declared `Field.datetime` columns (`normalizeSqliteDatetimeOutput`) only
* inside its `if (this.isSqlite)` arm, and `withPostgresCalendarDayAsText`
* leaves `timestamptz` / `timestamp` deliberately untouched. Both column
* classes therefore arrive as a JS `Date` on the live dialects — pinned in
* `packages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts`.
* on). On Postgres and MySQL that assertion USED to be false for both column
* classes: `SqlDriver#formatOutput` repaired the BUILTIN audit columns
* (`repairNaiveUtcAuditTimestamp`) and folded declared `Field.datetime` columns
* (`normalizeSqliteDatetimeOutput`) only inside its `if (this.isSqlite)` arm,
* so both arrived as a JS `Date` on the live dialects. #13973 ([ADR-0053 D-F1])
* lifted both passes out of that gate; they run on EVERY dialect now, and the
* pin that recorded the asymmetry records the canonical-text contract instead
* (`packages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts`
* §B, inverted on purpose).
*
* ⚠️ `withPostgresCalendarDayAsText` is UNCHANGED by that ruling and still
* leaves `timestamptz` / `timestamp` deliberately untouched ([ADR-0053 D-F2]):
* the client library still materialises those columns as a `Date`. What moved
* is where it is folded — at the driver's own read boundary, not at the parser
* — so what reaches this adapter is the canonical text.
*
* `MetadataRecord.createdAt` / `.updatedAt` and
* `MetadataHistoryRecord.recordedAt` are declared `z.string().datetime()`
* (`packages/spec/src/system/metadata-persistence.zod.ts`) — a refinement a
* `Date` fails outright. The cast is an assertion about a driver row, never a
* measurement of one, which is why tsc reports nothing.
* measurement of one, which is why tsc reports nothing — and the `Date` domain
* did not close: `driver-sql` hands an INVALID `Date` through unchanged
* ([ADR-0053 D-F3]) and non-SQL drivers materialise their own.
*
* ⚠️ Deliberately NOT {@link canonicalIsoInstant} above. That difference used
* to be exactly one input shape — the Invalid `Date` on which that spelling
Expand Down
Loading
Loading