Skip to content

Commit a543e24

Browse files
fix(packages): POST /packages/:id/revert reverts a Studio-authored package instead of answering 404 (#22112)
Fixes #22090 Clause-②: yes (widening) ## Summary `POST /api/v1/packages/:id/revert` answered `404 RESOURCE_NOT_FOUND "No metadata items found for package '…'"` for a Studio-authored package that held published items and a pending draft, and the draft stayed. The door asked only the metadata service, whose in-memory registry never holds a Studio package's stored rows. The door now asks the protocol first. `revertStoredPackage` reads the package's `sys_metadata` rows through the predicate `listDrafts` already used, refuses a never-published package with the declared `409`, and otherwise removes the drafts through the per-draft step `discardPackageDrafts` takes. A package with no stored row is answered by the metadata service's `revertPackage`, exactly as before. ## Where it landed, and why not in `metadata-manager.ts` The dispatch expected the fix in `MetadataManager.revertPackage` (`packages/metadata/src/metadata-manager.ts:2062`). Measurement put the producer elsewhere: - `@objectstack/metadata` cannot reach the stored rows. `@objectstack/metadata-protocol` depends on it, not the other way round. On a real boot the manager has no `sys_metadata` loader either: `setDataEngine` / `setDatabaseDriver` (`metadata-manager.ts:605`, `:635`) have no caller in the tree, and the repository the plugin attaches is a `FileSystemRepository` (`packages/metadata/src/plugin.ts:538`). - The stored rows, the package-scoped read and the draft discard all live in `@objectstack/metadata-protocol`. So the seam is a protocol verb, and the door in `@objectstack/runtime` composes the two services. `metadata-manager.ts` is unchanged. ## Mechanism (measured) - **H1: confirmed.** On `examples/app-showcase` booted with `objectstack dev --seed-admin --fresh` at base `b04a5295f7`, the following answered `404 RESOURCE_NOT_FOUND "No metadata items found for package 'com.example.repairs'"`, and `GET /meta/_drafts?packageId=com.example.repairs` still listed the draft. The steps: - `POST /packages` creates `com.example.repairs`; - `PUT /meta/object/repairs_ticket?package=com.example.repairs&mode=draft` saves an object; - `POST /packages/com.example.repairs/publish-drafts` publishes it (`outcome: published`); - a second draft save edits it; - `POST /packages/com.example.repairs/revert` returns the 404. The same package, published and with no draft, also answered 404. Call chain at base: - `dispatcher-plugin.ts:1380` mounts `/:id/revert`; - `http-dispatcher.ts:2238` `handlePackages`; - `domains/packages.ts:1882` revert branch; - `:1887` `metadataService.revertPackage(id)`; - `metadata-manager.ts:2062`, which collects members by `meta.packageId` / `meta.package` (`:2067`) and throws the 404 when there are none (`:2094`). - **H2: confirmed, seam moved.** *Discard changes* is `POST /packages/:id/discard-drafts` (base `domains/packages.ts:1792`), which calls `protocol.discardPackageDrafts` (base `protocol.ts:23716`). That reads `SysMetadataRepository.listDrafts({ packageId })`, which filters `sys_metadata.package_id` for the caller's organization plus env-wide rows (base `sys-metadata-repository.ts:1220`, `:1259`). The read path decorates the same column as `_packageId`. That `where` is now `packageScopedRowWhere` (`sys-metadata-repository.ts:406`), used by `listDrafts` (`:1285`) for drafts and by the revert (`protocol.ts:23853`) for published rows. It is one statement of the rule. `MetadataManager.revertPackage` is reachable from the protocol (the `metadata` service), but the stored rows are not reachable from the manager. So the seam is the protocol. - **H3: confirmed.** A stored item's published version is its `active` row, and a pending change is a separate `draft` row. `publish-drafts` promotes the draft (the drafts list empties). *Discard changes* removes the draft through `deleteMetaItem({ state: 'draft' })`, in the scope the draft lives in. The revert reuses that per-draft step, now shared as `discardDraftInItsScope` (`protocol.ts:23769`). There is no new revert mechanism. A package with stored rows but no active row answers `409 RESOURCE_CONFLICT`: "Package '…' has never been published, so there is no published version to revert to. Publish the package first, or discard its drafts to drop them." - **H4: confirmed.** `com.example.showcase` is the code-shipped control. Its two capabilities carry an authored `packageId` (`examples/app-showcase/src/security/capabilities.ts`). It has no stored row, so `revertStoredPackage` answers `stored: false` and the unchanged metadata-service branch answers. It returned `409 "Package 'com.example.showcase' has never been published"` live at both base and head. ## Pins: base and head answers | pin | base `b04a5295f7` | head | |:--|:--|:--| | (a) Studio package, published, one draft | 404 `RESOURCE_NOT_FOUND`, draft stays | 200 `{ success: true }`, drafts `[]`, the published body serves | | (b) Studio package, never published | 404 `RESOURCE_NOT_FOUND` | 409 `RESOURCE_CONFLICT`, worded, draft untouched | | (c) unknown id | 404 `RESOURCE_NOT_FOUND` | 404 `RESOURCE_NOT_FOUND` (unchanged) | | (d) code-shipped `com.example.showcase` | 409 `RESOURCE_CONFLICT` "has never been published" | the same 409, the same sentence | | published Studio package, no draft | 404 | 200 | The live door gave these answers on both boots. `packages/runtime/src/package-revert-stored-members.integration.test.ts` pins them at the door: a real `HttpDispatcher`, a real ObjectQL on better-sqlite3, the real protocol and a real `MetadataManager`. It has 9 cases. Two of them pin the organization rule: an org caller reverts the env-wide draft, and another organization's draft is not touched. `domains/packages-vetted-org-source.test.ts` adds the revert door to its roster, because the door now reads the caller's vetted organization. No door-level pin under `packages/qa/dogfood/test/` was needed. ## Reverse verification On committed HEAD, `packages/runtime/src/domains/packages.ts` was restored to base with `git restore --source=b04a5295f7 --worktree`. The anchor count dropped from 4 to 0, and the file's blob equalled the base blob. Predicted before the run: 6 red, 3 green. Measured: 6 red, 3 green. Every red was `expected 404 to be 200` or `expected 404 to be 409`, the base's 404. The three controls stayed green. Restored with `git checkout HEAD -- …`. Proof: `git hash-object` equals the HEAD blob `2df234a954`, the index blob equals the same, `git diff HEAD` is empty, and `git status` is clean. ## Clause-② (measured on the built entry declarations) The claim declared `no`. The built `@objectstack/metadata-protocol` `dist/index.d.ts`, base against head with comments stripped, gains one public method, `revertStoredPackage(request)`, and one `private` member. `packageScopedRowWhere` is not exported from the entry. The `@objectstack/runtime` `dist/index.d.ts` is byte-identical with comments stripped. One added public method on an exported class is a widening, as in the `declinesStoredRow` precedent. So the line above reads `yes (widening)`, `@objectstack/metadata-protocol` is graded `minor`, and `@objectstack/runtime` is graded `patch` with `Clause-②: no` in its changeset. The route's request and response contract is unchanged. ## Tests and gates (at `723afed248`) - `pnpm --filter @objectstack/metadata-protocol exec vitest run`: 221 files passed, 3 skipped; 28227 tests passed, 19 skipped. - `pnpm --filter @objectstack/runtime exec vitest run --project local`: 333 files passed; 4710 tests passed, 19 skipped. - `typecheck` for both packages exits 0. The runtime test layer holds its ledger (27 files, 190 errors, 68 signatures, unchanged). - The `node scripts/pm/dispatch-gates.mjs --commands` derivation at `723afed248` gives 65 commands, all run. `--ran` reconciles 65 of 65 with a derived zero NOT-MEASURED. - The 53-row artifact-roster block was run too. 51 rows are green; `check-partof-closing-keyword` was run with this body as `PR_BODY`. `check-closing-target-claim` and `check-single-claim-paths` need a PR number, so CI reads them. - `check-changeset-no-major` was run with this body as the event payload. Its level axis reads `Clause-②: yes (widening)` and finds `@objectstack/metadata-protocol` graded `minor`. It is green. - The four symbol-anchor sweeps (`adr-symbol-anchors`, `scripts-symbol-anchors`, `spec-docblock-symbol-anchors`, `adr-anchors`) are green. - `check:dual-build-cjs-loads` first reported PREREQUISITE NOT MET because 8 unrelated dists were missing. It is green after building them. - ESLint was run as a declared narrowing. The population is the 5 changed TypeScript files. The JSON output reports 5 files, 0 errors and 0 warnings. `eslint.config.mjs` enables no type-aware linting, so no untouched file's verdict can move. - `main` gained two commits after the last merge, `ac9f8bd471` and `aa71c4d9d1`. One edits a `metadata-protocol` test file and one is a `spec` change. CI's merge ref reads them. ## Acceptance notes - **An item created after the last publish is removed by the revert.** It is a draft with no published row, so it was not part of the published state. *Discard changes* removes it too. For a stored package the two buttons now do the same thing once the package has been published. The objectui wording follow-up triage foresaw (merge them, or keep both with distinct meanings) is unchanged. - **Code-shipped package with an org overlay draft (boundary, unchanged answer class).** This was measured live with an org-scoped draft of `showcase_task.done` bound to `com.example.showcase`. The package now has a stored draft and no active stored row, so the revert answers `409` with the stored half's sentence. At base it answered `409` with the registry's sentence. The draft stays in both. Whether a code baseline counts as "published" belongs to the same Revert-versus-Discard follow-up. - **A refused draft fails the call.** The error carries the refusal's own `code` and `status`. Drafts discarded before it stay discarded, as with `discard-drafts`. A collected list would have reached the door's `{ success: true }` as a success. - **The door does not forward a body `actor`.** It read no body before, and its request contract is unchanged. --- _Generated by [Claude Code](https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ace0a53 commit a543e24

7 files changed

Lines changed: 559 additions & 38 deletions
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/metadata-protocol": minor
3+
---
4+
5+
`ObjectStackProtocolImplementation.revertStoredPackage({ packageId, organizationId?, actor? })` reverts a package's stored members to their published version
6+
7+
Clause-②: yes (widening)
8+
9+
- A Studio-authored package's members are `sys_metadata` rows bound by `package_id`. The method reads them through the same predicate `listDrafts` uses, which now has one statement shared by both reads.
10+
- No stored row bound to the package: it answers `{ stored: false, discarded: [] }` and touches nothing. The package's members, if it has any, belong to the metadata service. An unprovisioned `sys_metadata` answers the same way.
11+
- Stored rows, none of them published: it refuses with `RESOURCE_CONFLICT` / 409, "Package '…' has never been published, so there is no published version to revert to." Nothing is touched.
12+
- Otherwise it removes every draft of the package, each in the scope it lives in, through the same per-draft step `discardPackageDrafts` takes. Each item then serves its published (active) row again. An item created after the last publish has no published row and is removed. A refused draft fails the call with that refusal's own code and status.
13+
- The only change to the public surface is this one added method. `discardPackageDrafts` and `listDrafts` answer as before.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@objectstack/runtime": patch
3+
---
4+
5+
`POST /packages/:id/revert` reverts a Studio-authored package instead of answering 404 "No metadata items found"
6+
7+
Clause-②: no
8+
9+
- The door asked only the metadata service, whose in-memory registry never holds a Studio package's stored rows. So a package with published items and a pending draft answered `404 RESOURCE_NOT_FOUND`, and the draft stayed.
10+
- The door now asks the protocol's `revertStoredPackage` first, with the caller's active organization. A package with stored rows answers `200 { success: true }` with its drafts removed, or `409 RESOURCE_CONFLICT` when it has never been published.
11+
- A package with no stored row is answered by the metadata service's `revertPackage`, exactly as before. That covers a code-shipped package and an unknown id (`404`). It is also what happens when the protocol does not provide `revertStoredPackage`.
12+
- The request and the response shape are unchanged.

‎packages/metadata-protocol/src/protocol.ts‎

Lines changed: 125 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ import type { RuntimeAuthoringIssue } from './runtime-authoring-gate.js';
4343
// ADR-0120 D4 reporting that replaced this file's empty `catch` blocks.
4444
import { ensureMetadataOverlayIndexes } from './migrations/overlay-index.js';
4545
import { driverCanRunSql, resolveDriverExec } from './migrations/driver-exec.js';
46-
import { DraftConflictError, SysMetadataRepository, type SysMetadataEngine } from './sys-metadata-repository.js';
46+
import { DraftConflictError, SysMetadataRepository, packageScopedRowWhere, type SysMetadataEngine } from './sys-metadata-repository.js';
4747
import { isOriginGatedType, packagedBaseRegimeSentence } from './packaged-base-regime.js';
4848
import {
4949
resolveArtifactLockLayer,
@@ -23711,7 +23711,8 @@ export class ObjectStackProtocolImplementation implements
2371123711
*
2371223712
* Use case: "I edited this app for a while and it turned out worse than
2371323713
* before — abandon all my changes." Routes through the sys_metadata path
23714-
* (no metadata-service dependency, unlike `POST /packages/:id/revert`).
23714+
* (no metadata-service dependency). {@link revertStoredPackage} takes the
23715+
* same per-draft step for `POST /packages/:id/revert`.
2371523716
*/
2371623717
async discardPackageDrafts(request: {
2371723718
packageId: string;
@@ -23734,25 +23735,14 @@ export class ObjectStackProtocolImplementation implements
2373423735

2373523736
for (const d of drafts) {
2373623737
try {
23737-
// Discard the draft in the scope it lives in (#3115). Like
23738-
// publish, `listDrafts` surfaces env-wide drafts to a non-null
23739-
// active org via `$or`; deleting under the request's active org
23740-
// would silently no-op on those env-wide rows.
23741-
const draftOrgId = d.organizationId ?? null;
23742-
await this.deleteMetaItem({
23743-
type: d.type,
23744-
name: d.name,
23745-
state: 'draft',
23746-
...(draftOrgId ? { organizationId: draftOrgId } : {}),
23747-
...(request.actor ? { actor: request.actor } : {}),
23748-
});
23738+
await this.discardDraftInItsScope(d, request.actor);
2374923739
discarded.push({ type: d.type, name: d.name });
2375023740
} catch (e: any) {
2375123741
// [#8136] Same source, same reasoning as `deletePackage`'s
2375223742
// `failed[]` collector below: this `try` wraps only
23753-
// `deleteMetaItem`, whose exits all now either declare a
23754-
// refusal or withhold at the source. Clean derivatively; no
23755-
// filter of its own.
23743+
// `deleteMetaItem` (through `discardDraftInItsScope`), whose
23744+
// exits all now either declare a refusal or withhold at the
23745+
// source. Clean derivatively; no filter of its own.
2375623746
failed.push({
2375723747
type: d.type,
2375823748
name: d.name,
@@ -23771,6 +23761,124 @@ export class ObjectStackProtocolImplementation implements
2377123761
};
2377223762
}
2377323763

23764+
/**
23765+
* Discard ONE pending draft in the scope it lives in — the per-draft step
23766+
* {@link discardPackageDrafts} and {@link revertStoredPackage} share, so
23767+
* the scope rule below has one home.
23768+
*/
23769+
private async discardDraftInItsScope(
23770+
draft: { type: string; name: string; organizationId: string | null },
23771+
actor: string | undefined,
23772+
): Promise<void> {
23773+
// Discard the draft in the scope it lives in (#3115). Like
23774+
// publish, `listDrafts` surfaces env-wide drafts to a non-null
23775+
// active org via `$or`; deleting under the request's active org
23776+
// would silently no-op on those env-wide rows.
23777+
const draftOrgId = draft.organizationId ?? null;
23778+
await this.deleteMetaItem({
23779+
type: draft.type,
23780+
name: draft.name,
23781+
state: 'draft',
23782+
...(draftOrgId ? { organizationId: draftOrgId } : {}),
23783+
...(actor ? { actor } : {}),
23784+
});
23785+
}
23786+
23787+
/**
23788+
* [#22090] Revert a package's STORED members to their published version —
23789+
* the half of `POST /packages/:id/revert` a Studio-authored package needs.
23790+
*
23791+
* ## Why the metadata service could not do this
23792+
*
23793+
* `MetadataManager.revertPackage` collects members from its in-memory
23794+
* registry, by an item's own `packageId` / `package` key. A Studio-authored
23795+
* package lives in `sys_metadata`, bound by each row's `package_id`
23796+
* column, and none of its rows is in that registry: a real boot gives the
23797+
* manager no `sys_metadata` loader, and `@objectstack/metadata` cannot
23798+
* reach this package (the dependency runs the other way). So the revert of
23799+
* a Studio package answered 404 "No metadata items found" while the
23800+
* package held published items and a pending draft.
23801+
*
23802+
* ## What "revert" means for a stored member
23803+
*
23804+
* A stored item's published version is its ACTIVE row, and a pending
23805+
* change is a separate DRAFT row beside it. Reverting the package to its
23806+
* published state is removing its drafts, through the per-draft step
23807+
* {@link discardPackageDrafts} takes ({@link discardDraftInItsScope}) —
23808+
* not a second revert mechanism. A draft with no active row (an item
23809+
* created after the last publish) is removed too: it was not part of the
23810+
* published state.
23811+
*
23812+
* Membership is the package's rows as {@link SysMetadataRepository.listDrafts}
23813+
* reads them — `package_id`, the caller's organization plus env-wide — and
23814+
* the active rows are read through the same predicate,
23815+
* {@link packageScopedRowWhere}, never a `where` of this method's own.
23816+
*
23817+
* ## Answers
23818+
*
23819+
* - No stored row bound to the package: `{ stored: false }`, and nothing
23820+
* is touched. Its members, if it has any, are the metadata service's;
23821+
* the caller asks that service's `revertPackage` next, so a code-shipped
23822+
* package reverts exactly as it did before. An unprovisioned
23823+
* `sys_metadata` answers the same way: there are genuinely no rows.
23824+
* - Stored rows, none of them active: `RESOURCE_CONFLICT` / 409. The
23825+
* package has never been published, so there is no published version to
23826+
* revert to. Nothing is touched.
23827+
* - Otherwise every draft is discarded. A published package with no draft
23828+
* is already at its published version, which is a success too.
23829+
*
23830+
* A draft whose discard is refused fails the call with that refusal's own
23831+
* `code` and `status` (ADR-0112). {@link discardPackageDrafts} answers a
23832+
* per-item outcome report; this verb's door answers `{ success: true }` or
23833+
* an error, so a refusal collected into a list would reach the caller as a
23834+
* success. Drafts discarded before the refusal stay discarded, as they do
23835+
* in {@link discardPackageDrafts}.
23836+
*/
23837+
async revertStoredPackage(request: {
23838+
packageId: string;
23839+
organizationId?: string;
23840+
actor?: string;
23841+
}): Promise<{
23842+
stored: boolean;
23843+
discarded: Array<{ type: string; name: string }>;
23844+
}> {
23845+
await this.ensureOverlayIndex();
23846+
const orgId = request.organizationId ?? null;
23847+
let drafts: Awaited<ReturnType<SysMetadataRepository['listDrafts']>>;
23848+
let publishedRows: unknown[];
23849+
try {
23850+
drafts = await this.getOverlayRepo(orgId).listDrafts({ packageId: request.packageId });
23851+
// [#21911] The explicit system opt-in — see findServedOverlayRow.
23852+
publishedRows = (await this.engine.find('sys_metadata', {
23853+
where: packageScopedRowWhere(orgId, 'active', { packageId: request.packageId }),
23854+
limit: 1,
23855+
context: { isSystem: true },
23856+
})) as unknown[];
23857+
} catch (error) {
23858+
this.rethrowUnlessMetadataStoreUnprovisioned(error, 'sys_metadata');
23859+
return { stored: false, discarded: [] };
23860+
}
23861+
23862+
if (drafts.length === 0 && publishedRows.length === 0) return { stored: false, discarded: [] };
23863+
23864+
if (publishedRows.length === 0) {
23865+
const err = new Error(
23866+
`Package '${request.packageId}' has never been published, so there is no published version to `
23867+
+ 'revert to. Publish the package first, or discard its drafts to drop them.',
23868+
) as Error & { code?: string; status?: number };
23869+
err.code = 'RESOURCE_CONFLICT';
23870+
err.status = 409;
23871+
throw err;
23872+
}
23873+
23874+
const discarded: Array<{ type: string; name: string }> = [];
23875+
for (const d of drafts) {
23876+
await this.discardDraftInItsScope(d, request.actor);
23877+
discarded.push({ type: d.type, name: d.name });
23878+
}
23879+
return { stored: true, discarded };
23880+
}
23881+
2377423882
/**
2377523883
* Delete an ENTIRE package: every `sys_metadata` row bound to it (active
2377623884
* AND draft) and — by default — the physical table of each object it

‎packages/metadata-protocol/src/sys-metadata-repository.ts‎

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,50 @@ export function resetEnvWritableMetadataTypes(): void {
386386
_envWritableMetadataTypes = null;
387387
}
388388

389+
/**
390+
* [#22090] The `sys_metadata` predicate for "the rows of one state that a
391+
* caller in `organizationId` sees, optionally narrowed to a `type` and a
392+
* package" — the ONE statement of that rule, which
393+
* {@link SysMetadataRepository.listDrafts} asks for `'draft'` and the
394+
* protocol's package revert asks for `'active'`.
395+
*
396+
* A package's membership in storage is its rows' `package_id` column, the
397+
* same column the read path decorates as `_packageId`. The revert has to know
398+
* which of a package's rows are published as well as which are drafts, and a
399+
* second spelling of the org half below is exactly the drift #3115 / #7705 /
400+
* #7779 each had to repair on one copy at a time — so the revert asks this
401+
* function, never a `where` of its own.
402+
*
403+
* Deliberately NOT exported from the package entry: it is a shared predicate
404+
* between this module and `protocol.ts`, not a public read.
405+
*/
406+
export function packageScopedRowWhere(
407+
organizationId: string | null,
408+
state: 'draft' | 'active',
409+
filter?: { type?: string; packageId?: string },
410+
): Record<string, unknown> {
411+
const where: Record<string, unknown> = { state };
412+
// Surface BOTH org-scoped drafts and env-wide (`organization_id IS NULL`)
413+
// drafts. Env-wide drafts are real pending changes — `getMetaItems`/preview
414+
// overlays them and `publish-drafts` promotes them — but a strict
415+
// `organization_id = this.organizationId` equality silently dropped them
416+
// whenever the active org was non-null. AI-authored metadata is written
417+
// env-wide, so its drafts vanished from the pending-changes list and the
418+
// Publish CTA never appeared: the change showed in preview yet could not be
419+
// published from the UI (the "orphaned draft" bug).
420+
if (organizationId != null) {
421+
where.$or = [
422+
{ organization_id: organizationId },
423+
{ organization_id: null },
424+
];
425+
} else {
426+
where.organization_id = null;
427+
}
428+
if (filter?.type) where.type = filter.type;
429+
if (filter?.packageId) where.package_id = filter.packageId;
430+
return where;
431+
}
432+
389433
/**
390434
* One live `watch()` subscription, as a record rather than a bare listener.
391435
*
@@ -1238,25 +1282,7 @@ export class SysMetadataRepository implements MetadataRepository {
12381282
}>
12391283
> {
12401284
this.assertOpen();
1241-
const where: Record<string, unknown> = { state: 'draft' };
1242-
// Surface BOTH org-scoped drafts and env-wide (`organization_id IS NULL`)
1243-
// drafts. Env-wide drafts are real pending changes — `getMetaItems`/preview
1244-
// overlays them and `publish-drafts` promotes them — but a strict
1245-
// `organization_id = this.organizationId` equality silently dropped them
1246-
// whenever the active org was non-null. AI-authored metadata is written
1247-
// env-wide, so its drafts vanished from the pending-changes list and the
1248-
// Publish CTA never appeared: the change showed in preview yet could not be
1249-
// published from the UI (the "orphaned draft" bug).
1250-
if (this.organizationId != null) {
1251-
where.$or = [
1252-
{ organization_id: this.organizationId },
1253-
{ organization_id: null },
1254-
];
1255-
} else {
1256-
where.organization_id = null;
1257-
}
1258-
if (filter?.type) where.type = filter.type;
1259-
if (filter?.packageId) where.package_id = filter.packageId;
1285+
const where = packageScopedRowWhere(this.organizationId, 'draft', filter);
12601286
const rows = await this.engine.find('sys_metadata', { where, context: { isSystem: true } });
12611287
return (rows as any[]).map((row) => ({
12621288
type: row.type,

‎packages/runtime/src/domains/packages-vetted-org-source.test.ts‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,12 @@ function protocolDouble() {
121121
const discarded = record('discardPackageDrafts', req).drafts.splice(0);
122122
return { success: true, discardedCount: discarded.length, discarded };
123123
},
124+
// [#22090] The stored-member half of `POST /packages/:id/revert`: the
125+
// partition's drafts are its stored rows, so it answers `stored: true`.
126+
revertStoredPackage: async (req: any) => {
127+
const discarded = record('revertStoredPackage', req).drafts.splice(0);
128+
return { stored: true, discarded: discarded.map((name) => ({ type: 'view', name })) };
129+
},
124130
listCommits: async (req: any) =>
125131
record('listCommits', req).commits.map((id) => ({ id, packageId: req.packageId })),
126132
revertCommit: async (req: any) => {
@@ -321,13 +327,16 @@ beforeEach(() => {
321327
});
322328
afterEach(() => { warnSpy.mockRestore(); });
323329

324-
// ── The nine doors that read the organization source ──────────────────────────
330+
// ── The doors that read the organization source ───────────────────────────────
331+
// The nine of the defect, and `POST /packages/:id/revert`, which reads it since
332+
// #22090 asks the protocol for the package's stored members.
325333

326334
interface Door { name: string; method: string; path: string; body?: unknown; verb: string }
327335

328336
const DOORS: Door[] = [
329337
{ name: 'POST /packages/:id/publish-drafts', method: 'POST', path: `/packages/${PKG}/publish-drafts`, body: {}, verb: 'publishPackageDrafts' },
330338
{ name: 'POST /packages/:id/discard-drafts', method: 'POST', path: `/packages/${PKG}/discard-drafts`, body: {}, verb: 'discardPackageDrafts' },
339+
{ name: 'POST /packages/:id/revert', method: 'POST', path: `/packages/${PKG}/revert`, body: {}, verb: 'revertStoredPackage' },
331340
{ name: 'GET /packages/:id/commits', method: 'GET', path: `/packages/${PKG}/commits`, verb: 'listCommits' },
332341
{ name: 'POST /packages/:id/commits/:commitId/revert', method: 'POST', path: `/packages/${PKG}/commits/cmt_alpha/revert`, body: {}, verb: 'revertCommit' },
333342
{ name: 'POST /packages/:id/rollback', method: 'POST', path: `/packages/${PKG}/rollback`, body: { commitId: 'cmt_alpha' }, verb: 'rollbackToPackageCommit' },

0 commit comments

Comments
 (0)