@powersync/service-module-mongodb 0.0.0-dev-20241001150444

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 (62) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/LICENSE +67 -0
  3. package/README.md +3 -0
  4. package/dist/api/MongoRouteAPIAdapter.d.ts +22 -0
  5. package/dist/api/MongoRouteAPIAdapter.js +64 -0
  6. package/dist/api/MongoRouteAPIAdapter.js.map +1 -0
  7. package/dist/index.d.ts +3 -0
  8. package/dist/index.js +4 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/module/MongoModule.d.ts +13 -0
  11. package/dist/module/MongoModule.js +46 -0
  12. package/dist/module/MongoModule.js.map +1 -0
  13. package/dist/replication/ChangeStream.d.ts +53 -0
  14. package/dist/replication/ChangeStream.js +389 -0
  15. package/dist/replication/ChangeStream.js.map +1 -0
  16. package/dist/replication/ChangeStreamReplicationJob.d.ts +16 -0
  17. package/dist/replication/ChangeStreamReplicationJob.js +90 -0
  18. package/dist/replication/ChangeStreamReplicationJob.js.map +1 -0
  19. package/dist/replication/ChangeStreamReplicator.d.ts +13 -0
  20. package/dist/replication/ChangeStreamReplicator.js +26 -0
  21. package/dist/replication/ChangeStreamReplicator.js.map +1 -0
  22. package/dist/replication/ConnectionManagerFactory.d.ts +9 -0
  23. package/dist/replication/ConnectionManagerFactory.js +21 -0
  24. package/dist/replication/ConnectionManagerFactory.js.map +1 -0
  25. package/dist/replication/MongoErrorRateLimiter.d.ts +11 -0
  26. package/dist/replication/MongoErrorRateLimiter.js +44 -0
  27. package/dist/replication/MongoErrorRateLimiter.js.map +1 -0
  28. package/dist/replication/MongoManager.d.ts +14 -0
  29. package/dist/replication/MongoManager.js +36 -0
  30. package/dist/replication/MongoManager.js.map +1 -0
  31. package/dist/replication/MongoRelation.d.ts +9 -0
  32. package/dist/replication/MongoRelation.js +174 -0
  33. package/dist/replication/MongoRelation.js.map +1 -0
  34. package/dist/replication/replication-index.d.ts +4 -0
  35. package/dist/replication/replication-index.js +5 -0
  36. package/dist/replication/replication-index.js.map +1 -0
  37. package/dist/types/types.d.ts +51 -0
  38. package/dist/types/types.js +37 -0
  39. package/dist/types/types.js.map +1 -0
  40. package/package.json +47 -0
  41. package/src/api/MongoRouteAPIAdapter.ts +86 -0
  42. package/src/index.ts +5 -0
  43. package/src/module/MongoModule.ts +52 -0
  44. package/src/replication/ChangeStream.ts +503 -0
  45. package/src/replication/ChangeStreamReplicationJob.ts +104 -0
  46. package/src/replication/ChangeStreamReplicator.ts +36 -0
  47. package/src/replication/ConnectionManagerFactory.ts +27 -0
  48. package/src/replication/MongoErrorRateLimiter.ts +45 -0
  49. package/src/replication/MongoManager.ts +47 -0
  50. package/src/replication/MongoRelation.ts +156 -0
  51. package/src/replication/replication-index.ts +4 -0
  52. package/src/types/types.ts +65 -0
  53. package/test/src/change_stream.test.ts +306 -0
  54. package/test/src/change_stream_utils.ts +148 -0
  55. package/test/src/env.ts +7 -0
  56. package/test/src/mongo_test.test.ts +219 -0
  57. package/test/src/setup.ts +7 -0
  58. package/test/src/util.ts +52 -0
  59. package/test/tsconfig.json +28 -0
  60. package/tsconfig.json +28 -0
  61. package/tsconfig.tsbuildinfo +1 -0
  62. package/vitest.config.ts +9 -0
