Skip to content

Commit 8569d5f

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-21308-tsx-esm-api-register
2 parents 11e63fc + 6c5bef5 commit 8569d5f

12 files changed

Lines changed: 1493 additions & 85 deletions
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/objectql': patch
3+
---
4+
5+
fix(objectql): a driver error that leaves the engine no longer carries the failing statement or the caller's values
6+
7+
Clause-②: no
8+
9+
The engine has cut the bound statement out of its own log line for a failed driver call for a long time, but it rethrew the driver's raw error. Any in-process code that logged what it caught, such as an auth library's error logger, printed the statement and the row's values. The same cut now runs where the error leaves the engine, so no consumer needs a patch of its own.
10+
11+
- **Where.** Every engine operation that reaches a driver: `find`, `findOne`, `count`, `aggregate`, `insert` (batch included), `update` and `delete` (by id and by predicate), `execute`, `transaction`, `resolveSecretField` and `resolveInternalField`.
12+
- **What is cut.** The statement and the caller's values, from the error's `message` and `stack`, from the properties drivers attach (mysql2's `sql` and `sqlMessage`; node-postgres' `detail`, `where` and `internalQuery`), and down the `cause` chain. A `DuplicateRecordError` keeps its own fields and carries a cut `cause`.
13+
- **What stays.** The error's class (`instanceof` still holds), `name`, `code`, `errno`, `sqlState`, Postgres' identifier fields (`constraint`, `table`, `column`, …) and the database's own diagnostic. The message now reads as the statement's kind, a `[statement and bound values redacted]` marker and the diagnostic. A Postgres key-shaped `detail` keeps its column list. Every REST answer keeps its status, code and `field`.
14+
- **What changes for a caller.** Code that read the statement or a value out of a driver error's message or properties now gets the marker instead. Branch on the class, `code` or `errno` instead. The driver error on a `DuplicateRecordError`'s `cause` is an equivalent copy, no longer the object the driver threw. An import's row report for a value-bearing database error no longer repeats the rejected value.

