@tanstack/powersync-db-collection 0.1.35 → 0.1.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/PowerSyncTransactor.cjs +7 -4
- package/dist/cjs/PowerSyncTransactor.cjs.map +1 -1
- package/dist/cjs/serialization.cjs +1 -0
- package/dist/cjs/serialization.cjs.map +1 -1
- package/dist/esm/PowerSyncTransactor.js +7 -4
- package/dist/esm/PowerSyncTransactor.js.map +1 -1
- package/dist/esm/serialization.js +1 -0
- package/dist/esm/serialization.js.map +1 -1
- package/package.json +2 -2
- package/src/PowerSyncTransactor.ts +7 -4
- package/src/serialization.ts +1 -0
|
@@ -76,6 +76,7 @@ class PowerSyncTransactor {
|
|
|
76
76
|
mutation,
|
|
77
77
|
context,
|
|
78
78
|
waitForCompletion,
|
|
79
|
+
// eslint-disable-next-line no-shadow
|
|
79
80
|
async (tableName, mutation2, serializeValue) => {
|
|
80
81
|
const values = serializeValue(mutation2.modified);
|
|
81
82
|
const keys = Object.keys(values).map((key) => common.sanitizeSQL`${key}`);
|
|
@@ -87,9 +88,9 @@ class PowerSyncTransactor {
|
|
|
87
88
|
}
|
|
88
89
|
await context.execute(
|
|
89
90
|
`
|
|
90
|
-
INSERT into ${tableName}
|
|
91
|
-
(${keys.join(`, `)})
|
|
92
|
-
VALUES
|
|
91
|
+
INSERT into ${tableName}
|
|
92
|
+
(${keys.join(`, `)})
|
|
93
|
+
VALUES
|
|
93
94
|
(${keys.map((_) => `?`).join(`, `)})
|
|
94
95
|
`,
|
|
95
96
|
queryParameters
|
|
@@ -103,6 +104,7 @@ class PowerSyncTransactor {
|
|
|
103
104
|
mutation,
|
|
104
105
|
context,
|
|
105
106
|
waitForCompletion,
|
|
107
|
+
// eslint-disable-next-line no-shadow
|
|
106
108
|
async (tableName, mutation2, serializeValue) => {
|
|
107
109
|
const values = serializeValue(mutation2.modified);
|
|
108
110
|
const keys = Object.keys(values).map((key) => common.sanitizeSQL`${key}`);
|
|
@@ -114,7 +116,7 @@ class PowerSyncTransactor {
|
|
|
114
116
|
}
|
|
115
117
|
await context.execute(
|
|
116
118
|
`
|
|
117
|
-
UPDATE ${tableName}
|
|
119
|
+
UPDATE ${tableName}
|
|
118
120
|
SET ${keys.map((key) => `${key} = ?`).join(`, `)}
|
|
119
121
|
WHERE id = ?
|
|
120
122
|
`,
|
|
@@ -129,6 +131,7 @@ class PowerSyncTransactor {
|
|
|
129
131
|
mutation,
|
|
130
132
|
context,
|
|
131
133
|
waitForCompletion,
|
|
134
|
+
// eslint-disable-next-line no-shadow
|
|
132
135
|
async (tableName, mutation2) => {
|
|
133
136
|
const metadataValue = this.processMutationMetadata(mutation2);
|
|
134
137
|
if (metadataValue != null) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PowerSyncTransactor.cjs","sources":["../../src/PowerSyncTransactor.ts"],"sourcesContent":["import { sanitizeSQL } from '@powersync/common'\nimport DebugModule from 'debug'\nimport { PendingOperationStore } from './PendingOperationStore'\nimport { asPowerSyncRecord, mapOperationToPowerSync } from './helpers'\nimport type { AbstractPowerSyncDatabase, LockContext } from '@powersync/common'\nimport type { PendingMutation, Transaction } from '@tanstack/db'\nimport type { PendingOperation } from './PendingOperationStore'\nimport type {\n EnhancedPowerSyncCollectionConfig,\n PowerSyncCollectionMeta,\n} from './definitions'\n\nconst debug = DebugModule.debug(`ts/db:powersync`)\n\nexport type TransactorOptions = {\n database: AbstractPowerSyncDatabase\n}\n\n/**\n * Applies mutations to the PowerSync database. This method is called automatically by the collection's\n * insert, update, and delete operations. You typically don't need to call this directly unless you\n * have special transaction requirements.\n *\n * @example\n * ```typescript\n * // Create a collection\n * const collection = createCollection(\n * powerSyncCollectionOptions<Document>({\n * database: db,\n * table: APP_SCHEMA.props.documents,\n * })\n * )\n *\n * const addTx = createTransaction({\n * autoCommit: false,\n * mutationFn: async ({ transaction }) => {\n * await new PowerSyncTransactor({ database: db }).applyTransaction(transaction)\n * },\n * })\n *\n * addTx.mutate(() => {\n * for (let i = 0; i < 5; i++) {\n * collection.insert({ id: randomUUID(), name: `tx-${i}` })\n * }\n * })\n *\n * await addTx.commit()\n * await addTx.isPersisted.promise\n * ```\n *\n * @param transaction - The transaction containing mutations to apply\n * @returns A promise that resolves when the mutations have been persisted to PowerSync\n */\nexport class PowerSyncTransactor {\n database: AbstractPowerSyncDatabase\n pendingOperationStore: PendingOperationStore\n\n constructor(options: TransactorOptions) {\n this.database = options.database\n this.pendingOperationStore = PendingOperationStore.GLOBAL\n }\n\n /**\n * Persists a {@link Transaction} to the PowerSync SQLite database.\n */\n async applyTransaction(transaction: Transaction<any>) {\n const { mutations } = transaction\n\n if (mutations.length == 0) {\n return\n }\n /**\n * The transaction might contain operations for different collections.\n * We can do some optimizations for single-collection transactions.\n */\n const mutationsCollectionIds = mutations.map(\n (mutation) => mutation.collection.id,\n )\n const collectionIds = Array.from(new Set(mutationsCollectionIds))\n const lastCollectionMutationIndexes = new Map<string, number>()\n const allCollections = collectionIds\n .map((id) => mutations.find((mutation) => mutation.collection.id == id)!)\n .map((mutation) => mutation.collection)\n for (const collectionId of collectionIds) {\n lastCollectionMutationIndexes.set(\n collectionId,\n mutationsCollectionIds.lastIndexOf(collectionId),\n )\n }\n\n // Check all the observers are ready before taking a lock\n await Promise.all(\n allCollections.map(async (collection) => {\n if (collection.isReady()) {\n return\n }\n await new Promise<void>((resolve) => collection.onFirstReady(resolve))\n }),\n )\n\n // Persist to PowerSync\n const { whenComplete } = await this.database.writeTransaction(\n async (tx) => {\n const pendingOperations: Array<PendingOperation | null> = []\n\n for (const [index, mutation] of mutations.entries()) {\n /**\n * Each collection processes events independently. We need to make sure the\n * last operation for each collection has been observed.\n */\n const shouldWait =\n index == lastCollectionMutationIndexes.get(mutation.collection.id)\n switch (mutation.type) {\n case `insert`:\n pendingOperations.push(\n await this.handleInsert(mutation, tx, shouldWait),\n )\n break\n case `update`:\n pendingOperations.push(\n await this.handleUpdate(mutation, tx, shouldWait),\n )\n break\n case `delete`:\n pendingOperations.push(\n await this.handleDelete(mutation, tx, shouldWait),\n )\n break\n }\n }\n\n /**\n * Return a promise from the writeTransaction, without awaiting it.\n * This promise will resolve once the entire transaction has been\n * observed via the diff triggers.\n * We return without awaiting in order to free the write lock.\n */\n return {\n whenComplete: Promise.all(\n pendingOperations\n .filter((op) => !!op)\n .map((op) => this.pendingOperationStore.waitFor(op)),\n ),\n }\n },\n )\n\n // Wait for the change to be observed via the diff trigger\n await whenComplete\n }\n\n protected async handleInsert(\n mutation: PendingMutation<any>,\n context: LockContext,\n waitForCompletion: boolean = false,\n ): Promise<PendingOperation | null> {\n debug(`insert`, mutation)\n\n return this.handleOperationWithCompletion(\n mutation,\n context,\n waitForCompletion,\n async (tableName, mutation, serializeValue) => {\n const values = serializeValue(mutation.modified)\n const keys = Object.keys(values).map((key) => sanitizeSQL`${key}`)\n const queryParameters = Object.values(values)\n\n const metadataValue = this.processMutationMetadata(mutation)\n if (metadataValue != null) {\n keys.push(`_metadata`)\n queryParameters.push(metadataValue)\n }\n\n await context.execute(\n `\n INSERT into ${tableName} \n (${keys.join(`, `)}) \n VALUES \n (${keys.map((_) => `?`).join(`, `)})\n `,\n queryParameters,\n )\n },\n )\n }\n\n protected async handleUpdate(\n mutation: PendingMutation<any>,\n context: LockContext,\n waitForCompletion: boolean = false,\n ): Promise<PendingOperation | null> {\n debug(`update`, mutation)\n\n return this.handleOperationWithCompletion(\n mutation,\n context,\n waitForCompletion,\n async (tableName, mutation, serializeValue) => {\n const values = serializeValue(mutation.modified)\n const keys = Object.keys(values).map((key) => sanitizeSQL`${key}`)\n const queryParameters = Object.values(values)\n\n const metadataValue = this.processMutationMetadata(mutation)\n if (metadataValue != null) {\n keys.push(`_metadata`)\n queryParameters.push(metadataValue)\n }\n\n await context.execute(\n `\n UPDATE ${tableName} \n SET ${keys.map((key) => `${key} = ?`).join(`, `)}\n WHERE id = ?\n `,\n [...queryParameters, asPowerSyncRecord(mutation.modified).id],\n )\n },\n )\n }\n\n protected async handleDelete(\n mutation: PendingMutation<any>,\n context: LockContext,\n waitForCompletion: boolean = false,\n ): Promise<PendingOperation | null> {\n debug(`update`, mutation)\n\n return this.handleOperationWithCompletion(\n mutation,\n context,\n waitForCompletion,\n async (tableName, mutation) => {\n const metadataValue = this.processMutationMetadata(mutation)\n if (metadataValue != null) {\n /**\n * Delete operations with metadata require a different approach to handle metadata.\n * This will delete the record.\n */\n await context.execute(\n `\n UPDATE ${tableName} SET _deleted = TRUE, _metadata = ? WHERE id = ?\n `,\n [metadataValue, asPowerSyncRecord(mutation.original).id],\n )\n } else {\n await context.execute(\n `\n DELETE FROM ${tableName} WHERE id = ?\n `,\n [asPowerSyncRecord(mutation.original).id],\n )\n }\n },\n )\n }\n\n /**\n * Helper function which wraps a persistence operation by:\n * - Fetching the mutation's collection's SQLite table details\n * - Executing the mutation\n * - Returning the last pending diff operation if required\n */\n protected async handleOperationWithCompletion(\n mutation: PendingMutation<any>,\n context: LockContext,\n waitForCompletion: boolean,\n handler: (\n tableName: string,\n mutation: PendingMutation<any>,\n serializeValue: (value: any) => Record<string, unknown>,\n ) => Promise<void>,\n ): Promise<PendingOperation | null> {\n const { tableName, trackedTableName, serializeValue } =\n this.getMutationCollectionMeta(mutation)\n\n await handler(sanitizeSQL`${tableName}`, mutation, serializeValue)\n\n if (!waitForCompletion) {\n return null\n }\n\n // Need to get the operation in order to wait for it\n const diffOperation = await context.get<{ id: string; timestamp: string }>(\n sanitizeSQL`SELECT id, timestamp FROM ${trackedTableName} ORDER BY timestamp DESC LIMIT 1`,\n )\n return {\n tableName,\n id: diffOperation.id,\n operation: mapOperationToPowerSync(mutation.type),\n timestamp: diffOperation.timestamp,\n }\n }\n\n protected getMutationCollectionMeta(\n mutation: PendingMutation<any>,\n ): PowerSyncCollectionMeta<any> {\n if (\n typeof (mutation.collection.config as any).utils?.getMeta != `function`\n ) {\n throw new Error(`Collection is not a PowerSync collection.`)\n }\n return (\n mutation.collection\n .config as unknown as EnhancedPowerSyncCollectionConfig<any>\n ).utils.getMeta()\n }\n\n /**\n * Processes collection mutation metadata for persistence to the database.\n * We only support storing string metadata.\n * @returns null if no metadata should be stored.\n */\n protected processMutationMetadata(\n mutation: PendingMutation<any>,\n ): string | null {\n const { metadataIsTracked } = this.getMutationCollectionMeta(mutation)\n if (!metadataIsTracked) {\n // If it's not supported, we don't store metadata.\n if (typeof mutation.metadata != `undefined`) {\n // Log a warning if metadata is provided but not tracked.\n this.database.logger.warn(\n `Metadata provided for collection ${mutation.collection.id} but the PowerSync table does not track metadata. The PowerSync table should be configured with trackMetadata: true.`,\n mutation.metadata,\n )\n }\n return null\n } else if (typeof mutation.metadata == `undefined`) {\n return null\n } else if (typeof mutation.metadata == `string`) {\n return mutation.metadata\n } else {\n return JSON.stringify(mutation.metadata)\n }\n }\n}\n"],"names":["PendingOperationStore","mutation","sanitizeSQL","asPowerSyncRecord","mapOperationToPowerSync"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PowerSyncTransactor.cjs","sources":["../../src/PowerSyncTransactor.ts"],"sourcesContent":["import { sanitizeSQL } from '@powersync/common'\nimport DebugModule from 'debug'\nimport { PendingOperationStore } from './PendingOperationStore'\nimport { asPowerSyncRecord, mapOperationToPowerSync } from './helpers'\nimport type { AbstractPowerSyncDatabase, LockContext } from '@powersync/common'\nimport type { PendingMutation, Transaction } from '@tanstack/db'\nimport type { PendingOperation } from './PendingOperationStore'\nimport type {\n EnhancedPowerSyncCollectionConfig,\n PowerSyncCollectionMeta,\n} from './definitions'\n\nconst debug = DebugModule.debug(`ts/db:powersync`)\n\nexport type TransactorOptions = {\n database: AbstractPowerSyncDatabase\n}\n\n/**\n * Applies mutations to the PowerSync database. This method is called automatically by the collection's\n * insert, update, and delete operations. You typically don't need to call this directly unless you\n * have special transaction requirements.\n *\n * @example\n * ```typescript\n * // Create a collection\n * const collection = createCollection(\n * powerSyncCollectionOptions<Document>({\n * database: db,\n * table: APP_SCHEMA.props.documents,\n * })\n * )\n *\n * const addTx = createTransaction({\n * autoCommit: false,\n * mutationFn: async ({ transaction }) => {\n * await new PowerSyncTransactor({ database: db }).applyTransaction(transaction)\n * },\n * })\n *\n * addTx.mutate(() => {\n * for (let i = 0; i < 5; i++) {\n * collection.insert({ id: randomUUID(), name: `tx-${i}` })\n * }\n * })\n *\n * await addTx.commit()\n * await addTx.isPersisted.promise\n * ```\n *\n * @param transaction - The transaction containing mutations to apply\n * @returns A promise that resolves when the mutations have been persisted to PowerSync\n */\nexport class PowerSyncTransactor {\n database: AbstractPowerSyncDatabase\n pendingOperationStore: PendingOperationStore\n\n constructor(options: TransactorOptions) {\n this.database = options.database\n this.pendingOperationStore = PendingOperationStore.GLOBAL\n }\n\n /**\n * Persists a {@link Transaction} to the PowerSync SQLite database.\n */\n async applyTransaction(transaction: Transaction<any>) {\n const { mutations } = transaction\n\n if (mutations.length == 0) {\n return\n }\n /**\n * The transaction might contain operations for different collections.\n * We can do some optimizations for single-collection transactions.\n */\n const mutationsCollectionIds = mutations.map(\n (mutation) => mutation.collection.id,\n )\n const collectionIds = Array.from(new Set(mutationsCollectionIds))\n const lastCollectionMutationIndexes = new Map<string, number>()\n const allCollections = collectionIds\n .map((id) => mutations.find((mutation) => mutation.collection.id == id)!)\n .map((mutation) => mutation.collection)\n for (const collectionId of collectionIds) {\n lastCollectionMutationIndexes.set(\n collectionId,\n mutationsCollectionIds.lastIndexOf(collectionId),\n )\n }\n\n // Check all the observers are ready before taking a lock\n await Promise.all(\n allCollections.map(async (collection) => {\n if (collection.isReady()) {\n return\n }\n await new Promise<void>((resolve) => collection.onFirstReady(resolve))\n }),\n )\n\n // Persist to PowerSync\n const { whenComplete } = await this.database.writeTransaction(\n async (tx) => {\n const pendingOperations: Array<PendingOperation | null> = []\n\n for (const [index, mutation] of mutations.entries()) {\n /**\n * Each collection processes events independently. We need to make sure the\n * last operation for each collection has been observed.\n */\n const shouldWait =\n index == lastCollectionMutationIndexes.get(mutation.collection.id)\n switch (mutation.type) {\n case `insert`:\n pendingOperations.push(\n await this.handleInsert(mutation, tx, shouldWait),\n )\n break\n case `update`:\n pendingOperations.push(\n await this.handleUpdate(mutation, tx, shouldWait),\n )\n break\n case `delete`:\n pendingOperations.push(\n await this.handleDelete(mutation, tx, shouldWait),\n )\n break\n }\n }\n\n /**\n * Return a promise from the writeTransaction, without awaiting it.\n * This promise will resolve once the entire transaction has been\n * observed via the diff triggers.\n * We return without awaiting in order to free the write lock.\n */\n return {\n whenComplete: Promise.all(\n pendingOperations\n .filter((op) => !!op)\n .map((op) => this.pendingOperationStore.waitFor(op)),\n ),\n }\n },\n )\n\n // Wait for the change to be observed via the diff trigger\n await whenComplete\n }\n\n protected async handleInsert(\n mutation: PendingMutation<any>,\n context: LockContext,\n waitForCompletion: boolean = false,\n ): Promise<PendingOperation | null> {\n debug(`insert`, mutation)\n\n return this.handleOperationWithCompletion(\n mutation,\n context,\n waitForCompletion,\n // eslint-disable-next-line no-shadow\n async (tableName, mutation, serializeValue) => {\n const values = serializeValue(mutation.modified)\n const keys = Object.keys(values).map((key) => sanitizeSQL`${key}`)\n const queryParameters = Object.values(values)\n\n const metadataValue = this.processMutationMetadata(mutation)\n if (metadataValue != null) {\n keys.push(`_metadata`)\n queryParameters.push(metadataValue)\n }\n\n await context.execute(\n `\n INSERT into ${tableName}\n (${keys.join(`, `)})\n VALUES\n (${keys.map((_) => `?`).join(`, `)})\n `,\n queryParameters,\n )\n },\n )\n }\n\n protected async handleUpdate(\n mutation: PendingMutation<any>,\n context: LockContext,\n waitForCompletion: boolean = false,\n ): Promise<PendingOperation | null> {\n debug(`update`, mutation)\n\n return this.handleOperationWithCompletion(\n mutation,\n context,\n waitForCompletion,\n // eslint-disable-next-line no-shadow\n async (tableName, mutation, serializeValue) => {\n const values = serializeValue(mutation.modified)\n const keys = Object.keys(values).map((key) => sanitizeSQL`${key}`)\n const queryParameters = Object.values(values)\n\n const metadataValue = this.processMutationMetadata(mutation)\n if (metadataValue != null) {\n keys.push(`_metadata`)\n queryParameters.push(metadataValue)\n }\n\n await context.execute(\n `\n UPDATE ${tableName}\n SET ${keys.map((key) => `${key} = ?`).join(`, `)}\n WHERE id = ?\n `,\n [...queryParameters, asPowerSyncRecord(mutation.modified).id],\n )\n },\n )\n }\n\n protected async handleDelete(\n mutation: PendingMutation<any>,\n context: LockContext,\n waitForCompletion: boolean = false,\n ): Promise<PendingOperation | null> {\n debug(`update`, mutation)\n\n return this.handleOperationWithCompletion(\n mutation,\n context,\n waitForCompletion,\n // eslint-disable-next-line no-shadow\n async (tableName, mutation) => {\n const metadataValue = this.processMutationMetadata(mutation)\n if (metadataValue != null) {\n /**\n * Delete operations with metadata require a different approach to handle metadata.\n * This will delete the record.\n */\n await context.execute(\n `\n UPDATE ${tableName} SET _deleted = TRUE, _metadata = ? WHERE id = ?\n `,\n [metadataValue, asPowerSyncRecord(mutation.original).id],\n )\n } else {\n await context.execute(\n `\n DELETE FROM ${tableName} WHERE id = ?\n `,\n [asPowerSyncRecord(mutation.original).id],\n )\n }\n },\n )\n }\n\n /**\n * Helper function which wraps a persistence operation by:\n * - Fetching the mutation's collection's SQLite table details\n * - Executing the mutation\n * - Returning the last pending diff operation if required\n */\n protected async handleOperationWithCompletion(\n mutation: PendingMutation<any>,\n context: LockContext,\n waitForCompletion: boolean,\n handler: (\n tableName: string,\n mutation: PendingMutation<any>,\n serializeValue: (value: any) => Record<string, unknown>,\n ) => Promise<void>,\n ): Promise<PendingOperation | null> {\n const { tableName, trackedTableName, serializeValue } =\n this.getMutationCollectionMeta(mutation)\n\n await handler(sanitizeSQL`${tableName}`, mutation, serializeValue)\n\n if (!waitForCompletion) {\n return null\n }\n\n // Need to get the operation in order to wait for it\n const diffOperation = await context.get<{ id: string; timestamp: string }>(\n sanitizeSQL`SELECT id, timestamp FROM ${trackedTableName} ORDER BY timestamp DESC LIMIT 1`,\n )\n return {\n tableName,\n id: diffOperation.id,\n operation: mapOperationToPowerSync(mutation.type),\n timestamp: diffOperation.timestamp,\n }\n }\n\n protected getMutationCollectionMeta(\n mutation: PendingMutation<any>,\n ): PowerSyncCollectionMeta<any> {\n if (\n typeof (mutation.collection.config as any).utils?.getMeta != `function`\n ) {\n throw new Error(`Collection is not a PowerSync collection.`)\n }\n return (\n mutation.collection\n .config as unknown as EnhancedPowerSyncCollectionConfig<any>\n ).utils.getMeta()\n }\n\n /**\n * Processes collection mutation metadata for persistence to the database.\n * We only support storing string metadata.\n * @returns null if no metadata should be stored.\n */\n protected processMutationMetadata(\n mutation: PendingMutation<any>,\n ): string | null {\n const { metadataIsTracked } = this.getMutationCollectionMeta(mutation)\n if (!metadataIsTracked) {\n // If it's not supported, we don't store metadata.\n if (typeof mutation.metadata != `undefined`) {\n // Log a warning if metadata is provided but not tracked.\n this.database.logger.warn(\n `Metadata provided for collection ${mutation.collection.id} but the PowerSync table does not track metadata. The PowerSync table should be configured with trackMetadata: true.`,\n mutation.metadata,\n )\n }\n return null\n } else if (typeof mutation.metadata == `undefined`) {\n return null\n } else if (typeof mutation.metadata == `string`) {\n return mutation.metadata\n } else {\n return JSON.stringify(mutation.metadata)\n }\n }\n}\n"],"names":["PendingOperationStore","mutation","sanitizeSQL","asPowerSyncRecord","mapOperationToPowerSync"],"mappings":";;;;;;AAYA,MAAM,QAAQ,YAAY,MAAM,iBAAiB;AAyC1C,MAAM,oBAAoB;AAAA,EAI/B,YAAY,SAA4B;AACtC,SAAK,WAAW,QAAQ;AACxB,SAAK,wBAAwBA,sBAAAA,sBAAsB;AAAA,EACrD;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,iBAAiB,aAA+B;AACpD,UAAM,EAAE,cAAc;AAEtB,QAAI,UAAU,UAAU,GAAG;AACzB;AAAA,IACF;AAKA,UAAM,yBAAyB,UAAU;AAAA,MACvC,CAAC,aAAa,SAAS,WAAW;AAAA,IAAA;AAEpC,UAAM,gBAAgB,MAAM,KAAK,IAAI,IAAI,sBAAsB,CAAC;AAChE,UAAM,oDAAoC,IAAA;AAC1C,UAAM,iBAAiB,cACpB,IAAI,CAAC,OAAO,UAAU,KAAK,CAAC,aAAa,SAAS,WAAW,MAAM,EAAE,CAAE,EACvE,IAAI,CAAC,aAAa,SAAS,UAAU;AACxC,eAAW,gBAAgB,eAAe;AACxC,oCAA8B;AAAA,QAC5B;AAAA,QACA,uBAAuB,YAAY,YAAY;AAAA,MAAA;AAAA,IAEnD;AAGA,UAAM,QAAQ;AAAA,MACZ,eAAe,IAAI,OAAO,eAAe;AACvC,YAAI,WAAW,WAAW;AACxB;AAAA,QACF;AACA,cAAM,IAAI,QAAc,CAAC,YAAY,WAAW,aAAa,OAAO,CAAC;AAAA,MACvE,CAAC;AAAA,IAAA;AAIH,UAAM,EAAE,aAAA,IAAiB,MAAM,KAAK,SAAS;AAAA,MAC3C,OAAO,OAAO;AACZ,cAAM,oBAAoD,CAAA;AAE1D,mBAAW,CAAC,OAAO,QAAQ,KAAK,UAAU,WAAW;AAKnD,gBAAM,aACJ,SAAS,8BAA8B,IAAI,SAAS,WAAW,EAAE;AACnE,kBAAQ,SAAS,MAAA;AAAA,YACf,KAAK;AACH,gCAAkB;AAAA,gBAChB,MAAM,KAAK,aAAa,UAAU,IAAI,UAAU;AAAA,cAAA;AAElD;AAAA,YACF,KAAK;AACH,gCAAkB;AAAA,gBAChB,MAAM,KAAK,aAAa,UAAU,IAAI,UAAU;AAAA,cAAA;AAElD;AAAA,YACF,KAAK;AACH,gCAAkB;AAAA,gBAChB,MAAM,KAAK,aAAa,UAAU,IAAI,UAAU;AAAA,cAAA;AAElD;AAAA,UAAA;AAAA,QAEN;AAQA,eAAO;AAAA,UACL,cAAc,QAAQ;AAAA,YACpB,kBACG,OAAO,CAAC,OAAO,CAAC,CAAC,EAAE,EACnB,IAAI,CAAC,OAAO,KAAK,sBAAsB,QAAQ,EAAE,CAAC;AAAA,UAAA;AAAA,QACvD;AAAA,MAEJ;AAAA,IAAA;AAIF,UAAM;AAAA,EACR;AAAA,EAEA,MAAgB,aACd,UACA,SACA,oBAA6B,OACK;AAClC,UAAM,UAAU,QAAQ;AAExB,WAAO,KAAK;AAAA,MACV;AAAA,MACA;AAAA,MACA;AAAA;AAAA,MAEA,OAAO,WAAWC,WAAU,mBAAmB;AAC7C,cAAM,SAAS,eAAeA,UAAS,QAAQ;AAC/C,cAAM,OAAO,OAAO,KAAK,MAAM,EAAE,IAAI,CAAC,QAAQC,OAAAA,cAAc,GAAG,EAAE;AACjE,cAAM,kBAAkB,OAAO,OAAO,MAAM;AAE5C,cAAM,gBAAgB,KAAK,wBAAwBD,SAAQ;AAC3D,YAAI,iBAAiB,MAAM;AACzB,eAAK,KAAK,WAAW;AACrB,0BAAgB,KAAK,aAAa;AAAA,QACpC;AAEA,cAAM,QAAQ;AAAA,UACZ;AAAA,sBACY,SAAS;AAAA,eAChB,KAAK,KAAK,IAAI,CAAC;AAAA;AAAA,eAEf,KAAK,IAAI,CAAC,MAAM,GAAG,EAAE,KAAK,IAAI,CAAC;AAAA;AAAA,UAEpC;AAAA,QAAA;AAAA,MAEJ;AAAA,IAAA;AAAA,EAEJ;AAAA,EAEA,MAAgB,aACd,UACA,SACA,oBAA6B,OACK;AAClC,UAAM,UAAU,QAAQ;AAExB,WAAO,KAAK;AAAA,MACV;AAAA,MACA;AAAA,MACA;AAAA;AAAA,MAEA,OAAO,WAAWA,WAAU,mBAAmB;AAC7C,cAAM,SAAS,eAAeA,UAAS,QAAQ;AAC/C,cAAM,OAAO,OAAO,KAAK,MAAM,EAAE,IAAI,CAAC,QAAQC,OAAAA,cAAc,GAAG,EAAE;AACjE,cAAM,kBAAkB,OAAO,OAAO,MAAM;AAE5C,cAAM,gBAAgB,KAAK,wBAAwBD,SAAQ;AAC3D,YAAI,iBAAiB,MAAM;AACzB,eAAK,KAAK,WAAW;AACrB,0BAAgB,KAAK,aAAa;AAAA,QACpC;AAEA,cAAM,QAAQ;AAAA,UACZ;AAAA,iBACO,SAAS;AAAA,cACZ,KAAK,IAAI,CAAC,QAAQ,GAAG,GAAG,MAAM,EAAE,KAAK,IAAI,CAAC;AAAA;AAAA;AAAA,UAG9C,CAAC,GAAG,iBAAiBE,QAAAA,kBAAkBF,UAAS,QAAQ,EAAE,EAAE;AAAA,QAAA;AAAA,MAEhE;AAAA,IAAA;AAAA,EAEJ;AAAA,EAEA,MAAgB,aACd,UACA,SACA,oBAA6B,OACK;AAClC,UAAM,UAAU,QAAQ;AAExB,WAAO,KAAK;AAAA,MACV;AAAA,MACA;AAAA,MACA;AAAA;AAAA,MAEA,OAAO,WAAWA,cAAa;AAC7B,cAAM,gBAAgB,KAAK,wBAAwBA,SAAQ;AAC3D,YAAI,iBAAiB,MAAM;AAKzB,gBAAM,QAAQ;AAAA,YACZ;AAAA,qBACS,SAAS;AAAA;AAAA,YAElB,CAAC,eAAeE,QAAAA,kBAAkBF,UAAS,QAAQ,EAAE,EAAE;AAAA,UAAA;AAAA,QAE3D,OAAO;AACL,gBAAM,QAAQ;AAAA,YACZ;AAAA,0BACc,SAAS;AAAA;AAAA,YAEvB,CAACE,0BAAkBF,UAAS,QAAQ,EAAE,EAAE;AAAA,UAAA;AAAA,QAE5C;AAAA,MACF;AAAA,IAAA;AAAA,EAEJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAgB,8BACd,UACA,SACA,mBACA,SAKkC;AAClC,UAAM,EAAE,WAAW,kBAAkB,mBACnC,KAAK,0BAA0B,QAAQ;AAEzC,UAAM,QAAQC,OAAAA,cAAc,SAAS,IAAI,UAAU,cAAc;AAEjE,QAAI,CAAC,mBAAmB;AACtB,aAAO;AAAA,IACT;AAGA,UAAM,gBAAgB,MAAM,QAAQ;AAAA,MAClCA,+CAAwC,gBAAgB;AAAA,IAAA;AAE1D,WAAO;AAAA,MACL;AAAA,MACA,IAAI,cAAc;AAAA,MAClB,WAAWE,QAAAA,wBAAwB,SAAS,IAAI;AAAA,MAChD,WAAW,cAAc;AAAA,IAAA;AAAA,EAE7B;AAAA,EAEU,0BACR,UAC8B;AAC9B,QACE,OAAQ,SAAS,WAAW,OAAe,OAAO,WAAW,YAC7D;AACA,YAAM,IAAI,MAAM,2CAA2C;AAAA,IAC7D;AACA,WACE,SAAS,WACN,OACH,MAAM,QAAA;AAAA,EACV;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOU,wBACR,UACe;AACf,UAAM,EAAE,kBAAA,IAAsB,KAAK,0BAA0B,QAAQ;AACrE,QAAI,CAAC,mBAAmB;AAEtB,UAAI,OAAO,SAAS,YAAY,aAAa;AAE3C,aAAK,SAAS,OAAO;AAAA,UACnB,oCAAoC,SAAS,WAAW,EAAE;AAAA,UAC1D,SAAS;AAAA,QAAA;AAAA,MAEb;AACA,aAAO;AAAA,IACT,WAAW,OAAO,SAAS,YAAY,aAAa;AAClD,aAAO;AAAA,IACT,WAAW,OAAO,SAAS,YAAY,UAAU;AAC/C,aAAO,SAAS;AAAA,IAClB,OAAO;AACL,aAAO,KAAK,UAAU,SAAS,QAAQ;AAAA,IACzC;AAAA,EACF;AACF;;"}
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
3
3
|
const common = require("@powersync/common");
|
|
4
4
|
function serializeForSQLite(value, tableSchema, customSerializer = {}) {
|
|
5
5
|
return Object.fromEntries(
|
|
6
|
+
// eslint-disable-next-line no-shadow
|
|
6
7
|
Object.entries(value).map(([key, value2]) => {
|
|
7
8
|
const outputType = key == `id` ? common.ColumnType.TEXT : tableSchema.columns.find((column) => column.name == key)?.type;
|
|
8
9
|
if (!outputType) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serialization.cjs","sources":["../../src/serialization.ts"],"sourcesContent":["import { ColumnType } from '@powersync/common'\nimport type { Table } from '@powersync/common'\nimport type { CustomSQLiteSerializer } from './definitions'\nimport type {\n ExtractedTable,\n ExtractedTableColumns,\n MapBaseColumnType,\n} from './helpers'\n\n/**\n * Serializes an object for persistence to a SQLite table, mapping its values to appropriate SQLite types.\n *\n * This function takes an object representing a row, a table schema, and an optional custom serializer map.\n * It returns a new object with values transformed to be compatible with SQLite column types.\n *\n * ## Generics\n * - `TOutput`: The shape of the input object, typically matching the row data.\n * - `TTable`: The table schema, which must match the keys of `TOutput`.\n *\n * ## Parameters\n * - `value`: The object to serialize (row data).\n * - `tableSchema`: The schema describing the SQLite table columns and types.\n * - `customSerializer`: An optional map of custom serialization functions for specific keys.\n *\n * ## Behavior\n * - For each key in `value`, finds the corresponding column in `tableSchema`.\n * - If a custom serializer is provided for a key, it is used to transform the value.\n * - Otherwise, values are mapped according to the column type:\n * - `TEXT`: Strings are passed through; Dates are converted to ISO strings; other types are JSON-stringified.\n * - `INTEGER`/`REAL`: Numbers are passed through; booleans are mapped to 1/0; other types are coerced to numbers.\n * - Throws if a column type is unknown or a value cannot be converted.\n *\n * ## Returns\n * - An object with the same keys as `value`, with values transformed for SQLite compatibility.\n *\n * ## Errors\n * - Throws if a key in `value` does not exist in the schema.\n * - Throws if a value cannot be converted to the required SQLite type.\n */\nexport function serializeForSQLite<\n TOutput extends Record<string, unknown>,\n // The keys should match\n TTable extends Table<MapBaseColumnType<TOutput>> = Table<\n MapBaseColumnType<TOutput>\n >,\n>(\n value: TOutput,\n tableSchema: TTable,\n customSerializer: Partial<\n CustomSQLiteSerializer<TOutput, ExtractedTableColumns<TTable>>\n > = {},\n): ExtractedTable<TTable> {\n return Object.fromEntries(\n Object.entries(value).map(([key, value]) => {\n // First get the output schema type\n const outputType =\n key == `id`\n ? ColumnType.TEXT\n : tableSchema.columns.find((column) => column.name == key)?.type\n if (!outputType) {\n throw new Error(`Could not find schema for ${key} column.`)\n }\n\n if (value == null) {\n return [key, value]\n }\n\n const customTransform = customSerializer[key]\n if (customTransform) {\n return [key, customTransform(value as TOutput[string])]\n }\n\n // Map to the output\n switch (outputType) {\n case ColumnType.TEXT:\n if (typeof value == `string`) {\n return [key, value]\n } else if (value instanceof Date) {\n return [key, value.toISOString()]\n } else {\n return [key, JSON.stringify(value)]\n }\n case ColumnType.INTEGER:\n case ColumnType.REAL:\n if (typeof value == `number`) {\n return [key, value]\n } else if (typeof value == `boolean`) {\n return [key, value ? 1 : 0]\n } else {\n const numberValue = Number(value)\n if (isNaN(numberValue)) {\n throw new Error(\n `Could not convert ${key}=${value} to a number for SQLite`,\n )\n }\n return [key, numberValue]\n }\n }\n }),\n )\n}\n"],"names":["value","ColumnType"],"mappings":";;;AAuCO,SAAS,mBAOd,OACA,aACA,mBAEI,CAAA,GACoB;AACxB,SAAO,OAAO;AAAA,
|
|
1
|
+
{"version":3,"file":"serialization.cjs","sources":["../../src/serialization.ts"],"sourcesContent":["import { ColumnType } from '@powersync/common'\nimport type { Table } from '@powersync/common'\nimport type { CustomSQLiteSerializer } from './definitions'\nimport type {\n ExtractedTable,\n ExtractedTableColumns,\n MapBaseColumnType,\n} from './helpers'\n\n/**\n * Serializes an object for persistence to a SQLite table, mapping its values to appropriate SQLite types.\n *\n * This function takes an object representing a row, a table schema, and an optional custom serializer map.\n * It returns a new object with values transformed to be compatible with SQLite column types.\n *\n * ## Generics\n * - `TOutput`: The shape of the input object, typically matching the row data.\n * - `TTable`: The table schema, which must match the keys of `TOutput`.\n *\n * ## Parameters\n * - `value`: The object to serialize (row data).\n * - `tableSchema`: The schema describing the SQLite table columns and types.\n * - `customSerializer`: An optional map of custom serialization functions for specific keys.\n *\n * ## Behavior\n * - For each key in `value`, finds the corresponding column in `tableSchema`.\n * - If a custom serializer is provided for a key, it is used to transform the value.\n * - Otherwise, values are mapped according to the column type:\n * - `TEXT`: Strings are passed through; Dates are converted to ISO strings; other types are JSON-stringified.\n * - `INTEGER`/`REAL`: Numbers are passed through; booleans are mapped to 1/0; other types are coerced to numbers.\n * - Throws if a column type is unknown or a value cannot be converted.\n *\n * ## Returns\n * - An object with the same keys as `value`, with values transformed for SQLite compatibility.\n *\n * ## Errors\n * - Throws if a key in `value` does not exist in the schema.\n * - Throws if a value cannot be converted to the required SQLite type.\n */\nexport function serializeForSQLite<\n TOutput extends Record<string, unknown>,\n // The keys should match\n TTable extends Table<MapBaseColumnType<TOutput>> = Table<\n MapBaseColumnType<TOutput>\n >,\n>(\n value: TOutput,\n tableSchema: TTable,\n customSerializer: Partial<\n CustomSQLiteSerializer<TOutput, ExtractedTableColumns<TTable>>\n > = {},\n): ExtractedTable<TTable> {\n return Object.fromEntries(\n // eslint-disable-next-line no-shadow\n Object.entries(value).map(([key, value]) => {\n // First get the output schema type\n const outputType =\n key == `id`\n ? ColumnType.TEXT\n : tableSchema.columns.find((column) => column.name == key)?.type\n if (!outputType) {\n throw new Error(`Could not find schema for ${key} column.`)\n }\n\n if (value == null) {\n return [key, value]\n }\n\n const customTransform = customSerializer[key]\n if (customTransform) {\n return [key, customTransform(value as TOutput[string])]\n }\n\n // Map to the output\n switch (outputType) {\n case ColumnType.TEXT:\n if (typeof value == `string`) {\n return [key, value]\n } else if (value instanceof Date) {\n return [key, value.toISOString()]\n } else {\n return [key, JSON.stringify(value)]\n }\n case ColumnType.INTEGER:\n case ColumnType.REAL:\n if (typeof value == `number`) {\n return [key, value]\n } else if (typeof value == `boolean`) {\n return [key, value ? 1 : 0]\n } else {\n const numberValue = Number(value)\n if (isNaN(numberValue)) {\n throw new Error(\n `Could not convert ${key}=${value} to a number for SQLite`,\n )\n }\n return [key, numberValue]\n }\n }\n }),\n )\n}\n"],"names":["value","ColumnType"],"mappings":";;;AAuCO,SAAS,mBAOd,OACA,aACA,mBAEI,CAAA,GACoB;AACxB,SAAO,OAAO;AAAA;AAAA,IAEZ,OAAO,QAAQ,KAAK,EAAE,IAAI,CAAC,CAAC,KAAKA,MAAK,MAAM;AAE1C,YAAM,aACJ,OAAO,OACHC,OAAAA,WAAW,OACX,YAAY,QAAQ,KAAK,CAAC,WAAW,OAAO,QAAQ,GAAG,GAAG;AAChE,UAAI,CAAC,YAAY;AACf,cAAM,IAAI,MAAM,6BAA6B,GAAG,UAAU;AAAA,MAC5D;AAEA,UAAID,UAAS,MAAM;AACjB,eAAO,CAAC,KAAKA,MAAK;AAAA,MACpB;AAEA,YAAM,kBAAkB,iBAAiB,GAAG;AAC5C,UAAI,iBAAiB;AACnB,eAAO,CAAC,KAAK,gBAAgBA,MAAwB,CAAC;AAAA,MACxD;AAGA,cAAQ,YAAA;AAAA,QACN,KAAKC,OAAAA,WAAW;AACd,cAAI,OAAOD,UAAS,UAAU;AAC5B,mBAAO,CAAC,KAAKA,MAAK;AAAA,UACpB,WAAWA,kBAAiB,MAAM;AAChC,mBAAO,CAAC,KAAKA,OAAM,aAAa;AAAA,UAClC,OAAO;AACL,mBAAO,CAAC,KAAK,KAAK,UAAUA,MAAK,CAAC;AAAA,UACpC;AAAA,QACF,KAAKC,OAAAA,WAAW;AAAA,QAChB,KAAKA,OAAAA,WAAW;AACd,cAAI,OAAOD,UAAS,UAAU;AAC5B,mBAAO,CAAC,KAAKA,MAAK;AAAA,UACpB,WAAW,OAAOA,UAAS,WAAW;AACpC,mBAAO,CAAC,KAAKA,SAAQ,IAAI,CAAC;AAAA,UAC5B,OAAO;AACL,kBAAM,cAAc,OAAOA,MAAK;AAChC,gBAAI,MAAM,WAAW,GAAG;AACtB,oBAAM,IAAI;AAAA,gBACR,qBAAqB,GAAG,IAAIA,MAAK;AAAA,cAAA;AAAA,YAErC;AACA,mBAAO,CAAC,KAAK,WAAW;AAAA,UAC1B;AAAA,MAAA;AAAA,IAEN,CAAC;AAAA,EAAA;AAEL;;"}
|
|
@@ -74,6 +74,7 @@ class PowerSyncTransactor {
|
|
|
74
74
|
mutation,
|
|
75
75
|
context,
|
|
76
76
|
waitForCompletion,
|
|
77
|
+
// eslint-disable-next-line no-shadow
|
|
77
78
|
async (tableName, mutation2, serializeValue) => {
|
|
78
79
|
const values = serializeValue(mutation2.modified);
|
|
79
80
|
const keys = Object.keys(values).map((key) => sanitizeSQL`${key}`);
|
|
@@ -85,9 +86,9 @@ class PowerSyncTransactor {
|
|
|
85
86
|
}
|
|
86
87
|
await context.execute(
|
|
87
88
|
`
|
|
88
|
-
INSERT into ${tableName}
|
|
89
|
-
(${keys.join(`, `)})
|
|
90
|
-
VALUES
|
|
89
|
+
INSERT into ${tableName}
|
|
90
|
+
(${keys.join(`, `)})
|
|
91
|
+
VALUES
|
|
91
92
|
(${keys.map((_) => `?`).join(`, `)})
|
|
92
93
|
`,
|
|
93
94
|
queryParameters
|
|
@@ -101,6 +102,7 @@ class PowerSyncTransactor {
|
|
|
101
102
|
mutation,
|
|
102
103
|
context,
|
|
103
104
|
waitForCompletion,
|
|
105
|
+
// eslint-disable-next-line no-shadow
|
|
104
106
|
async (tableName, mutation2, serializeValue) => {
|
|
105
107
|
const values = serializeValue(mutation2.modified);
|
|
106
108
|
const keys = Object.keys(values).map((key) => sanitizeSQL`${key}`);
|
|
@@ -112,7 +114,7 @@ class PowerSyncTransactor {
|
|
|
112
114
|
}
|
|
113
115
|
await context.execute(
|
|
114
116
|
`
|
|
115
|
-
UPDATE ${tableName}
|
|
117
|
+
UPDATE ${tableName}
|
|
116
118
|
SET ${keys.map((key) => `${key} = ?`).join(`, `)}
|
|
117
119
|
WHERE id = ?
|
|
118
120
|
`,
|
|
@@ -127,6 +129,7 @@ class PowerSyncTransactor {
|
|
|
127
129
|
mutation,
|
|
128
130
|
context,
|
|
129
131
|
waitForCompletion,
|
|
132
|
+
// eslint-disable-next-line no-shadow
|
|
130
133
|
async (tableName, mutation2) => {
|
|
131
134
|
const metadataValue = this.processMutationMetadata(mutation2);
|
|
132
135
|
if (metadataValue != null) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PowerSyncTransactor.js","sources":["../../src/PowerSyncTransactor.ts"],"sourcesContent":["import { sanitizeSQL } from '@powersync/common'\nimport DebugModule from 'debug'\nimport { PendingOperationStore } from './PendingOperationStore'\nimport { asPowerSyncRecord, mapOperationToPowerSync } from './helpers'\nimport type { AbstractPowerSyncDatabase, LockContext } from '@powersync/common'\nimport type { PendingMutation, Transaction } from '@tanstack/db'\nimport type { PendingOperation } from './PendingOperationStore'\nimport type {\n EnhancedPowerSyncCollectionConfig,\n PowerSyncCollectionMeta,\n} from './definitions'\n\nconst debug = DebugModule.debug(`ts/db:powersync`)\n\nexport type TransactorOptions = {\n database: AbstractPowerSyncDatabase\n}\n\n/**\n * Applies mutations to the PowerSync database. This method is called automatically by the collection's\n * insert, update, and delete operations. You typically don't need to call this directly unless you\n * have special transaction requirements.\n *\n * @example\n * ```typescript\n * // Create a collection\n * const collection = createCollection(\n * powerSyncCollectionOptions<Document>({\n * database: db,\n * table: APP_SCHEMA.props.documents,\n * })\n * )\n *\n * const addTx = createTransaction({\n * autoCommit: false,\n * mutationFn: async ({ transaction }) => {\n * await new PowerSyncTransactor({ database: db }).applyTransaction(transaction)\n * },\n * })\n *\n * addTx.mutate(() => {\n * for (let i = 0; i < 5; i++) {\n * collection.insert({ id: randomUUID(), name: `tx-${i}` })\n * }\n * })\n *\n * await addTx.commit()\n * await addTx.isPersisted.promise\n * ```\n *\n * @param transaction - The transaction containing mutations to apply\n * @returns A promise that resolves when the mutations have been persisted to PowerSync\n */\nexport class PowerSyncTransactor {\n database: AbstractPowerSyncDatabase\n pendingOperationStore: PendingOperationStore\n\n constructor(options: TransactorOptions) {\n this.database = options.database\n this.pendingOperationStore = PendingOperationStore.GLOBAL\n }\n\n /**\n * Persists a {@link Transaction} to the PowerSync SQLite database.\n */\n async applyTransaction(transaction: Transaction<any>) {\n const { mutations } = transaction\n\n if (mutations.length == 0) {\n return\n }\n /**\n * The transaction might contain operations for different collections.\n * We can do some optimizations for single-collection transactions.\n */\n const mutationsCollectionIds = mutations.map(\n (mutation) => mutation.collection.id,\n )\n const collectionIds = Array.from(new Set(mutationsCollectionIds))\n const lastCollectionMutationIndexes = new Map<string, number>()\n const allCollections = collectionIds\n .map((id) => mutations.find((mutation) => mutation.collection.id == id)!)\n .map((mutation) => mutation.collection)\n for (const collectionId of collectionIds) {\n lastCollectionMutationIndexes.set(\n collectionId,\n mutationsCollectionIds.lastIndexOf(collectionId),\n )\n }\n\n // Check all the observers are ready before taking a lock\n await Promise.all(\n allCollections.map(async (collection) => {\n if (collection.isReady()) {\n return\n }\n await new Promise<void>((resolve) => collection.onFirstReady(resolve))\n }),\n )\n\n // Persist to PowerSync\n const { whenComplete } = await this.database.writeTransaction(\n async (tx) => {\n const pendingOperations: Array<PendingOperation | null> = []\n\n for (const [index, mutation] of mutations.entries()) {\n /**\n * Each collection processes events independently. We need to make sure the\n * last operation for each collection has been observed.\n */\n const shouldWait =\n index == lastCollectionMutationIndexes.get(mutation.collection.id)\n switch (mutation.type) {\n case `insert`:\n pendingOperations.push(\n await this.handleInsert(mutation, tx, shouldWait),\n )\n break\n case `update`:\n pendingOperations.push(\n await this.handleUpdate(mutation, tx, shouldWait),\n )\n break\n case `delete`:\n pendingOperations.push(\n await this.handleDelete(mutation, tx, shouldWait),\n )\n break\n }\n }\n\n /**\n * Return a promise from the writeTransaction, without awaiting it.\n * This promise will resolve once the entire transaction has been\n * observed via the diff triggers.\n * We return without awaiting in order to free the write lock.\n */\n return {\n whenComplete: Promise.all(\n pendingOperations\n .filter((op) => !!op)\n .map((op) => this.pendingOperationStore.waitFor(op)),\n ),\n }\n },\n )\n\n // Wait for the change to be observed via the diff trigger\n await whenComplete\n }\n\n protected async handleInsert(\n mutation: PendingMutation<any>,\n context: LockContext,\n waitForCompletion: boolean = false,\n ): Promise<PendingOperation | null> {\n debug(`insert`, mutation)\n\n return this.handleOperationWithCompletion(\n mutation,\n context,\n waitForCompletion,\n async (tableName, mutation, serializeValue) => {\n const values = serializeValue(mutation.modified)\n const keys = Object.keys(values).map((key) => sanitizeSQL`${key}`)\n const queryParameters = Object.values(values)\n\n const metadataValue = this.processMutationMetadata(mutation)\n if (metadataValue != null) {\n keys.push(`_metadata`)\n queryParameters.push(metadataValue)\n }\n\n await context.execute(\n `\n INSERT into ${tableName} \n (${keys.join(`, `)}) \n VALUES \n (${keys.map((_) => `?`).join(`, `)})\n `,\n queryParameters,\n )\n },\n )\n }\n\n protected async handleUpdate(\n mutation: PendingMutation<any>,\n context: LockContext,\n waitForCompletion: boolean = false,\n ): Promise<PendingOperation | null> {\n debug(`update`, mutation)\n\n return this.handleOperationWithCompletion(\n mutation,\n context,\n waitForCompletion,\n async (tableName, mutation, serializeValue) => {\n const values = serializeValue(mutation.modified)\n const keys = Object.keys(values).map((key) => sanitizeSQL`${key}`)\n const queryParameters = Object.values(values)\n\n const metadataValue = this.processMutationMetadata(mutation)\n if (metadataValue != null) {\n keys.push(`_metadata`)\n queryParameters.push(metadataValue)\n }\n\n await context.execute(\n `\n UPDATE ${tableName} \n SET ${keys.map((key) => `${key} = ?`).join(`, `)}\n WHERE id = ?\n `,\n [...queryParameters, asPowerSyncRecord(mutation.modified).id],\n )\n },\n )\n }\n\n protected async handleDelete(\n mutation: PendingMutation<any>,\n context: LockContext,\n waitForCompletion: boolean = false,\n ): Promise<PendingOperation | null> {\n debug(`update`, mutation)\n\n return this.handleOperationWithCompletion(\n mutation,\n context,\n waitForCompletion,\n async (tableName, mutation) => {\n const metadataValue = this.processMutationMetadata(mutation)\n if (metadataValue != null) {\n /**\n * Delete operations with metadata require a different approach to handle metadata.\n * This will delete the record.\n */\n await context.execute(\n `\n UPDATE ${tableName} SET _deleted = TRUE, _metadata = ? WHERE id = ?\n `,\n [metadataValue, asPowerSyncRecord(mutation.original).id],\n )\n } else {\n await context.execute(\n `\n DELETE FROM ${tableName} WHERE id = ?\n `,\n [asPowerSyncRecord(mutation.original).id],\n )\n }\n },\n )\n }\n\n /**\n * Helper function which wraps a persistence operation by:\n * - Fetching the mutation's collection's SQLite table details\n * - Executing the mutation\n * - Returning the last pending diff operation if required\n */\n protected async handleOperationWithCompletion(\n mutation: PendingMutation<any>,\n context: LockContext,\n waitForCompletion: boolean,\n handler: (\n tableName: string,\n mutation: PendingMutation<any>,\n serializeValue: (value: any) => Record<string, unknown>,\n ) => Promise<void>,\n ): Promise<PendingOperation | null> {\n const { tableName, trackedTableName, serializeValue } =\n this.getMutationCollectionMeta(mutation)\n\n await handler(sanitizeSQL`${tableName}`, mutation, serializeValue)\n\n if (!waitForCompletion) {\n return null\n }\n\n // Need to get the operation in order to wait for it\n const diffOperation = await context.get<{ id: string; timestamp: string }>(\n sanitizeSQL`SELECT id, timestamp FROM ${trackedTableName} ORDER BY timestamp DESC LIMIT 1`,\n )\n return {\n tableName,\n id: diffOperation.id,\n operation: mapOperationToPowerSync(mutation.type),\n timestamp: diffOperation.timestamp,\n }\n }\n\n protected getMutationCollectionMeta(\n mutation: PendingMutation<any>,\n ): PowerSyncCollectionMeta<any> {\n if (\n typeof (mutation.collection.config as any).utils?.getMeta != `function`\n ) {\n throw new Error(`Collection is not a PowerSync collection.`)\n }\n return (\n mutation.collection\n .config as unknown as EnhancedPowerSyncCollectionConfig<any>\n ).utils.getMeta()\n }\n\n /**\n * Processes collection mutation metadata for persistence to the database.\n * We only support storing string metadata.\n * @returns null if no metadata should be stored.\n */\n protected processMutationMetadata(\n mutation: PendingMutation<any>,\n ): string | null {\n const { metadataIsTracked } = this.getMutationCollectionMeta(mutation)\n if (!metadataIsTracked) {\n // If it's not supported, we don't store metadata.\n if (typeof mutation.metadata != `undefined`) {\n // Log a warning if metadata is provided but not tracked.\n this.database.logger.warn(\n `Metadata provided for collection ${mutation.collection.id} but the PowerSync table does not track metadata. The PowerSync table should be configured with trackMetadata: true.`,\n mutation.metadata,\n )\n }\n return null\n } else if (typeof mutation.metadata == `undefined`) {\n return null\n } else if (typeof mutation.metadata == `string`) {\n return mutation.metadata\n } else {\n return JSON.stringify(mutation.metadata)\n }\n }\n}\n"],"names":["mutation"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PowerSyncTransactor.js","sources":["../../src/PowerSyncTransactor.ts"],"sourcesContent":["import { sanitizeSQL } from '@powersync/common'\nimport DebugModule from 'debug'\nimport { PendingOperationStore } from './PendingOperationStore'\nimport { asPowerSyncRecord, mapOperationToPowerSync } from './helpers'\nimport type { AbstractPowerSyncDatabase, LockContext } from '@powersync/common'\nimport type { PendingMutation, Transaction } from '@tanstack/db'\nimport type { PendingOperation } from './PendingOperationStore'\nimport type {\n EnhancedPowerSyncCollectionConfig,\n PowerSyncCollectionMeta,\n} from './definitions'\n\nconst debug = DebugModule.debug(`ts/db:powersync`)\n\nexport type TransactorOptions = {\n database: AbstractPowerSyncDatabase\n}\n\n/**\n * Applies mutations to the PowerSync database. This method is called automatically by the collection's\n * insert, update, and delete operations. You typically don't need to call this directly unless you\n * have special transaction requirements.\n *\n * @example\n * ```typescript\n * // Create a collection\n * const collection = createCollection(\n * powerSyncCollectionOptions<Document>({\n * database: db,\n * table: APP_SCHEMA.props.documents,\n * })\n * )\n *\n * const addTx = createTransaction({\n * autoCommit: false,\n * mutationFn: async ({ transaction }) => {\n * await new PowerSyncTransactor({ database: db }).applyTransaction(transaction)\n * },\n * })\n *\n * addTx.mutate(() => {\n * for (let i = 0; i < 5; i++) {\n * collection.insert({ id: randomUUID(), name: `tx-${i}` })\n * }\n * })\n *\n * await addTx.commit()\n * await addTx.isPersisted.promise\n * ```\n *\n * @param transaction - The transaction containing mutations to apply\n * @returns A promise that resolves when the mutations have been persisted to PowerSync\n */\nexport class PowerSyncTransactor {\n database: AbstractPowerSyncDatabase\n pendingOperationStore: PendingOperationStore\n\n constructor(options: TransactorOptions) {\n this.database = options.database\n this.pendingOperationStore = PendingOperationStore.GLOBAL\n }\n\n /**\n * Persists a {@link Transaction} to the PowerSync SQLite database.\n */\n async applyTransaction(transaction: Transaction<any>) {\n const { mutations } = transaction\n\n if (mutations.length == 0) {\n return\n }\n /**\n * The transaction might contain operations for different collections.\n * We can do some optimizations for single-collection transactions.\n */\n const mutationsCollectionIds = mutations.map(\n (mutation) => mutation.collection.id,\n )\n const collectionIds = Array.from(new Set(mutationsCollectionIds))\n const lastCollectionMutationIndexes = new Map<string, number>()\n const allCollections = collectionIds\n .map((id) => mutations.find((mutation) => mutation.collection.id == id)!)\n .map((mutation) => mutation.collection)\n for (const collectionId of collectionIds) {\n lastCollectionMutationIndexes.set(\n collectionId,\n mutationsCollectionIds.lastIndexOf(collectionId),\n )\n }\n\n // Check all the observers are ready before taking a lock\n await Promise.all(\n allCollections.map(async (collection) => {\n if (collection.isReady()) {\n return\n }\n await new Promise<void>((resolve) => collection.onFirstReady(resolve))\n }),\n )\n\n // Persist to PowerSync\n const { whenComplete } = await this.database.writeTransaction(\n async (tx) => {\n const pendingOperations: Array<PendingOperation | null> = []\n\n for (const [index, mutation] of mutations.entries()) {\n /**\n * Each collection processes events independently. We need to make sure the\n * last operation for each collection has been observed.\n */\n const shouldWait =\n index == lastCollectionMutationIndexes.get(mutation.collection.id)\n switch (mutation.type) {\n case `insert`:\n pendingOperations.push(\n await this.handleInsert(mutation, tx, shouldWait),\n )\n break\n case `update`:\n pendingOperations.push(\n await this.handleUpdate(mutation, tx, shouldWait),\n )\n break\n case `delete`:\n pendingOperations.push(\n await this.handleDelete(mutation, tx, shouldWait),\n )\n break\n }\n }\n\n /**\n * Return a promise from the writeTransaction, without awaiting it.\n * This promise will resolve once the entire transaction has been\n * observed via the diff triggers.\n * We return without awaiting in order to free the write lock.\n */\n return {\n whenComplete: Promise.all(\n pendingOperations\n .filter((op) => !!op)\n .map((op) => this.pendingOperationStore.waitFor(op)),\n ),\n }\n },\n )\n\n // Wait for the change to be observed via the diff trigger\n await whenComplete\n }\n\n protected async handleInsert(\n mutation: PendingMutation<any>,\n context: LockContext,\n waitForCompletion: boolean = false,\n ): Promise<PendingOperation | null> {\n debug(`insert`, mutation)\n\n return this.handleOperationWithCompletion(\n mutation,\n context,\n waitForCompletion,\n // eslint-disable-next-line no-shadow\n async (tableName, mutation, serializeValue) => {\n const values = serializeValue(mutation.modified)\n const keys = Object.keys(values).map((key) => sanitizeSQL`${key}`)\n const queryParameters = Object.values(values)\n\n const metadataValue = this.processMutationMetadata(mutation)\n if (metadataValue != null) {\n keys.push(`_metadata`)\n queryParameters.push(metadataValue)\n }\n\n await context.execute(\n `\n INSERT into ${tableName}\n (${keys.join(`, `)})\n VALUES\n (${keys.map((_) => `?`).join(`, `)})\n `,\n queryParameters,\n )\n },\n )\n }\n\n protected async handleUpdate(\n mutation: PendingMutation<any>,\n context: LockContext,\n waitForCompletion: boolean = false,\n ): Promise<PendingOperation | null> {\n debug(`update`, mutation)\n\n return this.handleOperationWithCompletion(\n mutation,\n context,\n waitForCompletion,\n // eslint-disable-next-line no-shadow\n async (tableName, mutation, serializeValue) => {\n const values = serializeValue(mutation.modified)\n const keys = Object.keys(values).map((key) => sanitizeSQL`${key}`)\n const queryParameters = Object.values(values)\n\n const metadataValue = this.processMutationMetadata(mutation)\n if (metadataValue != null) {\n keys.push(`_metadata`)\n queryParameters.push(metadataValue)\n }\n\n await context.execute(\n `\n UPDATE ${tableName}\n SET ${keys.map((key) => `${key} = ?`).join(`, `)}\n WHERE id = ?\n `,\n [...queryParameters, asPowerSyncRecord(mutation.modified).id],\n )\n },\n )\n }\n\n protected async handleDelete(\n mutation: PendingMutation<any>,\n context: LockContext,\n waitForCompletion: boolean = false,\n ): Promise<PendingOperation | null> {\n debug(`update`, mutation)\n\n return this.handleOperationWithCompletion(\n mutation,\n context,\n waitForCompletion,\n // eslint-disable-next-line no-shadow\n async (tableName, mutation) => {\n const metadataValue = this.processMutationMetadata(mutation)\n if (metadataValue != null) {\n /**\n * Delete operations with metadata require a different approach to handle metadata.\n * This will delete the record.\n */\n await context.execute(\n `\n UPDATE ${tableName} SET _deleted = TRUE, _metadata = ? WHERE id = ?\n `,\n [metadataValue, asPowerSyncRecord(mutation.original).id],\n )\n } else {\n await context.execute(\n `\n DELETE FROM ${tableName} WHERE id = ?\n `,\n [asPowerSyncRecord(mutation.original).id],\n )\n }\n },\n )\n }\n\n /**\n * Helper function which wraps a persistence operation by:\n * - Fetching the mutation's collection's SQLite table details\n * - Executing the mutation\n * - Returning the last pending diff operation if required\n */\n protected async handleOperationWithCompletion(\n mutation: PendingMutation<any>,\n context: LockContext,\n waitForCompletion: boolean,\n handler: (\n tableName: string,\n mutation: PendingMutation<any>,\n serializeValue: (value: any) => Record<string, unknown>,\n ) => Promise<void>,\n ): Promise<PendingOperation | null> {\n const { tableName, trackedTableName, serializeValue } =\n this.getMutationCollectionMeta(mutation)\n\n await handler(sanitizeSQL`${tableName}`, mutation, serializeValue)\n\n if (!waitForCompletion) {\n return null\n }\n\n // Need to get the operation in order to wait for it\n const diffOperation = await context.get<{ id: string; timestamp: string }>(\n sanitizeSQL`SELECT id, timestamp FROM ${trackedTableName} ORDER BY timestamp DESC LIMIT 1`,\n )\n return {\n tableName,\n id: diffOperation.id,\n operation: mapOperationToPowerSync(mutation.type),\n timestamp: diffOperation.timestamp,\n }\n }\n\n protected getMutationCollectionMeta(\n mutation: PendingMutation<any>,\n ): PowerSyncCollectionMeta<any> {\n if (\n typeof (mutation.collection.config as any).utils?.getMeta != `function`\n ) {\n throw new Error(`Collection is not a PowerSync collection.`)\n }\n return (\n mutation.collection\n .config as unknown as EnhancedPowerSyncCollectionConfig<any>\n ).utils.getMeta()\n }\n\n /**\n * Processes collection mutation metadata for persistence to the database.\n * We only support storing string metadata.\n * @returns null if no metadata should be stored.\n */\n protected processMutationMetadata(\n mutation: PendingMutation<any>,\n ): string | null {\n const { metadataIsTracked } = this.getMutationCollectionMeta(mutation)\n if (!metadataIsTracked) {\n // If it's not supported, we don't store metadata.\n if (typeof mutation.metadata != `undefined`) {\n // Log a warning if metadata is provided but not tracked.\n this.database.logger.warn(\n `Metadata provided for collection ${mutation.collection.id} but the PowerSync table does not track metadata. The PowerSync table should be configured with trackMetadata: true.`,\n mutation.metadata,\n )\n }\n return null\n } else if (typeof mutation.metadata == `undefined`) {\n return null\n } else if (typeof mutation.metadata == `string`) {\n return mutation.metadata\n } else {\n return JSON.stringify(mutation.metadata)\n }\n }\n}\n"],"names":["mutation"],"mappings":"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"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ColumnType } from "@powersync/common";
|
|
2
2
|
function serializeForSQLite(value, tableSchema, customSerializer = {}) {
|
|
3
3
|
return Object.fromEntries(
|
|
4
|
+
// eslint-disable-next-line no-shadow
|
|
4
5
|
Object.entries(value).map(([key, value2]) => {
|
|
5
6
|
const outputType = key == `id` ? ColumnType.TEXT : tableSchema.columns.find((column) => column.name == key)?.type;
|
|
6
7
|
if (!outputType) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serialization.js","sources":["../../src/serialization.ts"],"sourcesContent":["import { ColumnType } from '@powersync/common'\nimport type { Table } from '@powersync/common'\nimport type { CustomSQLiteSerializer } from './definitions'\nimport type {\n ExtractedTable,\n ExtractedTableColumns,\n MapBaseColumnType,\n} from './helpers'\n\n/**\n * Serializes an object for persistence to a SQLite table, mapping its values to appropriate SQLite types.\n *\n * This function takes an object representing a row, a table schema, and an optional custom serializer map.\n * It returns a new object with values transformed to be compatible with SQLite column types.\n *\n * ## Generics\n * - `TOutput`: The shape of the input object, typically matching the row data.\n * - `TTable`: The table schema, which must match the keys of `TOutput`.\n *\n * ## Parameters\n * - `value`: The object to serialize (row data).\n * - `tableSchema`: The schema describing the SQLite table columns and types.\n * - `customSerializer`: An optional map of custom serialization functions for specific keys.\n *\n * ## Behavior\n * - For each key in `value`, finds the corresponding column in `tableSchema`.\n * - If a custom serializer is provided for a key, it is used to transform the value.\n * - Otherwise, values are mapped according to the column type:\n * - `TEXT`: Strings are passed through; Dates are converted to ISO strings; other types are JSON-stringified.\n * - `INTEGER`/`REAL`: Numbers are passed through; booleans are mapped to 1/0; other types are coerced to numbers.\n * - Throws if a column type is unknown or a value cannot be converted.\n *\n * ## Returns\n * - An object with the same keys as `value`, with values transformed for SQLite compatibility.\n *\n * ## Errors\n * - Throws if a key in `value` does not exist in the schema.\n * - Throws if a value cannot be converted to the required SQLite type.\n */\nexport function serializeForSQLite<\n TOutput extends Record<string, unknown>,\n // The keys should match\n TTable extends Table<MapBaseColumnType<TOutput>> = Table<\n MapBaseColumnType<TOutput>\n >,\n>(\n value: TOutput,\n tableSchema: TTable,\n customSerializer: Partial<\n CustomSQLiteSerializer<TOutput, ExtractedTableColumns<TTable>>\n > = {},\n): ExtractedTable<TTable> {\n return Object.fromEntries(\n Object.entries(value).map(([key, value]) => {\n // First get the output schema type\n const outputType =\n key == `id`\n ? ColumnType.TEXT\n : tableSchema.columns.find((column) => column.name == key)?.type\n if (!outputType) {\n throw new Error(`Could not find schema for ${key} column.`)\n }\n\n if (value == null) {\n return [key, value]\n }\n\n const customTransform = customSerializer[key]\n if (customTransform) {\n return [key, customTransform(value as TOutput[string])]\n }\n\n // Map to the output\n switch (outputType) {\n case ColumnType.TEXT:\n if (typeof value == `string`) {\n return [key, value]\n } else if (value instanceof Date) {\n return [key, value.toISOString()]\n } else {\n return [key, JSON.stringify(value)]\n }\n case ColumnType.INTEGER:\n case ColumnType.REAL:\n if (typeof value == `number`) {\n return [key, value]\n } else if (typeof value == `boolean`) {\n return [key, value ? 1 : 0]\n } else {\n const numberValue = Number(value)\n if (isNaN(numberValue)) {\n throw new Error(\n `Could not convert ${key}=${value} to a number for SQLite`,\n )\n }\n return [key, numberValue]\n }\n }\n }),\n )\n}\n"],"names":["value"],"mappings":";AAuCO,SAAS,mBAOd,OACA,aACA,mBAEI,CAAA,GACoB;AACxB,SAAO,OAAO;AAAA,
|
|
1
|
+
{"version":3,"file":"serialization.js","sources":["../../src/serialization.ts"],"sourcesContent":["import { ColumnType } from '@powersync/common'\nimport type { Table } from '@powersync/common'\nimport type { CustomSQLiteSerializer } from './definitions'\nimport type {\n ExtractedTable,\n ExtractedTableColumns,\n MapBaseColumnType,\n} from './helpers'\n\n/**\n * Serializes an object for persistence to a SQLite table, mapping its values to appropriate SQLite types.\n *\n * This function takes an object representing a row, a table schema, and an optional custom serializer map.\n * It returns a new object with values transformed to be compatible with SQLite column types.\n *\n * ## Generics\n * - `TOutput`: The shape of the input object, typically matching the row data.\n * - `TTable`: The table schema, which must match the keys of `TOutput`.\n *\n * ## Parameters\n * - `value`: The object to serialize (row data).\n * - `tableSchema`: The schema describing the SQLite table columns and types.\n * - `customSerializer`: An optional map of custom serialization functions for specific keys.\n *\n * ## Behavior\n * - For each key in `value`, finds the corresponding column in `tableSchema`.\n * - If a custom serializer is provided for a key, it is used to transform the value.\n * - Otherwise, values are mapped according to the column type:\n * - `TEXT`: Strings are passed through; Dates are converted to ISO strings; other types are JSON-stringified.\n * - `INTEGER`/`REAL`: Numbers are passed through; booleans are mapped to 1/0; other types are coerced to numbers.\n * - Throws if a column type is unknown or a value cannot be converted.\n *\n * ## Returns\n * - An object with the same keys as `value`, with values transformed for SQLite compatibility.\n *\n * ## Errors\n * - Throws if a key in `value` does not exist in the schema.\n * - Throws if a value cannot be converted to the required SQLite type.\n */\nexport function serializeForSQLite<\n TOutput extends Record<string, unknown>,\n // The keys should match\n TTable extends Table<MapBaseColumnType<TOutput>> = Table<\n MapBaseColumnType<TOutput>\n >,\n>(\n value: TOutput,\n tableSchema: TTable,\n customSerializer: Partial<\n CustomSQLiteSerializer<TOutput, ExtractedTableColumns<TTable>>\n > = {},\n): ExtractedTable<TTable> {\n return Object.fromEntries(\n // eslint-disable-next-line no-shadow\n Object.entries(value).map(([key, value]) => {\n // First get the output schema type\n const outputType =\n key == `id`\n ? ColumnType.TEXT\n : tableSchema.columns.find((column) => column.name == key)?.type\n if (!outputType) {\n throw new Error(`Could not find schema for ${key} column.`)\n }\n\n if (value == null) {\n return [key, value]\n }\n\n const customTransform = customSerializer[key]\n if (customTransform) {\n return [key, customTransform(value as TOutput[string])]\n }\n\n // Map to the output\n switch (outputType) {\n case ColumnType.TEXT:\n if (typeof value == `string`) {\n return [key, value]\n } else if (value instanceof Date) {\n return [key, value.toISOString()]\n } else {\n return [key, JSON.stringify(value)]\n }\n case ColumnType.INTEGER:\n case ColumnType.REAL:\n if (typeof value == `number`) {\n return [key, value]\n } else if (typeof value == `boolean`) {\n return [key, value ? 1 : 0]\n } else {\n const numberValue = Number(value)\n if (isNaN(numberValue)) {\n throw new Error(\n `Could not convert ${key}=${value} to a number for SQLite`,\n )\n }\n return [key, numberValue]\n }\n }\n }),\n )\n}\n"],"names":["value"],"mappings":";AAuCO,SAAS,mBAOd,OACA,aACA,mBAEI,CAAA,GACoB;AACxB,SAAO,OAAO;AAAA;AAAA,IAEZ,OAAO,QAAQ,KAAK,EAAE,IAAI,CAAC,CAAC,KAAKA,MAAK,MAAM;AAE1C,YAAM,aACJ,OAAO,OACH,WAAW,OACX,YAAY,QAAQ,KAAK,CAAC,WAAW,OAAO,QAAQ,GAAG,GAAG;AAChE,UAAI,CAAC,YAAY;AACf,cAAM,IAAI,MAAM,6BAA6B,GAAG,UAAU;AAAA,MAC5D;AAEA,UAAIA,UAAS,MAAM;AACjB,eAAO,CAAC,KAAKA,MAAK;AAAA,MACpB;AAEA,YAAM,kBAAkB,iBAAiB,GAAG;AAC5C,UAAI,iBAAiB;AACnB,eAAO,CAAC,KAAK,gBAAgBA,MAAwB,CAAC;AAAA,MACxD;AAGA,cAAQ,YAAA;AAAA,QACN,KAAK,WAAW;AACd,cAAI,OAAOA,UAAS,UAAU;AAC5B,mBAAO,CAAC,KAAKA,MAAK;AAAA,UACpB,WAAWA,kBAAiB,MAAM;AAChC,mBAAO,CAAC,KAAKA,OAAM,aAAa;AAAA,UAClC,OAAO;AACL,mBAAO,CAAC,KAAK,KAAK,UAAUA,MAAK,CAAC;AAAA,UACpC;AAAA,QACF,KAAK,WAAW;AAAA,QAChB,KAAK,WAAW;AACd,cAAI,OAAOA,UAAS,UAAU;AAC5B,mBAAO,CAAC,KAAKA,MAAK;AAAA,UACpB,WAAW,OAAOA,UAAS,WAAW;AACpC,mBAAO,CAAC,KAAKA,SAAQ,IAAI,CAAC;AAAA,UAC5B,OAAO;AACL,kBAAM,cAAc,OAAOA,MAAK;AAChC,gBAAI,MAAM,WAAW,GAAG;AACtB,oBAAM,IAAI;AAAA,gBACR,qBAAqB,GAAG,IAAIA,MAAK;AAAA,cAAA;AAAA,YAErC;AACA,mBAAO,CAAC,KAAK,WAAW;AAAA,UAC1B;AAAA,MAAA;AAAA,IAEN,CAAC;AAAA,EAAA;AAEL;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/powersync-db-collection",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.37",
|
|
4
4
|
"description": "PowerSync collection for TanStack DB",
|
|
5
5
|
"author": "POWERSYNC",
|
|
6
6
|
"license": "MIT",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"@tanstack/store": "^0.8.0",
|
|
51
51
|
"debug": "^4.4.3",
|
|
52
52
|
"p-defer": "^4.0.1",
|
|
53
|
-
"@tanstack/db": "0.5.
|
|
53
|
+
"@tanstack/db": "0.5.33"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
56
|
"@powersync/common": "^1.41.0"
|
|
@@ -160,6 +160,7 @@ export class PowerSyncTransactor {
|
|
|
160
160
|
mutation,
|
|
161
161
|
context,
|
|
162
162
|
waitForCompletion,
|
|
163
|
+
// eslint-disable-next-line no-shadow
|
|
163
164
|
async (tableName, mutation, serializeValue) => {
|
|
164
165
|
const values = serializeValue(mutation.modified)
|
|
165
166
|
const keys = Object.keys(values).map((key) => sanitizeSQL`${key}`)
|
|
@@ -173,9 +174,9 @@ export class PowerSyncTransactor {
|
|
|
173
174
|
|
|
174
175
|
await context.execute(
|
|
175
176
|
`
|
|
176
|
-
INSERT into ${tableName}
|
|
177
|
-
(${keys.join(`, `)})
|
|
178
|
-
VALUES
|
|
177
|
+
INSERT into ${tableName}
|
|
178
|
+
(${keys.join(`, `)})
|
|
179
|
+
VALUES
|
|
179
180
|
(${keys.map((_) => `?`).join(`, `)})
|
|
180
181
|
`,
|
|
181
182
|
queryParameters,
|
|
@@ -195,6 +196,7 @@ export class PowerSyncTransactor {
|
|
|
195
196
|
mutation,
|
|
196
197
|
context,
|
|
197
198
|
waitForCompletion,
|
|
199
|
+
// eslint-disable-next-line no-shadow
|
|
198
200
|
async (tableName, mutation, serializeValue) => {
|
|
199
201
|
const values = serializeValue(mutation.modified)
|
|
200
202
|
const keys = Object.keys(values).map((key) => sanitizeSQL`${key}`)
|
|
@@ -208,7 +210,7 @@ export class PowerSyncTransactor {
|
|
|
208
210
|
|
|
209
211
|
await context.execute(
|
|
210
212
|
`
|
|
211
|
-
UPDATE ${tableName}
|
|
213
|
+
UPDATE ${tableName}
|
|
212
214
|
SET ${keys.map((key) => `${key} = ?`).join(`, `)}
|
|
213
215
|
WHERE id = ?
|
|
214
216
|
`,
|
|
@@ -229,6 +231,7 @@ export class PowerSyncTransactor {
|
|
|
229
231
|
mutation,
|
|
230
232
|
context,
|
|
231
233
|
waitForCompletion,
|
|
234
|
+
// eslint-disable-next-line no-shadow
|
|
232
235
|
async (tableName, mutation) => {
|
|
233
236
|
const metadataValue = this.processMutationMetadata(mutation)
|
|
234
237
|
if (metadataValue != null) {
|
package/src/serialization.ts
CHANGED