From c570f132cd23c0238d0b3d11dd40627e759e1e1c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 31 Jul 2026 11:33:42 +0000 Subject: [PATCH 1/4] =?UTF-8?q?fix(spec,driver-sql,driver-memory):=20the?= =?UTF-8?q?=20three=20drivers=20type-check=20=E2=80=94=20and=20165=20of=20?= =?UTF-8?q?their=20292=20errors=20were=20the=20types,=20not=20the=20tests?= =?UTF-8?q?=20(#4311)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `driver-sql` (241), `driver-sqlite-wasm` (27) and `driver-memory` (23) were filed as one playbook: author-tier literals handed to a parsed-tier parameter. That is what 127 of them were. The other 165 were the types being wrong, and only a tsc that had never run could have kept them invisible: - **`bypassTenantAudit` was never on `DriverOptionsSchema`** (118 errors). `SqlDriver.auditMissingTenant` reads it, its own warning text tells callers to pass it, `ObjectQLEngine` sets it for system-context calls, and both `service-settings` and `service-datasource` send it on global-scope writes. Declared now, with the limit stated: it silences a diagnostic, it never changes which rows a write touches. The driver read it — and `timezone`, `tenantId`, `tenantIds`, `preserveAudit`, all four long since declared — through `(options as any)`; those seven casts are gone, so the next undeclared option fails the build instead of hiding behind one. - **`findOne(object, id)` was on no contract** (41). An undeclared `typeof query === 'string'` branch in `SqlDriver`, used by nothing outside this package's own tests, and answered differently by the other two drivers: `MemoryDriver` spreads the string (`{0:'t',1:'1'}`), `MongoDriver` reads `query.where` (undefined → an arbitrary row). It also bypassed `find()`, so it skipped field selection, temporal coercion and the deterministic-order tie-breaker `IDataDriver.find` promises. Removed; the call sites spell the lookup as `{ object, where: { id } }`. - **`initObjects` did not declare the `tenancy` it consumes** (4) — each object flows into `computeAndRecordTenantField`, which reads `obj.tenancy` to pick the tenant column and set the sticky opt-out. `registerExternalObject` had it right all along. - **`AnalyticsQuery` has no author tier** (19+1). `timezone` is `.default('UTC')` and `public` is `.default(false)`, so the parsed types require both; the tests wrote author-tier literals. Added `AnalyticsQueryInput` beside `AnalyticsQuery` (the `QueryInput`/`QueryAST` pattern) and routed the literals through the schema, so the parse itself is the proof the default lands — `defineCube` does the same job for the cube. - **`InMemoryDriver.create` declared no return type** (3), so TS inferred the literal it builds and every other column of the created row vanished from the caller's view. The remaining 127 are the filed playbook: `object` added to the query literal (the driver reads the name from parameter 1 — the AST field is required and redundant there, filed separately), and `count`/`find` calls likewise. Six call sites the type checker could not see at all wrote `findOne('users', alice.id as any)`; the cast is what let an off-contract call survive a narrowing. They surfaced as runtime failures the moment the branch was removed, which is the point. All three packages now declare `typecheck` and leave the DEBT ledger: 60/77 packages covered, 17 ledgered. 1069 tests green across the three. Claude-Session: https://claude.ai/code/session_011m13wbaZziPveBdtQthrXd Co-authored-by: Claude --- packages/plugins/driver-memory/package.json | 3 +- .../src/memory-analytics.test.ts | 99 +++++++++++-------- .../driver-memory/src/memory-driver.ts | 9 +- packages/plugins/driver-sql/package.json | 3 +- .../src/sql-driver-advanced.test.ts | 34 ++++--- .../src/sql-driver-array-fields.test.ts | 6 +- .../sql-driver-date-now-default-live.test.ts | 2 +- .../src/sql-driver-date-only.test.ts | 20 ++-- ...-driver-datetime-canonical-storage.test.ts | 6 +- ...river-datetime-filter-text-storage.test.ts | 19 +++- .../src/sql-driver-datetime-filter.test.ts | 7 ++ .../sql-driver-datetime-mysql-storage.test.ts | 8 +- ...-driver-datetime-postgres-timezone.test.ts | 4 +- .../sql-driver-external-remote-name.test.ts | 2 +- .../src/sql-driver-like-escape.test.ts | 6 +- .../src/sql-driver-numeric-fidelity.test.ts | 12 +-- .../src/sql-driver-or-filter.test.ts | 4 +- .../src/sql-driver-retention-prune.test.ts | 8 +- .../driver-sql/src/sql-driver-schema.test.ts | 12 +-- .../src/sql-driver-server-timing.test.ts | 2 +- .../sql-driver-time-canonical-storage.test.ts | 9 +- .../src/sql-driver-time-live-dialects.test.ts | 13 +-- .../src/sql-driver-time-of-day.test.ts | 12 +-- .../src/sql-driver-timestamp-format.test.ts | 8 +- ...river-user-datetime-default-format.test.ts | 16 +-- .../plugins/driver-sql/src/sql-driver.test.ts | 21 ++-- packages/plugins/driver-sql/src/sql-driver.ts | 56 ++++++----- .../plugins/driver-sqlite-wasm/package.json | 3 +- .../src/sqlite-wasm-driver-advanced.test.ts | 34 ++++--- .../src/sqlite-wasm-driver-schema.test.ts | 6 +- .../src/sqlite-wasm-driver.test.ts | 20 ++-- packages/spec/authorable-surface.json | 1 + packages/spec/src/data/analytics.zod.ts | 13 +++ packages/spec/src/data/driver.zod.ts | 20 ++++ scripts/check-type-check-coverage.mjs | 30 +++--- 35 files changed, 322 insertions(+), 206 deletions(-) diff --git a/packages/plugins/driver-memory/package.json b/packages/plugins/driver-memory/package.json index f330baaf53..24a94928fd 100644 --- a/packages/plugins/driver-memory/package.json +++ b/packages/plugins/driver-memory/package.json @@ -15,7 +15,8 @@ "scripts": { "build": "tsup --config ../../../tsup.config.ts", "dev": "tsc -w", - "test": "vitest run" + "test": "vitest run", + "typecheck": "tsc --noEmit" }, "dependencies": { "@objectstack/core": "workspace:*", diff --git a/packages/plugins/driver-memory/src/memory-analytics.test.ts b/packages/plugins/driver-memory/src/memory-analytics.test.ts index 45d8008562..1d42b13245 100644 --- a/packages/plugins/driver-memory/src/memory-analytics.test.ts +++ b/packages/plugins/driver-memory/src/memory-analytics.test.ts @@ -3,7 +3,21 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { InMemoryDriver } from './memory-driver.js'; import { MemoryAnalyticsService } from './memory-analytics.js'; -import type { Cube } from '@objectstack/spec/data'; +import { AnalyticsQuerySchema, defineCube } from '@objectstack/spec/data'; +import type { AnalyticsQuery, AnalyticsQueryInput, Cube } from '@objectstack/spec/data'; + +/** + * Author-tier literal → the parsed `AnalyticsQuery` the service contract takes. + * + * `timezone` is `.default('UTC')` on the schema, so it is optional to write and + * required on the parsed type — the two tiers are genuinely different types. A + * real query reaches `query()` through the schema (the REST layer parses the + * request body), so these tests take the same route rather than hand-writing + * the filled-in default: the parse IS the proof that the default lands. Until + * #4311 no tsc read this file, so 19 author-tier literals sat unnoticed in a + * parameter that had required `timezone` all along. + */ +const asQuery = (input: AnalyticsQueryInput): AnalyticsQuery => AnalyticsQuerySchema.parse(input); describe('MemoryAnalyticsService', () => { let driver: InMemoryDriver; @@ -160,10 +174,10 @@ describe('MemoryAnalyticsService', () => { describe('query', () => { it('should execute a simple count query', async () => { - const result = await service.query({ + const result = await service.query(asQuery({ cube: 'orders', measures: ['orders.count'] - }); + })); expect(result.rows).toHaveLength(1); expect(result.rows[0]['orders.count']).toBe(5); @@ -173,11 +187,11 @@ describe('MemoryAnalyticsService', () => { }); it('should group by a dimension', async () => { - const result = await service.query({ + const result = await service.query(asQuery({ cube: 'orders', measures: ['orders.count'], dimensions: ['orders.status'] - }); + })); expect(result.rows).toHaveLength(3); // completed, pending, cancelled @@ -187,11 +201,11 @@ describe('MemoryAnalyticsService', () => { }); it('should calculate sum aggregation', async () => { - const result = await service.query({ + const result = await service.query(asQuery({ cube: 'orders', measures: ['orders.totalAmount'], dimensions: ['orders.customer'] - }); + })); const aliceRow = result.rows.find(r => r['orders.customer'] === 'Alice'); expect(aliceRow).toBeDefined(); @@ -199,11 +213,11 @@ describe('MemoryAnalyticsService', () => { }); it('should calculate average aggregation', async () => { - const result = await service.query({ + const result = await service.query(asQuery({ cube: 'products', measures: ['products.avgPrice'], dimensions: ['products.category'] - }); + })); const electronicsRow = result.rows.find(r => r['products.category'] === 'electronics'); expect(electronicsRow).toBeDefined(); @@ -211,10 +225,10 @@ describe('MemoryAnalyticsService', () => { }); it('should support multiple measures', async () => { - const result = await service.query({ + const result = await service.query(asQuery({ cube: 'orders', measures: ['orders.count', 'orders.totalAmount', 'orders.avgAmount'] - }); + })); expect(result.rows).toHaveLength(1); expect(result.rows[0]['orders.count']).toBe(5); @@ -223,11 +237,11 @@ describe('MemoryAnalyticsService', () => { }); it('should apply filters via canonical `where`', async () => { - const result = await service.query({ + const result = await service.query(asQuery({ cube: 'orders', measures: ['orders.count', 'orders.totalAmount'], where: { 'orders.status': 'completed' }, - }); + })); expect(result.rows).toHaveLength(1); expect(result.rows[0]['orders.count']).toBe(3); @@ -235,11 +249,11 @@ describe('MemoryAnalyticsService', () => { }); it('should accept FilterCondition (short-form equality)', async () => { - const result = await service.query({ + const result = await service.query(asQuery({ cube: 'orders', measures: ['orders.count', 'orders.totalAmount'], where: { status: 'completed' }, - }); + })); expect(result.rows).toHaveLength(1); expect(result.rows[0]['orders.count']).toBe(3); @@ -247,32 +261,32 @@ describe('MemoryAnalyticsService', () => { }); it('should accept FilterCondition with $in operator', async () => { - const result = await service.query({ + const result = await service.query(asQuery({ cube: 'orders', measures: ['orders.count'], where: { status: { $in: ['completed', 'pending'] } }, - }); + })); expect(result.rows[0]['orders.count']).toBe(4); // 3 completed + 1 pending }); it('should accept FilterCondition with $gte operator', async () => { - const result = await service.query({ + const result = await service.query(asQuery({ cube: 'orders', measures: ['orders.count'], where: { amount: { $gte: 200 } }, - }); + })); expect(result.rows[0]['orders.count']).toBe(2); // 200, 300 }); it('should support sorting', async () => { - const result = await service.query({ + const result = await service.query(asQuery({ cube: 'orders', measures: ['orders.totalAmount'], dimensions: ['orders.customer'], order: { 'orders.totalAmount': 'desc' } - }); + })); expect(result.rows[0]['orders.customer']).toBe('Charlie'); // 300 expect(result.rows[1]['orders.customer']).toBe('Alice'); // 250 @@ -280,14 +294,14 @@ describe('MemoryAnalyticsService', () => { }); it('should support limit and offset', async () => { - const result = await service.query({ + const result = await service.query(asQuery({ cube: 'orders', measures: ['orders.count'], dimensions: ['orders.customer'], order: { 'orders.customer': 'asc' }, limit: 2, offset: 1 - }); + })); expect(result.rows).toHaveLength(2); expect(result.rows[0]['orders.customer']).toBe('Bob'); @@ -299,7 +313,10 @@ describe('MemoryAnalyticsService', () => { // 'amount' of type 'sum'). Clients that build measure names from // (field, function) pairs send 'amount_sum' — the resolver should // accept that alias and produce the same aggregate value. - const aliasCube: Cube = { + // `defineCube` rather than a bare `: Cube` literal: `public` is + // `.default(false)`, so it is required on the parsed `Cube` type and + // absent here — the factory is the spec's own answer to that split. + const aliasCube = defineCube({ name: 'opps', title: 'Opps', sql: 'orders', @@ -309,14 +326,14 @@ describe('MemoryAnalyticsService', () => { dimensions: { status: { name: 'status', label: 'Status', type: 'string', sql: 'status' }, }, - }; + }); const aliasService = new MemoryAnalyticsService({ driver, cubes: [aliasCube] }); - const aliased = await aliasService.query({ + const aliased = await aliasService.query(asQuery({ cube: 'opps', measures: ['amount_sum'], dimensions: ['status'], - }); + })); const completed = aliased.rows.find(r => r.status === 'completed'); expect(completed).toBeDefined(); @@ -325,18 +342,18 @@ describe('MemoryAnalyticsService', () => { it('should throw error for unknown cube', async () => { await expect(async () => { - await service.query({ + await service.query(asQuery({ cube: 'unknown', measures: ['unknown.count'] - }); + })); }).rejects.toThrow('Cube not found: unknown'); }); it('should include SQL in result for debugging', async () => { - const result = await service.query({ + const result = await service.query(asQuery({ cube: 'orders', measures: ['orders.count'] - }); + })); expect(result.sql).toBeDefined(); expect(result.sql).toContain('orders'); @@ -345,10 +362,10 @@ describe('MemoryAnalyticsService', () => { describe('generateSql', () => { it('should generate SQL for a simple query', async () => { - const result = await service.generateSql({ + const result = await service.generateSql(asQuery({ cube: 'orders', measures: ['orders.count'] - }); + })); expect(result.sql).toContain('SELECT'); expect(result.sql).toContain('COUNT(*)'); @@ -356,45 +373,45 @@ describe('MemoryAnalyticsService', () => { }); it('should generate SQL with GROUP BY', async () => { - const result = await service.generateSql({ + const result = await service.generateSql(asQuery({ cube: 'orders', measures: ['orders.count'], dimensions: ['orders.status'] - }); + })); expect(result.sql).toContain('GROUP BY status'); }); it('should generate SQL with WHERE clause', async () => { - const result = await service.generateSql({ + const result = await service.generateSql(asQuery({ cube: 'orders', measures: ['orders.count'], where: { 'orders.status': 'completed' }, - }); + })); expect(result.sql).toContain('WHERE'); expect(result.sql).toContain('status'); }); it('should generate SQL with ORDER BY', async () => { - const result = await service.generateSql({ + const result = await service.generateSql(asQuery({ cube: 'orders', measures: ['orders.count'], dimensions: ['orders.status'], order: { 'orders.status': 'asc' } - }); + })); expect(result.sql).toContain('ORDER BY'); expect(result.sql).toContain('ASC'); }); it('should generate SQL with LIMIT and OFFSET', async () => { - const result = await service.generateSql({ + const result = await service.generateSql(asQuery({ cube: 'orders', measures: ['orders.count'], limit: 10, offset: 5 - }); + })); expect(result.sql).toContain('LIMIT 10'); expect(result.sql).toContain('OFFSET 5'); diff --git a/packages/plugins/driver-memory/src/memory-driver.ts b/packages/plugins/driver-memory/src/memory-driver.ts index 77696cfa53..229f13ad86 100644 --- a/packages/plugins/driver-memory/src/memory-driver.ts +++ b/packages/plugins/driver-memory/src/memory-driver.ts @@ -377,7 +377,12 @@ export class InMemoryDriver implements IDataDriver { return result; } - async create(object: string, data: Record, options?: DriverOptions) { + // The `IDataDriver.create` return type, spelled out: without it TS infers the + // literal built below (`{ id, created_at, updated_at }`) and every OTHER + // column of the created row vanishes from the caller's view (#4311 — the + // driver's own tests read `.name` off a create() result and no tsc had ever + // told them it wasn't there). + async create(object: string, data: Record, options?: DriverOptions): Promise> { this.logger.debug('Create operation', { object, hasData: !!data }); const table = this.getTable(object); @@ -482,7 +487,7 @@ export class InMemoryDriver implements IDataDriver { // Bulk Operations // =================================== - async bulkCreate(object: string, dataArray: Record[], options?: DriverOptions) { + async bulkCreate(object: string, dataArray: Record[], options?: DriverOptions): Promise[]> { this.logger.debug('BulkCreate operation', { object, count: dataArray.length }); const results = await Promise.all(dataArray.map(data => this.create(object, data, options))); this.logger.debug('BulkCreate completed', { object, count: results.length }); diff --git a/packages/plugins/driver-sql/package.json b/packages/plugins/driver-sql/package.json index fac695d484..3150c1d7a1 100644 --- a/packages/plugins/driver-sql/package.json +++ b/packages/plugins/driver-sql/package.json @@ -15,7 +15,8 @@ "scripts": { "build": "tsup --config ../../../tsup.config.ts", "dev": "tsc -w", - "test": "vitest run" + "test": "vitest run", + "typecheck": "tsc --noEmit" }, "dependencies": { "@objectstack/core": "workspace:*", diff --git a/packages/plugins/driver-sql/src/sql-driver-advanced.test.ts b/packages/plugins/driver-sql/src/sql-driver-advanced.test.ts index 1fa42c9379..97a4d0a4b3 100644 --- a/packages/plugins/driver-sql/src/sql-driver-advanced.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-advanced.test.ts @@ -139,7 +139,7 @@ describe('SqlDriver Advanced Operations (SQLite)', () => { expect(result).toBeDefined(); expect(result.length).toBe(3); - const count = await driver.count('orders', {}); + const count = await driver.count('orders', { object: 'orders' }); expect(count).toBe(8); }); @@ -148,7 +148,7 @@ describe('SqlDriver Advanced Operations (SQLite)', () => { expect(result).toBeGreaterThan(0); - const results = await driver.find('orders', { where: { status: 'processing' } }); + const results = await driver.find('orders', { object: 'orders', where: { status: 'processing' } }); expect(results.length).toBe(1); }); @@ -157,7 +157,7 @@ describe('SqlDriver Advanced Operations (SQLite)', () => { expect(result).toBe(1); - const remaining = await driver.count('orders', {}); + const remaining = await driver.count('orders', { object: 'orders' }); expect(remaining).toBe(4); }); @@ -190,7 +190,7 @@ describe('SqlDriver Advanced Operations (SQLite)', () => { await driver.commitTransaction(trx); - const result = await driver.findOne('orders', 'trx1' as any); + const result = await driver.findOne('orders', { object: 'orders', where: { id: 'trx1' } }); expect(result).toBeDefined(); expect(result.customer).toBe('TxUser'); } catch (e) { @@ -218,7 +218,7 @@ describe('SqlDriver Advanced Operations (SQLite)', () => { await driver.rollbackTransaction(trx); - const result = await driver.findOne('orders', 'trx2' as any); + const result = await driver.findOne('orders', { object: 'orders', where: { id: 'trx2' } }); expect(result).toBeNull(); } catch (e) { await driver.rollbackTransaction(trx); @@ -249,13 +249,13 @@ describe('SqlDriver Advanced Operations (SQLite)', () => { await driver.commitTransaction(trx); - const created = await driver.findOne('orders', 'trx3' as any); + const created = await driver.findOne('orders', { object: 'orders', where: { id: 'trx3' } }); expect(created).toBeDefined(); - const updated = await driver.findOne('orders', '1' as any); + const updated = await driver.findOne('orders', { object: 'orders', where: { id: '1' } }); expect(updated.status).toBe('shipped'); - const deleted = await driver.findOne('orders', '5' as any); + const deleted = await driver.findOne('orders', { object: 'orders', where: { id: '5' } }); expect(deleted).toBeNull(); } catch (e) { await driver.rollbackTransaction(trx); @@ -266,12 +266,12 @@ describe('SqlDriver Advanced Operations (SQLite)', () => { describe('Edge Cases and Error Handling', () => { it('should handle empty filters gracefully', async () => { - const results = await driver.find('orders', { where: {} }); + const results = await driver.find('orders', { object: 'orders', where: {} }); expect(results.length).toBe(5); }); it('should handle undefined query parameters', async () => { - const results = await driver.find('orders', {}); + const results = await driver.find('orders', { object: 'orders' }); expect(results.length).toBe(5); }); @@ -284,7 +284,7 @@ describe('SqlDriver Advanced Operations (SQLite)', () => { await driver.create('nullable_test', { id: '1', name: null, value: null }); - const result = await driver.findOne('nullable_test', '1' as any); + const result = await driver.findOne('nullable_test', { object: 'nullable_test', where: { id: '1' } }); expect(result).toBeDefined(); expect(result.name).toBeNull(); expect(result.value).toBeNull(); @@ -292,6 +292,7 @@ describe('SqlDriver Advanced Operations (SQLite)', () => { it('should handle pagination with offset and limit', async () => { const page1 = await driver.find('orders', { + object: 'orders', orderBy: [{ field: 'id', order: 'asc' }], offset: 0, limit: 2, @@ -300,6 +301,7 @@ describe('SqlDriver Advanced Operations (SQLite)', () => { expect(page1[0].id).toBe('1'); const page2 = await driver.find('orders', { + object: 'orders', orderBy: [{ field: 'id', order: 'asc' }], offset: 2, limit: 2, @@ -309,12 +311,13 @@ describe('SqlDriver Advanced Operations (SQLite)', () => { }); it('should handle offset beyond total records', async () => { - const results = await driver.find('orders', { offset: 100, limit: 10 }); + const results = await driver.find('orders', { object: 'orders', offset: 100, limit: 10 }); expect(results.length).toBe(0); }); it('should handle complex nested filters', async () => { const results = await driver.find('orders', { + object: 'orders', where: { $or: [ { $and: [{ status: 'completed' }, { amount: { $gt: 100 } }] }, @@ -335,6 +338,7 @@ describe('SqlDriver Advanced Operations (SQLite)', () => { it('should handle contains filter', async () => { const results = await driver.find('orders', { + object: 'orders', where: { product: { $contains: 'top' } }, }); @@ -344,6 +348,7 @@ describe('SqlDriver Advanced Operations (SQLite)', () => { it('should handle in filter', async () => { const results = await driver.find('orders', { + object: 'orders', where: { status: { $in: ['completed', 'pending'] } }, }); @@ -352,6 +357,7 @@ describe('SqlDriver Advanced Operations (SQLite)', () => { it('should handle nin (not in) filter', async () => { const results = await driver.find('orders', { + object: 'orders', where: { status: { $nin: ['cancelled'] } }, }); @@ -359,14 +365,14 @@ describe('SqlDriver Advanced Operations (SQLite)', () => { }); it('should handle findOne with query parameter', async () => { - const result = await driver.findOne('orders', { where: { customer: 'Charlie' } }); + const result = await driver.findOne('orders', { object: 'orders', where: { customer: 'Charlie' } }); expect(result).toBeDefined(); expect(result.customer).toBe('Charlie'); }); it('should return null for non-existent record', async () => { - const result = await driver.findOne('orders', 'nonexistent' as any); + const result = await driver.findOne('orders', { object: 'orders', where: { id: 'nonexistent' } }); expect(result).toBeNull(); }); diff --git a/packages/plugins/driver-sql/src/sql-driver-array-fields.test.ts b/packages/plugins/driver-sql/src/sql-driver-array-fields.test.ts index cff141738e..a1e01edda9 100644 --- a/packages/plugins/driver-sql/src/sql-driver-array-fields.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-array-fields.test.ts @@ -63,7 +63,7 @@ describe('SqlDriver array/object field persistence', () => { }, { bypassTenantAudit: true }, ); - const row = await driver.findOne('zoo', 'z1', { bypassTenantAudit: true }); + const row = await driver.findOne('zoo', { object: 'zoo', where: { id: 'z1' } }, { bypassTenantAudit: true }); expect(row.tags).toEqual(['x', 'y']); expect(row.ms).toEqual(['red', 'green']); expect(row.cbs).toEqual(['email', 'push']); @@ -76,13 +76,13 @@ describe('SqlDriver array/object field persistence', () => { it('updates an array field to a new array', async () => { await driver.create('zoo', { id: 'z2', name: 'B', tags: ['a'] }, { bypassTenantAudit: true }); await driver.update('zoo', 'z2', { tags: ['a', 'b', 'c'] }, { bypassTenantAudit: true }); - const row = await driver.findOne('zoo', 'z2', { bypassTenantAudit: true }); + const row = await driver.findOne('zoo', { object: 'zoo', where: { id: 'z2' } }, { bypassTenantAudit: true }); expect(row.tags).toEqual(['a', 'b', 'c']); }); it('does not crash on an empty array', async () => { await driver.create('zoo', { id: 'z3', name: 'C', ms: [] }, { bypassTenantAudit: true }); - const row = await driver.findOne('zoo', 'z3', { bypassTenantAudit: true }); + const row = await driver.findOne('zoo', { object: 'zoo', where: { id: 'z3' } }, { bypassTenantAudit: true }); expect(row.ms).toEqual([]); }); }); diff --git a/packages/plugins/driver-sql/src/sql-driver-date-now-default-live.test.ts b/packages/plugins/driver-sql/src/sql-driver-date-now-default-live.test.ts index 3527249a22..7b1fa32839 100644 --- a/packages/plugins/driver-sql/src/sql-driver-date-now-default-live.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-date-now-default-live.test.ts @@ -73,7 +73,7 @@ function suite(dialect: 'pg' | 'mysql', url: string | undefined) { it('a defaulted insert round-trips a valid canonical calendar day', async () => { await driver.create(TABLE, { id: 'x', label: 'x' }, { bypassTenantAudit: true }); - const row: any = await driver.findOne(TABLE, 'x', { bypassTenantAudit: true }); + const row: any = await driver.findOne(TABLE, { object: TABLE, where: { id: 'x' } }, { bypassTenantAudit: true }); expect(String(row.due_on)).toMatch(/^\d{4}-\d{2}-\d{2}$/); // The UTC calendar day only ever differs from this probe's own UTC clock // across a midnight boundary — accept today or the day either side rather diff --git a/packages/plugins/driver-sql/src/sql-driver-date-only.test.ts b/packages/plugins/driver-sql/src/sql-driver-date-only.test.ts index 7e7f248607..b486adb174 100644 --- a/packages/plugins/driver-sql/src/sql-driver-date-only.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-date-only.test.ts @@ -49,7 +49,7 @@ describe('SqlDriver Field.date is a tz-naive calendar day (ADR-0053 Phase 1)', ( { id: 'd1', name: 'A', close_date: new Date('2026-07-15T17:24:56.533Z') }, { bypassTenantAudit: true }, ); - const row = await driver.findOne('deal', 'd1', { bypassTenantAudit: true }); + const row = await driver.findOne('deal', { object: 'deal', where: { id: 'd1' } }, { bypassTenantAudit: true }); expect(row.close_date).toBe('2026-07-15'); }); @@ -59,7 +59,7 @@ describe('SqlDriver Field.date is a tz-naive calendar day (ADR-0053 Phase 1)', ( { id: 'd2', name: 'B', close_date: '2026-07-15T17:24:56.533Z' }, { bypassTenantAudit: true }, ); - const row = await driver.findOne('deal', 'd2', { bypassTenantAudit: true }); + const row = await driver.findOne('deal', { object: 'deal', where: { id: 'd2' } }, { bypassTenantAudit: true }); expect(row.close_date).toBe('2026-07-15'); }); @@ -69,7 +69,7 @@ describe('SqlDriver Field.date is a tz-naive calendar day (ADR-0053 Phase 1)', ( { id: 'd3', name: 'C', close_date: '2026-07-15' }, { bypassTenantAudit: true }, ); - const row = await driver.findOne('deal', 'd3', { bypassTenantAudit: true }); + const row = await driver.findOne('deal', { object: 'deal', where: { id: 'd3' } }, { bypassTenantAudit: true }); expect(row.close_date).toBe('2026-07-15'); }); @@ -79,7 +79,7 @@ describe('SqlDriver Field.date is a tz-naive calendar day (ADR-0053 Phase 1)', ( { id: 'd4', name: 'D', signed_at: new Date('2026-03-20T12:34:56.000Z') }, { bypassTenantAudit: true }, ); - const row = await driver.findOne('deal', 'd4', { bypassTenantAudit: true }); + const row = await driver.findOne('deal', { object: 'deal', where: { id: 'd4' } }, { bypassTenantAudit: true }); // datetime must retain its wall-clock time — never sliced to YYYY-MM-DD. expect(new Date(row.signed_at).toISOString()).toBe('2026-03-20T12:34:56.000Z'); }); @@ -92,7 +92,7 @@ describe('SqlDriver Field.date is a tz-naive calendar day (ADR-0053 Phase 1)', ( { id: 'd5', name: 'E', close_date: new Date('2026-07-15T17:24:56.533Z') }, { bypassTenantAudit: true }, ); - const rows = await driver.find('deal', { where: { close_date: '2026-07-15' } }); + const rows = await driver.find('deal', { object: 'deal', where: { close_date: '2026-07-15' } }); expect(rows.map((r: any) => r.id)).toEqual(['d5']); }); @@ -100,7 +100,7 @@ describe('SqlDriver Field.date is a tz-naive calendar day (ADR-0053 Phase 1)', ( await driver.create('deal', { id: 'd6', name: 'F', close_date: new Date('2026-07-15T08:00:00Z') }, { bypassTenantAudit: true }); await driver.create('deal', { id: 'd7', name: 'G', close_date: '2026-07-16' }, { bypassTenantAudit: true }); await driver.create('deal', { id: 'd8', name: 'H', close_date: '2026-07-17' }, { bypassTenantAudit: true }); - const rows = await driver.find('deal', { where: { close_date: { $in: ['2026-07-15', '2026-07-17'] } } }); + const rows = await driver.find('deal', { object: 'deal', where: { close_date: { $in: ['2026-07-15', '2026-07-17'] } } }); expect(rows.map((r: any) => r.id).sort()).toEqual(['d6', 'd8']); }); @@ -108,7 +108,7 @@ describe('SqlDriver Field.date is a tz-naive calendar day (ADR-0053 Phase 1)', ( await driver.create('deal', { id: 'r1', close_date: '2025-01-15' }, { bypassTenantAudit: true }); await driver.create('deal', { id: 'r2', close_date: '2026-03-20' }, { bypassTenantAudit: true }); await driver.create('deal', { id: 'r3', close_date: '2026-05-25' }, { bypassTenantAudit: true }); - const rows = await driver.find('deal', { where: { close_date: { $gte: '2026-01-01', $lt: '2026-05-01' } } }); + const rows = await driver.find('deal', { object: 'deal', where: { close_date: { $gte: '2026-01-01', $lt: '2026-05-01' } } }); expect(rows.map((r: any) => r.id)).toEqual(['r2']); }); @@ -119,7 +119,7 @@ describe('SqlDriver Field.date is a tz-naive calendar day (ADR-0053 Phase 1)', ( await (driver as any).knex('deal').insert({ id: 'legacy', name: 'L', close_date: '2026-08-15T17:24:56.533Z' }); // Read-side repair: the returned value is date-only with no migration. - const row = await driver.findOne('deal', 'legacy', { bypassTenantAudit: true }); + const row = await driver.findOne('deal', { object: 'deal', where: { id: 'legacy' } }, { bypassTenantAudit: true }); expect(row.close_date).toBe('2026-08-15'); // …but the value still stored in SQL keeps its time, so a SQL equality @@ -127,13 +127,13 @@ describe('SqlDriver Field.date is a tz-naive calendar day (ADR-0053 Phase 1)', ( // ADR-0053 calls out: read-repair fixes display/read, and an optional // one-time migration (or any write through the normalized path) rewrites // legacy rows at rest. - const beforeRewrite = await driver.find('deal', { where: { close_date: '2026-08-15' } }); + const beforeRewrite = await driver.find('deal', { object: 'deal', where: { close_date: '2026-08-15' } }); expect(beforeRewrite.map((r: any) => r.id)).toEqual([]); // Rewriting through the normalized write path (formatInput) collapses the // stored value to date-only, after which the equality filter matches. await driver.update('deal', 'legacy', { close_date: '2026-08-15' }, { bypassTenantAudit: true }); - const afterRewrite = await driver.find('deal', { where: { close_date: '2026-08-15' } }); + const afterRewrite = await driver.find('deal', { object: 'deal', where: { close_date: '2026-08-15' } }); expect(afterRewrite.map((r: any) => r.id)).toEqual(['legacy']); }); }); diff --git a/packages/plugins/driver-sql/src/sql-driver-datetime-canonical-storage.test.ts b/packages/plugins/driver-sql/src/sql-driver-datetime-canonical-storage.test.ts index 8ac02bb52b..7d32b25824 100644 --- a/packages/plugins/driver-sql/src/sql-driver-datetime-canonical-storage.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-datetime-canonical-storage.test.ts @@ -100,7 +100,7 @@ describe('Field.datetime writes land in ONE canonical form (#3912)', () => { ); } - const rows = await driver.find('evt', { orderBy: [{ field: 'at', order: 'asc' }] }); + const rows = await driver.find('evt', { object: 'evt', orderBy: [{ field: 'at', order: 'asc' }] }); expect(rows.map((r: any) => r.id)).toEqual(instants.map(([id]) => id)); // …and the raw column sorts identically, i.e. the DB did the ordering. @@ -188,12 +188,12 @@ describe('backfillCanonicalDatetimes converges a legacy database (#3912)', () => // it fast. Both must agree, or the migration would be observable as a change // in results — which is exactly what it must never be. const window = { at: { $gte: '2026-03-20T00:00:00.000Z', $lte: '2026-03-21T00:00:00.000Z' } }; - const before = (await driver.find('evt', { where: window })).map((r: any) => r.id).sort(); + const before = (await driver.find('evt', { object: 'evt', where: window })).map((r: any) => r.id).sort(); expect(before).toEqual(['canon', 'epoch', 'naive', 'offset']); await (driver as any).backfillCanonicalDatetimes('evt', true); - const after = (await driver.find('evt', { where: window })).map((r: any) => r.id).sort(); + const after = (await driver.find('evt', { object: 'evt', where: window })).map((r: any) => r.id).sort(); expect(after).toEqual(before); }); diff --git a/packages/plugins/driver-sql/src/sql-driver-datetime-filter-text-storage.test.ts b/packages/plugins/driver-sql/src/sql-driver-datetime-filter-text-storage.test.ts index c3c2753b1e..840e08e016 100644 --- a/packages/plugins/driver-sql/src/sql-driver-datetime-filter-text-storage.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-datetime-filter-text-storage.test.ts @@ -68,6 +68,7 @@ describe('SqlDriver datetime filters on ISO-TEXT-stored columns (#3912)', () => it('matches $gte against an ISO date string', async () => { const rows = await driver.find('lead', { + object: 'lead', where: { created_date: { $gte: '2026-01-01' } }, orderBy: [{ field: 'id', order: 'asc' }], }); @@ -76,6 +77,7 @@ describe('SqlDriver datetime filters on ISO-TEXT-stored columns (#3912)', () => it('matches a $gte / $lt window — the dashboard dateRange shape', async () => { const rows = await driver.find('lead', { + object: 'lead', where: { created_date: { $gte: '2026-01-01', $lt: '2026-05-01' } }, }); expect(rows.map((r: any) => r.id)).toEqual(['l2']); @@ -83,11 +85,13 @@ describe('SqlDriver datetime filters on ISO-TEXT-stored columns (#3912)', () => it('matches a full ISO timestamp comparand, to the millisecond', async () => { const inclusive = await driver.find('lead', { + object: 'lead', where: { created_date: { $gte: '2026-05-25T08:30:15.250Z' } }, }); expect(inclusive.map((r: any) => r.id)).toEqual(['l3']); const exclusive = await driver.find('lead', { + object: 'lead', where: { created_date: { $gt: '2026-05-25T08:30:15.250Z' } }, }); expect(exclusive).toEqual([]); @@ -95,6 +99,7 @@ describe('SqlDriver datetime filters on ISO-TEXT-stored columns (#3912)', () => it('matches a JS Date comparand against a TEXT-stored row', async () => { const rows = await driver.find('lead', { + object: 'lead', where: { created_date: { $gte: new Date('2026-05-01T00:00:00Z') } }, }); expect(rows.map((r: any) => r.id)).toEqual(['l3']); @@ -102,6 +107,7 @@ describe('SqlDriver datetime filters on ISO-TEXT-stored columns (#3912)', () => it('matches equality on the exact stored instant', async () => { const rows = await driver.find('lead', { + object: 'lead', where: { created_date: '2026-03-20T12:00:00.000Z' }, }); expect(rows.map((r: any) => r.id)).toEqual(['l2']); @@ -109,17 +115,20 @@ describe('SqlDriver datetime filters on ISO-TEXT-stored columns (#3912)', () => it('matches $between, $in and $nin', async () => { const between = await driver.find('lead', { + object: 'lead', where: { created_date: { $between: ['2026-01-01', '2026-04-01'] } }, }); expect(between.map((r: any) => r.id)).toEqual(['l2']); const isIn = await driver.find('lead', { + object: 'lead', where: { created_date: { $in: ['2026-03-20T12:00:00.000Z', '2026-05-25T08:30:15.250Z'] } }, orderBy: [{ field: 'id', order: 'asc' }], }); expect(isIn.map((r: any) => r.id)).toEqual(['l2', 'l3']); const notIn = await driver.find('lead', { + object: 'lead', where: { created_date: { $nin: ['2026-03-20T12:00:00.000Z'] } }, orderBy: [{ field: 'id', order: 'asc' }], }); @@ -128,16 +137,18 @@ describe('SqlDriver datetime filters on ISO-TEXT-stored columns (#3912)', () => it('matches $ne and the null predicates', async () => { const ne = await driver.find('lead', { + object: 'lead', where: { created_date: { $ne: '2026-03-20T12:00:00.000Z' } }, orderBy: [{ field: 'id', order: 'asc' }], }); expect(ne.map((r: any) => r.id)).toEqual(['l1', 'l3']); await driver.create('lead', { id: 'l4', name: 'Undated' }, { bypassTenantAudit: true }); - const missing = await driver.find('lead', { where: { created_date: { $null: true } } }); + const missing = await driver.find('lead', { object: 'lead', where: { created_date: { $null: true } } }); expect(missing.map((r: any) => r.id)).toEqual(['l4']); const present = await driver.find('lead', { + object: 'lead', where: { created_date: { $null: false } }, orderBy: [{ field: 'id', order: 'asc' }], }); @@ -153,6 +164,7 @@ describe('SqlDriver datetime filters on ISO-TEXT-stored columns (#3912)', () => it('matches inside an $or branch', async () => { const rows = await driver.find('lead', { + object: 'lead', where: { $or: [{ created_date: { $lt: '2025-06-01' } }, { name: 'Newer' }] }, orderBy: [{ field: 'id', order: 'asc' }], }); @@ -161,6 +173,7 @@ describe('SqlDriver datetime filters on ISO-TEXT-stored columns (#3912)', () => it('leaves the Field.date column on its own YYYY-MM-DD rule', async () => { const rows = await driver.find('lead', { + object: 'lead', where: { closed_on: { $gte: '2026-01-01' } }, orderBy: [{ field: 'id', order: 'asc' }], }); @@ -209,6 +222,7 @@ describe('SqlDriver datetime filters on the created_at audit column (#3912)', () expect(stamped[0]).toEqual({ c: 'text', u: 'text' }); const matched = await driver.find('ticket', { + object: 'ticket', where: { created_at: { $gte: '2000-01-01' } }, }); expect(matched.map((r: any) => r.id)).toEqual(['t1']); @@ -216,6 +230,7 @@ describe('SqlDriver datetime filters on the created_at audit column (#3912)', () // …and a window that starts after the stamp excludes it, so the match above // is a real comparison rather than "the predicate was dropped". const future = await driver.find('ticket', { + object: 'ticket', where: { created_at: { $gte: '2999-01-01' } }, }); expect(future).toEqual([]); @@ -260,6 +275,7 @@ describe('SqlDriver datetime filters on a legacy MIXED-storage column (#3912)', it('returns rows of BOTH forms from one window filter', async () => { const rows = await driver.find('lead', { + object: 'lead', where: { created_date: { $gte: '2026-01-01' } }, orderBy: [{ field: 'id', order: 'asc' }], }); @@ -268,6 +284,7 @@ describe('SqlDriver datetime filters on a legacy MIXED-storage column (#3912)', it('excludes rows of BOTH forms that fall outside the window', async () => { const rows = await driver.find('lead', { + object: 'lead', where: { created_date: { $lt: '2026-01-01' } }, orderBy: [{ field: 'id', order: 'asc' }], }); diff --git a/packages/plugins/driver-sql/src/sql-driver-datetime-filter.test.ts b/packages/plugins/driver-sql/src/sql-driver-datetime-filter.test.ts index ff609a89fb..3e3226365e 100644 --- a/packages/plugins/driver-sql/src/sql-driver-datetime-filter.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-datetime-filter.test.ts @@ -62,6 +62,7 @@ describe('SqlDriver datetime filter coercion', () => { it('matches datetime $gte against an ISO date string', async () => { const rows = await driver.find('publication', { + object: 'publication', where: { published_at: { $gte: '2026-01-01' } }, orderBy: [{ field: 'published_at', order: 'asc' }], }); @@ -70,6 +71,7 @@ describe('SqlDriver datetime filter coercion', () => { it('matches datetime range with $gte / $lt', async () => { const rows = await driver.find('publication', { + object: 'publication', where: { published_at: { $gte: '2026-01-01', $lt: '2026-05-01' } }, }); expect(rows.map((r: any) => r.id)).toEqual(['p2']); @@ -77,6 +79,7 @@ describe('SqlDriver datetime filter coercion', () => { it('accepts a full ISO timestamp', async () => { const rows = await driver.find('publication', { + object: 'publication', where: { published_at: { $gte: '2026-05-25T00:00:00.000Z' } }, }); expect(rows.map((r: any) => r.id)).toEqual(['p3']); @@ -84,6 +87,7 @@ describe('SqlDriver datetime filter coercion', () => { it('accepts a JS Date object', async () => { const rows = await driver.find('publication', { + object: 'publication', where: { published_at: { $gte: new Date('2026-01-01T00:00:00Z') } }, }); expect(rows.map((r: any) => r.id).sort()).toEqual(['p2', 'p3']); @@ -92,6 +96,7 @@ describe('SqlDriver datetime filter coercion', () => { it('accepts a numeric epoch millisecond value', async () => { const ms = Date.parse('2026-01-01T00:00:00Z'); const rows = await driver.find('publication', { + object: 'publication', where: { published_at: { $gte: ms } }, }); expect(rows.map((r: any) => r.id).sort()).toEqual(['p2', 'p3']); @@ -99,6 +104,7 @@ describe('SqlDriver datetime filter coercion', () => { it('still filters non-date columns normally', async () => { const rows = await driver.find('publication', { + object: 'publication', where: { views: { $gte: 200 } }, }); expect(rows.map((r: any) => r.id).sort()).toEqual(['p2', 'p3']); @@ -106,6 +112,7 @@ describe('SqlDriver datetime filter coercion', () => { it('matches date (YYYY-MM-DD) columns with ISO comparand', async () => { const rows = await driver.find('publication', { + object: 'publication', where: { period_start: { $gte: '2026-01-01' } }, }); expect(rows.map((r: any) => r.id).sort()).toEqual(['p2', 'p3']); diff --git a/packages/plugins/driver-sql/src/sql-driver-datetime-mysql-storage.test.ts b/packages/plugins/driver-sql/src/sql-driver-datetime-mysql-storage.test.ts index 6c3c6e87d4..12079bef2e 100644 --- a/packages/plugins/driver-sql/src/sql-driver-datetime-mysql-storage.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-datetime-mysql-storage.test.ts @@ -130,7 +130,7 @@ describe.skipIf(!URL)('Field.datetime on MySQL (#3942)', () => { it('anchors a bare calendar-day comparand to UTC midnight, like the other dialects', async () => { await driver.create(TABLE, { id: 'b1', label: 'x', at: BOUNDARY }, { bypassTenantAudit: true }); const day = async (from: string, to: string) => - (await driver.find(TABLE, { where: { at: { $gte: from, $lt: to } } })).map((r: any) => r.id); + (await driver.find(TABLE, { object: TABLE, where: { at: { $gte: from, $lt: to } } })).map((r: any) => r.id); // 20:00Z belongs to 2026-03-20 in UTC; on a `+08:00` server read as local // midnight it would fall on the 21st — the divergence #3912 measured on PG. @@ -140,7 +140,7 @@ describe.skipIf(!URL)('Field.datetime on MySQL (#3942)', () => { it('round-trips the instant through a read', async () => { await driver.create(TABLE, { id: 'r', label: 'r', at: MIDDAY }, { bypassTenantAudit: true }); - const row: any = await driver.findOne(TABLE, 'r', { bypassTenantAudit: true }); + const row: any = await driver.findOne(TABLE, { object: TABLE, where: { id: 'r' } }, { bypassTenantAudit: true }); expect(new Date(row.at).toISOString()).toBe(MIDDAY); }); }); @@ -219,7 +219,7 @@ describe.skipIf(!URL)('MySQL TIMESTAMP → DATETIME(3) migration (#3942)', () => { name: LEGACY, fields: { label: { type: 'string' }, at: { type: 'datetime' } } }, ]); await driver.create(LEGACY, { id: 'fresh', label: 'f' }, { bypassTenantAudit: true }); - const row: any = await driver.findOne(LEGACY, 'fresh', { bypassTenantAudit: true }); + const row: any = await driver.findOne(LEGACY, { object: LEGACY, where: { id: 'fresh' } }, { bypassTenantAudit: true }); expect(row.created_at ?? null, 'created_at must still default').not.toBeNull(); }); }); @@ -319,7 +319,7 @@ async function columnTypes(driver: SqlDriver, table: string): Promise { - const row: any = await driver.findOne(table, id, { bypassTenantAudit: true }); + const row: any = await driver.findOne(table, { object: table, where: { id: id } }, { bypassTenantAudit: true }); return new Date(row.at).toISOString(); } diff --git a/packages/plugins/driver-sql/src/sql-driver-datetime-postgres-timezone.test.ts b/packages/plugins/driver-sql/src/sql-driver-datetime-postgres-timezone.test.ts index 6d62c1a4e9..0b18ce1ca8 100644 --- a/packages/plugins/driver-sql/src/sql-driver-datetime-postgres-timezone.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-datetime-postgres-timezone.test.ts @@ -104,7 +104,7 @@ describe.skipIf(!URL)('Field.datetime on Postgres is timezone-independent (#3912 await driver.create(TABLE, { id: 'b1', label: 'x', at: new Date(BOUNDARY) }, { bypassTenantAudit: true }); const day = async (from: string, to: string) => - (await driver.find(TABLE, { where: { at: { $gte: from, $lt: to } } })).map((r: any) => r.id); + (await driver.find(TABLE, { object: TABLE, where: { at: { $gte: from, $lt: to } } })).map((r: any) => r.id); // 20:00Z belongs to 2026-03-20 in UTC. Read against the server's local // midnight (Asia/Shanghai) it would fall on the 21st instead — which is @@ -115,7 +115,7 @@ describe.skipIf(!URL)('Field.datetime on Postgres is timezone-independent (#3912 it('presents the stored instant as canonical UTC on read', async () => { await driver.create(TABLE, { id: 'r', label: 'r', at: '2026-03-20 12:00:00' }, { bypassTenantAudit: true }); - const row: any = await driver.findOne(TABLE, 'r', { bypassTenantAudit: true }); + const row: any = await driver.findOne(TABLE, { object: TABLE, where: { id: 'r' } }, { bypassTenantAudit: true }); expect(new Date(row.at).toISOString()).toBe(MIDDAY); }); }); diff --git a/packages/plugins/driver-sql/src/sql-driver-external-remote-name.test.ts b/packages/plugins/driver-sql/src/sql-driver-external-remote-name.test.ts index 643ec0477e..fc857d709d 100644 --- a/packages/plugins/driver-sql/src/sql-driver-external-remote-name.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-external-remote-name.test.ts @@ -99,7 +99,7 @@ describe('SqlDriver external read path — remoteName resolution (ADR-0015)', () // count + findOne route to the remote table too. expect(await ext.count('ext_customer', {} as any)).toBe(2); - const one = await ext.findOne('ext_customer', { where: { name: 'Globex' } } as any); + const one = await ext.findOne('ext_customer', { object: 'ext_customer', where: { name: 'Globex' } }); expect(one?.name).toBe('Globex'); // Filtered reads hit the remote table. diff --git a/packages/plugins/driver-sql/src/sql-driver-like-escape.test.ts b/packages/plugins/driver-sql/src/sql-driver-like-escape.test.ts index f684d7c133..39139d6048 100644 --- a/packages/plugins/driver-sql/src/sql-driver-like-escape.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-like-escape.test.ts @@ -35,17 +35,17 @@ describe('SqlDriver — contains escapes LIKE metacharacters (P0-3)', () => { }); it('a "%" value matches only rows containing a literal %, not every row', async () => { - const r = await driver.find('docs', { where: { title: { $contains: '%' } } }); + const r = await driver.find('docs', { object: 'docs', where: { title: { $contains: '%' } } }); expect(r.map((x: any) => x.id)).toEqual(['1']); }); it('a "_" value matches only rows containing a literal _, not any single char', async () => { - const r = await driver.find('docs', { where: { title: { $contains: '_' } } }); + const r = await driver.find('docs', { object: 'docs', where: { title: { $contains: '_' } } }); expect(r.map((x: any) => x.id)).toEqual(['3']); }); it('an ordinary substring still matches normally', async () => { - const r = await driver.find('docs', { where: { title: { $contains: 'sale' } } }); + const r = await driver.find('docs', { object: 'docs', where: { title: { $contains: 'sale' } } }); expect(r.map((x: any) => x.id)).toEqual(['1']); }); }); diff --git a/packages/plugins/driver-sql/src/sql-driver-numeric-fidelity.test.ts b/packages/plugins/driver-sql/src/sql-driver-numeric-fidelity.test.ts index f062807260..2c9b9fc70e 100644 --- a/packages/plugins/driver-sql/src/sql-driver-numeric-fidelity.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-numeric-fidelity.test.ts @@ -75,7 +75,7 @@ describe('SqlDriver scalar type fidelity (rating/slider/toggle/progress)', () => }, { bypassTenantAudit: true }, ); - const row = await driver.findOne('zoo', 'z1', { bypassTenantAudit: true }); + const row = await driver.findOne('zoo', { object: 'zoo', where: { id: 'z1' } }, { bypassTenantAudit: true }); // control expect(row.f_number).toBe(42); @@ -95,8 +95,8 @@ describe('SqlDriver scalar type fidelity (rating/slider/toggle/progress)', () => await driver.create('zoo', { id: 'z2', name: 'B', f_boolean: true, f_toggle: true }, { bypassTenantAudit: true }); await driver.create('zoo', { id: 'z3', name: 'C', f_boolean: false, f_toggle: false }, { bypassTenantAudit: true }); - const on = await driver.findOne('zoo', 'z2', { bypassTenantAudit: true }); - const off = await driver.findOne('zoo', 'z3', { bypassTenantAudit: true }); + const on = await driver.findOne('zoo', { object: 'zoo', where: { id: 'z2' } }, { bypassTenantAudit: true }); + const off = await driver.findOne('zoo', { object: 'zoo', where: { id: 'z3' } }, { bypassTenantAudit: true }); // control expect(on.f_boolean).toBe(true); @@ -121,7 +121,7 @@ describe('SqlDriver scalar type fidelity (rating/slider/toggle/progress)', () => }, { bypassTenantAudit: true }, ); - const row = await driver.findOne('zoo', 'z4', { bypassTenantAudit: true }); + const row = await driver.findOne('zoo', { object: 'zoo', where: { id: 'z4' } }, { bypassTenantAudit: true }); expect(row.f_record).toEqual({ home: '+1', work: '+2' }); expect(row.f_video).toEqual({ url: 'https://cdn/v.mp4', duration: 12 }); @@ -187,7 +187,7 @@ describe('SqlDriver numeric read coercion repairs legacy TEXT columns', () => { { id: 'L1', name: 'old-row', f_rating: 4, f_slider: 25, f_progress: 60 }, { bypassTenantAudit: true }, ); - const row = await driver.findOne('legacy', 'L1', { bypassTenantAudit: true }); + const row = await driver.findOne('legacy', { object: 'legacy', where: { id: 'L1' } }, { bypassTenantAudit: true }); expect(typeof row.f_rating).toBe('number'); expect(row.f_rating).toBe(4); @@ -202,7 +202,7 @@ describe('SqlDriver numeric read coercion repairs legacy TEXT columns', () => { // Hand-write a row with a null and a non-numeric string straight to the // TEXT columns, bypassing the driver, to model messy legacy data. await knex('legacy').insert({ id: 'L2', name: 'messy', f_rating: null, f_slider: 'n/a', f_progress: '60' }); - const row = await driver.findOne('legacy', 'L2', { bypassTenantAudit: true }); + const row = await driver.findOne('legacy', { object: 'legacy', where: { id: 'L2' } }, { bypassTenantAudit: true }); expect(row.f_rating).toBeNull(); // null stays null, not 0 expect(row.f_slider).toBe('n/a'); // non-numeric junk is preserved, not NaN diff --git a/packages/plugins/driver-sql/src/sql-driver-or-filter.test.ts b/packages/plugins/driver-sql/src/sql-driver-or-filter.test.ts index 8897c259d1..2cab2940ba 100644 --- a/packages/plugins/driver-sql/src/sql-driver-or-filter.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-or-filter.test.ts @@ -56,7 +56,7 @@ describe('SqlDriver filter logic conformance (SQLite)', () => { describe('shared conformance cases', () => { for (const c of FILTER_LOGIC_CASES) { it(c.name, async () => { - const rows = await driver.find('t', { where: c.filter }); + const rows = await driver.find('t', { object: 't', where: c.filter }); const got = rows .map((r: any) => String(r.id)) .sort((x: string, y: string) => x.localeCompare(y)); @@ -91,6 +91,7 @@ describe('SqlDriver filter logic conformance (SQLite)', () => { it('matches only the rows inside the abutting windows', async () => { const rows = await driver.find('task', { + object: 'task', where: { $or: [ { end_date: { $gte: '2026-08-07', $lt: '2026-08-08' } }, @@ -103,6 +104,7 @@ describe('SqlDriver filter logic conformance (SQLite)', () => { it('keeps a window AND-ed with a sibling key in the same branch', async () => { const rows = await driver.find('task', { + object: 'task', where: { $or: [{ id: 'nope' }, { end_date: { $gte: '2026-08-07', $lt: '2026-08-31' }, id: 'd15' }] }, }); expect(rows.map((r: any) => r.id)).toEqual(['d15']); diff --git a/packages/plugins/driver-sql/src/sql-driver-retention-prune.test.ts b/packages/plugins/driver-sql/src/sql-driver-retention-prune.test.ts index 0c43eae8d0..958594eba1 100644 --- a/packages/plugins/driver-sql/src/sql-driver-retention-prune.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-retention-prune.test.ts @@ -59,24 +59,24 @@ describe('SqlDriver retention prune on a builtin created_at timestamp column', ( const deleted = await driver.deleteMany( 'retention_probe', - { where: { created_at: { $lt: cutoffIso } } }, + { object: 'retention_probe', where: { created_at: { $lt: cutoffIso } } }, { bypassTenantAudit: true }, ); expect(deleted).toBe(2); - const remaining = await driver.find('retention_probe', {}); + const remaining = await driver.find('retention_probe', { object: 'retention_probe' }); expect(remaining.map((r: any) => r.id)).toEqual(['new1']); }); it('an ISO-8601 cutoff before every row deletes nothing', async () => { const deleted = await driver.deleteMany( 'retention_probe', - { where: { created_at: { $lt: '2020-01-01T00:00:00.000Z' } } }, + { object: 'retention_probe', where: { created_at: { $lt: '2020-01-01T00:00:00.000Z' } } }, { bypassTenantAudit: true }, ); expect(deleted).toBe(0); - const remaining = await driver.find('retention_probe', {}); + const remaining = await driver.find('retention_probe', { object: 'retention_probe' }); expect(remaining.map((r: any) => r.id).sort()).toEqual(['new1', 'old1', 'old2']); }); }); diff --git a/packages/plugins/driver-sql/src/sql-driver-schema.test.ts b/packages/plugins/driver-sql/src/sql-driver-schema.test.ts index 346d7fe3b1..ec3df8fe58 100644 --- a/packages/plugins/driver-sql/src/sql-driver-schema.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-schema.test.ts @@ -131,7 +131,7 @@ describe('SqlDriver Schema Sync (SQLite)', () => { users: ['u1', 'u2'], }); - const results = await driver.find('multi_test', {}); + const results = await driver.find('multi_test', { object: 'multi_test' }); const row = results[0]; expect(row.tags).toEqual(['a', 'b']); @@ -154,7 +154,7 @@ describe('SqlDriver Schema Sync (SQLite)', () => { expect(columns).toHaveProperty('completion'); await driver.create('percent_test', { completion: 0.85 }); - const res = await driver.find('percent_test', {}); + const res = await driver.find('percent_test', { object: 'percent_test' }); expect(res[0].completion).toBe(0.85); }); @@ -183,7 +183,7 @@ describe('SqlDriver Schema Sync (SQLite)', () => { owner: 'u-1', watchers: ['u-2', 'u-3'], }); - const res = await driver.find('ticket_user_test', {}); + const res = await driver.find('ticket_user_test', { object: 'ticket_user_test' }); expect(res[0].owner).toBe('u-1'); expect(res[0].watchers).toEqual(['u-2', 'u-3']); }); @@ -276,7 +276,7 @@ describe('SqlDriver Schema Sync (SQLite)', () => { work_hours: '09:00:00', }); - const res = await driver.find('new_types_test', {}); + const res = await driver.find('new_types_test', { object: 'new_types_test' }); const row = res[0]; expect(row.email).toBe('test@example.com'); @@ -382,7 +382,7 @@ describe('SqlDriver Schema Sync (SQLite)', () => { ).rejects.toThrow(/UNIQUE constraint failed|duplicate key value/); // Differing channel → allowed. await driver.create('idx_multi_obj', { notification_id: 'n1', recipient_id: 'r1', channel: 'email' }); - const rows = await driver.find('idx_multi_obj', {}); + const rows = await driver.find('idx_multi_obj', { object: 'idx_multi_obj' }); expect(rows.length).toBe(2); }); @@ -402,7 +402,7 @@ describe('SqlDriver Schema Sync (SQLite)', () => { // Two rows with no dedup_key (NULL) must both be insertable. await driver.create('idx_null_obj', {}); await driver.create('idx_null_obj', {}); - const rows = await driver.find('idx_null_obj', {}); + const rows = await driver.find('idx_null_obj', { object: 'idx_null_obj' }); expect(rows.length).toBe(2); }); diff --git a/packages/plugins/driver-sql/src/sql-driver-server-timing.test.ts b/packages/plugins/driver-sql/src/sql-driver-server-timing.test.ts index 84dc093eee..6bb01bbf47 100644 --- a/packages/plugins/driver-sql/src/sql-driver-server-timing.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-server-timing.test.ts @@ -44,7 +44,7 @@ describe('SqlDriver Server-Timing db span', () => { it('records a db mark with a query count for a real find()', async () => { const t = new PerfTiming(); const rows = await runWithPerfTiming(t, () => - driver.find('orders', { where: { status: 'open' } }), + driver.find('orders', { object: 'orders', where: { status: 'open' } }), ); expect(rows).toHaveLength(1); const mark = dbMark(t); diff --git a/packages/plugins/driver-sql/src/sql-driver-time-canonical-storage.test.ts b/packages/plugins/driver-sql/src/sql-driver-time-canonical-storage.test.ts index 2c0ff526ab..5b407844b6 100644 --- a/packages/plugins/driver-sql/src/sql-driver-time-canonical-storage.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-time-canonical-storage.test.ts @@ -81,6 +81,7 @@ describe('Field.time canonical writes (#3994)', () => { } const hits = await driver.find('shift', { + object: 'shift', where: { starts_at: { $gte: '09:00:00', $lte: '18:00:00' } }, orderBy: [{ field: 'id', order: 'asc' }], }); @@ -95,11 +96,11 @@ describe('Field.time canonical writes (#3994)', () => { await driver.create('shift', { id: 'a', label: 'a', starts_at: '14:30' }, { bypassTenantAudit: true }); await driver.create('shift', { id: 'b', label: 'b', starts_at: '14:30:00' }, { bypassTenantAudit: true }); - const hits = await driver.find('shift', { where: { starts_at: '14:30:00' } }); + const hits = await driver.find('shift', { object: 'shift', where: { starts_at: '14:30:00' } }); expect(hits.map((r: any) => r.id).sort()).toEqual(['a', 'b']); // And the comparand is canonicalised too — the minutes-only spelling // matches the same two rows. - const hits2 = await driver.find('shift', { where: { starts_at: '14:30' } }); + const hits2 = await driver.find('shift', { object: 'shift', where: { starts_at: '14:30' } }); expect(hits2.map((r: any) => r.id).sort()).toEqual(['a', 'b']); }); @@ -109,7 +110,7 @@ describe('Field.time canonical writes (#3994)', () => { for (const [id, v] of WRITE_SHAPES) { await driver.create('shift', { id, label: id, starts_at: v }, { bypassTenantAudit: true }); } - const rows = await driver.find('shift', { orderBy: [{ field: 'starts_at', order: 'asc' }] }); + const rows = await driver.find('shift', { object: 'shift', orderBy: [{ field: 'starts_at', order: 'asc' }] }); // 08:00 first; the `.500` rows after their `14:30:00` flat siblings // (`.` sorts below every digit, so lexicographic == chronological). expect((rows[0] as any).id).toBe('s_early'); @@ -161,6 +162,7 @@ describe('Field.time legacy storage: backfill and read-side repair (#3994)', () await seedLegacy(driver); const hits = await driver.find('shift', { + object: 'shift', where: { starts_at: { $gte: '09:00:00', $lte: '18:00:00' } }, orderBy: [{ field: 'id', order: 'asc' }], }); @@ -188,6 +190,7 @@ describe('Field.time legacy storage: backfill and read-side repair (#3994)', () // Converged storage means the plain indexable comparison now works. const hits = await driver.find('shift', { + object: 'shift', where: { starts_at: { $gte: '09:00:00', $lte: '18:00:00' } }, orderBy: [{ field: 'id', order: 'asc' }], }); diff --git a/packages/plugins/driver-sql/src/sql-driver-time-live-dialects.test.ts b/packages/plugins/driver-sql/src/sql-driver-time-live-dialects.test.ts index 8b95541003..697a30d446 100644 --- a/packages/plugins/driver-sql/src/sql-driver-time-live-dialects.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-time-live-dialects.test.ts @@ -88,7 +88,7 @@ function suite(dialect: 'pg' | 'mysql', url: string | undefined) { await driver.create(TABLE, { id, label: id, starts_at: v }, { bypassTenantAudit: true }); } for (const [id, , presented] of WRITES) { - const row: any = await driver.findOne(TABLE, id, { bypassTenantAudit: true }); + const row: any = await driver.findOne(TABLE, { object: TABLE, where: { id: id } }, { bypassTenantAudit: true }); expect(row.starts_at, id).toBe(presented); } }); @@ -101,14 +101,14 @@ function suite(dialect: 'pg' | 'mysql', url: string | undefined) { { id: 'd', label: 'd', starts_at: new Date(Date.UTC(2026, 0, 15, 14, 30, 0, 500)) }, { bypassTenantAudit: true }, ); - const row: any = await driver.findOne(TABLE, 'd', { bypassTenantAudit: true }); + const row: any = await driver.findOne(TABLE, { object: TABLE, where: { id: 'd' } }, { bypassTenantAudit: true }); expect(row.starts_at).toBe('14:30:00.500'); }); it('keeps milliseconds — no zero-precision rounding to the next second', async () => { // MySQL's bare TIME would ROUND '14:30:00.500' up to 14:30:01. await driver.create(TABLE, { id: 'ms', label: 'ms', starts_at: '14:30:00.500' }, { bypassTenantAudit: true }); - const row: any = await driver.findOne(TABLE, 'ms', { bypassTenantAudit: true }); + const row: any = await driver.findOne(TABLE, { object: TABLE, where: { id: 'ms' } }, { bypassTenantAudit: true }); expect(row.starts_at).toBe('14:30:00.500'); }); @@ -119,6 +119,7 @@ function suite(dialect: 'pg' | 'mysql', url: string | undefined) { await driver.create(TABLE, { id: 'early', label: 'e', starts_at: '08:00:00' }, { bypassTenantAudit: true }); const hits = await driver.find(TABLE, { + object: TABLE, where: { starts_at: { $gte: '09:00:00', $lte: '18:00:00' } }, orderBy: [{ field: 'id', order: 'asc' }], }); @@ -127,7 +128,7 @@ function suite(dialect: 'pg' | 'mysql', url: string | undefined) { it("a NOW()-default time column records the UTC time-of-day, not the server's or session's", async () => { await driver.create(TABLE, { id: 'now', label: 'n' }, { bypassTenantAudit: true }); - const row: any = await driver.findOne(TABLE, 'now', { bypassTenantAudit: true }); + const row: any = await driver.findOne(TABLE, { object: TABLE, where: { id: 'now' } }, { bypassTenantAudit: true }); // A leak of the +08:00 server zone is ~480 minutes; of the -04:00/-05:00 // process zone, ~240-300. Genuine clock skew is seconds. expect(minutesOffUtc(String(row.auto_at))).toBeLessThan(5); @@ -188,11 +189,11 @@ describe.skipIf(!MY_URL)('MySQL TIME → TIME(3) widening (#3994)', () => { const colType = String((list[0] as any).COLUMN_TYPE ?? (list[0] as any).column_type).toLowerCase(); expect(colType).toBe('time(3)'); - const old: any = await driver.findOne(LEGACY, 'old', { bypassTenantAudit: true }); + const old: any = await driver.findOne(LEGACY, { object: LEGACY, where: { id: 'old' } }, { bypassTenantAudit: true }); expect(old.starts_at).toBe('14:30:00'); // the wall clock must not move await driver.create(LEGACY, { id: 'ms', label: 'm', starts_at: '14:30:00.500' }, { bypassTenantAudit: true }); - const ms: any = await driver.findOne(LEGACY, 'ms', { bypassTenantAudit: true }); + const ms: any = await driver.findOne(LEGACY, { object: LEGACY, where: { id: 'ms' } }, { bypassTenantAudit: true }); expect(ms.starts_at).toBe('14:30:00.500'); // pre-widen this ROUNDED to 14:30:01 }); diff --git a/packages/plugins/driver-sql/src/sql-driver-time-of-day.test.ts b/packages/plugins/driver-sql/src/sql-driver-time-of-day.test.ts index 457833608b..a824b0a37c 100644 --- a/packages/plugins/driver-sql/src/sql-driver-time-of-day.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-time-of-day.test.ts @@ -49,13 +49,13 @@ describe('Field.time canonical presentation (time-of-day, SQLite)', () => { // default (or any full timestamp that leaked into the column), bypassing the // driver write path. await raw('shift').insert({ id: 'legacy', label: 'L', starts_at: '2026-01-15 14:30:00' }); - const row: any = await driver.findOne('shift', 'legacy', { bypassTenantAudit: true }); + const row: any = await driver.findOne('shift', { object: 'shift', where: { id: 'legacy' } }, { bypassTenantAudit: true }); expect(row.starts_at).toBe('14:30:00'); }); it('repairs a full-ISO value (with Z) in a time column to its time-of-day', async () => { await raw('shift').insert({ id: 'iso', label: 'I', starts_at: '2026-01-15T14:30:00.500Z' }); - const row: any = await driver.findOne('shift', 'iso', { bypassTenantAudit: true }); + const row: any = await driver.findOne('shift', { object: 'shift', where: { id: 'iso' } }, { bypassTenantAudit: true }); expect(row.starts_at).toBe('14:30:00.500'); }); @@ -71,7 +71,7 @@ describe('Field.time canonical presentation (time-of-day, SQLite)', () => { ['d', '09:05:30.250', '09:05:30.250'], ] as const) { await driver.create('shift', { id, label: id, starts_at: written }, { bypassTenantAudit: true }); - const row: any = await driver.findOne('shift', id, { bypassTenantAudit: true }); + const row: any = await driver.findOne('shift', { object: 'shift', where: { id: id } }, { bypassTenantAudit: true }); expect(row.starts_at).toBe(presented); } }); @@ -79,7 +79,7 @@ describe('Field.time canonical presentation (time-of-day, SQLite)', () => { it('a NOW()-default time column reads back a time-of-day, not a full timestamp', async () => { // `auto_at` omitted → the DDL default fires. await driver.create('shift', { id: 'd', label: 'D' }, { bypassTenantAudit: true }); - const row: any = await driver.findOne('shift', 'd', { bypassTenantAudit: true }); + const row: any = await driver.findOne('shift', { object: 'shift', where: { id: 'd' } }, { bypassTenantAudit: true }); expect(row.auto_at).toMatch(TIME_OF_DAY); expect(row.auto_at).not.toContain('-'); // not a `YYYY-MM-DD …` timestamp }); @@ -87,7 +87,7 @@ describe('Field.time canonical presentation (time-of-day, SQLite)', () => { it('find() (list path) normalizes time identically to findOne()', async () => { await raw('shift').insert({ id: 'l1', label: 'L1', starts_at: '2026-02-02 08:15:00' }); await driver.create('shift', { id: 'l2', label: 'L2', starts_at: '08:15:00' }, { bypassTenantAudit: true }); - const rows = await driver.find('shift', { orderBy: [{ field: 'id', order: 'asc' }] }); + const rows = await driver.find('shift', { object: 'shift', orderBy: [{ field: 'id', order: 'asc' }] }); const byId = Object.fromEntries(rows.map((r: any) => [r.id, r])); expect(byId.l1.starts_at).toBe('08:15:00'); // legacy full-timestamp repaired expect(byId.l2.starts_at).toBe('08:15:00'); // canonical write round-trips @@ -95,7 +95,7 @@ describe('Field.time canonical presentation (time-of-day, SQLite)', () => { it('leaves null untouched', async () => { await driver.create('shift', { id: 'n', label: 'N', starts_at: null }, { bypassTenantAudit: true }); - const row: any = await driver.findOne('shift', 'n', { bypassTenantAudit: true }); + const row: any = await driver.findOne('shift', { object: 'shift', where: { id: 'n' } }, { bypassTenantAudit: true }); expect(row.starts_at).toBeNull(); }); }); diff --git a/packages/plugins/driver-sql/src/sql-driver-timestamp-format.test.ts b/packages/plugins/driver-sql/src/sql-driver-timestamp-format.test.ts index e6ab2864fc..e3c7026b1c 100644 --- a/packages/plugins/driver-sql/src/sql-driver-timestamp-format.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-timestamp-format.test.ts @@ -143,7 +143,7 @@ describe('SqlDriver canonical audit-timestamp format (SQLite)', () => { // Simulate a row written by the OLD update stamp / CURRENT_TIMESTAMP default, // bypassing the driver write path entirely. await raw('thing').insert({ id: 'legacy', name: 'L', created_at: '2026-01-15 08:30:00', updated_at: '2026-01-15 08:30:00.246' }); - const row: any = await driver.findOne('thing', 'legacy', { bypassTenantAudit: true }); + const row: any = await driver.findOne('thing', { object: 'thing', where: { id: 'legacy' } }, { bypassTenantAudit: true }); expect(row.created_at).toBe('2026-01-15T08:30:00.000Z'); expect(row.updated_at).toBe('2026-01-15T08:30:00.246Z'); }); @@ -151,14 +151,14 @@ describe('SqlDriver canonical audit-timestamp format (SQLite)', () => { it('REGRESSION (freshness probe): the repaired instant equals the UTC wall-clock, host-timezone-independent', async () => { // The zone-naive '2026-01-15 08:30:00' must mean 08:30 UTC, NOT 08:30 local. await raw('thing').insert({ id: 'fr', name: 'F', created_at: '2026-01-15 08:30:00', updated_at: '2026-01-15 08:30:00' }); - const row: any = await driver.findOne('thing', 'fr', { bypassTenantAudit: true }); + const row: any = await driver.findOne('thing', { object: 'thing', where: { id: 'fr' } }, { bypassTenantAudit: true }); expect(new Date(row.updated_at as string).getTime()).toBe(Date.parse('2026-01-15T08:30:00.000Z')); }); it('read-repair is idempotent: an already-canonical value is returned unchanged', async () => { const canonical = '2026-02-02T02:02:02.222Z'; await raw('thing').insert({ id: 'canon', name: 'C', created_at: canonical, updated_at: canonical }); - const row: any = await driver.findOne('thing', 'canon', { bypassTenantAudit: true }); + const row: any = await driver.findOne('thing', { object: 'thing', where: { id: 'canon' } }, { bypassTenantAudit: true }); expect(row.created_at).toBe(canonical); expect(row.updated_at).toBe(canonical); }); @@ -175,7 +175,7 @@ describe('SqlDriver canonical audit-timestamp format (SQLite)', () => { try { await dd.initObjects([{ name: 'evt', fields: { created_at: { type: 'datetime' }, label: { type: 'string' } } }]); await dd.create('evt', { id: 'e1', label: 'x', created_at: new Date('2026-04-04T04:04:04.004Z') }, { bypassTenantAudit: true }); - const row: any = await dd.findOne('evt', 'e1', { bypassTenantAudit: true }); + const row: any = await dd.findOne('evt', { object: 'evt', where: { id: 'e1' } }, { bypassTenantAudit: true }); expect(typeof row.created_at).toBe('string'); expect(row.created_at).toBe('2026-04-04T04:04:04.004Z'); } finally { diff --git a/packages/plugins/driver-sql/src/sql-driver-user-datetime-default-format.test.ts b/packages/plugins/driver-sql/src/sql-driver-user-datetime-default-format.test.ts index f717ad0ed6..c524192c7c 100644 --- a/packages/plugins/driver-sql/src/sql-driver-user-datetime-default-format.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-user-datetime-default-format.test.ts @@ -102,7 +102,7 @@ describe('User NOW()-default temporal fields — canonical format (SQLite)', () expect(typeof rawRow.starts_at).toBe('string'); expect(rawRow.starts_at).toBe('2026-03-20T12:34:56.789Z'); - const row: any = await driver.findOne('event', 'e3', { bypassTenantAudit: true }); + const row: any = await driver.findOne('event', { object: 'event', where: { id: 'e3' } }, { bypassTenantAudit: true }); expect(row.starts_at).toBe(rawRow.starts_at); }); @@ -123,7 +123,7 @@ describe('User NOW()-default temporal fields — canonical format (SQLite)', () expect(sorted.map((r: any) => r.id)).toEqual(['explicit', 'defaulted']); for (const id of ['explicit', 'defaulted']) { - const row: any = await driver.findOne('event', id, { bypassTenantAudit: true }); + const row: any = await driver.findOne('event', { object: 'event', where: { id: id } }, { bypassTenantAudit: true }); expect(row.starts_at).toMatch(ISO_Z); expect(Number.isNaN(new Date(row.starts_at).getTime())).toBe(false); } @@ -137,14 +137,14 @@ describe('User NOW()-default temporal fields — canonical format (SQLite)', () const rawRow = await raw('event').where('id', 'legacy').first(); expect(typeof rawRow.starts_at).toBe('number'); - const row: any = await driver.findOne('event', 'legacy', { bypassTenantAudit: true }); + const row: any = await driver.findOne('event', { object: 'event', where: { id: 'legacy' } }, { bypassTenantAudit: true }); expect(row.starts_at).toBe('2026-03-20T12:34:56.789Z'); }); it('an explicit ISO-8601-Z string is preserved (idempotent) on read', async () => { const iso = '2026-05-25T08:00:00.000Z'; await driver.create('event', { id: 'e4', label: 'D', starts_at: iso }, { bypassTenantAudit: true }); - const row: any = await driver.findOne('event', 'e4', { bypassTenantAudit: true }); + const row: any = await driver.findOne('event', { object: 'event', where: { id: 'e4' } }, { bypassTenantAudit: true }); expect(row.starts_at).toBe(iso); }); @@ -154,21 +154,21 @@ describe('User NOW()-default temporal fields — canonical format (SQLite)', () // A row written before this fix (or by a raw insert that took the OLD naive // `CURRENT_TIMESTAMP` default), bypassing the driver write path entirely. await raw('event').insert({ id: 'legacy', label: 'L', starts_at: '2026-01-15 08:30:00' }); - const row: any = await driver.findOne('event', 'legacy', { bypassTenantAudit: true }); + const row: any = await driver.findOne('event', { object: 'event', where: { id: 'legacy' } }, { bypassTenantAudit: true }); expect(row.starts_at).toBe('2026-01-15T08:30:00.000Z'); }); it('REGRESSION (host-timezone independence): the repaired instant equals the UTC wall-clock', async () => { // The zone-naive `2026-01-15 08:30:00` must mean 08:30 UTC, NOT 08:30 local. await raw('event').insert({ id: 'tz', label: 'T', starts_at: '2026-01-15 08:30:00' }); - const row: any = await driver.findOne('event', 'tz', { bypassTenantAudit: true }); + const row: any = await driver.findOne('event', { object: 'event', where: { id: 'tz' } }, { bypassTenantAudit: true }); expect(new Date(row.starts_at).getTime()).toBe(Date.parse('2026-01-15T08:30:00.000Z')); }); it('find() (list path) normalizes datetime identically to findOne(), across mixed storage', async () => { await raw('event').insert({ id: 'list1', label: 'L1', starts_at: '2026-02-02 02:02:02.200' }); await driver.create('event', { id: 'list2', label: 'L2', starts_at: new Date('2026-02-02T02:02:02.200Z') }, { bypassTenantAudit: true }); - const rows = await driver.find('event', { orderBy: [{ field: 'id', order: 'asc' }] }); + const rows = await driver.find('event', { object: 'event', orderBy: [{ field: 'id', order: 'asc' }] }); const byId = Object.fromEntries(rows.map((r: any) => [r.id, r])); expect(byId.list1.starts_at).toBe('2026-02-02T02:02:02.200Z'); expect(byId.list2.starts_at).toBe('2026-02-02T02:02:02.200Z'); @@ -179,7 +179,7 @@ describe('User NOW()-default temporal fields — canonical format (SQLite)', () try { await d2.initObjects([{ name: 'evt2', fields: { dt: { type: 'datetime' }, label: { type: 'string' } } }]); await d2.create('evt2', { id: 'n1', label: 'N', dt: null }, { bypassTenantAudit: true }); - const row: any = await d2.findOne('evt2', 'n1', { bypassTenantAudit: true }); + const row: any = await d2.findOne('evt2', { object: 'evt2', where: { id: 'n1' } }, { bypassTenantAudit: true }); expect(row.dt).toBeNull(); } finally { await d2.disconnect(); diff --git a/packages/plugins/driver-sql/src/sql-driver.test.ts b/packages/plugins/driver-sql/src/sql-driver.test.ts index 10c2cf24a3..183562a9c7 100644 --- a/packages/plugins/driver-sql/src/sql-driver.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver.test.ts @@ -40,6 +40,7 @@ describe('SqlDriver (SQLite Integration)', () => { it('should find objects with filters', async () => { const results = await driver.find('users', { + object: 'users', fields: ['name', 'age'], where: { age: { $gt: 18 } }, orderBy: [{ field: 'name', order: 'asc' }], @@ -54,6 +55,7 @@ describe('SqlDriver (SQLite Integration)', () => { // image, ...) the object may not have. The unknown column must NOT zero // the whole result — the real rows still come back, minus the phantom field. const results = await driver.find('users', { + object: 'users', fields: ['id', 'name', 'status', 'due_date', 'image'], orderBy: [{ field: 'name', order: 'asc' }], }); @@ -66,12 +68,13 @@ describe('SqlDriver (SQLite Integration)', () => { it('still surfaces non-column errors (e.g. unknown table) instead of empty', async () => { await expect( - driver.find('no_such_table', { fields: ['id'] }), + driver.find('no_such_table', { object: 'no_such_table', fields: ['id'] }), ).rejects.toThrow(); }); it('should apply simple AND/OR logic', async () => { const results = await driver.find('users', { + object: 'users', where: { $or: [{ age: 17 }, { age: { $gt: 29 } }], }, @@ -81,10 +84,10 @@ describe('SqlDriver (SQLite Integration)', () => { }); it('should find one object by id', async () => { - const [alice] = await driver.find('users', { where: { name: 'Alice' } }); + const [alice] = await driver.find('users', { object: 'users', where: { name: 'Alice' } }); expect(alice).toBeDefined(); - const fetched = await driver.findOne('users', alice.id as any); + const fetched = await driver.findOne('users', { object: 'users', where: { id: alice.id } }); expect(fetched).toBeDefined(); expect(fetched.name).toBe('Alice'); }); @@ -92,24 +95,24 @@ describe('SqlDriver (SQLite Integration)', () => { it('should create an object', async () => { await driver.create('users', { name: 'Eve', age: 22 }); - const [eve] = await driver.find('users', { where: { name: 'Eve' } }); + const [eve] = await driver.find('users', { object: 'users', where: { name: 'Eve' } }); expect(eve).toBeDefined(); expect(eve.age).toBe(22); }); it('should update an object', async () => { - const [bob] = await driver.find('users', { where: { name: 'Bob' } }); + const [bob] = await driver.find('users', { object: 'users', where: { name: 'Bob' } }); await driver.update('users', bob.id, { age: 18 }); - const updated = await driver.findOne('users', bob.id as any); + const updated = await driver.findOne('users', { object: 'users', where: { id: bob.id } }); expect(updated.age).toBe(18); }); it('should delete an object', async () => { - const [charlie] = await driver.find('users', { where: { name: 'Charlie' } }); + const [charlie] = await driver.find('users', { object: 'users', where: { name: 'Charlie' } }); await driver.delete('users', charlie.id); - const deleted = await driver.findOne('users', charlie.id as any); + const deleted = await driver.findOne('users', { object: 'users', where: { id: charlie.id } }); expect(deleted).toBeNull(); }); @@ -122,7 +125,7 @@ describe('SqlDriver (SQLite Integration)', () => { const created = await driver.create('users', { _id: 'custom-id', name: 'Frank', age: 40 }); expect(created.id).toBe('custom-id'); - const fetched = await driver.findOne('users', 'custom-id' as any); + const fetched = await driver.findOne('users', { object: 'users', where: { id: 'custom-id' } }); expect(fetched).toBeDefined(); expect(fetched.name).toBe('Frank'); }); diff --git a/packages/plugins/driver-sql/src/sql-driver.ts b/packages/plugins/driver-sql/src/sql-driver.ts index a0c13c9287..c63ece7672 100644 --- a/packages/plugins/driver-sql/src/sql-driver.ts +++ b/packages/plugins/driver-sql/src/sql-driver.ts @@ -1432,21 +1432,25 @@ export class SqlDriver implements IDataDriver { return results; } + /** + * `IDataDriver.findOne` — find a single record BY QUERY. + * + * This also accepted a bare id (`findOne('task', 't1')`) through an + * undeclared `typeof query === 'string' | 'number'` branch until #4311. No + * caller outside this package's own tests used it, it was on no contract, and + * the other two drivers answer that same call differently: `MemoryDriver` + * spreads the argument (`{ ...query }` over a string yields `{0:'t',1:'1'}`) + * and `MongoDriver` reads `query.where` (undefined → an unfiltered findOne, + * i.e. an arbitrary row). One spelling meaning three things across three + * drivers is the second de-facto contract Prime Directive #12 exists to + * prevent — and the branch also bypassed this method's own `find()` path + * (field selection, temporal coercion, the deterministic-order tie-breaker). + * Spell an id lookup as what it is: `{ object, where: { id } }`. + */ async findOne(object: string, query: QueryAST, options?: DriverOptions): Promise { - // When called with a string/number id fall back gracefully - if (typeof query === 'string' || typeof query === 'number') { - const builder = this.getBuilder(object, options).where('id', query); - this.applyTenantScope(builder, object, options); - const res = await builder.first(); - return this.formatOutput(object, res) || null; - } - - if (query && typeof query === 'object') { - const results = await this.find(object, { ...query, limit: 1 }, options); - return results[0] || null; - } - - return null; + if (!query || typeof query !== 'object') return null; + const results = await this.find(object, { ...query, limit: 1 }, options); + return results[0] || null; } /** @@ -1784,7 +1788,7 @@ export class SqlDriver implements IDataDriver { const cfgs = this.autoNumberFields[object] || this.autoNumberFields[tableName]; if (!cfgs || cfgs.length === 0) return; const parentTrx = options?.transaction as Knex.Transaction | undefined; - const timezone = (options as any)?.timezone as string | undefined; + const timezone = options?.timezone; const now = new Date(); for (const cfg of cfgs) { if (row[cfg.name] !== undefined && row[cfg.name] !== null && row[cfg.name] !== '') continue; @@ -1803,7 +1807,7 @@ export class SqlDriver implements IDataDriver { // Resolve tenant for this row: explicit field on the record wins, // then driver options, else null → global sequence. const rowTenant = cfg.tenantField ? row[cfg.tenantField] : undefined; - const optTenant = (options as any)?.tenantId; + const optTenant = options?.tenantId; const tenantId = rowTenant != null && rowTenant !== '' ? String(rowTenant) : optTenant != null && optTenant !== '' @@ -1862,7 +1866,7 @@ export class SqlDriver implements IDataDriver { * A normal update leaves the flag unset, so `updated_at` always advances. */ protected keepSuppliedUpdatedAt(formatted: Record, options?: DriverOptions): boolean { - return (options as any)?.preserveAudit === true && formatted.updated_at != null; + return options?.preserveAudit === true && formatted.updated_at != null; } async update(object: string, id: string | number, data: Record, options?: DriverOptions): Promise { @@ -2810,7 +2814,13 @@ export class SqlDriver implements IDataDriver { if (timeCols.length) this.timeFields[key] = new Set(timeCols); } - async initObjects(objects: Array<{ name: string; fields?: Record }>): Promise { + // `tenancy` is part of what this method READS — each object flows into + // `computeAndRecordTenantField`, which consumes `obj.tenancy` to pick the + // tenant column and to set or clear the sticky explicit-opt-out. It went + // undeclared here until #4311 (`registerExternalObject` and + // `computeAndRecordTenantField` both had it), so a caller spelling the key + // correctly was rejected by the type while the driver read it regardless. + async initObjects(objects: Array<{ name: string; fields?: Record; tenancy?: any }>): Promise { // DDL gate (ADR-0015 §5.1): createTable/alterTable below mutate schema. // Also covers `syncSchema`, which delegates here. this.assertSchemaMutable('initObjects'); @@ -4331,7 +4341,7 @@ export class SqlDriver implements IDataDriver { object: string, options?: DriverOptions, ): Knex.QueryBuilder { - const tenantId = (options as any)?.tenantId; + const tenantId = options?.tenantId; if (tenantId === undefined || tenantId === null || tenantId === '') return builder; const field = this.resolveTenantField(object); if (!field) return builder; @@ -4343,7 +4353,7 @@ export class SqlDriver implements IDataDriver { // empty set falls through to the equality path: fail toward isolation, // never toward exposure. Insert-side injection (injectTenantOnInsert) // deliberately keeps `tenantId` — the active org is the write target (D5). - const rawIds = (options as any)?.tenantIds; + const rawIds = options?.tenantIds; const tenantIds = Array.isArray(rawIds) ? rawIds.filter((v: unknown) => typeof v === 'string' && v !== '') : []; @@ -4380,7 +4390,7 @@ export class SqlDriver implements IDataDriver { row: Record, options?: DriverOptions, ): void { - const tenantId = (options as any)?.tenantId; + const tenantId = options?.tenantId; if (tenantId === undefined || tenantId === null || tenantId === '') return; const field = this.resolveTenantField(object); if (!field) return; @@ -4405,7 +4415,7 @@ export class SqlDriver implements IDataDriver { options?: DriverOptions, ): void { if (process.env.OS_TENANT_AUDIT === '0') return; - if ((options as any)?.bypassTenantAudit === true) return; + if (options?.bypassTenantAudit === true) return; // Only meaningful in multi-tenant deployments. Single-tenant stacks have no // tenant isolation, yet the kernel now ALWAYS provisions an `organization_id` // column (its existence is decoupled from the tenant flag). Column presence @@ -4413,7 +4423,7 @@ export class SqlDriver implements IDataDriver { // system/sudo write (e.g. the notification/http delivery dispatchers' claim // updates) would spam a meaningless warning on single-tenant boots. if (!this.isMultiTenantMode()) return; - const tenantId = (options as any)?.tenantId; + const tenantId = options?.tenantId; if (tenantId !== undefined && tenantId !== null && tenantId !== '') return; const field = this.resolveTenantField(object); if (!field) return; diff --git a/packages/plugins/driver-sqlite-wasm/package.json b/packages/plugins/driver-sqlite-wasm/package.json index 308ce7d0a0..cc69ea148e 100644 --- a/packages/plugins/driver-sqlite-wasm/package.json +++ b/packages/plugins/driver-sqlite-wasm/package.json @@ -25,7 +25,8 @@ "scripts": { "build": "tsup --config ../../../tsup.config.ts", "dev": "tsc -w", - "test": "vitest run" + "test": "vitest run", + "typecheck": "tsc --noEmit" }, "dependencies": { "@objectstack/core": "workspace:*", diff --git a/packages/plugins/driver-sqlite-wasm/src/sqlite-wasm-driver-advanced.test.ts b/packages/plugins/driver-sqlite-wasm/src/sqlite-wasm-driver-advanced.test.ts index 62c9e7a251..ed0a379c20 100644 --- a/packages/plugins/driver-sqlite-wasm/src/sqlite-wasm-driver-advanced.test.ts +++ b/packages/plugins/driver-sqlite-wasm/src/sqlite-wasm-driver-advanced.test.ts @@ -135,7 +135,7 @@ describe('SqliteWasmDriver Advanced Operations (SQLite)', () => { expect(result).toBeDefined(); expect(result.length).toBe(3); - const count = await driver.count('orders', {}); + const count = await driver.count('orders', { object: 'orders' }); expect(count).toBe(8); }); @@ -144,7 +144,7 @@ describe('SqliteWasmDriver Advanced Operations (SQLite)', () => { expect(result).toBeGreaterThan(0); - const results = await driver.find('orders', { where: { status: 'processing' } }); + const results = await driver.find('orders', { object: 'orders', where: { status: 'processing' } }); expect(results.length).toBe(1); }); @@ -153,7 +153,7 @@ describe('SqliteWasmDriver Advanced Operations (SQLite)', () => { expect(result).toBe(1); - const remaining = await driver.count('orders', {}); + const remaining = await driver.count('orders', { object: 'orders' }); expect(remaining).toBe(4); }); @@ -186,7 +186,7 @@ describe('SqliteWasmDriver Advanced Operations (SQLite)', () => { await driver.commitTransaction(trx); - const result = await driver.findOne('orders', 'trx1' as any); + const result = await driver.findOne('orders', { object: 'orders', where: { id: 'trx1' } }); expect(result).toBeDefined(); expect(result.customer).toBe('TxUser'); } catch (e) { @@ -214,7 +214,7 @@ describe('SqliteWasmDriver Advanced Operations (SQLite)', () => { await driver.rollbackTransaction(trx); - const result = await driver.findOne('orders', 'trx2' as any); + const result = await driver.findOne('orders', { object: 'orders', where: { id: 'trx2' } }); expect(result).toBeNull(); } catch (e) { await driver.rollbackTransaction(trx); @@ -245,13 +245,13 @@ describe('SqliteWasmDriver Advanced Operations (SQLite)', () => { await driver.commitTransaction(trx); - const created = await driver.findOne('orders', 'trx3' as any); + const created = await driver.findOne('orders', { object: 'orders', where: { id: 'trx3' } }); expect(created).toBeDefined(); - const updated = await driver.findOne('orders', '1' as any); + const updated = await driver.findOne('orders', { object: 'orders', where: { id: '1' } }); expect(updated.status).toBe('shipped'); - const deleted = await driver.findOne('orders', '5' as any); + const deleted = await driver.findOne('orders', { object: 'orders', where: { id: '5' } }); expect(deleted).toBeNull(); } catch (e) { await driver.rollbackTransaction(trx); @@ -262,12 +262,12 @@ describe('SqliteWasmDriver Advanced Operations (SQLite)', () => { describe('Edge Cases and Error Handling', () => { it('should handle empty filters gracefully', async () => { - const results = await driver.find('orders', { where: {} }); + const results = await driver.find('orders', { object: 'orders', where: {} }); expect(results.length).toBe(5); }); it('should handle undefined query parameters', async () => { - const results = await driver.find('orders', {}); + const results = await driver.find('orders', { object: 'orders' }); expect(results.length).toBe(5); }); @@ -280,7 +280,7 @@ describe('SqliteWasmDriver Advanced Operations (SQLite)', () => { await driver.create('nullable_test', { id: '1', name: null, value: null }); - const result = await driver.findOne('nullable_test', '1' as any); + const result = await driver.findOne('nullable_test', { object: 'nullable_test', where: { id: '1' } }); expect(result).toBeDefined(); expect(result.name).toBeNull(); expect(result.value).toBeNull(); @@ -288,6 +288,7 @@ describe('SqliteWasmDriver Advanced Operations (SQLite)', () => { it('should handle pagination with offset and limit', async () => { const page1 = await driver.find('orders', { + object: 'orders', orderBy: [{ field: 'id', order: 'asc' }], offset: 0, limit: 2, @@ -296,6 +297,7 @@ describe('SqliteWasmDriver Advanced Operations (SQLite)', () => { expect(page1[0].id).toBe('1'); const page2 = await driver.find('orders', { + object: 'orders', orderBy: [{ field: 'id', order: 'asc' }], offset: 2, limit: 2, @@ -305,12 +307,13 @@ describe('SqliteWasmDriver Advanced Operations (SQLite)', () => { }); it('should handle offset beyond total records', async () => { - const results = await driver.find('orders', { offset: 100, limit: 10 }); + const results = await driver.find('orders', { object: 'orders', offset: 100, limit: 10 }); expect(results.length).toBe(0); }); it('should handle complex nested filters', async () => { const results = await driver.find('orders', { + object: 'orders', where: { $or: [ { $and: [{ status: 'completed' }, { amount: { $gt: 100 } }] }, @@ -324,6 +327,7 @@ describe('SqliteWasmDriver Advanced Operations (SQLite)', () => { it('should handle contains filter', async () => { const results = await driver.find('orders', { + object: 'orders', where: { product: { $contains: 'top' } }, }); @@ -333,6 +337,7 @@ describe('SqliteWasmDriver Advanced Operations (SQLite)', () => { it('should handle in filter', async () => { const results = await driver.find('orders', { + object: 'orders', where: { status: { $in: ['completed', 'pending'] } }, }); @@ -341,6 +346,7 @@ describe('SqliteWasmDriver Advanced Operations (SQLite)', () => { it('should handle nin (not in) filter', async () => { const results = await driver.find('orders', { + object: 'orders', where: { status: { $nin: ['cancelled'] } }, }); @@ -348,14 +354,14 @@ describe('SqliteWasmDriver Advanced Operations (SQLite)', () => { }); it('should handle findOne with query parameter', async () => { - const result = await driver.findOne('orders', { where: { customer: 'Charlie' } }); + const result = await driver.findOne('orders', { object: 'orders', where: { customer: 'Charlie' } }); expect(result).toBeDefined(); expect(result.customer).toBe('Charlie'); }); it('should return null for non-existent record', async () => { - const result = await driver.findOne('orders', 'nonexistent' as any); + const result = await driver.findOne('orders', { object: 'orders', where: { id: 'nonexistent' } }); expect(result).toBeNull(); }); diff --git a/packages/plugins/driver-sqlite-wasm/src/sqlite-wasm-driver-schema.test.ts b/packages/plugins/driver-sqlite-wasm/src/sqlite-wasm-driver-schema.test.ts index 9ae8c49634..d5a270d1ac 100644 --- a/packages/plugins/driver-sqlite-wasm/src/sqlite-wasm-driver-schema.test.ts +++ b/packages/plugins/driver-sqlite-wasm/src/sqlite-wasm-driver-schema.test.ts @@ -127,7 +127,7 @@ describe('SqliteWasmDriver Schema Sync (SQLite)', () => { users: ['u1', 'u2'], }); - const results = await driver.find('multi_test', {}); + const results = await driver.find('multi_test', { object: 'multi_test' }); const row = results[0]; expect(row.tags).toEqual(['a', 'b']); @@ -150,7 +150,7 @@ describe('SqliteWasmDriver Schema Sync (SQLite)', () => { expect(columns).toHaveProperty('completion'); await driver.create('percent_test', { completion: 0.85 }); - const res = await driver.find('percent_test', {}); + const res = await driver.find('percent_test', { object: 'percent_test' }); expect(res[0].completion).toBe(0.85); }); @@ -244,7 +244,7 @@ describe('SqliteWasmDriver Schema Sync (SQLite)', () => { work_hours: '09:00:00', }); - const res = await driver.find('new_types_test', {}); + const res = await driver.find('new_types_test', { object: 'new_types_test' }); const row = res[0]; expect(row.email).toBe('test@example.com'); diff --git a/packages/plugins/driver-sqlite-wasm/src/sqlite-wasm-driver.test.ts b/packages/plugins/driver-sqlite-wasm/src/sqlite-wasm-driver.test.ts index 3123e2ea46..328e3d3b05 100644 --- a/packages/plugins/driver-sqlite-wasm/src/sqlite-wasm-driver.test.ts +++ b/packages/plugins/driver-sqlite-wasm/src/sqlite-wasm-driver.test.ts @@ -40,6 +40,7 @@ describe('SqliteWasmDriver (in-memory)', () => { it('should find objects with filters', async () => { const results = await driver.find('users', { + object: 'users', fields: ['name', 'age'], where: { age: { $gt: 18 } }, orderBy: [{ field: 'name', order: 'asc' }], @@ -51,6 +52,7 @@ describe('SqliteWasmDriver (in-memory)', () => { it('should apply simple AND/OR logic', async () => { const results = await driver.find('users', { + object: 'users', where: { $or: [{ age: 17 }, { age: { $gt: 29 } }] }, }); const names = results.map((r: any) => r.name).sort(); @@ -58,29 +60,29 @@ describe('SqliteWasmDriver (in-memory)', () => { }); it('should find one object by id', async () => { - const [alice] = await driver.find('users', { where: { name: 'Alice' } }); + const [alice] = await driver.find('users', { object: 'users', where: { name: 'Alice' } }); expect(alice).toBeDefined(); - const fetched = await driver.findOne('users', alice.id as any); + const fetched = await driver.findOne('users', { object: 'users', where: { id: alice.id } }); expect(fetched.name).toBe('Alice'); }); it('should create an object', async () => { await driver.create('users', { name: 'Eve', age: 22 }); - const [eve] = await driver.find('users', { where: { name: 'Eve' } }); + const [eve] = await driver.find('users', { object: 'users', where: { name: 'Eve' } }); expect(eve.age).toBe(22); }); it('should update an object', async () => { - const [bob] = await driver.find('users', { where: { name: 'Bob' } }); + const [bob] = await driver.find('users', { object: 'users', where: { name: 'Bob' } }); await driver.update('users', bob.id, { age: 18 }); - const updated = await driver.findOne('users', bob.id as any); + const updated = await driver.findOne('users', { object: 'users', where: { id: bob.id } }); expect(updated.age).toBe(18); }); it('should delete an object', async () => { - const [charlie] = await driver.find('users', { where: { name: 'Charlie' } }); + const [charlie] = await driver.find('users', { object: 'users', where: { name: 'Charlie' } }); await driver.delete('users', charlie.id); - const deleted = await driver.findOne('users', charlie.id as any); + const deleted = await driver.findOne('users', { object: 'users', where: { id: charlie.id } }); expect(deleted).toBeNull(); }); @@ -114,7 +116,7 @@ describe('SqliteWasmDriver (file persistence)', () => { await d1.disconnect(); const d2 = new SqliteWasmDriver({ filename: dbPath, persist: 'on-disconnect' }); - const rows = await d2.find('items', {}); + const rows = await d2.find('items', { object: 'items' }); expect(rows.length).toBe(1); expect(rows[0].label).toBe('first'); await d2.disconnect(); @@ -132,7 +134,7 @@ describe('SqliteWasmDriver (file persistence)', () => { await d1.flush(); const d2 = new SqliteWasmDriver({ filename: dbPath, persist: 'on-write' }); - const rows = await d2.find('items', {}); + const rows = await d2.find('items', { object: 'items' }); expect(rows.length).toBe(1); await d1.disconnect(); await d2.disconnect(); diff --git a/packages/spec/authorable-surface.json b/packages/spec/authorable-surface.json index cde48c6e1a..e619d73b9e 100644 --- a/packages/spec/authorable-surface.json +++ b/packages/spec/authorable-surface.json @@ -3390,6 +3390,7 @@ "data/DriverDefinition:icon", "data/DriverDefinition:id", "data/DriverDefinition:label", + "data/DriverOptions:bypassTenantAudit", "data/DriverOptions:preserveAudit", "data/DriverOptions:skipCache", "data/DriverOptions:tenantId", diff --git a/packages/spec/src/data/analytics.zod.ts b/packages/spec/src/data/analytics.zod.ts index 8be3b647ef..4729f904a0 100644 --- a/packages/spec/src/data/analytics.zod.ts +++ b/packages/spec/src/data/analytics.zod.ts @@ -186,3 +186,16 @@ export function defineCube(config: z.input): Cube { return CubeSchema.parse(config); } export type AnalyticsQuery = z.infer; + +/** + * Author-tier `AnalyticsQuery` — what a caller writes, before `.parse()` fills + * the defaults in. `timezone` is `.default('UTC')`, so it is optional here and + * REQUIRED on {@link AnalyticsQuery}: the two tiers are genuinely different + * types, and only the parse turns one into the other. + * + * Every executor (`IAnalyticsService.query`, the analytics strategies) takes + * the PARSED type, because a request body reaches them through the schema. Use + * this one for the literal handed to `AnalyticsQuerySchema.parse()` — the + * mirror of `QueryInput` / `QueryAST` in `data/query.zod.ts`. + */ +export type AnalyticsQueryInput = z.input; diff --git a/packages/spec/src/data/driver.zod.ts b/packages/spec/src/data/driver.zod.ts index 6f36f141dd..57099123cb 100644 --- a/packages/spec/src/data/driver.zod.ts +++ b/packages/spec/src/data/driver.zod.ts @@ -76,6 +76,26 @@ export const DriverOptionsSchema = lazySchema(() => z.object({ * usual. A normal update leaves this unset, so `updated_at` always advances. */ preserveAudit: z.boolean().optional().describe('Historical import: keep a supplied updated_at instead of force-stamping now (from ExecutionContext.preserveAudit)'), + + /** + * Silence the tenant-audit warning a driver emits when it writes to a + * tenant-scoped object without a `tenantId` — the write is deliberately + * global, not an isolation bug. + * + * This key was live long before it was declared (#4311): `SqlDriver` reads it + * (`bypassTenantAudit === true` → skip the warning) and its own warning text + * tells callers to set it, the ObjectQL engine sets it for system-context + * calls, and `service-settings` / `service-datasource` pass it on every + * global-scope write. Only the schema never heard of it, so every driver read + * went through an `as any` and every caller through an untyped options object. + * Nothing type-checked either side, so the gap stayed invisible. + * + * A driver MAY warn on a tenant-scoped write that carries neither `tenantId` + * nor this flag; it MUST NOT change what the write does. Suppressing an audit + * warning is not a permission — it does not widen, narrow, or re-target the + * rows a write touches. + */ + bypassTenantAudit: z.boolean().optional().describe('Suppress the driver tenant-audit warning for a deliberately global write on a tenant-scoped object (diagnostics only — never changes what the write touches)'), })); /** diff --git a/scripts/check-type-check-coverage.mjs b/scripts/check-type-check-coverage.mjs index 80e2e958eb..d5e00ab200 100644 --- a/scripts/check-type-check-coverage.mjs +++ b/scripts/check-type-check-coverage.mjs @@ -9,11 +9,22 @@ // typecheck job covered exactly four targets (spec, examples, // downstream-contract, docs code blocks) -- so for most packages NOTHING read // src/ or the tests with a type checker at all. #4311 measured the hole: -// 380 code-tier errors across 18 packages, 241 of them in driver-sql tests -// passing authored-shape literals into the freshly narrowed QueryAST. A green +// 380 code-tier errors across 18 packages, 241 of them in driver-sql. A green // test suite no tsc has ever read is not evidence of a contract; this gate // makes the coverage hole itself the failure, so it can only shrink. // +// What the first burn-down found is worth knowing before reading a number +// below as "N test literals to fix". The three drivers (driver-sql 241, +// driver-sqlite-wasm 27, driver-memory 23) were filed as one playbook -- +// author-tier literals in a parsed-tier parameter -- and 165 of their 292 +// were something else entirely: 118 for a `bypassTenantAudit` driver option +// that SqlDriver read (through an `as any`), the engine set, and two services +// passed, while `DriverOptionsSchema` had never declared it; 41 for a bare-id +// `findOne(object, id)` branch on no contract, which the other two drivers +// answered differently; 4 for a `tenancy` key `initObjects` consumed but did +// not declare; 19 for an analytics `timezone` default. The tests were right +// and the types were wrong. A tsc count is a place to look, never a verdict. +// // node scripts/check-type-check-coverage.mjs // node scripts/check-type-check-coverage.mjs --self-test // @@ -87,7 +98,8 @@ const TEST_FILE = /\.(test|spec)\.tsx?$/; // implicit-any params, TS6133 unused) -- so each note says what the pile is // made of. Nobody should mistake a config-tier pile for real breakage, or -- // worse -- the reverse: `core` at 91 raw has 3 real errors, while -// `driver-sql`'s 241 are ALL real. +// `driver-sql`'s 241 were ALL real (and, as the header notes, mostly real +// about the types rather than about the tests). // // The tiers are not independent, which is why these numbers get re-measured // rather than decremented. Under `moduleResolution: NodeNext` a relative @@ -111,18 +123,6 @@ const DEBT = { errors: 12, note: 'code-tier 8 (TS2322/TS2554) + 3 config-tier + 1 noise.', }, - '@objectstack/driver-memory': { - errors: 23, - note: 'all code-tier: TS2345/TS2741/TS2339, the QueryAST authored-vs-parsed playbook.', - }, - '@objectstack/driver-sql': { - errors: 241, - note: 'ALL code-tier (TS2345 x123, TS2353 x118): tests pass authored-shape literals into the parsed QueryAST type across 8+ test files; #4196/#4286 narrowed QueryAST and no tsc ever read these tests. 63% of the whole audit; single playbook, fix as one batch.', - }, - '@objectstack/driver-sqlite-wasm': { - errors: 27, - note: 'all code-tier (TS2345/TS2353), same QueryAST playbook as driver-sql.', - }, '@objectstack/hono': { errors: 3, note: 'all code-tier (TS2769/TS18046).', From c9e3154440f48fc0e24fbf23f78bc66dc96d52ff Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 31 Jul 2026 11:56:54 +0000 Subject: [PATCH 2/4] fix(driver-sql,driver-sqlite-wasm,driver-memory): 111 `as any` casts were opting the driver call sites out of the check they had just been given (#4311) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Onboarding a package is supposed to make its `typecheck` mean something. These three declared it while 111 driver-call arguments were cast to `any` — the gate went green over call sites the type checker was not allowed to read. Stripping the casts produced 66 fresh errors, every one real: - 65 were a missing `object` on the query literal, the same defect the first commit fixed 127 times in the sites that were visible. - 1 was a silent runtime bug. `sql-driver-aggregate-temporal-output.test.ts` asked for `orderBy: [['id', 'asc']]`; the driver reads `item.field`, a tuple has none, so `if (item.field)` was false and the sort never reached SQL. The helper is named `viaFind` and exists to read a column in order — it had been reading whatever order the rows came back in, in the one file whose subject is output ordering. The tuple form appears nowhere else in the repo. 43 of the casts were needed by nothing at all. Exactly 2 are load-bearing and stay: `sql-driver-filter-no-silent-drop` and `memory-filter-ast-vocabulary` both feed `where` shapes `isFilterAST()` refuses, deliberately, to prove the driver throws rather than dropping the condition — those now say `where as FilterCondition` next to a comment, so the one honest cast in the package is not camouflaged by a hundred idle ones. Verified: `turbo run typecheck` 122/122 (three new), `pnpm test` 132/132, eslint, the 12 lint.yml check gates, coverage ratchet self-test 18/18. Claude-Session: https://claude.ai/code/session_011m13wbaZziPveBdtQthrXd Co-authored-by: Claude --- .../src/memory-datetime-storage.test.ts | 16 ++++++------- ...ry-driver-calendar-day-upper-bound.test.ts | 6 ++--- .../src/memory-filter-ast-vocabulary.test.ts | 6 ++++- .../src/memory-pagination-conformance.test.ts | 2 +- .../src/memory-temporal-conformance.test.ts | 14 +++++------ .../src/sql-driver-advanced.test.ts | 8 +++---- ...l-driver-aggregate-temporal-output.test.ts | 5 +++- .../src/sql-driver-autonumber-tokens.test.ts | 12 +++++----- .../src/sql-driver-autonumber-tx.test.ts | 6 ++--- .../src/sql-driver-autonumber.test.ts | 6 ++--- ...ql-driver-calendar-day-upper-bound.test.ts | 6 ++--- .../sql-driver-external-remote-name.test.ts | 22 ++++++++--------- .../sql-driver-filter-no-silent-drop.test.ts | 7 +++++- .../src/sql-driver-index-drift.test.ts | 2 +- .../src/sql-driver-multiwrite-tx.test.ts | 8 +++---- .../src/sql-driver-nested-and-filter.test.ts | 2 +- .../src/sql-driver-null-operators.test.ts | 24 +++++++++---------- .../sql-driver-pagination-conformance.test.ts | 2 +- .../src/sql-driver-schema-sync-stats.test.ts | 16 ++++++------- .../driver-sql/src/sql-driver-schema.test.ts | 14 +++++------ .../sql-driver-temporal-conformance.test.ts | 10 ++++---- .../src/sql-driver-timestamp-format.test.ts | 4 ++-- .../src/sql-driver-unique-tenancy.test.ts | 4 ++-- ...sql-driver-unknown-column-recovery.test.ts | 2 +- .../plugins/driver-sql/src/sql-driver.test.ts | 2 +- .../src/sqlite-wasm-driver-advanced.test.ts | 8 +++---- .../src/sqlite-wasm-driver-autonumber.test.ts | 6 ++--- .../src/sqlite-wasm-driver.test.ts | 2 +- .../sqlite-wasm-temporal-conformance.test.ts | 10 ++++---- scripts/check-type-check-coverage.mjs | 10 ++++++++ 30 files changed, 132 insertions(+), 110 deletions(-) diff --git a/packages/plugins/driver-memory/src/memory-datetime-storage.test.ts b/packages/plugins/driver-memory/src/memory-datetime-storage.test.ts index 2d6d868ecb..cc093639b0 100644 --- a/packages/plugins/driver-memory/src/memory-datetime-storage.test.ts +++ b/packages/plugins/driver-memory/src/memory-datetime-storage.test.ts @@ -60,12 +60,12 @@ describe('InMemoryDriver Field.datetime storage (#4047)', () => { it('stores every writer form as canonical UTC ISO text', async () => { await seedMixed(); - const raw = await driver.find('task', {} as any); + const raw = await driver.find('task', { object: 'task' }); for (const row of raw) { expect(typeof (row as any).created_at, `${(row as any).id} stored form`).toBe('string'); expect((row as any).created_at).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/); } - const midnight = raw.find((r: any) => r.id === 'd_midnight') as any; + const midnight = raw.find((r: any) => r.id === 'd_midnight'); expect(midnight.created_at).toBe('2026-07-28T00:00:00.000Z'); }); @@ -92,10 +92,10 @@ describe('InMemoryDriver Field.datetime storage (#4047)', () => { it('keeps #3777/#4042 bound semantics on top of the converged storage', async () => { await seedMixed(); - const gte = await driver.find('task', { where: { created_at: { $gte: '2026-07-28' } } } as any); + const gte = await driver.find('task', { object: 'task', where: { created_at: { $gte: '2026-07-28' } } }); expect(ids(gte)).toEqual(['d_midnight', 'd_next_day', 's_evening', 's_morning']); - const lt = await driver.find('task', { where: { created_at: { $lt: '2026-07-28' } } } as any); + const lt = await driver.find('task', { object: 'task', where: { created_at: { $lt: '2026-07-28' } } }); expect(ids(lt)).toEqual(['d_yesterday', 's_old']); const instant = await driver.find('task', { @@ -144,7 +144,7 @@ describe('InMemoryDriver Field.datetime storage (#4047)', () => { // honest subject for an update-path assertion. await driver.create('task', { id: 'u1', due_at: '2026-04-19T10:00:00.000Z' }); await driver.update('task', 'u1', { due_at: new Date('2026-07-28T09:15:00Z') }); - const row: any = (await driver.find('task', { where: { id: 'u1' } } as any))[0]; + const row: any = (await driver.find('task', { object: 'task', where: { id: 'u1' } }))[0]; expect(row.due_at).toBe('2026-07-28T09:15:00.000Z'); // …and the converged value is reachable by a date window, which is the @@ -165,9 +165,9 @@ describe('InMemoryDriver Field.datetime storage (#4047)', () => { ] as const) { await driver.create('task', { id, created_on }); } - const all = await driver.find('task', {} as any); + const all = await driver.find('task', { object: 'task' }); for (const row of all) expect(typeof (row as any).created_on).toBe('string'); - expect((all.find((r: any) => r.id === 'on_obj') as any).created_on).toBe('2026-07-28'); + expect((all.find((r: any) => r.id === 'on_obj')).created_on).toBe('2026-07-28'); const found = await driver.find('task', { where: { created_on: { $gte: '2026-04-29', $lte: '2026-07-28' } }, @@ -177,7 +177,7 @@ describe('InMemoryDriver Field.datetime storage (#4047)', () => { it('an undeclared object is left alone (no schema → no coercion)', async () => { await driver.create('freeform', { id: 'f1', when: new Date('2026-07-28T09:15:00Z') }); - const row: any = (await driver.find('freeform', {} as any))[0]; + const row: any = (await driver.find('freeform', { object: 'freeform' }))[0]; expect(row.when).toBeInstanceOf(Date); }); }); diff --git a/packages/plugins/driver-memory/src/memory-driver-calendar-day-upper-bound.test.ts b/packages/plugins/driver-memory/src/memory-driver-calendar-day-upper-bound.test.ts index 6a19a275c4..d46ad87846 100644 --- a/packages/plugins/driver-memory/src/memory-driver-calendar-day-upper-bound.test.ts +++ b/packages/plugins/driver-memory/src/memory-driver-calendar-day-upper-bound.test.ts @@ -58,13 +58,13 @@ describe('InMemoryDriver — bare-day $lte covers the whole day (#4042)', () => }); it('$gte / $gt / $lt keep their midnight anchoring', async () => { - const gte = await driver.find('task', { where: { created_at: { $gte: '2026-07-28' } } } as any); + const gte = await driver.find('task', { object: 'task', where: { created_at: { $gte: '2026-07-28' } } }); expect(ids(gte)).toEqual(['t_evening', 't_midnight', 't_morning']); - const gt = await driver.find('task', { where: { created_at: { $gt: '2026-07-28' } } } as any); + const gt = await driver.find('task', { object: 'task', where: { created_at: { $gt: '2026-07-28' } } }); expect(ids(gt)).toEqual(['t_evening', 't_midnight', 't_morning']); // string '…T00:00' > '2026-07-28' - const lt = await driver.find('task', { where: { created_at: { $lt: '2026-07-28' } } } as any); + const lt = await driver.find('task', { object: 'task', where: { created_at: { $lt: '2026-07-28' } } }); expect(ids(lt)).toEqual(['t_old', 't_yesterday']); }); diff --git a/packages/plugins/driver-memory/src/memory-filter-ast-vocabulary.test.ts b/packages/plugins/driver-memory/src/memory-filter-ast-vocabulary.test.ts index 2c588ad624..42c05d9066 100644 --- a/packages/plugins/driver-memory/src/memory-filter-ast-vocabulary.test.ts +++ b/packages/plugins/driver-memory/src/memory-filter-ast-vocabulary.test.ts @@ -22,6 +22,7 @@ */ import { describe, it, expect, beforeEach } from 'vitest'; import { VALID_AST_OPERATORS } from '@objectstack/spec/data'; +import type { FilterCondition } from '@objectstack/spec/data'; import { InMemoryDriver } from './memory-driver.js'; const TABLE = 'vocab_probe'; @@ -37,8 +38,11 @@ describe('InMemoryDriver filter vocabulary ↔ VALID_AST_OPERATORS', () => { }); /** Operators are exercised through `find`, the path a real query takes. */ + // Cast deliberately: several `where` shapes below are ones the AST gate + // refuses, fed in to prove the driver throws instead of silently dropping + // the condition. `unknown` is the honest parameter type. const find = (where: unknown) => - driver.find(TABLE, { object: TABLE, fields: ['id'], where } as any); + driver.find(TABLE, { object: TABLE, fields: ['id'], where: where as FilterCondition }); it('reads a non-empty operator set from the spec', () => { // Guards every assertion below from passing vacuously. diff --git a/packages/plugins/driver-memory/src/memory-pagination-conformance.test.ts b/packages/plugins/driver-memory/src/memory-pagination-conformance.test.ts index 3a8dcd912a..153803e30d 100644 --- a/packages/plugins/driver-memory/src/memory-pagination-conformance.test.ts +++ b/packages/plugins/driver-memory/src/memory-pagination-conformance.test.ts @@ -63,7 +63,7 @@ describe('InMemoryDriver — paged reads are a partition of the result set (obje paged.push(...page); } - const whole = await driver.find('ticket', { orderBy: [...testCase.orderBy] } as any); + const whole = await driver.find('ticket', { object: 'ticket', orderBy: [...testCase.orderBy] }); expect(paged.map((r) => r.id)).toEqual(whole.map((r: any) => r.id)); }); } diff --git a/packages/plugins/driver-memory/src/memory-temporal-conformance.test.ts b/packages/plugins/driver-memory/src/memory-temporal-conformance.test.ts index 60a999e8b1..c8a415b1d1 100644 --- a/packages/plugins/driver-memory/src/memory-temporal-conformance.test.ts +++ b/packages/plugins/driver-memory/src/memory-temporal-conformance.test.ts @@ -103,14 +103,14 @@ describe('driver-memory — temporal conformance', () => { for (const c of TEMPORAL_CASES) { it(c.name, async () => { - const rows = await driver.find('conformance', { where: c.filter } as any); + const rows = await driver.find('conformance', { object: 'conformance', where: c.filter }); const got = (rows as any[]).map((r) => r.id).sort(); expect(got, c.note).toEqual([...c.expected].sort()); }); if (c.tokenFilter) { it(`${c.name} — via relative tokens`, async () => { - const rows = await driver.find('conformance', { where: resolveTokens(c.tokenFilter) } as any); + const rows = await driver.find('conformance', { object: 'conformance', where: resolveTokens(c.tokenFilter) }); const got = (rows as any[]).map((r) => r.id).sort(); expect(got, c.note).toEqual([...c.expected].sort()); }); @@ -141,7 +141,7 @@ describe('driver-memory — Field.time conformance', () => { for (const c of TEMPORAL_TIME_CASES) { it(c.name, async () => { - const rows = await driver.find('time_conformance', { where: c.filter } as any); + const rows = await driver.find('time_conformance', { object: 'time_conformance', where: c.filter }); const got = (rows as any[]).map((r) => r.id).sort(); expect(got, c.note).toEqual([...c.expected].sort()); }); @@ -164,7 +164,7 @@ describe('driver-memory — temporal conformance on rows that predate the schema }); it('converged every pre-schema row to the storage canon (the premise, so the sweep cannot pass vacuously)', async () => { - const rows = await driver.find('conformance', {} as any); + const rows = await driver.find('conformance', { object: 'conformance' }); expect(rows).toHaveLength(TEMPORAL_ROWS.length); for (const row of rows as any[]) { const expected = TEMPORAL_ROWS.find((r) => r.id === row.id)!; @@ -179,7 +179,7 @@ describe('driver-memory — temporal conformance on rows that predate the schema // already swept above — a divergence here is a convergence bug by construction. for (const c of TEMPORAL_CASES) { it(c.name, async () => { - const rows = await driver.find('conformance', { where: c.filter } as any); + const rows = await driver.find('conformance', { object: 'conformance', where: c.filter }); const got = (rows as any[]).map((r) => r.id).sort(); expect(got, c.note).toEqual([...c.expected].sort()); }); @@ -199,7 +199,7 @@ describe('driver-memory — Field.time conformance on rows that predate the sche }); it('converged every pre-schema wall clock to the storage canon (the premise)', async () => { - const rows = await driver.find('time_conformance', {} as any); + const rows = await driver.find('time_conformance', { object: 'time_conformance' }); expect(rows).toHaveLength(TEMPORAL_TIME_ROWS.length); for (const row of rows as any[]) { const expected = TEMPORAL_TIME_ROWS.find((r) => r.id === row.id)!; @@ -209,7 +209,7 @@ describe('driver-memory — Field.time conformance on rows that predate the sche for (const c of TEMPORAL_TIME_CASES) { it(c.name, async () => { - const rows = await driver.find('time_conformance', { where: c.filter } as any); + const rows = await driver.find('time_conformance', { object: 'time_conformance', where: c.filter }); const got = (rows as any[]).map((r) => r.id).sort(); expect(got, c.note).toEqual([...c.expected].sort()); }); diff --git a/packages/plugins/driver-sql/src/sql-driver-advanced.test.ts b/packages/plugins/driver-sql/src/sql-driver-advanced.test.ts index 97a4d0a4b3..c1f8d80b9e 100644 --- a/packages/plugins/driver-sql/src/sql-driver-advanced.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-advanced.test.ts @@ -144,7 +144,7 @@ describe('SqlDriver Advanced Operations (SQLite)', () => { }); it('should update many records', async () => { - const result = await driver.updateMany('orders', { where: { status: 'pending' } } as any, { status: 'processing' }); + const result = await driver.updateMany('orders', { object: 'orders', where: { status: 'pending' } }, { status: 'processing' }); expect(result).toBeGreaterThan(0); @@ -153,7 +153,7 @@ describe('SqlDriver Advanced Operations (SQLite)', () => { }); it('should delete many records', async () => { - const result = await driver.deleteMany('orders', { where: { status: 'cancelled' } } as any); + const result = await driver.deleteMany('orders', { object: 'orders', where: { status: 'cancelled' } }); expect(result).toBe(1); @@ -162,10 +162,10 @@ describe('SqlDriver Advanced Operations (SQLite)', () => { }); it('should handle empty bulk update and delete', async () => { - const result = await driver.updateMany('orders', { where: { status: 'nonexistent' } } as any, { status: 'updated' }); + const result = await driver.updateMany('orders', { object: 'orders', where: { status: 'nonexistent' } }, { status: 'updated' }); expect(result).toBe(0); - const deleteResult = await driver.deleteMany('orders', { where: { id: 'nonexistent' } } as any); + const deleteResult = await driver.deleteMany('orders', { object: 'orders', where: { id: 'nonexistent' } }); expect(deleteResult).toBe(0); }); }); diff --git a/packages/plugins/driver-sql/src/sql-driver-aggregate-temporal-output.test.ts b/packages/plugins/driver-sql/src/sql-driver-aggregate-temporal-output.test.ts index 048fa982a7..0ccfc8866e 100644 --- a/packages/plugins/driver-sql/src/sql-driver-aggregate-temporal-output.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-aggregate-temporal-output.test.ts @@ -71,7 +71,10 @@ describe('temporal values leaving aggregate()/distinct() (#3797)', () => { /** What the same column looks like through the path that always formatted it. */ const viaFind = async (field: string) => { - const rows = await driver.find(TABLE, { orderBy: [['id', 'asc']] } as any); + // `orderBy: [['id', 'asc']]` until #4311. The driver reads `item.field`, + // which a tuple does not have, so the sort was silently dropped — this + // helper had been reading whatever order the rows came back in. + const rows = await driver.find(TABLE, { object: TABLE, orderBy: [{ field: 'id', order: 'asc' }] }); return rows.map((r: any) => r[field]); }; diff --git a/packages/plugins/driver-sql/src/sql-driver-autonumber-tokens.test.ts b/packages/plugins/driver-sql/src/sql-driver-autonumber-tokens.test.ts index 6cc3106487..4e9d7ed27f 100644 --- a/packages/plugins/driver-sql/src/sql-driver-autonumber-tokens.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-autonumber-tokens.test.ts @@ -43,8 +43,8 @@ describe('SqlDriver auto_number format tokens', () => { await driver.initObjects([ { name: 'andon', fields: { andon_no: { type: 'autonumber', format: 'AD{YYYYMMDD}{0000}' } } }, ]); - const r1 = await driver.create('andon', {}, { timezone: 'UTC' } as any); - const r2 = await driver.create('andon', {}, { timezone: 'UTC' } as any); + const r1 = await driver.create('andon', {}, { timezone: 'UTC' }); + const r2 = await driver.create('andon', {}, { timezone: 'UTC' }); expect(r1.andon_no).toBe(`AD${utcYmd()}0001`); expect(r2.andon_no).toBe(`AD${utcYmd()}0002`); }); @@ -57,7 +57,7 @@ describe('SqlDriver auto_number format tokens', () => { const k = (driver as any).knex; await k('andon').insert({ id: 'old', andon_no: 'AD202001010099' }); - const r = await driver.create('andon', {}, { timezone: 'UTC' } as any); + const r = await driver.create('andon', {}, { timezone: 'UTC' }); expect(r.andon_no).toBe(`AD${utcYmd()}0001`); // fresh scope, not 0100 }); @@ -113,9 +113,9 @@ describe('SqlDriver auto_number format tokens', () => { }, }, ]); - const a = await driver.create('plan', { organization_id: 'orgA', line: 'PROD' }, { timezone: 'UTC' } as any); - const b = await driver.create('plan', { organization_id: 'orgB', line: 'PROD' }, { timezone: 'UTC' } as any); - const a2 = await driver.create('plan', { organization_id: 'orgA', line: 'PROD' }, { timezone: 'UTC' } as any); + const a = await driver.create('plan', { organization_id: 'orgA', line: 'PROD' }, { timezone: 'UTC' }); + const b = await driver.create('plan', { organization_id: 'orgB', line: 'PROD' }, { timezone: 'UTC' }); + const a2 = await driver.create('plan', { organization_id: 'orgA', line: 'PROD' }, { timezone: 'UTC' }); expect(a.plan_no).toBe(`PROD${utcYmd()}001`); expect(a2.plan_no).toBe(`PROD${utcYmd()}002`); diff --git a/packages/plugins/driver-sql/src/sql-driver-autonumber-tx.test.ts b/packages/plugins/driver-sql/src/sql-driver-autonumber-tx.test.ts index 0864f92113..36e2153e07 100644 --- a/packages/plugins/driver-sql/src/sql-driver-autonumber-tx.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-autonumber-tx.test.ts @@ -73,12 +73,12 @@ describe('SqlDriver auto_number in-transaction (deadlock regression)', () => { // /batch endpoint wraps operations. Must commit, not hang. const trx = await driver!.beginTransaction(); const r1 = await withTimeout( - driver!.create('contract', { organization_id: 'org_x', name: 'A' }, { transaction: trx } as any), + driver!.create('contract', { organization_id: 'org_x', name: 'A' }, { transaction: trx }), 6000, 'batch create #1 (autonumber)', ); const r2 = await withTimeout( - driver!.create('contract', { organization_id: 'org_x', name: 'B' }, { transaction: trx } as any), + driver!.create('contract', { organization_id: 'org_x', name: 'B' }, { transaction: trx }), 6000, 'batch create #2 (autonumber)', ); @@ -106,7 +106,7 @@ describe('SqlDriver auto_number in-transaction (deadlock regression)', () => { const trx = await driver!.beginTransaction(); const r1 = await withTimeout( - driver!.create('contract', { organization_id: 'org_cold', name: 'C' }, { transaction: trx } as any), + driver!.create('contract', { organization_id: 'org_cold', name: 'C' }, { transaction: trx }), 6000, 'cold-cache create inside tx', ); diff --git a/packages/plugins/driver-sql/src/sql-driver-autonumber.test.ts b/packages/plugins/driver-sql/src/sql-driver-autonumber.test.ts index e691e0e2e9..6381d23853 100644 --- a/packages/plugins/driver-sql/src/sql-driver-autonumber.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-autonumber.test.ts @@ -229,9 +229,9 @@ describe('SqlDriver auto_number sequence', () => { ]); // No organization_id on the row, but provided via DriverOptions. - const r1 = await driver.create('contract', {}, { tenantId: 'org_options' } as any); - const r2 = await driver.create('contract', {}, { tenantId: 'org_options' } as any); - const r3 = await driver.create('contract', {}, { tenantId: 'org_other' } as any); + const r1 = await driver.create('contract', {}, { tenantId: 'org_options' }); + const r2 = await driver.create('contract', {}, { tenantId: 'org_options' }); + const r3 = await driver.create('contract', {}, { tenantId: 'org_other' }); expect(r1.contract_number).toBe('CTR-0001'); expect(r2.contract_number).toBe('CTR-0002'); diff --git a/packages/plugins/driver-sql/src/sql-driver-calendar-day-upper-bound.test.ts b/packages/plugins/driver-sql/src/sql-driver-calendar-day-upper-bound.test.ts index 72292b154a..c7624bf783 100644 --- a/packages/plugins/driver-sql/src/sql-driver-calendar-day-upper-bound.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-calendar-day-upper-bound.test.ts @@ -101,13 +101,13 @@ describe('bare-day $lte on Field.datetime — the #3777 repro', () => { }); it('$gte / $gt / $lt keep their midnight anchoring (the issue-table rows marked correct)', async () => { - const gte = await driver.find('task', { where: { created_at: { $gte: '2026-07-28' } } } as any); + const gte = await driver.find('task', { object: 'task', where: { created_at: { $gte: '2026-07-28' } } }); expect(ids(gte)).toEqual(['t_evening', 't_midnight', 't_morning']); - const gt = await driver.find('task', { where: { created_at: { $gt: '2026-07-28' } } } as any); + const gt = await driver.find('task', { object: 'task', where: { created_at: { $gt: '2026-07-28' } } }); expect(ids(gt)).toEqual(['t_evening', 't_morning']); // excludes the exact-midnight row - const lt = await driver.find('task', { where: { created_at: { $lt: '2026-07-28' } } } as any); + const lt = await driver.find('task', { object: 'task', where: { created_at: { $lt: '2026-07-28' } } }); expect(ids(lt)).toEqual(['t_old', 't_yesterday']); }); diff --git a/packages/plugins/driver-sql/src/sql-driver-external-remote-name.test.ts b/packages/plugins/driver-sql/src/sql-driver-external-remote-name.test.ts index fc857d709d..7ff55c2eda 100644 --- a/packages/plugins/driver-sql/src/sql-driver-external-remote-name.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-external-remote-name.test.ts @@ -49,12 +49,12 @@ async function seedRemote(path: string) { (fixture as any).name = 'fixture'; await fixture.connect?.(); // Physical table name deliberately differs from the object name used later. - await (fixture as any).initObjects([{ name: 'remote_customers', fields: FIELDS }]); - await (fixture as any).create('remote_customers', { + await (fixture).initObjects([{ name: 'remote_customers', fields: FIELDS }]); + await (fixture).create('remote_customers', { id: 'c1', name: 'Acme', flag: true, meta: { tier: 'gold' }, when: '2026-01-01', amount: 100, seen_at: new Date('2026-01-02T10:00:00.000Z'), }); - await (fixture as any).create('remote_customers', { + await (fixture).create('remote_customers', { id: 'c2', name: 'Globex', flag: false, meta: { tier: 'silver' }, when: '2026-02-15', amount: 250, seen_at: new Date('2026-02-16T08:00:00.000Z'), }); @@ -86,7 +86,7 @@ describe('SqlDriver external read path — remoteName resolution (ADR-0015)', () ).not.toThrow(); // The bug: this used to throw `no such table: ext_customer`. - const rows = await ext.find('ext_customer', {} as any); + const rows = await ext.find('ext_customer', { object: 'ext_customer' }); expect(rows).toHaveLength(2); const acme = rows.find((r: any) => r.name === 'Acme'); @@ -98,24 +98,24 @@ describe('SqlDriver external read path — remoteName resolution (ADR-0015)', () expect(typeof acme.amount).toBe('number'); // numeric scalar // count + findOne route to the remote table too. - expect(await ext.count('ext_customer', {} as any)).toBe(2); + expect(await ext.count('ext_customer', { object: 'ext_customer' })).toBe(2); const one = await ext.findOne('ext_customer', { object: 'ext_customer', where: { name: 'Globex' } }); expect(one?.name).toBe('Globex'); // Filtered reads hit the remote table. - const filtered = await ext.find('ext_customer', { where: { name: 'Acme' } } as any); + const filtered = await ext.find('ext_customer', { object: 'ext_customer', where: { name: 'Acme' } }); expect(filtered).toHaveLength(1); expect(filtered[0].name).toBe('Acme'); // Date filter — guards the coercion re-keying (§3): coercion maps are keyed // by the OBJECT name even though the builder now targets the remote table. - const byDate = await ext.find('ext_customer', { where: { when: '2026-02-15' } } as any); + const byDate = await ext.find('ext_customer', { object: 'ext_customer', where: { when: '2026-02-15' } }); expect(byDate).toHaveLength(1); expect(byDate[0].name).toBe('Globex'); // Datetime filter — the SQLite epoch-affinity case the §3 trap would break // if coercion were keyed by the physical (remote) name instead of object. - const byDatetime = await ext.find('ext_customer', { where: { seen_at: '2026-01-02T10:00:00.000Z' } } as any); + const byDatetime = await ext.find('ext_customer', { object: 'ext_customer', where: { seen_at: '2026-01-02T10:00:00.000Z' } }); expect(byDatetime.map((r: any) => r.name)).toContain('Acme'); // No object-named table was ever created in the remote db (no DDL leaked). @@ -132,7 +132,7 @@ describe('SqlDriver external read path — remoteName resolution (ADR-0015)', () await ext.connect?.(); try { await expect( - ext.initObjects([{ name: 'ext_customer', fields: FIELDS as any }]), + ext.initObjects([{ name: 'ext_customer', fields: FIELDS }]), ).rejects.toBeInstanceOf(ExternalSchemaModeViolationError); } finally { await ext.disconnect?.(); @@ -144,7 +144,7 @@ describe('SqlDriver external read path — remoteName resolution (ADR-0015)', () await ext.connect?.(); try { ext.registerExternalObject!({ name: 'remote_customers', external: {}, fields: FIELDS as any }); - const rows = await ext.find('remote_customers', {} as any); + const rows = await ext.find('remote_customers', { object: 'remote_customers' }); expect(rows.length).toBe(2); } finally { await ext.disconnect?.(); @@ -156,7 +156,7 @@ describe('SqlDriver external read path — remoteName resolution (ADR-0015)', () await ext.connect?.(); try { ext.registerExternalObject!({ name: 'ext_cust2', external: { remoteName: 'remote_customers', remoteSchema: 'mart' }, fields: FIELDS as any }); - const rows = await ext.find('ext_cust2', {} as any); + const rows = await ext.find('ext_cust2', { object: 'ext_cust2' }); expect(rows.length).toBe(2); } finally { await ext.disconnect?.(); diff --git a/packages/plugins/driver-sql/src/sql-driver-filter-no-silent-drop.test.ts b/packages/plugins/driver-sql/src/sql-driver-filter-no-silent-drop.test.ts index aae994e8a8..27aae14e04 100644 --- a/packages/plugins/driver-sql/src/sql-driver-filter-no-silent-drop.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-filter-no-silent-drop.test.ts @@ -22,6 +22,7 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { SqlDriver } from '../src/index.js'; +import type { FilterCondition } from '@objectstack/spec/data'; describe('SqlDriver rejects an uncompilable filter instead of dropping it', () => { let driver: SqlDriver; @@ -48,8 +49,12 @@ describe('SqlDriver rejects an uncompilable filter instead of dropping it', () = await driver.create('deal', { id: '2', stage: 'lost', amount: 20 }); }); + // The cast is the point of the file: every `where` below is a shape + // `isFilterAST()` REFUSED, fed in deliberately to prove the driver throws + // rather than dropping it. `unknown` is the honest parameter type; the cast + // is what lets an off-spec value reach a parameter that forbids it. const find = (where: unknown) => - driver.find('deal', { object: 'deal', fields: ['id'], where } as any); + driver.find('deal', { object: 'deal', fields: ['id'], where: where as FilterCondition }); it('throws on a bare triple whose operator the AST gate refused', async () => { // The exact shape a stored single-condition `before` view produced. diff --git a/packages/plugins/driver-sql/src/sql-driver-index-drift.test.ts b/packages/plugins/driver-sql/src/sql-driver-index-drift.test.ts index 6e323e95f9..e1587ea931 100644 --- a/packages/plugins/driver-sql/src/sql-driver-index-drift.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-index-drift.test.ts @@ -263,7 +263,7 @@ describe('SqlDriver index drift (#3728)', () => { expect(Object.values(uniques)).toContainEqual(['organization_id', 'code']); // Existing rows survived, and the cross-tenant insert the issue is about works. - expect(await driver.count('product', {} as any)).toBe(2); + expect(await driver.count('product', { object: 'product' })).toBe(2); const b = await driver.create('product', { organization_id: 'org_b', code: 'PROD-00001' }); expect(b.code).toBe('PROD-00001'); diff --git a/packages/plugins/driver-sql/src/sql-driver-multiwrite-tx.test.ts b/packages/plugins/driver-sql/src/sql-driver-multiwrite-tx.test.ts index 368a57e8c7..bf2e708542 100644 --- a/packages/plugins/driver-sql/src/sql-driver-multiwrite-tx.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-multiwrite-tx.test.ts @@ -41,8 +41,8 @@ describe('SqlDriver multi-write transaction (deadlock regression)', () => { it('commits TWO writes in one transaction without hanging', async () => { const k = await setup(); const trx = await driver!.beginTransaction(); - await withTimeout(driver!.create('t', { id: '1', name: 'A' }, { transaction: trx } as any), 6000, 'create #1'); - await withTimeout(driver!.create('t', { id: '2', name: 'B' }, { transaction: trx } as any), 6000, 'create #2'); + await withTimeout(driver!.create('t', { id: '1', name: 'A' }, { transaction: trx }), 6000, 'create #1'); + await withTimeout(driver!.create('t', { id: '2', name: 'B' }, { transaction: trx }), 6000, 'create #2'); await withTimeout(driver!.commit(trx), 6000, 'commit'); const rows = await k('t').select(); expect(rows.map((r: any) => r.id).sort()).toEqual(['1', '2']); @@ -51,8 +51,8 @@ describe('SqlDriver multi-write transaction (deadlock regression)', () => { it('rolls back all writes when the transaction is aborted', async () => { const k = await setup(); const trx = await driver!.beginTransaction(); - await driver!.create('t', { id: '1', name: 'A' }, { transaction: trx } as any); - await driver!.create('t', { id: '2', name: 'B' }, { transaction: trx } as any); + await driver!.create('t', { id: '1', name: 'A' }, { transaction: trx }); + await driver!.create('t', { id: '2', name: 'B' }, { transaction: trx }); await driver!.rollback(trx); const rows = await k('t').select(); expect(rows.length).toBe(0); diff --git a/packages/plugins/driver-sql/src/sql-driver-nested-and-filter.test.ts b/packages/plugins/driver-sql/src/sql-driver-nested-and-filter.test.ts index eb1f27d606..83850cfd1a 100644 --- a/packages/plugins/driver-sql/src/sql-driver-nested-and-filter.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-nested-and-filter.test.ts @@ -68,7 +68,7 @@ describe('SqlDriver — field key alongside a nested $and (#3650)', () => { }); it('intersects every level on find()', async () => { - const rows = await driver.find('opportunity', { where: NESTED_AND } as any); + const rows = await driver.find('opportunity', { object: 'opportunity', where: NESTED_AND }); expect(rows.map((r: any) => r.id)).toEqual(['b']); }); diff --git a/packages/plugins/driver-sql/src/sql-driver-null-operators.test.ts b/packages/plugins/driver-sql/src/sql-driver-null-operators.test.ts index 938eee2976..0386994792 100644 --- a/packages/plugins/driver-sql/src/sql-driver-null-operators.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-null-operators.test.ts @@ -42,60 +42,60 @@ describe('SqlDriver — null / empty operators (#2704)', () => { describe('array-format where', () => { it('equals + null → IS NULL (baseline that already worked)', async () => { - const rows = await driver.find('tasks', { where: [['assignee', '=', null]] } as any); + const rows = await driver.find('tasks', { object: 'tasks', where: [['assignee', '=', null]] }); expect(ids(rows)).toEqual(['2', '4']); }); it.each(['is_null', 'isnull', 'is_empty'])('%s → IS NULL', async (op) => { - const rows = await driver.find('tasks', { where: [['assignee', op, true]] } as any); + const rows = await driver.find('tasks', { object: 'tasks', where: [['assignee', op, true]] }); expect(ids(rows)).toEqual(['2', '4']); }); it.each(['is_not_null', 'isnotnull', 'is_not_empty'])('%s → IS NOT NULL', async (op) => { - const rows = await driver.find('tasks', { where: [['assignee', op, true]] } as any); + const rows = await driver.find('tasks', { object: 'tasks', where: [['assignee', op, true]] }); expect(ids(rows)).toEqual(['1', '3']); }); it('!= null → IS NOT NULL (not a `<> NULL` that matches nothing)', async () => { - const rows = await driver.find('tasks', { where: [['assignee', '!=', null]] } as any); + const rows = await driver.find('tasks', { object: 'tasks', where: [['assignee', '!=', null]] }); expect(ids(rows)).toEqual(['1', '3']); }); it('unknown operator throws instead of returning the whole table', async () => { await expect( - driver.find('tasks', { where: [['assignee', 'totally_bogus', null]] } as any), + driver.find('tasks', { object: 'tasks', where: [['assignee', 'totally_bogus', null]] }), ).rejects.toThrow(/Unsupported filter operator/); }); it('count with is_null is scoped, not the whole table', async () => { - const count = await driver.count('tasks', { where: [['assignee', 'isnull', true]] } as any); + const count = await driver.count('tasks', { object: 'tasks', where: [['assignee', 'isnull', true]] }); expect(count).toBe(2); }); }); describe('object-format where ($-operators)', () => { it('$null: true → IS NULL', async () => { - const rows = await driver.find('tasks', { where: { assignee: { $null: true } } } as any); + const rows = await driver.find('tasks', { object: 'tasks', where: { assignee: { $null: true } } }); expect(ids(rows)).toEqual(['2', '4']); }); it('$null: false → IS NOT NULL', async () => { - const rows = await driver.find('tasks', { where: { assignee: { $null: false } } } as any); + const rows = await driver.find('tasks', { object: 'tasks', where: { assignee: { $null: false } } }); expect(ids(rows)).toEqual(['1', '3']); }); it('$ne: null → IS NOT NULL', async () => { - const rows = await driver.find('tasks', { where: { assignee: { $ne: null } } } as any); + const rows = await driver.find('tasks', { object: 'tasks', where: { assignee: { $ne: null } } }); expect(ids(rows)).toEqual(['1', '3']); }); it('$startsWith → prefix LIKE', async () => { - const rows = await driver.find('tasks', { where: { assignee: { $startsWith: 'a' } } } as any); + const rows = await driver.find('tasks', { object: 'tasks', where: { assignee: { $startsWith: 'a' } } }); expect(ids(rows)).toEqual(['1']); }); it('$regex (better-auth contains) → substring LIKE, not exact match', async () => { - const rows = await driver.find('tasks', { where: { assignee: { $regex: 'aro' } } } as any); + const rows = await driver.find('tasks', { object: 'tasks', where: { assignee: { $regex: 'aro' } } }); expect(ids(rows)).toEqual(['3']); }); @@ -112,7 +112,7 @@ describe('SqlDriver — null / empty operators (#2704)', () => { it('unknown $-operator throws instead of a silent equality compare', async () => { await expect( - driver.find('tasks', { where: { assignee: { $bogus: 1 } } } as any), + driver.find('tasks', { object: 'tasks', where: { assignee: { $bogus: 1 } } }), ).rejects.toThrow(/Unsupported filter operator/); }); }); diff --git a/packages/plugins/driver-sql/src/sql-driver-pagination-conformance.test.ts b/packages/plugins/driver-sql/src/sql-driver-pagination-conformance.test.ts index 4f8d3aab06..b6c0d77005 100644 --- a/packages/plugins/driver-sql/src/sql-driver-pagination-conformance.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-pagination-conformance.test.ts @@ -116,7 +116,7 @@ describe('SqlDriver — paged reads are a partition of the result set (objectui# } it('leaves an unordered read alone — no sort is imposed on a caller who asked for none', async () => { - const rows = await driver.find('ticket', {} as any, { bypassTenantAudit: true } as any); + const rows = await driver.find('ticket', { object: 'ticket' }, { bypassTenantAudit: true }); expect(rows.map((r) => r.id)).toEqual(PAGINATION_ROWS.map((r) => r.id)); }); }); diff --git a/packages/plugins/driver-sql/src/sql-driver-schema-sync-stats.test.ts b/packages/plugins/driver-sql/src/sql-driver-schema-sync-stats.test.ts index bbdccfae59..bce0ce5339 100644 --- a/packages/plugins/driver-sql/src/sql-driver-schema-sync-stats.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-schema-sync-stats.test.ts @@ -44,7 +44,7 @@ describe('SqlDriver.getSchemaSyncStats', () => { it('counts every table it creates in an empty store', async () => { const driver = sqlite(); try { - await driver.initObjects(OBJECTS as any); + await driver.initObjects(OBJECTS); expect(driver.getSchemaSyncStats()).toEqual({ created: 2, existing: 0 }); } finally { @@ -56,9 +56,9 @@ describe('SqlDriver.getSchemaSyncStats', () => { it('stays "created from empty" across repeated syncs of its own tables', async () => { const driver = sqlite(); try { - await driver.initObjects(OBJECTS as any); - await driver.initObjects(OBJECTS as any); - await driver.syncSchema('alpha', OBJECTS[0] as any); + await driver.initObjects(OBJECTS); + await driver.initObjects(OBJECTS); + await driver.syncSchema('alpha', OBJECTS[0]); expect(driver.getSchemaSyncStats()).toEqual({ created: 2, existing: 0 }); } finally { @@ -84,7 +84,7 @@ describe('SqlDriver.getSchemaSyncStats', () => { // long after the run itself has passed. const first = new SqlDriver(opts); try { - await first.initObjects(OBJECTS as any); + await first.initObjects(OBJECTS); expect(first.getSchemaSyncStats()).toEqual({ created: 2, existing: 0 }); } finally { await first.disconnect(); @@ -93,7 +93,7 @@ describe('SqlDriver.getSchemaSyncStats', () => { // A later boot against the same store — the tables precede it. const second = new SqlDriver(opts); try { - await second.initObjects(OBJECTS as any); + await second.initObjects(OBJECTS); expect(second.getSchemaSyncStats()).toEqual({ created: 0, existing: 2 }); } finally { @@ -104,8 +104,8 @@ describe('SqlDriver.getSchemaSyncStats', () => { it('a store that gains an object later is still not one we found', async () => { const driver = sqlite(); try { - await driver.initObjects([OBJECTS[0]] as any); - await driver.syncSchema('gamma', { name: 'gamma', fields: { n: { type: 'string' } } } as any); + await driver.initObjects([OBJECTS[0]]); + await driver.syncSchema('gamma', { name: 'gamma', fields: { n: { type: 'string' } } }); expect(driver.getSchemaSyncStats()).toEqual({ created: 2, existing: 0 }); } finally { diff --git a/packages/plugins/driver-sql/src/sql-driver-schema.test.ts b/packages/plugins/driver-sql/src/sql-driver-schema.test.ts index ec3df8fe58..a7f133c8a6 100644 --- a/packages/plugins/driver-sql/src/sql-driver-schema.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-schema.test.ts @@ -341,7 +341,7 @@ describe('SqlDriver Schema Sync (SQLite)', () => { }, ]; - await driver.initObjects(objects as any); + await driver.initObjects(objects); // PRAGMA proves a non-PK unique index was actually created. const list: any = await knexInstance.raw('PRAGMA index_list(idx_unique_obj)'); @@ -368,7 +368,7 @@ describe('SqlDriver Schema Sync (SQLite)', () => { }, ]; - await driver.initObjects(objects as any); + await driver.initObjects(objects); const list: any = await knexInstance.raw('PRAGMA index_list(idx_multi_obj)'); const declared = list.filter((i: any) => !String(i.name).startsWith('sqlite_autoindex')); @@ -397,7 +397,7 @@ describe('SqlDriver Schema Sync (SQLite)', () => { }, ]; - await driver.initObjects(objects as any); + await driver.initObjects(objects); // Two rows with no dedup_key (NULL) must both be insertable. await driver.create('idx_null_obj', {}); @@ -418,7 +418,7 @@ describe('SqlDriver Schema Sync (SQLite)', () => { }, ]; - await driver.initObjects(objects as any); + await driver.initObjects(objects); const list: any = await knexInstance.raw('PRAGMA index_list(idx_plain_obj)'); const declared = list.filter((i: any) => !String(i.name).startsWith('sqlite_autoindex')); @@ -435,9 +435,9 @@ describe('SqlDriver Schema Sync (SQLite)', () => { }, ]; - await driver.initObjects(objects as any); + await driver.initObjects(objects); // Second run must not throw "index already exists". - await driver.initObjects(objects as any); + await driver.initObjects(objects); const list: any = await knexInstance.raw('PRAGMA index_list(idx_idem_obj)'); const declared = list.filter((i: any) => !String(i.name).startsWith('sqlite_autoindex')); @@ -460,7 +460,7 @@ describe('SqlDriver Schema Sync (SQLite)', () => { ]; // Must not throw even though `total` has no physical column. - await driver.initObjects(objects as any); + await driver.initObjects(objects); const list: any = await knexInstance.raw('PRAGMA index_list(idx_virtual_obj)'); const declared = list.filter((i: any) => !String(i.name).startsWith('sqlite_autoindex')); diff --git a/packages/plugins/driver-sql/src/sql-driver-temporal-conformance.test.ts b/packages/plugins/driver-sql/src/sql-driver-temporal-conformance.test.ts index 292cafc8c0..c54c2e9239 100644 --- a/packages/plugins/driver-sql/src/sql-driver-temporal-conformance.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-temporal-conformance.test.ts @@ -92,14 +92,14 @@ describe('sql-driver — temporal conformance', () => { for (const c of TEMPORAL_CASES) { it(c.name, async () => { - const rows = await driver.find('conformance', { where: c.filter } as any); + const rows = await driver.find('conformance', { object: 'conformance', where: c.filter }); const got = (rows as any[]).map((r) => r.id).sort(); expect(got, c.note).toEqual([...c.expected].sort()); }); if (c.tokenFilter) { it(`${c.name} — via relative tokens`, async () => { - const rows = await driver.find('conformance', { where: resolveTokens(c.tokenFilter) } as any); + const rows = await driver.find('conformance', { object: 'conformance', where: resolveTokens(c.tokenFilter) }); const got = (rows as any[]).map((r) => r.id).sort(); expect(got, c.note).toEqual([...c.expected].sort()); }); @@ -143,7 +143,7 @@ describe('sql-driver — temporal conformance on un-backfilled legacy storage', // already swept above — a divergence here is a repair-path bug by construction. for (const c of TEMPORAL_CASES) { it(c.name, async () => { - const rows = await driver.find('conformance', { where: c.filter } as any); + const rows = await driver.find('conformance', { object: 'conformance', where: c.filter }); const got = (rows as any[]).map((r) => r.id).sort(); expect(got, c.note).toEqual([...c.expected].sort()); }); @@ -183,7 +183,7 @@ describe('sql-driver — Field.time conformance', () => { for (const c of TEMPORAL_TIME_CASES) { it(c.name, async () => { - const rows = await driver.find('time_conformance', { where: c.filter } as any); + const rows = await driver.find('time_conformance', { object: 'time_conformance', where: c.filter }); const got = (rows as any[]).map((r) => r.id).sort(); expect(got, c.note).toEqual([...c.expected].sort()); }); @@ -227,7 +227,7 @@ describe('sql-driver — Field.time conformance on un-backfilled legacy storage' for (const c of TEMPORAL_TIME_CASES) { it(c.name, async () => { - const rows = await driver.find('time_conformance', { where: c.filter } as any); + const rows = await driver.find('time_conformance', { object: 'time_conformance', where: c.filter }); const got = (rows as any[]).map((r) => r.id).sort(); expect(got, c.note).toEqual([...c.expected].sort()); }); diff --git a/packages/plugins/driver-sql/src/sql-driver-timestamp-format.test.ts b/packages/plugins/driver-sql/src/sql-driver-timestamp-format.test.ts index e3c7026b1c..8559f4de66 100644 --- a/packages/plugins/driver-sql/src/sql-driver-timestamp-format.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-timestamp-format.test.ts @@ -71,7 +71,7 @@ describe('SqlDriver canonical audit-timestamp format (SQLite)', () => { it('update({ preserveAudit }) KEEPS a caller-supplied updated_at instead of force-stamping now', async () => { await driver.create('thing', { id: 'h1', name: 'A' }, { bypassTenantAudit: true }); const historical = '2021-03-01T09:00:00.000Z'; - await driver.update('thing', 'h1', { name: 'B', updated_at: historical }, { bypassTenantAudit: true, preserveAudit: true } as any); + await driver.update('thing', 'h1', { name: 'B', updated_at: historical }, { bypassTenantAudit: true, preserveAudit: true }); const row = await raw('thing').where('id', 'h1').first(); expect(row.name).toBe('B'); expect(row.updated_at).toBe(historical); // original timeline preserved, not "now" @@ -80,7 +80,7 @@ describe('SqlDriver canonical audit-timestamp format (SQLite)', () => { it('update({ preserveAudit }) with NO supplied updated_at still stamps now (fills-only-empty)', async () => { await driver.create('thing', { id: 'h2', name: 'A' }, { bypassTenantAudit: true }); await new Promise((r) => setTimeout(r, 5)); - await driver.update('thing', 'h2', { name: 'B' }, { bypassTenantAudit: true, preserveAudit: true } as any); + await driver.update('thing', 'h2', { name: 'B' }, { bypassTenantAudit: true, preserveAudit: true }); const row = await raw('thing').where('id', 'h2').first(); expect(row.updated_at).toMatch(ISO_Z); }); diff --git a/packages/plugins/driver-sql/src/sql-driver-unique-tenancy.test.ts b/packages/plugins/driver-sql/src/sql-driver-unique-tenancy.test.ts index 1d423822fa..aa5e6f75b0 100644 --- a/packages/plugins/driver-sql/src/sql-driver-unique-tenancy.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-unique-tenancy.test.ts @@ -299,7 +299,7 @@ describe('SqlDriver unique × tenancy (#3696)', () => { expect(Object.values(uniques)).toContainEqual(['organization_id', 'code']); // Existing data survived, and the cross-tenant insert now works. - expect(await driver.count('product', {} as any)).toBe(2); + expect(await driver.count('product', { object: 'product' })).toBe(2); const b = await driver.create('product', { organization_id: 'org_b', code: 'PROD-00001' }); expect(b.code).toBe('PROD-00001'); }); @@ -401,7 +401,7 @@ describe('SqlDriver unique × tenancy (#3696)', () => { expect(Object.values(uniques)).not.toContainEqual(['code']); // Data preserved, and cross-tenant reuse still works post-rebuild. - expect(await driver.count('product', {} as any)).toBe(1); + expect(await driver.count('product', { object: 'product' })).toBe(1); const b = await driver.create('product', { organization_id: 'org_b', code: 'C1', note: 'n' }); expect(b.code).toBe('C1'); }); diff --git a/packages/plugins/driver-sql/src/sql-driver-unknown-column-recovery.test.ts b/packages/plugins/driver-sql/src/sql-driver-unknown-column-recovery.test.ts index 0c7fe247ab..33317e2a55 100644 --- a/packages/plugins/driver-sql/src/sql-driver-unknown-column-recovery.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-unknown-column-recovery.test.ts @@ -93,6 +93,6 @@ describe('SqlDriver find() recovers from unknown columns (objectstack#3821)', () }); it('propagates errors that are not about an unknown column', async () => { - await expect(driver.find('no_such_table', {} as any)).rejects.toThrow(); + await expect(driver.find('no_such_table', { object: 'no_such_table' })).rejects.toThrow(); }); }); diff --git a/packages/plugins/driver-sql/src/sql-driver.test.ts b/packages/plugins/driver-sql/src/sql-driver.test.ts index 183562a9c7..86c61ca640 100644 --- a/packages/plugins/driver-sql/src/sql-driver.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver.test.ts @@ -117,7 +117,7 @@ describe('SqlDriver (SQLite Integration)', () => { }); it('should count objects', async () => { - const count = await driver.count('users', { where: { age: 17 } } as any); + const count = await driver.count('users', { object: 'users', where: { age: 17 } }); expect(count).toBe(2); }); diff --git a/packages/plugins/driver-sqlite-wasm/src/sqlite-wasm-driver-advanced.test.ts b/packages/plugins/driver-sqlite-wasm/src/sqlite-wasm-driver-advanced.test.ts index ed0a379c20..0af0b73bb0 100644 --- a/packages/plugins/driver-sqlite-wasm/src/sqlite-wasm-driver-advanced.test.ts +++ b/packages/plugins/driver-sqlite-wasm/src/sqlite-wasm-driver-advanced.test.ts @@ -140,7 +140,7 @@ describe('SqliteWasmDriver Advanced Operations (SQLite)', () => { }); it('should update many records', async () => { - const result = await driver.updateMany('orders', { where: { status: 'pending' } } as any, { status: 'processing' }); + const result = await driver.updateMany('orders', { object: 'orders', where: { status: 'pending' } }, { status: 'processing' }); expect(result).toBeGreaterThan(0); @@ -149,7 +149,7 @@ describe('SqliteWasmDriver Advanced Operations (SQLite)', () => { }); it('should delete many records', async () => { - const result = await driver.deleteMany('orders', { where: { status: 'cancelled' } } as any); + const result = await driver.deleteMany('orders', { object: 'orders', where: { status: 'cancelled' } }); expect(result).toBe(1); @@ -158,10 +158,10 @@ describe('SqliteWasmDriver Advanced Operations (SQLite)', () => { }); it('should handle empty bulk update and delete', async () => { - const result = await driver.updateMany('orders', { where: { status: 'nonexistent' } } as any, { status: 'updated' }); + const result = await driver.updateMany('orders', { object: 'orders', where: { status: 'nonexistent' } }, { status: 'updated' }); expect(result).toBe(0); - const deleteResult = await driver.deleteMany('orders', { where: { id: 'nonexistent' } } as any); + const deleteResult = await driver.deleteMany('orders', { object: 'orders', where: { id: 'nonexistent' } }); expect(deleteResult).toBe(0); }); }); diff --git a/packages/plugins/driver-sqlite-wasm/src/sqlite-wasm-driver-autonumber.test.ts b/packages/plugins/driver-sqlite-wasm/src/sqlite-wasm-driver-autonumber.test.ts index 5e219ce1e6..570e468442 100644 --- a/packages/plugins/driver-sqlite-wasm/src/sqlite-wasm-driver-autonumber.test.ts +++ b/packages/plugins/driver-sqlite-wasm/src/sqlite-wasm-driver-autonumber.test.ts @@ -178,9 +178,9 @@ describe('SqliteWasmDriver auto_number sequence', () => { ]); // No organization_id on the row, but provided via DriverOptions. - const r1 = await driver.create('contract', {}, { tenantId: 'org_options' } as any); - const r2 = await driver.create('contract', {}, { tenantId: 'org_options' } as any); - const r3 = await driver.create('contract', {}, { tenantId: 'org_other' } as any); + const r1 = await driver.create('contract', {}, { tenantId: 'org_options' }); + const r2 = await driver.create('contract', {}, { tenantId: 'org_options' }); + const r3 = await driver.create('contract', {}, { tenantId: 'org_other' }); expect(r1.contract_number).toBe('CTR-0001'); expect(r2.contract_number).toBe('CTR-0002'); diff --git a/packages/plugins/driver-sqlite-wasm/src/sqlite-wasm-driver.test.ts b/packages/plugins/driver-sqlite-wasm/src/sqlite-wasm-driver.test.ts index 328e3d3b05..4ba1ac2d17 100644 --- a/packages/plugins/driver-sqlite-wasm/src/sqlite-wasm-driver.test.ts +++ b/packages/plugins/driver-sqlite-wasm/src/sqlite-wasm-driver.test.ts @@ -87,7 +87,7 @@ describe('SqliteWasmDriver (in-memory)', () => { }); it('should count objects', async () => { - const count = await driver.count('users', { where: { age: 17 } } as any); + const count = await driver.count('users', { object: 'users', where: { age: 17 } }); expect(count).toBe(2); }); }); diff --git a/packages/plugins/driver-sqlite-wasm/src/sqlite-wasm-temporal-conformance.test.ts b/packages/plugins/driver-sqlite-wasm/src/sqlite-wasm-temporal-conformance.test.ts index 148409b102..3c459e17c2 100644 --- a/packages/plugins/driver-sqlite-wasm/src/sqlite-wasm-temporal-conformance.test.ts +++ b/packages/plugins/driver-sqlite-wasm/src/sqlite-wasm-temporal-conformance.test.ts @@ -88,14 +88,14 @@ describe('driver-sqlite-wasm — temporal conformance', () => { for (const c of TEMPORAL_CASES) { it(c.name, async () => { - const rows = await driver.find('conformance', { where: c.filter } as any); + const rows = await driver.find('conformance', { object: 'conformance', where: c.filter }); const got = (rows as any[]).map((r) => r.id).sort(); expect(got, c.note).toEqual([...c.expected].sort()); }); if (c.tokenFilter) { it(`${c.name} — via relative tokens`, async () => { - const rows = await driver.find('conformance', { where: resolveTokens(c.tokenFilter) } as any); + const rows = await driver.find('conformance', { object: 'conformance', where: resolveTokens(c.tokenFilter) }); const got = (rows as any[]).map((r) => r.id).sort(); expect(got, c.note).toEqual([...c.expected].sort()); }); @@ -132,7 +132,7 @@ describe('driver-sqlite-wasm — Field.time conformance', () => { for (const c of TEMPORAL_TIME_CASES) { it(c.name, async () => { - const rows = await driver.find('time_conformance', { where: c.filter } as any); + const rows = await driver.find('time_conformance', { object: 'time_conformance', where: c.filter }); const got = (rows as any[]).map((r) => r.id).sort(); expect(got, c.note).toEqual([...c.expected].sort()); }); @@ -174,7 +174,7 @@ describe('driver-sqlite-wasm — temporal conformance on un-backfilled legacy st // already swept above — a divergence here is a repair-path bug by construction. for (const c of TEMPORAL_CASES) { it(c.name, async () => { - const rows = await driver.find('conformance', { where: c.filter } as any); + const rows = await driver.find('conformance', { object: 'conformance', where: c.filter }); const got = (rows as any[]).map((r) => r.id).sort(); expect(got, c.note).toEqual([...c.expected].sort()); }); @@ -209,7 +209,7 @@ describe('driver-sqlite-wasm — Field.time conformance on un-backfilled legacy for (const c of TEMPORAL_TIME_CASES) { it(c.name, async () => { - const rows = await driver.find('time_conformance', { where: c.filter } as any); + const rows = await driver.find('time_conformance', { object: 'time_conformance', where: c.filter }); const got = (rows as any[]).map((r) => r.id).sort(); expect(got, c.note).toEqual([...c.expected].sort()); }); diff --git a/scripts/check-type-check-coverage.mjs b/scripts/check-type-check-coverage.mjs index d5e00ab200..4c1489971e 100644 --- a/scripts/check-type-check-coverage.mjs +++ b/scripts/check-type-check-coverage.mjs @@ -25,6 +25,16 @@ // not declare; 19 for an analytics `timezone` default. The tests were right // and the types were wrong. A tsc count is a place to look, never a verdict. // +// The reverse also holds: a LOW count can be call sites opting out. Those same +// three packages carried 111 `as any` casts on driver-call arguments. Removing +// them left 66 fresh errors -- every one a real missing `object` the cast had +// hidden, including an `orderBy: [['id','asc']]` tuple the driver reads as +// `item.field` and therefore silently dropped, inside a helper whose whole job +// was reading rows in order. 43 of the casts were needed by nothing at all; +// exactly 2 were load-bearing (tests feeding a filter the AST gate refuses, on +// purpose). Onboarding a package is supposed to make its `typecheck` mean +// something, so the casts belong in the diff too. +// // node scripts/check-type-check-coverage.mjs // node scripts/check-type-check-coverage.mjs --self-test // From 4284d8338e7780a3d2e867383ba4d8aedd01bc5c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 31 Jul 2026 12:42:34 +0000 Subject: [PATCH 3/4] docs(spec): regenerate the DriverOptions reference for `bypassTenantAudit` (#4311) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `content/docs/references/data/driver.mdx` is generated from the Zod schema, so declaring a new `DriverOptions` key leaves it stale until `gen:docs` runs. `@objectstack/spec check:docs` caught it — the generated table is what a driver author reads to learn which options exist, and it had been complete for every key except the one this branch adds. `pnpm --filter @objectstack/spec gen:schema && gen:docs`; the diff is the one row. 252 generated files back in sync. Claude-Session: https://claude.ai/code/session_011m13wbaZziPveBdtQthrXd Co-authored-by: Claude --- content/docs/references/data/driver.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/content/docs/references/data/driver.mdx b/content/docs/references/data/driver.mdx index 74236e59cb..74e6d31fed 100644 --- a/content/docs/references/data/driver.mdx +++ b/content/docs/references/data/driver.mdx @@ -98,6 +98,7 @@ const result = DriverCapabilities.parse(data); | **tenantIds** | `string[]` | optional | Union tenant access set (group posture): native read scoping widens to organization_id IN (...); inserts still stamp from tenantId | | **timezone** | `string` | optional | Business reference timezone (IANA) for date-dependent generation, e.g. autonumber date tokens | | **preserveAudit** | `boolean` | optional | Historical import: keep a supplied updated_at instead of force-stamping now (from ExecutionContext.preserveAudit) | +| **bypassTenantAudit** | `boolean` | optional | Suppress the driver tenant-audit warning for a deliberately global write on a tenant-scoped object (diagnostics only — never changes what the write touches) | --- From 957fe3cc8809f00488320ddf279041c3757c1419 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 31 Jul 2026 12:54:43 +0000 Subject: [PATCH 4/4] docs(spec): record `AnalyticsQueryInput` in the api-surface snapshot (#4311) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `check:api-surface` reports every public export against a committed snapshot, so a new type — additive, 0 breaking — still has to be acknowledged rather than appearing unannounced in a consumer's build. One line. Also ran the other fifteen `@objectstack/spec` gates against this branch rather than waiting to meet them one CI round at a time: docs, generated, skill-refs, skill-docs, exported-any, authorable-surface, spec-changes, upgrade-guide, liveness, empty-state, variant-docs, strictness-ledger, react-blocks, react-conformance, skill-examples — plus `check:i18n` / `check:i18n-coverage` and `check:generated --reconcile-only`, which are the remaining steps of the CI job that failed. All green; only this snapshot needed regenerating. The pattern behind both this and the previous commit: `pnpm build` does not run the generators, so a schema edit type-checks and tests clean locally while two committed artifacts derived from it sit stale. Worth knowing before the next spec change — the generators are `gen:*` in `packages/spec`, and their gates are the `check:*` beside them. Claude-Session: https://claude.ai/code/session_011m13wbaZziPveBdtQthrXd Co-authored-by: Claude --- packages/spec/api-surface.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/spec/api-surface.json b/packages/spec/api-surface.json index cf70b02d62..fcf00e05a7 100644 --- a/packages/spec/api-surface.json +++ b/packages/spec/api-surface.json @@ -192,6 +192,7 @@ "AggregationStage (type)", "AggregationStageSchema (const)", "AnalyticsQuery (type)", + "AnalyticsQueryInput (type)", "AnalyticsQuerySchema (const)", "ApiMethod (type)", "ApiMethodsMode (type)",