‎docs/adr/0128-producer-discriminated-aad-for-cryptocontext.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ Any **one** of these turns the deferral over; none requires re-opening the direc
101101
- **The hazard stops being hypothetical**: any intersection appears between the settings-namespace set and the set of object names carrying a `Field.secret()` field — in shipped platform metadata, in an example app, or in a reported customer deployment. §1.4's "no intersection" is the whole of the deferral's evidence, and it is a fact with a shelf life.
102102
- **A fourth producer of `CryptoContext` is added**, or an existing one's vocabulary is widened to accept names it does not control. A fourth vocabulary in a flat space is the same defect with more surface, and the discriminant is far cheaper to introduce *with* the new producer than after it.
103103
- **`ICryptoProvider` is opened for another breaking change.** The migration is the expensive half; the breaking-export half should be paid once. A queued breaking change to this interface should pull D1 in with it.
104+
> **Note (2026-10-02).** This trigger was met by the keyed-digest break ([#21263](https://github.com/objectstack-ai/objectstack/issues/21263), PR [#21292](https://github.com/objectstack-ai/objectstack/pull/21292): a required `keyedDigest` member on `ICryptoProvider`), which landed without D1 by the maintainer's ruling A (comment [5945612493](https://github.com/objectstack-ai/objectstack/issues/21263#issuecomment-5945612493)) because the director's census (comment [5945420994](https://github.com/objectstack-ai/objectstack/issues/21263#issuecomment-5945420994)) measured zero out-of-repo `ICryptoProvider` implementations in the organisation's repositories, so pulling D1 in would have saved a second breaking-export change for no measured implementer population; D1 is scheduled on its own as [#21326](https://github.com/objectstack-ai/objectstack/issues/21326).
104105
- **A compliance or customer requirement asks the platform to state its at-rest ciphertext binding.** The honest present-tense answer is §1's, and an organisation that needs a stronger one funds the work.
105106

106107
## 5. Alternatives considered

‎packages/objectql/src/driver-fault-boundary-redaction.test.ts‎

Lines changed: 625 additions & 0 deletions
Large diffs are not rendered by default.

‎packages/objectql/src/driver-fault-redaction.test.ts‎

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
// the tolerant-fallback direction, not the loud one.
2424

2525
import { describe, it, expect } from 'vitest';
26-
import { isUniqueViolationError, uniqueViolationColumn } from '@objectstack/types';
26+
import { isUniqueViolationError, mapDataError, uniqueViolationColumn } from '@objectstack/types';
2727
import { ObjectQL } from './engine.js';
2828
import {
2929
VALUE_BEARING_TEMPLATES,
@@ -853,14 +853,23 @@ describe('#8682 half B — the write-path loggers', () => {
853853
}
854854
});
855855

856-
it('the RETHROWN error is untouched — the caller`s 400 must not move', async () => {
857-
// `mapDataError` reads the driver's raw message to extract the failing
858-
// field and answer `400 INVALID_FIELD`. Redacting what we THROW would break
859-
// that answer; the redaction is one argument at one call site.
856+
it('the caller`s 400 does not move — and since #21274 the rethrown error carries no caller value either', async () => {
857+
// `mapDataError` reads the thrown message to extract the failing field and
858+
// answer `400 INVALID_FIELD`. This pin used to hold that by asserting the
859+
// thrown error was the driver's RAW one, statement and values included —
860+
// which is the exposure #21274 closed for every in-process logger. What it
861+
// protects is the ANSWER, so the answer is what it asserts now: the cut at
862+
// the engine boundary keeps the diagnostic the field is read from.
860863
const { thrown } = await insertAgainstADriftedColumn();
861864

862-
expect(String(thrown?.message)).toContain('insert into');
863-
expect(String(thrown?.message)).toContain(SECRET);
865+
expect(String(thrown?.message)).not.toContain(SECRET);
866+
expect(String(thrown?.message)).not.toContain(DESCRIPTION);
867+
expect(String(thrown?.stack)).not.toContain(SECRET);
868+
expect(String(thrown?.message)).toContain('has no column named secret_note');
869+
expect(thrown?.code).toBe('SQLITE_ERROR');
870+
const answer = mapDataError(thrown, 'crm_account');
871+
expect(answer.status).toBe(400);
872+
expect(answer.body).toMatchObject({ code: 'INVALID_FIELD', field: 'secret_note' });
864873
});
865874

866875
/**
@@ -905,10 +914,13 @@ describe('#8682 half B — the write-path loggers', () => {
905914
}
906915
});
907916

908-
it('MySQL duplicate entry — the driver error reaches the caller UNTOUCHED, on `cause`', async () => {
909-
// Same boundary as above: the log narrows, and what the driver said is not
910-
// rewritten anywhere. `isUniqueViolationError` and `uniqueViolationColumn`
911-
// read this text downstream and must keep seeing it.
917+
it('MySQL duplicate entry — the driver error reaches the caller on `cause`, cut, with every verdict kept', async () => {
918+
// `isUniqueViolationError` and `uniqueViolationColumn` read this error
919+
// downstream and must keep answering as they did. This pin used to hold
920+
// that by asserting the `cause` was the driver's error UNTOUCHED, statement
921+
// and value included; #21274 cuts both where the error leaves the engine,
922+
// so what is asserted now is what those readers actually read — the code,
923+
// the diagnostic and the index name — and that the value is gone.
912924
//
913925
// ⚠️ [#14095] WHERE the caller finds it moved by one step, and only for a
914926
// recognised unique violation: the insert door now answers the
@@ -924,9 +936,10 @@ describe('#8682 half B — the write-path loggers', () => {
924936
expect((thrown as any)?.status).toBe(409);
925937

926938
const cause = (thrown as any)?.cause;
927-
expect(String(cause?.message)).toContain('insert into');
928-
expect(String(cause?.message)).toContain(SECRET);
939+
expect(String(cause?.message)).not.toContain(SECRET);
940+
expect(String(cause?.message)).not.toContain(DESCRIPTION);
929941
expect(String(cause?.message)).toContain('Duplicate entry');
942+
expect(String(cause?.message)).toContain("for key 'crm_account.secret_note'");
930943
expect(cause?.code).toBe('ER_DUP_ENTRY');
931944

932945
// The verdicts the downstream consumers ask of it, asked of the envelope.

0 commit comments

Comments
 (0)