From 4ce2ed060b4a7f2035a91d735bdff5bf24fadd91 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 21 Jul 2026 14:02:17 +0200 Subject: [PATCH 1/2] Upgrade PowerSync to version 2 --- .changeset/olive-guests-bathe.md | 5 ++ packages/powersync-db-collection/package.json | 6 +- .../src/PowerSyncTransactor.ts | 16 ++-- .../powersync-db-collection/src/helpers.ts | 35 ++++---- .../powersync-db-collection/src/powersync.ts | 85 +++++++++---------- .../src/serialization.ts | 21 ++--- .../tests/powersync.test.ts | 39 +++------ pnpm-lock.yaml | 47 ++++------ 8 files changed, 113 insertions(+), 141 deletions(-) create mode 100644 .changeset/olive-guests-bathe.md diff --git a/.changeset/olive-guests-bathe.md b/.changeset/olive-guests-bathe.md new file mode 100644 index 0000000000..3b5c5554be --- /dev/null +++ b/.changeset/olive-guests-bathe.md @@ -0,0 +1,5 @@ +--- +'@tanstack/powersync-db-collection': minor +--- + +Upgrade PowerSync to version 2. diff --git a/packages/powersync-db-collection/package.json b/packages/powersync-db-collection/package.json index 46ef3fd931..131c57c6d1 100644 --- a/packages/powersync-db-collection/package.json +++ b/packages/powersync-db-collection/package.json @@ -59,11 +59,11 @@ "p-defer": "^4.0.1" }, "peerDependencies": { - "@powersync/common": "^1.41.0" + "@powersync/common": "^2.0.0" }, "devDependencies": { - "@powersync/common": "1.49.0", - "@powersync/node": "0.18.1", + "@powersync/common": "^2.0.0", + "@powersync/node": "^0.20.0", "@types/debug": "^4.1.12", "@vitest/coverage-istanbul": "^3.2.4", "better-sqlite3": "^12.6.2" diff --git a/packages/powersync-db-collection/src/PowerSyncTransactor.ts b/packages/powersync-db-collection/src/PowerSyncTransactor.ts index 2542b6d545..c19869619d 100644 --- a/packages/powersync-db-collection/src/PowerSyncTransactor.ts +++ b/packages/powersync-db-collection/src/PowerSyncTransactor.ts @@ -1,8 +1,8 @@ -import { sanitizeSQL } from '@powersync/common' +import { LogLevels, sanitizeSQL } from '@powersync/common' import DebugModule from 'debug' import { PendingOperationStore } from './PendingOperationStore' import { asPowerSyncRecord, mapOperationToPowerSync } from './helpers' -import type { AbstractPowerSyncDatabase, LockContext } from '@powersync/common' +import type { CommonPowerSyncDatabase, LockContext } from '@powersync/common' import type { PendingMutation, Transaction } from '@tanstack/db' import type { PendingOperation } from './PendingOperationStore' import type { @@ -13,7 +13,7 @@ import type { const debug = DebugModule.debug(`ts/db:powersync`) export type TransactorOptions = { - database: AbstractPowerSyncDatabase + database: CommonPowerSyncDatabase } /** @@ -52,7 +52,7 @@ export type TransactorOptions = { * @returns A promise that resolves when the mutations have been persisted to PowerSync */ export class PowerSyncTransactor { - database: AbstractPowerSyncDatabase + database: CommonPowerSyncDatabase pendingOperationStore: PendingOperationStore constructor(options: TransactorOptions) { @@ -321,10 +321,10 @@ export class PowerSyncTransactor { // If it's not supported, we don't store metadata. if (typeof mutation.metadata != `undefined`) { // Log a warning if metadata is provided but not tracked. - this.database.logger.warn( - `Metadata provided for collection ${mutation.collection.id} but the PowerSync table does not track metadata. The PowerSync table should be configured with trackMetadata: true.`, - mutation.metadata, - ) + this.database.logger.log({ + level: LogLevels.info, + message: `Metadata provided for collection ${mutation.collection.id} but the PowerSync table does not track metadata. The PowerSync table should be configured with trackMetadata: true.`, + }) } return null } else if (typeof mutation.metadata == `undefined`) { diff --git a/packages/powersync-db-collection/src/helpers.ts b/packages/powersync-db-collection/src/helpers.ts index af33d259a1..acb40bf014 100644 --- a/packages/powersync-db-collection/src/helpers.ts +++ b/packages/powersync-db-collection/src/helpers.ts @@ -2,6 +2,7 @@ import { DiffTriggerOperation } from '@powersync/common' import type { BaseColumnType, ExtractColumnValueType, + RowType, Table, } from '@powersync/common' @@ -28,11 +29,11 @@ type OptionalIfUndefined = { /** * Provides the base column types for a table. This excludes the `id` column. */ -export type ExtractedTableColumns = { - [K in keyof TTable[`columnMap`]]: ExtractColumnValueType< - TTable[`columnMap`][K] - > -} +export type ExtractedTableColumns> = Omit< + RowType, + 'id' +> + /** * Utility type that extracts the typed structure of a table based on its column definitions. * Maps each column to its corresponding TypeScript type using ExtractColumnValueType. @@ -48,25 +49,25 @@ export type ExtractedTableColumns = { * // Results in: { id: string, name: string | null, age: number | null } * ``` */ -export type ExtractedTable = - ExtractedTableColumns & { - id: string - } +export type ExtractedTable = RowType -export type OptionalExtractedTable = OptionalIfUndefined<{ - [K in keyof TTable[`columnMap`]]: WithUndefinedIfNull< - ExtractColumnValueType - > -}> & { - id: string -} +export type OptionalExtractedTable = + TTable extends Table + ? OptionalIfUndefined<{ + [K in keyof Columns]: WithUndefinedIfNull< + ExtractColumnValueType + > + }> & { + id: string + } + : never /** * Maps the schema of TTable to a type which * requires the keys be equal, but the values can have any value type. */ export type AnyTableColumnType = { - [K in keyof TTable[`columnMap`]]: any + [K in keyof ExtractedTableColumns]: any } & { id: string } export function asPowerSyncRecord(record: any): PowerSyncRecord { diff --git a/packages/powersync-db-collection/src/powersync.ts b/packages/powersync-db-collection/src/powersync.ts index c11c1d2698..5e284d227a 100644 --- a/packages/powersync-db-collection/src/powersync.ts +++ b/packages/powersync-db-collection/src/powersync.ts @@ -1,4 +1,4 @@ -import { DiffTriggerOperation, sanitizeSQL } from '@powersync/common' +import { DiffTriggerOperation, LogLevels, sanitizeSQL } from '@powersync/common' import { or } from '@tanstack/db' import { compileSQLite } from './sqlite-compiler' import { PendingOperationStore } from './PendingOperationStore' @@ -16,8 +16,6 @@ import type { import type { AnyTableColumnType, ExtractedTable, - ExtractedTableColumns, - MapBaseColumnType, OptionalExtractedTable, } from './helpers' import type { @@ -25,7 +23,6 @@ import type { ConfigWithArbitraryCollectionTypes, ConfigWithSQLiteInputType, ConfigWithSQLiteTypes, - CustomSQLiteSerializer, EnhancedPowerSyncCollectionConfig, InferPowerSyncOutputType, PowerSyncCollectionConfig, @@ -262,12 +259,15 @@ export function powerSyncCollectionOptions< return validation.value } else if (`issues` in validation) { const issueMessage = `Failed to validate incoming data for ${viewName}. Issues: ${validation.issues.map((issue) => `${issue.path} - ${issue.message}`)}` - database.logger.error(issueMessage) + database.logger.log({ level: LogLevels.error, message: issueMessage }) onDeserializationError!(validation) throw new Error(issueMessage) } else { const unknownErrorMessage = `Unknown deserialization error for ${viewName}` - database.logger.error(unknownErrorMessage) + database.logger.log({ + level: LogLevels.error, + message: unknownErrorMessage, + }) onDeserializationError!({ issues: [{ message: unknownErrorMessage }] }) throw new Error(unknownErrorMessage) } @@ -358,9 +358,10 @@ export function powerSyncCollectionOptions< commit() } onReady() - database.logger.info( - `Sync is ready for ${viewName} into ${trackedTableName}`, - ) + database.logger.log({ + level: LogLevels.info, + message: `Sync is ready for ${viewName} into ${trackedTableName}`, + }) }, }, }) @@ -372,10 +373,11 @@ export function powerSyncCollectionOptions< await flushDiffRecordsWithContext(context) }) .catch((error) => { - database.logger.error( - `An error has been detected in the sync handler`, + database.logger.log({ + level: LogLevels.error, + message: `An error has been detected in the sync handler`, error, - ) + }) }) } @@ -422,18 +424,20 @@ export function powerSyncCollectionOptions< commit() pendingOperationStore.resolvePendingFor(pendingOperations) } catch (error) { - database.logger.error( - `An error has been detected in the sync handler`, + database.logger.log({ + level: LogLevels.error, + message: `An error has been detected in the sync handler`, error, - ) + }) } } // The sync function needs to be synchronous. async function start(afterOnChangeRegistered?: () => Promise) { - database.logger.info( - `Sync is starting for ${viewName} into ${trackedTableName}`, - ) + database.logger.log({ + level: LogLevels.info, + message: `Sync is starting for ${viewName} into ${trackedTableName}`, + }) database.onChangeWithCallback( { onChange: async () => { @@ -490,16 +494,18 @@ export function powerSyncCollectionOptions< onReady: () => markReady(), }) }).catch((error) => - database.logger.error( - `Could not start syncing process for ${viewName} into ${trackedTableName}`, + database.logger.log({ + level: LogLevels.error, + message: `Could not start syncing process for ${viewName} into ${trackedTableName}`, error, - ), + }), ) return () => { - database.logger.info( - `Sync has been stopped for ${viewName} into ${trackedTableName}`, - ) + database.logger.log({ + level: LogLevels.info, + message: `Sync has been stopped for ${viewName} into ${trackedTableName}`, + }) abortController.abort() onUnload?.() } @@ -511,10 +517,11 @@ export function powerSyncCollectionOptions< let onUnloadSubset: CleanupFn | void | null = null start().catch((error) => - database.logger.error( - `Could not start syncing process for ${viewName} into ${trackedTableName}`, + database.logger.log({ + level: LogLevels.error, + message: `Could not start syncing process for ${viewName} into ${trackedTableName}`, error, - ), + }), ) // Tracks all active WHERE expressions for on-demand sync filtering. @@ -651,9 +658,10 @@ export function powerSyncCollectionOptions< return { cleanup: () => { - database.logger.info( - `Sync has been stopped for ${viewName} into ${trackedTableName}`, - ) + database.logger.log({ + level: LogLevels.info, + message: `Sync has been stopped for ${viewName} into ${trackedTableName}`, + }) abortController.abort() }, loadSubset: (options: LoadSubsetOptions) => loadSubset(options), @@ -672,7 +680,9 @@ export function powerSyncCollectionOptions< OutputType, TSchema > = { - ...restConfig, + ...(restConfig as Partial< + EnhancedPowerSyncCollectionConfig + >), schema, getKey, // Syncing should start immediately since we need to monitor the changes for mutations @@ -697,18 +707,7 @@ export function powerSyncCollectionOptions< trackedTableName, metadataIsTracked, serializeValue: (value) => - serializeForSQLite( - value, - // This is required by the input generic - table as Table< - MapBaseColumnType> - >, - // Coerce serializer to the shape that corresponds to the Table constructed from OutputType - serializer as CustomSQLiteSerializer< - OutputType, - ExtractedTableColumns>> - >, - ), + serializeForSQLite(value, table, serializer), }), }, } diff --git a/packages/powersync-db-collection/src/serialization.ts b/packages/powersync-db-collection/src/serialization.ts index f25815f80e..bee6431b26 100644 --- a/packages/powersync-db-collection/src/serialization.ts +++ b/packages/powersync-db-collection/src/serialization.ts @@ -2,9 +2,9 @@ import { ColumnType } from '@powersync/common' import type { Table } from '@powersync/common' import type { CustomSQLiteSerializer } from './definitions' import type { + AnyTableColumnType, ExtractedTable, ExtractedTableColumns, - MapBaseColumnType, } from './helpers' /** @@ -37,17 +37,14 @@ import type { * - Throws if a key in `value` does not exist in the schema. * - Throws if a value cannot be converted to the required SQLite type. */ -export function serializeForSQLite< - TOutput extends Record, - // The keys should match - TTable extends Table> = Table< - MapBaseColumnType - >, ->( - value: TOutput, +export function serializeForSQLite( + value: AnyTableColumnType, tableSchema: TTable, customSerializer: Partial< - CustomSQLiteSerializer> + CustomSQLiteSerializer< + AnyTableColumnType, + ExtractedTableColumns + > > = {}, ): ExtractedTable { return Object.fromEntries( @@ -68,7 +65,7 @@ export function serializeForSQLite< const customTransform = customSerializer[key] if (customTransform) { - return [key, customTransform(value as TOutput[string])] + return [key, customTransform(value)] } // Map to the output @@ -98,5 +95,5 @@ export function serializeForSQLite< } } }), - ) + ) as ExtractedTable } diff --git a/packages/powersync-db-collection/tests/powersync.test.ts b/packages/powersync-db-collection/tests/powersync.test.ts index ebf93223ff..7c9efd2922 100644 --- a/packages/powersync-db-collection/tests/powersync.test.ts +++ b/packages/powersync-db-collection/tests/powersync.test.ts @@ -1,12 +1,6 @@ import { randomUUID } from 'node:crypto' import { tmpdir } from 'node:os' -import { - CrudEntry, - PowerSyncDatabase, - Schema, - Table, - column, -} from '@powersync/node' +import { PowerSyncDatabase, Schema, Table, column } from '@powersync/node' import { createCollection, createTransaction, @@ -17,7 +11,6 @@ import { describe, expect, it, onTestFinished, vi } from 'vitest' import { powerSyncCollectionOptions } from '../src' import { PowerSyncTransactor } from '../src/PowerSyncTransactor' import { TEST_DATABASE_IMPLEMENTATION } from './test-db-implementation' -import type { AbstractPowerSyncDatabase } from '@powersync/node' const APP_SCHEMA = new Schema({ users: new Table({ @@ -71,7 +64,7 @@ describePowerSync(`PowerSync Integration`, () => { return collection } - async function createTestData(db: AbstractPowerSyncDatabase) { + async function createTestData(db: PowerSyncDatabase) { await db.execute(` INSERT into documents (id, name) VALUES @@ -205,12 +198,8 @@ describePowerSync(`PowerSync Integration`, () => { await collection.delete(id).isPersisted.promise // There should be a crud entries for this - const _crudEntries = await db.getAll(` - SELECT * FROM ps_crud ORDER BY id`) - - const crudEntries = _crudEntries.map((r) => - CrudEntry.fromRow(r as Parameters[0]), - ) + const crudEntries = await db.getAll<{ op: 'string' }>(` + SELECT data ->> 'op' AS op FROM ps_crud ORDER BY id`) expect(crudEntries.length).toBe(6) // We can only group transactions for similar operations @@ -255,14 +244,10 @@ describePowerSync(`PowerSync Integration`, () => { // fetch the ps_crud items // There should be a crud entries for this - const _crudEntries = await db.getAll(` + const crudEntries = await db.getAll<{ tx_id: number }>(` SELECT * FROM ps_crud ORDER BY id`) - const crudEntries = _crudEntries.map((r) => - CrudEntry.fromRow(r as Parameters[0]), - ) - const lastTransactionId = - crudEntries[crudEntries.length - 1]?.transactionId + const lastTransactionId = crudEntries[crudEntries.length - 1]?.tx_id /** * The last items, created in the same transaction, should be in the same * PowerSync transaction. @@ -271,7 +256,7 @@ describePowerSync(`PowerSync Integration`, () => { crudEntries .reverse() .slice(0, 5) - .every((crudEntry) => crudEntry.transactionId == lastTransactionId), + .every((crudEntry) => crudEntry.tx_id == lastTransactionId), ).true }) @@ -319,14 +304,10 @@ describePowerSync(`PowerSync Integration`, () => { // fetch the ps_crud items // There should be a crud entries for this - const _crudEntries = await db.getAll(` + const crudEntries = await db.getAll<{ tx_id: number }>(` SELECT * FROM ps_crud ORDER BY id`) - const crudEntries = _crudEntries.map((r) => - CrudEntry.fromRow(r as Parameters[0]), - ) - const lastTransactionId = - crudEntries[crudEntries.length - 1]?.transactionId + const lastTransactionId = crudEntries[crudEntries.length - 1]?.tx_id /** * The last items, created in the same transaction, should be in the same * PowerSync transaction. @@ -335,7 +316,7 @@ describePowerSync(`PowerSync Integration`, () => { crudEntries .reverse() .slice(0, 10) - .every((crudEntry) => crudEntry.transactionId == lastTransactionId), + .every((crudEntry) => crudEntry.tx_id == lastTransactionId), ).true }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 27ec7100cd..d0a4ddfbb4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1338,11 +1338,11 @@ importers: version: 4.0.1 devDependencies: '@powersync/common': - specifier: 1.49.0 - version: 1.49.0 + specifier: ^2.0.0 + version: 2.0.0 '@powersync/node': - specifier: 0.18.1 - version: 0.18.1(@powersync/common@1.49.0)(better-sqlite3@12.8.0) + specifier: ^0.20.0 + version: 0.20.0(@powersync/common@2.0.0)(better-sqlite3@12.8.0) '@types/debug': specifier: ^4.1.12 version: 4.1.12 @@ -4831,18 +4831,21 @@ packages: '@poppinss/exception@1.2.3': resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} - '@powersync/common@1.49.0': - resolution: {integrity: sha512-g6uonubvtmtyx8hS/G5trg9LsBvzHY3tAKHiV7SIQV3Xyz9ONM6NNnjDMP2vcLZVmsOSi8x/QJZmy/ig1YtBMg==} + '@powersync/common@2.0.0': + resolution: {integrity: sha512-GuWmk0w0Y6I1HDwX4uGFzzw1e4E4bkvDSSCpVikdTEIk1xcW5NNnqTX8xfDheZdoQZunm/XushrR/LsIp1Yc/g==} - '@powersync/node@0.18.1': - resolution: {integrity: sha512-fcTICgs61CAEb39xiC7pedYsPgbjUInJ/47dr7RIdnEHpAgjWH8bW95/b70qK1fQUANy9lKBBF3PcmfswVgfCw==} + '@powersync/node@0.20.0': + resolution: {integrity: sha512-jAfCMPnXiS9EeoebkDkAoE8iM8t8pvza7RTVgKgSQ1ks32rzIE2lTIcflh9tzuiA8F6uDvAHg94gJEet1zbORg==} peerDependencies: - '@powersync/common': ^1.49.0 + '@powersync/common': ^2.0.0 better-sqlite3: 12.x peerDependenciesMeta: better-sqlite3: optional: true + '@powersync/shared-internals@1.0.1': + resolution: {integrity: sha512-0IIuZbDwLEIaSAzvp8z8+CrjKnjja6BPv9WMtF1eExQVTBVOx1TXK4E3iLrEdLceByRcSFlQ0TmPNkPRe5BDow==} + '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -6799,9 +6802,6 @@ packages: async-limiter@1.0.1: resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==} - async-mutex@0.5.0: - resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==} - asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -8173,9 +8173,6 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} - event-iterator@2.0.0: - resolution: {integrity: sha512-KGft0ldl31BZVV//jj+IAIGCxkvvUkkON+ScH6zfoX+l+omX6001ggyRSpI0Io2Hlro0ThXotswCtfzS8UkIiQ==} - event-reduce-js@5.2.7: resolution: {integrity: sha512-Vi6aIiAmakzx81JAwhw8L988aSX5a3ZqqVjHyZa9xFU6P4oT1IotoDreWtjNlS+fvEnASvyIQT565nmkOtns/Q==} engines: {node: '>=16'} @@ -16480,21 +16477,19 @@ snapshots: '@poppinss/exception@1.2.3': {} - '@powersync/common@1.49.0': - dependencies: - async-mutex: 0.5.0 - event-iterator: 2.0.0 + '@powersync/common@2.0.0': {} - '@powersync/node@0.18.1(@powersync/common@1.49.0)(better-sqlite3@12.8.0)': + '@powersync/node@0.20.0(@powersync/common@2.0.0)(better-sqlite3@12.8.0)': dependencies: - '@powersync/common': 1.49.0 - async-mutex: 0.5.0 - bson: 6.10.4 + '@powersync/common': 2.0.0 + '@powersync/shared-internals': 1.0.1 comlink: 4.4.2 undici: 7.24.4 optionalDependencies: better-sqlite3: 12.8.0 + '@powersync/shared-internals@1.0.1': {} + '@protobufjs/aspromise@1.1.2': {} '@protobufjs/base64@1.1.2': {} @@ -18957,10 +18952,6 @@ snapshots: async-limiter@1.0.1: {} - async-mutex@0.5.0: - dependencies: - tslib: 2.8.1 - asynckit@0.4.0: {} at-least-node@1.0.0: {} @@ -20577,8 +20568,6 @@ snapshots: etag@1.8.1: {} - event-iterator@2.0.0: {} - event-reduce-js@5.2.7: dependencies: array-push-at-sort-position: 4.0.1 From 485206cf9cbfb71578fe66ff33d896b79b97df63 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 21 Jul 2026 14:09:51 +0200 Subject: [PATCH 2/2] Update docs --- docs/collections/powersync-collection.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/collections/powersync-collection.md b/docs/collections/powersync-collection.md index c8ddbabbbe..090fdacacd 100644 --- a/docs/collections/powersync-collection.md +++ b/docs/collections/powersync-collection.md @@ -58,7 +58,7 @@ const db = new PowerSyncDatabase({ ```ts import { - AbstractPowerSyncDatabase, + CommonPowerSyncDatabase, PowerSyncBackendConnector, PowerSyncCredentials, } from "@powersync/web" @@ -69,11 +69,11 @@ class Connector implements PowerSyncBackendConnector { /** Upload local changes to the app backend. * - * Use {@link AbstractPowerSyncDatabase.getCrudBatch} to get a batch of changes to upload. + * Use {@link CommonPowerSyncDatabase.getCrudBatch} to get a batch of changes to upload. * * Any thrown errors will result in a retry after the configured wait period (default: 5 seconds). */ - uploadData: (database: AbstractPowerSyncDatabase) => Promise + uploadData: (database: CommonPowerSyncDatabase) => Promise } // Configure the client to connect to a PowerSync service and your backend