@powersync/common 1.46.0 → 1.48.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/README.md +5 -1
- package/dist/bundle.cjs +1298 -395
- package/dist/bundle.cjs.map +1 -1
- package/dist/bundle.mjs +1291 -395
- package/dist/bundle.mjs.map +1 -1
- package/dist/bundle.node.cjs +1298 -395
- package/dist/bundle.node.cjs.map +1 -1
- package/dist/bundle.node.mjs +1291 -395
- package/dist/bundle.node.mjs.map +1 -1
- package/dist/index.d.cts +652 -106
- package/lib/attachments/AttachmentContext.d.ts +86 -0
- package/lib/attachments/AttachmentContext.js +229 -0
- package/lib/attachments/AttachmentContext.js.map +1 -0
- package/lib/attachments/AttachmentErrorHandler.d.ts +31 -0
- package/lib/attachments/AttachmentErrorHandler.js +2 -0
- package/lib/attachments/AttachmentErrorHandler.js.map +1 -0
- package/lib/attachments/AttachmentQueue.d.ts +149 -0
- package/lib/attachments/AttachmentQueue.js +362 -0
- package/lib/attachments/AttachmentQueue.js.map +1 -0
- package/lib/attachments/AttachmentService.d.ts +29 -0
- package/lib/attachments/AttachmentService.js +56 -0
- package/lib/attachments/AttachmentService.js.map +1 -0
- package/lib/attachments/LocalStorageAdapter.d.ts +62 -0
- package/lib/attachments/LocalStorageAdapter.js +6 -0
- package/lib/attachments/LocalStorageAdapter.js.map +1 -0
- package/lib/attachments/RemoteStorageAdapter.d.ts +27 -0
- package/lib/attachments/RemoteStorageAdapter.js +2 -0
- package/lib/attachments/RemoteStorageAdapter.js.map +1 -0
- package/lib/attachments/Schema.d.ts +50 -0
- package/lib/attachments/Schema.js +62 -0
- package/lib/attachments/Schema.js.map +1 -0
- package/lib/attachments/SyncingService.d.ts +62 -0
- package/lib/attachments/SyncingService.js +168 -0
- package/lib/attachments/SyncingService.js.map +1 -0
- package/lib/attachments/WatchedAttachmentItem.d.ts +17 -0
- package/lib/attachments/WatchedAttachmentItem.js +2 -0
- package/lib/attachments/WatchedAttachmentItem.js.map +1 -0
- package/lib/db/schema/RawTable.d.ts +61 -26
- package/lib/db/schema/RawTable.js +1 -32
- package/lib/db/schema/RawTable.js.map +1 -1
- package/lib/db/schema/Schema.d.ts +14 -7
- package/lib/db/schema/Schema.js +25 -3
- package/lib/db/schema/Schema.js.map +1 -1
- package/lib/db/schema/Table.d.ts +13 -8
- package/lib/db/schema/Table.js +3 -8
- package/lib/db/schema/Table.js.map +1 -1
- package/lib/db/schema/internal.d.ts +12 -0
- package/lib/db/schema/internal.js +15 -0
- package/lib/db/schema/internal.js.map +1 -0
- package/lib/index.d.ts +11 -1
- package/lib/index.js +10 -1
- package/lib/index.js.map +1 -1
- package/lib/utils/mutex.d.ts +1 -1
- package/lib/utils/mutex.js.map +1 -1
- package/package.json +1 -1
- package/src/attachments/AttachmentContext.ts +279 -0
- package/src/attachments/AttachmentErrorHandler.ts +34 -0
- package/src/attachments/AttachmentQueue.ts +472 -0
- package/src/attachments/AttachmentService.ts +62 -0
- package/src/attachments/LocalStorageAdapter.ts +72 -0
- package/src/attachments/README.md +718 -0
- package/src/attachments/RemoteStorageAdapter.ts +30 -0
- package/src/attachments/Schema.ts +87 -0
- package/src/attachments/SyncingService.ts +193 -0
- package/src/attachments/WatchedAttachmentItem.ts +19 -0
- package/src/db/schema/RawTable.ts +66 -31
- package/src/db/schema/Schema.ts +27 -2
- package/src/db/schema/Table.ts +11 -11
- package/src/db/schema/internal.ts +17 -0
- package/src/index.ts +12 -1
- package/src/utils/mutex.ts +1 -1
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { AttachmentRecord } from './Schema.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* RemoteStorageAdapter defines the interface for remote storage operations.
|
|
5
|
+
* Implementations handle uploading, downloading, and deleting files from remote storage.
|
|
6
|
+
*
|
|
7
|
+
* @experimental
|
|
8
|
+
* @alpha This is currently experimental and may change without a major version bump.
|
|
9
|
+
*/
|
|
10
|
+
export interface RemoteStorageAdapter {
|
|
11
|
+
/**
|
|
12
|
+
* Uploads a file to remote storage.
|
|
13
|
+
* @param fileData The binary content of the file to upload
|
|
14
|
+
* @param attachment The associated attachment metadata
|
|
15
|
+
*/
|
|
16
|
+
uploadFile(fileData: ArrayBuffer, attachment: AttachmentRecord): Promise<void>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Downloads a file from remote storage.
|
|
20
|
+
* @param attachment The attachment describing the file to download
|
|
21
|
+
* @returns The binary data of the downloaded file
|
|
22
|
+
*/
|
|
23
|
+
downloadFile(attachment: AttachmentRecord): Promise<ArrayBuffer>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Deletes a file from remote storage.
|
|
27
|
+
* @param attachment The attachment describing the file to delete
|
|
28
|
+
*/
|
|
29
|
+
deleteFile(attachment: AttachmentRecord): Promise<void>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { column } from '../db/schema/Column.js';
|
|
2
|
+
import { Table } from '../db/schema/Table.js';
|
|
3
|
+
import { TableV2Options } from '../db/schema/Table.js';
|
|
4
|
+
|
|
5
|
+
export const ATTACHMENT_TABLE = 'attachments';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* AttachmentRecord represents an attachment in the local database.
|
|
9
|
+
*
|
|
10
|
+
* @experimental
|
|
11
|
+
*/
|
|
12
|
+
export interface AttachmentRecord {
|
|
13
|
+
id: string;
|
|
14
|
+
filename: string;
|
|
15
|
+
localUri?: string;
|
|
16
|
+
size?: number;
|
|
17
|
+
mediaType?: string;
|
|
18
|
+
timestamp?: number;
|
|
19
|
+
metaData?: string;
|
|
20
|
+
hasSynced?: boolean;
|
|
21
|
+
state: AttachmentState;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Maps a database row to an AttachmentRecord.
|
|
26
|
+
*
|
|
27
|
+
* @param row - The database row object
|
|
28
|
+
* @returns The corresponding AttachmentRecord
|
|
29
|
+
*
|
|
30
|
+
* @experimental
|
|
31
|
+
*/
|
|
32
|
+
export function attachmentFromSql(row: any): AttachmentRecord {
|
|
33
|
+
return {
|
|
34
|
+
id: row.id,
|
|
35
|
+
filename: row.filename,
|
|
36
|
+
localUri: row.local_uri,
|
|
37
|
+
size: row.size,
|
|
38
|
+
mediaType: row.media_type,
|
|
39
|
+
timestamp: row.timestamp,
|
|
40
|
+
metaData: row.meta_data,
|
|
41
|
+
hasSynced: row.has_synced === 1,
|
|
42
|
+
state: row.state
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* AttachmentState represents the current synchronization state of an attachment.
|
|
48
|
+
*
|
|
49
|
+
* @experimental
|
|
50
|
+
*/
|
|
51
|
+
export enum AttachmentState {
|
|
52
|
+
QUEUED_UPLOAD = 0, // Attachment to be uploaded
|
|
53
|
+
QUEUED_DOWNLOAD = 1, // Attachment to be downloaded
|
|
54
|
+
QUEUED_DELETE = 2, // Attachment to be deleted
|
|
55
|
+
SYNCED = 3, // Attachment has been synced
|
|
56
|
+
ARCHIVED = 4 // Attachment has been orphaned, i.e. the associated record has been deleted
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface AttachmentTableOptions extends Omit<TableV2Options, 'name' | 'columns'> {}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* AttachmentTable defines the schema for the attachment queue table.
|
|
63
|
+
*
|
|
64
|
+
* @internal
|
|
65
|
+
*/
|
|
66
|
+
export class AttachmentTable extends Table {
|
|
67
|
+
constructor(options?: AttachmentTableOptions) {
|
|
68
|
+
super(
|
|
69
|
+
{
|
|
70
|
+
filename: column.text,
|
|
71
|
+
local_uri: column.text,
|
|
72
|
+
timestamp: column.integer,
|
|
73
|
+
size: column.integer,
|
|
74
|
+
media_type: column.text,
|
|
75
|
+
state: column.integer, // Corresponds to AttachmentState
|
|
76
|
+
has_synced: column.integer,
|
|
77
|
+
meta_data: column.text
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
...options,
|
|
81
|
+
viewName: options?.viewName ?? ATTACHMENT_TABLE,
|
|
82
|
+
localOnly: true,
|
|
83
|
+
insertOnly: false
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { ILogger } from '../utils/Logger.js';
|
|
2
|
+
import { AttachmentService } from './AttachmentService.js';
|
|
3
|
+
import { LocalStorageAdapter } from './LocalStorageAdapter.js';
|
|
4
|
+
import { RemoteStorageAdapter } from './RemoteStorageAdapter.js';
|
|
5
|
+
import { AttachmentRecord, AttachmentState } from './Schema.js';
|
|
6
|
+
import { AttachmentErrorHandler } from './AttachmentErrorHandler.js';
|
|
7
|
+
import { AttachmentContext } from './AttachmentContext.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Orchestrates attachment synchronization between local and remote storage.
|
|
11
|
+
* Handles uploads, downloads, deletions, and state transitions.
|
|
12
|
+
*
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
export class SyncingService {
|
|
16
|
+
private attachmentService: AttachmentService;
|
|
17
|
+
private localStorage: LocalStorageAdapter;
|
|
18
|
+
private remoteStorage: RemoteStorageAdapter;
|
|
19
|
+
private logger: ILogger;
|
|
20
|
+
private errorHandler?: AttachmentErrorHandler;
|
|
21
|
+
|
|
22
|
+
constructor(
|
|
23
|
+
attachmentService: AttachmentService,
|
|
24
|
+
localStorage: LocalStorageAdapter,
|
|
25
|
+
remoteStorage: RemoteStorageAdapter,
|
|
26
|
+
logger: ILogger,
|
|
27
|
+
errorHandler?: AttachmentErrorHandler
|
|
28
|
+
) {
|
|
29
|
+
this.attachmentService = attachmentService;
|
|
30
|
+
this.localStorage = localStorage;
|
|
31
|
+
this.remoteStorage = remoteStorage;
|
|
32
|
+
this.logger = logger;
|
|
33
|
+
this.errorHandler = errorHandler;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Processes attachments based on their state (upload, download, or delete).
|
|
38
|
+
* All updates are saved in a single batch after processing.
|
|
39
|
+
*
|
|
40
|
+
* @param attachments - Array of attachment records to process
|
|
41
|
+
* @param context - Attachment context for database operations
|
|
42
|
+
* @returns Promise that resolves when all attachments have been processed and saved
|
|
43
|
+
*/
|
|
44
|
+
async processAttachments(attachments: AttachmentRecord[], context: AttachmentContext): Promise<void> {
|
|
45
|
+
const updatedAttachments: AttachmentRecord[] = [];
|
|
46
|
+
for (const attachment of attachments) {
|
|
47
|
+
switch (attachment.state) {
|
|
48
|
+
case AttachmentState.QUEUED_UPLOAD:
|
|
49
|
+
const uploaded = await this.uploadAttachment(attachment);
|
|
50
|
+
updatedAttachments.push(uploaded);
|
|
51
|
+
break;
|
|
52
|
+
case AttachmentState.QUEUED_DOWNLOAD:
|
|
53
|
+
const downloaded = await this.downloadAttachment(attachment);
|
|
54
|
+
updatedAttachments.push(downloaded);
|
|
55
|
+
break;
|
|
56
|
+
case AttachmentState.QUEUED_DELETE:
|
|
57
|
+
const deleted = await this.deleteAttachment(attachment);
|
|
58
|
+
updatedAttachments.push(deleted);
|
|
59
|
+
break;
|
|
60
|
+
|
|
61
|
+
default:
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await context.saveAttachments(updatedAttachments);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Uploads an attachment from local storage to remote storage.
|
|
71
|
+
* On success, marks as SYNCED. On failure, defers to error handler or archives.
|
|
72
|
+
*
|
|
73
|
+
* @param attachment - The attachment record to upload
|
|
74
|
+
* @returns Updated attachment record with new state
|
|
75
|
+
* @throws Error if the attachment has no localUri
|
|
76
|
+
*/
|
|
77
|
+
async uploadAttachment(attachment: AttachmentRecord): Promise<AttachmentRecord> {
|
|
78
|
+
this.logger.info(`Uploading attachment ${attachment.filename}`);
|
|
79
|
+
try {
|
|
80
|
+
if (attachment.localUri == null) {
|
|
81
|
+
throw new Error(`No localUri for attachment ${attachment.id}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const fileBlob = await this.localStorage.readFile(attachment.localUri);
|
|
85
|
+
await this.remoteStorage.uploadFile(fileBlob, attachment);
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
...attachment,
|
|
89
|
+
state: AttachmentState.SYNCED,
|
|
90
|
+
hasSynced: true
|
|
91
|
+
};
|
|
92
|
+
} catch (error) {
|
|
93
|
+
const shouldRetry = (await this.errorHandler?.onUploadError(attachment, error)) ?? true;
|
|
94
|
+
if (!shouldRetry) {
|
|
95
|
+
return {
|
|
96
|
+
...attachment,
|
|
97
|
+
state: AttachmentState.ARCHIVED
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return attachment;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Downloads an attachment from remote storage to local storage.
|
|
107
|
+
* Retrieves the file, converts to base64, and saves locally.
|
|
108
|
+
* On success, marks as SYNCED. On failure, defers to error handler or archives.
|
|
109
|
+
*
|
|
110
|
+
* @param attachment - The attachment record to download
|
|
111
|
+
* @returns Updated attachment record with local URI and new state
|
|
112
|
+
*/
|
|
113
|
+
async downloadAttachment(attachment: AttachmentRecord): Promise<AttachmentRecord> {
|
|
114
|
+
this.logger.info(`Downloading attachment ${attachment.filename}`);
|
|
115
|
+
try {
|
|
116
|
+
const fileData = await this.remoteStorage.downloadFile(attachment);
|
|
117
|
+
|
|
118
|
+
const localUri = this.localStorage.getLocalUri(attachment.filename);
|
|
119
|
+
await this.localStorage.saveFile(localUri, fileData);
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
...attachment,
|
|
123
|
+
state: AttachmentState.SYNCED,
|
|
124
|
+
localUri: localUri,
|
|
125
|
+
hasSynced: true
|
|
126
|
+
};
|
|
127
|
+
} catch (error) {
|
|
128
|
+
const shouldRetry = (await this.errorHandler?.onDownloadError(attachment, error)) ?? true;
|
|
129
|
+
if (!shouldRetry) {
|
|
130
|
+
return {
|
|
131
|
+
...attachment,
|
|
132
|
+
state: AttachmentState.ARCHIVED
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return attachment;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Deletes an attachment from both remote and local storage.
|
|
142
|
+
* Removes the remote file, local file (if exists), and the attachment record.
|
|
143
|
+
* On failure, defers to error handler or archives.
|
|
144
|
+
*
|
|
145
|
+
* @param attachment - The attachment record to delete
|
|
146
|
+
* @returns Updated attachment record
|
|
147
|
+
*/
|
|
148
|
+
async deleteAttachment(attachment: AttachmentRecord): Promise<AttachmentRecord> {
|
|
149
|
+
try {
|
|
150
|
+
await this.remoteStorage.deleteFile(attachment);
|
|
151
|
+
if (attachment.localUri) {
|
|
152
|
+
await this.localStorage.deleteFile(attachment.localUri);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
await this.attachmentService.withContext(async (ctx) => {
|
|
156
|
+
await ctx.deleteAttachment(attachment.id);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
...attachment,
|
|
161
|
+
state: AttachmentState.ARCHIVED
|
|
162
|
+
};
|
|
163
|
+
} catch (error) {
|
|
164
|
+
const shouldRetry = (await this.errorHandler?.onDeleteError(attachment, error)) ?? true;
|
|
165
|
+
if (!shouldRetry) {
|
|
166
|
+
return {
|
|
167
|
+
...attachment,
|
|
168
|
+
state: AttachmentState.ARCHIVED
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return attachment;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Performs cleanup of archived attachments by removing their local files and records.
|
|
178
|
+
* Errors during local file deletion are logged but do not prevent record deletion.
|
|
179
|
+
*/
|
|
180
|
+
async deleteArchivedAttachments(context: AttachmentContext): Promise<boolean> {
|
|
181
|
+
return await context.deleteArchivedAttachments(async (archivedAttachments) => {
|
|
182
|
+
for (const attachment of archivedAttachments) {
|
|
183
|
+
if (attachment.localUri) {
|
|
184
|
+
try {
|
|
185
|
+
await this.localStorage.deleteFile(attachment.localUri);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
this.logger.error('Error deleting local file for archived attachment', error);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WatchedAttachmentItem represents an attachment reference in your application's data model.
|
|
3
|
+
* Use either filename OR fileExtension (not both).
|
|
4
|
+
*
|
|
5
|
+
* @experimental
|
|
6
|
+
*/
|
|
7
|
+
export type WatchedAttachmentItem =
|
|
8
|
+
| {
|
|
9
|
+
id: string;
|
|
10
|
+
filename: string;
|
|
11
|
+
fileExtension?: never;
|
|
12
|
+
metaData?: string;
|
|
13
|
+
}
|
|
14
|
+
| {
|
|
15
|
+
id: string;
|
|
16
|
+
fileExtension: string;
|
|
17
|
+
filename?: never;
|
|
18
|
+
metaData?: string;
|
|
19
|
+
};
|
|
@@ -1,8 +1,23 @@
|
|
|
1
|
+
import { TableOrRawTableOptions } from './Table.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
+
* Instructs PowerSync to sync data into a "raw" table.
|
|
5
|
+
*
|
|
6
|
+
* Since raw tables are not backed by JSON, running complex queries on them may be more efficient. Further, they allow
|
|
7
|
+
* using client-side table and column constraints.
|
|
8
|
+
*
|
|
9
|
+
* To collect local writes to raw tables with PowerSync, custom triggers are required. See
|
|
10
|
+
* {@link https://docs.powersync.com/usage/use-case-examples/raw-tables the documentation} for details and an example on
|
|
11
|
+
* using raw tables.
|
|
12
|
+
*
|
|
13
|
+
* Note that raw tables are only supported when using the new `SyncClientImplementation.rust` sync client.
|
|
14
|
+
*
|
|
15
|
+
* @experimental Please note that this feature is experimental at the moment, and not covered by PowerSync semver or
|
|
16
|
+
* stability guarantees.
|
|
4
17
|
*/
|
|
5
|
-
export type RawTableType =
|
|
18
|
+
export type RawTableType = RawTableTypeWithStatements | InferredRawTableType;
|
|
19
|
+
|
|
20
|
+
interface RawTableTypeWithStatements {
|
|
6
21
|
/**
|
|
7
22
|
* The statement to run when PowerSync detects that a row needs to be inserted or updated.
|
|
8
23
|
*/
|
|
@@ -11,7 +26,44 @@ export type RawTableType = {
|
|
|
11
26
|
* The statement to run when PowerSync detects that a row needs to be deleted.
|
|
12
27
|
*/
|
|
13
28
|
delete: PendingStatement;
|
|
14
|
-
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* An optional statement to run when `disconnectAndClear()` is called on a PowerSync database.
|
|
32
|
+
*/
|
|
33
|
+
clear?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The schema of a {@link RawTableType} in the local database.
|
|
38
|
+
*
|
|
39
|
+
* This information is optional when declaring raw tables. However, providing it allows the sync client to infer `put`
|
|
40
|
+
* and `delete` statements automatically.
|
|
41
|
+
*/
|
|
42
|
+
interface RawTableSchema extends TableOrRawTableOptions {
|
|
43
|
+
/**
|
|
44
|
+
* The actual name of the raw table in the local schema.
|
|
45
|
+
*
|
|
46
|
+
* Unlike {@link RawTable.name}, which describes the name of synced tables to match, this reflects the SQLite table
|
|
47
|
+
* name. This is used to infer {@link RawTableType.put} and {@link RawTableType.delete} statements for the sync
|
|
48
|
+
* client. It can also be used to auto-generate triggers forwarding writes on raw tables into the CRUD upload queue
|
|
49
|
+
* (using the `powersync_create_raw_table_crud_trigger` SQL function).
|
|
50
|
+
*
|
|
51
|
+
* When absent, defaults to {@link RawTable.name}.
|
|
52
|
+
*/
|
|
53
|
+
tableName?: string;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* An optional filter of columns that should be synced.
|
|
57
|
+
*
|
|
58
|
+
* By default, all columns in a raw table are considered for sync. If a filter is specified, PowerSync treats
|
|
59
|
+
* unmatched columns as local-only and will not attempt to sync them.
|
|
60
|
+
*/
|
|
61
|
+
syncedColumns?: string[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface InferredRawTableType extends Partial<RawTableTypeWithStatements> {
|
|
65
|
+
schema: RawTableSchema;
|
|
66
|
+
}
|
|
15
67
|
|
|
16
68
|
/**
|
|
17
69
|
* A parameter to use as part of {@link PendingStatement}.
|
|
@@ -21,8 +73,10 @@ export type RawTableType = {
|
|
|
21
73
|
*
|
|
22
74
|
* For insert and replace operations, the values of columns in the table are available as parameters through
|
|
23
75
|
* `{Column: 'name'}`.
|
|
76
|
+
* The `"Rest"` parameter gets resolved to a JSON object covering all values from the synced row that haven't been
|
|
77
|
+
* covered by a `Column` parameter.
|
|
24
78
|
*/
|
|
25
|
-
export type PendingStatementParameter = 'Id' | { Column: string };
|
|
79
|
+
export type PendingStatementParameter = 'Id' | { Column: string } | 'Rest';
|
|
26
80
|
|
|
27
81
|
/**
|
|
28
82
|
* A statement that the PowerSync client should use to insert or delete data into a table managed by the user.
|
|
@@ -33,35 +87,16 @@ export type PendingStatement = {
|
|
|
33
87
|
};
|
|
34
88
|
|
|
35
89
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* Since raw tables are not backed by JSON, running complex queries on them may be more efficient. Further, they allow
|
|
39
|
-
* using client-side table and column constraints.
|
|
40
|
-
*
|
|
41
|
-
* To collect local writes to raw tables with PowerSync, custom triggers are required. See
|
|
42
|
-
* {@link https://docs.powersync.com/usage/use-case-examples/raw-tables the documentation} for details and an example on
|
|
43
|
-
* using raw tables.
|
|
44
|
-
*
|
|
45
|
-
* Note that raw tables are only supported when using the new `SyncClientImplementation.rust` sync client.
|
|
46
|
-
*
|
|
47
|
-
* @experimental Please note that this feature is experimental at the moment, and not covered by PowerSync semver or
|
|
48
|
-
* stability guarantees.
|
|
90
|
+
* @internal
|
|
49
91
|
*/
|
|
50
|
-
export
|
|
92
|
+
export type RawTable<T extends RawTableType = RawTableType> = T & {
|
|
51
93
|
/**
|
|
52
94
|
* The name of the table.
|
|
53
95
|
*
|
|
54
|
-
* This does not have to match the actual table name in the schema - {@link put} and
|
|
55
|
-
* another table. Instead, this name is used by the sync client to
|
|
56
|
-
* appears in the source / backend database) are to be handled
|
|
96
|
+
* This does not have to match the actual table name in the schema - {@link RawTableType.put} and
|
|
97
|
+
* {@link RawTableType.delete} are free to use another table. Instead, this name is used by the sync client to
|
|
98
|
+
* recognize that operations on this table (as it appears in the source / backend database) are to be handled
|
|
99
|
+
* specially.
|
|
57
100
|
*/
|
|
58
101
|
name: string;
|
|
59
|
-
|
|
60
|
-
delete: PendingStatement;
|
|
61
|
-
|
|
62
|
-
constructor(name: string, type: RawTableType) {
|
|
63
|
-
this.name = name;
|
|
64
|
-
this.put = type.put;
|
|
65
|
-
this.delete = type.delete;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
102
|
+
};
|
package/src/db/schema/Schema.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { encodeTableOptions } from './internal.js';
|
|
1
2
|
import { RawTable, RawTableType } from './RawTable.js';
|
|
2
3
|
import { RowType, Table } from './Table.js';
|
|
3
4
|
|
|
@@ -57,7 +58,7 @@ export class Schema<S extends SchemaType = SchemaType> {
|
|
|
57
58
|
*/
|
|
58
59
|
withRawTables(tables: Record<string, RawTableType>) {
|
|
59
60
|
for (const [name, rawTableDefinition] of Object.entries(tables)) {
|
|
60
|
-
this.rawTables.push(
|
|
61
|
+
this.rawTables.push({ name, ...rawTableDefinition });
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
|
|
@@ -70,7 +71,31 @@ export class Schema<S extends SchemaType = SchemaType> {
|
|
|
70
71
|
toJSON() {
|
|
71
72
|
return {
|
|
72
73
|
tables: this.tables.map((t) => t.toJSON()),
|
|
73
|
-
raw_tables: this.rawTables
|
|
74
|
+
raw_tables: this.rawTables.map(Schema.rawTableToJson)
|
|
74
75
|
};
|
|
75
76
|
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Returns a representation of the raw table that is understood by the PowerSync SQLite core extension.
|
|
80
|
+
*
|
|
81
|
+
* The output of this can be passed through `JSON.serialize` and then used in `powersync_create_raw_table_crud_trigger`
|
|
82
|
+
* to define triggers for this table.
|
|
83
|
+
*/
|
|
84
|
+
static rawTableToJson(table: RawTable): unknown {
|
|
85
|
+
const serialized: any = {
|
|
86
|
+
name: table.name,
|
|
87
|
+
put: table.put,
|
|
88
|
+
delete: table.delete,
|
|
89
|
+
clear: table.clear
|
|
90
|
+
};
|
|
91
|
+
if ('schema' in table) {
|
|
92
|
+
// We have schema options, those are flattened into the outer JSON object for the core extension.
|
|
93
|
+
const schema = table.schema;
|
|
94
|
+
serialized.table_name = schema.tableName ?? table.name;
|
|
95
|
+
serialized.synced_columns = schema.syncedColumns;
|
|
96
|
+
Object.assign(serialized, encodeTableOptions(table.schema));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return serialized;
|
|
100
|
+
}
|
|
76
101
|
}
|
package/src/db/schema/Table.ts
CHANGED
|
@@ -8,17 +8,24 @@ import {
|
|
|
8
8
|
} from './Column.js';
|
|
9
9
|
import { Index } from './Index.js';
|
|
10
10
|
import { IndexedColumn } from './IndexedColumn.js';
|
|
11
|
+
import { encodeTableOptions } from './internal.js';
|
|
11
12
|
import { TableV2 } from './TableV2.js';
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Options that apply both to JSON-based tables and raw tables.
|
|
16
|
+
*/
|
|
17
|
+
export interface TableOrRawTableOptions {
|
|
14
18
|
localOnly?: boolean;
|
|
15
19
|
insertOnly?: boolean;
|
|
16
|
-
viewName?: string;
|
|
17
20
|
trackPrevious?: boolean | TrackPreviousOptions;
|
|
18
21
|
trackMetadata?: boolean;
|
|
19
22
|
ignoreEmptyUpdates?: boolean;
|
|
20
23
|
}
|
|
21
24
|
|
|
25
|
+
interface SharedTableOptions extends TableOrRawTableOptions {
|
|
26
|
+
viewName?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
22
29
|
/** Whether to include previous column values when PowerSync tracks local changes.
|
|
23
30
|
*
|
|
24
31
|
* Including old values may be helpful for some backend connector implementations, which is
|
|
@@ -341,19 +348,12 @@ export class Table<Columns extends ColumnsType = ColumnsType> {
|
|
|
341
348
|
}
|
|
342
349
|
|
|
343
350
|
toJSON() {
|
|
344
|
-
const trackPrevious = this.trackPrevious;
|
|
345
|
-
|
|
346
351
|
return {
|
|
347
352
|
name: this.name,
|
|
348
353
|
view_name: this.viewName,
|
|
349
|
-
local_only: this.localOnly,
|
|
350
|
-
insert_only: this.insertOnly,
|
|
351
|
-
include_old: trackPrevious && ((trackPrevious as any).columns ?? true),
|
|
352
|
-
include_old_only_when_changed: typeof trackPrevious == 'object' && trackPrevious.onlyWhenChanged == true,
|
|
353
|
-
include_metadata: this.trackMetadata,
|
|
354
|
-
ignore_empty_update: this.ignoreEmptyUpdates,
|
|
355
354
|
columns: this.columns.map((c) => c.toJSON()),
|
|
356
|
-
indexes: this.indexes.map((e) => e.toJSON(this))
|
|
355
|
+
indexes: this.indexes.map((e) => e.toJSON(this)),
|
|
356
|
+
...encodeTableOptions(this)
|
|
357
357
|
};
|
|
358
358
|
}
|
|
359
359
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { TableOrRawTableOptions } from './Table.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @internal Not exported from `index.ts`.
|
|
5
|
+
*/
|
|
6
|
+
export function encodeTableOptions(options: TableOrRawTableOptions) {
|
|
7
|
+
const trackPrevious = options.trackPrevious;
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
local_only: options.localOnly,
|
|
11
|
+
insert_only: options.insertOnly,
|
|
12
|
+
include_old: trackPrevious && ((trackPrevious as any).columns ?? true),
|
|
13
|
+
include_old_only_when_changed: typeof trackPrevious == 'object' && trackPrevious.onlyWhenChanged == true,
|
|
14
|
+
include_metadata: options.trackMetadata,
|
|
15
|
+
ignore_empty_update: options.ignoreEmptyUpdates
|
|
16
|
+
};
|
|
17
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
export * from './attachments/AttachmentContext.js';
|
|
2
|
+
export * from './attachments/AttachmentErrorHandler.js';
|
|
3
|
+
export * from './attachments/AttachmentQueue.js';
|
|
4
|
+
export * from './attachments/AttachmentService.js';
|
|
5
|
+
export * from './attachments/LocalStorageAdapter.js';
|
|
6
|
+
export * from './attachments/RemoteStorageAdapter.js';
|
|
7
|
+
export * from './attachments/Schema.js';
|
|
8
|
+
export * from './attachments/SyncingService.js';
|
|
9
|
+
export * from './attachments/WatchedAttachmentItem.js';
|
|
10
|
+
|
|
1
11
|
export * from './client/AbstractPowerSyncDatabase.js';
|
|
2
12
|
export * from './client/AbstractPowerSyncOpenFactory.js';
|
|
3
13
|
export { compilableQueryWatch, CompilableQueryWatchHandler } from './client/compilableQueryWatch.js';
|
|
@@ -29,7 +39,7 @@ export * from './db/DBAdapter.js';
|
|
|
29
39
|
export * from './db/schema/Column.js';
|
|
30
40
|
export * from './db/schema/Index.js';
|
|
31
41
|
export * from './db/schema/IndexedColumn.js';
|
|
32
|
-
export
|
|
42
|
+
export { RawTableType, PendingStatementParameter, PendingStatement } from './db/schema/RawTable.js';
|
|
33
43
|
export * from './db/schema/Schema.js';
|
|
34
44
|
export * from './db/schema/Table.js';
|
|
35
45
|
export * from './db/schema/TableV2.js';
|
|
@@ -51,6 +61,7 @@ export * from './utils/BaseObserver.js';
|
|
|
51
61
|
export * from './utils/ControlledExecutor.js';
|
|
52
62
|
export * from './utils/DataStream.js';
|
|
53
63
|
export * from './utils/Logger.js';
|
|
64
|
+
export * from './utils/mutex.js';
|
|
54
65
|
export * from './utils/parseQuery.js';
|
|
55
66
|
|
|
56
67
|
export * from './types/types.js';
|
package/src/utils/mutex.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { Mutex } from 'async-mutex';
|
|
|
6
6
|
export async function mutexRunExclusive<T>(
|
|
7
7
|
mutex: Mutex,
|
|
8
8
|
callback: () => Promise<T>,
|
|
9
|
-
options?: { timeoutMs
|
|
9
|
+
options?: { timeoutMs?: number }
|
|
10
10
|
): Promise<T> {
|
|
11
11
|
return new Promise((resolve, reject) => {
|
|
12
12
|
const timeout = options?.timeoutMs;
|