diff --git a/.changeset/some-wombats-rest.md b/.changeset/some-wombats-rest.md new file mode 100644 index 000000000..bccc489d4 --- /dev/null +++ b/.changeset/some-wombats-rest.md @@ -0,0 +1,39 @@ +--- +'@tanstack/powersync-db-collection': patch +--- + +Added support for tracking collection operation metadata in PowerSync CrudEntry operations. + +```typescript +// Schema config +const APP_SCHEMA = new Schema({ + documents: new Table( + { + name: column.text, + author: column.text, + created_at: column.text, + }, + { + // Metadata tracking must be enabled on the PowerSync table + trackMetadata: true, + }, + ), +}) + +// ... Other config + +// Collection operations which specify metadata +await collection.insert( + { + id, + name: `document`, + author: `Foo`, + }, + // The string version of this will be present in PowerSync `CrudEntry`s during uploads + { + metadata: { + extraInfo: 'Info', + }, + }, +) +``` diff --git a/docs/collections/powersync-collection.md b/docs/collections/powersync-collection.md index afc665836..36c2d8be2 100644 --- a/docs/collections/powersync-collection.md +++ b/docs/collections/powersync-collection.md @@ -401,6 +401,109 @@ task.due_date.getTime() // OK - TypeScript knows this is a Date Updates to the collection are applied optimistically to the local state first, then synchronized with PowerSync and the backend. If an error occurs during sync, the changes are automatically rolled back. +### Metadata Tracking + +Metadata tracking allows attaching custom metadata to collection operations (insert, update, delete). This metadata is persisted alongside the operation and available in PowerSync `CrudEntry` records during upload processing. This is useful for passing additional context about mutations to the backend, such as audit information, operation sources, or custom processing hints. + +#### Enabling Metadata Tracking + +Metadata tracking must be enabled on the PowerSync table: + +```typescript +const APP_SCHEMA = new Schema({ + documents: new Table( + { + name: column.text, + author: column.text, + }, + { + // Enable metadata tracking on this table + trackMetadata: true, + } + ), +}) +``` + +#### Using Metadata in Operations + +Once enabled, metadata can be passed to any collection operation: + +```typescript +const documents = createCollection( + powerSyncCollectionOptions({ + database: db, + table: APP_SCHEMA.props.documents, + }) +) + +// Insert with metadata +await documents.insert( + { + id: crypto.randomUUID(), + name: "Report Q4", + author: "Jane Smith", + }, + { + metadata: { + source: "web-app", + userId: "user-123", + timestamp: Date.now(), + }, + } +).isPersisted.promise + +// Update with metadata +await documents.update( + docId, + { metadata: { reason: "typo-fix", editor: "user-456" } }, + (doc) => { + doc.name = "Report Q4 (Updated)" + } +).isPersisted.promise + +// Delete with metadata +await documents.delete(docId, { + metadata: { deletedBy: "user-789", reason: "duplicate" }, +}).isPersisted.promise +``` + +#### Accessing Metadata During Upload + +The metadata is available in PowerSync `CrudEntry` records when processing uploads in the connector: + +```typescript +import { CrudEntry } from "@powersync/web" + +class Connector implements PowerSyncBackendConnector { + // ... + + async uploadData(database: AbstractPowerSyncDatabase) { + const batch = await database.getCrudBatch() + if (!batch) return + + for (const entry of batch.crud) { + console.log("Operation:", entry.op) // PUT, PATCH, DELETE + console.log("Table:", entry.table) + console.log("Data:", entry.opData) + console.log("Metadata:", entry.metadata) // Custom metadata (stringified) + + // Parse metadata if needed + if (entry.metadata) { + const meta = JSON.parse(entry.metadata) + console.log("Source:", meta.source) + console.log("User ID:", meta.userId) + } + + // Process the operation with the backend... + } + + await batch.complete() + } +} +``` + +**Note**: If metadata is provided to an operation but the table doesn't have `trackMetadata: true`, a warning will be logged and the metadata will be ignored. + ## Configuration Options The `powerSyncCollectionOptions` function accepts the following options: diff --git a/docs/reference/powersync-db-collection/classes/PowerSyncTransactor.md b/docs/reference/powersync-db-collection/classes/PowerSyncTransactor.md index b13c77ec4..e33ad5872 100644 --- a/docs/reference/powersync-db-collection/classes/PowerSyncTransactor.md +++ b/docs/reference/powersync-db-collection/classes/PowerSyncTransactor.md @@ -5,7 +5,7 @@ title: PowerSyncTransactor # Class: PowerSyncTransactor -Defined in: [PowerSyncTransactor.ts:51](https://github.com/TanStack/db/blob/main/packages/powersync-db-collection/src/PowerSyncTransactor.ts#L51) +Defined in: [PowerSyncTransactor.ts:54](https://github.com/TanStack/db/blob/main/packages/powersync-db-collection/src/PowerSyncTransactor.ts#L54) Applies mutations to the PowerSync database. This method is called automatically by the collection's insert, update, and delete operations. You typically don't need to call this directly unless you @@ -51,7 +51,7 @@ The transaction containing mutations to apply new PowerSyncTransactor(options): PowerSyncTransactor; ``` -Defined in: [PowerSyncTransactor.ts:55](https://github.com/TanStack/db/blob/main/packages/powersync-db-collection/src/PowerSyncTransactor.ts#L55) +Defined in: [PowerSyncTransactor.ts:58](https://github.com/TanStack/db/blob/main/packages/powersync-db-collection/src/PowerSyncTransactor.ts#L58) #### Parameters @@ -71,7 +71,7 @@ Defined in: [PowerSyncTransactor.ts:55](https://github.com/TanStack/db/blob/main database: AbstractPowerSyncDatabase; ``` -Defined in: [PowerSyncTransactor.ts:52](https://github.com/TanStack/db/blob/main/packages/powersync-db-collection/src/PowerSyncTransactor.ts#L52) +Defined in: [PowerSyncTransactor.ts:55](https://github.com/TanStack/db/blob/main/packages/powersync-db-collection/src/PowerSyncTransactor.ts#L55) *** @@ -81,7 +81,7 @@ Defined in: [PowerSyncTransactor.ts:52](https://github.com/TanStack/db/blob/main pendingOperationStore: PendingOperationStore; ``` -Defined in: [PowerSyncTransactor.ts:53](https://github.com/TanStack/db/blob/main/packages/powersync-db-collection/src/PowerSyncTransactor.ts#L53) +Defined in: [PowerSyncTransactor.ts:56](https://github.com/TanStack/db/blob/main/packages/powersync-db-collection/src/PowerSyncTransactor.ts#L56) ## Methods @@ -91,7 +91,7 @@ Defined in: [PowerSyncTransactor.ts:53](https://github.com/TanStack/db/blob/main applyTransaction(transaction): Promise; ``` -Defined in: [PowerSyncTransactor.ts:63](https://github.com/TanStack/db/blob/main/packages/powersync-db-collection/src/PowerSyncTransactor.ts#L63) +Defined in: [PowerSyncTransactor.ts:66](https://github.com/TanStack/db/blob/main/packages/powersync-db-collection/src/PowerSyncTransactor.ts#L66) Persists a Transaction to the PowerSync SQLite database. @@ -107,6 +107,26 @@ Persists a Transaction to the PowerSync SQLite database. *** +### getMutationCollectionMeta() + +```ts +protected getMutationCollectionMeta(mutation): PowerSyncCollectionMeta; +``` + +Defined in: [PowerSyncTransactor.ts:294](https://github.com/TanStack/db/blob/main/packages/powersync-db-collection/src/PowerSyncTransactor.ts#L294) + +#### Parameters + +##### mutation + +`PendingMutation`\<`any`\> + +#### Returns + +[`PowerSyncCollectionMeta`](../type-aliases/PowerSyncCollectionMeta.md)\<`any`\> + +*** + ### handleDelete() ```ts @@ -116,7 +136,7 @@ protected handleDelete( waitForCompletion): Promise; ``` -Defined in: [PowerSyncTransactor.ts:204](https://github.com/TanStack/db/blob/main/packages/powersync-db-collection/src/PowerSyncTransactor.ts#L204) +Defined in: [PowerSyncTransactor.ts:221](https://github.com/TanStack/db/blob/main/packages/powersync-db-collection/src/PowerSyncTransactor.ts#L221) #### Parameters @@ -147,7 +167,7 @@ protected handleInsert( waitForCompletion): Promise; ``` -Defined in: [PowerSyncTransactor.ts:149](https://github.com/TanStack/db/blob/main/packages/powersync-db-collection/src/PowerSyncTransactor.ts#L149) +Defined in: [PowerSyncTransactor.ts:152](https://github.com/TanStack/db/blob/main/packages/powersync-db-collection/src/PowerSyncTransactor.ts#L152) #### Parameters @@ -179,7 +199,7 @@ protected handleOperationWithCompletion( handler): Promise; ``` -Defined in: [PowerSyncTransactor.ts:232](https://github.com/TanStack/db/blob/main/packages/powersync-db-collection/src/PowerSyncTransactor.ts#L232) +Defined in: [PowerSyncTransactor.ts:263](https://github.com/TanStack/db/blob/main/packages/powersync-db-collection/src/PowerSyncTransactor.ts#L263) Helper function which wraps a persistence operation by: - Fetching the mutation's collection's SQLite table details @@ -219,7 +239,7 @@ protected handleUpdate( waitForCompletion): Promise; ``` -Defined in: [PowerSyncTransactor.ts:177](https://github.com/TanStack/db/blob/main/packages/powersync-db-collection/src/PowerSyncTransactor.ts#L177) +Defined in: [PowerSyncTransactor.ts:187](https://github.com/TanStack/db/blob/main/packages/powersync-db-collection/src/PowerSyncTransactor.ts#L187) #### Parameters @@ -238,3 +258,28 @@ Defined in: [PowerSyncTransactor.ts:177](https://github.com/TanStack/db/blob/mai #### Returns `Promise`\<`PendingOperation` \| `null`\> + +*** + +### processMutationMetadata() + +```ts +protected processMutationMetadata(mutation): string | null; +``` + +Defined in: [PowerSyncTransactor.ts:313](https://github.com/TanStack/db/blob/main/packages/powersync-db-collection/src/PowerSyncTransactor.ts#L313) + +Processes collection mutation metadata for persistence to the database. +We only support storing string metadata. + +#### Parameters + +##### mutation + +`PendingMutation`\<`any`\> + +#### Returns + +`string` \| `null` + +null if no metadata should be stored. diff --git a/docs/reference/powersync-db-collection/type-aliases/EnhancedPowerSyncCollectionConfig.md b/docs/reference/powersync-db-collection/type-aliases/EnhancedPowerSyncCollectionConfig.md index 79c292301..c005ab951 100644 --- a/docs/reference/powersync-db-collection/type-aliases/EnhancedPowerSyncCollectionConfig.md +++ b/docs/reference/powersync-db-collection/type-aliases/EnhancedPowerSyncCollectionConfig.md @@ -9,7 +9,7 @@ title: EnhancedPowerSyncCollectionConfig type EnhancedPowerSyncCollectionConfig = CollectionConfig & object; ``` -Defined in: [definitions.ts:254](https://github.com/TanStack/db/blob/main/packages/powersync-db-collection/src/definitions.ts#L254) +Defined in: [definitions.ts:259](https://github.com/TanStack/db/blob/main/packages/powersync-db-collection/src/definitions.ts#L259) A CollectionConfig which includes utilities for PowerSync. diff --git a/docs/reference/powersync-db-collection/type-aliases/PowerSyncCollectionMeta.md b/docs/reference/powersync-db-collection/type-aliases/PowerSyncCollectionMeta.md index 1fab936e6..b7dddfa05 100644 --- a/docs/reference/powersync-db-collection/type-aliases/PowerSyncCollectionMeta.md +++ b/docs/reference/powersync-db-collection/type-aliases/PowerSyncCollectionMeta.md @@ -21,6 +21,18 @@ Metadata for the PowerSync Collection. ## Properties +### metadataIsTracked + +```ts +metadataIsTracked: boolean; +``` + +Defined in: [definitions.ts:253](https://github.com/TanStack/db/blob/main/packages/powersync-db-collection/src/definitions.ts#L253) + +Whether the PowerSync table tracks metadata. + +*** + ### serializeValue() ```ts diff --git a/docs/reference/powersync-db-collection/type-aliases/PowerSyncCollectionUtils.md b/docs/reference/powersync-db-collection/type-aliases/PowerSyncCollectionUtils.md index 6657add36..116953154 100644 --- a/docs/reference/powersync-db-collection/type-aliases/PowerSyncCollectionUtils.md +++ b/docs/reference/powersync-db-collection/type-aliases/PowerSyncCollectionUtils.md @@ -9,7 +9,7 @@ title: PowerSyncCollectionUtils type PowerSyncCollectionUtils = object; ``` -Defined in: [definitions.ts:267](https://github.com/TanStack/db/blob/main/packages/powersync-db-collection/src/definitions.ts#L267) +Defined in: [definitions.ts:272](https://github.com/TanStack/db/blob/main/packages/powersync-db-collection/src/definitions.ts#L272) Collection-level utilities for PowerSync. @@ -27,7 +27,7 @@ Collection-level utilities for PowerSync. getMeta: () => PowerSyncCollectionMeta; ``` -Defined in: [definitions.ts:268](https://github.com/TanStack/db/blob/main/packages/powersync-db-collection/src/definitions.ts#L268) +Defined in: [definitions.ts:273](https://github.com/TanStack/db/blob/main/packages/powersync-db-collection/src/definitions.ts#L273) #### Returns diff --git a/docs/reference/powersync-db-collection/type-aliases/TransactorOptions.md b/docs/reference/powersync-db-collection/type-aliases/TransactorOptions.md index d38e7152f..7a9051b65 100644 --- a/docs/reference/powersync-db-collection/type-aliases/TransactorOptions.md +++ b/docs/reference/powersync-db-collection/type-aliases/TransactorOptions.md @@ -9,7 +9,7 @@ title: TransactorOptions type TransactorOptions = object; ``` -Defined in: [PowerSyncTransactor.ts:12](https://github.com/TanStack/db/blob/main/packages/powersync-db-collection/src/PowerSyncTransactor.ts#L12) +Defined in: [PowerSyncTransactor.ts:15](https://github.com/TanStack/db/blob/main/packages/powersync-db-collection/src/PowerSyncTransactor.ts#L15) ## Properties @@ -19,4 +19,4 @@ Defined in: [PowerSyncTransactor.ts:12](https://github.com/TanStack/db/blob/main database: AbstractPowerSyncDatabase; ``` -Defined in: [PowerSyncTransactor.ts:13](https://github.com/TanStack/db/blob/main/packages/powersync-db-collection/src/PowerSyncTransactor.ts#L13) +Defined in: [PowerSyncTransactor.ts:16](https://github.com/TanStack/db/blob/main/packages/powersync-db-collection/src/PowerSyncTransactor.ts#L16) diff --git a/docs/reference/powersync-db-collection/variables/DEFAULT_BATCH_SIZE.md b/docs/reference/powersync-db-collection/variables/DEFAULT_BATCH_SIZE.md index 16054c0b3..6c7b42275 100644 --- a/docs/reference/powersync-db-collection/variables/DEFAULT_BATCH_SIZE.md +++ b/docs/reference/powersync-db-collection/variables/DEFAULT_BATCH_SIZE.md @@ -9,6 +9,6 @@ title: DEFAULT_BATCH_SIZE const DEFAULT_BATCH_SIZE: 1000 = 1000; ``` -Defined in: [definitions.ts:274](https://github.com/TanStack/db/blob/main/packages/powersync-db-collection/src/definitions.ts#L274) +Defined in: [definitions.ts:279](https://github.com/TanStack/db/blob/main/packages/powersync-db-collection/src/definitions.ts#L279) Default value for [PowerSyncCollectionConfig#syncBatchSize](../type-aliases/BasePowerSyncCollectionConfig.md). diff --git a/packages/powersync-db-collection/src/PowerSyncTransactor.ts b/packages/powersync-db-collection/src/PowerSyncTransactor.ts index 655651b60..a45a4c751 100644 --- a/packages/powersync-db-collection/src/PowerSyncTransactor.ts +++ b/packages/powersync-db-collection/src/PowerSyncTransactor.ts @@ -1,11 +1,14 @@ import { sanitizeSQL } from '@powersync/common' import DebugModule from 'debug' -import { asPowerSyncRecord, mapOperationToPowerSync } from './helpers' import { PendingOperationStore } from './PendingOperationStore' +import { asPowerSyncRecord, mapOperationToPowerSync } from './helpers' import type { AbstractPowerSyncDatabase, LockContext } from '@powersync/common' import type { PendingMutation, Transaction } from '@tanstack/db' -import type { EnhancedPowerSyncCollectionConfig } from './definitions' import type { PendingOperation } from './PendingOperationStore' +import type { + EnhancedPowerSyncCollectionConfig, + PowerSyncCollectionMeta, +} from './definitions' const debug = DebugModule.debug(`ts/db:powersync`) @@ -160,6 +163,13 @@ export class PowerSyncTransactor { async (tableName, mutation, serializeValue) => { const values = serializeValue(mutation.modified) const keys = Object.keys(values).map((key) => sanitizeSQL`${key}`) + const queryParameters = Object.values(values) + + const metadataValue = this.processMutationMetadata(mutation) + if (metadataValue != null) { + keys.push(`_metadata`) + queryParameters.push(metadataValue) + } await context.execute( ` @@ -168,7 +178,7 @@ export class PowerSyncTransactor { VALUES (${keys.map((_) => `?`).join(`, `)}) `, - Object.values(values), + queryParameters, ) }, ) @@ -188,6 +198,13 @@ export class PowerSyncTransactor { async (tableName, mutation, serializeValue) => { const values = serializeValue(mutation.modified) const keys = Object.keys(values).map((key) => sanitizeSQL`${key}`) + const queryParameters = Object.values(values) + + const metadataValue = this.processMutationMetadata(mutation) + if (metadataValue != null) { + keys.push(`_metadata`) + queryParameters.push(metadataValue) + } await context.execute( ` @@ -195,7 +212,7 @@ export class PowerSyncTransactor { SET ${keys.map((key) => `${key} = ?`).join(`, `)} WHERE id = ? `, - [...Object.values(values), asPowerSyncRecord(mutation.modified).id], + [...queryParameters, asPowerSyncRecord(mutation.modified).id], ) }, ) @@ -213,12 +230,26 @@ export class PowerSyncTransactor { context, waitForCompletion, async (tableName, mutation) => { - await context.execute( - ` - DELETE FROM ${tableName} WHERE id = ? - `, - [asPowerSyncRecord(mutation.original).id], - ) + const metadataValue = this.processMutationMetadata(mutation) + if (metadataValue != null) { + /** + * Delete operations with metadata require a different approach to handle metadata. + * This will delete the record. + */ + await context.execute( + ` + UPDATE ${tableName} SET _deleted = TRUE, _metadata = ? WHERE id = ? + `, + [metadataValue, asPowerSyncRecord(mutation.original).id], + ) + } else { + await context.execute( + ` + DELETE FROM ${tableName} WHERE id = ? + `, + [asPowerSyncRecord(mutation.original).id], + ) + } }, ) } @@ -239,17 +270,8 @@ export class PowerSyncTransactor { serializeValue: (value: any) => Record, ) => Promise, ): Promise { - if ( - typeof (mutation.collection.config as any).utils?.getMeta != `function` - ) { - throw new Error(`Could not get tableName from mutation's collection config. - The provided mutation might not have originated from PowerSync.`) - } - - const { tableName, trackedTableName, serializeValue } = ( - mutation.collection - .config as unknown as EnhancedPowerSyncCollectionConfig - ).utils.getMeta() + const { tableName, trackedTableName, serializeValue } = + this.getMutationCollectionMeta(mutation) await handler(sanitizeSQL`${tableName}`, mutation, serializeValue) @@ -268,4 +290,46 @@ export class PowerSyncTransactor { timestamp: diffOperation.timestamp, } } + + protected getMutationCollectionMeta( + mutation: PendingMutation, + ): PowerSyncCollectionMeta { + if ( + typeof (mutation.collection.config as any).utils?.getMeta != `function` + ) { + throw new Error(`Collection is not a PowerSync collection.`) + } + return ( + mutation.collection + .config as unknown as EnhancedPowerSyncCollectionConfig + ).utils.getMeta() + } + + /** + * Processes collection mutation metadata for persistence to the database. + * We only support storing string metadata. + * @returns null if no metadata should be stored. + */ + protected processMutationMetadata( + mutation: PendingMutation, + ): string | null { + const { metadataIsTracked } = this.getMutationCollectionMeta(mutation) + if (!metadataIsTracked) { + // 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, + ) + } + return null + } else if (typeof mutation.metadata == `undefined`) { + return null + } else if (typeof mutation.metadata == `string`) { + return mutation.metadata + } else { + return JSON.stringify(mutation.metadata) + } + } } diff --git a/packages/powersync-db-collection/src/definitions.ts b/packages/powersync-db-collection/src/definitions.ts index 7da2e95d5..c65fa850e 100644 --- a/packages/powersync-db-collection/src/definitions.ts +++ b/packages/powersync-db-collection/src/definitions.ts @@ -246,6 +246,11 @@ export type PowerSyncCollectionMeta = { * Serializes a collection value to the SQLite type */ serializeValue: (value: any) => ExtractedTable + + /** + * Whether the PowerSync table tracks metadata. + */ + metadataIsTracked: boolean } /** diff --git a/packages/powersync-db-collection/src/powersync.ts b/packages/powersync-db-collection/src/powersync.ts index 4e40fe739..bc2e85bc2 100644 --- a/packages/powersync-db-collection/src/powersync.ts +++ b/packages/powersync-db-collection/src/powersync.ts @@ -242,7 +242,7 @@ export function powerSyncCollectionOptions< // The collection output type type OutputType = InferPowerSyncOutputType - const { viewName } = table + const { viewName, trackMetadata: metadataIsTracked } = table /** * Deserializes data from the incoming sync stream @@ -459,6 +459,7 @@ export function powerSyncCollectionOptions< getMeta: () => ({ tableName: viewName, trackedTableName, + metadataIsTracked, serializeValue: (value) => serializeForSQLite( value, diff --git a/packages/powersync-db-collection/tests/powersync.test.ts b/packages/powersync-db-collection/tests/powersync.test.ts index de946be38..4a4a5a544 100644 --- a/packages/powersync-db-collection/tests/powersync.test.ts +++ b/packages/powersync-db-collection/tests/powersync.test.ts @@ -23,11 +23,16 @@ const APP_SCHEMA = new Schema({ name: column.text, active: column.integer, // Will be mapped to Boolean }), - documents: new Table({ - name: column.text, - author: column.text, - created_at: column.text, // Will be mapped to Date - }), + documents: new Table( + { + name: column.text, + author: column.text, + created_at: column.text, // Will be mapped to Date + }, + { + trackMetadata: true, + }, + ), }) describe(`PowerSync Integration`, () => { @@ -322,6 +327,59 @@ describe(`PowerSync Integration`, () => { .every((crudEntry) => crudEntry.transactionId == lastTransactionId), ).true }) + + /** + * Metadata provided by the collection operation should be persisted to the database if supported by the SQLite table. + */ + it(`should persist collection operation metadata`, async () => { + const db = await createDatabase() + + const collection = createDocumentsCollection(db) + await collection.stateWhenReady() + + const metadata = { + text: `some text`, + number: 123, + boolean: true, + } + const id = randomUUID() + await collection.insert( + { + id, + name: `new`, + author: `somebody`, + }, + { + metadata, + }, + ).isPersisted.promise + + // Now do an update + await collection.update( + id, + { metadata: metadata }, + (d) => (d.name = `updatedNew`), + ).isPersisted.promise + + await collection.delete(id, { metadata }).isPersisted.promise + + // There should be a crud entries for this + const crudBatch = await db.getCrudBatch(100) + expect(crudBatch).toBeDefined() + const crudEntries = crudBatch!.crud + + // The metadata should be available in the CRUD entries for upload + const stringifiedMetadata = JSON.stringify(metadata) + expect(crudEntries.length).toBe(3) + expect(crudEntries[0]!.metadata).toEqual(stringifiedMetadata) + expect(crudEntries[1]!.metadata).toEqual(stringifiedMetadata) + expect(crudEntries[2]!.metadata).toEqual(stringifiedMetadata) + + // Verify the item is deleted from SQLite + const documents = await db.getAll(` + SELECT * FROM documents`) + expect(documents.length).toBe(0) + }) }) describe(`General use`, () => { @@ -337,6 +395,7 @@ describe(`PowerSync Integration`, () => { vi.spyOn(options.utils, `getMeta`).mockImplementation(() => ({ tableName: `fakeTable`, trackedTableName: `error`, + metadataIsTracked: true, serializeValue: () => ({}) as any, })) // Create two collections for the same table