Skip to content

The first-party SDK's meta.saveItem sends no query string at all, so the ?force=true the destructive 409 prescribes is unreachable through @objectstack/client on either REST door #11391

Description

@os-zhuang

Measured while implementing #11095 (threading ?force on the compound-name PUT /meta/:type/:a/:b). Separate surface, filed rather than folded — it needs a contract decision of its own and #11095's ruling does not reach it.

What was measured

saveMetaItem's Phase 3a-destructive gate raises 409 DESTRUCTIVE_CHANGE and ends the message with — re-submit with ?force=true to proceed. After #11095 that clause is TRUE of both REST PUT doors: each reads ?force off the query string and threads it.

It is still unreachable from the first-party SDK. @objectstack/client declares meta.saveItem twice — the unscoped client and the environment-scoped one — and neither builds a query string at all:

  • packages/client/src/index.ts:703saveItem: async (type, name, item)PUT ${route}/${type}/${name}, body only.
  • packages/client/src/index.ts:5151 — the scoped twin → PUT /meta/${type}/${name}, body only.

Measured on origin/main @ 11038529: grep -c force packages/client/src/index.ts is 9, and not one of those hits is on a metadata save — they are environments.delete({ force }) (?force=1), unrelated prose, and the word "enforced". The sibling getItem on the same object does compose a query string (if (options?.packageId) params.set('package', …)), so this is an absent option on one method rather than a client that cannot send query parameters.

So an SDK caller refused with the destructive 409 is told to re-submit with a parameter their client offers no way to set. The remedy is reachable only by dropping to raw fetch.

Why this is not #11095

#11095 was about two ROUTES that did not read a parameter callers could send. This is one CLIENT that cannot send a parameter both routes now read — a different layer, a different fix, and a different decision. It also predates #11095 on the single-segment door: the clause has been unactionable for SDK callers there the whole time, so this is not a regression that card introduced.

It is equally absent on both doors, so it is not a twin divergence either, and #7019's "twin doors must not diverge" argument — the load-bearing one for #11095's compound-name half — says nothing about it.

Why it is a contract question rather than an obvious fix

Adding force to meta.saveItem widens the published SDK surface, which is the same tier of decision #11095 was escalated for. Candidate shapes, none of them free:

  • A — an options bag: saveItem(type, name, item, { force?: boolean }). Matches getItem's existing options?.packageId shape on the same object, and leaves room for the twin's other two unexposed query parameters (?package, ?mode=draft), which are in exactly the same position. Widest, and the one that makes the whole door's surface reachable.
  • B — force alone, narrowest, on the argument that only this parameter is named by a refusal the caller is expected to act on. Leaves ?package / ?mode unreachable and invites a second card.
  • C — decline, and change the message instead: give the SDK-facing faces a remedy that names something an SDK caller can do. Zero widening, but the two REST doors would then be prescribing different things to HTTP callers and SDK callers for one refusal, which is close to the shape Two more faces of the DESTRUCTIVE_CHANGE 409 prescribe ?force=true on routes that never thread it — the compound-name PUT /meta/:type/:a/:b and the runtime dispatcher PUT /meta #11095 just removed.

Worth measuring before choosing, and not measured here: whether Studio and the CLI go through meta.saveItem or through raw HTTP for metadata saves. If the real destructive-edit surfaces already bypass the SDK, option C's cost is much lower than it looks, and option A is speculative surface.

Where the repair would land

packages/client/src/index.ts (both saveItem declarations, which must stay in step — they are the same method on two clients), plus whatever pins client.test.ts holds on the URL those methods build.

Related: #11095 (the routes, fixed), #11015 (the face-aware remedy clause), #10886 (the sole-carrier verdict on this message).

Measured on origin/main at 11038529.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions