Repository navigation
Commit 6a2866a
feat(plugin-approvals,rest,client): a record-anchored approval lock read,
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1X38EQhJnKnxazCiq8xQpv9c)_
Fixes #26314
Clause-②: yes (widening)
Metadata-protocol: none
## What changes
#25934 ruling A ("readers"): the record lock gets a carrier of its own,
so a member who can read a record but is not on its approval is no
longer offered an edit the server then refuses with `409 RECORD_LOCKED`.
- **`@objectstack/spec`**: one optional member on `IApprovalService`,
`getRecordLock?(object, recordId, context): Promise<{ locked: boolean }
| null>`, with its doc block. No schema, no exported type.
- **`@objectstack/plugin-approvals`**: `ApprovalService.getRecordLock`
reads the record **as the caller** (`engine.find` with the caller's
context, `fields: ['id']`, `limit: 1`). An unreadable record answers
`null`, the same as a missing id; a caller refused the object's read is
refused for every id. `locked` comes from the pending-request lookup the
hook already uses.
- **One lock judgement**: `nodeConfigLocksRecord(config)` (`lockRecord
!== false`) in `lifecycle-hooks.ts` is now the only spelling. The
`beforeUpdate` record-lock hook, the request row's `lock_record` mirror
and the new read all call it; the read goes through
`recordHeldByLockingApproval`, which shares the hook's lookup and its
bookkeeping-object exemption.
- **`@objectstack/rest`**: `GET
/api/v1/approvals/records/:object/:recordId/lock` answers `{ success:
true, data: { locked } }` and nothing else. Every `null` answers one
`404 RECORD_NOT_FOUND` body, so an unreadable record and a missing one
cannot be told apart. The closed query-parameter set is empty (route
rule 5). A service without the member answers `501`. Ledgered as `sdk`.
- **`@objectstack/client`**: `client.approvals.getRecordLock(object,
recordId)`.
- **Docs**: `content/docs/automation/approvals.mdx` gains a "Reading the
lock" paragraph with a curl example.
#8652's ruling is untouched: request content stays behind
`approvalsVisibleToReaders`, and the tests assert the non-participant
still gets no request row.
## Spec change: needs the spec seat's sign-off
The only `packages/spec/**` change is
`packages/spec/src/contracts/approval-service.ts`, new lines 935–972:
the doc block plus the optional member `getRecordLock?` inserted right
after `getRequest(...)` in `IApprovalService`. Nothing else under
`packages/spec/` changed. It is optional so existing implementations and
the spec's own `minimalService` literal still satisfy the contract.
## Tests
- `packages/qa/dogfood/test/approval-record-lock-read.dogfood.test.ts`
(new): the card's four cases through the REST door on a real boot with
the real `SecurityPlugin`, flows and approvals plugin, on two `private`
objects:
1. a non-participant who can read and edit the held record gets exactly
`{ success: true, data: { locked: true } }`, still gets `[]` from the
request list, and their `PATCH` is still `409 RECORD_LOCKED`;
2. a read-only reader gets `{ locked: true }`;
3. a caller who cannot read the record (their `GET /data` is 404) gets
the same status and body as for a non-existent id (`404
RECORD_NOT_FOUND`);
4. a pending node with `lockRecord: false`, and the held record after
approval, both read `{ locked: false }` and the edit lands.
-
`packages/plugins/plugin-approvals/src/record-lock-read.integration.test.ts`
(new, 4 tests): real ObjectQL over SQLite with the real plugin start;
also pins `lock_record` and the read agreeing on every request.
- `packages/rest/src/rest-approvals-record-lock-route.test.ts` (new, 5
tests): preservation (extra keys from a service are dropped), one 404
body for every `null`, the empty closed query set, `501`, and a `403`
relay.
- `packages/client/src/client.test.ts`: URL encoding and envelope
unwrapping.
- `packages/rest/src/execctx-consumer-census.test.ts`: counts move by
one for the new route.
Dogfood does not run on PR CI, so it was run locally under the verify
lock.
At head `6ba5f8cc`: plugin-approvals 99 files / 1280 tests pass, client
52 / 672 pass, the new dogfood file 1 / 1 pass. Rest (324 files / 6483
pass, 326 skipped) and spec (725 files / 21974 pass) ran at `99f67d74`;
the later merges of `main` changed no file in either package.
### Gates
- `dispatch-gates.mjs --commands` derived 114 commands; all 114 ran at
`6ba5f8cc` with exit 0, reconciled with `--ran` (114 run, 0 not
measured, 0 unrun). `main` moved 13 commits during the run; none touches
a file in this diff.
- `typecheck` on spec, plugin-approvals, rest and client: exit 0. Spec
`check:generated`: exit 0.
- `test:repo` on core, objectql, rest, runtime, spec and types: exit 0.
- Scoped `eslint` over the touched files and `check-nul-bytes`: exit 0.
### Ablations (through the REST door, dogfood)
1. Lock judgement mutated to always lock: red at
`approval-record-lock-read.dogfood.test.ts:217`, expected `locked:
false` for the `lockRecord: false` record, received `true`. Restored,
blob equals HEAD.
2. As-caller read mutated to a system context: red at `:209`, expected
`404` for the caller who cannot read the record, received `200`.
Restored, blob equals HEAD.
Each mutation was rebuilt into `dist` and confirmed on disk by
`scripts/ablation-dist-preflight.mjs` before the run, and `--absent`
after the restore. Ablation 2's declaration build failed (TS6133,
`context` unused once the mutation dropped its use); the JS build still
carried the mutation, which the preflight found in both `index.js` and
`index.mjs`, so the run is valid.
## Deviations from the claim
- The claim named plugin-approvals; the route, ledger row and client
method live in `packages/rest` (`approvals-routes.ts`,
`rest-route-ledger.ts`) and `packages/client/src/index.ts`. Agreed with
the dispatching seat before the change so the ledger row ships as `sdk`,
not as a gap.
- Skill teaching is not in this PR: `skills/**` is a governed surface
and would make the whole PR need maintainer approval. A follow-up card
is named in the report.
## Acceptance notes
- `pendingRequestFor` reads with `limit: 1`, so with two pending
requests on one record the hook judges only the first one it gets. The
new read reuses the same lookup, so the two still agree; whether two
pending requests on one record can happen is not verified here.
- Overlap: open #26337 also edits `approval-service.ts` (different
regions) and `content/docs/automation/approvals.mdx` (a nearby section);
whichever lands second may need a trivial rebase.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01QAsn9ToxaDxbnRkP6HxERw)_
---------
Co-authored-by: Claude <noreply@anthropic.com>{ locked } for every caller who can read the record (#26361)1 parent e843b5b commit 6a2866a
13 files changed
Lines changed: 884 additions & 23 deletions
File tree
- .changeset
- content/docs/automation
- packages
- client/src
- plugins/plugin-approvals/src
- qa/dogfood/test
- rest/src
- spec/src/contracts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
376 | 376 | | |
377 | 377 | | |
378 | 378 | | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
379 | 400 | | |
380 | 401 | | |
381 | 402 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
704 | 704 | | |
705 | 705 | | |
706 | 706 | | |
| 707 | + | |
| 708 | + | |
| 709 | + | |
| 710 | + | |
| 711 | + | |
| 712 | + | |
| 713 | + | |
| 714 | + | |
| 715 | + | |
707 | 716 | | |
708 | 717 | | |
709 | 718 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6260 | 6260 | | |
6261 | 6261 | | |
6262 | 6262 | | |
| 6263 | + | |
| 6264 | + | |
| 6265 | + | |
| 6266 | + | |
| 6267 | + | |
| 6268 | + | |
| 6269 | + | |
| 6270 | + | |
| 6271 | + | |
| 6272 | + | |
| 6273 | + | |
| 6274 | + | |
| 6275 | + | |
| 6276 | + | |
| 6277 | + | |
6263 | 6278 | | |
6264 | 6279 | | |
6265 | 6280 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
120 | 120 | | |
121 | 121 | | |
122 | 122 | | |
| 123 | + | |
123 | 124 | | |
124 | 125 | | |
125 | 126 | | |
| |||
1280 | 1281 | | |
1281 | 1282 | | |
1282 | 1283 | | |
1283 | | - | |
1284 | | - | |
1285 | | - | |
1286 | | - | |
1287 | | - | |
1288 | | - | |
1289 | | - | |
| 1284 | + | |
| 1285 | + | |
| 1286 | + | |
| 1287 | + | |
| 1288 | + | |
| 1289 | + | |
| 1290 | + | |
1290 | 1291 | | |
1291 | 1292 | | |
1292 | 1293 | | |
| |||
8641 | 8642 | | |
8642 | 8643 | | |
8643 | 8644 | | |
| 8645 | + | |
| 8646 | + | |
| 8647 | + | |
| 8648 | + | |
| 8649 | + | |
| 8650 | + | |
| 8651 | + | |
| 8652 | + | |
| 8653 | + | |
| 8654 | + | |
| 8655 | + | |
| 8656 | + | |
| 8657 | + | |
| 8658 | + | |
| 8659 | + | |
| 8660 | + | |
| 8661 | + | |
| 8662 | + | |
| 8663 | + | |
| 8664 | + | |
| 8665 | + | |
| 8666 | + | |
| 8667 | + | |
| 8668 | + | |
| 8669 | + | |
| 8670 | + | |
| 8671 | + | |
| 8672 | + | |
| 8673 | + | |
| 8674 | + | |
| 8675 | + | |
| 8676 | + | |
| 8677 | + | |
| 8678 | + | |
| 8679 | + | |
| 8680 | + | |
| 8681 | + | |
| 8682 | + | |
| 8683 | + | |
| 8684 | + | |
8644 | 8685 | | |
8645 | 8686 | | |
8646 | 8687 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
315 | 315 | | |
316 | 316 | | |
317 | 317 | | |
318 | | - | |
| 318 | + | |
319 | 319 | | |
320 | 320 | | |
321 | 321 | | |
| |||
333 | 333 | | |
334 | 334 | | |
335 | 335 | | |
336 | | - | |
| 336 | + | |
337 | 337 | | |
338 | 338 | | |
339 | 339 | | |
| |||
388 | 388 | | |
389 | 389 | | |
390 | 390 | | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
391 | 442 | | |
392 | 443 | | |
393 | 444 | | |
| |||
487 | 538 | | |
488 | 539 | | |
489 | 540 | | |
490 | | - | |
| 541 | + | |
491 | 542 | | |
492 | 543 | | |
493 | 544 | | |
| |||
538 | 589 | | |
539 | 590 | | |
540 | 591 | | |
541 | | - | |
| 592 | + | |
542 | 593 | | |
543 | 594 | | |
544 | 595 | | |
| |||
0 commit comments