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
23 changes: 23 additions & 0 deletions .changeset/23658-client-publish-package.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
'@objectstack/client': minor
---

feat(client): `meta.publishItem` can name the package whose pending draft it promotes (`packageId`, sent as `?package=`), so a caller can follow the `?package=` remedy the per-item publish door prescribes when it refuses a publish that names no package (#23658)

Clause-②: yes (widening)

**What is new.** `meta.publishItem`'s options gain `packageId?: string`, sent as `?package=<id>` on `POST /api/v1/meta/:type/:name/publish`. The name, the query key and the omission rule are the ones `saveItem`'s and `deleteItem`'s `packageId` already use: only a non-empty id reaches the wire, and `undefined` and `''` send nothing.

**Why.** A metadata item can hold one row per package (ADR-0048). A per-item publish that names no package promotes the draft a draft save naming no package writes, and the door refuses it in two states, each with `?package=` as the remedy and nothing published: `409 RESOURCE_CONFLICT` when two or more packages hold the item, and `404 NO_DRAFT` when the item's one package holds no draft and another package's draft is stored. This client had no option for it, so an SDK caller (an agent, the CLI, an app) could not do what the refusal said.

FROM → TO:

```ts
// FROM: refused 409 RESOURCE_CONFLICT when two packages hold the item, with no way to name one
await client.meta.publishItem('view', 'shared_grid');

// TO: promotes com.acme.crm's pending draft into com.acme.crm's active row, and no other package's draft
await client.meta.publishItem('view', 'shared_grid', { packageId: 'com.acme.crm' });
```

**Unchanged.** A publish without `packageId` sends the same request as before: no query string, and the body `{ message }` when a message is given, `{}` otherwise. `packageId` combines with `message`, which still rides the body. The environment-scoped `client.environment(id).meta` declares no `publishItem`, and this release adds none.
51 changes: 49 additions & 2 deletions packages/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2235,14 +2235,61 @@ export class ObjectStackClient {
* effect ran, and each reports its own `success`: a 200 here means the
* draft was promoted, NOT that a seed load or a data-plane projection
* caught up.
*
* @param type - Metadata type (e.g., 'view', 'flow')
* @param name - Item name (snake_case identifier)
* @param opts - The publish `message`, which rides the request body, and
* the package whose draft the publish promotes (`packageId`).
*
* NAME THE PACKAGE when the draft you mean is not the one a publish naming
* none promotes. A publish that names no package promotes the draft a
* draft save naming no package writes, and is refused in two states, each
* prescribing `?package=` and publishing nothing: `409 RESOURCE_CONFLICT`,
* naming the packages, when two or more packages hold the item; and `404
* NO_DRAFT`, naming the drafts that remain, when the item's one package
* holds no draft and another package's draft is stored. Pass the package
* whose draft you mean as `opts.packageId` (`?package=` on the wire), as
* `saveItem` and `deleteItem` name the row they act on.
*/
publishItem: async (
type: string,
name: string,
opts?: { message?: string },
opts?: {
message?: string;
/**
* `?package=<id>` — the software package whose pending draft this
* publish promotes (`sys_metadata.package_id`, ADR-0048), into
* that package's active row, and no other package's draft.
*
* Omit to name no package: the door then promotes the draft a
* draft save naming no package writes, and refuses the two states
* the method's docblock names. Naming the package is what both
* refusals prescribe.
*
* Same member name, same `package` query key and same truthy guard
* as {@link SaveMetaItemOptions.packageId} and
* {@link DeleteMetaItemOptions.packageId}: only a non-empty id
* reaches the wire, and `undefined` and `''` both send nothing.
* The door reads the value through the same function as the save
* and delete doors, so it reads `all` as naming no package too;
* this client forwards the id verbatim and decides nothing about
* it.
*/
packageId?: string;
},
): Promise<PublishMetaItemResponse> => {
const route = this.getRoute('metadata');
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/publish`, {
// [#23658] `metaSaveQuery`'s spelling and guard: an empty id names no
// package, so it is never put on the wire, and an options-less publish
// stays byte-identical to what this method sent before. `set`, never
// `append`: the door refuses a repeated `?package=` with a 400.
const params = new URLSearchParams();
if (opts?.packageId) params.set('package', opts.packageId);
const search = params.toString();
// `query`, not `qs` — it carries its own `?`; see `saveItem`'s note on
// the three meanings `qs` holds in this file.
const query = search ? `?${search}` : '';
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/publish${query}`, {
method: 'POST',
body: JSON.stringify(opts?.message ? { message: opts.message } : {}),
});
Expand Down
Loading
Loading