Skip to content

fix(service-datasource): name the reason and the bound objects in the datasource delete refusal - #30513

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-lth6k5
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-lth6k5

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Bill · project thread

domain:server#2 · os-bill · session_01Utp6Cb4nPCxmCZ7EzB1A1v

Fixes #30444

Clause-②: yes (widening) — the bound-object refusal keeps 400 DATASOURCE_ADMIN_ERROR and its message, and gains error.details; nothing a client reads today moves.
Metadata-protocol: none

维护者速读

  • 改了什么:删除一个仍有对象在用的数据源时,拒绝答复除了原来那句英文,还在 error.details 里带上机器可读的原因 objects_bound、数量和这些对象的名字(排好序的完整列表)。状态码、错误码和英文原句都不变。
  • 为什么改:以前拒绝只有一句英文和一个数字,客户端分不清这是哪种 400,也说不出是哪些对象挡着;设置页只能自己再去读对象元数据才能告诉管理员。现在任何客户端(设置页、CLI、API 调用方)都能用自己的语言直接列出对象名。
  • 风险与回滚:只加字段,不改已有的字段。名字和数量出自同一次读取,二者不会对不上;元数据读不全时的 503 拒绝原样不动;其他 400 一律不带 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 by boundObjectsError: the same message (Cannot remove datasource 'NAME': N object(s) are still bound to it.), plus code: 'DATASOURCE_ADMIN_ERROR', status: 400 and details: { reason: 'objects_bound', count, objects }. objects is the full list of bound object names, sorted; no cap (the list is bounded by the objects bound to one datasource).
  • One read. DatasourceAdminServiceConfig.countBoundObjectsDiagnosed may now return objects?: string[], and the plugin wiring returns the names of the same filtered listObjects() rows it counts. The count in the message, details.count and details.objects all come from that one result. ⛔ Not listObjectsBoundTo: 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 only countBoundObjects (no diagnosed read) gets { reason, count } without objects.
  • badRequest in admin-routes.ts. A thrown error whose status is 400, whose code is the route's own SERVICE_ERROR_CODE[service] and whose details is a plain object is answered with that details. All three must hold, the same pattern as the existing 503 and 404 relays, so a driver or library error that carries a stray details field keeps answering the bare message. Every other 400 answers exactly as before.
  • details.reason spelling. snake_case, following the envelope's existing details.reason values in plugin-auth (invalid_body, user_not_found, cross_organization, …) and AGENTS.md's rule for machine-name data values. service-settings spells its one reason in kebab-case (locked-by-env); this package had no prior details.reason.
  • Unchanged: the status, the code (no new ledger code; packages/spec untouched), the English message, the degraded-read refusal (503 SERVICE_UNAVAILABLE, metadataIncompleteError), the code-defined refusal, the 204 delete. The IDatasourceAdminService.removeDatasource docblock names the new details. No hand-written doc page states this refusal (searched content/docs outside references/ and releases/ 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 (real DatasourceAdminService + real registerDatasourceAdminRoutes on HonoHttpServer, 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."}}, no details.
  • 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."}}.
  • badRequest forwarded the message alone (sendError(res, 400, code, message), no extra).
Tests
  • admin-routes.test.ts › new top-level describe DELETE /datasources/:name — the bound-object refusal carries details (#30444), over the real service and routes: two bound objects answer 400 DATASOURCE_ADMIN_ERROR, the unchanged message and details { reason: 'objects_bound', count: 2, objects: ['bin', 'part'] } (sent unsorted; the harness's listObjectsBoundTo answers a different name on purpose), and the record survives; a count-only host answers { reason, count }; nothing bound deletes with 204; the code-defined 400 carries no details (exact body); an error with a stray details and 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 asserts objects (['wh_line', 'wh_order']), and the predating-listDiagnosed case's exact toEqual gains objects: ['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), at bba4b00f and again at f4cc8056 (after merging main, 14 commits, none in this package or its route dependencies). typecheck: exit 0 (the package tsconfig compiles src/__tests__, counted with --listFiles).
Ablations (scripts/ablation-replace.mjs; the suite imports src directly, no build between legs)
# Mutation Result
A1 badRequest relays details from any error (drop the status/code conjuncts) stray-details pin red (1 failed)
A2 badRequest's default 400 branch attaches a details code-defined pin, stray-details pin and the existing surfaces lifecycle errors exact-body pin red (3 failed)
A3 names taken from listObjectsBoundTo (a second scan) instead of the count's read two-bound pin and count-only pin red (2 failed)
A4 badRequest stops forwarding details two-bound pin and count-only pin red (2 failed)

Each mutation landed (anchor 1 to 0, replacement 0 to 1); each restore left git diff HEAD empty.

Gates
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on bba4b00f: 62 families, the same 62 on f4cc8056. All 62 run, exit captured per line before any pipe: 60 exit 0 on the first pass; check-plugin-teardown-shape --self-test exit 3 (shallow clone, pinned fixture commit fetched, rerun exit 0); check:dual-build-cjs-loads exit 3 (no dist, full build under the lock, rerun exit 0). --ran: 62 derived, 62 run, 0 NOT-MEASURED.
  • pnpm lint (eslint . --no-inline-config): exit 0 on f4cc8056.
  • Full dogfood under the verify lock on bba4b00f (after a full turbo run build): VERDICT command-exit 0, Test Files 419 passed, 3 skipped (422), Tests 3492 passed, 36 skipped (3528). The merge of main adds no file in this package.

Acceptance notes

  • The bound-object count (and now the names) comes from the metadata service's listObjects(), while listObjectsBoundTo reads 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.
  • Once this lands, Setup (PR fix(app-shell): the datasource delete confirm and its refusal name the objects that still use it, in the console's language #30442) may switch from reading object metadata to error.details.objects; that is the ui lane's call.

Generated by Claude Code

… 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>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-datasource, touching 8 documentable anchor(s).

2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/external-datasources.mdx (via DatasourceAdminServicePlugin (symbol, a top-level class))
  • content/docs/data-modeling/odata-datasources.mdx (via DatasourceAdminServicePlugin (symbol, a top-level class))

⛔ 2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-1.mdx (via IDatasourceAdminService (symbol, a top-level interface))
  • content/docs/releases/v17/17-6.mdx (via DATASOURCE_ADMIN_ERROR (literal, a string literal in boundObjectsError))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 103 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 3 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json f6faf7511635fc36509e1c8b61d8f0446f3b42c1 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from be51654bae67cc0714f8f696dcad41cfcd203c5c — the merge of head f4cc8056838b82ca26ff8b4c9760b18a179e9656 into base f6faf7511635fc36509e1c8b61d8f0446f3b42c1, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs f6faf7511635fc36509e1c8b61d8f0446f3b42c1 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

维护者速读

  • 改了什么: 删除一个还有对象在用的数据源时,拒绝答复仍是 400 DATASOURCE_ADMIN_ERROR,英文原句不变;另外多带一个 error.details:{ reason: 'objects_bound', count, objects },写明原因代码和按名字排好序的完整对象清单。
  • 为什么改: 原来只有一句英文加一个数字,客户端分不清这是哪种 400,也不知道是哪些对象挡着,Setup 只能自己再去读对象元数据(Datasource delete refusal: a datasource that objects still use is refused with a plain English sentence and a count, no error code and no object names #30444,Setup: datasource and storage results reach the toast in English, and a refused delete does not name the objects #30335 的 server 半边)。
  • 风险与回滚: 只是答复里多了字段,状态码、错误码和原句都不变;其他 400 和 503 降级路径照旧。回滚即撤本 PR。
  • 先看: packages/services/service-datasource/src/datasource-admin-service.ts 的 boundObjectsError,再看 admin-routes.ts badRequest 新加的转发条件。
  • 两分钟验证: pnpm --filter @objectstack/service-datasource exec vitest run src/__tests__/admin-routes.test.ts -t 30444。
  • 需要您做的: 无,席位经合并队列落地(非受管面,221 行,非 BREAKING)。
  • 席位意见: ACCEPT。数量和名字出自同一次读取,不会对不上;badRequest 只在状态、本路由错误码、普通对象 details 三者都成立时才转发,别的 400 不会意外带出字段。

席位核对 — ACCEPT · domain:server#2 · head f4cc8056838b82ca26ff8b4c9760b18a179e9656

  • 形态: draft, author objectstack-fleet[bot] (relay pr_create), no assignee, no review request; 7 files, +207 −14 against its merge base; check-governed-merges --pr 30513: NOT governed (0 of 7 paths), 221 lines ≤ 3000. Title has no !.
  • Review face: TIER_DEFAULT (declaration face: Clause-②: yes (widening) — the refusal gains details; no packages/spec path, no ledger code, no status change, no governed path), written here by the seat.
  • Seat rulings held: (1) status and code kept (400 DATASOURCE_ADMIN_ERROR, message byte-identical), no new ledger code; (2) details: { reason: 'objects_bound', count, objects } — reason snake_case like the envelope's other details.reason values, objects sorted full list; (3) one read: the plugin's countBoundObjectsDiagnosed now returns the names off the same filtered listing as the count (the dev chose this over listObjectsBoundTo, which is a second read — within ruling 3's "or the same read the count already uses").
  • Seat read: boundObjectsError sets code/status/details; badRequest relays details through the shared sendError(…, { details }) (ApiError.details is an admitted extra) only when status === 400, code equals the route's own code and details is a plain object — a driver error with a stray details keeps the bare message. The degraded path (metadataIncompleteError, 503) runs first and is untouched. A host whose countBoundObjectsDiagnosed returns no objects gets count alone (optional field, contract docblock updated). Names that are not strings are dropped from objects while still counted — a defensive edge, not a disagreement a real object can produce.
  • Deviation accepted: one toEqual in datasource-removal-bound-count-outage.test.ts gains objects: ['wh_order'] — the plugin's diagnosed read now returns the names; the outage assertions themselves are unchanged.
  • Pins: admin-routes.test.ts (+118): two bound objects → 400 DATASOURCE_ADMIN_ERROR, same message, details.reason, count, sorted names; no bound objects → deletes; a code-defined datasource's 400 carries no details; a stray details on another error is not relayed. Dev ablations: 4, each red, restored.
  • Serial: seat 4's PR fix(service-datasource)!: Test connection and Save refuse a SQLite filename that is neither :memory: nor a usable file path #30395 (New datasource: the file path field holds a real :memory: value, and Test connection passes on the broken path it produces #30331) edits assertValidConfig in the same file, no overlap; the later lander merges main.
  • Declarations: Clause-②: yes (widening), Metadata-protocol: none, line-initial in PR body and changeset; patch for @objectstack/service-datasource.
  • Dev readings (f4cc8056, main merged, 14 commits not touching the package): derived gates 62/62, pnpm lint 0, full dogfood 3492 passed. os-dev-report 6659669742.
  • 门禁 check-runs: in progress at ACCEPT; the seat queues once every check is green or an expected skip.
  • 落地条款: Fixes Datasource delete refusal: a datasource that objects still use is refused with a plain English sentence and a count, no error code and no object names #30444. Ordinary queue landing by this seat: pr_ready + automerge_enable on this head.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Datasource delete refusal: a datasource that objects still use is refused with a plain English sentence and a count, no error code and no object names

1 participant