@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.
@@ -1,5 +1,12 @@
1
1
  import { mongo } from '@powersync/lib-service-mongodb';
2
- import { container, logger } from '@powersync/lib-services-framework';
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 Error('Sharded MongoDB Clusters are not supported yet (including MongoDB Serverless instances).');
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 Error('Standalone MongoDB instances are not supported - use a replicaset.');
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 Error('MongoDB lastWrite timestamp not found.');
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 Error(`Aborted initial replication`);
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 Error(`postImages not enabled on ${db}.${collectionInfo.name}`);
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 Error('objectId expected');
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 Error(`Unsupported operation: ${change.operationType}`);
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 Error(`Incomplete splitEvent: ${JSON.stringify(splitDocument.splitEvent)}`);
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
- private readonly dbConnectionConfig: NormalizedMongoConnectionConfig;
7
+ public readonly dbConnectionConfig: NormalizedMongoConnectionConfig;
8
8
 
9
9
  constructor(dbConnectionConfig: NormalizedMongoConnectionConfig) {
10
10
  this.dbConnectionConfig = dbConnectionConfig;
@@ -19,6 +19,8 @@ export class MongoManager {
19
19
  username: options.username,
20
20
  password: options.password
21
21
  },
22
+
23
+ lookup: options.lookup,
22
24
  // Time for connection to timeout
23
25
  connectTimeoutMS: 5_000,
24
26
  // Time for individual requests to timeout
@@ -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 Error(`json nested object depth exceeds the limit of ${DEPTH_LIMIT}`);
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 Error('Sharded MongoDB Clusters are not supported yet (including MongoDB Serverless instances).');
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 Error('Standalone MongoDB instances are not supported - use a replicaset.');
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
  }
@@ -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