@powersync/common 1.41.1 → 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.
@@ -14,8 +14,11 @@ export enum DiffTriggerOperation {
14
14
  * @experimental
15
15
  * Diffs created by {@link TriggerManager#createDiffTrigger} are stored in a temporary table.
16
16
  * This is the base record structure for all diff records.
17
+ *
18
+ * @template TOperationId - The type for `operation_id`. Defaults to `number` as returned by default SQLite database queries.
19
+ * Use `string` for full 64-bit precision when using `{ castOperationIdAsText: true }` option.
17
20
  */
18
- export interface BaseTriggerDiffRecord {
21
+ export interface BaseTriggerDiffRecord<TOperationId extends string | number = number> {
19
22
  /**
20
23
  * The modified row's `id` column value.
21
24
  */
@@ -24,6 +27,13 @@ export interface BaseTriggerDiffRecord {
24
27
  * The operation performed which created this record.
25
28
  */
26
29
  operation: DiffTriggerOperation;
30
+ /**
31
+ * Auto-incrementing primary key for the operation.
32
+ * Defaults to number as returned by database queries (wa-sqlite returns lower 32 bits).
33
+ * Can be string for full 64-bit precision when using `{ castOperationIdAsText: true }` option.
34
+ */
35
+ operation_id: TOperationId;
36
+
27
37
  /**
28
38
  * Time the change operation was recorded.
29
39
  * This is in ISO 8601 format, e.g. `2023-10-01T12:00:00.000Z`.
@@ -37,7 +47,8 @@ export interface BaseTriggerDiffRecord {
37
47
  * This record contains the new value and optionally the previous value.
38
48
  * Values are stored as JSON strings.
39
49
  */
40
- export interface TriggerDiffUpdateRecord extends BaseTriggerDiffRecord {
50
+ export interface TriggerDiffUpdateRecord<TOperationId extends string | number = number>
51
+ extends BaseTriggerDiffRecord<TOperationId> {
41
52
  operation: DiffTriggerOperation.UPDATE;
42
53
  /**
43
54
  * The updated state of the row in JSON string format.
@@ -54,7 +65,8 @@ export interface TriggerDiffUpdateRecord extends BaseTriggerDiffRecord {
54
65
  * Represents a diff record for a SQLite INSERT operation.
55
66
  * This record contains the new value represented as a JSON string.
56
67
  */
57
- export interface TriggerDiffInsertRecord extends BaseTriggerDiffRecord {
68
+ export interface TriggerDiffInsertRecord<TOperationId extends string | number = number>
69
+ extends BaseTriggerDiffRecord<TOperationId> {
58
70
  operation: DiffTriggerOperation.INSERT;
59
71
  /**
60
72
  * The value of the row, at the time of INSERT, in JSON string format.
@@ -67,7 +79,8 @@ export interface TriggerDiffInsertRecord extends BaseTriggerDiffRecord {
67
79
  * Represents a diff record for a SQLite DELETE operation.
68
80
  * This record contains the new value represented as a JSON string.
69
81
  */
70
- export interface TriggerDiffDeleteRecord extends BaseTriggerDiffRecord {
82
+ export interface TriggerDiffDeleteRecord<TOperationId extends string | number = number>
83
+ extends BaseTriggerDiffRecord<TOperationId> {
71
84
  operation: DiffTriggerOperation.DELETE;
72
85
  /**
73
86
  * The value of the row, before the DELETE operation, in JSON string format.
@@ -82,27 +95,53 @@ export interface TriggerDiffDeleteRecord extends BaseTriggerDiffRecord {
82
95
  *
83
96
  * Querying the DIFF table directly with {@link TriggerDiffHandlerContext#withDiff} will return records
84
97
  * with the structure of this type.
98
+ *
99
+ * @template TOperationId - The type for `operation_id`. Defaults to `number` as returned by database queries.
100
+ * Use `string` for full 64-bit precision when using `{ castOperationIdAsText: true }` option.
101
+ *
85
102
  * @example
86
103
  * ```typescript
104
+ * // Default: operation_id is number
87
105
  * const diffs = await context.withDiff<TriggerDiffRecord>('SELECT * FROM DIFF');
88
- * diff.forEach(diff => console.log(diff.operation, diff.timestamp, JSON.parse(diff.value)))
106
+ *
107
+ * // With string operation_id for full precision
108
+ * const diffsWithString = await context.withDiff<TriggerDiffRecord<string>>(
109
+ * 'SELECT * FROM DIFF',
110
+ * undefined,
111
+ * { castOperationIdAsText: true }
112
+ * );
89
113
  * ```
90
114
  */
91
- export type TriggerDiffRecord = TriggerDiffUpdateRecord | TriggerDiffInsertRecord | TriggerDiffDeleteRecord;
115
+ export type TriggerDiffRecord<TOperationId extends string | number = number> =
116
+ | TriggerDiffUpdateRecord<TOperationId>
117
+ | TriggerDiffInsertRecord<TOperationId>
118
+ | TriggerDiffDeleteRecord<TOperationId>;
92
119
 
93
120
  /**
94
121
  * @experimental
95
122
  * Querying the DIFF table directly with {@link TriggerDiffHandlerContext#withExtractedDiff} will return records
96
123
  * with the tracked columns extracted from the JSON value.
97
124
  * This type represents the structure of such records.
125
+ *
126
+ * @template T - The type for the extracted columns from the tracked JSON value.
127
+ * @template TOperationId - The type for `operation_id`. Defaults to `number` as returned by database queries.
128
+ * Use `string` for full 64-bit precision when using `{ castOperationIdAsText: true }` option.
129
+ *
98
130
  * @example
99
131
  * ```typescript
132
+ * // Default: operation_id is number
100
133
  * const diffs = await context.withExtractedDiff<ExtractedTriggerDiffRecord<{id: string, name: string}>>('SELECT * FROM DIFF');
101
- * diff.forEach(diff => console.log(diff.__operation, diff.__timestamp, diff.columnName))
134
+ *
135
+ * // With string operation_id for full precision
136
+ * const diffsWithString = await context.withExtractedDiff<ExtractedTriggerDiffRecord<{id: string, name: string}, string>>(
137
+ * 'SELECT * FROM DIFF',
138
+ * undefined,
139
+ * { castOperationIdAsText: true }
140
+ * );
102
141
  * ```
103
142
  */
104
- export type ExtractedTriggerDiffRecord<T> = T & {
105
- [K in keyof Omit<BaseTriggerDiffRecord, 'id'> as `__${string & K}`]: TriggerDiffRecord[K];
143
+ export type ExtractedTriggerDiffRecord<T, TOperationId extends string | number = number> = T & {
144
+ [K in keyof Omit<BaseTriggerDiffRecord<TOperationId>, 'id'> as `__${string & K}`]: TriggerDiffRecord<TOperationId>[K];
106
145
  } & {
107
146
  __previous_value?: string;
108
147
  };
@@ -183,6 +222,21 @@ export interface CreateDiffTriggerOptions extends BaseCreateDiffTriggerOptions {
183
222
  */
184
223
  export type TriggerRemoveCallback = () => Promise<void>;
185
224
 
225
+ /**
226
+ * @experimental
227
+ * Options for {@link TriggerDiffHandlerContext#withDiff}.
228
+ */
229
+ export interface WithDiffOptions {
230
+ /**
231
+ * If true, casts `operation_id` as TEXT in the internal CTE to preserve full 64-bit precision.
232
+ * Use this when you need to ensure `operation_id` is treated as a string to avoid precision loss
233
+ * for values exceeding JavaScript's Number.MAX_SAFE_INTEGER.
234
+ *
235
+ * When enabled, use {@link TriggerDiffRecord}<string> to type the result correctly.
236
+ */
237
+ castOperationIdAsText?: boolean;
238
+ }
239
+
186
240
  /**
187
241
  * @experimental
188
242
  * Context for the `onChange` handler provided to {@link TriggerManager#trackTableDiff}.
@@ -200,9 +254,10 @@ export interface TriggerDiffHandlerContext extends LockContext {
200
254
  * The `DIFF` table is of the form described in {@link TriggerManager#createDiffTrigger}
201
255
  * ```sql
202
256
  * CREATE TEMP DIFF (
257
+ * operation_id INTEGER PRIMARY KEY AUTOINCREMENT,
203
258
  * id TEXT,
204
259
  * operation TEXT,
205
- * timestamp TEXT
260
+ * timestamp TEXT,
206
261
  * value TEXT,
207
262
  * previous_value TEXT
208
263
  * );
@@ -222,8 +277,19 @@ export interface TriggerDiffHandlerContext extends LockContext {
222
277
  * JOIN todos ON DIFF.id = todos.id
223
278
  * WHERE json_extract(DIFF.value, '$.status') = 'active'
224
279
  * ```
280
+ *
281
+ * @example
282
+ * ```typescript
283
+ * // With operation_id cast as TEXT for full precision
284
+ * const diffs = await context.withDiff<TriggerDiffRecord<string>>(
285
+ * 'SELECT * FROM DIFF',
286
+ * undefined,
287
+ * { castOperationIdAsText: true }
288
+ * );
289
+ * // diffs[0].operation_id is now typed as string
290
+ * ```
225
291
  */
226
- withDiff: <T = any>(query: string, params?: ReadonlyArray<Readonly<any>>) => Promise<T[]>;
292
+ withDiff: <T = any>(query: string, params?: ReadonlyArray<Readonly<any>>, options?: WithDiffOptions) => Promise<T[]>;
227
293
 
228
294
  /**
229
295
  * Allows querying the database with access to the table containing diff records.
@@ -292,9 +358,10 @@ export interface TriggerManager {
292
358
  *
293
359
  * ```sql
294
360
  * CREATE TEMP TABLE ${destination} (
361
+ * operation_id INTEGER PRIMARY KEY AUTOINCREMENT,
295
362
  * id TEXT,
296
363
  * operation TEXT,
297
- * timestamp TEXT
364
+ * timestamp TEXT,
298
365
  * value TEXT,
299
366
  * previous_value TEXT
300
367
  * );
@@ -7,7 +7,8 @@ import {
7
7
  DiffTriggerOperation,
8
8
  TrackDiffOptions,
9
9
  TriggerManager,
10
- TriggerRemoveCallback
10
+ TriggerRemoveCallback,
11
+ WithDiffOptions
11
12
  } from './TriggerManager.js';
12
13
 
13
14
  export type TriggerManagerImplOptions = {
@@ -117,6 +118,7 @@ export class TriggerManagerImpl implements TriggerManager {
117
118
  await hooks?.beforeCreate?.(tx);
118
119
  await tx.execute(/* sql */ `
119
120
  CREATE TEMP TABLE ${destination} (
121
+ operation_id INTEGER PRIMARY KEY AUTOINCREMENT,
120
122
  id TEXT,
121
123
  operation TEXT,
122
124
  timestamp TEXT,
@@ -243,17 +245,20 @@ export class TriggerManagerImpl implements TriggerManager {
243
245
  const callbackResult = await options.onChange({
244
246
  ...tx,
245
247
  destinationTable: destination,
246
- withDiff: async <T>(query, params) => {
248
+ withDiff: async <T>(query, params, options?: WithDiffOptions) => {
247
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
+ : '*';
248
253
  const wrappedQuery = /* sql */ `
249
254
  WITH
250
255
  DIFF AS (
251
256
  SELECT
252
- *
257
+ ${operationIdSelect}
253
258
  FROM
254
259
  ${destination}
255
260
  ORDER BY
256
- timestamp ASC
261
+ operation_id ASC
257
262
  ) ${query}
258
263
  `;
259
264
  return tx.getAll<T>(wrappedQuery, params);
@@ -267,13 +272,14 @@ export class TriggerManagerImpl implements TriggerManager {
267
272
  id,
268
273
  ${contextColumns.length > 0
269
274
  ? `${contextColumns.map((col) => `json_extract(value, '$.${col}') as ${col}`).join(', ')},`
270
- : ''} operation as __operation,
275
+ : ''} operation_id as __operation_id,
276
+ operation as __operation,
271
277
  timestamp as __timestamp,
272
278
  previous_value as __previous_value
273
279
  FROM
274
280
  ${destination}
275
281
  ORDER BY
276
- __timestamp ASC
282
+ __operation_id ASC
277
283
  ) ${query}
278
284
  `;
279
285
  return tx.getAll<T>(wrappedQuery, params);