Skip to content

Commit eb44c17

Browse files
committed
test(driver-turso): pin all 17 options doors + changeset (#6402)
1 parent 260fca0 commit eb44c17

2 files changed

Lines changed: 171 additions & 0 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
"@objectstack/driver-turso": patch
3+
---
4+
5+
fix(driver-turso): narrow every override's `options` from `any` to `DriverOptions` (#6402)
6+
7+
`TursoDriver` overrides 17 methods that take an `options` argument, and every one
8+
of them declared `options?: any` while the base it forwards to (`SqlDriver`, and
9+
behind it the `IDataDriver` contract) declared `DriverOptions`. The keys
10+
`DriverOptions` names — `bypassTenantAudit`, `tenantId`, `transaction`,
11+
`accessible_org_ids`, `skipCache`, `timeout`, … — were therefore unchecked at all
12+
17 doors.
13+
14+
The argument is #5181's, one axis over. An internal caller that misspells
15+
`bypassTenantAudit` gets no runtime complaint: the typo'd key is simply never
16+
read, the write proceeds unaudited, and nothing anywhere says so. `tsc` is the
17+
only channel that ever objects, and `any` had switched it off. Nothing is known
18+
to have gone wrong through this gap — it is closed because the door was open, not
19+
because someone walked through it.
20+
21+
**Why all 17 at once.** The shape was character-identical across every override,
22+
so narrowing a subset would read to the next person as a *verdict* on the rest.
23+
That is not hypothetical: #6075 (PR #6210) narrowed `count`'s `query` and
24+
deliberately left its `options`, and #6212 batch B did the same on `aggregate` —
25+
each leaving a comment saying so. Those comments are now discharged. The three
26+
prior narrowings (#5181 / PR #6076, #6075 / PR #6210, #6212) each closed the
27+
`query` axis; this closes the `options` axis, which had never been touched.
28+
29+
**Consumer impact.** Annotation-only — no runtime behaviour changes, and the full
30+
monorepo typecheck is unchanged at 125/125 green, so no caller in this repo was
31+
passing an off-contract value. It is a `patch` rather than a docs-only change
32+
because the narrowed signatures are public: a downstream TypeScript consumer
33+
holding a `TursoDriver`-typed reference and passing an `options` value that is not
34+
a `DriverOptions` will now see a compile error where it previously saw none. That
35+
error is the point — the value was already being ignored by the driver.
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#6402] Every `options` door on `TursoDriver` is a `DriverOptions`.
5+
*
6+
* # What was open
7+
*
8+
* `TursoDriver` overrides 17 methods that take an `options` argument, and every
9+
* one of them declared it `options?: any` while the base it forwards to
10+
* (`SqlDriver`, and behind it the `IDataDriver` contract) declared
11+
* `DriverOptions`. The keys `DriverOptions` names — `bypassTenantAudit`,
12+
* `tenantId`, `transaction`, `accessible_org_ids`, … — were therefore unchecked
13+
* at all 17 doors. The argument is #5181's, verbatim, one axis over: an internal
14+
* caller that misspells `bypassTenantAudit` has `tsc` as its ONLY warning
15+
* channel, and `any` switched that channel off.
16+
*
17+
* # Why all 17 at once
18+
*
19+
* The shape was character-identical across every override, so narrowing a subset
20+
* would read to the next person as a *verdict* on the rest. That is not
21+
* hypothetical: #6075 (PR #6210) narrowed `count`'s `query` and deliberately left
22+
* its `options`, and #6212 batch B did the same on `aggregate` — each leaving a
23+
* comment saying so. This file is the pin for the sweep that closed all of them
24+
* together, so no half-narrowed state exists to be misread.
25+
*
26+
* Note the issue that prompted this counted FIVE such overrides (`update`,
27+
* `upsert`, `delete`, `count`, `aggregate`) — the CRUD block it was reading while
28+
* #6212 was in flight. The measurement against `main` found 17: the bulk block,
29+
* `execute`, and the schema block carry the identical shape. Narrowing the five
30+
* would have reproduced, at larger scale, exactly the partial-narrowing the issue
31+
* was filed to prevent.
32+
*
33+
* # The table below is the pin, and it is a compile-time one
34+
*
35+
* `Door<T>` reports `'any'` for an `any` door and `'DriverOptions'` only for one
36+
* that is exactly `DriverOptions | undefined`. Each row then `satisfies` it with
37+
* `'DriverOptions'`, so putting any single signature back to `any` fails `tsc` on
38+
* that row with `error TS1360: Type '"DriverOptions"' does not satisfy the
39+
* expected type '"any"'` — naming the method that drifted. A mutual-`extends`
40+
* check could not do this: `any` satisfies both directions and reports green.
41+
*
42+
* # Reverse verification — direction predicted BEFORE it was run, per channel
43+
*
44+
* Revert `turso-driver.ts` to `options?: any` and:
45+
*
46+
* - `pnpm typecheck` goes RED, one error per reverted signature, on the table row
47+
* for that method — plus TS2578 `Unused '@ts-expect-error' directive` on the
48+
* misspelling case below, since under `any` the typo compiles fine.
49+
* - `pnpm test` stays GREEN. Every assertion here is a type-level fact carried by
50+
* a string literal; vitest sees three passing string comparisons either way.
51+
* That split is the point — this defect has no runtime face at all, which is
52+
* why it went 17-for-17 unnoticed.
53+
*
54+
* Measured with all 17 reverted, as predicted: 18 typecheck errors — 17 × TS1360,
55+
* one per row, plus the TS2578 at the misspelling case; `pnpm test` green at 4/4.
56+
*/
57+
58+
import { describe, it, expect } from 'vitest';
59+
import { TursoDriver } from './turso-driver.js';
60+
import type { DriverOptions } from '@objectstack/spec/data';
61+
62+
/** `any` defeats ordinary assignability checks; this is the standard detector. */
63+
type IsAny<T> = 0 extends 1 & T ? true : false;
64+
65+
/**
66+
* Reports what a given `options` door actually is. `'any'` for a widened door,
67+
* `'DriverOptions'` for one that matches the base contract exactly.
68+
*/
69+
type Door<T> = IsAny<T> extends true
70+
? 'any'
71+
: [T] extends [DriverOptions | undefined]
72+
? [DriverOptions | undefined] extends [T]
73+
? 'DriverOptions'
74+
: 'other'
75+
: 'other';
76+
77+
/**
78+
* One row per override, keyed by the method and the positional index of its
79+
* `options` argument. Adding an override with a widened `options` and forgetting
80+
* this table is caught by the exhaustiveness assertion at the end.
81+
*/
82+
const doors = {
83+
// CRUD
84+
find: 'DriverOptions' satisfies Door<Parameters<TursoDriver['find']>[2]>,
85+
findOne: 'DriverOptions' satisfies Door<Parameters<TursoDriver['findOne']>[2]>,
86+
create: 'DriverOptions' satisfies Door<Parameters<TursoDriver['create']>[2]>,
87+
update: 'DriverOptions' satisfies Door<Parameters<TursoDriver['update']>[3]>,
88+
upsert: 'DriverOptions' satisfies Door<Parameters<TursoDriver['upsert']>[3]>,
89+
delete: 'DriverOptions' satisfies Door<Parameters<TursoDriver['delete']>[2]>,
90+
count: 'DriverOptions' satisfies Door<Parameters<TursoDriver['count']>[2]>,
91+
aggregate: 'DriverOptions' satisfies Door<Parameters<TursoDriver['aggregate']>[2]>,
92+
// Bulk
93+
bulkCreate: 'DriverOptions' satisfies Door<Parameters<TursoDriver['bulkCreate']>[2]>,
94+
bulkUpdate: 'DriverOptions' satisfies Door<Parameters<TursoDriver['bulkUpdate']>[2]>,
95+
bulkDelete: 'DriverOptions' satisfies Door<Parameters<TursoDriver['bulkDelete']>[2]>,
96+
updateMany: 'DriverOptions' satisfies Door<Parameters<TursoDriver['updateMany']>[3]>,
97+
deleteMany: 'DriverOptions' satisfies Door<Parameters<TursoDriver['deleteMany']>[2]>,
98+
// Raw execution
99+
execute: 'DriverOptions' satisfies Door<Parameters<TursoDriver['execute']>[2]>,
100+
// Schema
101+
syncSchema: 'DriverOptions' satisfies Door<Parameters<TursoDriver['syncSchema']>[2]>,
102+
syncSchemasBatch: 'DriverOptions' satisfies Door<Parameters<TursoDriver['syncSchemasBatch']>[1]>,
103+
dropTable: 'DriverOptions' satisfies Door<Parameters<TursoDriver['dropTable']>[1]>,
104+
} as const;
105+
106+
describe('[#6402] TursoDriver `options` doors are DriverOptions, all 17 of them', () => {
107+
it('pins every override — each row is a compile-time check, listed here so a drift names the method', () => {
108+
// The assertion that matters already ran in `tsc`. This keeps the count
109+
// honest: a row silently deleted to make a revert compile shows up here.
110+
expect(Object.keys(doors)).toHaveLength(17);
111+
expect(Object.values(doors).every((d) => d === 'DriverOptions')).toBe(true);
112+
});
113+
114+
it('admits the declared keys — the pin is not green because nothing fits', () => {
115+
const declared: Parameters<TursoDriver['update']>[3] = {
116+
bypassTenantAudit: true,
117+
tenantId: 'org_1',
118+
skipCache: true,
119+
timeout: 5_000,
120+
};
121+
expect(declared.tenantId).toBe('org_1');
122+
});
123+
124+
it('refuses the misspelling that motivated the narrowing', () => {
125+
// @ts-expect-error [#6402] `bypassTenantAdit` is not a key of DriverOptions.
126+
const typo: Parameters<TursoDriver['update']>[3] = { bypassTenantAdit: true };
127+
// The typo'd write silently does nothing at runtime — which is the whole
128+
// point: `tsc` above is the only channel that ever objects.
129+
expect(Object.keys(typo!)).toEqual(['bypassTenantAdit']);
130+
});
131+
132+
it('the door is the base contract, not a structural look-alike', () => {
133+
const asBase: DriverOptions | undefined = undefined satisfies Parameters<TursoDriver['count']>[2];
134+
expect(asBase).toBeUndefined();
135+
});
136+
});

0 commit comments

Comments
 (0)