@powersync/service-module-mongodb 0.10.4 → 0.12.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/CHANGELOG.md +61 -0
- package/LICENSE +3 -3
- package/dist/api/MongoRouteAPIAdapter.js +18 -2
- package/dist/api/MongoRouteAPIAdapter.js.map +1 -1
- package/dist/replication/ChangeStream.d.ts +15 -3
- package/dist/replication/ChangeStream.js +68 -22
- package/dist/replication/ChangeStream.js.map +1 -1
- package/dist/replication/MongoRelation.d.ts +3 -3
- package/dist/replication/MongoRelation.js +16 -15
- package/dist/replication/MongoRelation.js.map +1 -1
- package/dist/replication/replication-utils.js +3 -3
- package/dist/replication/replication-utils.js.map +1 -1
- package/package.json +11 -11
- package/src/api/MongoRouteAPIAdapter.ts +19 -19
- package/src/replication/ChangeStream.ts +74 -26
- package/src/replication/MongoRelation.ts +28 -18
- package/src/replication/replication-utils.ts +3 -3
- package/test/src/change_stream.test.ts +11 -6
- package/test/src/change_stream_utils.ts +20 -8
- package/test/src/mongo_test.test.ts +58 -16
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -52,7 +52,7 @@ export async function checkSourceConfiguration(connectionManager: MongoManager):
|
|
|
52
52
|
const fullName = `${db.databaseName}.${CHECKPOINTS_COLLECTION}`;
|
|
53
53
|
throw new ServiceError(
|
|
54
54
|
ErrorCode.PSYNC_S1307,
|
|
55
|
-
`MongoDB user does not have the required ${missingCheckpointActions.map((a) => `"${a}"`).join(', ')}
|
|
55
|
+
`MongoDB user does not have the required ${missingCheckpointActions.map((a) => `"${a}"`).join(', ')} privilege(s) on "${fullName}".`
|
|
56
56
|
);
|
|
57
57
|
}
|
|
58
58
|
|
|
@@ -62,14 +62,14 @@ export async function checkSourceConfiguration(connectionManager: MongoManager):
|
|
|
62
62
|
if (!anyCollectionActions.has('collMod')) {
|
|
63
63
|
throw new ServiceError(
|
|
64
64
|
ErrorCode.PSYNC_S1307,
|
|
65
|
-
`MongoDB user does not have the required "collMod"
|
|
65
|
+
`MongoDB user does not have the required "collMod" privilege on "${db.databaseName}", required for "post_images: auto_configure".`
|
|
66
66
|
);
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
if (!anyCollectionActions.has('listCollections')) {
|
|
70
70
|
throw new ServiceError(
|
|
71
71
|
ErrorCode.PSYNC_S1307,
|
|
72
|
-
`MongoDB user does not have the required "listCollections"
|
|
72
|
+
`MongoDB user does not have the required "listCollections" privilege on "${db.databaseName}".`
|
|
73
73
|
);
|
|
74
74
|
}
|
|
75
75
|
} else {
|
|
@@ -23,7 +23,9 @@ describe('change stream', () => {
|
|
|
23
23
|
|
|
24
24
|
function defineChangeStreamTests(factory: storage.TestStorageFactory) {
|
|
25
25
|
test('replicating basic values', async () => {
|
|
26
|
-
await using context = await ChangeStreamTestContext.open(factory
|
|
26
|
+
await using context = await ChangeStreamTestContext.open(factory, {
|
|
27
|
+
mongoOptions: { postImages: PostImagesOption.READ_ONLY }
|
|
28
|
+
});
|
|
27
29
|
const { db } = context;
|
|
28
30
|
await context.updateSyncRules(`
|
|
29
31
|
bucket_definitions:
|
|
@@ -32,7 +34,7 @@ bucket_definitions:
|
|
|
32
34
|
- SELECT _id as id, description, num FROM "test_data"`);
|
|
33
35
|
|
|
34
36
|
await db.createCollection('test_data', {
|
|
35
|
-
changeStreamPreAndPostImages: { enabled:
|
|
37
|
+
changeStreamPreAndPostImages: { enabled: true }
|
|
36
38
|
});
|
|
37
39
|
const collection = db.collection('test_data');
|
|
38
40
|
|
|
@@ -42,11 +44,8 @@ bucket_definitions:
|
|
|
42
44
|
|
|
43
45
|
const result = await collection.insertOne({ description: 'test1', num: 1152921504606846976n });
|
|
44
46
|
const test_id = result.insertedId;
|
|
45
|
-
await setTimeout(30);
|
|
46
47
|
await collection.updateOne({ _id: test_id }, { $set: { description: 'test2' } });
|
|
47
|
-
await setTimeout(30);
|
|
48
48
|
await collection.replaceOne({ _id: test_id }, { description: 'test3' });
|
|
49
|
-
await setTimeout(30);
|
|
50
49
|
await collection.deleteOne({ _id: test_id });
|
|
51
50
|
|
|
52
51
|
const data = await context.getBucketData('global[]');
|
|
@@ -354,6 +353,9 @@ bucket_definitions:
|
|
|
354
353
|
const test_id = result.insertedId.toHexString();
|
|
355
354
|
|
|
356
355
|
await context.replicateSnapshot();
|
|
356
|
+
// Note: snapshot is only consistent some time into the streaming request.
|
|
357
|
+
// At the point that we get the first acknowledged checkpoint, as is required
|
|
358
|
+
// for getBucketData(), the data should be consistent.
|
|
357
359
|
context.startStreaming();
|
|
358
360
|
|
|
359
361
|
const data = await context.getBucketData('global[]');
|
|
@@ -512,10 +514,13 @@ bucket_definitions:
|
|
|
512
514
|
await collection.insertOne({ description: 'test1', num: 1152921504606846976n });
|
|
513
515
|
|
|
514
516
|
await context.replicateSnapshot();
|
|
517
|
+
await context.markSnapshotConsistent();
|
|
515
518
|
|
|
516
519
|
// Simulate an error
|
|
517
520
|
await context.storage!.reportError(new Error('simulated error'));
|
|
518
|
-
|
|
521
|
+
const syncRules = await context.factory.getActiveSyncRulesContent();
|
|
522
|
+
expect(syncRules).toBeTruthy();
|
|
523
|
+
expect(syncRules?.last_fatal_error).toEqual('simulated error');
|
|
519
524
|
|
|
520
525
|
// startStreaming() should automatically clear the error.
|
|
521
526
|
context.startStreaming();
|
|
@@ -17,7 +17,7 @@ import { MongoManager } from '@module/replication/MongoManager.js';
|
|
|
17
17
|
import { createCheckpoint, STANDALONE_CHECKPOINT_ID } from '@module/replication/MongoRelation.js';
|
|
18
18
|
import { NormalizedMongoConnectionConfig } from '@module/types/types.js';
|
|
19
19
|
|
|
20
|
-
import {
|
|
20
|
+
import { clearTestDb, TEST_CONNECTION_OPTIONS } from './util.js';
|
|
21
21
|
|
|
22
22
|
export class ChangeStreamTestContext {
|
|
23
23
|
private _walStream?: ChangeStream;
|
|
@@ -119,7 +119,20 @@ export class ChangeStreamTestContext {
|
|
|
119
119
|
|
|
120
120
|
async replicateSnapshot() {
|
|
121
121
|
await this.walStream.initReplication();
|
|
122
|
-
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* A snapshot is not consistent until streaming replication has caught up.
|
|
126
|
+
* We simulate that for tests.
|
|
127
|
+
* Do not use if there are any writes performed while doing the snapshot, as that
|
|
128
|
+
* would result in inconsistent data.
|
|
129
|
+
*/
|
|
130
|
+
async markSnapshotConsistent() {
|
|
131
|
+
const checkpoint = await createCheckpoint(this.client, this.db, STANDALONE_CHECKPOINT_ID);
|
|
132
|
+
|
|
133
|
+
await this.storage!.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
|
|
134
|
+
await batch.keepalive(checkpoint);
|
|
135
|
+
});
|
|
123
136
|
}
|
|
124
137
|
|
|
125
138
|
startStreaming() {
|
|
@@ -195,12 +208,11 @@ export async function getClientCheckpoint(
|
|
|
195
208
|
while (Date.now() - start < timeout) {
|
|
196
209
|
const storage = await storageFactory.getActiveStorage();
|
|
197
210
|
const cp = await storage?.getCheckpoint();
|
|
198
|
-
if (cp
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
return cp.checkpoint;
|
|
211
|
+
if (cp != null) {
|
|
212
|
+
lastCp = cp;
|
|
213
|
+
if (cp.lsn && cp.lsn >= lsn) {
|
|
214
|
+
return cp.checkpoint;
|
|
215
|
+
}
|
|
204
216
|
}
|
|
205
217
|
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
206
218
|
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { mongo } from '@powersync/lib-service-mongodb';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
applyRowContext,
|
|
4
|
+
CompatibilityContext,
|
|
5
|
+
CompatibilityEdition,
|
|
6
|
+
SqliteInputRow,
|
|
7
|
+
SqlSyncRules
|
|
8
|
+
} from '@powersync/service-sync-rules';
|
|
3
9
|
import { describe, expect, test } from 'vitest';
|
|
4
10
|
|
|
5
11
|
import { MongoRouteAPIAdapter } from '@module/api/MongoRouteAPIAdapter.js';
|
|
@@ -138,8 +144,10 @@ describe('mongo data types', () => {
|
|
|
138
144
|
]);
|
|
139
145
|
}
|
|
140
146
|
|
|
141
|
-
function checkResults(transformed:
|
|
142
|
-
|
|
147
|
+
function checkResults(transformed: SqliteInputRow[]) {
|
|
148
|
+
const sqliteValue = transformed.map((e) => applyRowContext(e, CompatibilityContext.FULL_BACKWARDS_COMPATIBILITY));
|
|
149
|
+
|
|
150
|
+
expect(sqliteValue[0]).toMatchObject({
|
|
143
151
|
_id: 1n,
|
|
144
152
|
text: 'text',
|
|
145
153
|
uuid: 'baeb2514-4c57-436d-b3cc-c1256211656d',
|
|
@@ -152,17 +160,17 @@ describe('mongo data types', () => {
|
|
|
152
160
|
null: null,
|
|
153
161
|
decimal: '3.14'
|
|
154
162
|
});
|
|
155
|
-
expect(
|
|
163
|
+
expect(sqliteValue[1]).toMatchObject({
|
|
156
164
|
_id: 2n,
|
|
157
165
|
nested: '{"test":"thing"}'
|
|
158
166
|
});
|
|
159
167
|
|
|
160
|
-
expect(
|
|
168
|
+
expect(sqliteValue[2]).toMatchObject({
|
|
161
169
|
_id: 3n,
|
|
162
170
|
date: '2023-03-06 13:47:00.000Z'
|
|
163
171
|
});
|
|
164
172
|
|
|
165
|
-
expect(
|
|
173
|
+
expect(sqliteValue[3]).toMatchObject({
|
|
166
174
|
_id: 4n,
|
|
167
175
|
objectId: '66e834cc91d805df11fa0ecb',
|
|
168
176
|
timestamp: 1958505087099n,
|
|
@@ -177,9 +185,9 @@ describe('mongo data types', () => {
|
|
|
177
185
|
});
|
|
178
186
|
|
|
179
187
|
// This must specifically be null, and not undefined.
|
|
180
|
-
expect(
|
|
188
|
+
expect(sqliteValue[4].undefined).toBeNull();
|
|
181
189
|
|
|
182
|
-
expect(
|
|
190
|
+
expect(sqliteValue[5]).toMatchObject({
|
|
183
191
|
_id: 6n,
|
|
184
192
|
int4: -1n,
|
|
185
193
|
int8: -9007199254740993n,
|
|
@@ -188,8 +196,10 @@ describe('mongo data types', () => {
|
|
|
188
196
|
});
|
|
189
197
|
}
|
|
190
198
|
|
|
191
|
-
function checkResultsNested(transformed:
|
|
192
|
-
|
|
199
|
+
function checkResultsNested(transformed: SqliteInputRow[]) {
|
|
200
|
+
const sqliteValue = transformed.map((e) => applyRowContext(e, CompatibilityContext.FULL_BACKWARDS_COMPATIBILITY));
|
|
201
|
+
|
|
202
|
+
expect(sqliteValue[0]).toMatchObject({
|
|
193
203
|
_id: 1n,
|
|
194
204
|
text: `["text"]`,
|
|
195
205
|
uuid: '["baeb2514-4c57-436d-b3cc-c1256211656d"]',
|
|
@@ -204,22 +214,22 @@ describe('mongo data types', () => {
|
|
|
204
214
|
|
|
205
215
|
// Note: Depending on to what extent we use the original postgres value, the whitespace may change, and order may change.
|
|
206
216
|
// We do expect that decimals and big numbers are preserved.
|
|
207
|
-
expect(
|
|
217
|
+
expect(sqliteValue[1]).toMatchObject({
|
|
208
218
|
_id: 2n,
|
|
209
219
|
nested: '[{"test":"thing"}]'
|
|
210
220
|
});
|
|
211
221
|
|
|
212
|
-
expect(
|
|
222
|
+
expect(sqliteValue[2]).toMatchObject({
|
|
213
223
|
_id: 3n,
|
|
214
224
|
date: '["2023-03-06 13:47:00.000Z"]'
|
|
215
225
|
});
|
|
216
226
|
|
|
217
|
-
expect(
|
|
227
|
+
expect(sqliteValue[3]).toMatchObject({
|
|
218
228
|
_id: 5n,
|
|
219
229
|
undefined: '[null]'
|
|
220
230
|
});
|
|
221
231
|
|
|
222
|
-
expect(
|
|
232
|
+
expect(sqliteValue[4]).toMatchObject({
|
|
223
233
|
_id: 6n,
|
|
224
234
|
int4: '[-1]',
|
|
225
235
|
int8: '[-9007199254740993]',
|
|
@@ -227,7 +237,7 @@ describe('mongo data types', () => {
|
|
|
227
237
|
decimal: '["-3.14"]'
|
|
228
238
|
});
|
|
229
239
|
|
|
230
|
-
expect(
|
|
240
|
+
expect(sqliteValue[5]).toMatchObject({
|
|
231
241
|
_id: 10n,
|
|
232
242
|
objectId: '["66e834cc91d805df11fa0ecb"]',
|
|
233
243
|
timestamp: '[1958505087099]',
|
|
@@ -522,13 +532,45 @@ bucket_definitions:
|
|
|
522
532
|
errors: []
|
|
523
533
|
});
|
|
524
534
|
});
|
|
535
|
+
|
|
536
|
+
test('date format', async () => {
|
|
537
|
+
const { db, client } = await connectMongoData();
|
|
538
|
+
const collection = db.collection('test_data');
|
|
539
|
+
try {
|
|
540
|
+
await setupTable(db);
|
|
541
|
+
await collection.insertOne({
|
|
542
|
+
fraction: new Date('2023-03-06 15:47:01.123+02'),
|
|
543
|
+
noFraction: new Date('2023-03-06 15:47:01+02')
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
const rawResults = await db
|
|
547
|
+
.collection('test_data')
|
|
548
|
+
.find({}, { sort: { _id: 1 } })
|
|
549
|
+
.toArray();
|
|
550
|
+
const [row] = [...ChangeStream.getQueryData(rawResults)];
|
|
551
|
+
|
|
552
|
+
const oldFormat = applyRowContext(row, CompatibilityContext.FULL_BACKWARDS_COMPATIBILITY);
|
|
553
|
+
expect(oldFormat).toMatchObject({
|
|
554
|
+
fraction: '2023-03-06 13:47:01.123Z',
|
|
555
|
+
noFraction: '2023-03-06 13:47:01.000Z'
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
const newFormat = applyRowContext(row, new CompatibilityContext(CompatibilityEdition.SYNC_STREAMS));
|
|
559
|
+
expect(newFormat).toMatchObject({
|
|
560
|
+
fraction: '2023-03-06T13:47:01.123Z',
|
|
561
|
+
noFraction: '2023-03-06T13:47:01.000Z'
|
|
562
|
+
});
|
|
563
|
+
} finally {
|
|
564
|
+
await client.close();
|
|
565
|
+
}
|
|
566
|
+
});
|
|
525
567
|
});
|
|
526
568
|
|
|
527
569
|
/**
|
|
528
570
|
* Return all the inserts from the first transaction in the replication stream.
|
|
529
571
|
*/
|
|
530
572
|
async function getReplicationTx(replicationStream: mongo.ChangeStream, count: number) {
|
|
531
|
-
let transformed:
|
|
573
|
+
let transformed: SqliteInputRow[] = [];
|
|
532
574
|
for await (const doc of replicationStream) {
|
|
533
575
|
// Specifically filter out map_input / map_output collections
|
|
534
576
|
if (!(doc as any)?.ns?.coll?.startsWith('test_data')) {
|