Repository navigation
feat(objectql,metadata-protocol,rest,spec): the object read names the objects a delete can cascade into - #28451
Conversation
…e objects a delete can cascade into The delete walk's own relation scan (cascadeRelationsInto) now also feeds an object-level closure, ObjectQL.getCascadeDeleteObjects, which the object item read serves beside the item as cascadeDeleteObjects (both doors; folded into the cached REST door's ETag). Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5pEcJ17rvFVfE3NXzN
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5pEcJ17rvFVfE3NXzN
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5pEcJ17rvFVfE3NXzN
… read serves it Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5pEcJ17rvFVfE3NXzN
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5pEcJ17rvFVfE3NXzN
📓 Docs Drift CheckThis PR changes 4 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 173 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 2efa81586180b3ac7b7dcae7dcb3ab45e5367454 && git checkout 2efa81586180b3ac7b7dcae7dcb3ab45e5367454
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 776c1d875b2d140b4c2419103a7ad8681c1d2610 7f3638c5bcacab92670113fbc367a58c1e211eab && git checkout -B drift-repro 776c1d875b2d140b4c2419103a7ad8681c1d2610 && git merge --no-ff 7f3638c5bcacab92670113fbc367a58c1e211eab
node scripts/docs-audit/affected-docs.mjs --json 776c1d875b2d140b4c2419103a7ad8681c1d2610
|
|
domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo Contract reviewServed-tier: Face: published schema ( ① Derived judgmentsEach acceptance-set and public-surface change the diff makes, judged on the committed tree against
② Semver levelDeclared: Declared:
Ruling the seat can adopt: MUST-FIX (② declaration): change the ③ Boundary flags
④ Design judgment
Visibility (order: "follow the rule the metadata answer already applies"): the served list is caller-independent, and the object read on ETag and cache: the REST door folds
Implemented-by: VERDICT: FAIL One must-fix, in ②: the Generated by Claude Code |
…ne (contract review 6546633648) Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5pEcJ17rvFVfE3NXzN
|
domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo Contract reviewServed-tier: Delta review. The full review of record on this PR is comment 6546633648 on head ① Derived judgmentsInherited from 6546633648, items 1 to 7, unchanged: the published schema ( ② Semver levelRe-judged on this head. The must-fix asked for three things; all three are present, in both carriers:
Gate runs, on a temporary worktree of
The must-fix in ② is discharged. ③ Boundary flags
④ Design judgmentInherited from 6546633648 in full: Implemented-by: VERDICT: PASS The one must-fix of 6546633648 is discharged on this head: Generated by Claude Code |
|
domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo 席位核对ACCEPT · The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.
Generated by Claude Code |
domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo
Requested by Leehom · project thread
Part of #28001 (the server half of ruling B; the ui half, the delete confirmations that read this key, is its own
domain:uicard, blocked by this PR).Clause-②: yes (widening) — a new optional key served beside the object read (
cascadeDeleteObjects, both item doors and the app bundle's object entries), a new optionalIObjectQLEnginemember and a new publicObjectQLmethod. Nothing is refused that ran before.Metadata-protocol: added — the served key
cascadeDeleteObjectsmoves theapiauthorable-surface shard (packages/spec/authorable-surface/api.json), whichscripts/check-changeset-no-major.mjsreads as an added protocol key;@objectstack/specis gradedminor(the #27842 / #27964 shape). Nothing an author can write moves: the key is served besideitem, never inside it, and has no authorable spelling.Skills-docs: none — a served-only key; no
skills/**text namescascadeDeleteObjectsordisabledActions.维护者速读
GET /api/v1/meta/object/:name(缓存默认门与非缓存门,以及 app bundle 里每个对象条目)在item旁边多返回一个cascadeDeleteObjects:删除这个对象的一条记录时会被级联一起删掉的对象名列表(主从子对象默认算,lookup/tree 只有声明deleteBehavior: 'cascade'才算,孙辈对象也算,restrict/set_null不算)。列表由引擎ObjectQL.getCascadeDeleteObjects给出,它和删除流程共用同一个「哪些关系会级联」的扫描函数。packages/objectql/src/engine-delete-cascade.ts(cascadeRelationsInto抽出与collectCascadeDeleteObjects);packages/metadata-protocol/src/protocol.ts(resolveCascadeDeleteObjects与getMetaItemCached的 304 退让);packages/rest/src/meta-item-read-routes.ts(ETag 折叠)。pnpm --filter @objectstack/objectql exec vitest run src/engine-cascade-delete-objects.test.ts→ 7 passed,其中cd_parent答['cd_child', 'cd_grandchild'],restrict / set_null 兄弟不在列。packages/spec/**,席位先做合约评审)。What changes
The shape, measured and chosen
ObjectQL.getCascadeDeleteObjects(object)reads the registry and no row, so it says what a delete MAY remove, never which records or how many.inspectCascadeDelete(record-level, reads as SYSTEM with no organization) is untouched and is not exposed anywhere.cascade, each object named once at the first depth that reaches it and expanded once, so a cycle (a self-cascadingtree, two objects cascading into each other) terminates. The object itself is named only when a cascading relation leads back to it.cascadingIntoinsidecollectCascadeDeleteSet) is lifted out unedited ascascadeRelationsInto, and bothcollectCascadeDeleteSetand the newcollectCascadeDeleteObjectscall it. It readscascadeRelationBehavior(target, child, fieldName, fdef) === 'cascade', the predicate the delete runs. No copy of the rule lives in rest, runtime, metadata-protocol or spec.disabledActions' (meta: GET /meta/object serves which packaged actions are switched off beside the item, and the disabled-action refusal reads as a plain sentence #27842 / rest: a cached GET /meta/object/:name that carries disabledActions is never answered 304 — fold the switched-off names into the ETag so object reads revalidate again #28044) terms. The console's grid and record page readGET /meta/object/:name(ObjectStackAdapter.getObjectSchema) or the app bundle's object entries, so a key on that answer needs no second request; a new read route would add a route-ledger row and a second round trip. Precisely asdisabledActionsdoes:IObjectQLEngine.getCascadeDeleteObjectsthrough the kernel's service table by name (objectql), per call, and imports no engine;GET /meta/object), since the confirmations read the item;[]where the engine answered that nothing cascades;getMetaItemCachedcarries it besidedataand stands its own 304 down for a read that carries it; the REST door folds the served list into the object read's ETag, so an unchanged object with an unchanged cascade revalidates to 304, and an object registered later that cascades into it moves the ETag. An object no engine answered for keeps its old validator byte for byte.getMetaItemthroughcreateMetaItemAnswer), so they carry the key with no change to the bundle route.createMetaItemReadGate'sobjectarm prunes onlylistViews; measured on this tree, a member with no nav reach tosecret_dealis answered200onGET /meta/object/secret_deal). Serving field names would disclose masked fields; serving object names discloses nothing an object read does not. One entry per object also absorbs an object reached by two fields. The ui half shows object labels, which the client resolves from names; no translated text is served.GetMetaItemResponseSchemaandGetMetaItemCachedResponseSchemagain the optionalcascadeDeleteObjects: string[], documented besidedisabledActions.IObjectQLEnginegains the optionalgetCascadeDeleteObjects?(objectName): string[] | undefined.Files
packages/objectql/src/engine-delete-cascade.ts:cascadeRelationsInto(lifted fromcollectCascadeDeleteSet),collectCascadeDeleteObjects.packages/objectql/src/engine.ts: publicgetCascadeDeleteObjects.packages/spec/src/contracts/objectql-engine.ts,packages/spec/src/api/protocol.zod.ts: the member and the key.packages/metadata-protocol/src/protocol.ts:resolveCascadeDeleteObjects, the item read's envelope,getMetaItemCached.packages/rest/src/meta-item-read-routes.ts: the cached arm carries it and folds it into the ETag.content/docs/references/api/protocol.mdx,app-bundle.mdx: regenerated (gen:docs).packages/objectql/src/engine-cascade-delete-objects.test.ts,packages/metadata-protocol/src/protocol.object-cascade-delete-objects.test.ts,packages/rest/src/meta-object-cached-cascade-delete-objects.test.ts, a[#28001]block inpackages/rest/src/meta-app-bundle-routes.test.tsand inpackages/spec/src/api/meta-item-response-shapes.test.ts..changeset/28001-cascade-delete-objects-beside-item.md:minorfor spec;patchfor objectql, metadata-protocol, rest.Tests
origin/main4bc077d2, objectql and metadata-protocoldist/rebuilt from them, tests kept):engine-cascade-delete-objects.test.ts7/7 failed;protocol.object-cascade-delete-objects.test.ts5 failed, 4 passed (the four controls: no key without an engine, other types, execution face, the list);meta-object-cached-cascade-delete-objects.test.ts3 failed, 3 passed (the no-key control and the unchanged-validator 304 controls);meta-item-response-shapes.test.ts4 failed, 28 passed. The restore leg'sgit diff HEADwas empty.cd_parent←cd_child(master_detail, default cascade) ←cd_grandchild(lookup,deleteBehavior: 'cascade'), with arestrictsibling (cd_pin), aset_nullsibling (cd_tag) and a default-set_nulllookup on the child (cd_note):cd_parentanswers['cd_child', 'cd_grandchild'],cd_childanswers['cd_grandchild']; the self-cascadingcd_folderanswers['cd_folder'];cd_loneranswers[]; an unknown objectundefined. The real protocol over the real engine serves the same list on both doors,[]forcd_loner, nothing on the execution face, and an object registered later that cascades intocd_loneris on the next read.delete()of acd_parentrecord empties exactlycd_childandcd_grandchild, leaves theset_nullholders standing, and equals the served list.scripts/ablation-replace.mjs, src-resolved suite, restore provenblob == HEAD,git diff HEADempty): replacingcascadeRelationsInto.call(this, target, objects)incollectCascadeDeleteObjectswith a hand-written lookup-only filter (type === 'lookup' && deleteBehavior === 'cascade') reds the master_detail case (expected [] to deeply equal [ 'cd_child', 'cd_grandchild' ]) and the self-cascading tree case, whileengine-cascade-delete.test.ts(the delete itself) stays green. The protocol-door rows of that run read a stale basedist/from the red leg and are not counted.engine-cascade-delete.test.ts,engine-inspect-cascade-delete.test.tsgreen); an object with no cascading children answers[]; no engine answer serves no key and keeps the old ETag.Acceptance notes
[Edit by the seat: the contract review (6546633648) ruled option B. The changeset gate refusesMetadata-protocol: nonewithpatcheverywherenonewhenever an authorable-surface shard moves, so head 7f3638c declaresMetadata-protocol: addedand grades@objectstack/specminor, as thedisabledActionsprecedent did.]GET /meta/object) does not carry the key. The confirmations read the item; adding it to the list is a widening with no reader yet.Verification
Local runs. The package suites and the first 88 derived gates ran on
81f57815; the last commit,8bfac916, adds only the engine-double ledger pin, after whichcheck:engine-double-contractand the 30 families it added to the derivation ran on8bfac916:pnpm --filter @objectstack/objectql run testexit 0, 9836 passed;typecheckexit 0.pnpm --filter @objectstack/metadata-protocol run testexit 0, 21881 passed, 26 skipped;typecheckexit 0.pnpm --filter @objectstack/rest run testexit 0, 6870 passed, 332 skipped;typecheckexit 0.pnpm --filter @objectstack/spec run testexit 0, 22834 passed, 1 todo;typecheckexit 0.@objectstack/runtimesrc/domains/meta-list-projection-parity.test.tsandsrc/domains/meta*: 29 files, 1425 passed.pnpm --filter @objectstack/spec check:generated:check:docsstale beforegen:docs, regenerated and committed; the other 13 artifacts up to date.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack): reconciled with--ran,118 derived famil(ies) accounted for — 117 run, 1 NOT-MEASURED. Every run family exit 0, after two fixes in this PR:check:engine-double-contractfirst answered exit 1 (RETAINED [findOne]for the new metadata-protocol double: new coverage the ledger must learn), fixed bynode scripts/check-engine-double-contract.mjs --write(scripts/engine-double-contract.pinned.json, +1 row) and re-run exit 0;check-plugin-teardown-shape.mjs --self-testfirst answered exit 3 on the shallow clone and exit 0 after fetching its positive-control commit.pnpm check:dual-build-cjs-loads(exit 3, PREREQUISITE NOT MET: it needs every package'sdist/, this worktree built only the touched closures). CI runs it.pnpm exec eslint --no-inline-config packages/objectql packages/metadata-protocol packages/rest packages/spec: exit 0.!in the title, no(narrowing)arm).test:repoprojects: declared to CI (Test Core).Generated by Claude Code