@powersync/service-module-mongodb 0.3.1 → 0.4.1
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 +30 -0
- package/dist/module/MongoModule.d.ts +3 -2
- package/dist/module/MongoModule.js +15 -6
- package/dist/module/MongoModule.js.map +1 -1
- package/dist/replication/ChangeStream.js +9 -9
- package/dist/replication/ChangeStream.js.map +1 -1
- package/dist/replication/ChangeStreamReplicator.d.ts +1 -0
- package/dist/replication/ChangeStreamReplicator.js +4 -0
- package/dist/replication/ChangeStreamReplicator.js.map +1 -1
- package/dist/replication/ConnectionManagerFactory.d.ts +1 -1
- package/dist/replication/MongoManager.js +1 -0
- package/dist/replication/MongoManager.js.map +1 -1
- package/dist/replication/MongoRelation.js +2 -1
- package/dist/replication/MongoRelation.js.map +1 -1
- package/dist/replication/replication-utils.js +49 -2
- package/dist/replication/replication-utils.js.map +1 -1
- package/dist/types/types.d.ts +4 -0
- package/dist/types/types.js.map +1 -1
- package/package.json +9 -9
- package/src/module/MongoModule.ts +24 -8
- package/src/replication/ChangeStream.ts +22 -9
- package/src/replication/ChangeStreamReplicator.ts +5 -0
- package/src/replication/ConnectionManagerFactory.ts +1 -1
- package/src/replication/MongoManager.ts +2 -0
- package/src/replication/MongoRelation.ts +2 -1
- package/src/replication/replication-utils.ts +77 -2
- package/src/types/types.ts +3 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { mongo } from '@powersync/lib-service-mongodb';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
container,
|
|
4
|
+
ErrorCode,
|
|
5
|
+
logger,
|
|
6
|
+
ReplicationAbortedError,
|
|
7
|
+
ReplicationAssertionError,
|
|
8
|
+
ServiceError
|
|
9
|
+
} from '@powersync/lib-services-framework';
|
|
3
10
|
import { Metrics, SaveOperationTag, SourceEntityDescriptor, SourceTable, storage } from '@powersync/service-core';
|
|
4
11
|
import { DatabaseInputRow, SqliteRow, SqlSyncRules, TablePattern } from '@powersync/service-sync-rules';
|
|
5
12
|
import { PostImagesOption } from '../types/types.js';
|
|
@@ -180,12 +187,18 @@ export class ChangeStream {
|
|
|
180
187
|
const hello = await this.defaultDb.command({ hello: 1 });
|
|
181
188
|
const snapshotTime = hello.lastWrite?.majorityOpTime?.ts as mongo.Timestamp;
|
|
182
189
|
if (hello.msg == 'isdbgrid') {
|
|
183
|
-
throw new
|
|
190
|
+
throw new ServiceError(
|
|
191
|
+
ErrorCode.PSYNC_S1341,
|
|
192
|
+
'Sharded MongoDB Clusters are not supported yet (including MongoDB Serverless instances).'
|
|
193
|
+
);
|
|
184
194
|
} else if (hello.setName == null) {
|
|
185
|
-
throw new
|
|
195
|
+
throw new ServiceError(
|
|
196
|
+
ErrorCode.PSYNC_S1342,
|
|
197
|
+
'Standalone MongoDB instances are not supported - use a replicaset.'
|
|
198
|
+
);
|
|
186
199
|
} else if (snapshotTime == null) {
|
|
187
200
|
// Not known where this would happen apart from the above cases
|
|
188
|
-
throw new
|
|
201
|
+
throw new ReplicationAssertionError('MongoDB lastWrite timestamp not found.');
|
|
189
202
|
}
|
|
190
203
|
// We previously used {snapshot: true} for the snapshot session.
|
|
191
204
|
// While it gives nice consistency guarantees, it fails when the
|
|
@@ -294,7 +307,7 @@ export class ChangeStream {
|
|
|
294
307
|
|
|
295
308
|
for await (let document of cursor) {
|
|
296
309
|
if (this.abort_signal.aborted) {
|
|
297
|
-
throw new
|
|
310
|
+
throw new ReplicationAbortedError(`Aborted initial replication`);
|
|
298
311
|
}
|
|
299
312
|
|
|
300
313
|
at += 1;
|
|
@@ -367,7 +380,7 @@ export class ChangeStream {
|
|
|
367
380
|
});
|
|
368
381
|
logger.info(`Enabled postImages on ${db}.${collectionInfo.name}`);
|
|
369
382
|
} else if (!enabled) {
|
|
370
|
-
throw new
|
|
383
|
+
throw new ServiceError(ErrorCode.PSYNC_S1343, `postImages not enabled on ${db}.${collectionInfo.name}`);
|
|
371
384
|
}
|
|
372
385
|
}
|
|
373
386
|
|
|
@@ -385,7 +398,7 @@ export class ChangeStream {
|
|
|
385
398
|
|
|
386
399
|
const snapshot = options.snapshot;
|
|
387
400
|
if (!descriptor.objectId && typeof descriptor.objectId != 'string') {
|
|
388
|
-
throw new
|
|
401
|
+
throw new ReplicationAssertionError('MongoDB replication - objectId expected');
|
|
389
402
|
}
|
|
390
403
|
const result = await this.storage.resolveTable({
|
|
391
404
|
group_id: this.group_id,
|
|
@@ -466,7 +479,7 @@ export class ChangeStream {
|
|
|
466
479
|
beforeReplicaId: change.documentKey._id
|
|
467
480
|
});
|
|
468
481
|
} else {
|
|
469
|
-
throw new
|
|
482
|
+
throw new ReplicationAssertionError(`Unsupported operation: ${change.operationType}`);
|
|
470
483
|
}
|
|
471
484
|
}
|
|
472
485
|
|
|
@@ -607,7 +620,7 @@ export class ChangeStream {
|
|
|
607
620
|
}
|
|
608
621
|
} else if (splitDocument != null) {
|
|
609
622
|
// We were waiting for fragments, but got a different event
|
|
610
|
-
throw new
|
|
623
|
+
throw new ReplicationAssertionError(`Incomplete splitEvent: ${JSON.stringify(splitDocument.splitEvent)}`);
|
|
611
624
|
}
|
|
612
625
|
|
|
613
626
|
// console.log('event', changeDocument);
|
|
@@ -2,6 +2,7 @@ import { storage, replication } from '@powersync/service-core';
|
|
|
2
2
|
import { ChangeStreamReplicationJob } from './ChangeStreamReplicationJob.js';
|
|
3
3
|
import { ConnectionManagerFactory } from './ConnectionManagerFactory.js';
|
|
4
4
|
import { MongoErrorRateLimiter } from './MongoErrorRateLimiter.js';
|
|
5
|
+
import { MongoModule } from '../module/MongoModule.js';
|
|
5
6
|
|
|
6
7
|
export interface ChangeStreamReplicatorOptions extends replication.AbstractReplicatorOptions {
|
|
7
8
|
connectionFactory: ConnectionManagerFactory;
|
|
@@ -33,4 +34,8 @@ export class ChangeStreamReplicator extends replication.AbstractReplicator<Chang
|
|
|
33
34
|
await super.stop();
|
|
34
35
|
await this.connectionFactory.shutdown();
|
|
35
36
|
}
|
|
37
|
+
|
|
38
|
+
async testConnection() {
|
|
39
|
+
return await MongoModule.testConnection(this.connectionFactory.dbConnectionConfig);
|
|
40
|
+
}
|
|
36
41
|
}
|
|
@@ -4,7 +4,7 @@ import { MongoManager } from './MongoManager.js';
|
|
|
4
4
|
|
|
5
5
|
export class ConnectionManagerFactory {
|
|
6
6
|
private readonly connectionManagers: MongoManager[];
|
|
7
|
-
|
|
7
|
+
public readonly dbConnectionConfig: NormalizedMongoConnectionConfig;
|
|
8
8
|
|
|
9
9
|
constructor(dbConnectionConfig: NormalizedMongoConnectionConfig) {
|
|
10
10
|
this.dbConnectionConfig = dbConnectionConfig;
|
|
@@ -4,6 +4,7 @@ import { JSONBig, JsonContainer } from '@powersync/service-jsonbig';
|
|
|
4
4
|
import { SqliteRow, SqliteValue } from '@powersync/service-sync-rules';
|
|
5
5
|
|
|
6
6
|
import { CHECKPOINTS_COLLECTION } from './replication-utils.js';
|
|
7
|
+
import { ErrorCode, ServiceError } from '@powersync/lib-services-framework';
|
|
7
8
|
|
|
8
9
|
export function getMongoRelation(source: mongo.ChangeStreamNameSpace): storage.SourceEntityDescriptor {
|
|
9
10
|
return {
|
|
@@ -97,7 +98,7 @@ function filterJsonData(data: any, depth = 0): any {
|
|
|
97
98
|
const autoBigNum = true;
|
|
98
99
|
if (depth > DEPTH_LIMIT) {
|
|
99
100
|
// This is primarily to prevent infinite recursion
|
|
100
|
-
throw new
|
|
101
|
+
throw new ServiceError(ErrorCode.PSYNC_S1004, `json nested object depth exceeds the limit of ${DEPTH_LIMIT}`);
|
|
101
102
|
}
|
|
102
103
|
if (data === null) {
|
|
103
104
|
return data;
|
|
@@ -1,13 +1,88 @@
|
|
|
1
|
+
import { ErrorCode, ServiceError } from '@powersync/lib-services-framework';
|
|
1
2
|
import { MongoManager } from './MongoManager.js';
|
|
3
|
+
import { PostImagesOption } from '../types/types.js';
|
|
2
4
|
|
|
3
5
|
export const CHECKPOINTS_COLLECTION = '_powersync_checkpoints';
|
|
4
6
|
|
|
7
|
+
const REQUIRED_CHECKPOINT_PERMISSIONS = ['find', 'insert', 'update', 'remove', 'changeStream', 'createCollection'];
|
|
8
|
+
|
|
5
9
|
export async function checkSourceConfiguration(connectionManager: MongoManager): Promise<void> {
|
|
6
10
|
const db = connectionManager.db;
|
|
11
|
+
|
|
7
12
|
const hello = await db.command({ hello: 1 });
|
|
8
13
|
if (hello.msg == 'isdbgrid') {
|
|
9
|
-
throw new
|
|
14
|
+
throw new ServiceError(
|
|
15
|
+
ErrorCode.PSYNC_S1341,
|
|
16
|
+
'Sharded MongoDB Clusters are not supported yet (including MongoDB Serverless instances).'
|
|
17
|
+
);
|
|
10
18
|
} else if (hello.setName == null) {
|
|
11
|
-
throw new
|
|
19
|
+
throw new ServiceError(ErrorCode.PSYNC_S1342, 'Standalone MongoDB instances are not supported - use a replicaset.');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// https://www.mongodb.com/docs/manual/reference/command/connectionStatus/
|
|
23
|
+
const connectionStatus = await db.command({ connectionStatus: 1, showPrivileges: true });
|
|
24
|
+
const priviledges = connectionStatus.authInfo?.authenticatedUserPrivileges as {
|
|
25
|
+
resource: { db: string; collection: string };
|
|
26
|
+
actions: string[];
|
|
27
|
+
}[];
|
|
28
|
+
let checkpointsActions = new Set<string>();
|
|
29
|
+
let anyCollectionActions = new Set<string>();
|
|
30
|
+
if (priviledges?.length > 0) {
|
|
31
|
+
const onDefaultDb = priviledges.filter((p) => p.resource.db == db.databaseName || p.resource.db == '');
|
|
32
|
+
const onCheckpoints = onDefaultDb.filter(
|
|
33
|
+
(p) => p.resource.collection == CHECKPOINTS_COLLECTION || p.resource?.collection == ''
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
for (let p of onCheckpoints) {
|
|
37
|
+
for (let a of p.actions) {
|
|
38
|
+
checkpointsActions.add(a);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
for (let p of onDefaultDb) {
|
|
42
|
+
for (let a of p.actions) {
|
|
43
|
+
anyCollectionActions.add(a);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const missingCheckpointActions = REQUIRED_CHECKPOINT_PERMISSIONS.filter(
|
|
48
|
+
(action) => !checkpointsActions.has(action)
|
|
49
|
+
);
|
|
50
|
+
if (missingCheckpointActions.length > 0) {
|
|
51
|
+
const fullName = `${db.databaseName}.${CHECKPOINTS_COLLECTION}`;
|
|
52
|
+
throw new ServiceError(
|
|
53
|
+
ErrorCode.PSYNC_S1307,
|
|
54
|
+
`MongoDB user does not have the required ${missingCheckpointActions.map((a) => `"${a}"`).join(', ')} priviledge(s) on "${fullName}".`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (connectionManager.options.postImages == PostImagesOption.AUTO_CONFIGURE) {
|
|
59
|
+
// This checks that we have collMod on _any_ collection in the db.
|
|
60
|
+
// This is not a complete check, but does give a basic sanity-check for testing the connection.
|
|
61
|
+
if (!anyCollectionActions.has('collMod')) {
|
|
62
|
+
throw new ServiceError(
|
|
63
|
+
ErrorCode.PSYNC_S1307,
|
|
64
|
+
`MongoDB user does not have the required "collMod" priviledge on "${db.databaseName}", required for "post_images: auto_configure".`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (!anyCollectionActions.has('listCollections')) {
|
|
69
|
+
throw new ServiceError(
|
|
70
|
+
ErrorCode.PSYNC_S1307,
|
|
71
|
+
`MongoDB user does not have the required "listCollections" priviledge on "${db.databaseName}".`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
// Assume auth is disabled.
|
|
76
|
+
// On Atlas, at least one role/priviledge is required for each user, which will trigger the above.
|
|
77
|
+
|
|
78
|
+
// We do still do a basic check that we can list the collection (it may not actually exist yet).
|
|
79
|
+
await db
|
|
80
|
+
.listCollections(
|
|
81
|
+
{
|
|
82
|
+
name: CHECKPOINTS_COLLECTION
|
|
83
|
+
},
|
|
84
|
+
{ nameOnly: false }
|
|
85
|
+
)
|
|
86
|
+
.toArray();
|
|
12
87
|
}
|
|
13
88
|
}
|
package/src/types/types.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as lib_mongo from '@powersync/lib-service-mongodb/types';
|
|
2
2
|
import * as service_types from '@powersync/service-types';
|
|
3
|
+
import { LookupFunction } from 'node:net';
|
|
3
4
|
import * as t from 'ts-codec';
|
|
4
5
|
|
|
5
6
|
export enum PostImagesOption {
|
|
@@ -48,6 +49,8 @@ export interface NormalizedMongoConnectionConfig {
|
|
|
48
49
|
username?: string;
|
|
49
50
|
password?: string;
|
|
50
51
|
|
|
52
|
+
lookup?: LookupFunction;
|
|
53
|
+
|
|
51
54
|
postImages: PostImagesOption;
|
|
52
55
|
}
|
|
53
56
|
|