Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/olive-guests-bathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/powersync-db-collection': minor
---

Upgrade PowerSync to version 2.
6 changes: 3 additions & 3 deletions docs/collections/powersync-collection.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const db = new PowerSyncDatabase({

```ts
import {
AbstractPowerSyncDatabase,
CommonPowerSyncDatabase,
PowerSyncBackendConnector,
PowerSyncCredentials,
} from "@powersync/web"
Expand All @@ -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<void>
uploadData: (database: CommonPowerSyncDatabase) => Promise<void>
}

// Configure the client to connect to a PowerSync service and your backend
Expand Down
6 changes: 3 additions & 3 deletions packages/powersync-db-collection/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
16 changes: 8 additions & 8 deletions packages/powersync-db-collection/src/PowerSyncTransactor.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -13,7 +13,7 @@ import type {
const debug = DebugModule.debug(`ts/db:powersync`)

export type TransactorOptions = {
database: AbstractPowerSyncDatabase
database: CommonPowerSyncDatabase
}

/**
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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`) {
Expand Down
35 changes: 18 additions & 17 deletions packages/powersync-db-collection/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { DiffTriggerOperation } from '@powersync/common'
import type {
BaseColumnType,
ExtractColumnValueType,
RowType,
Table,
} from '@powersync/common'

Expand All @@ -28,11 +29,11 @@ type OptionalIfUndefined<T> = {
/**
* Provides the base column types for a table. This excludes the `id` column.
*/
export type ExtractedTableColumns<TTable extends Table> = {
[K in keyof TTable[`columnMap`]]: ExtractColumnValueType<
TTable[`columnMap`][K]
>
}
export type ExtractedTableColumns<TTable extends Table<any>> = Omit<
RowType<TTable>,
'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.
Expand All @@ -48,25 +49,25 @@ export type ExtractedTableColumns<TTable extends Table> = {
* // Results in: { id: string, name: string | null, age: number | null }
* ```
*/
export type ExtractedTable<TTable extends Table> =
ExtractedTableColumns<TTable> & {
id: string
}
export type ExtractedTable<TTable extends Table> = RowType<TTable>

export type OptionalExtractedTable<TTable extends Table> = OptionalIfUndefined<{
[K in keyof TTable[`columnMap`]]: WithUndefinedIfNull<
ExtractColumnValueType<TTable[`columnMap`][K]>
>
}> & {
id: string
}
export type OptionalExtractedTable<TTable extends Table> =
TTable extends Table<infer Columns>
? OptionalIfUndefined<{
[K in keyof Columns]: WithUndefinedIfNull<
ExtractColumnValueType<Columns[K]>
>
}> & {
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<TTable extends Table> = {
[K in keyof TTable[`columnMap`]]: any
[K in keyof ExtractedTableColumns<TTable>]: any
} & { id: string }
Comment on lines 65 to 71

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Avoid using any types; prefer unknown.

As per coding guidelines: "Avoid using any types; use unknown instead when the type is truly unknown". Using unknown for the column values improves type safety by requiring type guards before operations on the values.

💻 Proposed fix
 export type AnyTableColumnType<TTable extends Table> = {
-  [K in keyof ExtractedTableColumns<TTable>]: any
+  [K in keyof ExtractedTableColumns<TTable>]: unknown
 } & { id: string }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* 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<TTable extends Table> = {
[K in keyof TTable[`columnMap`]]: any
[K in keyof ExtractedTableColumns<TTable>]: any
} & { id: string }
/**
* 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<TTable extends Table> = {
[K in keyof ExtractedTableColumns<TTable>]: unknown
} & { id: string }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/powersync-db-collection/src/helpers.ts` around lines 65 - 71, The
AnyTableColumnType type uses any for column values; replace it with unknown in
the mapped type while preserving the existing keys and id: string requirement.

Source: Coding guidelines


export function asPowerSyncRecord(record: any): PowerSyncRecord {
Expand Down
85 changes: 42 additions & 43 deletions packages/powersync-db-collection/src/powersync.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -16,16 +16,13 @@ import type {
import type {
AnyTableColumnType,
ExtractedTable,
ExtractedTableColumns,
MapBaseColumnType,
OptionalExtractedTable,
} from './helpers'
import type {
BasePowerSyncCollectionConfig,
ConfigWithArbitraryCollectionTypes,
ConfigWithSQLiteInputType,
ConfigWithSQLiteTypes,
CustomSQLiteSerializer,
EnhancedPowerSyncCollectionConfig,
InferPowerSyncOutputType,
PowerSyncCollectionConfig,
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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}`,
})
},
},
})
Expand All @@ -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,
)
})
})
}

Expand Down Expand Up @@ -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<void>) {
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 () => {
Expand Down Expand Up @@ -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?.()
}
Expand All @@ -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.
Expand Down Expand Up @@ -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),
Expand All @@ -672,7 +680,9 @@ export function powerSyncCollectionOptions<
OutputType,
TSchema
> = {
...restConfig,
...(restConfig as Partial<
EnhancedPowerSyncCollectionConfig<TTable, OutputType, TSchema>
>),
schema,
getKey,
// Syncing should start immediately since we need to monitor the changes for mutations
Expand All @@ -697,18 +707,7 @@ export function powerSyncCollectionOptions<
trackedTableName,
metadataIsTracked,
serializeValue: (value) =>
serializeForSQLite(
value,
// This is required by the input generic
table as Table<
MapBaseColumnType<InferPowerSyncOutputType<TTable, TSchema>>
>,
// Coerce serializer to the shape that corresponds to the Table constructed from OutputType
serializer as CustomSQLiteSerializer<
OutputType,
ExtractedTableColumns<Table<MapBaseColumnType<OutputType>>>
>,
),
serializeForSQLite<TTable>(value, table, serializer),
}),
},
}
Expand Down
21 changes: 9 additions & 12 deletions packages/powersync-db-collection/src/serialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

/**
Expand Down Expand Up @@ -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<string, unknown>,
// The keys should match
TTable extends Table<MapBaseColumnType<TOutput>> = Table<
MapBaseColumnType<TOutput>
>,
>(
value: TOutput,
export function serializeForSQLite<TTable extends Table>(
value: AnyTableColumnType<TTable>,
tableSchema: TTable,
customSerializer: Partial<
CustomSQLiteSerializer<TOutput, ExtractedTableColumns<TTable>>
CustomSQLiteSerializer<
AnyTableColumnType<TTable>,
ExtractedTableColumns<TTable>
>
> = {},
): ExtractedTable<TTable> {
return Object.fromEntries(
Expand All @@ -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
Expand Down Expand Up @@ -98,5 +95,5 @@ export function serializeForSQLite<
}
}
}),
)
) as ExtractedTable<TTable>
}
Loading