Repository navigation
fix(service-datasource): name the reason and the bound objects in the datasource delete refusal - #30513
Conversation
… datasource delete refusal The refusal to delete a datasource that objects still use answered only an English sentence with a count. It now carries error.details with reason objects_bound, the count and the sorted object names, taken from the same read as the count. The admin routes relay details only from a refusal that declared the route's own 400 envelope; every other 400 is unchanged. Claude-Session: https://claude.ai/code/session_01Utp6Cb4nPCxmCZ7EzB1A1v Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 3 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin be51654bae67cc0714f8f696dcad41cfcd203c5c && git checkout be51654bae67cc0714f8f696dcad41cfcd203c5c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f6faf7511635fc36509e1c8b61d8f0446f3b42c1 f4cc8056838b82ca26ff8b4c9760b18a179e9656 && git checkout -B drift-repro f6faf7511635fc36509e1c8b61d8f0446f3b42c1 && git merge --no-ff f4cc8056838b82ca26ff8b4c9760b18a179e9656
node scripts/docs-audit/affected-docs.mjs --json f6faf7511635fc36509e1c8b61d8f0446f3b42c1
|
|
domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL 维护者速读
席位核对 — ACCEPT · domain:server#2 · head
|
Requested by Bill · project thread
domain:server#2 · os-bill · session_01Utp6Cb4nPCxmCZ7EzB1A1v
Fixes #30444
Clause-②: yes (widening) — the bound-object refusal keeps
400DATASOURCE_ADMIN_ERRORand its message, and gainserror.details; nothing a client reads today moves.Metadata-protocol: none
维护者速读
error.details里带上机器可读的原因objects_bound、数量和这些对象的名字(排好序的完整列表)。状态码、错误码和英文原句都不变。details,路由只转发自己声明了 400 信封的拒绝的details,驱动错误身上碰巧有details也不会漏出去。回滚即撤本 PR。packages/services/service-datasource/src/admin-routes.ts的badRequest(转发details的三重条件);datasource-admin-service.ts的boundObjectsError与removeDatasource;datasource-admin-plugin.ts的countBoundObjectsDiagnosed(名字与数量同出一次读取)。pnpm --filter @objectstack/service-datasource exec vitest run src/__tests__/admin-routes.test.ts,预期 1 个文件全绿,其中#30444一组 5 条。What changes
The rule: the bound-object refusal says why and names what is in the way, in
error.details, from the read that produced its count.DatasourceAdminService.removeDatasource(@objectstack/service-datasource). The bound-object refusal is built byboundObjectsError: the same message (Cannot remove datasource 'NAME': N object(s) are still bound to it.), pluscode: 'DATASOURCE_ADMIN_ERROR',status: 400anddetails: { reason: 'objects_bound', count, objects }.objectsis the full list of bound object names, sorted; no cap (the list is bounded by the objects bound to one datasource).DatasourceAdminServiceConfig.countBoundObjectsDiagnosedmay now returnobjects?: string[], and the plugin wiring returns the names of the same filteredlistObjects()rows it counts. The count in the message,details.countanddetails.objectsall come from that one result. ⛔ NotlistObjectsBoundTo: that is a second scan over another registry (the engine's), and a refusal built from two reads could say "2 objects" and name three. A host that supplies onlycountBoundObjects(no diagnosed read) gets{ reason, count }withoutobjects.badRequestinadmin-routes.ts. A thrown error whosestatusis400, whosecodeis the route's ownSERVICE_ERROR_CODE[service]and whosedetailsis a plain object is answered with thatdetails. All three must hold, the same pattern as the existing503and404relays, so a driver or library error that carries a straydetailsfield keeps answering the bare message. Every other 400 answers exactly as before.details.reasonspelling. snake_case, following the envelope's existingdetails.reasonvalues inplugin-auth(invalid_body,user_not_found,cross_organization, …) and AGENTS.md's rule for machine-name data values.service-settingsspells its one reason in kebab-case (locked-by-env); this package had no priordetails.reason.packages/specuntouched), the English message, the degraded-read refusal (503SERVICE_UNAVAILABLE,metadataIncompleteError), the code-defined refusal, the 204 delete. TheIDatasourceAdminService.removeDatasourcedocblock names the newdetails. No hand-written doc page states this refusal (searchedcontent/docsoutsidereferences/andreleases/for the message and the code).Why
#30444: the refusal was an English sentence with a count and no machine-readable reason or names, so no client could tell it from any other 400 of this family or name the objects without re-reading metadata (PR #30442 does exactly that in Setup).
Verification
Measured first, at
6c4c43ab(realDatasourceAdminService+ realregisterDatasourceAdminRoutesonHonoHttpServer, a runtime datasource with two bound objects and a code-defined one):DELETE /api/v1/datasources/parts_db→400{"success":false,"error":{"code":"DATASOURCE_ADMIN_ERROR","message":"Cannot remove datasource 'parts_db': 2 object(s) are still bound to it."}}, nodetails.DELETE /api/v1/datasources/code_db→400{"success":false,"error":{"code":"DATASOURCE_ADMIN_ERROR","message":"Datasource 'code_db' is code-defined and cannot be removed at runtime."}}.badRequestforwarded the message alone (sendError(res, 400, code, message), noextra).Tests
admin-routes.test.ts› new top-level describeDELETE /datasources/:name — the bound-object refusal carries details (#30444), over the real service and routes: two bound objects answer400DATASOURCE_ADMIN_ERROR, the unchanged message anddetails { reason: 'objects_bound', count: 2, objects: ['bin', 'part'] }(sent unsorted; the harness'slistObjectsBoundToanswers a different name on purpose), and the record survives; a count-only host answers{ reason, count }; nothing bound deletes with204; the code-defined 400 carries nodetails(exact body); an error with a straydetailsand no 400 envelope answers the bare message (exact body).datasource-removal-bound-count-outage.test.ts› the plugin-wiring describe: the diagnosed read now also assertsobjects(['wh_line', 'wh_order']), and the predating-listDiagnosedcase's exacttoEqualgainsobjects: ['wh_order']. Every other assertion in that file is unchanged and green, the degraded 503 ones included.pnpm --filter @objectstack/service-datasource test: Test Files 71 passed (71), Tests 1174 passed (1174), atbba4b00fand again atf4cc8056(after mergingmain, 14 commits, none in this package or its route dependencies).typecheck: exit 0 (the package tsconfig compilessrc/__tests__, counted with--listFiles).Ablations (scripts/ablation-replace.mjs; the suite imports src directly, no build between legs)
badRequestrelaysdetailsfrom any error (drop thestatus/codeconjuncts)detailspin red (1 failed)badRequest's default 400 branch attaches adetailsdetailspin and the existingsurfaces lifecycle errorsexact-body pin red (3 failed)listObjectsBoundTo(a second scan) instead of the count's readbadRequeststops forwardingdetailsEach mutation landed (anchor 1 to 0, replacement 0 to 1); each restore left
git diff HEADempty.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackonbba4b00f: 62 families, the same 62 onf4cc8056. All 62 run, exit captured per line before any pipe: 60 exit 0 on the first pass;check-plugin-teardown-shape --self-testexit 3 (shallow clone, pinned fixture commit fetched, rerun exit 0);check:dual-build-cjs-loadsexit 3 (nodist, full build under the lock, rerun exit 0).--ran: 62 derived, 62 run, 0 NOT-MEASURED.pnpm lint(eslint . --no-inline-config): exit 0 onf4cc8056.bba4b00f(after a fullturbo run build): VERDICT command-exit 0, Test Files 419 passed, 3 skipped (422), Tests 3492 passed, 36 skipped (3528). The merge ofmainadds no file in this package.Acceptance notes
listObjects(), whilelistObjectsBoundToreads the engine registry; the plugin's own comment says the former is a boot-time copy that does not see runtime saves. This PR keeps the guard's read as it was (ruling 3: one read, the degraded path untouched) and does not measure whether a runtime-saved object escapes the count. Noted, not filed.error.details.objects; that is the ui lane's call.Generated by Claude Code