Repository navigation
Commit d7fff21
fix(cloud-connection): install-local purge deletes seed rows through the engine by their seed key (#21773)
Fixes #21728
Clause-②: no
`POST /api/v1/marketplace/install-local/:manifestId/purge-sample-data`
answered `500 DRIVER_UNAVAILABLE` on every runtime. It now deletes an
installed package's seed rows through the ObjectQL engine, matched by
the seed's own key, in the install's own scope.
## What was wrong (three defects, stacked)
1. The handler looked up a bare `driver` service. No kernel registers
that name; drivers register as `driver.NAME`. So the door always
refused.
2. Behind the lookup, it matched seed records by `rec.id`. The CRM
example's 28 records carry no `id` (they key by `name` / `email` /
`subject`), so fixing the lookup alone answers `deleted: 0, skipped:
28`. Ablation leg 2 below measures exactly that.
3. It called the driver's `delete` directly, past every engine hook and
the audit trail.
The unit suites stayed green because four of them mocked a bare `driver`
service.
## What changes
- **New
`packages/cloud-connection/src/marketplace-install-local-purge.ts`
(`purgeSeedRows`).**
- **Key.** Each dataset's `externalId` is read through the spec's own
declaration (`SeedSchema.shape.externalId`, default `name`). Rows are
keyed the way `SeedLoaderService` keys them for an upsert. A row whose
key no seed record declares is never touched.
- **Composite keys.** Reference parts (the showcase's `['team',
'project']` junction) are translated through the parent rows this purge
matched, as the loader does for same-load targets.
- **No guessing.** A key carried by more than one row in scope, a seed
record with no key value, a key part pointing at an object this package
does not seed, and an engine refusal each count in `errors`, with the
reason logged. None of them counts as `skipped`, and none deletes
anything.
- **Order.** The reverse of the loader's own topological insert order
(`SeedLoaderService.buildDependencyGraph`), so children go before
parents. No order is written by hand.
- **Context.** `engine.delete(object, { where: { id } })` under
`SEED_WRITE_EXECUTION_CONTEXT`, the posture the seed was written with.
Record-change automation is suppressed; lifecycle hooks and audit run.
- **`handlePurge`** resolves `objectql` and `metadata` (`500
DRIVER_UNAVAILABLE` if either is missing, now naming which). It picks
the scope with the install's own two primitives,
`organizationWallActive` and `resolveActiveOrgId`, and hands both to
`purgeSeedRows`. The docblock's "bypass ACL / lifecycle hooks (same
pattern as cloud purge)" sentence is gone. The response shape is
unchanged.
- **Bare-`driver` mocks.**
- Rewritten against the engine, with a pinned `delete` that opens with
`assertEngineDeleteDispatch`: heal and capability-enumeration, the two
suites that drive the purge.
- Removed, with the reason left in place: seed-replayer and
tenancy-posture, where the mock was dead fixture.
- **Changeset:** `patch` on `@objectstack/cloud-connection`. This is a
bug fix in a released package. It adds no export and changes no schema;
the door now does what its documented response says.
## Zone 2 measurements
- **A1, reach (`origin/main` 025008a, unchanged code).**
- Setup: a scratch copy of the showcase on `os dev --fresh` with
`OS_CLOUD_URL=off`, then `os package install
examples/app-crm/dist/objectstack.json` as the admin. Result: 3/3/12/5/5
rows, all carrying the default organization.
- `POST …/purge-sample-data {}` answered `500
{"code":"DRIVER_UNAVAILABLE",…}`. Afterwards `crm_account` still held 3
rows and `crm_opportunity` 12.
- The reseed answered `422 RESEED_NO_ROWS`.
- The same sequence on the fixed build:
- The purge answered `200 {"deleted":28,"skipped":0,"errors":0}` and the
user row `User Authored Co` survived.
- `sys_audit_log` gained 28 `delete` rows.
- The reseed answered `200 inserted: 28`. A second purge deleted 28, and
a third answered `skipped: 28`.
- **A2, scope.** The install and the reseed seed under
`organizationWallActive(ctx)`.
- Under a wall: they seed into `resolveActiveOrgId`'s organization, and
the loader pins `organization_id` on both its upsert match and its
writes.
- Without a wall (`single`, the card's boot): no organization is pinned,
and the loader's upsert match is table-wide. Measured at the door: `POST
/auth/organization/create` on that boot answers `403 "Creating
additional organizations is disabled on this deployment."`, so the table
is one tenant.
- The purge reuses those two primitives exactly. Under a wall, every
read is filtered to the active organization and the delete is by primary
key. A walled session with no active organization gets the reseed's skip
shape: `400 RESEED_SKIPPED`, `Purge did not run:
multi-tenant-no-active-org…`.
- Pinned on a real walled boot: a purge in organization A leaves
organization B's 28 seed rows untouched, and no delete hook ever saw one
of them.
- **A3, key.** The CRM's five datasets declare `externalId` `name` /
`email` / `name` / `email` / `subject` (3+3+12+5+5 = 28), with 0
authored ids. The loader writes the key to that same field
(`loadDataset`: `dataset.externalId || 'name'`; `externalIdKey`). The
purge matches on exactly that field, in scope.
- **A4, through the engine.**
- Context: `SEED_WRITE_EXECUTION_CONTEXT` (`isSystem`, `skipTriggers`,
`seedReplay`). `triggerHooks` runs every lifecycle hook unless
`skipAutomations` is set.
- Real boot: `beforeDelete` and `afterDelete` each fired once per seed
row (28), and the audit plugin wrote 28 `delete` rows.
- Order: the loader's graph reversed; the route test proves it against
the real `SeedLoaderService` with a manifest listing the child first.
- Constraint, measured: with a user opportunity still requiring seed
account `Acme Corp` (`crm_opportunity.account` is required +
`set_null`), the engine refused that account's delete ("still referenced
by 1 Opportunity record(s)… required and cannot be cleared"). The purge
answered `deleted: 27, errors: 1` with the reason logged, and the user's
row was untouched.
- No CRM hook refuses a delete; its only hook is
`beforeInsert`/`beforeUpdate`.
- **A5, ledger flag.** `sampleDataPurged` / `withSampleData` stay
install-wide; the ledger shape is unchanged here.
- Measured on a walled boot with a real restart: after a purge in A,
`GET /install-local` read as B answers `withSampleData: false` while B
still holds its 28 rows.
- After the restart, A has 0 rows and B has 28. The healer returns at
its wall check before reading anything, so it neither resurrects A nor
touches B.
- No data harm to B. The listing flag is wrong for B, and that is
reported to the seat as an out-of-scope finding.
- **A6, reverse verification.** Both legs ran through
`scripts/ablation-replace.mjs` on `marketplace-install-local-purge.ts`,
which the dogfood resolves through its source alias, so no build leg was
needed.
| leg | anchor → replacement | blob | dogfood pin |
|---|---|---|---|
| delete no-op | `const result = await
engine.delete(…SEED_WRITE_EXECUTION_CONTEXT });` → `const result = true
as boolean \| number;` | a6856b9 → 2d004c57 | 5 red / 3 green |
| match by `rec.id` | `const ids = rowsByKey.get(keyOf(parts)) ?? [];` →
`const ids: string[] = rec?.id ? [String(rec.id)] : [];` | a6856b9 →
7e1e9c83 | 5 red / 3 green; purge answered `deleted: 0, skipped: 28` |
- The 3 that stayed green both times are the two preconditions and
"organization B's seed rows are untouched".
- Restore, both legs: blob after restore == HEAD (a6856b9) and `git
diff HEAD` is empty.
## Tests (at 2b140e9)
- `pnpm --filter @objectstack/cloud-connection exec vitest run`: 37
files, 459 tests pass. The new `marketplace-install-local-purge.test.ts`
has 16 cases (match, no-guessing, order/context/scope, reference parts,
and the route under the real loader).
- `pnpm --filter @objectstack/cloud-connection typecheck`: clean (both
programs).
- New dogfood `install-local-purge-sample-data.dogfood.test.ts` on a
real boot: 8/8 pass. `pnpm --filter @objectstack/dogfood typecheck` is
clean.
- `pnpm lint` (full, `eslint . --no-inline-config`): exit 0.
## Gates (at 2b140e9)
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 86 families (the claim's 46 grew
with the dogfood wiring, the lockfile and the ratchet baseline).
- All 86 ran with exit 0. `--ran` reconciles 86 derived / 86 run / 0
NOT-MEASURED, every line carrying its exit code.
- `check:dual-build-cjs-loads` first answered PREREQUISITE NOT MET
(eight unrelated packages unbuilt). After building them it read 106
entry points across 66 packages, green.
- `check:slot-lookup` ratcheted down 16 → 15 for the plugin file (the
untyped `driver` lookup is gone). The baseline is committed.
## Deviations
- **Two stay-out dogfood files edited:**
`packages/qa/dogfood/package.json` (devDependency
`@objectstack/cloud-connection`) and
`packages/qa/dogfood/vitest.config.ts` (one anchored source alias), plus
their lockfile importer lines.
- Measured reason: a relative import of the plugin from the new dogfood
file puts the plugin's dynamic `@objectstack/runtime` import into
dogfood's shrink-only `check:test-source-alias` set (`NEW unaliased
artifact import(s): @objectstack/runtime`).
- The alias is the route the config already takes for its other
subjects. No test file another lane holds was touched.
- **The purge door gains a refusal it did not have:** `400
RESEED_SKIPPED` when a wall is up and the session has no active
organization. This was dispatched as "reseed's existing skip shape and
code". A dedicated code would need a spec ledger row (`domain:spec`).
- **`@objectstack/cloud-connection` gains a devDependency** on
`@objectstack/metadata-core`, for `assertEngineDeleteDispatch` in its
fake engines, as `check:engine-double-contract` prescribes. It is
aliased to source in its vitest config.
- **No merge commit.** `origin/main` stayed at 025008a from dispatch
to this PR, so `git merge origin/main` is a no-op.
## Acceptance notes
- Boundaries of the new purge (reported, not filed):
- A seed dataset keyed by `id` and replayed into a second organization
gets derived ids (`perOrganizationSeedRowId`, private to the loader), so
the purge answers `skipped` for those rows. No install-local package
measured has one.
- Master-detail children of seed rows go with them by their declared
`cascade`.
- `withSampleData: false` is written even when `errors` is non-zero,
which is the pre-existing flip.
- Comment drift for the spec seat: in `error-code-ledger.zod.ts`,
`DRIVER_UNAVAILABLE`'s note says "no driver service", and
`RESEED_SKIPPED`'s says "reseed declined", but the purge now emits both.
- Checklist drift: `docs/qa/platform-checklist/areas/platform-core.json`
still describes the purge as id-based.
- #21762 edits this file's install route. Install and reseed behaviour
are untouched here.
---
_Generated by [Claude
Code](https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent c9c555a commit d7fff21
15 files changed
Lines changed: 1180 additions & 43 deletions
File tree
- .changeset
- packages
- cloud-connection
- src
- qa/dogfood
- test
- scripts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
| 35 | + | |
35 | 36 | | |
36 | 37 | | |
37 | 38 | | |
| |||
Lines changed: 31 additions & 7 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
24 | | - | |
| 24 | + | |
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
| |||
73 | 73 | | |
74 | 74 | | |
75 | 75 | | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
76 | 84 | | |
77 | 85 | | |
78 | 86 | | |
79 | 87 | | |
| 88 | + | |
80 | 89 | | |
81 | 90 | | |
82 | 91 | | |
| |||
138 | 147 | | |
139 | 148 | | |
140 | 149 | | |
141 | | - | |
| 150 | + | |
142 | 151 | | |
143 | 152 | | |
144 | 153 | | |
| |||
155 | 164 | | |
156 | 165 | | |
157 | 166 | | |
158 | | - | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
159 | 180 | | |
160 | 181 | | |
161 | 182 | | |
| |||
167 | 188 | | |
168 | 189 | | |
169 | 190 | | |
170 | | - | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
171 | 196 | | |
172 | | - | |
173 | 197 | | |
174 | 198 | | |
175 | 199 | | |
| |||
189 | 213 | | |
190 | 214 | | |
191 | 215 | | |
192 | | - | |
| 216 | + | |
193 | 217 | | |
194 | 218 | | |
195 | 219 | | |
| |||
254 | 278 | | |
255 | 279 | | |
256 | 280 | | |
257 | | - | |
| 281 | + | |
258 | 282 | | |
259 | 283 | | |
260 | 284 | | |
| |||
Lines changed: 34 additions & 3 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
30 | 30 | | |
31 | 31 | | |
32 | 32 | | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
33 | 42 | | |
34 | 43 | | |
35 | 44 | | |
| |||
46 | 55 | | |
47 | 56 | | |
48 | 57 | | |
| 58 | + | |
49 | 59 | | |
50 | 60 | | |
51 | 61 | | |
| |||
101 | 111 | | |
102 | 112 | | |
103 | 113 | | |
104 | | - | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
105 | 121 | | |
106 | 122 | | |
107 | 123 | | |
108 | 124 | | |
109 | 125 | | |
110 | 126 | | |
111 | 127 | | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
112 | 137 | | |
113 | 138 | | |
114 | | - | |
115 | 139 | | |
116 | 140 | | |
117 | 141 | | |
| |||
211 | 235 | | |
212 | 236 | | |
213 | 237 | | |
214 | | - | |
| 238 | + | |
| 239 | + | |
215 | 240 | | |
216 | 241 | | |
217 | 242 | | |
| |||
220 | 245 | | |
221 | 246 | | |
222 | 247 | | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
223 | 252 | | |
224 | 253 | | |
225 | 254 | | |
226 | 255 | | |
227 | 256 | | |
228 | 257 | | |
| 258 | + | |
| 259 | + | |
229 | 260 | | |
230 | 261 | | |
231 | 262 | | |
| |||
Lines changed: 60 additions & 28 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
97 | 97 | | |
98 | 98 | | |
99 | 99 | | |
| 100 | + | |
100 | 101 | | |
101 | 102 | | |
102 | 103 | | |
| |||
1683 | 1684 | | |
1684 | 1685 | | |
1685 | 1686 | | |
1686 | | - | |
1687 | | - | |
1688 | | - | |
1689 | | - | |
1690 | | - | |
| 1687 | + | |
| 1688 | + | |
| 1689 | + | |
| 1690 | + | |
| 1691 | + | |
| 1692 | + | |
| 1693 | + | |
| 1694 | + | |
| 1695 | + | |
| 1696 | + | |
| 1697 | + | |
| 1698 | + | |
| 1699 | + | |
| 1700 | + | |
| 1701 | + | |
| 1702 | + | |
| 1703 | + | |
| 1704 | + | |
| 1705 | + | |
| 1706 | + | |
| 1707 | + | |
| 1708 | + | |
1691 | 1709 | | |
1692 | 1710 | | |
1693 | 1711 | | |
| |||
1718 | 1736 | | |
1719 | 1737 | | |
1720 | 1738 | | |
1721 | | - | |
1722 | | - | |
1723 | | - | |
| 1739 | + | |
| 1740 | + | |
| 1741 | + | |
| 1742 | + | |
| 1743 | + | |
| 1744 | + | |
| 1745 | + | |
1724 | 1746 | | |
1725 | 1747 | | |
1726 | | - | |
| 1748 | + | |
| 1749 | + | |
| 1750 | + | |
| 1751 | + | |
1727 | 1752 | | |
1728 | 1753 | | |
1729 | 1754 | | |
1730 | | - | |
1731 | | - | |
1732 | | - | |
1733 | | - | |
1734 | | - | |
1735 | | - | |
1736 | | - | |
1737 | | - | |
1738 | | - | |
1739 | | - | |
1740 | | - | |
1741 | | - | |
1742 | | - | |
1743 | | - | |
1744 | | - | |
1745 | | - | |
1746 | | - | |
1747 | | - | |
| 1755 | + | |
| 1756 | + | |
| 1757 | + | |
| 1758 | + | |
| 1759 | + | |
| 1760 | + | |
| 1761 | + | |
| 1762 | + | |
| 1763 | + | |
| 1764 | + | |
| 1765 | + | |
| 1766 | + | |
| 1767 | + | |
1748 | 1768 | | |
| 1769 | + | |
1749 | 1770 | | |
1750 | 1771 | | |
| 1772 | + | |
| 1773 | + | |
| 1774 | + | |
| 1775 | + | |
| 1776 | + | |
| 1777 | + | |
| 1778 | + | |
| 1779 | + | |
| 1780 | + | |
| 1781 | + | |
| 1782 | + | |
1751 | 1783 | | |
1752 | 1784 | | |
1753 | 1785 | | |
| |||
1757 | 1789 | | |
1758 | 1790 | | |
1759 | 1791 | | |
1760 | | - | |
| 1792 | + | |
1761 | 1793 | | |
1762 | 1794 | | |
1763 | 1795 | | |
| |||
0 commit comments