You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 6e8ec73
Browse filesBrowse the repository at this point in the historyBrowse files
feat(cloud-connection): count an installed app's remaining sample rows the purge's way (#30613)
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wg1b5CXBP4Wj2cbsMH4wGP31)_
domain:server#2 · os-bill · session_01Utp6CYE3Av1KFxjNwT6qzN
## 维护者速读
- 改了什么:新增只读接口 `GET
/api/v1/marketplace/install-local/:manifestId/sample-data`,回答「这个已装应用的示例数据现在还剩多少条」,即此刻点「清除示例数据」会删掉的条数。清除本身一行没动。
- 为什么改:#30323 的 ui
半边已落地,但只在本次浏览器会话里知道装了多少条;刷新后没有任何接口能给出数量,清除确认框也就说不出会删几条。
- 风险与回滚:只读,不删数据;和清除共用同一个匹配器、同一道 `manage_metadata`
权限,所以数量和清除结果一定一致。任何查询参数都会被 400 拒绝,GET 请求无论怎么写都删不了数据。回滚即还原本 PR。
- 先看:`marketplace-install-local-plugin.ts` 的
`handleSampleDataCount`;`marketplace-install-local-purge.ts` 的
`countSeedRows`;测试文件末尾新增的 `GET …/sample-data` describe。
- 两分钟验证:`pnpm --filter @objectstack/cloud-connection exec vitest run
--maxWorkers=2 src/marketplace-install-local-purge.test.ts`,预期 33 个全绿,其中
`GET …/sample-data` 下 7 个。
- 需要你做的:无,席位落地。
Fixes#30323
Clause-②: yes (widening)
Metadata-protocol: none
## What changes
After a reload, no API said how much sample data an installed app still
had. The install answer's `data.seeded` is gone, the purge has no dry
run, and the listing's `withSampleData` stops at the first seed row it
finds and answers yes or no. So the console's purge confirm could only
give a count for an install made in the same browser session (PR #30537,
the ui half of this card).
- **`GET /api/v1/marketplace/install-local/:manifestId/sample-data`**
(new, read-only) answers `{ success: true, data: { manifestId, count,
unidentified } }`.
- `count` is the number of rows `POST …/purge-sample-data` would delete
now: `countSeedRows`, which runs the purge's own `matchSeedRows` pass in
full (not `firstOnly`) and deletes nothing.
- `unidentified` is the seed records the purge would report in `errors`
and leave in place (no usable key, a key two rows carry, an object whose
rows could not be read). Each is logged at `warn` with its reason.
- **Same rules as the purge, in the same order:** `manage_metadata`
through the shared `requireInstallCapability` (401 / held-session 403 /
403 `FORBIDDEN`), then `404` for no install, the unreadable-ledger
answer, the protocol handshake's `422 OS_PROTOCOL_INCOMPATIBLE`, then
the purge's scope (`organizationWallActive` + `resolveActiveOrgId`).
- **Where it differs from the purge, because it is a read:** a walled
session with no active organization gets `200, count: 0` and no row is
read (ADR-0123 D2 for a tenant-scoped read; the listing answers
`withSampleData: false` to the same session), where the purge refuses
`403`. A package with no seed datasets gets `count: 0`, where the purge
answers `400 NOTHING_TO_PURGE`. No engine or metadata service gets the
purge's `500 DRIVER_UNAVAILABLE`, never a `0`.
- **It cannot delete.** It is a `GET` with an empty closed query set
(`SAMPLE_DATA_COUNT_QUERY_PARAMS`, AGENTS.md route rule 5): `?dryRun=0`,
`?confirm=true` or any other name is refused `400 VALIDATION_ERROR` with
the shared located sentence (`unknownQueryParamRefusalMessage` from
`@objectstack/rest`), before any row is read.
- **Route ledger:** one `server-only` row beside the purge in
`cloud-connection-route-ledger.ts`.
- **Dependency:** `@objectstack/cloud-connection` now depends on
`@objectstack/rest` (workspace) for that refusal sentence;
`pnpm-lock.yaml` gains the three importer lines. No cycle: `rest` does
not depend on `cloud-connection`, and `runtime`, which
`cloud-connection` already depends on, already depends on `rest`.
- **Unchanged:** what the purge deletes, its gate and its answer; the
listing and its `withSampleData`; the install door. No `packages/spec`
change.
### Why a separate read, not a count on the listing
The seat ruling asked to measure both and take the smaller. The listing
answers every installed package on every visit to Installed Apps, for
every signed-in caller, and today reads only until the first seed row
(`firstOnly`). A count has to read every seeded object's rows for every
package on each listing call. The new door does that for one package,
when the confirm needs it, behind the purge's gate. A `?dryRun=1` switch
on the purge was rejected because a misspelled switch on a `POST` that
deletes would delete.
### What "an edited sample record is kept" means
The purge matches a row by its dataset's `externalId` key. A sample
record whose **key field** was edited is no longer a seed row: it is
neither deleted nor counted. A sample record edited only in **other
fields** still carries its seed key: the purge deletes it, and the count
includes it. The card's observation ("kept a sample record I had
edited") holds for a key edit. The count follows the purge either way,
because it is the same matcher; the pins below cover all three cases.
## Tests
Pins go into the existing harness files as new top-level describes.
`marketplace-install-local-purge.test.ts`:
- `countSeedRows`: counts exactly the rows `purgeSeedRows` then deletes,
deleting nothing itself; a key two rows carry is `unidentified`, equal
to the purge's `errors`, and is not counted.
- `GET …/sample-data`, through the booted plugin with the real
`SeedLoaderService`:
- install with samples gives count 2; add a user record, edit the sample
contact's key and the sample account's non-key field, and the count is
1; the purge then deletes exactly that one row; afterwards the count is
0 and the listing says `withSampleData: false`;
- wall with an active organization: counts only that organization's
rows, every read pinned to it;
- wall with no active organization: `200, count 0`, no seed row read;
- a package without seed datasets: `count 0`, no row read;
- `?dryRun=0` and `?confirm=true`: `400 VALIDATION_ERROR` naming the
parameter, nothing read, nothing deleted;
- no install: `404`; no metadata service: `500 DRIVER_UNAVAILABLE`;
- signed in without `manage_metadata`: `403 FORBIDDEN` naming the
capability, no seed row read.
`marketplace-install-local-sample-data-not-loaded.test.ts`: on an entry
the rehydrate refused, the count answers the purge's `422` byte for byte
and changes nothing; the loadable entry beside it counts 1.
`cloud-connection-auth-gate.pin.test.ts` and
`marketplace-install-local-api-key-account-posture.pin.test.ts`: the
door joins the census (now six), so a held session and a held owner's
API key are refused `403` with the gate's code, an anonymous caller
`401`, and the un-held operator is served.
## Acceptance notes
- The console does not call the new door yet. That is the ui follow-up
the seat files: the purge confirm and the banner can read `count` on any
visit.
- No hand-written doc becomes false.
`content/docs/deployment/marketplace-handbook.mdx` step 6 names the
reseed and purge routes and says both need `manage_metadata`, which
still holds. Adding the new route there is left to the ui follow-up,
when the console shows the count.
- The file surface grew by `marketplace-install-local-plugin.ts` (told
to the seat first; handler and one mount line only, the install door and
its `heldBy` read untouched), the two auth-pin test files whose route
census must list the new door, `packages/cloud-connection/package.json`
and `pnpm-lock.yaml` (the `@objectstack/rest` dependency), and
`packages/cloud-connection/vitest.config.ts` (its source alias).
- Changeset: `patch`, because `Metadata-protocol: none` (AGENTS.md
Post-Task Checklist step 3: the level follows the kind; a non-protocol
widening ships `patch`).
## Verification
All on head `00d0c9d5` (`origin/main` `ad2393f7` merged at `05371373`;
the merge touched no `packages/cloud-connection` file; the dependency
closure was rebuilt after it).
<details><summary>Tests and typecheck</summary>
- `pnpm --filter @objectstack/cloud-connection exec vitest run
--maxWorkers=2` under the verify lock: Test Files 70 passed (70), Tests
888 passed (888).
- `pnpm --filter @objectstack/cloud-connection typecheck` (both `tsc`
programs, tests included): exit 0.
- `eslint --no-inline-config` over the seven changed `.ts` files: exit
0, no finding. The repo-wide `pnpm lint` is left to CI.
</details>
<details><summary>Ablations (scripts/ablation-replace.mjs, each
restored, tree clean after each)</summary>
The suites import the plugin and the matcher from `src/`, so no rebuild
sits between the mutation and the run.
1. `countSeedRows` passes `firstOnly: true` (the listing's early stop):
2 red, the unit "counts exactly the rows the purge then deletes" and the
route "counts N … purge then deletes exactly the count".
2. The query-parameter refusal deleted: 1 red, "a query parameter is
refused with a located 400".
3. The no-active-organization `count: 0` answer deleted: 1 red, "wall,
no active organization: 200 with count 0".
4. The `requireInstallCapability` admission deleted: 4 red, the
`manage_metadata` 403 pin, both held-session census cases and the
anonymous 401 case in `cloud-connection-auth-gate.pin.test.ts`.
5. The `DRIVER_UNAVAILABLE` branch disabled: 1 red, "no metadata
service: the purge's 500 DRIVER_UNAVAILABLE, never a 0".
</details>
<details><summary>Gates</summary>
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` on `00d0c9d5`: 75 families, each run with
its exit code captured before any pipe, all exit 0. `--ran`: 75 derived,
75 run, 0 NOT-MEASURED, 0 UNRUN.
On the first run `check:test-source-alias` was red: the new
`@objectstack/rest` import resolved to `rest/dist` in the tests. Fixed
by an anchored alias in `packages/cloud-connection/vitest.config.ts`, as
the gate prescribes.
</details>
Full dogfood battery: n/a. The title has no `!` and `Clause-②` has no
`(narrowing)` arm. CI's Dogfood Regression Gate runs on the PR.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Utp6CYE3Av1KFxjNwT6qzN)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
feat(cloud-connection): `GET /api/v1/marketplace/install-local/:manifestId/sample-data` answers how many of an installed app's sample records a purge would delete now
6
+
7
+
Clause-②: yes (widening)
8
+
Metadata-protocol: none
9
+
10
+
-**New read-only route.**`GET /api/v1/marketplace/install-local/:manifestId/sample-data` answers `{ success: true, data: { manifestId, count, unidentified } }`. `count` is the number of rows `POST …/purge-sample-data` would delete if it ran now, found by the purge's own matcher with nothing deleted. A record a user added is not counted, and neither is a sample record whose key field (its dataset's `externalId`) was edited, because the purge deletes neither. A sample record edited only in other fields is still a sample row: the purge deletes it and the count includes it. `unidentified` counts the sample records the purge would report in `errors` and leave in place (no usable key value, or a key two rows carry). Before this, the count was known only from the install answer's `data.seeded`, so after a reload no API could say how much sample data was left; the installed-apps listing's `withSampleData` says only whether any is left.
11
+
-**Same rules as the purge.** It requires `manage_metadata`, answers `404` for a package that is not installed and `422 OS_PROTOCOL_INCOMPATIBLE` for an entry this runtime refused to load, and counts in the purge's scope (the caller's active organization under an organization wall, every row without one). A walled session with no active organization gets `count: 0`, the same answer as the listing's `withSampleData: false` for that session. A package with no seed datasets gets `count: 0`. With no data engine or metadata service it answers `500 DRIVER_UNAVAILABLE`, never `0`.
12
+
-**It cannot delete.** It is a `GET` and accepts no query parameter: any parameter (`dryRun`, `confirm`, …) is refused with `400 VALIDATION_ERROR` before any row is read.
13
+
-**Unchanged.** What the purge deletes, its permission and its answer; the listing's `withSampleData`.
14
+
-`@objectstack/cloud-connection` now depends on `@objectstack/rest`, for the shared closed-query-set refusal message.
0 commit comments