diff --git a/.changeset/six-ties-leave.md b/.changeset/six-ties-leave.md new file mode 100644 index 0000000000..c6c48af3bd --- /dev/null +++ b/.changeset/six-ties-leave.md @@ -0,0 +1,6 @@ +--- +'@tanstack/expo-db-sqlite-persistence-e2e-app': patch +'@tanstack/expo-db-sqlite-persistence': patch +--- + +fixes a TS2322 type error when passing a SQLiteDatabase from expo-sqlite to createExpoSQLitePersistence diff --git a/packages/expo-db-sqlite-persistence/e2e/expo-runtime-app/App.tsx b/packages/expo-db-sqlite-persistence/e2e/expo-runtime-app/App.tsx index 616b733f71..3b61fd4aac 100644 --- a/packages/expo-db-sqlite-persistence/e2e/expo-runtime-app/App.tsx +++ b/packages/expo-db-sqlite-persistence/e2e/expo-runtime-app/App.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { ScrollView, Text, View } from 'react-native' import * as SQLite from 'expo-sqlite' import { createCollection } from '@tanstack/db' @@ -14,20 +14,12 @@ import type { } from '../runtime-protocol' type DatabaseHandle = Awaited> -type TransactionHandleLike = { - execAsync: (sql: string) => Promise - getAllAsync: ( - sql: string, - params?: ReadonlyArray | Record, - ) => Promise> - runAsync: ( - sql: string, - params?: ReadonlyArray | Record, - ) => Promise -} +type TransactionHandle = Parameters< + Parameters[0] +>[0] type ActiveTransaction = { - transaction: TransactionHandleLike + transaction: TransactionHandle complete: { resolve: () => void reject: (error?: unknown) => void @@ -63,12 +55,6 @@ async function postJson( } } -function normalizeSqliteParams( - params?: ReadonlyArray | Record, -): ReadonlyArray | Record | undefined { - return params === undefined ? undefined : params -} - async function closeDatabaseHandle(database: DatabaseHandle): Promise { const closableDatabase = database as DatabaseHandle & { closeAsync?: () => Promise @@ -129,14 +115,7 @@ export default function App() { ): Promise => { const database = await SQLite.openDatabaseAsync(databaseName) const collectionId = `expo-runtime-smoke-${Date.now().toString(36)}` - const persistence = createExpoSQLitePersistence< - { - id: string - title: string - score: number - }, - string - >({ + const persistence = createExpoSQLitePersistence({ database, }) const collection = createCollection( @@ -198,20 +177,18 @@ export default function App() { command.databaseId, command.databaseName, ) - const params = normalizeSqliteParams(command.params) - return params === undefined + return command.params === undefined ? database.getAllAsync(command.sql) - : database.getAllAsync(command.sql, params) + : database.getAllAsync(command.sql, command.params) } case `db:run`: { const database = await getDatabase( command.databaseId, command.databaseName, ) - const params = normalizeSqliteParams(command.params) - return params === undefined + return command.params === undefined ? database.runAsync(command.sql) - : database.runAsync(command.sql, params) + : database.runAsync(command.sql, command.params) } case `db:close`: { const database = databaseHandles.get(command.databaseId) @@ -270,20 +247,18 @@ export default function App() { if (!transaction) { throw new Error(`Unknown transaction id`) } - const params = normalizeSqliteParams(command.params) - return params === undefined + return command.params === undefined ? transaction.transaction.getAllAsync(command.sql) - : transaction.transaction.getAllAsync(command.sql, params) + : transaction.transaction.getAllAsync(command.sql, command.params) } case `tx:run`: { const transaction = activeTransactions.get(command.transactionId) if (!transaction) { throw new Error(`Unknown transaction id`) } - const params = normalizeSqliteParams(command.params) - return params === undefined + return command.params === undefined ? transaction.transaction.runAsync(command.sql) - : transaction.transaction.runAsync(command.sql, params) + : transaction.transaction.runAsync(command.sql, command.params) } case `tx:commit`: { const transaction = activeTransactions.get(command.transactionId) diff --git a/packages/expo-db-sqlite-persistence/e2e/runtime-protocol.ts b/packages/expo-db-sqlite-persistence/e2e/runtime-protocol.ts index 21f672e5c9..7377b09a2f 100644 --- a/packages/expo-db-sqlite-persistence/e2e/runtime-protocol.ts +++ b/packages/expo-db-sqlite-persistence/e2e/runtime-protocol.ts @@ -1,3 +1,8 @@ +export type ExpoRuntimeSQLiteBindValue = string | number | boolean | null +export type ExpoRuntimeSQLiteBindParams = + | Array + | Record + export type ExpoRuntimeCommand = | { id: string @@ -12,7 +17,7 @@ export type ExpoRuntimeCommand = databaseId: string databaseName: string sql: string - params?: ReadonlyArray | Record + params?: ExpoRuntimeSQLiteBindParams } | { id: string @@ -20,7 +25,7 @@ export type ExpoRuntimeCommand = databaseId: string databaseName: string sql: string - params?: ReadonlyArray | Record + params?: ExpoRuntimeSQLiteBindParams } | { id: string @@ -46,14 +51,14 @@ export type ExpoRuntimeCommand = type: `tx:getAll` transactionId: string sql: string - params?: ReadonlyArray | Record + params?: ExpoRuntimeSQLiteBindParams } | { id: string type: `tx:run` transactionId: string sql: string - params?: ReadonlyArray | Record + params?: ExpoRuntimeSQLiteBindParams } | { id: string diff --git a/packages/expo-db-sqlite-persistence/src/expo-sqlite-driver.ts b/packages/expo-db-sqlite-persistence/src/expo-sqlite-driver.ts index e85df20aac..c3bc25a1d0 100644 --- a/packages/expo-db-sqlite-persistence/src/expo-sqlite-driver.ts +++ b/packages/expo-db-sqlite-persistence/src/expo-sqlite-driver.ts @@ -1,33 +1,29 @@ import { InvalidPersistedCollectionConfigError } from '@tanstack/db-sqlite-persistence-core' import type { SQLiteDriver } from '@tanstack/db-sqlite-persistence-core' - -export type ExpoSQLiteBindParams = - | ReadonlyArray - | Record - -export type ExpoSQLiteRunResult = { - changes: number - lastInsertRowId: number -} +import type { + SQLiteBindParams, + SQLiteBindValue, + SQLiteRunResult, +} from 'expo-sqlite' export type ExpoSQLiteQueryable = { execAsync: (sql: string) => Promise - getAllAsync: ( - sql: string, - params?: ExpoSQLiteBindParams, - ) => Promise> - runAsync: ( - sql: string, - params?: ExpoSQLiteBindParams, - ) => Promise + getAllAsync: { + (sql: string, params: SQLiteBindParams): Promise> + (sql: string): Promise> + } + runAsync: { + (sql: string, params: SQLiteBindParams): Promise + (sql: string): Promise + } } export type ExpoSQLiteTransaction = ExpoSQLiteQueryable export type ExpoSQLiteDatabaseLike = ExpoSQLiteQueryable & { - withExclusiveTransactionAsync: ( - task: (transaction: ExpoSQLiteTransaction) => Promise, - ) => Promise + withExclusiveTransactionAsync: ( + task: (transaction: ExpoSQLiteTransaction) => Promise, + ) => Promise closeAsync?: () => Promise } @@ -144,10 +140,12 @@ export class ExpoSQLiteDriver implements SQLiteDriver { ): Promise { return this.enqueue(async () => { const database = await this.getDatabase() - return database.withExclusiveTransactionAsync(async (transaction) => { + let result: T | undefined + await database.withExclusiveTransactionAsync(async (transaction) => { const transactionDriver = this.createTransactionDriver(transaction) - return fn(transactionDriver) + result = await fn(transactionDriver) }) + return result as T }) } @@ -225,10 +223,24 @@ export class ExpoSQLiteDriver implements SQLiteDriver { } } -function normalizeParams( - params: ReadonlyArray, -): ExpoSQLiteBindParams | undefined { - return params.length > 0 ? [...params] : undefined +function normalizeBindValue(value: unknown): SQLiteBindValue { + if ( + value === null || + typeof value === `string` || + typeof value === `number` || + typeof value === `boolean` || + value instanceof Uint8Array + ) { + return value + } + + throw new TypeError( + `Expo SQLite bind parameters must be strings, numbers, booleans, null, or Uint8Array values`, + ) +} + +function normalizeParams(params: ReadonlyArray): SQLiteBindParams { + return params.map(normalizeBindValue) } export function createExpoSQLiteDriver( diff --git a/packages/expo-db-sqlite-persistence/tests/helpers/expo-emulator-database-factory.ts b/packages/expo-db-sqlite-persistence/tests/helpers/expo-emulator-database-factory.ts index de347ee076..2f68fbd98f 100644 --- a/packages/expo-db-sqlite-persistence/tests/helpers/expo-emulator-database-factory.ts +++ b/packages/expo-db-sqlite-persistence/tests/helpers/expo-emulator-database-factory.ts @@ -3,10 +3,8 @@ import type { ExpoSQLiteTestDatabase, ExpoSQLiteTestDatabaseFactory, } from './expo-sqlite-test-db' -import type { - ExpoSQLiteBindParams, - ExpoSQLiteTransaction, -} from '../../src/expo-sqlite-driver' +import type { SQLiteBindParams } from 'expo-sqlite' +import type { ExpoSQLiteTransaction } from '../../src/expo-sqlite-driver' function resolvePlatform(): `ios` | `android` { const platform = process.env.TANSTACK_DB_EXPO_RUNTIME_PLATFORM?.trim() @@ -46,13 +44,17 @@ export function createMobileSQLiteTestDatabaseFactory(): ExpoSQLiteTestDatabaseF execAsync: async (sql: string) => { await (await getDatabase()).execAsync(sql) }, - getAllAsync: async (sql: string, params?: ExpoSQLiteBindParams) => - (await getDatabase()).getAllAsync(sql, params), - runAsync: async (sql: string, params?: ExpoSQLiteBindParams) => - (await getDatabase()).runAsync(sql, params), - withExclusiveTransactionAsync: async ( - task: (transaction: ExpoSQLiteTransaction) => Promise, - ): Promise => + getAllAsync: async (sql: string, params?: SQLiteBindParams) => + params !== undefined + ? (await getDatabase()).getAllAsync(sql, params) + : (await getDatabase()).getAllAsync(sql), + runAsync: async (sql: string, params?: SQLiteBindParams) => + params !== undefined + ? (await getDatabase()).runAsync(sql, params) + : (await getDatabase()).runAsync(sql), + withExclusiveTransactionAsync: async ( + task: (transaction: ExpoSQLiteTransaction) => Promise, + ): Promise => (await getDatabase()).withExclusiveTransactionAsync(task), closeAsync: async () => { if (!databasePromise) { diff --git a/packages/expo-db-sqlite-persistence/tests/helpers/expo-emulator-runtime.ts b/packages/expo-db-sqlite-persistence/tests/helpers/expo-emulator-runtime.ts index 85cf208a6e..4da4f01c06 100644 --- a/packages/expo-db-sqlite-persistence/tests/helpers/expo-emulator-runtime.ts +++ b/packages/expo-db-sqlite-persistence/tests/helpers/expo-emulator-runtime.ts @@ -7,15 +7,20 @@ import { spawn } from 'node:child_process' import type { IncomingMessage, ServerResponse } from 'node:http' import type { ChildProcessWithoutNullStreams } from 'node:child_process' import type { - ExpoSQLiteBindParams, + SQLiteBindParams, + SQLiteBindValue, + SQLiteRunResult, +} from 'expo-sqlite' +import type { ExpoSQLiteDatabaseLike, - ExpoSQLiteRunResult, ExpoSQLiteTransaction, } from '../../src/expo-sqlite-driver' import type { ExpoRuntimeCommand, ExpoRuntimeCommandResult, ExpoRuntimeRegistration, + ExpoRuntimeSQLiteBindParams, + ExpoRuntimeSQLiteBindValue, ExpoRuntimeSmokeTestResult, } from '../../e2e/runtime-protocol' @@ -189,6 +194,36 @@ function parseCommandOverride(rawCommand: string): Array { .filter((part) => part.length > 0) } +function normalizeRuntimeSqliteValue( + value: SQLiteBindValue, +): ExpoRuntimeSQLiteBindValue { + if (value instanceof Uint8Array) { + throw new TypeError( + `Expo runtime bridge does not support binary SQLite params`, + ) + } + + return value +} + +function normalizeRuntimeSqliteParams( + params?: SQLiteBindParams, +): ExpoRuntimeSQLiteBindParams | undefined { + if (params === undefined) { + return undefined + } + + if (Array.isArray(params)) { + return params.map(normalizeRuntimeSqliteValue) + } + + const normalized: Record = {} + for (const [key, value] of Object.entries(params)) { + normalized[key] = normalizeRuntimeSqliteValue(value) + } + return normalized +} + class ExpoEmulatorRuntime { private static readonly instances = new Map< RuntimePlatform, @@ -239,29 +274,29 @@ class ExpoEmulatorRuntime { }, getAllAsync: async ( sql: string, - params?: ExpoSQLiteBindParams, + params?: SQLiteBindParams, ): Promise> => (await this.sendCommand({ type: `db:getAll`, databaseId, databaseName, sql, - params, + params: normalizeRuntimeSqliteParams(params), })) as ReadonlyArray, runAsync: async ( sql: string, - params?: ExpoSQLiteBindParams, - ): Promise => + params?: SQLiteBindParams, + ): Promise => (await this.sendCommand({ type: `db:run`, databaseId, databaseName, sql, - params, - })) as ExpoSQLiteRunResult, - withExclusiveTransactionAsync: async ( - task: (transaction: ExpoSQLiteTransaction) => Promise, - ): Promise => { + params: normalizeRuntimeSqliteParams(params), + })) as SQLiteRunResult, + withExclusiveTransactionAsync: async ( + task: (transaction: ExpoSQLiteTransaction) => Promise, + ): Promise => { const transactionId = randomUUID() await this.sendCommand({ type: `tx:start`, @@ -279,33 +314,32 @@ class ExpoEmulatorRuntime { }, getAllAsync: async ( sql: string, - params?: ExpoSQLiteBindParams, + params?: SQLiteBindParams, ): Promise> => (await this.sendCommand({ type: `tx:getAll`, transactionId, sql, - params, + params: normalizeRuntimeSqliteParams(params), })) as ReadonlyArray, runAsync: async ( sql: string, - params?: ExpoSQLiteBindParams, - ): Promise => + params?: SQLiteBindParams, + ): Promise => (await this.sendCommand({ type: `tx:run`, transactionId, sql, - params, - })) as ExpoSQLiteRunResult, + params: normalizeRuntimeSqliteParams(params), + })) as SQLiteRunResult, } try { - const result = await task(transaction) + await task(transaction) await this.sendCommand({ type: `tx:commit`, transactionId, }) - return result } catch (error) { await this.sendCommand({ type: `tx:rollback`, diff --git a/packages/expo-db-sqlite-persistence/tests/helpers/expo-sqlite-test-db.ts b/packages/expo-db-sqlite-persistence/tests/helpers/expo-sqlite-test-db.ts index 70cc5ede58..d4f27c4001 100644 --- a/packages/expo-db-sqlite-persistence/tests/helpers/expo-sqlite-test-db.ts +++ b/packages/expo-db-sqlite-persistence/tests/helpers/expo-sqlite-test-db.ts @@ -1,8 +1,11 @@ import BetterSqlite3 from 'better-sqlite3' import type { - ExpoSQLiteBindParams, + SQLiteBindParams, + SQLiteBindValue, + SQLiteRunResult, +} from 'expo-sqlite' +import type { ExpoSQLiteDatabaseLike, - ExpoSQLiteRunResult, ExpoSQLiteTransaction, } from '../../src/expo-sqlite-driver' @@ -21,9 +24,7 @@ declare global { | undefined } -function normalizeRunResult( - result: BetterSqlite3.RunResult, -): ExpoSQLiteRunResult { +function normalizeRunResult(result: BetterSqlite3.RunResult): SQLiteRunResult { return { changes: result.changes, lastInsertRowId: @@ -34,15 +35,15 @@ function normalizeRunResult( } function hasNamedParameters( - params: ExpoSQLiteBindParams | undefined, -): params is Record { + params: SQLiteBindParams | undefined, +): params is Record { return params !== undefined && !Array.isArray(params) } function executeAll( database: InstanceType, sql: string, - params?: ExpoSQLiteBindParams, + params?: SQLiteBindParams, ): ReadonlyArray { const statement = database.prepare(sql) @@ -60,8 +61,8 @@ function executeAll( function executeRun( database: InstanceType, sql: string, - params?: ExpoSQLiteBindParams, -): ExpoSQLiteRunResult { + params?: SQLiteBindParams, +): SQLiteRunResult { const statement = database.prepare(sql) const result = params === undefined @@ -83,13 +84,13 @@ function createTransactionHandle( }, getAllAsync: ( sql: string, - params?: ExpoSQLiteBindParams, + params?: SQLiteBindParams, ): Promise> => Promise.resolve(executeAll(database, sql, params)), runAsync: ( sql: string, - params?: ExpoSQLiteBindParams, - ): Promise => + params?: SQLiteBindParams, + ): Promise => Promise.resolve(executeRun(database, sql, params)), } } @@ -123,13 +124,13 @@ export function createExpoSQLiteTestDatabase(options: { }, getAllAsync: async ( sql: string, - params?: ExpoSQLiteBindParams, + params?: SQLiteBindParams, ): Promise> => enqueue(() => executeAll(nativeDatabase, sql, params)), runAsync: async ( sql: string, - params?: ExpoSQLiteBindParams, - ): Promise => + params?: SQLiteBindParams, + ): Promise => enqueue(() => executeRun(nativeDatabase, sql, params)), withExclusiveTransactionAsync: async ( task: (transaction: ExpoSQLiteTransaction) => Promise,