@powersync/common 1.41.0 → 1.42.0

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.
Files changed (183) hide show
  1. package/dist/bundle.cjs +10820 -22
  2. package/dist/bundle.cjs.map +1 -0
  3. package/dist/bundle.mjs +10741 -22
  4. package/dist/bundle.mjs.map +1 -0
  5. package/dist/bundle.node.cjs +10820 -0
  6. package/dist/bundle.node.cjs.map +1 -0
  7. package/dist/bundle.node.mjs +10741 -0
  8. package/dist/bundle.node.mjs.map +1 -0
  9. package/dist/index.d.cts +77 -13
  10. package/lib/client/AbstractPowerSyncDatabase.js +1 -0
  11. package/lib/client/AbstractPowerSyncDatabase.js.map +1 -0
  12. package/lib/client/AbstractPowerSyncOpenFactory.js +1 -0
  13. package/lib/client/AbstractPowerSyncOpenFactory.js.map +1 -0
  14. package/lib/client/ConnectionManager.js +1 -0
  15. package/lib/client/ConnectionManager.js.map +1 -0
  16. package/lib/client/CustomQuery.js +1 -0
  17. package/lib/client/CustomQuery.js.map +1 -0
  18. package/lib/client/Query.js +1 -0
  19. package/lib/client/Query.js.map +1 -0
  20. package/lib/client/SQLOpenFactory.js +1 -0
  21. package/lib/client/SQLOpenFactory.js.map +1 -0
  22. package/lib/client/compilableQueryWatch.js +1 -0
  23. package/lib/client/compilableQueryWatch.js.map +1 -0
  24. package/lib/client/connection/PowerSyncBackendConnector.js +1 -0
  25. package/lib/client/connection/PowerSyncBackendConnector.js.map +1 -0
  26. package/lib/client/connection/PowerSyncCredentials.js +1 -0
  27. package/lib/client/connection/PowerSyncCredentials.js.map +1 -0
  28. package/lib/client/constants.js +1 -0
  29. package/lib/client/constants.js.map +1 -0
  30. package/lib/client/runOnSchemaChange.js +1 -0
  31. package/lib/client/runOnSchemaChange.js.map +1 -0
  32. package/lib/client/sync/bucket/BucketStorageAdapter.js +1 -0
  33. package/lib/client/sync/bucket/BucketStorageAdapter.js.map +1 -0
  34. package/lib/client/sync/bucket/CrudBatch.js +1 -0
  35. package/lib/client/sync/bucket/CrudBatch.js.map +1 -0
  36. package/lib/client/sync/bucket/CrudEntry.js +1 -0
  37. package/lib/client/sync/bucket/CrudEntry.js.map +1 -0
  38. package/lib/client/sync/bucket/CrudTransaction.js +1 -0
  39. package/lib/client/sync/bucket/CrudTransaction.js.map +1 -0
  40. package/lib/client/sync/bucket/OpType.js +1 -0
  41. package/lib/client/sync/bucket/OpType.js.map +1 -0
  42. package/lib/client/sync/bucket/OplogEntry.js +1 -0
  43. package/lib/client/sync/bucket/OplogEntry.js.map +1 -0
  44. package/lib/client/sync/bucket/SqliteBucketStorage.js +1 -0
  45. package/lib/client/sync/bucket/SqliteBucketStorage.js.map +1 -0
  46. package/lib/client/sync/bucket/SyncDataBatch.js +1 -0
  47. package/lib/client/sync/bucket/SyncDataBatch.js.map +1 -0
  48. package/lib/client/sync/bucket/SyncDataBucket.js +1 -0
  49. package/lib/client/sync/bucket/SyncDataBucket.js.map +1 -0
  50. package/lib/client/sync/stream/AbstractRemote.d.ts +5 -0
  51. package/lib/client/sync/stream/AbstractRemote.js +19 -6
  52. package/lib/client/sync/stream/AbstractRemote.js.map +1 -0
  53. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +1 -0
  54. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js.map +1 -0
  55. package/lib/client/sync/stream/WebsocketClientTransport.js +1 -0
  56. package/lib/client/sync/stream/WebsocketClientTransport.js.map +1 -0
  57. package/lib/client/sync/stream/core-instruction.js +1 -0
  58. package/lib/client/sync/stream/core-instruction.js.map +1 -0
  59. package/lib/client/sync/stream/streaming-sync-types.js +1 -0
  60. package/lib/client/sync/stream/streaming-sync-types.js.map +1 -0
  61. package/lib/client/sync/sync-streams.js +1 -0
  62. package/lib/client/sync/sync-streams.js.map +1 -0
  63. package/lib/client/triggers/TriggerManager.d.ts +71 -12
  64. package/lib/client/triggers/TriggerManager.js +1 -0
  65. package/lib/client/triggers/TriggerManager.js.map +1 -0
  66. package/lib/client/triggers/TriggerManagerImpl.js +11 -5
  67. package/lib/client/triggers/TriggerManagerImpl.js.map +1 -0
  68. package/lib/client/triggers/sanitizeSQL.js +1 -0
  69. package/lib/client/triggers/sanitizeSQL.js.map +1 -0
  70. package/lib/client/watched/GetAllQuery.js +1 -0
  71. package/lib/client/watched/GetAllQuery.js.map +1 -0
  72. package/lib/client/watched/WatchedQuery.js +1 -0
  73. package/lib/client/watched/WatchedQuery.js.map +1 -0
  74. package/lib/client/watched/processors/AbstractQueryProcessor.js +1 -0
  75. package/lib/client/watched/processors/AbstractQueryProcessor.js.map +1 -0
  76. package/lib/client/watched/processors/DifferentialQueryProcessor.js +1 -0
  77. package/lib/client/watched/processors/DifferentialQueryProcessor.js.map +1 -0
  78. package/lib/client/watched/processors/OnChangeQueryProcessor.js +1 -0
  79. package/lib/client/watched/processors/OnChangeQueryProcessor.js.map +1 -0
  80. package/lib/client/watched/processors/comparators.js +1 -0
  81. package/lib/client/watched/processors/comparators.js.map +1 -0
  82. package/lib/db/DBAdapter.js +1 -0
  83. package/lib/db/DBAdapter.js.map +1 -0
  84. package/lib/db/crud/SyncProgress.js +1 -0
  85. package/lib/db/crud/SyncProgress.js.map +1 -0
  86. package/lib/db/crud/SyncStatus.js +1 -0
  87. package/lib/db/crud/SyncStatus.js.map +1 -0
  88. package/lib/db/crud/UploadQueueStatus.js +1 -0
  89. package/lib/db/crud/UploadQueueStatus.js.map +1 -0
  90. package/lib/db/schema/Column.js +1 -0
  91. package/lib/db/schema/Column.js.map +1 -0
  92. package/lib/db/schema/Index.js +1 -0
  93. package/lib/db/schema/Index.js.map +1 -0
  94. package/lib/db/schema/IndexedColumn.js +1 -0
  95. package/lib/db/schema/IndexedColumn.js.map +1 -0
  96. package/lib/db/schema/RawTable.js +1 -0
  97. package/lib/db/schema/RawTable.js.map +1 -0
  98. package/lib/db/schema/Schema.js +1 -0
  99. package/lib/db/schema/Schema.js.map +1 -0
  100. package/lib/db/schema/Table.js +1 -0
  101. package/lib/db/schema/Table.js.map +1 -0
  102. package/lib/db/schema/TableV2.js +1 -0
  103. package/lib/db/schema/TableV2.js.map +1 -0
  104. package/lib/index.js +1 -0
  105. package/lib/index.js.map +1 -0
  106. package/lib/types/types.js +1 -0
  107. package/lib/types/types.js.map +1 -0
  108. package/lib/utils/AbortOperation.js +1 -0
  109. package/lib/utils/AbortOperation.js.map +1 -0
  110. package/lib/utils/BaseObserver.js +1 -0
  111. package/lib/utils/BaseObserver.js.map +1 -0
  112. package/lib/utils/ControlledExecutor.js +1 -0
  113. package/lib/utils/ControlledExecutor.js.map +1 -0
  114. package/lib/utils/DataStream.js +1 -0
  115. package/lib/utils/DataStream.js.map +1 -0
  116. package/lib/utils/Logger.js +1 -0
  117. package/lib/utils/Logger.js.map +1 -0
  118. package/lib/utils/MetaBaseObserver.js +1 -0
  119. package/lib/utils/MetaBaseObserver.js.map +1 -0
  120. package/lib/utils/async.js +1 -0
  121. package/lib/utils/async.js.map +1 -0
  122. package/lib/utils/mutex.js +1 -0
  123. package/lib/utils/mutex.js.map +1 -0
  124. package/lib/utils/parseQuery.js +1 -0
  125. package/lib/utils/parseQuery.js.map +1 -0
  126. package/package.json +23 -15
  127. package/src/client/AbstractPowerSyncDatabase.ts +1343 -0
  128. package/src/client/AbstractPowerSyncOpenFactory.ts +39 -0
  129. package/src/client/ConnectionManager.ts +402 -0
  130. package/src/client/CustomQuery.ts +56 -0
  131. package/src/client/Query.ts +106 -0
  132. package/src/client/SQLOpenFactory.ts +55 -0
  133. package/src/client/compilableQueryWatch.ts +55 -0
  134. package/src/client/connection/PowerSyncBackendConnector.ts +25 -0
  135. package/src/client/connection/PowerSyncCredentials.ts +5 -0
  136. package/src/client/constants.ts +1 -0
  137. package/src/client/runOnSchemaChange.ts +31 -0
  138. package/src/client/sync/bucket/BucketStorageAdapter.ts +118 -0
  139. package/src/client/sync/bucket/CrudBatch.ts +21 -0
  140. package/src/client/sync/bucket/CrudEntry.ts +172 -0
  141. package/src/client/sync/bucket/CrudTransaction.ts +21 -0
  142. package/src/client/sync/bucket/OpType.ts +23 -0
  143. package/src/client/sync/bucket/OplogEntry.ts +50 -0
  144. package/src/client/sync/bucket/SqliteBucketStorage.ts +395 -0
  145. package/src/client/sync/bucket/SyncDataBatch.ts +11 -0
  146. package/src/client/sync/bucket/SyncDataBucket.ts +49 -0
  147. package/src/client/sync/stream/AbstractRemote.ts +636 -0
  148. package/src/client/sync/stream/AbstractStreamingSyncImplementation.ts +1258 -0
  149. package/src/client/sync/stream/WebsocketClientTransport.ts +80 -0
  150. package/src/client/sync/stream/core-instruction.ts +99 -0
  151. package/src/client/sync/stream/streaming-sync-types.ts +205 -0
  152. package/src/client/sync/sync-streams.ts +107 -0
  153. package/src/client/triggers/TriggerManager.ts +451 -0
  154. package/src/client/triggers/TriggerManagerImpl.ts +320 -0
  155. package/src/client/triggers/sanitizeSQL.ts +66 -0
  156. package/src/client/watched/GetAllQuery.ts +46 -0
  157. package/src/client/watched/WatchedQuery.ts +121 -0
  158. package/src/client/watched/processors/AbstractQueryProcessor.ts +226 -0
  159. package/src/client/watched/processors/DifferentialQueryProcessor.ts +305 -0
  160. package/src/client/watched/processors/OnChangeQueryProcessor.ts +122 -0
  161. package/src/client/watched/processors/comparators.ts +57 -0
  162. package/src/db/DBAdapter.ts +134 -0
  163. package/src/db/crud/SyncProgress.ts +100 -0
  164. package/src/db/crud/SyncStatus.ts +308 -0
  165. package/src/db/crud/UploadQueueStatus.ts +20 -0
  166. package/src/db/schema/Column.ts +60 -0
  167. package/src/db/schema/Index.ts +39 -0
  168. package/src/db/schema/IndexedColumn.ts +42 -0
  169. package/src/db/schema/RawTable.ts +67 -0
  170. package/src/db/schema/Schema.ts +76 -0
  171. package/src/db/schema/Table.ts +359 -0
  172. package/src/db/schema/TableV2.ts +9 -0
  173. package/src/index.ts +52 -0
  174. package/src/types/types.ts +9 -0
  175. package/src/utils/AbortOperation.ts +17 -0
  176. package/src/utils/BaseObserver.ts +41 -0
  177. package/src/utils/ControlledExecutor.ts +72 -0
  178. package/src/utils/DataStream.ts +211 -0
  179. package/src/utils/Logger.ts +47 -0
  180. package/src/utils/MetaBaseObserver.ts +81 -0
  181. package/src/utils/async.ts +61 -0
  182. package/src/utils/mutex.ts +34 -0
  183. package/src/utils/parseQuery.ts +25 -0
