@powersync/service-module-mongodb 0.0.0-dev-20260909133214 → 0.0.0-dev-20260929095716

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.
Files changed (88) hide show
  1. package/CHANGELOG.md +38 -6
  2. package/ci/test-connection.yaml +12 -0
  3. package/dist/api/MongoRouteAPIAdapter.d.ts +2 -2
  4. package/dist/api/MongoRouteAPIAdapter.js +73 -143
  5. package/dist/api/MongoRouteAPIAdapter.js.map +1 -1
  6. package/dist/api/infer-collection-schema.d.ts +12 -0
  7. package/dist/api/infer-collection-schema.js +142 -0
  8. package/dist/api/infer-collection-schema.js.map +1 -0
  9. package/dist/index.d.ts +1 -0
  10. package/dist/index.js +1 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/module/MongoModule.d.ts +1 -2
  13. package/dist/module/MongoModule.js +0 -1
  14. package/dist/module/MongoModule.js.map +1 -1
  15. package/dist/replication/ChangeStream.d.ts +10 -2
  16. package/dist/replication/ChangeStream.js +228 -317
  17. package/dist/replication/ChangeStream.js.map +1 -1
  18. package/dist/replication/ChangeStreamReplicationJob.d.ts +4 -1
  19. package/dist/replication/ChangeStreamReplicationJob.js +4 -1
  20. package/dist/replication/ChangeStreamReplicationJob.js.map +1 -1
  21. package/dist/replication/ChangeStreamReplicator.d.ts +3 -0
  22. package/dist/replication/ChangeStreamReplicator.js +3 -0
  23. package/dist/replication/ChangeStreamReplicator.js.map +1 -1
  24. package/dist/replication/MongoRelation.js +5 -2
  25. package/dist/replication/MongoRelation.js.map +1 -1
  26. package/dist/replication/MongoReplicationQueryProvider.d.ts +87 -0
  27. package/dist/replication/MongoReplicationQueryProvider.js +23 -0
  28. package/dist/replication/MongoReplicationQueryProvider.js.map +1 -0
  29. package/dist/replication/MongoReplicationStream.d.ts +85 -0
  30. package/dist/replication/MongoReplicationStream.js +101 -0
  31. package/dist/replication/MongoReplicationStream.js.map +1 -0
  32. package/dist/replication/MongoSnapshotQuery.d.ts +2 -0
  33. package/dist/replication/MongoSnapshotQuery.js +12 -2
  34. package/dist/replication/MongoSnapshotQuery.js.map +1 -1
  35. package/dist/replication/MongoSnapshotter.d.ts +7 -2
  36. package/dist/replication/MongoSnapshotter.js +58 -85
  37. package/dist/replication/MongoSnapshotter.js.map +1 -1
  38. package/dist/replication/RawChangeStream.d.ts +1 -1
  39. package/dist/replication/RawChangeStream.js +1 -1
  40. package/dist/replication/RawChangeStream.js.map +1 -1
  41. package/dist/replication/SourceRowConverter.d.ts +1 -0
  42. package/dist/replication/SourceRowConverter.js +4 -2
  43. package/dist/replication/SourceRowConverter.js.map +1 -1
  44. package/dist/replication/bufferToSqlite.d.ts +3 -1
  45. package/dist/replication/bufferToSqlite.js +35 -23
  46. package/dist/replication/bufferToSqlite.js.map +1 -1
  47. package/dist/replication/replication-index.d.ts +6 -0
  48. package/dist/replication/replication-index.js +6 -0
  49. package/dist/replication/replication-index.js.map +1 -1
  50. package/dist/test-utils/ChangeStreamTestContext.d.ts +131 -0
  51. package/dist/test-utils/ChangeStreamTestContext.js +391 -0
  52. package/dist/test-utils/ChangeStreamTestContext.js.map +1 -0
  53. package/dist/test-utils/test-utils-index.d.ts +1 -0
  54. package/dist/test-utils/test-utils-index.js +2 -0
  55. package/dist/test-utils/test-utils-index.js.map +1 -0
  56. package/package.json +8 -8
  57. package/src/api/MongoRouteAPIAdapter.ts +79 -127
  58. package/src/api/infer-collection-schema.ts +153 -0
  59. package/src/index.ts +1 -0
  60. package/src/module/MongoModule.ts +1 -3
  61. package/src/replication/ChangeStream.ts +253 -359
  62. package/src/replication/ChangeStreamReplicationJob.ts +7 -2
  63. package/src/replication/ChangeStreamReplicator.ts +5 -0
  64. package/src/replication/MongoRelation.ts +8 -1
  65. package/src/replication/MongoReplicationQueryProvider.ts +108 -0
  66. package/src/replication/MongoReplicationStream.ts +201 -0
  67. package/src/replication/MongoSnapshotQuery.ts +19 -3
  68. package/src/replication/MongoSnapshotter.ts +70 -92
  69. package/src/replication/RawChangeStream.ts +2 -2
  70. package/src/replication/SourceRowConverter.ts +4 -2
  71. package/src/replication/bufferToSqlite.ts +43 -23
  72. package/src/replication/replication-index.ts +6 -0
  73. package/src/test-utils/ChangeStreamTestContext.ts +520 -0
  74. package/src/test-utils/test-utils-index.ts +1 -0
  75. package/test/src/buffer_to_sqlite.test.ts +9 -0
  76. package/test/src/change_stream.test.ts +3 -3
  77. package/test/src/change_stream_test_setup.ts +38 -0
  78. package/test/src/chunked_snapshot.test.ts +3 -3
  79. package/test/src/documentdb_mode.test.ts +3 -3
  80. package/test/src/replication_stream.test.ts +311 -0
  81. package/test/src/resume.test.ts +10 -4
  82. package/test/src/resuming_snapshots.test.ts +3 -3
  83. package/test/src/schema.test.ts +223 -0
  84. package/test/src/slow_tests.test.ts +3 -3
  85. package/test/src/snapshot_query_provider.test.ts +95 -0
  86. package/test/src/stream_progress.test.ts +312 -0
  87. package/tsconfig.tsbuildinfo +1 -1
  88. package/test/src/change_stream_utils.ts +0 -375
