feat(cma-client)!: one default_field_metadata shape, whatever the environment does - #56
Merged
Conversation
…nvironment does
`default_field_metadata` goes over the wire in two shapes, and which one an
environment accepts is a per-environment setting, not a version: field-keyed
(`{ alt: { en } }`) where the `non_localized_focal_points` opt-in is active,
locale-keyed (`{ en: { alt } }`) where it isn't. Each environment rejects the
other with the same `422 INVALID_FORMAT`, and the types describe the field-keyed
one at both layers — so on a legacy environment there was no way through: the
correctly typed call failed at runtime, and the call the API accepted didn't
compile.
That is work the simple layer exists to absorb. `uploads.create`, `update`,
`find`, `list` and `listPagedIterator` now convert in both directions, so
callers only ever see the field-keyed shape. The four `*FromUrl` /
`*FromLocalFile` helpers in the node and browser packages delegate to
`create`/`update`, so they are covered without touching them. The raw methods
are deliberately untouched: they are the escape hatch for seeing what actually
goes over the wire, and the two legacy types stay exported for typing it.
Reads need no lookup — a field-keyed payload always carries a top-level
`focal_point` and a locale-keyed one never can, since no locale code is the
literal string `focal_point`. Writes do need one, so `utilities/environmentSettings.ts`
memoizes `site.find()` per client. It caches the *promise*, not its result,
which is the point: a batch of ten thousand concurrent uploads would otherwise
all miss an empty cache and fire ten thousand lookups. It also never fires at
all for a write that carries no metadata.
The twenty-minute TTL applies to everything uniformly. An earlier draft cached
an active opt-in forever, since opt-ins are one-way — but that bought one
request per twenty minutes in exchange for a hardcoded list of which flags are
one-way, which would have rotted. Worse, the list isn't mechanically derivable:
`activateDraftModeAsDefault` maps to the meta key `draft_mode_default`, so
deriving it from the `activate*` methods would have dropped that flag in
silence. `EnvironmentFlag` is now derived from the generated `SiteMeta` — every
boolean in it, present and future, with nothing to keep in sync.
BREAKING CHANGE: on an environment without the opt-in, the simple methods used
to return the locale-keyed payload as the API sent it, and the docblock on
`UploadLocaleKeyedDefaultFieldMetadata` told callers to cast the response to
read it. They now return the field-keyed shape, so
`upload.default_field_metadata.en.alt` reads `undefined` — silently. Read it as
`.alt.en`, which is what the types said all along, or use `rawFind`/`rawList` to
keep seeing the wire payload. Opted-in environments are unaffected.
Verified against a real (opted-in) project: create/update/find/list round-trip
field-keyed across two locales with partial patches, a batch of ten writes costs
one `site` lookup, and writes without metadata cost none. The opted-out branch
is covered by unit tests against a stubbed client — new projects always have the
opt-in on, so there is no opted-out project left to create.
Ref: https://3.basecamp.com/5656352/buckets/33592490/card_tables/cards/10248986623
Claude-Session: https://claude.ai/code/session_01KTcZMvDWbEysMxhdwsgHyi
stefanoverna
force-pushed
the
feat/uploads-normalize-default-field-metadata
branch
from
August 28, 2026 15:23
b3c4e18 to
39025ca
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
default_field_metadatagoes over the wire in two shapes:{ alt: { en }, title: { en }, focal_point }non_localized_focal_pointsopt-in active{ en: { alt, title, focal_point } }Each environment rejects the other with the same `422 INVALID_FORMAT.
What this does
uploads.create,update,find,listandlistPagedIteratorconvert in both directions, so callers only ever see the field-keyed shape the types describe:createFromUrl,createFromLocalFile,updateFromUrlandupdateFromLocalFiledelegate tocreate/update, so they are covered without being touched.The raw methods are deliberately unchanged and still hand you whatever the environment sends. That is the point of the raw layer, and
UploadLocaleKeyedDefaultFieldMetadata/UploadLocaleKeyedDefaultFieldMetadataInRequeststay exported for typing those payloads.Breaking
On an environment without the opt-in, the simple methods used to return the locale-keyed payload as the API sent it, and the docblock on
UploadLocaleKeyedDefaultFieldMetadatatold callers to cast the response to read it. They now return the field-keyed shape, soupload.default_field_metadata.en.altreadsundefined— silently. Read it as.alt.en, which is what the types said all along, or userawFind/rawListto keep seeing the wire payload.Opted-in environments are unaffected: they already spoke the field-keyed shape.