|
| 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