@@ -8,10 +8,12 @@ import { logger } from '@powersync/lib-services-framework';
8
8
  import { CheckpointImplementation } from '../replication/checkpoints/CheckpointImplementation.js';
9
9
  import { createCheckpointImplementation } from '../replication/checkpoints/create-checkpoint-implementation.js';
10
10
  import { MongoManager } from '../replication/MongoManager.js';
11
- import { constructAfterRecord } from '../replication/MongoRelation.js';
12
11
  import { CHECKPOINTS_COLLECTION, detectDocumentDb } from '../replication/replication-utils.js';
13
12
  import * as types from '../types/types.js';
14
13
  import { escapeRegExp } from '../utils.js';
14
+ import { inferCollectionSchema, isSchemaInferenceFailure } from './infer-collection-schema.js';
15
+
16
+ const SCHEMA_INFERENCE_CONCURRENCY = 4;
15
17
 
16
18
  export class MongoRouteAPIAdapter implements api.RouteAPI {
17
19
  protected client: mongo.MongoClient;
@@ -21,6 +23,7 @@ export class MongoRouteAPIAdapter implements api.RouteAPI {
21
23
  defaultSchema: string;
22
24
 
23
25
  private checkpointImplementation: CheckpointImplementation | null = null;
26
+ private documentDbDetection: Promise<boolean> | null = null;
24
27
 
25
28
  constructor(protected config: types.ResolvedConnectionConfig) {
26
29
  const manager = new MongoManager(config);
@@ -147,7 +150,7 @@ export class MongoRouteAPIAdapter implements api.RouteAPI {
147
150
  };
148
151
  let errors: service_types.ReplicationError[] = [];
149
152
  if (collection.type == 'view') {
150
- errors.push({ level: 'warning', message: `Collection ${schema}.${tablePattern.name} is a view` });
153
+ errors.push({ level: 'warning', message: `Collection ${schema}.${collection.name} is a view` });
151
154
  } else {
152
155
  errors.push(...validatePostImages(schema, collection));
153
156
  }
@@ -206,9 +209,18 @@ export class MongoRouteAPIAdapter implements api.RouteAPI {
206
209
  return checkpointImplementation.createReplicationHead(callback);
207
210
  }
208
211
 
212
+ private isDocumentDb(): Promise<boolean> {
213
+ // Share in-flight detection and cache its result for this adapter's lifetime.
214
+ return (this.documentDbDetection ??= detectDocumentDb(this.db).catch((error) => {
215
+ // Allow a later request to retry after a transient connection error.
216
+ this.documentDbDetection = null;
217
+ throw error;
218
+ }));
219
+ }
220
+
209
221
  private async getCheckpointImplementation(): Promise<CheckpointImplementation> {
210
222
  if (this.checkpointImplementation == null) {
211
- const isDocumentDb = await detectDocumentDb(this.db);
223
+ const isDocumentDb = await this.isDocumentDb();
212
224
  this.checkpointImplementation = createCheckpointImplementation(isDocumentDb, {
213
225
  client: this.client,
214
226
  db: this.db,
@@ -226,32 +238,38 @@ export class MongoRouteAPIAdapter implements api.RouteAPI {
226
238
  }
227
239
 
228
240
  async getConnectionSchema(): Promise<service_types.DatabaseSchema[]> {
229
- const sampleSize = 50;
230
-
241
+ const isDocumentDb = await this.isDocumentDb();
231
242
  const databases = await this.db.admin().listDatabases({ nameOnly: true });
232
243
  const filteredDatabases = databases.databases.filter((db) => {
233
244
  return !['local', 'admin', 'config'].includes(db.name);
234
245
  });
235
- const databaseSchemas = await Promise.all(
236
- filteredDatabases.map(async (db) => {
237
- /**
238
- * Filtering the list of database with `authorizedDatabases: true`
239
- * does not produce the full list of databases under some circumstances.
240
- * This catches any potential auth errors.
241
- */
242
- let collections: mongo.CollectionInfo[];
243
- try {
244
- collections = await this.client.db(db.name).listCollections().toArray();
245
- } catch (e) {
246
- if (lib_mongo.isMongoServerError(e) && e.codeName == 'Unauthorized') {
247
- // Ignore databases we're not authorized to query
248
- return null;
249
- }
250
- throw e;
246
+ const databaseSchemas: service_types.DatabaseSchema[] = [];
247
+ for (const db of filteredDatabases) {
248
+ /**
249
+ * Filtering the list of database with `authorizedDatabases: true`
250
+ * does not produce the full list of databases under some circumstances.
251
+ * This catches any potential auth errors.
252
+ */
253
+ let collections: mongo.CollectionInfo[];
254
+ try {
255
+ collections = await this.client.db(db.name).listCollections().toArray();
256
+ } catch (e) {
257
+ if (lib_mongo.isMongoServerError(e) && e.codeName == 'Unauthorized') {
258
+ // Ignore databases we're not authorized to query
259
+ continue;
251
260
  }
261
+ throw e;
262
+ }
252
263
 
253
- let tables: service_types.TableSchema[] = [];
254
- for (let collection of collections) {
264
+ const tables: (service_types.TableSchema | undefined)[] = new Array(collections.length);
265
+ const pendingCollections = collections.entries();
266
+ let failed = false;
267
+ const inferCollections = async () => {
268
+ // Reading from the iterator automatically manages the queue
269
+ for (const [index, collection] of pendingCollections) {
270
+ if (failed) {
271
+ return;
272
+ }
255
273
  if ([CHECKPOINTS_COLLECTION].includes(collection.name)) {
256
274
  continue;
257
275
  }
@@ -264,121 +282,55 @@ export class MongoRouteAPIAdapter implements api.RouteAPI {
264
282
  continue;
265
283
  }
266
284
  try {
267
- const sampleDocuments = await this.db
268
- .collection(collection.name)
269
- .aggregate([{ $sample: { size: sampleSize } }])
270
- .toArray();
271
-
272
- if (sampleDocuments.length > 0) {
273
- const columns = this.getColumnsFromDocuments(sampleDocuments);
274
-
275
- tables.push({
276
- name: collection.name,
277
- // Since documents are sampled in a random order, we need to sort
278
- // to get a consistent order
279
- columns: columns.sort((a, b) => a.name.localeCompare(b.name))
280
- });
281
- } else {
282
- tables.push({
283
- name: collection.name,
284
- columns: []
285
- });
286
- }
285
+ const columns = await inferCollectionSchema(
286
+ this.client.db(db.name).collection(collection.name),
287
+ isDocumentDb
288
+ );
289
+ // Preserve collection order even when queries finish out of order.
290
+ tables[index] = { name: collection.name, columns };
287
291
  } catch (e) {
288
292
  if (lib_mongo.isMongoServerError(e) && e.codeName == 'Unauthorized') {
289
293
  // Ignore collections we're not authorized to query
290
294
  continue;
291
295
  }
296
+ if (isSchemaInferenceFailure(e)) {
297
+ // We know the collection is there, we just could not read its fields. Report it
298
+ // without columns rather than failing: one collection we cannot sample should not
299
+ // block validation and deployment for every sync config on the instance. Queries
300
+ // naming its columns warn about unknown columns; the result is indistinguishable
301
+ // from an empty collection, so a `SELECT *` query still validates cleanly.
302
+ logger.warn(`Could not infer the schema of ${db.name}.${collection.name}`, e);
303
+ tables[index] = { name: collection.name, columns: [] };
304
+ continue;
305
+ }
306
+ // Fail the whole request on unexpected errors: the response cannot indicate
307
+ // partial results, so omitting a collection would make an incomplete schema
308
+ // appear complete and could produce an incorrect generated client schema.
309
+ failed = true;
292
310
  throw e;
293
311
  }
294
312
  }
295
-
296
- return {
297
- name: db.name,
298
- tables: tables
299
- } satisfies service_types.DatabaseSchema;
300
- })
301
- );
302
- return databaseSchemas.filter((schema) => !!schema);
303
- }
304
-
305
- private getColumnsFromDocuments(documents: mongo.BSON.Document[]) {
306
- let columns = new Map<string, { sqliteType: sync_rules.ExpressionType; bsonTypes: Set<string> }>();
307
- for (const document of documents) {
308
- const parsed = constructAfterRecord(document);
309
- for (const key in parsed) {
310
- const value = parsed[key];
311
- const type = sync_rules.sqliteTypeOf(value);
312
- const sqliteType = sync_rules.ExpressionType.fromTypeText(type);
313
- let entry = columns.get(key);
314
- if (entry == null) {
315
- entry = { sqliteType, bsonTypes: new Set() };
316
- columns.set(key, entry);
317
- } else {
318
- entry.sqliteType = entry.sqliteType.or(sqliteType);
319
- }
320
- const bsonType = this.getBsonType(document[key]);
321
- if (bsonType != null) {
322
- entry.bsonTypes.add(bsonType);
323
- }
324
- }
325
- }
326
- return [...columns.entries()].map(([key, value]) => {
327
- const internal_type = value.bsonTypes.size == 0 ? '' : [...value.bsonTypes].join(' | ');
328
- return {
329
- name: key,
330
- type: internal_type,
331
- sqlite_type: value.sqliteType.typeFlags,
332
- internal_type,
333
- pg_type: internal_type
334
313
  };
335
- });
336
- }
337
314
 
338
- private getBsonType(data: any): string | null {
339
- if (data == null) {
340
- // null or undefined
341
- return 'Null';
342
- } else if (typeof data == 'string') {
343
- return 'String';
344
- } else if (typeof data == 'number') {
345
- if (Number.isInteger(data)) {
346
- return 'Integer';
347
- } else {
348
- return 'Double';
315
+ // Each worker holds only schema metadata, with a bounded number of source queries.
316
+ const workers = Array.from(
317
+ { length: Math.min(SCHEMA_INFERENCE_CONCURRENCY, collections.length) },
318
+ inferCollections
319
+ );
320
+ try {
321
+ await Promise.all(workers);
322
+ } catch (e) {
323
+ // Finish in-flight queries before rejecting the request. Workers stop taking
324
+ // new collections as soon as one encounters a non-authorization error.
325
+ await Promise.allSettled(workers);
326
+ throw e;
349
327
  }
350
- } else if (typeof data == 'bigint') {
351
- return 'Long';
352
- } else if (typeof data == 'boolean') {
353
- return 'Boolean';
354
- } else if (data instanceof mongo.ObjectId) {
355
- return 'ObjectId';
356
- } else if (data instanceof mongo.UUID) {
357
- return 'UUID';
358
- } else if (data instanceof Date) {
359
- return 'Date';
360
- } else if (data instanceof mongo.Timestamp) {
361
- return 'Timestamp';
362
- } else if (data instanceof mongo.Binary) {
363
- return 'Binary';
364
- } else if (data instanceof mongo.Long) {
365
- return 'Long';
366
- } else if (data instanceof RegExp) {
367
- return 'RegExp';
368
- } else if (data instanceof mongo.MinKey) {
369
- return 'MinKey';
370
- } else if (data instanceof mongo.MaxKey) {
371
- return 'MaxKey';
372
- } else if (data instanceof mongo.Decimal128) {
373
- return 'Decimal';
374
- } else if (Array.isArray(data)) {
375
- return 'Array';
376
- } else if (data instanceof Uint8Array) {
377
- return 'Binary';
378
- } else if (typeof data == 'object') {
379
- return 'Object';
380
- } else {
381
- return null;
328
+
329
+ databaseSchemas.push({
330
+ name: db.name,
331
+ tables: tables.filter((table) => table != null)
332
+ });
382
333
  }
334
+ return databaseSchemas;
383
335
  }
384
336
  }
@@ -0,0 +1,153 @@
1
+ import * as lib_mongo from '@powersync/lib-service-mongodb';
2
+ import { mongo } from '@powersync/lib-service-mongodb';
3
+ import { ExpressionType } from '@powersync/service-sync-rules';
4
+ import { TableSchema } from '@powersync/service-types';
5
+
6
+ /**
7
+ * Server errors that mean this one collection could not be inspected, rather than the
8
+ * connection being unusable.
9
+ *
10
+ * These come from the memory ceiling {@link inferCollectionSchema} imposes on itself by
11
+ * refusing to spill to disk, and are deterministic per collection. Timeouts are
12
+ * deliberately excluded: they usually indicate an overloaded source, where degrading
13
+ * would silently empty the schema of every collection at once.
14
+ */
15
+ const SCHEMA_INFERENCE_FAILURE_CODES = new Set([
16
+ // ExceededMemoryLimit - a blocking stage ran out of memory with disk spill disabled.
17
+ 146,
18
+ // QueryExceededMemoryLimitNoDiskUseAllowed - as above, reported by the sort $sample
19
+ // uses on collections too small for its random cursor.
20
+ 292
21
+ ]);
22
+
23
+ /**
24
+ * Whether the error means we could not determine this collection's fields, as opposed to
25
+ * something that makes the rest of the schema unreliable too.
26
+ */
27
+ export function isSchemaInferenceFailure(e: unknown): boolean {
28
+ return lib_mongo.isMongoServerError(e) && SCHEMA_INFERENCE_FAILURE_CODES.has(e.code as number);
29
+ }
30
+
31
+ // Match the types exposed after BSON deserialization and conversion to sync rules values.
32
+ const BSON_TYPES: Record<string, { name: string; sqliteType: ExpressionType }> = {
33
+ int: { name: 'Integer', sqliteType: ExpressionType.INTEGER },
34
+ long: { name: 'Long', sqliteType: ExpressionType.INTEGER },
35
+ double: { name: 'Double', sqliteType: ExpressionType.REAL },
36
+ decimal: { name: 'Decimal', sqliteType: ExpressionType.TEXT },
37
+ string: { name: 'String', sqliteType: ExpressionType.TEXT },
38
+ symbol: { name: 'String', sqliteType: ExpressionType.TEXT },
39
+ bool: { name: 'Boolean', sqliteType: ExpressionType.INTEGER },
40
+ objectId: { name: 'ObjectId', sqliteType: ExpressionType.TEXT },
41
+ uuid: { name: 'UUID', sqliteType: ExpressionType.TEXT },
42
+ binData: { name: 'Binary', sqliteType: ExpressionType.BLOB },
43
+ date: { name: 'Date', sqliteType: ExpressionType.TEXT },
44
+ timestamp: { name: 'Timestamp', sqliteType: ExpressionType.INTEGER },
45
+ regex: { name: 'RegExp', sqliteType: ExpressionType.TEXT },
46
+ object: { name: 'Object', sqliteType: ExpressionType.TEXT },
47
+ array: { name: 'Array', sqliteType: ExpressionType.TEXT },
48
+ javascript: { name: 'Object', sqliteType: ExpressionType.TEXT },
49
+ javascriptWithScope: { name: 'Object', sqliteType: ExpressionType.TEXT },
50
+ dbPointer: { name: 'Object', sqliteType: ExpressionType.TEXT },
51
+ null: { name: 'Null', sqliteType: ExpressionType.NONE },
52
+ undefined: { name: 'Null', sqliteType: ExpressionType.NONE },
53
+ minKey: { name: 'MinKey', sqliteType: ExpressionType.NONE },
54
+ maxKey: { name: 'MaxKey', sqliteType: ExpressionType.NONE }
55
+ };
56
+
57
+ /**
58
+ * Infer top-level fields without transferring sampled document values to the service.
59
+ * Memory on the service scales with the inferred schema, not the size of those values.
60
+ */
61
+ export async function inferCollectionSchema(
62
+ collection: mongo.Collection,
63
+ isDocumentDb: boolean
64
+ ): Promise<TableSchema['columns']> {
65
+ const fields = await collection
66
+ .aggregate<{ _id: string; types: string[] }>(
67
+ [
68
+ // Keep this first so MongoDB can use its random cursor on large collections.
69
+ { $sample: { size: 50 } },
70
+ {
71
+ $project: {
72
+ _id: 0,
73
+ fields: {
74
+ $map: {
75
+ input: { $objectToArray: '$$ROOT' },
76
+ as: 'field',
77
+ in: {
78
+ name: '$$field.k',
79
+ type: {
80
+ $let: {
81
+ vars: { bsonType: { $type: '$$field.v' } },
82
+ in: {
83
+ $switch: {
84
+ branches: [
85
+ {
86
+ case: { $eq: ['$$bsonType', 'double'] },
87
+ // Whole-number doubles are replicated as SQLite integers.
88
+ then: { $cond: [{ $eq: [{ $mod: ['$$field.v', 1] }, 0] }, 'int', 'double'] }
89
+ },
90
+ {
91
+ case: { $eq: ['$$bsonType', 'binData'] },
92
+ then: {
93
+ $cond: [
94
+ // BinData compares by length, then subtype, then bytes. Only
95
+ // 16-byte values of subtype 4 fall within this UUID range.
96
+ // This works without transferring binary values or requiring
97
+ // the newer MongoDB binary conversion operators.
98
+ // https://www.mongodb.com/docs/manual/reference/bson-type-comparison-order/#bindata
99
+ {
100
+ $and: [
101
+ { $gte: ['$$field.v', new mongo.UUID('00000000-0000-0000-0000-000000000000')] },
102
+ { $lte: ['$$field.v', new mongo.UUID('ffffffff-ffff-ffff-ffff-ffffffffffff')] }
103
+ ]
104
+ },
105
+ 'uuid',
106
+ 'binData'
107
+ ]
108
+ }
109
+ }
110
+ ],
111
+ default: '$$bsonType'
112
+ }
113
+ }
114
+ }
115
+ }
116
+ }
117
+ }
118
+ }
119
+ }
120
+ },
121
+ // Discard values before unwinding so large documents aren't duplicated per field.
122
+ { $unwind: '$fields' },
123
+ { $group: { _id: '$fields.name', types: { $addToSet: '$fields.type' } } }
124
+ ],
125
+ {
126
+ // Bound execution per collection below the default 60-second socket timeout
127
+ // so MongoDB can return a query timeout before the connection times out.
128
+ maxTimeMS: 30_000,
129
+ // Small collections can require $sample to sort full documents. Disable
130
+ // disk spill explicitly so concurrent schema queries fail at the memory
131
+ // limit without adding temporary-file I/O on the source database.
132
+ allowDiskUse: false,
133
+ // Field names are case-sensitive even when the collection's default collation isn't.
134
+ // DocumentDB rejects the collation option, including simple collation.
135
+ ...(isDocumentDb ? {} : { collation: { locale: 'simple' } })
136
+ }
137
+ )
138
+ .toArray();
139
+
140
+ return fields
141
+ .map(({ _id: name, types }) => {
142
+ let sqliteType = ExpressionType.NONE;
143
+ const bsonTypes = new Set<string>();
144
+ for (const type of types) {
145
+ const inferred = BSON_TYPES[type];
146
+ sqliteType = sqliteType.or(inferred.sqliteType);
147
+ bsonTypes.add(inferred.name);
148
+ }
149
+ const internal_type = [...bsonTypes].sort().join(' | ');
150
+ return { name, type: internal_type, sqlite_type: sqliteType.typeFlags, internal_type, pg_type: internal_type };
151
+ })
152
+ .sort((a, b) => a.name.localeCompare(b.name));
153
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './api/MongoRouteAPIAdapter.js';
2
2
  export * from './module/MongoModule.js';
3
3
  export * from './replication/replication-index.js';
4
+ export * as test_utils from './test-utils/test-utils-index.js';
4
5
  export * from './types/types.js';
@@ -44,12 +44,10 @@ export class MongoModule extends replication.ReplicationModule<types.MongoConnec
44
44
  });
45
45
  }
46
46
 
47
- async onInitialized(context: system.ServiceContextContainer): Promise<void> {}
48
-
49
47
  /**
50
48
  * Combines base config with normalized connection settings
51
49
  */
52
- private resolveConfig(config: types.MongoConnectionConfig): types.ResolvedConnectionConfig {
50
+ protected resolveConfig(config: types.MongoConnectionConfig): types.ResolvedConnectionConfig {
53
51
  return {
54
52
  ...config,
55
53
  ...types.normalizeConnectionConfig(config)