@powersync/service-module-mongodb 0.0.0-dev-20250122110924 → 0.0.0-dev-20250227082606
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 +96 -11
- package/dist/api/MongoRouteAPIAdapter.d.ts +2 -1
- package/dist/api/MongoRouteAPIAdapter.js +39 -0
- package/dist/api/MongoRouteAPIAdapter.js.map +1 -1
- package/dist/common/MongoLSN.d.ts +31 -0
- package/dist/common/MongoLSN.js +47 -0
- package/dist/common/MongoLSN.js.map +1 -0
- package/dist/module/MongoModule.js +2 -2
- package/dist/module/MongoModule.js.map +1 -1
- package/dist/replication/ChangeStream.d.ts +4 -3
- package/dist/replication/ChangeStream.js +130 -52
- package/dist/replication/ChangeStream.js.map +1 -1
- package/dist/replication/ChangeStreamReplicationJob.js +7 -6
- package/dist/replication/ChangeStreamReplicationJob.js.map +1 -1
- package/dist/replication/ChangeStreamReplicator.js +1 -0
- package/dist/replication/ChangeStreamReplicator.js.map +1 -1
- package/dist/replication/ConnectionManagerFactory.js +2 -0
- package/dist/replication/ConnectionManagerFactory.js.map +1 -1
- package/dist/replication/MongoErrorRateLimiter.js +5 -7
- package/dist/replication/MongoErrorRateLimiter.js.map +1 -1
- package/dist/replication/MongoManager.d.ts +0 -3
- package/dist/replication/MongoManager.js +9 -4
- package/dist/replication/MongoManager.js.map +1 -1
- package/dist/replication/MongoRelation.d.ts +4 -2
- package/dist/replication/MongoRelation.js +10 -15
- package/dist/replication/MongoRelation.js.map +1 -1
- package/package.json +10 -10
- package/src/api/MongoRouteAPIAdapter.ts +41 -1
- package/src/common/MongoLSN.ts +74 -0
- package/src/replication/ChangeStream.ts +138 -57
- package/src/replication/ChangeStreamReplicationJob.ts +6 -6
- package/src/replication/MongoManager.ts +4 -3
- package/src/replication/MongoRelation.ts +10 -16
- package/test/src/change_stream.test.ts +6 -0
- package/test/src/change_stream_utils.ts +9 -8
- package/test/src/mongo_test.test.ts +47 -12
- package/test/src/resume.test.ts +152 -0
- package/test/src/util.ts +2 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -47,6 +47,13 @@ describe('mongo data types', () => {
|
|
|
47
47
|
'mydb',
|
|
48
48
|
{ foo: 'bar' }
|
|
49
49
|
)
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
_id: 6 as any,
|
|
53
|
+
int4: -1,
|
|
54
|
+
int8: -9007199254740993n,
|
|
55
|
+
float: -3.14,
|
|
56
|
+
decimal: new mongo.Decimal128('-3.14')
|
|
50
57
|
}
|
|
51
58
|
]);
|
|
52
59
|
}
|
|
@@ -109,6 +116,13 @@ describe('mongo data types', () => {
|
|
|
109
116
|
},
|
|
110
117
|
{ _id: 2 as any, nested: [{ test: 'thing' }] },
|
|
111
118
|
{ _id: 3 as any, date: [new Date('2023-03-06 15:47+02')] },
|
|
119
|
+
{
|
|
120
|
+
_id: 6 as any,
|
|
121
|
+
int4: [-1],
|
|
122
|
+
int8: [-9007199254740993n],
|
|
123
|
+
float: [-3.14],
|
|
124
|
+
decimal: [new mongo.Decimal128('-3.14')]
|
|
125
|
+
},
|
|
112
126
|
{
|
|
113
127
|
_id: 10 as any,
|
|
114
128
|
timestamp: [mongo.Timestamp.fromBits(123, 456)],
|
|
@@ -164,6 +178,14 @@ describe('mongo data types', () => {
|
|
|
164
178
|
|
|
165
179
|
// This must specifically be null, and not undefined.
|
|
166
180
|
expect(transformed[4].undefined).toBeNull();
|
|
181
|
+
|
|
182
|
+
expect(transformed[5]).toMatchObject({
|
|
183
|
+
_id: 6n,
|
|
184
|
+
int4: -1n,
|
|
185
|
+
int8: -9007199254740993n,
|
|
186
|
+
float: -3.14,
|
|
187
|
+
decimal: '-3.14'
|
|
188
|
+
});
|
|
167
189
|
}
|
|
168
190
|
|
|
169
191
|
function checkResultsNested(transformed: Record<string, any>[]) {
|
|
@@ -193,6 +215,19 @@ describe('mongo data types', () => {
|
|
|
193
215
|
});
|
|
194
216
|
|
|
195
217
|
expect(transformed[3]).toMatchObject({
|
|
218
|
+
_id: 5n,
|
|
219
|
+
undefined: '[null]'
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
expect(transformed[4]).toMatchObject({
|
|
223
|
+
_id: 6n,
|
|
224
|
+
int4: '[-1]',
|
|
225
|
+
int8: '[-9007199254740993]',
|
|
226
|
+
float: '[-3.14]',
|
|
227
|
+
decimal: '["-3.14"]'
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
expect(transformed[5]).toMatchObject({
|
|
196
231
|
_id: 10n,
|
|
197
232
|
objectId: '["66e834cc91d805df11fa0ecb"]',
|
|
198
233
|
timestamp: '[1958505087099]',
|
|
@@ -203,10 +238,6 @@ describe('mongo data types', () => {
|
|
|
203
238
|
minKey: '[null]',
|
|
204
239
|
maxKey: '[null]'
|
|
205
240
|
});
|
|
206
|
-
|
|
207
|
-
expect(transformed[4]).toMatchObject({
|
|
208
|
-
undefined: '[null]'
|
|
209
|
-
});
|
|
210
241
|
}
|
|
211
242
|
|
|
212
243
|
test('test direct queries', async () => {
|
|
@@ -218,11 +249,13 @@ describe('mongo data types', () => {
|
|
|
218
249
|
await insert(collection);
|
|
219
250
|
await insertUndefined(db, 'test_data');
|
|
220
251
|
|
|
221
|
-
const rawResults = await db
|
|
252
|
+
const rawResults = await db
|
|
253
|
+
.collection('test_data')
|
|
254
|
+
.find({}, { sort: { _id: 1 } })
|
|
255
|
+
.toArray();
|
|
222
256
|
// It is tricky to save "undefined" with mongo, so we check that it succeeded.
|
|
223
257
|
expect(rawResults[4].undefined).toBeUndefined();
|
|
224
258
|
const transformed = [...ChangeStream.getQueryData(rawResults)];
|
|
225
|
-
|
|
226
259
|
checkResults(transformed);
|
|
227
260
|
} finally {
|
|
228
261
|
await client.close();
|
|
@@ -238,8 +271,11 @@ describe('mongo data types', () => {
|
|
|
238
271
|
await insertNested(collection);
|
|
239
272
|
await insertUndefined(db, 'test_data_arrays', true);
|
|
240
273
|
|
|
241
|
-
const rawResults = await db
|
|
242
|
-
|
|
274
|
+
const rawResults = await db
|
|
275
|
+
.collection('test_data_arrays')
|
|
276
|
+
.find({}, { sort: { _id: 1 } })
|
|
277
|
+
.toArray();
|
|
278
|
+
expect(rawResults[3].undefined).toEqual([undefined]);
|
|
243
279
|
const transformed = [...ChangeStream.getQueryData(rawResults)];
|
|
244
280
|
|
|
245
281
|
checkResultsNested(transformed);
|
|
@@ -257,7 +293,6 @@ describe('mongo data types', () => {
|
|
|
257
293
|
await setupTable(db);
|
|
258
294
|
|
|
259
295
|
const stream = db.watch([], {
|
|
260
|
-
useBigInt64: true,
|
|
261
296
|
maxAwaitTimeMS: 50,
|
|
262
297
|
fullDocument: 'updateLookup'
|
|
263
298
|
});
|
|
@@ -267,7 +302,7 @@ describe('mongo data types', () => {
|
|
|
267
302
|
await insert(collection);
|
|
268
303
|
await insertUndefined(db, 'test_data');
|
|
269
304
|
|
|
270
|
-
const transformed = await getReplicationTx(stream,
|
|
305
|
+
const transformed = await getReplicationTx(stream, 6);
|
|
271
306
|
|
|
272
307
|
checkResults(transformed);
|
|
273
308
|
} finally {
|
|
@@ -282,7 +317,6 @@ describe('mongo data types', () => {
|
|
|
282
317
|
await setupTable(db);
|
|
283
318
|
|
|
284
319
|
const stream = db.watch([], {
|
|
285
|
-
useBigInt64: true,
|
|
286
320
|
maxAwaitTimeMS: 50,
|
|
287
321
|
fullDocument: 'updateLookup'
|
|
288
322
|
});
|
|
@@ -292,7 +326,7 @@ describe('mongo data types', () => {
|
|
|
292
326
|
await insertNested(collection);
|
|
293
327
|
await insertUndefined(db, 'test_data_arrays', true);
|
|
294
328
|
|
|
295
|
-
const transformed = await getReplicationTx(stream,
|
|
329
|
+
const transformed = await getReplicationTx(stream, 6);
|
|
296
330
|
|
|
297
331
|
checkResultsNested(transformed);
|
|
298
332
|
} finally {
|
|
@@ -505,5 +539,6 @@ async function getReplicationTx(replicationStream: mongo.ChangeStream, count: nu
|
|
|
505
539
|
break;
|
|
506
540
|
}
|
|
507
541
|
}
|
|
542
|
+
transformed.sort((a, b) => Number(a._id) - Number(b._id));
|
|
508
543
|
return transformed;
|
|
509
544
|
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { MongoLSN, ZERO_LSN } from '@module/common/MongoLSN.js';
|
|
2
|
+
|
|
3
|
+
import { MongoManager } from '@module/replication/MongoManager.js';
|
|
4
|
+
import { normalizeConnectionConfig } from '@module/types/types.js';
|
|
5
|
+
import { isMongoServerError, mongo } from '@powersync/lib-service-mongodb';
|
|
6
|
+
import { BucketStorageFactory, TestStorageOptions } from '@powersync/service-core';
|
|
7
|
+
import { describe, expect, test, vi } from 'vitest';
|
|
8
|
+
import { ChangeStreamTestContext } from './change_stream_utils.js';
|
|
9
|
+
import { env } from './env.js';
|
|
10
|
+
import { INITIALIZED_MONGO_STORAGE_FACTORY, INITIALIZED_POSTGRES_STORAGE_FACTORY } from './util.js';
|
|
11
|
+
|
|
12
|
+
describe('mongo lsn', () => {
|
|
13
|
+
test('LSN with resume tokens should be comparable', () => {
|
|
14
|
+
// Values without a resume token should be comparable
|
|
15
|
+
expect(
|
|
16
|
+
new MongoLSN({
|
|
17
|
+
timestamp: mongo.Timestamp.fromNumber(1)
|
|
18
|
+
}).comparable <
|
|
19
|
+
new MongoLSN({
|
|
20
|
+
timestamp: mongo.Timestamp.fromNumber(10)
|
|
21
|
+
}).comparable
|
|
22
|
+
).true;
|
|
23
|
+
|
|
24
|
+
// Values with resume tokens should correctly compare
|
|
25
|
+
expect(
|
|
26
|
+
new MongoLSN({
|
|
27
|
+
timestamp: mongo.Timestamp.fromNumber(1),
|
|
28
|
+
resume_token: { _data: 'resume1' }
|
|
29
|
+
}).comparable <
|
|
30
|
+
new MongoLSN({
|
|
31
|
+
timestamp: mongo.Timestamp.fromNumber(10),
|
|
32
|
+
resume_token: { _data: 'resume2' }
|
|
33
|
+
}).comparable
|
|
34
|
+
).true;
|
|
35
|
+
|
|
36
|
+
// The resume token should not affect comparison
|
|
37
|
+
expect(
|
|
38
|
+
new MongoLSN({
|
|
39
|
+
timestamp: mongo.Timestamp.fromNumber(1),
|
|
40
|
+
resume_token: { _data: '2' }
|
|
41
|
+
}).comparable <
|
|
42
|
+
new MongoLSN({
|
|
43
|
+
timestamp: mongo.Timestamp.fromNumber(10),
|
|
44
|
+
resume_token: { _data: '1' }
|
|
45
|
+
}).comparable
|
|
46
|
+
).true;
|
|
47
|
+
|
|
48
|
+
// Resume token should not be required for comparison
|
|
49
|
+
expect(
|
|
50
|
+
new MongoLSN({
|
|
51
|
+
timestamp: mongo.Timestamp.fromNumber(10),
|
|
52
|
+
resume_token: { _data: '2' }
|
|
53
|
+
}).comparable > // Switching the order to test this case
|
|
54
|
+
new MongoLSN({
|
|
55
|
+
timestamp: mongo.Timestamp.fromNumber(9)
|
|
56
|
+
}).comparable
|
|
57
|
+
).true;
|
|
58
|
+
|
|
59
|
+
// Comparison should be backwards compatible with old LSNs
|
|
60
|
+
expect(
|
|
61
|
+
new MongoLSN({
|
|
62
|
+
timestamp: mongo.Timestamp.fromNumber(10),
|
|
63
|
+
resume_token: { _data: '2' }
|
|
64
|
+
}).comparable > ZERO_LSN
|
|
65
|
+
).true;
|
|
66
|
+
expect(
|
|
67
|
+
new MongoLSN({
|
|
68
|
+
timestamp: mongo.Timestamp.fromNumber(10),
|
|
69
|
+
resume_token: { _data: '2' }
|
|
70
|
+
}).comparable >
|
|
71
|
+
new MongoLSN({
|
|
72
|
+
timestamp: mongo.Timestamp.fromNumber(1)
|
|
73
|
+
}).comparable.split('|')[0] // Simulate an old LSN
|
|
74
|
+
).true;
|
|
75
|
+
expect(
|
|
76
|
+
new MongoLSN({
|
|
77
|
+
timestamp: mongo.Timestamp.fromNumber(1),
|
|
78
|
+
resume_token: { _data: '2' }
|
|
79
|
+
}).comparable <
|
|
80
|
+
new MongoLSN({
|
|
81
|
+
timestamp: mongo.Timestamp.fromNumber(10)
|
|
82
|
+
}).comparable.split('|')[0] // Simulate an old LSN
|
|
83
|
+
).true;
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe.skipIf(!env.TEST_MONGO_STORAGE)('MongoDB resume - mongo storage', () => {
|
|
88
|
+
defineResumeTest(INITIALIZED_MONGO_STORAGE_FACTORY);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe.skipIf(!env.TEST_POSTGRES_STORAGE)('MongoDB resume - postgres storage', () => {
|
|
92
|
+
defineResumeTest(INITIALIZED_POSTGRES_STORAGE_FACTORY);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
function defineResumeTest(factoryGenerator: (options?: TestStorageOptions) => Promise<BucketStorageFactory>) {
|
|
96
|
+
test('resuming with a different source database', async () => {
|
|
97
|
+
await using context = await ChangeStreamTestContext.open(factoryGenerator);
|
|
98
|
+
const { db } = context;
|
|
99
|
+
|
|
100
|
+
await context.updateSyncRules(/* yaml */
|
|
101
|
+
` bucket_definitions:
|
|
102
|
+
global:
|
|
103
|
+
data:
|
|
104
|
+
- SELECT _id as id, description, num FROM "test_data"`);
|
|
105
|
+
|
|
106
|
+
await context.replicateSnapshot();
|
|
107
|
+
|
|
108
|
+
context.startStreaming();
|
|
109
|
+
|
|
110
|
+
const collection = db.collection('test_data');
|
|
111
|
+
await collection.insertOne({ description: 'test1', num: 1152921504606846976n });
|
|
112
|
+
|
|
113
|
+
// Wait for the item above to be replicated. The commit should store a resume token.
|
|
114
|
+
await vi.waitFor(
|
|
115
|
+
async () => {
|
|
116
|
+
const checkpoint = await context.storage?.getCheckpoint();
|
|
117
|
+
expect(MongoLSN.fromSerialized(checkpoint!.lsn!).resumeToken).exist;
|
|
118
|
+
},
|
|
119
|
+
{ timeout: 5000 }
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// Done with this context for now
|
|
123
|
+
await context.dispose();
|
|
124
|
+
|
|
125
|
+
// Use the provided MongoDB url to connect to a different source database
|
|
126
|
+
const originalUrl = env.MONGO_TEST_URL;
|
|
127
|
+
// Change this to a different database
|
|
128
|
+
const url = new URL(originalUrl);
|
|
129
|
+
const parts = url.pathname.split('/');
|
|
130
|
+
parts[1] = 'differentDB'; // Replace the database name
|
|
131
|
+
url.pathname = parts.join('/');
|
|
132
|
+
|
|
133
|
+
// Point to a new source DB
|
|
134
|
+
const connectionManager = new MongoManager(
|
|
135
|
+
normalizeConnectionConfig({
|
|
136
|
+
type: 'mongodb',
|
|
137
|
+
uri: url.toString()
|
|
138
|
+
})
|
|
139
|
+
);
|
|
140
|
+
const factory = await factoryGenerator({ doNotClear: true });
|
|
141
|
+
|
|
142
|
+
// Create a new context without updating the sync rules
|
|
143
|
+
await using context2 = new ChangeStreamTestContext(factory, connectionManager);
|
|
144
|
+
const activeContent = await factory.getActiveSyncRulesContent();
|
|
145
|
+
context2.storage = factory.getInstance(activeContent!);
|
|
146
|
+
|
|
147
|
+
const error = await context2.startStreaming().catch((ex) => ex);
|
|
148
|
+
expect(error).exist;
|
|
149
|
+
// The ChangeStreamReplicationJob will detect this and throw a ChangeStreamInvalidatedError
|
|
150
|
+
expect(isMongoServerError(error) && error.hasErrorLabel('NonResumableChangeStreamError'));
|
|
151
|
+
});
|
|
152
|
+
}
|
package/test/src/util.ts
CHANGED
|
@@ -4,6 +4,7 @@ import * as postgres_storage from '@powersync/service-module-postgres-storage';
|
|
|
4
4
|
|
|
5
5
|
import * as types from '@module/types/types.js';
|
|
6
6
|
import { env } from './env.js';
|
|
7
|
+
import { BSON_DESERIALIZE_DATA_OPTIONS } from '@powersync/service-core';
|
|
7
8
|
|
|
8
9
|
export const TEST_URI = env.MONGO_TEST_DATA_URL;
|
|
9
10
|
|
|
@@ -30,7 +31,7 @@ export async function connectMongoData() {
|
|
|
30
31
|
connectTimeoutMS: env.CI ? 15_000 : 5_000,
|
|
31
32
|
socketTimeoutMS: env.CI ? 15_000 : 5_000,
|
|
32
33
|
serverSelectionTimeoutMS: env.CI ? 15_000 : 2_500,
|
|
33
|
-
|
|
34
|
+
...BSON_DESERIALIZE_DATA_OPTIONS
|
|
34
35
|
});
|
|
35
36
|
const dbname = new URL(env.MONGO_TEST_DATA_URL).pathname.substring(1);
|
|
36
37
|
return { client, db: client.db(dbname) };
|