Skip to content

Commit 8290dc0

Browse files
feat(client): meta.deleteItem names the package whose row it removes, sent as ?package= (#23293) (#23349)
Fixes #23293 Clause-②: yes (widening): `DeleteMetaItemOptions` gains the optional `packageId`, which both `meta.deleteItem` declarations send as `?package=`. ## What this does `@objectstack/client`'s `meta.deleteItem` can name the package whose row it removes. `DeleteMetaItemOptions` gains `packageId?: string`, and the one query builder both declarations call (`metaDeleteQuery`, `packages/client/src/index.ts`) appends it as `package=`, beside `state`. The name, the key and the omission rule are the save path's (`SaveMetaItemOptions.packageId` and `metaSaveQuery`): only a non-empty id reaches the wire, and `undefined` and `''` send nothing. Both declarations, the unscoped `ObjectStackClient.meta` and the environment-scoped `client.environment(id).meta`, get it with no per-declaration code, because both already call the one builder. Why: since PR #23290 (merged as `543b33dee`), the REST reset door refuses a delete that names no package of an item two or more packages hold with `409 RESOURCE_CONFLICT`, and the refusal prescribes `?package=`. Before this change the SDK could not send it. This is the same class as #11391 on `saveItem`. The option bag's docblock now says the door reads four carriers and the bag declares three. `?dropStorage` is still withheld, under the same ruling as before. **Level: `minor`** for `@objectstack/client` (`.changeset/23293-client-delete-package.md`). An optional member on an exported request-options type is an additive widening. The house form was checked, not assumed: the two sibling widenings on this same surface shipped as minors. `SaveMetaItemOptions`, the #11391 save-path bag, and the `cf71d73` `DeleteMetaItemOptions` bag are both listed under `17.3.0 ### Minor Changes` in `packages/client/CHANGELOG.md`. ## Premises re-read on `main` (at `614fdec83`, the branch point) - **The door.** `DELETE /api/v1/meta/:type/:name` (`packages/rest/src/rest-server.ts`) declares `package` in `META_ITEM_RESET_PARAMS`, its accepted query set, and refuses a repeated `package` (`refuseRepeatedQueryParams(..., ['state', 'dropStorage', 'package', ...])`). It reads the value through `metaItemPackageBinding`, which is also the save door's reading (`all`, the empty value and a non-string name no package), and threads it as `packageId` into `deleteMetaItem`. `543b33dee` is an ancestor of the base: `git merge-base --is-ancestor 543b33d HEAD` exits 0. - **One builder.** Both declarations called `metaDeleteQuery(options)` and `metaDeleteHeaders(options)`, so the member lands once, in the shared type and the shared builder. Confirmed: the change to those two declarations is docblock text only. - **Spec.** `DeleteMetaItemRequestSchema.packageId` already exists (it arrived with #23165's change). Nothing changes in `packages/spec` or `packages/rest`. ## Tests (at `13e311f07`, the head this PR opens with: the three commits plus a merge of `origin/main` at `3490bf5fa`) New cases in `packages/client/src/meta-delete-item-carriers.test.ts`. Each claim is made on both declarations, and each has a negative control. - **Wire, unscoped and scoped:** a delete naming a package sends `?package=PACKAGE_ID`. A delete naming none (no bag, `{}`, `packageId: ''`, `packageId: undefined`) sends no query string. With `state`, the query is `?state=draft&package=PACKAGE_ID` and `package` appears once; `If-Match` is not disturbed. Encoding matches `saveItem` byte for byte. Both twins put the same query on the wire for the same options. - **The real reset door** (the file's existing `bootDoor`: real `RestServer` routes, real `ObjectStackProtocolImplementation`, real `sys_metadata` on SQLite). One view is saved into two packages through the SDK. A delete naming no package is refused with `err.code === 'RESOURCE_CONFLICT'` and `err.httpStatus === 409`, both rows survive, and the protocol probe shows no `packageId` arrived. With `{ packageId }`, only that package's row is gone and the probe shows the package arrived. The same holds on the scoped twin. Runs, at `13e311f07` after rebuilding the client's dependency closure (`pnpm --filter '@objectstack/client^...' build`) on the merged tree: - `pnpm --filter @objectstack/client exec vitest run --maxWorkers=2`: `Test Files 51 passed (51)`, `Tests 663 passed (663)`. The same counts were read at `4ae4bd784`, before the merge. - `pnpm --filter @objectstack/client run typecheck`: exit 0, `check:test-typecheck: OK`. `tsc -p tsconfig.test.json --listFiles` lists `meta-delete-item-carriers.test.ts`, so the new cases are type-checked. - Gates: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `13e311f07` derives 63 commands. All 63 exit 0, and `--ran` reconciles them as `63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN`. The four artifact-roster gates whose roster sits under `.changeset/` or `packages/` (`check-changeset-fixed`, `check:authz-resolver`, `check:error-code-casing`, `check:filter-alias-parity`) were also run, and each exits 0. - Lint, narrowed to the two touched TypeScript files: `eslint --no-inline-config --format json` reads 2 files with 0 errors and 0 warnings. `--print-config` resolves 6 rules for `index.ts`, so the file is linted and not ignored. The config sets no `parserOptions.project` or `projectService`, so linting is not type-aware and this diff cannot move the verdict on any untouched file. The full `pnpm lint` is left to CI. **Ablation (one-off, no test file kept).** The implementation was committed first. Then `node scripts/ablation-replace.mjs` removed the builder's `params.set('package', ...)` line from `metaDeleteQuery`: the anchor went from 1 hit to 0, the marker from 0 to 1, and the blob from `3d93926e9ec5` to `209aedd32d88`. The same file then ran **7 failed / 23 passed**. All five wire cases that send a package failed, and so did both real-door cases that name one. The negative controls and the package-less 409 case stayed green, which is the expected direction because they do not depend on the line. The restore was proven on disk: blob equal to HEAD (`3d93926e9ec5`), `git diff HEAD` empty, `git status` clean. `index.ts` is imported from source (`./index`), so no build sits between the mutation and the run. **Not run locally, declared to CI.** `packages/cli` imports `DeleteMetaItemOptions` (`src/commands/meta/delete.ts`) and builds the bag from its flags. The member is additive and optional, and the command reads no member list. The CLI's typecheck lane and its tests need a 25-package build closure that is not built in this container, so CI runs them. ## Acceptance notes - `os meta delete` (and `os meta register`) has no `--package` flag, so the CLI still cannot name a package on its own commands. The SDK half is what this PR delivers. The command layer is a separate widening in `packages/cli`, and no PR or card in flight carries it. - The console's reset is objectstack-ai/objectui#12335's half, and objectstack-ai/objectui#12335 remains open. - The client forwards `packageId` verbatim. How `all` is read is the door's decision (`metaItemPackageBinding`), as on the save path, and this client adds no reading of its own. --- _Generated by [Claude Code](https://claude.ai/code/session_01BRQ9Mp1yH7R1az6MaUM18H)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8639a19 commit 8290dc0

3 files changed

Lines changed: 230 additions & 6 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'@objectstack/client': minor
3+
---
4+
5+
feat(client): `meta.deleteItem` can name the package whose row it removes (`packageId`, sent as `?package=`), so a caller can follow the `409 RESOURCE_CONFLICT` remedy the reset door prescribes for an item two packages hold (#23293)
6+
7+
Clause-②: yes (widening)
8+
9+
**What is new.** `DeleteMetaItemOptions` gains `packageId?: string`. Both `deleteItem` declarations take it, the unscoped `ObjectStackClient.meta` and the environment-scoped `client.environment(id).meta`, and send it as `?package=<id>` through the one query builder they share. The name, the query key and the omission rule are the ones `saveItem`'s `packageId` already uses: only a non-empty id reaches the wire, and `undefined` and `''` send nothing.
10+
11+
**Why.** A metadata item can hold one row per package (ADR-0048). The REST reset door `DELETE /api/v1/meta/:type/:name` reads `?package=` and refuses a delete that names no package of an item two or more packages hold, with `409 RESOURCE_CONFLICT`. The refusal names the packages and the remedy, which is to name one with `?package=`. This client had no option for it, so an SDK caller (an agent, the CLI, an app) could not do what the refusal said.
12+
13+
FROM → TO:
14+
15+
```ts
16+
// FROM: refused 409 RESOURCE_CONFLICT when two packages hold the item, with no way to name one
17+
await client.meta.deleteItem('view', 'shared_grid');
18+
19+
// TO: removes com.acme.crm's row of the item and no other package's row
20+
await client.meta.deleteItem('view', 'shared_grid', { packageId: 'com.acme.crm' });
21+
```
22+
23+
**Unchanged.** A delete without `packageId` sends the same request as before: no query string unless `state: 'draft'` is set, and no header unless `ifMatch` is set. `packageId` combines with `ifMatch` and `state` (`?state=draft&package=<id>`). `?dropStorage` is still not an option.

‎packages/client/src/index.ts‎

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -825,7 +825,7 @@ function metaSaveHeaders(options?: SaveMetaItemOptions): Record<string, string>
825825
* reads, made reachable from the SDK (commit cf71d73f8).
826826
*
827827
* `DELETE /meta/:type/:name` ("reset metadata item to artifact default")
828-
* reads THREE carriers. This bag declares TWO of them, and the third's
828+
* reads FOUR carriers. This bag declares THREE of them, and the fourth's
829829
* absence is a ruling rather than an oversight:
830830
*
831831
* - {@link DeleteMetaItemOptions.ifMatch} — the ADR-0008 OCC pin. The door
@@ -837,9 +837,15 @@ function metaSaveHeaders(options?: SaveMetaItemOptions): Record<string, string>
837837
* reset: discard the pending draft and leave the published overlay
838838
* serving. Its absence did not make the SDK safer, it made the SDK's only
839839
* reachable reset the full one.
840+
* - {@link DeleteMetaItemOptions.packageId} — `?package=<id>` (#23293), the
841+
* package whose row the delete removes (ADR-0048: one row per package).
842+
* The door refuses a delete that names no package of an item two or more
843+
* packages hold, `409 RESOURCE_CONFLICT`, and naming the package is that
844+
* refusal's remedy, so without this member an SDK caller was prescribed a
845+
* parameter their client could not send.
840846
*
841847
* ⛔ `?dropStorage=true` is deliberately NOT a member, and adding it "for
842-
* completeness" reverses a decision. It is the one carrier of the three that
848+
* completeness" reverses a decision. It is the one carrier of the four that
843849
* ADDS destructive reach — it drops the object's physical table after the
844850
* metadata row goes — no caller was measured needing it from this client,
845851
* and the door's repeated-parameter refusal exists because of that
@@ -894,6 +900,26 @@ export interface DeleteMetaItemOptions {
894900
* overlay too.
895901
*/
896902
state?: 'active' | 'draft';
903+
/**
904+
* `?package=<id>` — the software package whose row this delete removes
905+
* (`sys_metadata.package_id`, ADR-0048), and no other package's row.
906+
*
907+
* Omit to name no package. The door then removes the row of the one
908+
* package the item's rows (at the deleted `state`) are bound to, removes
909+
* the package-unbound row when none is bound, and refuses `409
910+
* RESOURCE_CONFLICT`, naming the packages and deleting nothing, when two
911+
* or more hold the item. Naming one of them here is what that refusal
912+
* prescribes. A named package that holds no row removes nothing
913+
* (`reset: false`).
914+
*
915+
* Same member name, same `package` query key and same truthy guard as
916+
* {@link SaveMetaItemOptions.packageId}, the save door's address for the
917+
* row it writes: only a non-empty id reaches the wire, and `undefined` and
918+
* `''` both send nothing. The door reads the value through the same
919+
* function as the save door, so it reads `all` as naming no package there
920+
* too; this client forwards the id verbatim and decides nothing about it.
921+
*/
922+
packageId?: string;
897923
}
898924

899925
/**
@@ -906,15 +932,18 @@ export interface DeleteMetaItemOptions {
906932
* BYTE-IDENTICAL to what this method sent before the bag existed.
907933
*
908934
* `URLSearchParams.set` (never `append`) is load-bearing: the door REFUSES a
909-
* repeated `state` (#6877), so a builder that could emit the key twice would
910-
* turn a caller's option into a 400.
935+
* repeated `state` (#6877) or `package` (#23165), so a builder that could emit
936+
* a key twice would turn a caller's option into a 400.
911937
*/
912938
function metaDeleteQuery(options?: DeleteMetaItemOptions): string {
913939
if (!options) return '';
914940
const params = new URLSearchParams();
915941
// Only `'draft'` is actionable server-side; `'active'` is the default
916942
// said out loud and sends nothing.
917943
if (options.state === 'draft') params.set('state', 'draft');
944+
// [#23293] The save path's spelling and guard (`metaSaveQuery`): an empty
945+
// id names no package, so it is never put on the wire.
946+
if (options.packageId) params.set('package', options.packageId);
918947
const qs = params.toString();
919948
return qs ? `?${qs}` : '';
920949
}
@@ -1881,7 +1910,14 @@ export class ObjectStackClient {
18811910
* @param type - Metadata type (e.g., 'object', 'plugin')
18821911
* @param name - Item name (snake_case identifier)
18831912
* @param options - {@link DeleteMetaItemOptions}: the ADR-0008 OCC pin
1884-
* (`ifMatch`) and the narrower draft-only discard (`state: 'draft'`).
1913+
* (`ifMatch`), the narrower draft-only discard (`state: 'draft'`), and
1914+
* the package whose row the delete removes (`packageId`).
1915+
*
1916+
* NAME THE PACKAGE when two packages hold the item. A delete that names
1917+
* none of an item two or more packages hold is refused `409
1918+
* RESOURCE_CONFLICT`, naming them, and nothing is deleted; pass the
1919+
* package whose row you mean as `options.packageId` (`?package=` on the
1920+
* wire), as `saveItem` names the row it writes.
18851921
*
18861922
* PIN THE RESET. This verb destroys a row: unpinned, a reset issued
18871923
* against a version somebody else has already replaced silently destroys
@@ -7710,7 +7746,8 @@ export class ScopedEnvironmentClient {
77107746
* twin takes, and reaches the SAME handler: the scoped mount is not a
77117747
* second implementation, it is one `registerForBase` call replayed
77127748
* against `/environments/:environmentId` (see `RestServer`), so this door
7713-
* reads `?state=` — and the `If-Match` header — byte-identically. A bag
7749+
* reads `?state=` and `?package=` — and the `If-Match` header —
7750+
* byte-identically. A bag
77147751
* on only one of the two clients would be a fresh divergence of the kind
77157752
* #7019 rules against, not half a fix.
77167753
*

‎packages/client/src/meta-delete-item-carriers.test.ts‎

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@
3333
* (`the withheld third carrier`) so a later "completeness" patch has to argue
3434
* with a test rather than with a comment.
3535
*
36+
* [#23293] A fourth carrier joined later: `?package=`, which the door reads
37+
* since #23165, as `packageId` in the same bag. A delete naming no package of
38+
* an item two packages hold is refused `409 RESOURCE_CONFLICT` with
39+
* `?package=` as the remedy; the `[#23293]` cases pin that the SDK can now
40+
* send it, on both declarations, and that the real door then removes that
41+
* package's row and no other.
42+
*
3643
* ## ⚠️ The instrument trap this file is shaped around
3744
*
3845
* The two declarations are TEXTUALLY IDENTICAL — `deleteItem: async (type:
@@ -257,6 +264,95 @@ describe('[#12181] meta.deleteItem carriers — ENVIRONMENT-SCOPED twin', () =>
257264
});
258265
});
259266

267+
// ---------------------------------------------------------------------------
268+
// [#23293] The fourth carrier, `?package=` — on the wire, both declarations
269+
// ---------------------------------------------------------------------------
270+
//
271+
// Since #23165 the reset door reads `?package=` (the package whose row the
272+
// delete removes) and refuses a delete that names no package of an item two
273+
// packages hold, `409 RESOURCE_CONFLICT`, prescribing `?package=`. The bag had
274+
// no member for it, so an SDK caller could not follow that remedy. Every claim
275+
// is made on BOTH declarations, each beside its negative control, for the
276+
// textual-identity reason in this file's header.
277+
278+
const PKG = 'com.qa.alpha';
279+
280+
describe('[#23293] meta.deleteItem `packageId` — UNSCOPED ObjectStackClient.meta', () => {
281+
it('a delete naming a package sends it as `?package=`', async () => {
282+
const { client, fetchMock } = createMockClient(RESET_OK);
283+
await client.meta.deleteItem('view', 'shared_grid', { packageId: PKG });
284+
expect(String(fetchMock.mock.calls[0][0])).toBe(
285+
`http://localhost:3000/api/v1/meta/view/shared_grid?package=${PKG}`,
286+
);
287+
expect(fetchMock.mock.calls[0][1].method).toBe('DELETE');
288+
// A query parameter, never a header: the header set is the unpinned one.
289+
expect(headerNamesOf(fetchMock)).toEqual(BASELINE_HEADERS);
290+
});
291+
292+
it('NEGATIVE CONTROL: a delete naming none sends no query string, and `\'\'` names none', async () => {
293+
const { client, fetchMock } = createMockClient(RESET_OK);
294+
await client.meta.deleteItem('view', 'shared_grid');
295+
await client.meta.deleteItem('view', 'shared_grid', {});
296+
await client.meta.deleteItem('view', 'shared_grid', { packageId: '' });
297+
await client.meta.deleteItem('view', 'shared_grid', { packageId: undefined });
298+
for (let i = 0; i < 4; i++) {
299+
expect(String(fetchMock.mock.calls[i][0])).toBe('http://localhost:3000/api/v1/meta/view/shared_grid');
300+
}
301+
});
302+
303+
it('rides beside `state` and `ifMatch` without disturbing either, the key sent once', async () => {
304+
const { client, fetchMock } = createMockClient(RESET_OK);
305+
await client.meta.deleteItem('view', 'shared_grid', { ifMatch: OCC_TOKEN, state: 'draft', packageId: PKG });
306+
const url = new URL(String(fetchMock.mock.calls[0][0]));
307+
expect(url.search).toBe(`?state=draft&package=${PKG}`);
308+
// The door refuses a repeated `?package=` with a 400.
309+
expect(url.searchParams.getAll('package')).toEqual([PKG]);
310+
expect(headersOfCall(fetchMock)['If-Match']).toBe(OCC_TOKEN);
311+
});
312+
313+
it('url-encodes a package id that needs it, as `saveItem` does', async () => {
314+
const { client, fetchMock } = createMockClient(RESET_OK);
315+
await client.meta.deleteItem('view', 'shared_grid', { packageId: 'acme/crm suite' });
316+
await client.meta.saveItem('view', 'shared_grid', {}, { packageId: 'acme/crm suite' });
317+
const queryOf = (i: number) => new URL(String(fetchMock.mock.calls[i][0])).search;
318+
expect(queryOf(0)).toBe('?package=acme%2Fcrm+suite');
319+
// IN STEP with the save door's address: the same id, the same bytes.
320+
expect(queryOf(0)).toBe(queryOf(1));
321+
});
322+
});
323+
324+
describe('[#23293] meta.deleteItem `packageId` — ENVIRONMENT-SCOPED twin', () => {
325+
it('a delete naming a package sends it as `?package=` on the scoped client too', async () => {
326+
const { client, fetchMock } = createMockClient(RESET_OK);
327+
await client.environment('env-123').meta.deleteItem('view', 'shared_grid', { packageId: PKG });
328+
expect(String(fetchMock.mock.calls[0][0])).toBe(
329+
`http://localhost:3000/api/v1/environments/env-123/meta/view/shared_grid?package=${PKG}`,
330+
);
331+
});
332+
333+
it('NEGATIVE CONTROL: on the scoped client a delete naming none sends no query string', async () => {
334+
const { client, fetchMock } = createMockClient(RESET_OK);
335+
const scoped = client.environment('env-123').meta;
336+
await scoped.deleteItem('view', 'shared_grid');
337+
await scoped.deleteItem('view', 'shared_grid', { packageId: '' });
338+
for (let i = 0; i < 2; i++) {
339+
expect(String(fetchMock.mock.calls[i][0])).toBe(
340+
'http://localhost:3000/api/v1/environments/env-123/meta/view/shared_grid',
341+
);
342+
}
343+
});
344+
345+
it('IN STEP with the unscoped twin: the same options put the same query on the wire', async () => {
346+
const { client, fetchMock } = createMockClient(RESET_OK);
347+
const opts = { state: 'draft', packageId: PKG } as const;
348+
await client.meta.deleteItem('view', 'shared_grid', opts);
349+
await client.environment('env-123').meta.deleteItem('view', 'shared_grid', opts);
350+
const queryOf = (i: number) => new URL(String(fetchMock.mock.calls[i][0])).search;
351+
expect(queryOf(1)).toBe(queryOf(0));
352+
expect(queryOf(0)).toBe(`?state=draft&package=${PKG}`);
353+
});
354+
});
355+
260356
describe('[#12181] the withheld third carrier', () => {
261357
it('`dropStorage` is not a member of the bag, and never reaches the wire', async () => {
262358
const { client, fetchMock } = createMockClient(RESET_OK);
@@ -631,3 +727,71 @@ describe('[#12181] the real reset door: `?state=draft` discards ONLY the pending
631727
expect(after[0].state).toBe('active');
632728
}, 60_000);
633729
});
730+
731+
describe('[#23293] the real reset door: an item two packages hold is deleted by naming its package', () => {
732+
const OTHER_PKG = 'com.qa.beta';
733+
734+
/** One item saved into two packages THROUGH THE SDK: one row per package (ADR-0048). */
735+
async function twoPackageItem(meta: ObjectStackClient['meta'] | ReturnType<ObjectStackClient['environment']>['meta'], name: string) {
736+
await meta.saveItem('view', name, VIEW(name, 'A'), { packageId: PKG });
737+
await meta.saveItem('view', name, VIEW(name, 'B'), { packageId: OTHER_PKG });
738+
}
739+
740+
/** The package each stored row of `name` is bound to, sorted. */
741+
async function packagesOf(engine: any, name: string): Promise<string[]> {
742+
return (await overlayRows(engine, name)).map((r: any) => String(r.package_id)).sort();
743+
}
744+
745+
it('NAMING NONE (the only delete the SDK could express before): refused 409 RESOURCE_CONFLICT, and nothing is deleted', async () => {
746+
const { engine, client, deleteRequests } = await bootDoor();
747+
await twoPackageItem(client.meta, 'two_pkg_probe');
748+
expect(await packagesOf(engine, 'two_pkg_probe')).toEqual([PKG, OTHER_PKG].sort());
749+
750+
const err: any = await client.meta.deleteItem('view', 'two_pkg_probe').then(
751+
() => { throw new Error('expected the package-less delete to be refused'); },
752+
(e: any) => e,
753+
);
754+
// The envelope the caller branches on, not merely that something threw.
755+
expect(err.code).toBe('RESOURCE_CONFLICT');
756+
expect(err.httpStatus).toBe(409);
757+
expect(await packagesOf(engine, 'two_pkg_probe')).toEqual([PKG, OTHER_PKG].sort());
758+
// The probe: no package reached the protocol.
759+
expect(deleteRequests).toHaveLength(1);
760+
expect(deleteRequests[0]).not.toHaveProperty('packageId');
761+
}, 60_000);
762+
763+
it('NAMING ONE: `{ packageId }` removes that package\'s row and no other', async () => {
764+
const { engine, client, deleteRequests } = await bootDoor();
765+
await twoPackageItem(client.meta, 'two_pkg_probe');
766+
767+
const reset: DeleteMetaItemResponse = await client.meta.deleteItem('view', 'two_pkg_probe', { packageId: PKG });
768+
769+
expect(reset.success).toBe(true);
770+
expect(reset.reset).toBe(true);
771+
expect(await packagesOf(engine, 'two_pkg_probe')).toEqual([OTHER_PKG]);
772+
// POSITIVE CONTROL for the previous case's absence: the same probe sees
773+
// the package arrive, read off `?package=` by the door.
774+
expect(deleteRequests).toHaveLength(1);
775+
expect(deleteRequests[0].packageId).toBe(PKG);
776+
}, 60_000);
777+
778+
it('the ENVIRONMENT-SCOPED twin: refused naming none, and naming one removes only that row', async () => {
779+
const { engine, client, deleteRequests } = await bootDoor();
780+
const scoped = client.environment('env-123').meta;
781+
await twoPackageItem(scoped, 'scoped_two_pkg');
782+
783+
const err: any = await scoped.deleteItem('view', 'scoped_two_pkg').then(
784+
() => { throw new Error('expected the scoped package-less delete to be refused'); },
785+
(e: any) => e,
786+
);
787+
expect(err.code).toBe('RESOURCE_CONFLICT');
788+
expect(err.httpStatus).toBe(409);
789+
expect(await packagesOf(engine, 'scoped_two_pkg')).toEqual([PKG, OTHER_PKG].sort());
790+
expect(deleteRequests[0]).not.toHaveProperty('packageId');
791+
792+
const reset: DeleteMetaItemResponse = await scoped.deleteItem('view', 'scoped_two_pkg', { packageId: OTHER_PKG });
793+
expect(reset.reset).toBe(true);
794+
expect(await packagesOf(engine, 'scoped_two_pkg')).toEqual([PKG]);
795+
expect(deleteRequests[1].packageId).toBe(OTHER_PKG);
796+
}, 60_000);
797+
});

0 commit comments

Comments
 (0)