@@ -0,0 +1,320 @@
1
+ import { LockContext } from '../../db/DBAdapter.js';
2
+ import { Schema } from '../../db/schema/Schema.js';
3
+ import { type AbstractPowerSyncDatabase } from '../AbstractPowerSyncDatabase.js';
4
+ import { DEFAULT_WATCH_THROTTLE_MS } from '../watched/WatchedQuery.js';
5
+ import {
6
+ CreateDiffTriggerOptions,
7
+ DiffTriggerOperation,
8
+ TrackDiffOptions,
9
+ TriggerManager,
10
+ TriggerRemoveCallback,
11
+ WithDiffOptions
12
+ } from './TriggerManager.js';
13
+
14
+ export type TriggerManagerImplOptions = {
15
+ db: AbstractPowerSyncDatabase;
16
+ schema: Schema;
17
+ };
18
+
19
+ export class TriggerManagerImpl implements TriggerManager {
20
+ protected schema: Schema;
21
+
22
+ constructor(protected options: TriggerManagerImplOptions) {
23
+ this.schema = options.schema;
24
+ options.db.registerListener({
25
+ schemaChanged: (schema) => {
26
+ this.schema = schema;
27
+ }
28
+ });
29
+ }
30
+
31
+ protected get db() {
32
+ return this.options.db;
33
+ }
34
+
35
+ protected async getUUID() {
36
+ const { id: uuid } = await this.db.get<{ id: string }>(/* sql */ `
37
+ SELECT
38
+ uuid () as id
39
+ `);
40
+
41
+ // Replace dashes with underscores for SQLite table/trigger name compatibility
42
+ return uuid.replace(/-/g, '_');
43
+ }
44
+
45
+ protected async removeTriggers(tx: LockContext, triggerIds: string[]) {
46
+ for (const triggerId of triggerIds) {
47
+ await tx.execute(/* sql */ `DROP TRIGGER IF EXISTS ${triggerId}; `);
48
+ }
49
+ }
50
+
51
+ async createDiffTrigger(options: CreateDiffTriggerOptions) {
52
+ await this.db.waitForReady();
53
+ const { source, destination, columns, when, hooks } = options;
54
+ const operations = Object.keys(when) as DiffTriggerOperation[];
55
+ if (operations.length == 0) {
56
+ throw new Error('At least one WHEN operation must be specified for the trigger.');
57
+ }
58
+
59
+ const whenClauses = Object.fromEntries(
60
+ Object.entries(when).map(([operation, filter]) => [operation, `WHEN ${filter}`])
61
+ );
62
+
63
+ /**
64
+ * Allow specifying the View name as the source.
65
+ * We can lookup the internal table name from the schema.
66
+ */
67
+ const sourceDefinition = this.schema.tables.find((table) => table.viewName == source);
68
+ if (!sourceDefinition) {
69
+ throw new Error(`Source table or view "${source}" not found in the schema.`);
70
+ }
71
+
72
+ const replicatedColumns = columns ?? sourceDefinition.columns.map((col) => col.name);
73
+
74
+ const internalSource = sourceDefinition.internalName;
75
+ const triggerIds: string[] = [];
76
+
77
+ const id = await this.getUUID();
78
+
79
+ /**
80
+ * We default to replicating all columns if no columns array is provided.
81
+ */
82
+ const jsonFragment = (source: 'NEW' | 'OLD' = 'NEW') => {
83
+ if (columns == null) {
84
+ // Track all columns
85
+ return `${source}.data`;
86
+ } else if (columns.length == 0) {
87
+ // Don't track any columns except for the id
88
+ return `'{}'`;
89
+ } else {
90
+ // Filter the data by the replicated columns
91
+ return `json_object(${replicatedColumns.map((col) => `'${col}', json_extract(${source}.data, '$.${col}')`).join(', ')})`;
92
+ }
93
+ };
94
+
95
+ const disposeWarningListener = this.db.registerListener({
96
+ schemaChanged: () => {
97
+ this.db.logger.warn(
98
+ `The PowerSync schema has changed while previously configured triggers are still operational. This might cause unexpected results.`
99
+ );
100
+ }
101
+ });
102
+
103
+ /**
104
+ * Declare the cleanup function early since if any of the init steps fail,
105
+ * we need to ensure we can cleanup the created resources.
106
+ * We unfortunately cannot rely on transaction rollback.
107
+ */
108
+ const cleanup = async () => {
109
+ disposeWarningListener();
110
+ return this.db.writeLock(async (tx) => {
111
+ await this.removeTriggers(tx, triggerIds);
112
+ await tx.execute(/* sql */ `DROP TABLE IF EXISTS ${destination};`);
113
+ });
114
+ };
115
+
116
+ const setup = async (tx: LockContext) => {
117
+ // Allow user code to execute in this lock context before the trigger is created.
118
+ await hooks?.beforeCreate?.(tx);
119
+ await tx.execute(/* sql */ `
120
+ CREATE TEMP TABLE ${destination} (
121
+ operation_id INTEGER PRIMARY KEY AUTOINCREMENT,
122
+ id TEXT,
123
+ operation TEXT,
124
+ timestamp TEXT,
125
+ value TEXT,
126
+ previous_value TEXT
127
+ );
128
+ `);
129
+
130
+ if (operations.includes(DiffTriggerOperation.INSERT)) {
131
+ const insertTriggerId = `ps_temp_trigger_insert_${id}`;
132
+ triggerIds.push(insertTriggerId);
133
+
134
+ await tx.execute(/* sql */ `
135
+ CREATE TEMP TRIGGER ${insertTriggerId} AFTER INSERT ON ${internalSource} ${whenClauses[
136
+ DiffTriggerOperation.INSERT
137
+ ]} BEGIN
138
+ INSERT INTO
139
+ ${destination} (id, operation, timestamp, value)
140
+ VALUES
141
+ (
142
+ NEW.id,
143
+ 'INSERT',
144
+ strftime ('%Y-%m-%dT%H:%M:%fZ', 'now'),
145
+ ${jsonFragment('NEW')}
146
+ );
147
+
148
+ END;
149
+ `);
150
+ }
151
+
152
+ if (operations.includes(DiffTriggerOperation.UPDATE)) {
153
+ const updateTriggerId = `ps_temp_trigger_update_${id}`;
154
+ triggerIds.push(updateTriggerId);
155
+
156
+ await tx.execute(/* sql */ `
157
+ CREATE TEMP TRIGGER ${updateTriggerId} AFTER
158
+ UPDATE ON ${internalSource} ${whenClauses[DiffTriggerOperation.UPDATE]} BEGIN
159
+ INSERT INTO
160
+ ${destination} (id, operation, timestamp, value, previous_value)
161
+ VALUES
162
+ (
163
+ NEW.id,
164
+ 'UPDATE',
165
+ strftime ('%Y-%m-%dT%H:%M:%fZ', 'now'),
166
+ ${jsonFragment('NEW')},
167
+ ${jsonFragment('OLD')}
168
+ );
169
+
170
+ END;
171
+ `);
172
+ }
173
+
174
+ if (operations.includes(DiffTriggerOperation.DELETE)) {
175
+ const deleteTriggerId = `ps_temp_trigger_delete_${id}`;
176
+ triggerIds.push(deleteTriggerId);
177
+
178
+ // Create delete trigger for basic JSON
179
+ await tx.execute(/* sql */ `
180
+ CREATE TEMP TRIGGER ${deleteTriggerId} AFTER DELETE ON ${internalSource} ${whenClauses[
181
+ DiffTriggerOperation.DELETE
182
+ ]} BEGIN
183
+ INSERT INTO
184
+ ${destination} (id, operation, timestamp, value)
185
+ VALUES
186
+ (
187
+ OLD.id,
188
+ 'DELETE',
189
+ strftime ('%Y-%m-%dT%H:%M:%fZ', 'now'),
190
+ ${jsonFragment('OLD')}
191
+ );
192
+
193
+ END;
194
+ `);
195
+ }
196
+ };
197
+
198
+ try {
199
+ await this.db.writeLock(setup);
200
+ return cleanup;
201
+ } catch (error) {
202
+ try {
203
+ await cleanup();
204
+ } catch (cleanupError) {
205
+ throw new AggregateError([error, cleanupError], 'Error during operation and cleanup');
206
+ }
207
+ throw error;
208
+ }
209
+ }
210
+
211
+ async trackTableDiff(options: TrackDiffOptions): Promise<TriggerRemoveCallback> {
212
+ const { source, when, columns, hooks, throttleMs = DEFAULT_WATCH_THROTTLE_MS } = options;
213
+
214
+ await this.db.waitForReady();
215
+
216
+ /**
217
+ * Allow specifying the View name as the source.
218
+ * We can lookup the internal table name from the schema.
219
+ */
220
+ const sourceDefinition = this.schema.tables.find((table) => table.viewName == source);
221
+ if (!sourceDefinition) {
222
+ throw new Error(`Source table or view "${source}" not found in the schema.`);
223
+ }
224
+
225
+ // The columns to present in the onChange context methods.
226
+ // If no array is provided, we use all columns from the source table.
227
+ const contextColumns = columns ?? sourceDefinition.columns.map((col) => col.name);
228
+
229
+ const id = await this.getUUID();
230
+ const destination = `ps_temp_track_${source}_${id}`;
231
+
232
+ // register an onChange before the trigger is created
233
+ const abortController = new AbortController();
234
+ const abortOnChange = () => abortController.abort();
235
+ this.db.onChange(
236
+ {
237
+ // Note that the onChange events here have their execution scheduled.
238
+ // Callbacks are throttled and are sequential.
239
+ onChange: async () => {
240
+ if (abortController.signal.aborted) return;
241
+
242
+ // Run the handler in a write lock to keep the state of the
243
+ // destination table consistent.
244
+ await this.db.writeTransaction(async (tx) => {
245
+ const callbackResult = await options.onChange({
246
+ ...tx,
247
+ destinationTable: destination,
248
+ withDiff: async <T>(query, params, options?: WithDiffOptions) => {
249
+ // Wrap the query to expose the destination table
250
+ const operationIdSelect = options?.castOperationIdAsText
251
+ ? 'id, operation, CAST(operation_id AS TEXT) as operation_id, timestamp, value, previous_value'
252
+ : '*';
253
+ const wrappedQuery = /* sql */ `
254
+ WITH
255
+ DIFF AS (
256
+ SELECT
257
+ ${operationIdSelect}
258
+ FROM
259
+ ${destination}
260
+ ORDER BY
261
+ operation_id ASC
262
+ ) ${query}
263
+ `;
264
+ return tx.getAll<T>(wrappedQuery, params);
265
+ },
266
+ withExtractedDiff: async <T>(query, params) => {
267
+ // Wrap the query to expose the destination table
268
+ const wrappedQuery = /* sql */ `
269
+ WITH
270
+ DIFF AS (
271
+ SELECT
272
+ id,
273
+ ${contextColumns.length > 0
274
+ ? `${contextColumns.map((col) => `json_extract(value, '$.${col}') as ${col}`).join(', ')},`
275
+ : ''} operation_id as __operation_id,
276
+ operation as __operation,
277
+ timestamp as __timestamp,
278
+ previous_value as __previous_value
279
+ FROM
280
+ ${destination}
281
+ ORDER BY
282
+ __operation_id ASC
283
+ ) ${query}
284
+ `;
285
+ return tx.getAll<T>(wrappedQuery, params);
286
+ }
287
+ });
288
+
289
+ // Clear the destination table after processing
290
+ await tx.execute(/* sql */ `DELETE FROM ${destination};`);
291
+ return callbackResult;
292
+ });
293
+ }
294
+ },
295
+ { tables: [destination], signal: abortController.signal, throttleMs }
296
+ );
297
+
298
+ try {
299
+ const removeTrigger = await this.createDiffTrigger({
300
+ source,
301
+ destination,
302
+ columns: contextColumns,
303
+ when,
304
+ hooks
305
+ });
306
+
307
+ return async () => {
308
+ abortOnChange();
309
+ await removeTrigger();
310
+ };
311
+ } catch (error) {
312
+ try {
313
+ abortOnChange();
314
+ } catch (cleanupError) {
315
+ throw new AggregateError([error, cleanupError], 'Error during operation and cleanup');
316
+ }
317
+ throw error;
318
+ }
319
+ }
320
+ }
@@ -0,0 +1,66 @@
1
+ function sanitizeString(input: string): string {
2
+ return `'${input.replace(/'/g, "''")}'`;
3
+ }
4
+ /**
5
+ * Helper function for sanitizing UUID input strings.
6
+ * Typically used with {@link sanitizeSQL}.
7
+ */
8
+ export function sanitizeUUID(uuid: string): string {
9
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
10
+ const isValid = uuidRegex.test(uuid);
11
+ if (!isValid) {
12
+ throw new Error(`${uuid} is not a valid UUID`);
13
+ }
14
+ return uuid;
15
+ }
16
+
17
+ /**
18
+ * SQL string template function for {@link TrackDiffOptions#when} and {@link CreateDiffTriggerOptions#when}.
19
+ *
20
+ * This function performs basic string interpolation for SQLite WHEN clauses.
21
+ *
22
+ * **String placeholders:**
23
+ * - All string values passed as placeholders are automatically wrapped in single quotes (`'`).
24
+ * - Do not manually wrap placeholders in single quotes in your template string; the function will handle quoting and escaping for you.
25
+ * - Any single quotes within the string value are escaped by doubling them (`''`), as required by SQL syntax.
26
+ *
27
+ * **Other types:**
28
+ * - `null` and `undefined` are converted to SQL `NULL`.
29
+ * - Objects are stringified using `JSON.stringify()` and wrapped in single quotes, with any single quotes inside the stringified value escaped.
30
+ * - Numbers and other primitive types are inserted directly.
31
+ *
32
+ * **Usage example:**
33
+ * ```typescript
34
+ * const myID = "O'Reilly";
35
+ * const clause = sanitizeSQL`New.id = ${myID}`;
36
+ * // Result: "New.id = 'O''Reilly'"
37
+ * ```
38
+ *
39
+ * Avoid manually quoting placeholders:
40
+ * ```typescript
41
+ * // Incorrect:
42
+ * sanitizeSQL`New.id = '${myID}'` // Produces double quotes: New.id = ''O''Reilly''
43
+ * ```
44
+ */
45
+ export function sanitizeSQL(strings: TemplateStringsArray, ...values: any[]): string {
46
+ let result = '';
47
+ strings.forEach((str, i) => {
48
+ result += str;
49
+ if (i < values.length) {
50
+ // For SQL, escape single quotes in string values
51
+ const value = values[i];
52
+ if (typeof value == 'string') {
53
+ result += sanitizeString(value);
54
+ } else if (value == null) {
55
+ result += 'NULL';
56
+ } else if (typeof value == 'object') {
57
+ // Stringify the object and escape single quotes in the result
58
+ const stringified = JSON.stringify(value);
59
+ result += sanitizeString(stringified);
60
+ } else {
61
+ result += value;
62
+ }
63
+ }
64
+ });
65
+ return result;
66
+ }
@@ -0,0 +1,46 @@
1
+ import { CompiledQuery } from '../../types/types.js';
2
+ import { AbstractPowerSyncDatabase } from '../AbstractPowerSyncDatabase.js';
3
+ import { WatchCompatibleQuery } from './WatchedQuery.js';
4
+
5
+ /**
6
+ * Options for {@link GetAllQuery}.
7
+ */
8
+ export type GetAllQueryOptions<RowType = unknown> = {
9
+ sql: string;
10
+ parameters?: ReadonlyArray<unknown>;
11
+ /**
12
+ * Optional mapper function to convert raw rows into the desired RowType.
13
+ * @example
14
+ * ```javascript
15
+ * (rawRow) => ({
16
+ * id: rawRow.id,
17
+ * created_at: new Date(rawRow.created_at),
18
+ * })
19
+ * ```
20
+ */
21
+ mapper?: (rawRow: Record<string, unknown>) => RowType;
22
+ };
23
+
24
+ /**
25
+ * Performs a {@link AbstractPowerSyncDatabase.getAll} operation for a watched query.
26
+ */
27
+ export class GetAllQuery<RowType = unknown> implements WatchCompatibleQuery<RowType[]> {
28
+ constructor(protected options: GetAllQueryOptions<RowType>) {}
29
+
30
+ compile(): CompiledQuery {
31
+ return {
32
+ sql: this.options.sql,
33
+ parameters: this.options.parameters ?? []
34
+ };
35
+ }
36
+
37
+ async execute(options: { db: AbstractPowerSyncDatabase }): Promise<RowType[]> {
38
+ const { db } = options;
39
+ const { sql, parameters = [] } = this.compile();
40
+ const rawResult = await db.getAll<unknown>(sql, [...parameters]);
41
+ if (this.options.mapper) {
42
+ return rawResult.map(this.options.mapper);
43
+ }
44
+ return rawResult as RowType[];
45
+ }
46
+ }
@@ -0,0 +1,121 @@
1
+ import { CompiledQuery } from '../../types/types.js';
2
+ import { BaseListener } from '../../utils/BaseObserver.js';
3
+ import { MetaBaseObserverInterface } from '../../utils/MetaBaseObserver.js';
4
+ import { AbstractPowerSyncDatabase } from '../AbstractPowerSyncDatabase.js';
5
+
6
+ /**
7
+ * State for {@link WatchedQuery} instances.
8
+ */
9
+ export interface WatchedQueryState<Data> {
10
+ /**
11
+ * Indicates the initial loading state (hard loading).
12
+ * Loading becomes false once the first set of results from the watched query is available or an error occurs.
13
+ */
14
+ readonly isLoading: boolean;
15
+ /**
16
+ * Indicates whether the query is currently fetching data, is true during the initial load
17
+ * and any time when the query is re-evaluating (useful for large queries).
18
+ */
19
+ readonly isFetching: boolean;
20
+ /**
21
+ * The last error that occurred while executing the query.
22
+ */
23
+ readonly error: Error | null;
24
+ /**
25
+ * The last time the query was updated.
26
+ */
27
+ readonly lastUpdated: Date | null;
28
+ /**
29
+ * The last data returned by the query.
30
+ */
31
+ readonly data: Data;
32
+ }
33
+
34
+ /**
35
+ * Options provided to the `execute` method of a {@link WatchCompatibleQuery}.
36
+ */
37
+ export interface WatchExecuteOptions {
38
+ sql: string;
39
+ parameters: any[];
40
+ db: AbstractPowerSyncDatabase;
41
+ }
42
+
43
+ /**
44
+ * Similar to {@link CompatibleQuery}, except the `execute` method
45
+ * does not enforce an Array result type.
46
+ */
47
+ export interface WatchCompatibleQuery<ResultType> {
48
+ execute(options: WatchExecuteOptions): Promise<ResultType>;
49
+ compile(): CompiledQuery;
50
+ }
51
+
52
+ export interface WatchedQueryOptions {
53
+ /** The minimum interval between queries. */
54
+ throttleMs?: number;
55
+ /**
56
+ * If true (default) the watched query will update its state to report
57
+ * on the fetching state of the query.
58
+ * Setting to false reduces the number of state changes if the fetch status
59
+ * is not relevant to the consumer.
60
+ */
61
+ reportFetching?: boolean;
62
+
63
+ /**
64
+ * By default, watched queries requery the database on any change to any dependent table of the query.
65
+ * Supplying an override here can be used to limit the tables which trigger querying the database.
66
+ */
67
+ triggerOnTables?: string[];
68
+ }
69
+
70
+ export enum WatchedQueryListenerEvent {
71
+ ON_DATA = 'onData',
72
+ ON_ERROR = 'onError',
73
+ ON_STATE_CHANGE = 'onStateChange',
74
+ SETTINGS_WILL_UPDATE = 'settingsWillUpdate',
75
+ CLOSED = 'closed'
76
+ }
77
+
78
+ export interface WatchedQueryListener<Data> extends BaseListener {
79
+ [WatchedQueryListenerEvent.ON_DATA]?: (data: Data) => void | Promise<void>;
80
+ [WatchedQueryListenerEvent.ON_ERROR]?: (error: Error) => void | Promise<void>;
81
+ [WatchedQueryListenerEvent.ON_STATE_CHANGE]?: (state: WatchedQueryState<Data>) => void | Promise<void>;
82
+ [WatchedQueryListenerEvent.SETTINGS_WILL_UPDATE]?: () => void;
83
+ [WatchedQueryListenerEvent.CLOSED]?: () => void | Promise<void>;
84
+ }
85
+
86
+ export const DEFAULT_WATCH_THROTTLE_MS = 30;
87
+
88
+ export const DEFAULT_WATCH_QUERY_OPTIONS: WatchedQueryOptions = {
89
+ throttleMs: DEFAULT_WATCH_THROTTLE_MS,
90
+ reportFetching: true
91
+ };
92
+
93
+ export interface WatchedQuery<
94
+ Data = unknown,
95
+ Settings extends WatchedQueryOptions = WatchedQueryOptions,
96
+ Listener extends WatchedQueryListener<Data> = WatchedQueryListener<Data>
97
+ > extends MetaBaseObserverInterface<Listener> {
98
+ /**
99
+ * Current state of the watched query.
100
+ */
101
+ readonly state: WatchedQueryState<Data>;
102
+
103
+ readonly closed: boolean;
104
+
105
+ /**
106
+ * Subscribe to watched query events.
107
+ * @returns A function to unsubscribe from the events.
108
+ */
109
+ registerListener(listener: Listener): () => void;
110
+
111
+ /**
112
+ * Updates the underlying query options.
113
+ * This will trigger a re-evaluation of the query and update the state.
114
+ */
115
+ updateSettings(options: Settings): Promise<void>;
116
+
117
+ /**
118
+ * Close the watched query and end all subscriptions.
119
+ */
120
+ close(): Promise<void>;
121
+ }