@powersync/common 1.36.0 → 1.38.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.
- package/dist/bundle.cjs +3 -3
- package/dist/bundle.mjs +3 -3
- package/dist/index.d.cts +1300 -858
- package/lib/client/AbstractPowerSyncDatabase.d.ts +38 -0
- package/lib/client/AbstractPowerSyncDatabase.js +80 -21
- package/lib/client/sync/stream/AbstractRemote.js +3 -0
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +8 -2
- package/lib/client/triggers/TriggerManager.d.ts +363 -0
- package/lib/client/triggers/TriggerManager.js +10 -0
- package/lib/client/triggers/TriggerManagerImpl.d.ts +18 -0
- package/lib/client/triggers/TriggerManagerImpl.js +265 -0
- package/lib/client/triggers/sanitizeSQL.d.ts +34 -0
- package/lib/client/triggers/sanitizeSQL.js +68 -0
- package/lib/client/watched/processors/AbstractQueryProcessor.js +1 -1
- package/lib/client/watched/processors/DifferentialQueryProcessor.js +4 -1
- package/lib/client/watched/processors/OnChangeQueryProcessor.js +4 -1
- package/lib/db/crud/SyncStatus.d.ts +9 -0
- package/lib/db/crud/SyncStatus.js +9 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +2 -0
- package/lib/utils/async.d.ts +9 -0
- package/lib/utils/async.js +9 -0
- package/package.json +1 -1
|
@@ -13,6 +13,7 @@ import { BucketStorageAdapter } from './sync/bucket/BucketStorageAdapter.js';
|
|
|
13
13
|
import { CrudBatch } from './sync/bucket/CrudBatch.js';
|
|
14
14
|
import { CrudTransaction } from './sync/bucket/CrudTransaction.js';
|
|
15
15
|
import { StreamingSyncImplementation, StreamingSyncImplementationListener, type AdditionalConnectionOptions, type PowerSyncConnectionOptions, type RequiredAdditionalConnectionOptions } from './sync/stream/AbstractStreamingSyncImplementation.js';
|
|
16
|
+
import { TriggerManager } from './triggers/TriggerManager.js';
|
|
16
17
|
import { WatchCompatibleQuery } from './watched/WatchedQuery.js';
|
|
17
18
|
import { WatchedQueryComparator } from './watched/processors/comparators.js';
|
|
18
19
|
export interface DisconnectAndClearOptions {
|
|
@@ -132,6 +133,11 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
|
|
|
132
133
|
protected _schema: Schema;
|
|
133
134
|
private _database;
|
|
134
135
|
protected runExclusiveMutex: Mutex;
|
|
136
|
+
/**
|
|
137
|
+
* @experimental
|
|
138
|
+
* Allows creating SQLite triggers which can be used to track various operations on SQLite tables.
|
|
139
|
+
*/
|
|
140
|
+
readonly triggers: TriggerManager;
|
|
135
141
|
logger: ILogger;
|
|
136
142
|
constructor(options: PowerSyncDatabaseOptionsWithDBAdapter);
|
|
137
143
|
constructor(options: PowerSyncDatabaseOptionsWithOpenFactory);
|
|
@@ -280,6 +286,38 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
|
|
|
280
286
|
* @returns A transaction of CRUD operations to upload, or null if there are none
|
|
281
287
|
*/
|
|
282
288
|
getNextCrudTransaction(): Promise<CrudTransaction | null>;
|
|
289
|
+
/**
|
|
290
|
+
* Returns an async iterator of completed transactions with local writes against the database.
|
|
291
|
+
*
|
|
292
|
+
* This is typically used from the {@link PowerSyncBackendConnector.uploadData} callback. Each entry emitted by the
|
|
293
|
+
* returned iterator is a full transaction containing all local writes made while that transaction was active.
|
|
294
|
+
*
|
|
295
|
+
* Unlike {@link getNextCrudTransaction}, which always returns the oldest transaction that hasn't been
|
|
296
|
+
* {@link CrudTransaction.complete}d yet, this iterator can be used to receive multiple transactions. Calling
|
|
297
|
+
* {@link CrudTransaction.complete} will mark that and all prior transactions emitted by the iterator as completed.
|
|
298
|
+
*
|
|
299
|
+
* This can be used to upload multiple transactions in a single batch, e.g with:
|
|
300
|
+
*
|
|
301
|
+
* ```JavaScript
|
|
302
|
+
* let lastTransaction = null;
|
|
303
|
+
* let batch = [];
|
|
304
|
+
*
|
|
305
|
+
* for await (const transaction of database.getCrudTransactions()) {
|
|
306
|
+
* batch.push(...transaction.crud);
|
|
307
|
+
* lastTransaction = transaction;
|
|
308
|
+
*
|
|
309
|
+
* if (batch.length > 10) {
|
|
310
|
+
* break;
|
|
311
|
+
* }
|
|
312
|
+
* }
|
|
313
|
+
* ```
|
|
314
|
+
*
|
|
315
|
+
* If there is no local data to upload, the async iterator complete without emitting any items.
|
|
316
|
+
*
|
|
317
|
+
* Note that iterating over async iterables requires a [polyfill](https://github.com/powersync-ja/powersync-js/tree/main/packages/react-native#babel-plugins-watched-queries)
|
|
318
|
+
* for React Native.
|
|
319
|
+
*/
|
|
320
|
+
getCrudTransactions(): AsyncIterable<CrudTransaction, null>;
|
|
283
321
|
/**
|
|
284
322
|
* Get an unique client id for this database.
|
|
285
323
|
*
|
|
@@ -7,7 +7,7 @@ import { SyncStatus } from '../db/crud/SyncStatus.js';
|
|
|
7
7
|
import { UploadQueueStats } from '../db/crud/UploadQueueStatus.js';
|
|
8
8
|
import { BaseObserver } from '../utils/BaseObserver.js';
|
|
9
9
|
import { ControlledExecutor } from '../utils/ControlledExecutor.js';
|
|
10
|
-
import { throttleTrailing } from '../utils/async.js';
|
|
10
|
+
import { symbolAsyncIterator, throttleTrailing } from '../utils/async.js';
|
|
11
11
|
import { ConnectionManager } from './ConnectionManager.js';
|
|
12
12
|
import { CustomQuery } from './CustomQuery.js';
|
|
13
13
|
import { isDBAdapter, isSQLOpenFactory, isSQLOpenOptions } from './SQLOpenFactory.js';
|
|
@@ -16,6 +16,7 @@ import { CrudBatch } from './sync/bucket/CrudBatch.js';
|
|
|
16
16
|
import { CrudEntry } from './sync/bucket/CrudEntry.js';
|
|
17
17
|
import { CrudTransaction } from './sync/bucket/CrudTransaction.js';
|
|
18
18
|
import { DEFAULT_CRUD_UPLOAD_THROTTLE_MS, DEFAULT_RETRY_DELAY_MS } from './sync/stream/AbstractStreamingSyncImplementation.js';
|
|
19
|
+
import { TriggerManagerImpl } from './triggers/TriggerManagerImpl.js';
|
|
19
20
|
import { DEFAULT_WATCH_THROTTLE_MS } from './watched/WatchedQuery.js';
|
|
20
21
|
import { OnChangeQueryProcessor } from './watched/processors/OnChangeQueryProcessor.js';
|
|
21
22
|
const POWERSYNC_TABLE_MATCH = /(^ps_data__|^ps_data_local__)/;
|
|
@@ -64,6 +65,11 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
64
65
|
_schema;
|
|
65
66
|
_database;
|
|
66
67
|
runExclusiveMutex;
|
|
68
|
+
/**
|
|
69
|
+
* @experimental
|
|
70
|
+
* Allows creating SQLite triggers which can be used to track various operations on SQLite tables.
|
|
71
|
+
*/
|
|
72
|
+
triggers;
|
|
67
73
|
logger;
|
|
68
74
|
constructor(options) {
|
|
69
75
|
super();
|
|
@@ -118,6 +124,10 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
118
124
|
logger: this.logger
|
|
119
125
|
});
|
|
120
126
|
this._isReadyPromise = this.initialize();
|
|
127
|
+
this.triggers = new TriggerManagerImpl({
|
|
128
|
+
db: this,
|
|
129
|
+
schema: this.schema
|
|
130
|
+
});
|
|
121
131
|
}
|
|
122
132
|
/**
|
|
123
133
|
* Schema used for the local database.
|
|
@@ -213,11 +223,11 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
213
223
|
.map((n) => parseInt(n));
|
|
214
224
|
}
|
|
215
225
|
catch (e) {
|
|
216
|
-
throw new Error(`Unsupported powersync extension version. Need >=0.
|
|
226
|
+
throw new Error(`Unsupported powersync extension version. Need >=0.4.5 <1.0.0, got: ${this.sdkVersion}. Details: ${e.message}`);
|
|
217
227
|
}
|
|
218
|
-
// Validate >=0.
|
|
219
|
-
if (versionInts[0] != 0 || versionInts[1] <
|
|
220
|
-
throw new Error(`Unsupported powersync extension version. Need >=0.
|
|
228
|
+
// Validate >=0.4.5 <1.0.0
|
|
229
|
+
if (versionInts[0] != 0 || versionInts[1] < 4 || (versionInts[1] == 4 && versionInts[2] < 5)) {
|
|
230
|
+
throw new Error(`Unsupported powersync extension version. Need >=0.4.5 <1.0.0, got: ${this.sdkVersion}`);
|
|
221
231
|
}
|
|
222
232
|
}
|
|
223
233
|
async updateHasSynced() {
|
|
@@ -426,23 +436,72 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
426
436
|
* @returns A transaction of CRUD operations to upload, or null if there are none
|
|
427
437
|
*/
|
|
428
438
|
async getNextCrudTransaction() {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
439
|
+
const iterator = this.getCrudTransactions()[symbolAsyncIterator]();
|
|
440
|
+
return (await iterator.next()).value;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Returns an async iterator of completed transactions with local writes against the database.
|
|
444
|
+
*
|
|
445
|
+
* This is typically used from the {@link PowerSyncBackendConnector.uploadData} callback. Each entry emitted by the
|
|
446
|
+
* returned iterator is a full transaction containing all local writes made while that transaction was active.
|
|
447
|
+
*
|
|
448
|
+
* Unlike {@link getNextCrudTransaction}, which always returns the oldest transaction that hasn't been
|
|
449
|
+
* {@link CrudTransaction.complete}d yet, this iterator can be used to receive multiple transactions. Calling
|
|
450
|
+
* {@link CrudTransaction.complete} will mark that and all prior transactions emitted by the iterator as completed.
|
|
451
|
+
*
|
|
452
|
+
* This can be used to upload multiple transactions in a single batch, e.g with:
|
|
453
|
+
*
|
|
454
|
+
* ```JavaScript
|
|
455
|
+
* let lastTransaction = null;
|
|
456
|
+
* let batch = [];
|
|
457
|
+
*
|
|
458
|
+
* for await (const transaction of database.getCrudTransactions()) {
|
|
459
|
+
* batch.push(...transaction.crud);
|
|
460
|
+
* lastTransaction = transaction;
|
|
461
|
+
*
|
|
462
|
+
* if (batch.length > 10) {
|
|
463
|
+
* break;
|
|
464
|
+
* }
|
|
465
|
+
* }
|
|
466
|
+
* ```
|
|
467
|
+
*
|
|
468
|
+
* If there is no local data to upload, the async iterator complete without emitting any items.
|
|
469
|
+
*
|
|
470
|
+
* Note that iterating over async iterables requires a [polyfill](https://github.com/powersync-ja/powersync-js/tree/main/packages/react-native#babel-plugins-watched-queries)
|
|
471
|
+
* for React Native.
|
|
472
|
+
*/
|
|
473
|
+
getCrudTransactions() {
|
|
474
|
+
return {
|
|
475
|
+
[symbolAsyncIterator]: () => {
|
|
476
|
+
let lastCrudItemId = -1;
|
|
477
|
+
const sql = `
|
|
478
|
+
WITH RECURSIVE crud_entries AS (
|
|
479
|
+
SELECT id, tx_id, data FROM ps_crud WHERE id = (SELECT min(id) FROM ps_crud WHERE id > ?)
|
|
480
|
+
UNION ALL
|
|
481
|
+
SELECT ps_crud.id, ps_crud.tx_id, ps_crud.data FROM ps_crud
|
|
482
|
+
INNER JOIN crud_entries ON crud_entries.id + 1 = rowid
|
|
483
|
+
WHERE crud_entries.tx_id = ps_crud.tx_id
|
|
484
|
+
)
|
|
485
|
+
SELECT * FROM crud_entries;
|
|
486
|
+
`;
|
|
487
|
+
return {
|
|
488
|
+
next: async () => {
|
|
489
|
+
const nextTransaction = await this.database.getAll(sql, [lastCrudItemId]);
|
|
490
|
+
if (nextTransaction.length == 0) {
|
|
491
|
+
return { done: true, value: null };
|
|
492
|
+
}
|
|
493
|
+
const items = nextTransaction.map((row) => CrudEntry.fromRow(row));
|
|
494
|
+
const last = items[items.length - 1];
|
|
495
|
+
const txId = last.transactionId;
|
|
496
|
+
lastCrudItemId = last.clientId;
|
|
497
|
+
return {
|
|
498
|
+
done: false,
|
|
499
|
+
value: new CrudTransaction(items, async (writeCheckpoint) => this.handleCrudCheckpoint(last.clientId, writeCheckpoint), txId)
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
};
|
|
442
503
|
}
|
|
443
|
-
|
|
444
|
-
return new CrudTransaction(all, async (writeCheckpoint) => this.handleCrudCheckpoint(last.clientId, writeCheckpoint), txId);
|
|
445
|
-
});
|
|
504
|
+
};
|
|
446
505
|
}
|
|
447
506
|
/**
|
|
448
507
|
* Get an unique client id for this database.
|
|
@@ -382,6 +382,9 @@ export class AbstractRemote {
|
|
|
382
382
|
* Aborting the active fetch request while it is being consumed seems to throw
|
|
383
383
|
* an unhandled exception on the window level.
|
|
384
384
|
*/
|
|
385
|
+
if (abortSignal?.aborted) {
|
|
386
|
+
throw new AbortOperation('Abort request received before making postStreamRaw request');
|
|
387
|
+
}
|
|
385
388
|
const controller = new AbortController();
|
|
386
389
|
let requestResolved = false;
|
|
387
390
|
abortSignal?.addEventListener('abort', () => {
|
|
@@ -439,7 +439,9 @@ The next upload iteration will be delayed.`);
|
|
|
439
439
|
...DEFAULT_STREAM_CONNECTION_OPTIONS,
|
|
440
440
|
...(options ?? {})
|
|
441
441
|
};
|
|
442
|
-
|
|
442
|
+
const clientImplementation = resolvedOptions.clientImplementation;
|
|
443
|
+
this.updateSyncStatus({ clientImplementation });
|
|
444
|
+
if (clientImplementation == SyncClientImplementation.JAVASCRIPT) {
|
|
443
445
|
await this.legacyStreamingSyncIteration(signal, resolvedOptions);
|
|
444
446
|
}
|
|
445
447
|
else {
|
|
@@ -693,6 +695,9 @@ The next upload iteration will be delayed.`);
|
|
|
693
695
|
const remote = this.options.remote;
|
|
694
696
|
let receivingLines = null;
|
|
695
697
|
let hadSyncLine = false;
|
|
698
|
+
if (signal.aborted) {
|
|
699
|
+
throw new AbortOperation('Connection request has been aborted');
|
|
700
|
+
}
|
|
696
701
|
const abortController = new AbortController();
|
|
697
702
|
signal.addEventListener('abort', () => abortController.abort());
|
|
698
703
|
// Pending sync lines received from the service, as well as local events that trigger a powersync_control
|
|
@@ -934,7 +939,8 @@ The next upload iteration will be delayed.`);
|
|
|
934
939
|
...this.syncStatus.dataFlowStatus,
|
|
935
940
|
...options.dataFlow
|
|
936
941
|
},
|
|
937
|
-
priorityStatusEntries: options.priorityStatusEntries ?? this.syncStatus.priorityStatusEntries
|
|
942
|
+
priorityStatusEntries: options.priorityStatusEntries ?? this.syncStatus.priorityStatusEntries,
|
|
943
|
+
clientImplementation: options.clientImplementation ?? this.syncStatus.clientImplementation
|
|
938
944
|
});
|
|
939
945
|
if (!this.syncStatus.isEqual(updatedStatus)) {
|
|
940
946
|
this.syncStatus = updatedStatus;
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { LockContext } from '../../db/DBAdapter.js';
|
|
2
|
+
/**
|
|
3
|
+
* SQLite operations to track changes for with {@link TriggerManager}
|
|
4
|
+
* @experimental
|
|
5
|
+
*/
|
|
6
|
+
export declare enum DiffTriggerOperation {
|
|
7
|
+
INSERT = "INSERT",
|
|
8
|
+
UPDATE = "UPDATE",
|
|
9
|
+
DELETE = "DELETE"
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* @experimental
|
|
13
|
+
* Diffs created by {@link TriggerManager#createDiffTrigger} are stored in a temporary table.
|
|
14
|
+
* This is the base record structure for all diff records.
|
|
15
|
+
*/
|
|
16
|
+
export interface BaseTriggerDiffRecord {
|
|
17
|
+
/**
|
|
18
|
+
* The modified row's `id` column value.
|
|
19
|
+
*/
|
|
20
|
+
id: string;
|
|
21
|
+
/**
|
|
22
|
+
* The operation performed which created this record.
|
|
23
|
+
*/
|
|
24
|
+
operation: DiffTriggerOperation;
|
|
25
|
+
/**
|
|
26
|
+
* Time the change operation was recorded.
|
|
27
|
+
* This is in ISO 8601 format, e.g. `2023-10-01T12:00:00.000Z`.
|
|
28
|
+
*/
|
|
29
|
+
timestamp: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* @experimental
|
|
33
|
+
* Represents a diff record for a SQLite UPDATE operation.
|
|
34
|
+
* This record contains the new value and optionally the previous value.
|
|
35
|
+
* Values are stored as JSON strings.
|
|
36
|
+
*/
|
|
37
|
+
export interface TriggerDiffUpdateRecord extends BaseTriggerDiffRecord {
|
|
38
|
+
operation: DiffTriggerOperation.UPDATE;
|
|
39
|
+
/**
|
|
40
|
+
* The updated state of the row in JSON string format.
|
|
41
|
+
*/
|
|
42
|
+
value: string;
|
|
43
|
+
/**
|
|
44
|
+
* The previous value of the row in JSON string format.
|
|
45
|
+
*/
|
|
46
|
+
previous_value: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* @experimental
|
|
50
|
+
* Represents a diff record for a SQLite INSERT operation.
|
|
51
|
+
* This record contains the new value represented as a JSON string.
|
|
52
|
+
*/
|
|
53
|
+
export interface TriggerDiffInsertRecord extends BaseTriggerDiffRecord {
|
|
54
|
+
operation: DiffTriggerOperation.INSERT;
|
|
55
|
+
/**
|
|
56
|
+
* The value of the row, at the time of INSERT, in JSON string format.
|
|
57
|
+
*/
|
|
58
|
+
value: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* @experimental
|
|
62
|
+
* Represents a diff record for a SQLite DELETE operation.
|
|
63
|
+
* This record contains the new value represented as a JSON string.
|
|
64
|
+
*/
|
|
65
|
+
export interface TriggerDiffDeleteRecord extends BaseTriggerDiffRecord {
|
|
66
|
+
operation: DiffTriggerOperation.DELETE;
|
|
67
|
+
/**
|
|
68
|
+
* The value of the row, before the DELETE operation, in JSON string format.
|
|
69
|
+
*/
|
|
70
|
+
value: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* @experimental
|
|
74
|
+
* Diffs created by {@link TriggerManager#createDiffTrigger} are stored in a temporary table.
|
|
75
|
+
* This is the record structure for all diff records.
|
|
76
|
+
*
|
|
77
|
+
* Querying the DIFF table directly with {@link TriggerDiffHandlerContext#withDiff} will return records
|
|
78
|
+
* with the structure of this type.
|
|
79
|
+
* @example
|
|
80
|
+
* ```typescript
|
|
81
|
+
* const diffs = await context.withDiff<TriggerDiffRecord>('SELECT * FROM DIFF');
|
|
82
|
+
* diff.forEach(diff => console.log(diff.operation, diff.timestamp, JSON.parse(diff.value)))
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
export type TriggerDiffRecord = TriggerDiffUpdateRecord | TriggerDiffInsertRecord | TriggerDiffDeleteRecord;
|
|
86
|
+
/**
|
|
87
|
+
* @experimental
|
|
88
|
+
* Querying the DIFF table directly with {@link TriggerDiffHandlerContext#withExtractedDiff} will return records
|
|
89
|
+
* with the tracked columns extracted from the JSON value.
|
|
90
|
+
* This type represents the structure of such records.
|
|
91
|
+
* @example
|
|
92
|
+
* ```typescript
|
|
93
|
+
* const diffs = await context.withExtractedDiff<ExtractedTriggerDiffRecord<{id: string, name: string}>>('SELECT * FROM DIFF');
|
|
94
|
+
* diff.forEach(diff => console.log(diff.__operation, diff.__timestamp, diff.columnName))
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
export type ExtractedTriggerDiffRecord<T> = T & {
|
|
98
|
+
[K in keyof Omit<BaseTriggerDiffRecord, 'id'> as `__${string & K}`]: TriggerDiffRecord[K];
|
|
99
|
+
} & {
|
|
100
|
+
__previous_value?: string;
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* @experimental
|
|
104
|
+
* Hooks used in the creation of a table diff trigger.
|
|
105
|
+
*/
|
|
106
|
+
export interface TriggerCreationHooks {
|
|
107
|
+
/**
|
|
108
|
+
* Executed inside a write lock before the trigger is created.
|
|
109
|
+
*/
|
|
110
|
+
beforeCreate?: (context: LockContext) => Promise<void>;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Common interface for options used in creating a diff trigger.
|
|
114
|
+
*/
|
|
115
|
+
interface BaseCreateDiffTriggerOptions {
|
|
116
|
+
/**
|
|
117
|
+
* PowerSync source table/view to trigger and track changes from.
|
|
118
|
+
* This should be present in the PowerSync database's schema.
|
|
119
|
+
*/
|
|
120
|
+
source: string;
|
|
121
|
+
/**
|
|
122
|
+
* Columns to track and report changes for.
|
|
123
|
+
* Defaults to all columns in the source table.
|
|
124
|
+
* Use an empty array to track only the ID and operation.
|
|
125
|
+
*/
|
|
126
|
+
columns?: string[];
|
|
127
|
+
/**
|
|
128
|
+
* Condition to filter when the triggers should fire.
|
|
129
|
+
* This corresponds to a SQLite [WHEN](https://sqlite.org/lang_createtrigger.html) clause in the trigger body.
|
|
130
|
+
* This is useful for only triggering on specific conditions.
|
|
131
|
+
* For example, you can use it to only trigger on certain values in the NEW row.
|
|
132
|
+
* Note that for PowerSync the row data is stored in a JSON column named `data`.
|
|
133
|
+
* The row id is available in the `id` column.
|
|
134
|
+
*
|
|
135
|
+
* NB! The WHEN clauses here are added directly to the SQLite trigger creation SQL.
|
|
136
|
+
* Any user input strings here should be sanitized externally. The {@link when} string template function performs
|
|
137
|
+
* some basic sanitization, extra external sanitization is recommended.
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* {
|
|
141
|
+
* 'INSERT': sanitizeSQL`json_extract(NEW.data, '$.list_id') = ${sanitizeUUID(list.id)}`,
|
|
142
|
+
* 'INSERT': `TRUE`,
|
|
143
|
+
* 'UPDATE': sanitizeSQL`NEW.id = 'abcd' AND json_extract(NEW.data, '$.status') = 'active'`,
|
|
144
|
+
* 'DELETE': sanitizeSQL`json_extract(OLD.data, '$.list_id') = 'abcd'`
|
|
145
|
+
* }
|
|
146
|
+
*/
|
|
147
|
+
when: Partial<Record<DiffTriggerOperation, string>>;
|
|
148
|
+
/**
|
|
149
|
+
* Hooks which allow execution during the trigger creation process.
|
|
150
|
+
*/
|
|
151
|
+
hooks?: TriggerCreationHooks;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* @experimental
|
|
155
|
+
* Options for {@link TriggerManager#createDiffTrigger}.
|
|
156
|
+
*/
|
|
157
|
+
export interface CreateDiffTriggerOptions extends BaseCreateDiffTriggerOptions {
|
|
158
|
+
/**
|
|
159
|
+
* Destination table to send changes to.
|
|
160
|
+
* This table is created internally as a SQLite temporary table.
|
|
161
|
+
* This table will be dropped once the trigger is removed.
|
|
162
|
+
*/
|
|
163
|
+
destination: string;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* @experimental
|
|
167
|
+
* Callback to drop a trigger after it has been created.
|
|
168
|
+
*/
|
|
169
|
+
export type TriggerRemoveCallback = () => Promise<void>;
|
|
170
|
+
/**
|
|
171
|
+
* @experimental
|
|
172
|
+
* Context for the `onChange` handler provided to {@link TriggerManager#trackTableDiff}.
|
|
173
|
+
*/
|
|
174
|
+
export interface TriggerDiffHandlerContext extends LockContext {
|
|
175
|
+
/**
|
|
176
|
+
* The name of the temporary destination table created by the trigger.
|
|
177
|
+
*/
|
|
178
|
+
destinationTable: string;
|
|
179
|
+
/**
|
|
180
|
+
* Allows querying the database with access to the table containing DIFF records.
|
|
181
|
+
* The diff table is accessible via the `DIFF` accessor.
|
|
182
|
+
*
|
|
183
|
+
* The `DIFF` table is of the form described in {@link TriggerManager#createDiffTrigger}
|
|
184
|
+
* ```sql
|
|
185
|
+
* CREATE TEMP DIFF (
|
|
186
|
+
* id TEXT,
|
|
187
|
+
* operation TEXT,
|
|
188
|
+
* timestamp TEXT
|
|
189
|
+
* value TEXT,
|
|
190
|
+
* previous_value TEXT
|
|
191
|
+
* );
|
|
192
|
+
* ```
|
|
193
|
+
*
|
|
194
|
+
* Note that the `value` and `previous_value` columns store the row state in JSON string format.
|
|
195
|
+
* To access the row state in an extracted form see {@link TriggerDiffHandlerContext#withExtractedDiff}.
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* ```sql
|
|
199
|
+
* --- This fetches the current state of `todo` rows which have a diff operation present.
|
|
200
|
+
* --- The state of the row at the time of the operation is accessible in the DIFF records.
|
|
201
|
+
* SELECT
|
|
202
|
+
* todos.*
|
|
203
|
+
* FROM
|
|
204
|
+
* DIFF
|
|
205
|
+
* JOIN todos ON DIFF.id = todos.id
|
|
206
|
+
* WHERE json_extract(DIFF.value, '$.status') = 'active'
|
|
207
|
+
* ```
|
|
208
|
+
*/
|
|
209
|
+
withDiff: <T = any>(query: string, params?: ReadonlyArray<Readonly<any>>) => Promise<T[]>;
|
|
210
|
+
/**
|
|
211
|
+
* Allows querying the database with access to the table containing diff records.
|
|
212
|
+
* The diff table is accessible via the `DIFF` accessor.
|
|
213
|
+
*
|
|
214
|
+
* This is similar to {@link withDiff} but extracts the row columns from the tracked JSON value. The diff operation
|
|
215
|
+
* data is aliased as `__` columns to avoid column conflicts.
|
|
216
|
+
*
|
|
217
|
+
* For {@link DiffTriggerOperation#DELETE} operations the previous_value columns are extracted for convenience.
|
|
218
|
+
*
|
|
219
|
+
*
|
|
220
|
+
* ```sql
|
|
221
|
+
* CREATE TEMP TABLE DIFF (
|
|
222
|
+
* id TEXT,
|
|
223
|
+
* replicated_column_1 COLUMN_TYPE,
|
|
224
|
+
* replicated_column_2 COLUMN_TYPE,
|
|
225
|
+
* __operation TEXT,
|
|
226
|
+
* __timestamp TEXT,
|
|
227
|
+
* __previous_value TEXT
|
|
228
|
+
* );
|
|
229
|
+
* ```
|
|
230
|
+
*
|
|
231
|
+
* @example
|
|
232
|
+
* ```sql
|
|
233
|
+
* SELECT
|
|
234
|
+
* todos.*
|
|
235
|
+
* FROM
|
|
236
|
+
* DIFF
|
|
237
|
+
* JOIN todos ON DIFF.id = todos.id
|
|
238
|
+
* --- The todo column names are extracted from json and are available as DIFF.name
|
|
239
|
+
* WHERE DIFF.name = 'example'
|
|
240
|
+
* ```
|
|
241
|
+
*/
|
|
242
|
+
withExtractedDiff: <T = any>(query: string, params?: ReadonlyArray<Readonly<any>>) => Promise<T[]>;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* @experimental
|
|
246
|
+
* Options for tracking changes to a table with {@link TriggerManager#trackTableDiff}.
|
|
247
|
+
*/
|
|
248
|
+
export interface TrackDiffOptions extends BaseCreateDiffTriggerOptions {
|
|
249
|
+
/**
|
|
250
|
+
* Handler for processing diff operations.
|
|
251
|
+
* Automatically invoked once diff items are present.
|
|
252
|
+
* Diff items are automatically cleared after the handler is invoked.
|
|
253
|
+
*/
|
|
254
|
+
onChange: (context: TriggerDiffHandlerContext) => Promise<void>;
|
|
255
|
+
/**
|
|
256
|
+
* The minimum interval, in milliseconds, between {@link onChange} invocations.
|
|
257
|
+
* @default {@link DEFAULT_WATCH_THROTTLE_MS}
|
|
258
|
+
*/
|
|
259
|
+
throttleMs?: number;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* @experimental
|
|
263
|
+
*/
|
|
264
|
+
export interface TriggerManager {
|
|
265
|
+
/**
|
|
266
|
+
* @experimental
|
|
267
|
+
* Creates a temporary trigger which tracks changes to a source table
|
|
268
|
+
* and writes changes to a destination table.
|
|
269
|
+
* The temporary destination table is created internally and will be dropped when the trigger is removed.
|
|
270
|
+
* The temporary destination table is created with the structure:
|
|
271
|
+
*
|
|
272
|
+
* ```sql
|
|
273
|
+
* CREATE TEMP TABLE ${destination} (
|
|
274
|
+
* id TEXT,
|
|
275
|
+
* operation TEXT,
|
|
276
|
+
* timestamp TEXT
|
|
277
|
+
* value TEXT,
|
|
278
|
+
* previous_value TEXT
|
|
279
|
+
* );
|
|
280
|
+
* ```
|
|
281
|
+
* The `value` column contains the JSON representation of the row's value at the change.
|
|
282
|
+
*
|
|
283
|
+
* For {@link DiffTriggerOperation#UPDATE} operations the `previous_value` column contains the previous value of the changed row
|
|
284
|
+
* in a JSON format.
|
|
285
|
+
*
|
|
286
|
+
* NB: The triggers created by this method might be invalidated by {@link AbstractPowerSyncDatabase#updateSchema} calls.
|
|
287
|
+
* These triggers should manually be dropped and recreated when updating the schema.
|
|
288
|
+
*
|
|
289
|
+
* @returns A callback to remove the trigger and drop the destination table.
|
|
290
|
+
*
|
|
291
|
+
* @example
|
|
292
|
+
* ```javascript
|
|
293
|
+
* const dispose = await database.triggers.createDiffTrigger({
|
|
294
|
+
* source: 'lists',
|
|
295
|
+
* destination: 'ps_temp_lists_diff',
|
|
296
|
+
* columns: ['name'],
|
|
297
|
+
* when: {
|
|
298
|
+
* [DiffTriggerOperation.INSERT]: 'TRUE',
|
|
299
|
+
* [DiffTriggerOperation.UPDATE]: 'TRUE',
|
|
300
|
+
* [DiffTriggerOperation.DELETE]: 'TRUE'
|
|
301
|
+
* }
|
|
302
|
+
* });
|
|
303
|
+
* ```
|
|
304
|
+
*/
|
|
305
|
+
createDiffTrigger(options: CreateDiffTriggerOptions): Promise<TriggerRemoveCallback>;
|
|
306
|
+
/**
|
|
307
|
+
* @experimental
|
|
308
|
+
* Tracks changes for a table. Triggering a provided handler on changes.
|
|
309
|
+
* Uses {@link createDiffTrigger} internally to create a temporary destination table.
|
|
310
|
+
*
|
|
311
|
+
* @returns A callback to cleanup the trigger and stop tracking changes.
|
|
312
|
+
*
|
|
313
|
+
* NB: The triggers created by this method might be invalidated by {@link AbstractPowerSyncDatabase#updateSchema} calls.
|
|
314
|
+
* These triggers should manually be dropped and recreated when updating the schema.
|
|
315
|
+
*
|
|
316
|
+
* @example
|
|
317
|
+
* ```javascript
|
|
318
|
+
* const dispose = database.triggers.trackTableDiff({
|
|
319
|
+
* source: 'todos',
|
|
320
|
+
* columns: ['list_id'],
|
|
321
|
+
* when: {
|
|
322
|
+
* [DiffTriggerOperation.INSERT]: sanitizeSQL`json_extract(NEW.data, '$.list_id') = ${sanitizeUUID(someIdVariable)}`
|
|
323
|
+
* },
|
|
324
|
+
* onChange: async (context) => {
|
|
325
|
+
* // Fetches the todo records that were inserted during this diff
|
|
326
|
+
* const newTodos = await context.getAll<Database['todos']>(`
|
|
327
|
+
* SELECT
|
|
328
|
+
* todos.*
|
|
329
|
+
* FROM
|
|
330
|
+
* DIFF
|
|
331
|
+
* JOIN todos ON DIFF.id = todos.id
|
|
332
|
+
* `);
|
|
333
|
+
*
|
|
334
|
+
* // Process newly created todos
|
|
335
|
+
* },
|
|
336
|
+
* hooks: {
|
|
337
|
+
* beforeCreate: async (lockContext) => {
|
|
338
|
+
* // This hook is executed inside the write lock before the trigger is created.
|
|
339
|
+
* // It can be used to synchronize the current state of the table with processor logic.
|
|
340
|
+
* // Any changes after this callback are guaranteed to trigger the `onChange` handler.
|
|
341
|
+
*
|
|
342
|
+
* // Read the current state of the todos table
|
|
343
|
+
* const currentTodos = await lockContext.getAll<Database['todos']>(
|
|
344
|
+
* `
|
|
345
|
+
* SELECT
|
|
346
|
+
* *
|
|
347
|
+
* FROM
|
|
348
|
+
* todos
|
|
349
|
+
* WHERE
|
|
350
|
+
* list_id = ?
|
|
351
|
+
* `,
|
|
352
|
+
* ['123']
|
|
353
|
+
* );
|
|
354
|
+
*
|
|
355
|
+
* // Process existing todos
|
|
356
|
+
* }
|
|
357
|
+
* }
|
|
358
|
+
* });
|
|
359
|
+
* ```
|
|
360
|
+
*/
|
|
361
|
+
trackTableDiff(options: TrackDiffOptions): Promise<TriggerRemoveCallback>;
|
|
362
|
+
}
|
|
363
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite operations to track changes for with {@link TriggerManager}
|
|
3
|
+
* @experimental
|
|
4
|
+
*/
|
|
5
|
+
export var DiffTriggerOperation;
|
|
6
|
+
(function (DiffTriggerOperation) {
|
|
7
|
+
DiffTriggerOperation["INSERT"] = "INSERT";
|
|
8
|
+
DiffTriggerOperation["UPDATE"] = "UPDATE";
|
|
9
|
+
DiffTriggerOperation["DELETE"] = "DELETE";
|
|
10
|
+
})(DiffTriggerOperation || (DiffTriggerOperation = {}));
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { LockContext } from '../../db/DBAdapter.js';
|
|
2
|
+
import { Schema } from '../../db/schema/Schema.js';
|
|
3
|
+
import { type AbstractPowerSyncDatabase } from '../AbstractPowerSyncDatabase.js';
|
|
4
|
+
import { CreateDiffTriggerOptions, TrackDiffOptions, TriggerManager, TriggerRemoveCallback } from './TriggerManager.js';
|
|
5
|
+
export type TriggerManagerImplOptions = {
|
|
6
|
+
db: AbstractPowerSyncDatabase;
|
|
7
|
+
schema: Schema;
|
|
8
|
+
};
|
|
9
|
+
export declare class TriggerManagerImpl implements TriggerManager {
|
|
10
|
+
protected options: TriggerManagerImplOptions;
|
|
11
|
+
protected schema: Schema;
|
|
12
|
+
constructor(options: TriggerManagerImplOptions);
|
|
13
|
+
protected get db(): AbstractPowerSyncDatabase;
|
|
14
|
+
protected getUUID(): Promise<string>;
|
|
15
|
+
protected removeTriggers(tx: LockContext, triggerIds: string[]): Promise<void>;
|
|
16
|
+
createDiffTrigger(options: CreateDiffTriggerOptions): Promise<() => Promise<void>>;
|
|
17
|
+
trackTableDiff(options: TrackDiffOptions): Promise<TriggerRemoveCallback>;
|
|
18
|
+
}
|