@@ -0,0 +1,44 @@
1
+ import { setTimeout } from 'timers/promises';
2
+ export class MongoErrorRateLimiter {
3
+ constructor() {
4
+ this.nextAllowed = Date.now();
5
+ }
6
+ async waitUntilAllowed(options) {
7
+ const delay = Math.max(0, this.nextAllowed - Date.now());
8
+ // Minimum delay between connections, even without errors
9
+ this.setDelay(500);
10
+ await setTimeout(delay, undefined, { signal: options?.signal });
11
+ }
12
+ mayPing() {
13
+ return Date.now() >= this.nextAllowed;
14
+ }
15
+ reportError(e) {
16
+ // FIXME: Check mongodb-specific requirements
17
+ const message = e.message ?? '';
18
+ if (message.includes('password authentication failed')) {
19
+ // Wait 15 minutes, to avoid triggering Supabase's fail2ban
20
+ this.setDelay(900000);
21
+ }
22
+ else if (message.includes('ENOTFOUND')) {
23
+ // DNS lookup issue - incorrect URI or deleted instance
24
+ this.setDelay(120000);
25
+ }
26
+ else if (message.includes('ECONNREFUSED')) {
27
+ // Could be fail2ban or similar
28
+ this.setDelay(120000);
29
+ }
30
+ else if (message.includes('Unable to do postgres query on ended pool') ||
31
+ message.includes('Postgres unexpectedly closed connection')) {
32
+ // Connection timed out - ignore / immediately retry
33
+ // We don't explicitly set the delay to 0, since there could have been another error that
34
+ // we need to respect.
35
+ }
36
+ else {
37
+ this.setDelay(30000);
38
+ }
39
+ }
40
+ setDelay(delay) {
41
+ this.nextAllowed = Math.max(this.nextAllowed, Date.now() + delay);
42
+ }
43
+ }
44
+ //# sourceMappingURL=MongoErrorRateLimiter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MongoErrorRateLimiter.js","sourceRoot":"","sources":["../../src/replication/MongoErrorRateLimiter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAG7C,MAAM,OAAO,qBAAqB;IAAlC;QACE,gBAAW,GAAW,IAAI,CAAC,GAAG,EAAE,CAAC;IAwCnC,CAAC;IAtCC,KAAK,CAAC,gBAAgB,CAAC,OAA0D;QAC/E,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;QACzD,yDAAyD;QACzD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QACnB,MAAM,UAAU,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;IAClE,CAAC;IAED,OAAO;QACL,OAAO,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,WAAW,CAAC;IACxC,CAAC;IAED,WAAW,CAAC,CAAM;QAChB,6CAA6C;QAC7C,MAAM,OAAO,GAAI,CAAC,CAAC,OAAkB,IAAI,EAAE,CAAC;QAC5C,IAAI,OAAO,CAAC,QAAQ,CAAC,gCAAgC,CAAC,EAAE;YACtD,2DAA2D;YAC3D,IAAI,CAAC,QAAQ,CAAC,MAAO,CAAC,CAAC;SACxB;aAAM,IAAI,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE;YACxC,uDAAuD;YACvD,IAAI,CAAC,QAAQ,CAAC,MAAO,CAAC,CAAC;SACxB;aAAM,IAAI,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE;YAC3C,+BAA+B;YAC/B,IAAI,CAAC,QAAQ,CAAC,MAAO,CAAC,CAAC;SACxB;aAAM,IACL,OAAO,CAAC,QAAQ,CAAC,2CAA2C,CAAC;YAC7D,OAAO,CAAC,QAAQ,CAAC,yCAAyC,CAAC,EAC3D;YACA,oDAAoD;YACpD,yFAAyF;YACzF,sBAAsB;SACvB;aAAM;YACL,IAAI,CAAC,QAAQ,CAAC,KAAM,CAAC,CAAC;SACvB;IACH,CAAC;IAEO,QAAQ,CAAC,KAAa;QAC5B,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,CAAC;IACpE,CAAC;CACF"}
@@ -0,0 +1,14 @@
1
+ import * as mongo from 'mongodb';
2
+ import { NormalizedMongoConnectionConfig } from '../types/types.js';
3
+ export declare class MongoManager {
4
+ options: NormalizedMongoConnectionConfig;
5
+ /**
6
+ * Do not use this for any transactions.
7
+ */
8
+ readonly client: mongo.MongoClient;
9
+ readonly db: mongo.Db;
10
+ constructor(options: NormalizedMongoConnectionConfig);
11
+ get connectionTag(): string;
12
+ end(): Promise<void>;
13
+ destroy(): Promise<void>;
14
+ }
@@ -0,0 +1,36 @@
1
+ import * as mongo from 'mongodb';
2
+ export class MongoManager {
3
+ constructor(options) {
4
+ this.options = options;
5
+ // The pool is lazy - no connections are opened until a query is performed.
6
+ this.client = new mongo.MongoClient(options.uri, {
7
+ auth: {
8
+ username: options.username,
9
+ password: options.password
10
+ },
11
+ // Time for connection to timeout
12
+ connectTimeoutMS: 5000,
13
+ // Time for individual requests to timeout
14
+ socketTimeoutMS: 60000,
15
+ // How long to wait for new primary selection
16
+ serverSelectionTimeoutMS: 30000,
17
+ // Avoid too many connections:
18
+ // 1. It can overwhelm the source database.
19
+ // 2. Processing too many queries in parallel can cause the process to run out of memory.
20
+ maxPoolSize: 8,
21
+ maxConnecting: 3,
22
+ maxIdleTimeMS: 60000
23
+ });
24
+ this.db = this.client.db(options.database, {});
25
+ }
26
+ get connectionTag() {
27
+ return this.options.tag;
28
+ }
29
+ async end() {
30
+ await this.client.close();
31
+ }
32
+ async destroy() {
33
+ // TODO: Implement?
34
+ }
35
+ }
36
+ //# sourceMappingURL=MongoManager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MongoManager.js","sourceRoot":"","sources":["../../src/replication/MongoManager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,SAAS,CAAC;AAGjC,MAAM,OAAO,YAAY;IAOvB,YAAmB,OAAwC;QAAxC,YAAO,GAAP,OAAO,CAAiC;QACzD,2EAA2E;QAC3E,IAAI,CAAC,MAAM,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,OAAO,CAAC,GAAG,EAAE;YAC/C,IAAI,EAAE;gBACJ,QAAQ,EAAE,OAAO,CAAC,QAAQ;gBAC1B,QAAQ,EAAE,OAAO,CAAC,QAAQ;aAC3B;YACD,iCAAiC;YACjC,gBAAgB,EAAE,IAAK;YACvB,0CAA0C;YAC1C,eAAe,EAAE,KAAM;YACvB,6CAA6C;YAC7C,wBAAwB,EAAE,KAAM;YAEhC,8BAA8B;YAC9B,2CAA2C;YAC3C,yFAAyF;YACzF,WAAW,EAAE,CAAC;YAEd,aAAa,EAAE,CAAC;YAChB,aAAa,EAAE,KAAM;SACtB,CAAC,CAAC;QACH,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IACjD,CAAC;IAED,IAAW,aAAa;QACtB,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC;IAC1B,CAAC;IAED,KAAK,CAAC,GAAG;QACP,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;IAC5B,CAAC;IAED,KAAK,CAAC,OAAO;QACX,mBAAmB;IACrB,CAAC;CACF"}
@@ -0,0 +1,9 @@
1
+ import { storage } from '@powersync/service-core';
2
+ import { SqliteRow, SqliteValue } from '@powersync/service-sync-rules';
3
+ import * as mongo from 'mongodb';
4
+ export declare function getMongoRelation(source: mongo.ChangeStreamNameSpace): storage.SourceEntityDescriptor;
5
+ export declare function getMongoLsn(timestamp: mongo.Timestamp): string;
6
+ export declare function mongoLsnToTimestamp(lsn: string | null): mongo.BSON.Timestamp | null;
7
+ export declare function constructAfterRecord(document: mongo.Document): SqliteRow;
8
+ export declare function toMongoSyncRulesValue(data: any): SqliteValue;
9
+ export declare function createCheckpoint(client: mongo.MongoClient, db: mongo.Db): Promise<string>;
@@ -0,0 +1,174 @@
1
+ import * as mongo from 'mongodb';
2
+ import { JSONBig, JsonContainer } from '@powersync/service-jsonbig';
3
+ export function getMongoRelation(source) {
4
+ return {
5
+ name: source.coll,
6
+ schema: source.db,
7
+ objectId: source.coll,
8
+ replicationColumns: [{ name: '_id' }]
9
+ };
10
+ }
11
+ export function getMongoLsn(timestamp) {
12
+ const a = timestamp.high.toString(16).padStart(8, '0');
13
+ const b = timestamp.low.toString(16).padStart(8, '0');
14
+ return a + b;
15
+ }
16
+ export function mongoLsnToTimestamp(lsn) {
17
+ if (lsn == null) {
18
+ return null;
19
+ }
20
+ const a = parseInt(lsn.substring(0, 8), 16);
21
+ const b = parseInt(lsn.substring(8, 16), 16);
22
+ return mongo.Timestamp.fromBits(b, a);
23
+ }
24
+ export function constructAfterRecord(document) {
25
+ let record = {};
26
+ for (let key of Object.keys(document)) {
27
+ record[key] = toMongoSyncRulesValue(document[key]);
28
+ }
29
+ return record;
30
+ }
31
+ export function toMongoSyncRulesValue(data) {
32
+ const autoBigNum = true;
33
+ if (data == null) {
34
+ // null or undefined
35
+ return data;
36
+ }
37
+ else if (typeof data == 'string') {
38
+ return data;
39
+ }
40
+ else if (typeof data == 'number') {
41
+ if (Number.isInteger(data) && autoBigNum) {
42
+ return BigInt(data);
43
+ }
44
+ else {
45
+ return data;
46
+ }
47
+ }
48
+ else if (typeof data == 'bigint') {
49
+ return data;
50
+ }
51
+ else if (typeof data == 'boolean') {
52
+ return data ? 1n : 0n;
53
+ }
54
+ else if (data instanceof mongo.ObjectId) {
55
+ return data.toHexString();
56
+ }
57
+ else if (data instanceof mongo.UUID) {
58
+ return data.toHexString();
59
+ }
60
+ else if (data instanceof Date) {
61
+ return data.toISOString().replace('T', ' ');
62
+ }
63
+ else if (data instanceof mongo.Binary) {
64
+ return new Uint8Array(data.buffer);
65
+ }
66
+ else if (data instanceof mongo.Long) {
67
+ return data.toBigInt();
68
+ }
69
+ else if (Array.isArray(data)) {
70
+ // We may be able to avoid some parse + stringify cycles here for JsonSqliteContainer.
71
+ return JSONBig.stringify(data.map((element) => filterJsonData(element)));
72
+ }
73
+ else if (data instanceof Uint8Array) {
74
+ return data;
75
+ }
76
+ else if (data instanceof JsonContainer) {
77
+ return data.toString();
78
+ }
79
+ else if (typeof data == 'object') {
80
+ let record = {};
81
+ for (let key of Object.keys(data)) {
82
+ record[key] = filterJsonData(data[key]);
83
+ }
84
+ return JSONBig.stringify(record);
85
+ }
86
+ else {
87
+ return null;
88
+ }
89
+ }
90
+ const DEPTH_LIMIT = 20;
91
+ function filterJsonData(data, depth = 0) {
92
+ const autoBigNum = true;
93
+ if (depth > DEPTH_LIMIT) {
94
+ // This is primarily to prevent infinite recursion
95
+ throw new Error(`json nested object depth exceeds the limit of ${DEPTH_LIMIT}`);
96
+ }
97
+ if (data == null) {
98
+ return data; // null or undefined
99
+ }
100
+ else if (typeof data == 'string') {
101
+ return data;
102
+ }
103
+ else if (typeof data == 'number') {
104
+ if (autoBigNum && Number.isInteger(data)) {
105
+ return BigInt(data);
106
+ }
107
+ else {
108
+ return data;
109
+ }
110
+ }
111
+ else if (typeof data == 'boolean') {
112
+ return data ? 1n : 0n;
113
+ }
114
+ else if (typeof data == 'bigint') {
115
+ return data;
116
+ }
117
+ else if (data instanceof Date) {
118
+ return data.toISOString().replace('T', ' ');
119
+ }
120
+ else if (data instanceof mongo.ObjectId) {
121
+ return data.toHexString();
122
+ }
123
+ else if (data instanceof mongo.UUID) {
124
+ return data.toHexString();
125
+ }
126
+ else if (data instanceof mongo.Binary) {
127
+ return undefined;
128
+ }
129
+ else if (data instanceof mongo.Long) {
130
+ return data.toBigInt();
131
+ }
132
+ else if (Array.isArray(data)) {
133
+ return data.map((element) => filterJsonData(element, depth + 1));
134
+ }
135
+ else if (ArrayBuffer.isView(data)) {
136
+ return undefined;
137
+ }
138
+ else if (data instanceof JsonContainer) {
139
+ // Can be stringified directly when using our JSONBig implementation
140
+ return data;
141
+ }
142
+ else if (typeof data == 'object') {
143
+ let record = {};
144
+ for (let key of Object.keys(data)) {
145
+ record[key] = filterJsonData(data[key], depth + 1);
146
+ }
147
+ return record;
148
+ }
149
+ else {
150
+ return undefined;
151
+ }
152
+ }
153
+ export async function createCheckpoint(client, db) {
154
+ const session = client.startSession();
155
+ try {
156
+ const result = await db.collection('_powersync_checkpoints').findOneAndUpdate({
157
+ _id: 'checkpoint'
158
+ }, {
159
+ $inc: { i: 1 }
160
+ }, {
161
+ upsert: true,
162
+ returnDocument: 'after',
163
+ session
164
+ });
165
+ const time = session.operationTime;
166
+ // console.log('marked checkpoint at', time, getMongoLsn(time));
167
+ // TODO: Use the above when we support custom write checkpoints
168
+ return getMongoLsn(time);
169
+ }
170
+ finally {
171
+ await session.endSession();
172
+ }
173
+ }
174
+ //# sourceMappingURL=MongoRelation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MongoRelation.js","sourceRoot":"","sources":["../../src/replication/MongoRelation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ export * from './MongoRelation.js';
2
+ export * from './ChangeStream.js';
3
+ export * from './ChangeStreamReplicator.js';
4
+ export * from './ChangeStreamReplicationJob.js';
@@ -0,0 +1,5 @@
1
+ export * from './MongoRelation.js';
2
+ export * from './ChangeStream.js';
3
+ export * from './ChangeStreamReplicator.js';
4
+ export * from './ChangeStreamReplicationJob.js';
5
+ //# sourceMappingURL=replication-index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replication-index.js","sourceRoot":"","sources":["../../src/replication/replication-index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,iCAAiC,CAAC"}
@@ -0,0 +1,51 @@
1
+ import * as t from 'ts-codec';
2
+ export declare const MONGO_CONNECTION_TYPE: "mongodb";
3
+ export interface NormalizedMongoConnectionConfig {
4
+ id: string;
5
+ tag: string;
6
+ uri: string;
7
+ database: string;
8
+ username?: string;
9
+ password?: string;
10
+ }
11
+ export declare const MongoConnectionConfig: t.Intersection<t.Codec<{
12
+ type: string;
13
+ id?: string | undefined;
14
+ tag?: string | undefined;
15
+ debug_api?: boolean | undefined;
16
+ }, {
17
+ type: string;
18
+ id?: string | undefined;
19
+ tag?: string | undefined;
20
+ debug_api?: boolean | undefined;
21
+ }, string, t.CodecProps>, t.ObjectCodec<{
22
+ type: t.LiteralCodec<"mongodb">;
23
+ /** Unique identifier for the connection - optional when a single connection is present. */
24
+ id: t.OptionalCodec<t.Codec<string, string, string, t.CodecProps>>;
25
+ /** Tag used as reference in sync rules. Defaults to "default". Does not have to be unique. */
26
+ tag: t.OptionalCodec<t.Codec<string, string, string, t.CodecProps>>;
27
+ uri: t.IdentityCodec<t.CodecType.String>;
28
+ username: t.OptionalCodec<t.Codec<string, string, string, t.CodecProps>>;
29
+ password: t.OptionalCodec<t.Codec<string, string, string, t.CodecProps>>;
30
+ database: t.OptionalCodec<t.Codec<string, string, string, t.CodecProps>>;
31
+ }>>;
32
+ /**
33
+ * Config input specified when starting services
34
+ */
35
+ export type MongoConnectionConfig = t.Decoded<typeof MongoConnectionConfig>;
36
+ /**
37
+ * Resolved version of {@link MongoConnectionConfig}
38
+ */
39
+ export type ResolvedConnectionConfig = MongoConnectionConfig & NormalizedMongoConnectionConfig;
40
+ /**
41
+ * Validate and normalize connection options.
42
+ *
43
+ * Returns destructured options.
44
+ */
45
+ export declare function normalizeConnectionConfig(options: MongoConnectionConfig): NormalizedMongoConnectionConfig;
46
+ /**
47
+ * Construct a mongodb URI, without username, password or ssl options.
48
+ *
49
+ * Only contains hostname, port, database.
50
+ */
51
+ export declare function baseUri(options: NormalizedMongoConnectionConfig): string;
@@ -0,0 +1,37 @@
1
+ import { normalizeMongoConfig } from '@powersync/service-core';
2
+ import * as service_types from '@powersync/service-types';
3
+ import * as t from 'ts-codec';
4
+ export const MONGO_CONNECTION_TYPE = 'mongodb';
5
+ export const MongoConnectionConfig = service_types.configFile.dataSourceConfig.and(t.object({
6
+ type: t.literal(MONGO_CONNECTION_TYPE),
7
+ /** Unique identifier for the connection - optional when a single connection is present. */
8
+ id: t.string.optional(),
9
+ /** Tag used as reference in sync rules. Defaults to "default". Does not have to be unique. */
10
+ tag: t.string.optional(),
11
+ uri: t.string,
12
+ username: t.string.optional(),
13
+ password: t.string.optional(),
14
+ database: t.string.optional()
15
+ }));
16
+ /**
17
+ * Validate and normalize connection options.
18
+ *
19
+ * Returns destructured options.
20
+ */
21
+ export function normalizeConnectionConfig(options) {
22
+ const base = normalizeMongoConfig(options);
23
+ return {
24
+ id: options.id ?? 'default',
25
+ tag: options.tag ?? 'default',
26
+ ...base
27
+ };
28
+ }
29
+ /**
30
+ * Construct a mongodb URI, without username, password or ssl options.
31
+ *
32
+ * Only contains hostname, port, database.
33
+ */
34
+ export function baseUri(options) {
35
+ return options.uri;
36
+ }
37
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,KAAK,aAAa,MAAM,0BAA0B,CAAC;AAC1D,OAAO,KAAK,CAAC,MAAM,UAAU,CAAC;AAE9B,MAAM,CAAC,MAAM,qBAAqB,GAAG,SAAkB,CAAC;AAaxD,MAAM,CAAC,MAAM,qBAAqB,GAAG,aAAa,CAAC,UAAU,CAAC,gBAAgB,CAAC,GAAG,CAChF,CAAC,CAAC,MAAM,CAAC;IACP,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,qBAAqB,CAAC;IACtC,2FAA2F;IAC3F,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE;IACvB,8FAA8F;IAC9F,GAAG,EAAE,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE;IACxB,GAAG,EAAE,CAAC,CAAC,MAAM;IACb,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE;IAC7B,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE;IAC7B,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE;CAC9B,CAAC,CACH,CAAC;AAYF;;;;GAIG;AACH,MAAM,UAAU,yBAAyB,CAAC,OAA8B;IACtE,MAAM,IAAI,GAAG,oBAAoB,CAAC,OAAO,CAAC,CAAC;IAE3C,OAAO;QACL,EAAE,EAAE,OAAO,CAAC,EAAE,IAAI,SAAS;QAC3B,GAAG,EAAE,OAAO,CAAC,GAAG,IAAI,SAAS;QAE7B,GAAG,IAAI;KACR,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,OAAO,CAAC,OAAwC;IAC9D,OAAO,OAAO,CAAC,GAAG,CAAC;AACrB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@powersync/service-module-mongodb",
3
+ "repository": "https://github.com/powersync-ja/powersync-service",
4
+ "types": "dist/index.d.ts",
5
+ "version": "0.0.0-dev-20241001150444",
6
+ "main": "dist/index.js",
7
+ "license": "FSL-1.1-Apache-2.0",
8
+ "type": "module",
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "import": "./dist/index.js",
15
+ "require": "./dist/index.js",
16
+ "default": "./dist/index.js"
17
+ },
18
+ "./types": {
19
+ "import": "./dist/types/types.js",
20
+ "require": "./dist/types/types.js",
21
+ "default": "./dist/types/types.js"
22
+ }
23
+ },
24
+ "dependencies": {
25
+ "mongodb": "^6.7.0",
26
+ "ts-codec": "^1.2.2",
27
+ "uuid": "^9.0.1",
28
+ "uri-js": "^4.4.1",
29
+ "@powersync/lib-services-framework": "0.0.0-dev-20241001150444",
30
+ "@powersync/service-core": "0.0.0-dev-20241001150444",
31
+ "@powersync/service-jsonbig": "0.17.10",
32
+ "@powersync/service-sync-rules": "0.0.0-dev-20241001150444",
33
+ "@powersync/service-types": "0.0.0-dev-20241001150444"
34
+ },
35
+ "devDependencies": {
36
+ "@types/uuid": "^9.0.4",
37
+ "typescript": "^5.2.2",
38
+ "vitest": "^0.34.6",
39
+ "vite-tsconfig-paths": "^4.3.2"
40
+ },
41
+ "scripts": {
42
+ "build": "tsc -b",
43
+ "build:tests": "tsc -b test/tsconfig.json",
44
+ "clean": "rm -rf ./lib && tsc -b --clean",
45
+ "test": "vitest --no-threads"
46
+ }
47
+ }
@@ -0,0 +1,86 @@
1
+ import { api, ParseSyncRulesOptions } from '@powersync/service-core';
2
+ import * as mongo from 'mongodb';
3
+
4
+ import * as sync_rules from '@powersync/service-sync-rules';
5
+ import * as service_types from '@powersync/service-types';
6
+ import * as types from '../types/types.js';
7
+ import { MongoManager } from '../replication/MongoManager.js';
8
+ import { createCheckpoint, getMongoLsn } from '../replication/MongoRelation.js';
9
+
10
+ export class MongoRouteAPIAdapter implements api.RouteAPI {
11
+ protected client: mongo.MongoClient;
12
+ private db: mongo.Db;
13
+
14
+ connectionTag: string;
15
+ defaultSchema: string;
16
+
17
+ constructor(protected config: types.ResolvedConnectionConfig) {
18
+ const manager = new MongoManager(config);
19
+ this.client = manager.client;
20
+ this.db = manager.db;
21
+ this.defaultSchema = manager.db.databaseName;
22
+ this.connectionTag = config.tag ?? sync_rules.DEFAULT_TAG;
23
+ }
24
+
25
+ getParseSyncRulesOptions(): ParseSyncRulesOptions {
26
+ return {
27
+ defaultSchema: this.defaultSchema
28
+ };
29
+ }
30
+
31
+ async shutdown(): Promise<void> {
32
+ await this.client.close();
33
+ }
34
+
35
+ async getSourceConfig(): Promise<service_types.configFile.DataSourceConfig> {
36
+ return this.config;
37
+ }
38
+
39
+ async getConnectionStatus(): Promise<service_types.ConnectionStatusV2> {
40
+ // TODO: Implement
41
+ const base = {
42
+ id: this.config.id,
43
+ uri: types.baseUri(this.config)
44
+ };
45
+ return {
46
+ ...base,
47
+ connected: true,
48
+ errors: []
49
+ };
50
+ }
51
+
52
+ async executeQuery(query: string, params: any[]): Promise<service_types.internal_routes.ExecuteSqlResponse> {
53
+ return service_types.internal_routes.ExecuteSqlResponse.encode({
54
+ results: {
55
+ columns: [],
56
+ rows: []
57
+ },
58
+ success: false,
59
+ error: 'SQL querying is not supported for MongoDB'
60
+ });
61
+ }
62
+
63
+ async getDebugTablesInfo(
64
+ tablePatterns: sync_rules.TablePattern[],
65
+ sqlSyncRules: sync_rules.SqlSyncRules
66
+ ): Promise<api.PatternResult[]> {
67
+ // TODO: Implement
68
+ return [];
69
+ }
70
+
71
+ async getReplicationLag(syncRulesId: string): Promise<number> {
72
+ // TODO: Implement
73
+
74
+ return 0;
75
+ }
76
+
77
+ async getReplicationHead(): Promise<string> {
78
+ return createCheckpoint(this.client, this.db);
79
+ }
80
+
81
+ async getConnectionSchema(): Promise<service_types.DatabaseSchema[]> {
82
+ // TODO: Implement
83
+
84
+ return [];
85
+ }
86
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { MongoModule } from './module/MongoModule.js';
2
+
3
+ export const module = new MongoModule();
4
+
5
+ export default module;
@@ -0,0 +1,52 @@
1
+ import { api, ConfigurationFileSyncRulesProvider, replication, system, TearDownOptions } from '@powersync/service-core';
2
+ import { MongoRouteAPIAdapter } from '../api/MongoRouteAPIAdapter.js';
3
+ import { ConnectionManagerFactory } from '../replication/ConnectionManagerFactory.js';
4
+ import { MongoErrorRateLimiter } from '../replication/MongoErrorRateLimiter.js';
5
+ import { ChangeStreamReplicator } from '../replication/ChangeStreamReplicator.js';
6
+ import * as types from '../types/types.js';
7
+
8
+ export class MongoModule extends replication.ReplicationModule<types.MongoConnectionConfig> {
9
+ constructor() {
10
+ super({
11
+ name: 'MongoDB',
12
+ type: types.MONGO_CONNECTION_TYPE,
13
+ configSchema: types.MongoConnectionConfig
14
+ });
15
+ }
16
+
17
+ async initialize(context: system.ServiceContextContainer): Promise<void> {
18
+ await super.initialize(context);
19
+ }
20
+
21
+ protected createRouteAPIAdapter(): api.RouteAPI {
22
+ return new MongoRouteAPIAdapter(this.resolveConfig(this.decodedConfig!));
23
+ }
24
+
25
+ protected createReplicator(context: system.ServiceContext): replication.AbstractReplicator {
26
+ const normalisedConfig = this.resolveConfig(this.decodedConfig!);
27
+ const syncRuleProvider = new ConfigurationFileSyncRulesProvider(context.configuration.sync_rules);
28
+ const connectionFactory = new ConnectionManagerFactory(normalisedConfig);
29
+
30
+ return new ChangeStreamReplicator({
31
+ id: this.getDefaultId(normalisedConfig.database ?? ''),
32
+ syncRuleProvider: syncRuleProvider,
33
+ storageEngine: context.storageEngine,
34
+ connectionFactory: connectionFactory,
35
+ rateLimiter: new MongoErrorRateLimiter()
36
+ });
37
+ }
38
+
39
+ /**
40
+ * Combines base config with normalized connection settings
41
+ */
42
+ private resolveConfig(config: types.MongoConnectionConfig): types.ResolvedConnectionConfig {
43
+ return {
44
+ ...config,
45
+ ...types.normalizeConnectionConfig(config)
46
+ };
47
+ }
48
+
49
+ async teardown(options: TearDownOptions): Promise<void> {
50
+ // TODO: Implement?
51
+ }
52
+ }