@powersync/common 1.29.0 → 1.31.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.mjs +3 -3
- package/lib/client/sync/bucket/CrudEntry.d.ts +13 -1
- package/lib/client/sync/bucket/CrudEntry.js +16 -2
- package/lib/client/sync/bucket/SqliteBucketStorage.js +2 -2
- package/lib/client/sync/stream/AbstractRemote.d.ts +29 -0
- package/lib/client/sync/stream/AbstractRemote.js +71 -10
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +7 -0
- package/lib/db/crud/SyncProgress.d.ts +1 -1
- package/lib/db/crud/SyncProgress.js +1 -1
- package/lib/db/schema/Schema.d.ts +4 -0
- package/lib/db/schema/Schema.js +1 -10
- package/lib/db/schema/Table.d.ts +34 -8
- package/lib/db/schema/Table.js +48 -9
- package/package.json +1 -1
|
@@ -51,6 +51,11 @@ export declare class CrudEntry {
|
|
|
51
51
|
* Data associated with the change.
|
|
52
52
|
*/
|
|
53
53
|
opData?: Record<string, any>;
|
|
54
|
+
/**
|
|
55
|
+
* For tables where the `trackPreviousValues` option has been enabled, this tracks previous values for
|
|
56
|
+
* `UPDATE` and `DELETE` statements.
|
|
57
|
+
*/
|
|
58
|
+
previousValues?: Record<string, any>;
|
|
54
59
|
/**
|
|
55
60
|
* Table that contained the change.
|
|
56
61
|
*/
|
|
@@ -59,8 +64,15 @@ export declare class CrudEntry {
|
|
|
59
64
|
* Auto-incrementing transaction id. This is the same for all operations within the same transaction.
|
|
60
65
|
*/
|
|
61
66
|
transactionId?: number;
|
|
67
|
+
/**
|
|
68
|
+
* Client-side metadata attached with this write.
|
|
69
|
+
*
|
|
70
|
+
* This field is only available when the `trackMetadata` option was set to `true` when creating a table
|
|
71
|
+
* and the insert or update statement set the `_metadata` column.
|
|
72
|
+
*/
|
|
73
|
+
metadata?: string;
|
|
62
74
|
static fromRow(dbRow: CrudEntryJSON): CrudEntry;
|
|
63
|
-
constructor(clientId: number, op: UpdateType, table: string, id: string, transactionId?: number, opData?: Record<string, any
|
|
75
|
+
constructor(clientId: number, op: UpdateType, table: string, id: string, transactionId?: number, opData?: Record<string, any>, previousValues?: Record<string, any>, metadata?: string);
|
|
64
76
|
/**
|
|
65
77
|
* Converts the change to JSON format.
|
|
66
78
|
*/
|
|
@@ -30,6 +30,11 @@ export class CrudEntry {
|
|
|
30
30
|
* Data associated with the change.
|
|
31
31
|
*/
|
|
32
32
|
opData;
|
|
33
|
+
/**
|
|
34
|
+
* For tables where the `trackPreviousValues` option has been enabled, this tracks previous values for
|
|
35
|
+
* `UPDATE` and `DELETE` statements.
|
|
36
|
+
*/
|
|
37
|
+
previousValues;
|
|
33
38
|
/**
|
|
34
39
|
* Table that contained the change.
|
|
35
40
|
*/
|
|
@@ -38,17 +43,26 @@ export class CrudEntry {
|
|
|
38
43
|
* Auto-incrementing transaction id. This is the same for all operations within the same transaction.
|
|
39
44
|
*/
|
|
40
45
|
transactionId;
|
|
46
|
+
/**
|
|
47
|
+
* Client-side metadata attached with this write.
|
|
48
|
+
*
|
|
49
|
+
* This field is only available when the `trackMetadata` option was set to `true` when creating a table
|
|
50
|
+
* and the insert or update statement set the `_metadata` column.
|
|
51
|
+
*/
|
|
52
|
+
metadata;
|
|
41
53
|
static fromRow(dbRow) {
|
|
42
54
|
const data = JSON.parse(dbRow.data);
|
|
43
|
-
return new CrudEntry(parseInt(dbRow.id), data.op, data.type, data.id, dbRow.tx_id, data.data);
|
|
55
|
+
return new CrudEntry(parseInt(dbRow.id), data.op, data.type, data.id, dbRow.tx_id, data.data, data.old, data.metadata);
|
|
44
56
|
}
|
|
45
|
-
constructor(clientId, op, table, id, transactionId, opData) {
|
|
57
|
+
constructor(clientId, op, table, id, transactionId, opData, previousValues, metadata) {
|
|
46
58
|
this.clientId = clientId;
|
|
47
59
|
this.id = id;
|
|
48
60
|
this.op = op;
|
|
49
61
|
this.opData = opData;
|
|
50
62
|
this.table = table;
|
|
51
63
|
this.transactionId = transactionId;
|
|
64
|
+
this.previousValues = previousValues;
|
|
65
|
+
this.metadata = metadata;
|
|
52
66
|
}
|
|
53
67
|
/**
|
|
54
68
|
* Converts the change to JSON format.
|
|
@@ -119,9 +119,9 @@ export class SqliteBucketStorage extends BaseObserver {
|
|
|
119
119
|
}
|
|
120
120
|
return { ready: false, checkpointValid: false, checkpointFailures: r.checkpointFailures };
|
|
121
121
|
}
|
|
122
|
-
|
|
122
|
+
let buckets = checkpoint.buckets;
|
|
123
123
|
if (priority !== undefined) {
|
|
124
|
-
buckets.filter((b) => hasMatchingPriority(priority, b));
|
|
124
|
+
buckets = buckets.filter((b) => hasMatchingPriority(priority, b));
|
|
125
125
|
}
|
|
126
126
|
const bucketNames = buckets.map((b) => b.bucket);
|
|
127
127
|
await this.writeTransaction(async (tx) => {
|
|
@@ -7,6 +7,7 @@ import { StreamingSyncLine, StreamingSyncRequest } from './streaming-sync-types.
|
|
|
7
7
|
export type BSONImplementation = typeof BSON;
|
|
8
8
|
export type RemoteConnector = {
|
|
9
9
|
fetchCredentials: () => Promise<PowerSyncCredentials | null>;
|
|
10
|
+
invalidateCredentials?: () => void;
|
|
10
11
|
};
|
|
11
12
|
export declare const DEFAULT_REMOTE_LOGGER: Logger.ILogger;
|
|
12
13
|
export type SyncStreamOptions = {
|
|
@@ -74,7 +75,35 @@ export declare abstract class AbstractRemote {
|
|
|
74
75
|
* which can be called to perform fetch requests
|
|
75
76
|
*/
|
|
76
77
|
get fetch(): FetchImplementation;
|
|
78
|
+
/**
|
|
79
|
+
* Get credentials currently cached, or fetch new credentials if none are
|
|
80
|
+
* available.
|
|
81
|
+
*
|
|
82
|
+
* These credentials may have expired already.
|
|
83
|
+
*/
|
|
77
84
|
getCredentials(): Promise<PowerSyncCredentials | null>;
|
|
85
|
+
/**
|
|
86
|
+
* Fetch a new set of credentials and cache it.
|
|
87
|
+
*
|
|
88
|
+
* Until this call succeeds, `getCredentials` will still return the
|
|
89
|
+
* old credentials.
|
|
90
|
+
*
|
|
91
|
+
* This may be called before the current credentials have expired.
|
|
92
|
+
*/
|
|
93
|
+
prefetchCredentials(): Promise<PowerSyncCredentials | null>;
|
|
94
|
+
/**
|
|
95
|
+
* Get credentials for PowerSync.
|
|
96
|
+
*
|
|
97
|
+
* This should always fetch a fresh set of credentials - don't use cached
|
|
98
|
+
* values.
|
|
99
|
+
*/
|
|
100
|
+
fetchCredentials(): Promise<PowerSyncCredentials | null>;
|
|
101
|
+
/***
|
|
102
|
+
* Immediately invalidate credentials.
|
|
103
|
+
*
|
|
104
|
+
* This may be called when the current credentials have expired.
|
|
105
|
+
*/
|
|
106
|
+
invalidateCredentials(): void;
|
|
78
107
|
getUserAgent(): string;
|
|
79
108
|
protected buildRequest(path: string): Promise<{
|
|
80
109
|
url: string;
|
|
@@ -8,8 +8,6 @@ import { AbortOperation } from '../../../utils/AbortOperation.js';
|
|
|
8
8
|
import { DataStream } from '../../../utils/DataStream.js';
|
|
9
9
|
const POWERSYNC_TRAILING_SLASH_MATCH = /\/+$/;
|
|
10
10
|
const POWERSYNC_JS_VERSION = PACKAGE.version;
|
|
11
|
-
// Refresh at least 30 sec before it expires
|
|
12
|
-
const REFRESH_CREDENTIALS_SAFETY_PERIOD_MS = 30_000;
|
|
13
11
|
const SYNC_QUEUE_REQUEST_LOW_WATER = 5;
|
|
14
12
|
// Keep alive message is sent every period
|
|
15
13
|
const KEEP_ALIVE_MS = 20_000;
|
|
@@ -70,17 +68,52 @@ export class AbstractRemote {
|
|
|
70
68
|
? fetchImplementation.getFetch()
|
|
71
69
|
: fetchImplementation;
|
|
72
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Get credentials currently cached, or fetch new credentials if none are
|
|
73
|
+
* available.
|
|
74
|
+
*
|
|
75
|
+
* These credentials may have expired already.
|
|
76
|
+
*/
|
|
73
77
|
async getCredentials() {
|
|
74
|
-
|
|
75
|
-
if (expiresAt && expiresAt > new Date(new Date().valueOf() + REFRESH_CREDENTIALS_SAFETY_PERIOD_MS)) {
|
|
78
|
+
if (this.credentials) {
|
|
76
79
|
return this.credentials;
|
|
77
80
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
return this.prefetchCredentials();
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Fetch a new set of credentials and cache it.
|
|
85
|
+
*
|
|
86
|
+
* Until this call succeeds, `getCredentials` will still return the
|
|
87
|
+
* old credentials.
|
|
88
|
+
*
|
|
89
|
+
* This may be called before the current credentials have expired.
|
|
90
|
+
*/
|
|
91
|
+
async prefetchCredentials() {
|
|
92
|
+
this.credentials = await this.fetchCredentials();
|
|
82
93
|
return this.credentials;
|
|
83
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Get credentials for PowerSync.
|
|
97
|
+
*
|
|
98
|
+
* This should always fetch a fresh set of credentials - don't use cached
|
|
99
|
+
* values.
|
|
100
|
+
*/
|
|
101
|
+
async fetchCredentials() {
|
|
102
|
+
const credentials = await this.connector.fetchCredentials();
|
|
103
|
+
if (credentials?.endpoint.match(POWERSYNC_TRAILING_SLASH_MATCH)) {
|
|
104
|
+
throw new Error(`A trailing forward slash "/" was found in the fetchCredentials endpoint: "${credentials.endpoint}". Remove the trailing forward slash "/" to fix this error.`);
|
|
105
|
+
}
|
|
106
|
+
return credentials;
|
|
107
|
+
}
|
|
108
|
+
/***
|
|
109
|
+
* Immediately invalidate credentials.
|
|
110
|
+
*
|
|
111
|
+
* This may be called when the current credentials have expired.
|
|
112
|
+
*/
|
|
113
|
+
invalidateCredentials() {
|
|
114
|
+
this.credentials = null;
|
|
115
|
+
this.connector.invalidateCredentials?.();
|
|
116
|
+
}
|
|
84
117
|
getUserAgent() {
|
|
85
118
|
return `powersync-js/${POWERSYNC_JS_VERSION}`;
|
|
86
119
|
}
|
|
@@ -114,6 +147,9 @@ export class AbstractRemote {
|
|
|
114
147
|
},
|
|
115
148
|
body: JSON.stringify(data)
|
|
116
149
|
});
|
|
150
|
+
if (res.status === 401) {
|
|
151
|
+
this.invalidateCredentials();
|
|
152
|
+
}
|
|
117
153
|
if (!res.ok) {
|
|
118
154
|
throw new Error(`Received ${res.status} - ${res.statusText} when posting to ${path}: ${await res.text()}}`);
|
|
119
155
|
}
|
|
@@ -128,6 +164,9 @@ export class AbstractRemote {
|
|
|
128
164
|
...request.headers
|
|
129
165
|
}
|
|
130
166
|
});
|
|
167
|
+
if (res.status === 401) {
|
|
168
|
+
this.invalidateCredentials();
|
|
169
|
+
}
|
|
131
170
|
if (!res.ok) {
|
|
132
171
|
throw new Error(`Received ${res.status} - ${res.statusText} when getting from ${path}: ${await res.text()}}`);
|
|
133
172
|
}
|
|
@@ -145,6 +184,9 @@ export class AbstractRemote {
|
|
|
145
184
|
this.logger.error(`Caught ex when POST streaming to ${path}`, ex);
|
|
146
185
|
throw ex;
|
|
147
186
|
});
|
|
187
|
+
if (res.status === 401) {
|
|
188
|
+
this.invalidateCredentials();
|
|
189
|
+
}
|
|
148
190
|
if (!res.ok) {
|
|
149
191
|
const text = await res.text();
|
|
150
192
|
this.logger.error(`Could not POST streaming to ${path} - ${res.status} - ${res.statusText}: ${text}`);
|
|
@@ -169,10 +211,18 @@ export class AbstractRemote {
|
|
|
169
211
|
// headers with websockets on web. The browser userAgent is however added
|
|
170
212
|
// automatically as a header.
|
|
171
213
|
const userAgent = this.getUserAgent();
|
|
214
|
+
let socketCreationError;
|
|
172
215
|
const connector = new RSocketConnector({
|
|
173
216
|
transport: new WebsocketClientTransport({
|
|
174
217
|
url: this.options.socketUrlTransformer(request.url),
|
|
175
|
-
wsCreator: (url) =>
|
|
218
|
+
wsCreator: (url) => {
|
|
219
|
+
const s = this.createSocket(url);
|
|
220
|
+
s.addEventListener('error', (e) => {
|
|
221
|
+
socketCreationError = new Error('Failed to create connection to websocket: ', e.target.url ?? '');
|
|
222
|
+
this.logger.warn('Socket error', e);
|
|
223
|
+
});
|
|
224
|
+
return s;
|
|
225
|
+
}
|
|
176
226
|
}),
|
|
177
227
|
setup: {
|
|
178
228
|
keepAlive: KEEP_ALIVE_MS,
|
|
@@ -197,7 +247,7 @@ export class AbstractRemote {
|
|
|
197
247
|
* On React native the connection exception can be `undefined` this causes issues
|
|
198
248
|
* with detecting the exception inside async-mutex
|
|
199
249
|
*/
|
|
200
|
-
throw new Error(`Could not connect to PowerSync instance: ${JSON.stringify(ex)}`);
|
|
250
|
+
throw new Error(`Could not connect to PowerSync instance: ${JSON.stringify(ex ?? socketCreationError)}`);
|
|
201
251
|
}
|
|
202
252
|
const stream = new DataStream({
|
|
203
253
|
logger: this.logger,
|
|
@@ -233,6 +283,17 @@ export class AbstractRemote {
|
|
|
233
283
|
}, syncQueueRequestSize, // The initial N amount
|
|
234
284
|
{
|
|
235
285
|
onError: (e) => {
|
|
286
|
+
if (e.message.includes('PSYNC_')) {
|
|
287
|
+
if (e.message.includes('PSYNC_S21')) {
|
|
288
|
+
this.invalidateCredentials();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
// Possible that connection is with an older service, always invalidate to be safe
|
|
293
|
+
if (e.message !== 'Closed. ') {
|
|
294
|
+
this.invalidateCredentials();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
236
297
|
// Don't log closed as an error
|
|
237
298
|
if (e.message !== 'Closed. ') {
|
|
238
299
|
this.logger.error(e);
|
|
@@ -514,6 +514,7 @@ The next upload iteration will be delayed.`);
|
|
|
514
514
|
if (remaining_seconds == 0) {
|
|
515
515
|
// Connection would be closed automatically right after this
|
|
516
516
|
this.logger.debug('Token expiring; reconnect');
|
|
517
|
+
this.options.remote.invalidateCredentials();
|
|
517
518
|
/**
|
|
518
519
|
* For a rare case where the backend connector does not update the token
|
|
519
520
|
* (uses the same one), this should have some delay.
|
|
@@ -521,6 +522,12 @@ The next upload iteration will be delayed.`);
|
|
|
521
522
|
await this.delayRetry();
|
|
522
523
|
return;
|
|
523
524
|
}
|
|
525
|
+
else if (remaining_seconds < 30) {
|
|
526
|
+
this.logger.debug('Token will expire soon; reconnect');
|
|
527
|
+
// Pre-emptively refresh the token
|
|
528
|
+
this.options.remote.invalidateCredentials();
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
524
531
|
this.triggerCrudUpload();
|
|
525
532
|
}
|
|
526
533
|
else {
|
|
@@ -44,7 +44,7 @@ export interface ProgressWithOperations {
|
|
|
44
44
|
* Additionally, the {@link SyncProgress.untilPriority} method can be used to otbain progress towards
|
|
45
45
|
* a specific priority (instead of the progress for the entire download).
|
|
46
46
|
*
|
|
47
|
-
* The reported progress always reflects the status towards
|
|
47
|
+
* The reported progress always reflects the status towards the end of a sync iteration (after
|
|
48
48
|
* which a consistent snapshot of all buckets is available locally).
|
|
49
49
|
*
|
|
50
50
|
* In rare cases (in particular, when a [compacting](https://docs.powersync.com/usage/lifecycle-maintenance/compacting-buckets)
|
|
@@ -10,7 +10,7 @@ export const FULL_SYNC_PRIORITY = 2147483647;
|
|
|
10
10
|
* Additionally, the {@link SyncProgress.untilPriority} method can be used to otbain progress towards
|
|
11
11
|
* a specific priority (instead of the progress for the entire download).
|
|
12
12
|
*
|
|
13
|
-
* The reported progress always reflects the status towards
|
|
13
|
+
* The reported progress always reflects the status towards the end of a sync iteration (after
|
|
14
14
|
* which a consistent snapshot of all buckets is available locally).
|
|
15
15
|
*
|
|
16
16
|
* In rare cases (in particular, when a [compacting](https://docs.powersync.com/usage/lifecycle-maintenance/compacting-buckets)
|
|
@@ -18,6 +18,10 @@ export declare class Schema<S extends SchemaType = SchemaType> {
|
|
|
18
18
|
view_name: string;
|
|
19
19
|
local_only: boolean;
|
|
20
20
|
insert_only: boolean;
|
|
21
|
+
include_old: any;
|
|
22
|
+
include_old_only_when_changed: boolean;
|
|
23
|
+
include_metadata: boolean;
|
|
24
|
+
ignore_empty_update: boolean;
|
|
21
25
|
columns: {
|
|
22
26
|
name: string;
|
|
23
27
|
type: import("./Column.js").ColumnType | undefined;
|
package/lib/db/schema/Schema.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { Table } from './Table.js';
|
|
2
1
|
/**
|
|
3
2
|
* A schema is a collection of tables. It is used to define the structure of a database.
|
|
4
3
|
*/
|
|
@@ -41,15 +40,7 @@ export class Schema {
|
|
|
41
40
|
}
|
|
42
41
|
convertToClassicTables(props) {
|
|
43
42
|
return Object.entries(props).map(([name, table]) => {
|
|
44
|
-
|
|
45
|
-
name,
|
|
46
|
-
columns: table.columns,
|
|
47
|
-
indexes: table.indexes,
|
|
48
|
-
localOnly: table.localOnly,
|
|
49
|
-
insertOnly: table.insertOnly,
|
|
50
|
-
viewName: table.viewNameOverride || name
|
|
51
|
-
});
|
|
52
|
-
return convertedTable;
|
|
43
|
+
return table.copyWithName(name);
|
|
53
44
|
});
|
|
54
45
|
}
|
|
55
46
|
}
|
package/lib/db/schema/Table.d.ts
CHANGED
|
@@ -1,16 +1,32 @@
|
|
|
1
1
|
import { Column, ColumnsType, ColumnType, ExtractColumnValueType } from './Column.js';
|
|
2
2
|
import { Index } from './Index.js';
|
|
3
3
|
import { TableV2 } from './TableV2.js';
|
|
4
|
-
|
|
4
|
+
interface SharedTableOptions {
|
|
5
|
+
localOnly?: boolean;
|
|
6
|
+
insertOnly?: boolean;
|
|
7
|
+
viewName?: string;
|
|
8
|
+
trackPrevious?: boolean | TrackPreviousOptions;
|
|
9
|
+
trackMetadata?: boolean;
|
|
10
|
+
ignoreEmptyUpdates?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/** Whether to include previous column values when PowerSync tracks local changes.
|
|
13
|
+
*
|
|
14
|
+
* Including old values may be helpful for some backend connector implementations, which is
|
|
15
|
+
* why it can be enabled on per-table or per-columm basis.
|
|
16
|
+
*/
|
|
17
|
+
export interface TrackPreviousOptions {
|
|
18
|
+
/** When defined, a list of column names for which old values should be tracked. */
|
|
19
|
+
columns?: string[];
|
|
20
|
+
/** When enabled, only include values that have actually been changed by an update. */
|
|
21
|
+
onlyWhenChanged?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface TableOptions extends SharedTableOptions {
|
|
5
24
|
/**
|
|
6
25
|
* The synced table name, matching sync rules
|
|
7
26
|
*/
|
|
8
27
|
name: string;
|
|
9
28
|
columns: Column[];
|
|
10
29
|
indexes?: Index[];
|
|
11
|
-
localOnly?: boolean;
|
|
12
|
-
insertOnly?: boolean;
|
|
13
|
-
viewName?: string;
|
|
14
30
|
}
|
|
15
31
|
export type RowType<T extends TableV2<any>> = {
|
|
16
32
|
[K in keyof T['columnMap']]: ExtractColumnValueType<T['columnMap'][K]>;
|
|
@@ -18,16 +34,16 @@ export type RowType<T extends TableV2<any>> = {
|
|
|
18
34
|
id: string;
|
|
19
35
|
};
|
|
20
36
|
export type IndexShorthand = Record<string, string[]>;
|
|
21
|
-
export interface TableV2Options {
|
|
37
|
+
export interface TableV2Options extends SharedTableOptions {
|
|
22
38
|
indexes?: IndexShorthand;
|
|
23
|
-
localOnly?: boolean;
|
|
24
|
-
insertOnly?: boolean;
|
|
25
|
-
viewName?: string;
|
|
26
39
|
}
|
|
27
40
|
export declare const DEFAULT_TABLE_OPTIONS: {
|
|
28
41
|
indexes: never[];
|
|
29
42
|
insertOnly: boolean;
|
|
30
43
|
localOnly: boolean;
|
|
44
|
+
trackPrevious: boolean;
|
|
45
|
+
trackMetadata: boolean;
|
|
46
|
+
ignoreEmptyUpdates: boolean;
|
|
31
47
|
};
|
|
32
48
|
export declare const InvalidSQLCharacters: RegExp;
|
|
33
49
|
export declare class Table<Columns extends ColumnsType = ColumnsType> {
|
|
@@ -96,9 +112,11 @@ export declare class Table<Columns extends ColumnsType = ColumnsType> {
|
|
|
96
112
|
*```
|
|
97
113
|
*/
|
|
98
114
|
constructor(options: TableOptions);
|
|
115
|
+
copyWithName(name: string): Table;
|
|
99
116
|
private isTableV1;
|
|
100
117
|
private initTableV1;
|
|
101
118
|
private initTableV2;
|
|
119
|
+
private applyDefaultOptions;
|
|
102
120
|
get name(): string;
|
|
103
121
|
get viewNameOverride(): string | undefined;
|
|
104
122
|
get viewName(): string;
|
|
@@ -107,6 +125,9 @@ export declare class Table<Columns extends ColumnsType = ColumnsType> {
|
|
|
107
125
|
get indexes(): Index[];
|
|
108
126
|
get localOnly(): boolean;
|
|
109
127
|
get insertOnly(): boolean;
|
|
128
|
+
get trackPrevious(): boolean | TrackPreviousOptions;
|
|
129
|
+
get trackMetadata(): boolean;
|
|
130
|
+
get ignoreEmptyUpdates(): boolean;
|
|
110
131
|
get internalName(): string;
|
|
111
132
|
get validName(): boolean;
|
|
112
133
|
validate(): void;
|
|
@@ -115,6 +136,10 @@ export declare class Table<Columns extends ColumnsType = ColumnsType> {
|
|
|
115
136
|
view_name: string;
|
|
116
137
|
local_only: boolean;
|
|
117
138
|
insert_only: boolean;
|
|
139
|
+
include_old: any;
|
|
140
|
+
include_old_only_when_changed: boolean;
|
|
141
|
+
include_metadata: boolean;
|
|
142
|
+
ignore_empty_update: boolean;
|
|
118
143
|
columns: {
|
|
119
144
|
name: string;
|
|
120
145
|
type: ColumnType | undefined;
|
|
@@ -129,3 +154,4 @@ export declare class Table<Columns extends ColumnsType = ColumnsType> {
|
|
|
129
154
|
}[];
|
|
130
155
|
};
|
|
131
156
|
}
|
|
157
|
+
export {};
|
package/lib/db/schema/Table.js
CHANGED
|
@@ -4,7 +4,10 @@ import { IndexedColumn } from './IndexedColumn.js';
|
|
|
4
4
|
export const DEFAULT_TABLE_OPTIONS = {
|
|
5
5
|
indexes: [],
|
|
6
6
|
insertOnly: false,
|
|
7
|
-
localOnly: false
|
|
7
|
+
localOnly: false,
|
|
8
|
+
trackPrevious: false,
|
|
9
|
+
trackMetadata: false,
|
|
10
|
+
ignoreEmptyUpdates: false
|
|
8
11
|
};
|
|
9
12
|
export const InvalidSQLCharacters = /["'%,.#\s[\]]/;
|
|
10
13
|
export class Table {
|
|
@@ -41,16 +44,21 @@ export class Table {
|
|
|
41
44
|
this.initTableV2(optionsOrColumns, v2Options);
|
|
42
45
|
}
|
|
43
46
|
}
|
|
47
|
+
copyWithName(name) {
|
|
48
|
+
return new Table({
|
|
49
|
+
...this.options,
|
|
50
|
+
name
|
|
51
|
+
});
|
|
52
|
+
}
|
|
44
53
|
isTableV1(arg) {
|
|
45
54
|
return 'columns' in arg && Array.isArray(arg.columns);
|
|
46
55
|
}
|
|
47
56
|
initTableV1(options) {
|
|
48
57
|
this.options = {
|
|
49
58
|
...options,
|
|
50
|
-
indexes: options.indexes || []
|
|
51
|
-
insertOnly: options.insertOnly ?? DEFAULT_TABLE_OPTIONS.insertOnly,
|
|
52
|
-
localOnly: options.localOnly ?? DEFAULT_TABLE_OPTIONS.localOnly
|
|
59
|
+
indexes: options.indexes || []
|
|
53
60
|
};
|
|
61
|
+
this.applyDefaultOptions();
|
|
54
62
|
}
|
|
55
63
|
initTableV2(columns, options) {
|
|
56
64
|
const convertedColumns = Object.entries(columns).map(([name, columnInfo]) => new Column({ name, type: columnInfo.type }));
|
|
@@ -65,12 +73,23 @@ export class Table {
|
|
|
65
73
|
name: '',
|
|
66
74
|
columns: convertedColumns,
|
|
67
75
|
indexes: convertedIndexes,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
viewName: options?.viewName,
|
|
77
|
+
insertOnly: options?.insertOnly,
|
|
78
|
+
localOnly: options?.localOnly,
|
|
79
|
+
trackPrevious: options?.trackPrevious,
|
|
80
|
+
trackMetadata: options?.trackMetadata,
|
|
81
|
+
ignoreEmptyUpdates: options?.ignoreEmptyUpdates
|
|
71
82
|
};
|
|
83
|
+
this.applyDefaultOptions();
|
|
72
84
|
this._mappedColumns = columns;
|
|
73
85
|
}
|
|
86
|
+
applyDefaultOptions() {
|
|
87
|
+
this.options.insertOnly ??= DEFAULT_TABLE_OPTIONS.insertOnly;
|
|
88
|
+
this.options.localOnly ??= DEFAULT_TABLE_OPTIONS.localOnly;
|
|
89
|
+
this.options.trackPrevious ??= DEFAULT_TABLE_OPTIONS.trackPrevious;
|
|
90
|
+
this.options.trackMetadata ??= DEFAULT_TABLE_OPTIONS.trackMetadata;
|
|
91
|
+
this.options.ignoreEmptyUpdates ??= DEFAULT_TABLE_OPTIONS.ignoreEmptyUpdates;
|
|
92
|
+
}
|
|
74
93
|
get name() {
|
|
75
94
|
return this.options.name;
|
|
76
95
|
}
|
|
@@ -94,10 +113,19 @@ export class Table {
|
|
|
94
113
|
return this.options.indexes ?? [];
|
|
95
114
|
}
|
|
96
115
|
get localOnly() {
|
|
97
|
-
return this.options.localOnly
|
|
116
|
+
return this.options.localOnly;
|
|
98
117
|
}
|
|
99
118
|
get insertOnly() {
|
|
100
|
-
return this.options.insertOnly
|
|
119
|
+
return this.options.insertOnly;
|
|
120
|
+
}
|
|
121
|
+
get trackPrevious() {
|
|
122
|
+
return this.options.trackPrevious;
|
|
123
|
+
}
|
|
124
|
+
get trackMetadata() {
|
|
125
|
+
return this.options.trackMetadata;
|
|
126
|
+
}
|
|
127
|
+
get ignoreEmptyUpdates() {
|
|
128
|
+
return this.options.ignoreEmptyUpdates;
|
|
101
129
|
}
|
|
102
130
|
get internalName() {
|
|
103
131
|
if (this.options.localOnly) {
|
|
@@ -124,6 +152,12 @@ export class Table {
|
|
|
124
152
|
if (this.columns.length > MAX_AMOUNT_OF_COLUMNS) {
|
|
125
153
|
throw new Error(`Table has too many columns. The maximum number of columns is ${MAX_AMOUNT_OF_COLUMNS}.`);
|
|
126
154
|
}
|
|
155
|
+
if (this.trackMetadata && this.localOnly) {
|
|
156
|
+
throw new Error(`Can't include metadata for local-only tables.`);
|
|
157
|
+
}
|
|
158
|
+
if (this.trackPrevious != false && this.localOnly) {
|
|
159
|
+
throw new Error(`Can't include old values for local-only tables.`);
|
|
160
|
+
}
|
|
127
161
|
const columnNames = new Set();
|
|
128
162
|
columnNames.add('id');
|
|
129
163
|
for (const column of this.columns) {
|
|
@@ -156,11 +190,16 @@ export class Table {
|
|
|
156
190
|
}
|
|
157
191
|
}
|
|
158
192
|
toJSON() {
|
|
193
|
+
const trackPrevious = this.trackPrevious;
|
|
159
194
|
return {
|
|
160
195
|
name: this.name,
|
|
161
196
|
view_name: this.viewName,
|
|
162
197
|
local_only: this.localOnly,
|
|
163
198
|
insert_only: this.insertOnly,
|
|
199
|
+
include_old: trackPrevious && (trackPrevious.columns ?? true),
|
|
200
|
+
include_old_only_when_changed: typeof trackPrevious == 'object' && trackPrevious.onlyWhenChanged == true,
|
|
201
|
+
include_metadata: this.trackMetadata,
|
|
202
|
+
ignore_empty_update: this.ignoreEmptyUpdates,
|
|
164
203
|
columns: this.columns.map((c) => c.toJSON()),
|
|
165
204
|
indexes: this.indexes.map((e) => e.toJSON(this))
|
|
166
205
|
};
|