@semics-tech/mongolite 1.2.0 → 1.4.0

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 (99) hide show
  1. package/README.md +91 -20
  2. package/dist/bin/mongolite-debug.d.ts +6 -1
  3. package/dist/bin/mongolite-debug.js +6 -113
  4. package/dist/bin/mongolite-debug.js.map +1 -1
  5. package/dist/bin/mongolite.d.ts +13 -0
  6. package/dist/bin/mongolite.js +172 -0
  7. package/dist/bin/mongolite.js.map +1 -0
  8. package/dist/cjs/bin/mongolite-debug.js +6 -149
  9. package/dist/cjs/bin/mongolite.js +207 -0
  10. package/dist/cjs/cli/discover.js +156 -0
  11. package/dist/cjs/cli/explorer.js +929 -0
  12. package/dist/cjs/cli/filter-builder.js +276 -0
  13. package/dist/cjs/cli/format.js +161 -0
  14. package/dist/cjs/cli/schema.js +211 -0
  15. package/dist/cjs/cli/ui.js +431 -0
  16. package/dist/cjs/collection.js +8 -1
  17. package/dist/cjs/cursors/findCursor.js +20 -8
  18. package/dist/cjs/index.js +108 -1
  19. package/dist/cjs/mongo-client.js +18 -1
  20. package/dist/cjs/server.js +164 -0
  21. package/dist/cjs/sync/commands.js +126 -0
  22. package/dist/cjs/sync/diff.js +174 -0
  23. package/dist/cjs/sync/httpSink.js +159 -0
  24. package/dist/cjs/sync/index.js +40 -0
  25. package/dist/cjs/sync/mongoExecutor.js +306 -0
  26. package/dist/cjs/sync/mongoSink.js +161 -0
  27. package/dist/cjs/sync/outbox.js +358 -0
  28. package/dist/cjs/sync/protocol.js +177 -0
  29. package/dist/cjs/sync/replicator.js +747 -0
  30. package/dist/cjs/sync/shadow.js +178 -0
  31. package/dist/cjs/sync/types.js +2 -0
  32. package/dist/cli/discover.d.ts +40 -0
  33. package/dist/cli/discover.js +146 -0
  34. package/dist/cli/discover.js.map +1 -0
  35. package/dist/cli/explorer.d.ts +59 -0
  36. package/dist/cli/explorer.js +923 -0
  37. package/dist/cli/explorer.js.map +1 -0
  38. package/dist/cli/filter-builder.d.ts +52 -0
  39. package/dist/cli/filter-builder.js +268 -0
  40. package/dist/cli/filter-builder.js.map +1 -0
  41. package/dist/cli/format.d.ts +29 -0
  42. package/dist/cli/format.js +153 -0
  43. package/dist/cli/format.js.map +1 -0
  44. package/dist/cli/schema.d.ts +58 -0
  45. package/dist/cli/schema.js +205 -0
  46. package/dist/cli/schema.js.map +1 -0
  47. package/dist/cli/ui.d.ts +71 -0
  48. package/dist/cli/ui.js +379 -0
  49. package/dist/cli/ui.js.map +1 -0
  50. package/dist/cloudflare.d.ts +1 -1
  51. package/dist/cloudflare.js.map +1 -1
  52. package/dist/collection.js +8 -1
  53. package/dist/collection.js.map +1 -1
  54. package/dist/cursors/findCursor.d.ts +12 -0
  55. package/dist/cursors/findCursor.js +20 -8
  56. package/dist/cursors/findCursor.js.map +1 -1
  57. package/dist/index.d.ts +68 -2
  58. package/dist/index.js +96 -1
  59. package/dist/index.js.map +1 -1
  60. package/dist/mongo-client.d.ts +13 -0
  61. package/dist/mongo-client.js +18 -1
  62. package/dist/mongo-client.js.map +1 -1
  63. package/dist/server.d.ts +97 -0
  64. package/dist/server.js +158 -0
  65. package/dist/server.js.map +1 -0
  66. package/dist/sync/commands.d.ts +49 -0
  67. package/dist/sync/commands.js +118 -0
  68. package/dist/sync/commands.js.map +1 -0
  69. package/dist/sync/diff.d.ts +55 -0
  70. package/dist/sync/diff.js +169 -0
  71. package/dist/sync/diff.js.map +1 -0
  72. package/dist/sync/httpSink.d.ts +93 -0
  73. package/dist/sync/httpSink.js +155 -0
  74. package/dist/sync/httpSink.js.map +1 -0
  75. package/dist/sync/index.d.ts +17 -0
  76. package/dist/sync/index.js +10 -0
  77. package/dist/sync/index.js.map +1 -0
  78. package/dist/sync/mongoExecutor.d.ts +37 -0
  79. package/dist/sync/mongoExecutor.js +302 -0
  80. package/dist/sync/mongoExecutor.js.map +1 -0
  81. package/dist/sync/mongoSink.d.ts +109 -0
  82. package/dist/sync/mongoSink.js +123 -0
  83. package/dist/sync/mongoSink.js.map +1 -0
  84. package/dist/sync/outbox.d.ts +120 -0
  85. package/dist/sync/outbox.js +355 -0
  86. package/dist/sync/outbox.js.map +1 -0
  87. package/dist/sync/protocol.d.ts +93 -0
  88. package/dist/sync/protocol.js +168 -0
  89. package/dist/sync/protocol.js.map +1 -0
  90. package/dist/sync/replicator.d.ts +177 -0
  91. package/dist/sync/replicator.js +744 -0
  92. package/dist/sync/replicator.js.map +1 -0
  93. package/dist/sync/shadow.d.ts +54 -0
  94. package/dist/sync/shadow.js +173 -0
  95. package/dist/sync/shadow.js.map +1 -0
  96. package/dist/sync/types.d.ts +388 -0
  97. package/dist/sync/types.js +2 -0
  98. package/dist/sync/types.js.map +1 -0
  99. package/package.json +16 -1
package/dist/index.js CHANGED
@@ -3,9 +3,11 @@ import { MongoLite as MongoLiteBase } from './mongo-client.js';
3
3
  export { MongoLiteCollection } from './collection.js';
4
4
  export * from './types.js';
5
5
  export { ChangeStream } from './changeStream.js';
6
- export { CloudflareDurableObjectAdapter, } from './adapters/cloudflare.js';
6
+ export { CloudflareDurableObjectAdapter } from './adapters/cloudflare.js';
7
7
  export { BrowserSqliteAdapter } from './adapters/browser.js';
8
8
  export { NodeSqliteAdapter } from './adapters/node-sqlite.js';
9
+ import { HttpUpstreamSink, MongoUpstreamSink } from './sync/index.js';
10
+ export { SyncReplicator, SyncOutbox, SyncShadow, MongoUpstreamSink, HttpUpstreamSink, SyncRequestRejected, SYNC_PROTOCOL_VERSION, buildWriteCommand, computeDiff, applyDiff, isEmptyDiff, projectToLocalShape, } from './sync/index.js';
9
11
  /**
10
12
  * MongoLite class is the main entry point for interacting with the SQLite-backed database.
11
13
  *
@@ -39,5 +41,98 @@ export class MongoLite extends MongoLiteBase {
39
41
  super(new NodeSqliteAdapter(dbPathOrOptions), options);
40
42
  }
41
43
  }
44
+ /**
45
+ * Replicates every insert, update and delete in this database to an upstream
46
+ * MongoDB deployment.
47
+ *
48
+ * Changes are captured by SQLite triggers into a durable outbox inside this
49
+ * database, in the same transaction as the write itself, then streamed
50
+ * upstream in batches. Replication survives process restarts and upstream
51
+ * outages: the outbox holds the backlog and a checkpoint records exactly how
52
+ * far it got.
53
+ *
54
+ * Requires the optional `mongodb` peer dependency.
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * const sync = client.syncToMongo({
59
+ * connectionString: 'mongodb+srv://user:pass@cluster.example.com/app',
60
+ * collections: ['users', 'orders'],
61
+ * });
62
+ *
63
+ * sync.on('error', (err) => console.error('replication degraded:', err));
64
+ * await sync.start();
65
+ *
66
+ * // …later, on shutdown
67
+ * await sync.stop({ flush: true });
68
+ * ```
69
+ */
70
+ syncToMongo(options) {
71
+ const { connectionString, database, writeConcern, driverOptions, driver, authSource, authMechanism, authMechanismProperties, username, password, tls, tlsCAFile, tlsCertificateKeyFile, tlsCertificateKeyFilePassword, tlsCRLFile, tlsAllowInvalidHostnames, tlsAllowInvalidCertificates, ...syncOptions } = options;
72
+ const sink = new MongoUpstreamSink({
73
+ connectionString,
74
+ database,
75
+ writeConcern,
76
+ driverOptions,
77
+ driver,
78
+ authSource,
79
+ authMechanism,
80
+ authMechanismProperties,
81
+ username,
82
+ password,
83
+ tls,
84
+ tlsCAFile,
85
+ tlsCertificateKeyFile,
86
+ tlsCertificateKeyFilePassword,
87
+ tlsCRLFile,
88
+ tlsAllowInvalidHostnames,
89
+ tlsAllowInvalidCertificates,
90
+ idMapping: syncOptions.idMapping,
91
+ // The sink builds the conditional writes, so it needs the same answer the
92
+ // replicator uses when deciding whether to track shadows at all.
93
+ versioning: syncOptions.versioning,
94
+ });
95
+ return this.createSync(sink, syncOptions);
96
+ }
97
+ /**
98
+ * Replicates changes to a remote HTTP API, which applies them to MongoDB on this
99
+ * client's behalf. For deployments where the local instance cannot reach the database
100
+ * directly.
101
+ *
102
+ * The wire format carries MongoDB-shaped commands along with the version predicates,
103
+ * so replication behaves the same as over a direct connection — the upstream stays the
104
+ * source of truth and concurrent writers are still detected. Mount
105
+ * `@semics-tech/mongolite/server` on the API to receive the messages.
106
+ *
107
+ * @example
108
+ * ```typescript
109
+ * const sync = client.syncToHttp({
110
+ * baseUrl: 'https://api.example.com',
111
+ * database: 'app',
112
+ * headers: { 'luna-environment': 'prod' },
113
+ * getAuthHeaders: async () => ({ Authorization: `Bearer ${await getAccessToken()}` }),
114
+ * });
115
+ *
116
+ * await sync.start();
117
+ * ```
118
+ */
119
+ syncToHttp(options) {
120
+ const { baseUrl, database, pathPrefix, getAuthHeaders, headers, cookies, fetch: fetchImpl, timeoutMs, includeCommands, ...syncOptions } = options;
121
+ const sink = new HttpUpstreamSink({
122
+ baseUrl,
123
+ database,
124
+ pathPrefix,
125
+ getAuthHeaders,
126
+ headers,
127
+ cookies,
128
+ fetch: fetchImpl,
129
+ timeoutMs,
130
+ includeCommands,
131
+ idMapping: syncOptions.idMapping,
132
+ versioning: syncOptions.versioning,
133
+ replicator: syncOptions.name,
134
+ });
135
+ return this.createSync(sink, syncOptions);
136
+ }
42
137
  }
43
138
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAE9D,OAAO,EAAE,SAAS,IAAI,aAAa,EAAwB,MAAM,mBAAmB,CAAC;AAErF,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAOjD,OAAO,EACL,8BAA8B,GAC/B,MAAM,0BAA0B,CAAC;AAMlC,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAE7D,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAI9D;;;;;;;;;;GAUG;AACH,MAAM,OAAO,SAAU,SAAQ,aAAa;IAC1C;;;;OAIG;IACH,YACE,eAAmE,EACnE,UAAgC,EAAE;QAElC,IACE,eAAe;YACf,OAAO,eAAe,KAAK,QAAQ;YACnC,OAAQ,eAAoC,CAAC,OAAO,KAAK,UAAU;YACnE,OAAQ,eAAoC,CAAC,GAAG,KAAK,UAAU;YAC/D,OAAQ,eAAoC,CAAC,GAAG,KAAK,UAAU;YAC/D,OAAQ,eAAoC,CAAC,GAAG,KAAK,UAAU;YAC/D,OAAQ,eAAoC,CAAC,IAAI,KAAK,UAAU;YAChE,OAAQ,eAAoC,CAAC,KAAK,KAAK,UAAU,EACjE,CAAC;YACD,uDAAuD;YACvD,KAAK,CAAC,eAAmC,EAAE,OAAO,CAAC,CAAC;QACtD,CAAC;aAAM,CAAC;YACN,KAAK,CAAC,IAAI,iBAAiB,CAAC,eAAkD,CAAC,EAAE,OAAO,CAAC,CAAC;QAC5F,CAAC;IACH,CAAC;CACF"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -11,6 +11,8 @@
11
11
  import type { IDatabaseAdapter } from './db.js';
12
12
  import { MongoLiteCollection } from './collection.js';
13
13
  import { DocumentWithId } from './types.js';
14
+ import { SyncReplicator } from './sync/replicator.js';
15
+ import type { SyncOptions, SyncSink } from './sync/types.js';
14
16
  export type { IDatabaseAdapter };
15
17
  export type MongoLiteBaseOptions = {
16
18
  verbose?: boolean;
@@ -48,6 +50,17 @@ export declare class MongoLite {
48
50
  listCollections(): {
49
51
  toArray: () => Promise<string[]>;
50
52
  };
53
+ /**
54
+ * Starts replicating this database's changes to an upstream {@link SyncSink}.
55
+ *
56
+ * Node.js users replicating to MongoDB should prefer the `syncToMongo`
57
+ * overload on the package-root `MongoLite`, which takes a connection string
58
+ * directly. This method is the backend-agnostic form — pass any sink to
59
+ * replicate over HTTP, into a queue, or to a test double.
60
+ *
61
+ * The returned replicator is **not** started; call `start()` on it.
62
+ */
63
+ createSync(sink: SyncSink, options?: SyncOptions): SyncReplicator;
51
64
  /**
52
65
  * Returns a typed collection handle.
53
66
  * @param name The collection (table) name.
@@ -1,4 +1,5 @@
1
1
  import { MongoLiteCollection } from './collection.js';
2
+ import { SyncReplicator } from './sync/replicator.js';
2
3
  /**
3
4
  * Core MongoLite client. Accepts any `IDatabaseAdapter` implementation.
4
5
  *
@@ -39,11 +40,27 @@ export class MongoLite {
39
40
  listCollections() {
40
41
  return {
41
42
  toArray: async () => {
42
- const result = await this.db.all(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`);
43
+ // `__mongolite*` tables are internal bookkeeping (change log, sync
44
+ // outbox and checkpoints), not user collections.
45
+ const result = await this.db.all(`SELECT name FROM sqlite_master
46
+ WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '__mongolite%'`);
43
47
  return result.map((row) => row.name);
44
48
  },
45
49
  };
46
50
  }
51
+ /**
52
+ * Starts replicating this database's changes to an upstream {@link SyncSink}.
53
+ *
54
+ * Node.js users replicating to MongoDB should prefer the `syncToMongo`
55
+ * overload on the package-root `MongoLite`, which takes a connection string
56
+ * directly. This method is the backend-agnostic form — pass any sink to
57
+ * replicate over HTTP, into a queue, or to a test double.
58
+ *
59
+ * The returned replicator is **not** started; call `start()` on it.
60
+ */
61
+ createSync(sink, options = {}) {
62
+ return new SyncReplicator(this.db, sink, { verbose: this.options.verbose, ...options });
63
+ }
47
64
  /**
48
65
  * Returns a typed collection handle.
49
66
  * @param name The collection (table) name.
@@ -1 +1 @@
1
- {"version":3,"file":"mongo-client.js","sourceRoot":"","sources":["../src/mongo-client.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAStD;;;;;;;GAOG;AACH,MAAM,OAAO,SAAS;IAIpB,YAAY,OAAyB,EAAE,UAAgC,EAAE;QACvE,IAAI,CAAC,EAAE,GAAG,OAAO,CAAC;QAClB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,OAAO;QACX,OAAO,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC;IAC3B,CAAC;IAED;;OAEG;IACH,IAAI,QAAQ;QACV,OAAO,IAAI,CAAC,EAAE,CAAC;IACjB,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,KAAK;QACT,OAAO,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC;IACzB,CAAC;IAED;;;OAGG;IACH,eAAe;QACb,OAAO;YACL,OAAO,EAAE,KAAK,IAAI,EAAE;gBAClB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,EAAE,CAAC,GAAG,CAC9B,gFAAgF,CACjF,CAAC;gBACF,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACvC,CAAC;SACF,CAAC;IACJ,CAAC;IAED;;;OAGG;IACH,UAAU,CAA4C,IAAY;QAChE,OAAO,IAAI,mBAAmB,CAAI,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IACjE,CAAC;CACF"}
1
+ {"version":3,"file":"mongo-client.js","sourceRoot":"","sources":["../src/mongo-client.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAEtD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAStD;;;;;;;GAOG;AACH,MAAM,OAAO,SAAS;IAIpB,YAAY,OAAyB,EAAE,UAAgC,EAAE;QACvE,IAAI,CAAC,EAAE,GAAG,OAAO,CAAC;QAClB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,OAAO;QACX,OAAO,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC;IAC3B,CAAC;IAED;;OAEG;IACH,IAAI,QAAQ;QACV,OAAO,IAAI,CAAC,EAAE,CAAC;IACjB,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,KAAK;QACT,OAAO,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC;IACzB,CAAC;IAED;;;OAGG;IACH,eAAe;QACb,OAAO;YACL,OAAO,EAAE,KAAK,IAAI,EAAE;gBAClB,mEAAmE;gBACnE,iDAAiD;gBACjD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,EAAE,CAAC,GAAG,CAC9B;4FACkF,CACnF,CAAC;gBACF,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACvC,CAAC;SACF,CAAC;IACJ,CAAC;IAED;;;;;;;;;OASG;IACH,UAAU,CAAC,IAAc,EAAE,UAAuB,EAAE;QAClD,OAAO,IAAI,cAAc,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC;IAC1F,CAAC;IAED;;;OAGG;IACH,UAAU,CAA4C,IAAY;QAChE,OAAO,IAAI,mBAAmB,CAAI,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IACjE,CAAC;CACF"}
@@ -0,0 +1,97 @@
1
+ import type { MongoClientLike, SyncIdMapping } from './sync/types.js';
2
+ export { SYNC_PROTOCOL_VERSION, SyncProtocolError } from './sync/protocol.js';
3
+ export type { SyncApplyRequest, SyncApplyResponse, SyncFetchRequest, SyncFetchResponse, WireOperation, } from './sync/protocol.js';
4
+ export type { SyncApplyConflict, SyncApplyFailure, SyncOperation } from './sync/types.js';
5
+ /** Context handed to {@link SyncReceiverOptions.verifyRequest}. */
6
+ export interface SyncRequestContext {
7
+ /** Replicator name the client reported. Advisory — clients choose it themselves. */
8
+ replicator: string;
9
+ /** Collections this batch touches, after validation. */
10
+ collections: string[];
11
+ /** Number of operations in the batch. */
12
+ operationCount: number;
13
+ }
14
+ export interface SyncReceiverOptions {
15
+ /** A connected MongoDB client. The receiver never opens or closes it. */
16
+ client: MongoClientLike;
17
+ /** Target database. Defaults to the client's default database. */
18
+ database?: string;
19
+ /**
20
+ * Collections a client is permitted to write.
21
+ *
22
+ * Strongly recommended. Without it, any client that can reach this endpoint can write
23
+ * to **every** collection in the database, including ones sync was never meant to
24
+ * touch.
25
+ */
26
+ allowedCollections?: string[];
27
+ /**
28
+ * Authorise a request before anything is applied. Throw, or return `false`, to reject
29
+ * it with 403.
30
+ *
31
+ * This is the receiver's own hook; it does not replace whatever authentication the
32
+ * surrounding API already performs.
33
+ */
34
+ verifyRequest?: (context: SyncRequestContext) => Promise<boolean | void> | boolean | void;
35
+ /**
36
+ * Execute the command the client sent rather than rebuilding it locally.
37
+ * Defaults to `false`, and should stay that way on any endpoint reachable by a
38
+ * client you do not fully control.
39
+ *
40
+ * A receiver that forwards client-supplied command documents to MongoDB will execute
41
+ * whatever it is given — an update with arbitrary operators, a filter matching far
42
+ * more than one document. Rebuilding from the operation's fields produces an identical
43
+ * command for well-behaved clients while making the abusive case unrepresentable.
44
+ */
45
+ trustClientCommands?: boolean;
46
+ /** Maximum operations accepted in one batch. Defaults to `1000`. */
47
+ maxOperations?: number;
48
+ /** See {@link SyncIdMapping}. Must match the client. Defaults to `'auto'`. */
49
+ idMapping?: SyncIdMapping;
50
+ /** Conditional writes against `_v`. Must match the client. Defaults to `true`. */
51
+ versioning?: boolean;
52
+ /** Write concern for applied batches. Defaults to `{ w: 'majority' }`. */
53
+ writeConcern?: Record<string, unknown>;
54
+ /**
55
+ * `ObjectId` implementation, normally the one from the `mongodb` driver you are
56
+ * already using so BSON instances match the connection. Defaults to `bson`'s.
57
+ */
58
+ ObjectId?: {
59
+ new (id: string): unknown;
60
+ isValid(id: string): boolean;
61
+ };
62
+ }
63
+ /** A response ready to be written to the wire. */
64
+ export interface SyncHttpResponse {
65
+ status: number;
66
+ /** Canonical Extended JSON. */
67
+ body: string;
68
+ headers: Record<string, string>;
69
+ }
70
+ export declare class SyncReceiver {
71
+ private readonly options;
72
+ private readonly db;
73
+ constructor(options: SyncReceiverOptions);
74
+ /**
75
+ * Applies a batch of operations.
76
+ *
77
+ * @param body The raw request body — a string, or an already-parsed object.
78
+ * @returns Status and Extended JSON body to send back.
79
+ */
80
+ apply(body: string | Record<string, unknown>): Promise<SyncHttpResponse>;
81
+ /** Reads documents back by `_id`, so a client can refresh its shadow after a conflict. */
82
+ fetch(body: string | Record<string, unknown>): Promise<SyncHttpResponse>;
83
+ /**
84
+ * Produces the command to execute for an operation.
85
+ *
86
+ * By default this **rebuilds** the command from the operation's own fields rather than
87
+ * using whatever the client sent. For a well-behaved client the two are identical;
88
+ * for a hostile one the difference is the whole security boundary.
89
+ */
90
+ private commandFor;
91
+ private assertCollectionsAllowed;
92
+ private authorise;
93
+ private ok;
94
+ private error;
95
+ }
96
+ /** Creates a receiver for sync messages posted by a MongoLite client. */
97
+ export declare function createSyncReceiver(options: SyncReceiverOptions): SyncReceiver;
package/dist/server.js ADDED
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Server-side entry point: receives sync messages from a MongoLite client and applies
3
+ * them to MongoDB.
4
+ *
5
+ * Install this on the API that sits in front of your database, for deployments where
6
+ * the local instance cannot reach MongoDB directly.
7
+ *
8
+ * ```ts
9
+ * import { MongoClient } from 'mongodb';
10
+ * import { createSyncReceiver } from '@semics-tech/mongolite/server';
11
+ *
12
+ * const mongo = await new MongoClient(process.env.MONGO_URL!).connect();
13
+ * const receiver = createSyncReceiver({
14
+ * client: mongo,
15
+ * database: 'app',
16
+ * allowedCollections: ['users', 'orders'],
17
+ * });
18
+ *
19
+ * app.post('/sync/app/_sync', async (req, res) => {
20
+ * const result = await receiver.apply(req.body);
21
+ * res.status(result.status).type('application/json').send(result.body);
22
+ * });
23
+ * ```
24
+ *
25
+ * This module imports nothing SQLite-related, so it is safe to install on a server that
26
+ * has no local database of its own.
27
+ */
28
+ import { buildWriteCommand } from './sync/commands.js';
29
+ import { executeWrites, fetchDocuments } from './sync/mongoExecutor.js';
30
+ import { SYNC_PROTOCOL_VERSION, SyncProtocolError, decodeBody, encodeBody, parseApplyRequest, parseFetchRequest, } from './sync/protocol.js';
31
+ export { SYNC_PROTOCOL_VERSION, SyncProtocolError } from './sync/protocol.js';
32
+ const JSON_HEADERS = { 'content-type': 'application/json' };
33
+ export class SyncReceiver {
34
+ constructor(options) {
35
+ this.options = options;
36
+ if (!options.client)
37
+ throw new Error('createSyncReceiver requires a connected `client`.');
38
+ this.db = options.client.db(options.database);
39
+ }
40
+ /**
41
+ * Applies a batch of operations.
42
+ *
43
+ * @param body The raw request body — a string, or an already-parsed object.
44
+ * @returns Status and Extended JSON body to send back.
45
+ */
46
+ async apply(body) {
47
+ try {
48
+ const decoded = decodeBody(body);
49
+ const { replicator, operations } = parseApplyRequest(decoded);
50
+ const maxOperations = this.options.maxOperations ?? 1000;
51
+ if (operations.length > maxOperations) {
52
+ throw new SyncProtocolError(`Batch of ${operations.length} exceeds the limit of ${maxOperations}`, 413);
53
+ }
54
+ const collections = [...new Set(operations.map((op) => op.collection))];
55
+ this.assertCollectionsAllowed(collections);
56
+ await this.authorise({ replicator, collections, operationCount: operations.length });
57
+ const writes = operations.map((op, index) => ({
58
+ index,
59
+ op,
60
+ command: this.commandFor(op, decoded, index),
61
+ }));
62
+ const result = await executeWrites(this.db, writes, {
63
+ writeConcern: this.options.writeConcern ?? { w: 'majority' },
64
+ versioning: this.options.versioning ?? true,
65
+ idMapping: this.options.idMapping,
66
+ ObjectId: this.options.ObjectId,
67
+ });
68
+ return this.ok({
69
+ protocol: SYNC_PROTOCOL_VERSION,
70
+ applied: result.applied,
71
+ failures: result.failures,
72
+ conflicts: result.conflicts,
73
+ });
74
+ }
75
+ catch (err) {
76
+ return this.error(err);
77
+ }
78
+ }
79
+ /** Reads documents back by `_id`, so a client can refresh its shadow after a conflict. */
80
+ async fetch(body) {
81
+ try {
82
+ const { collection, documentIds } = parseFetchRequest(decodeBody(body));
83
+ this.assertCollectionsAllowed([collection]);
84
+ await this.authorise({
85
+ replicator: 'fetch',
86
+ collections: [collection],
87
+ operationCount: documentIds.length,
88
+ });
89
+ const documents = await fetchDocuments(this.db, collection, documentIds, this.options.idMapping, this.options.ObjectId);
90
+ return this.ok({ protocol: SYNC_PROTOCOL_VERSION, documents });
91
+ }
92
+ catch (err) {
93
+ return this.error(err);
94
+ }
95
+ }
96
+ // ---------------------------------------------------------------- internals
97
+ /**
98
+ * Produces the command to execute for an operation.
99
+ *
100
+ * By default this **rebuilds** the command from the operation's own fields rather than
101
+ * using whatever the client sent. For a well-behaved client the two are identical;
102
+ * for a hostile one the difference is the whole security boundary.
103
+ */
104
+ commandFor(op, decoded, index) {
105
+ if (this.options.trustClientCommands) {
106
+ const wire = decoded.operations?.[index];
107
+ const command = wire?.command;
108
+ if (command && typeof command === 'object')
109
+ return command;
110
+ }
111
+ return buildWriteCommand(op, {
112
+ idMapping: this.options.idMapping,
113
+ versioning: this.options.versioning ?? true,
114
+ ObjectId: this.options.ObjectId,
115
+ });
116
+ }
117
+ assertCollectionsAllowed(collections) {
118
+ const allowed = this.options.allowedCollections;
119
+ if (!allowed)
120
+ return;
121
+ const permitted = new Set(allowed);
122
+ for (const collection of collections) {
123
+ if (!permitted.has(collection)) {
124
+ throw new SyncProtocolError(`Collection "${collection}" is not permitted`, 403);
125
+ }
126
+ }
127
+ }
128
+ async authorise(context) {
129
+ if (!this.options.verifyRequest)
130
+ return;
131
+ const verdict = await this.options.verifyRequest(context);
132
+ if (verdict === false) {
133
+ throw new SyncProtocolError('Request rejected', 403);
134
+ }
135
+ }
136
+ ok(payload) {
137
+ return { status: 200, body: encodeBody(payload), headers: JSON_HEADERS };
138
+ }
139
+ error(err) {
140
+ const status = err instanceof SyncProtocolError ? err.status : 500;
141
+ const message = err instanceof Error ? err.message : String(err);
142
+ return {
143
+ status,
144
+ // 5xx bodies stay vague: an unexpected error can carry connection strings or
145
+ // document contents, and this response goes to a client.
146
+ body: encodeBody({
147
+ protocol: SYNC_PROTOCOL_VERSION,
148
+ error: status >= 500 ? 'Internal error applying sync batch' : message,
149
+ }),
150
+ headers: JSON_HEADERS,
151
+ };
152
+ }
153
+ }
154
+ /** Creates a receiver for sync messages posted by a MongoLite client. */
155
+ export function createSyncReceiver(options) {
156
+ return new SyncReceiver(options);
157
+ }
158
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ import type { SyncIdMapping, SyncOperation } from './types.js';
2
+ /** Field carrying the optimistic-concurrency version on upstream documents. */
3
+ export declare const VERSION_FIELD = "_v";
4
+ /** Field carrying the server-stamped last-write time on upstream documents. */
5
+ export declare const UPDATED_AT_FIELD = "_updatedAt";
6
+ /** A single entry in a `bulkWrite` array. */
7
+ export type MongoWriteCommand = Record<string, unknown>;
8
+ /** Constructs an upstream `_id` value from MongoLite's string id. */
9
+ export interface ObjectIdFactory {
10
+ new (id: string): unknown;
11
+ isValid(id: string): boolean;
12
+ }
13
+ export interface BuildCommandOptions {
14
+ /** See {@link SyncIdMapping}. Defaults to `'auto'`. */
15
+ idMapping?: SyncIdMapping;
16
+ /** Guard writes with a conditional update against `_v`. Defaults to `true`. */
17
+ versioning?: boolean;
18
+ /**
19
+ * `ObjectId` implementation to use. The direct sink passes the driver's so BSON
20
+ * instances match the connection that will serialise them; the HTTP sink uses the
21
+ * one from `bson`, which Extended JSON encodes as `{"$oid": …}` for the wire.
22
+ */
23
+ ObjectId?: ObjectIdFactory;
24
+ }
25
+ /** True when this operation asserts a specific upstream revision. */
26
+ export declare function isConditional(op: SyncOperation): boolean;
27
+ /** `_id` is the identity; `_v`/`_updatedAt` are managed by the update itself. */
28
+ export declare function isReservedPath(path: string): boolean;
29
+ export declare function stripReservedFields(value: Record<string, unknown>): Record<string, unknown>;
30
+ /** Reads `_v` off an upstream document, tolerating its absence. */
31
+ export declare function readVersion(document: Record<string, unknown> | undefined): number | null;
32
+ /**
33
+ * MongoLite stores `_id` as a string. Converting ObjectId-shaped ids back to real
34
+ * `ObjectId`s keeps replicated documents indistinguishable from ones written by a
35
+ * native MongoDB client.
36
+ */
37
+ export declare function toUpstreamId(documentId: string, idMapping?: SyncIdMapping, ObjectId?: ObjectIdFactory): unknown;
38
+ /**
39
+ * Builds the upstream write for one operation.
40
+ *
41
+ * Every form is idempotent by `_id`, which is what makes at-least-once delivery safe:
42
+ * replaying a batch reproduces the same upstream state.
43
+ *
44
+ * With versioning on, writes are additionally *conditional* on the document still being
45
+ * at the version we last saw. That is the difference between "my change is newer, so it
46
+ * wins" — which cannot detect a lost update — and "I am replacing exactly the revision
47
+ * I read", which can.
48
+ */
49
+ export declare function buildWriteCommand(op: SyncOperation, options?: BuildCommandOptions): MongoWriteCommand;
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Builds the MongoDB write commands that realise a {@link SyncOperation}.
3
+ *
4
+ * This lives on its own because three places must agree on it byte for byte: the
5
+ * direct MongoDB sink, the HTTP sink that ships commands to a remote API, and the
6
+ * receiver on the far side that applies them. If any of the three built its own
7
+ * commands they would drift, and the drift would show up as data loss rather than as a
8
+ * test failure.
9
+ */
10
+ import { ObjectId as BsonObjectId } from 'bson';
11
+ /** Field carrying the optimistic-concurrency version on upstream documents. */
12
+ export const VERSION_FIELD = '_v';
13
+ /** Field carrying the server-stamped last-write time on upstream documents. */
14
+ export const UPDATED_AT_FIELD = '_updatedAt';
15
+ /** True when this operation asserts a specific upstream revision. */
16
+ export function isConditional(op) {
17
+ return op.baseVersion !== null && op.baseVersion !== undefined;
18
+ }
19
+ /** `_id` is the identity; `_v`/`_updatedAt` are managed by the update itself. */
20
+ export function isReservedPath(path) {
21
+ const root = path.split('.')[0];
22
+ return root === '_id' || root === VERSION_FIELD || root === UPDATED_AT_FIELD;
23
+ }
24
+ export function stripReservedFields(value) {
25
+ return Object.fromEntries(Object.entries(value).filter(([key]) => !isReservedPath(key)));
26
+ }
27
+ /** Reads `_v` off an upstream document, tolerating its absence. */
28
+ export function readVersion(document) {
29
+ if (!document)
30
+ return null;
31
+ const raw = document[VERSION_FIELD];
32
+ return typeof raw === 'number' ? raw : null;
33
+ }
34
+ /**
35
+ * MongoLite stores `_id` as a string. Converting ObjectId-shaped ids back to real
36
+ * `ObjectId`s keeps replicated documents indistinguishable from ones written by a
37
+ * native MongoDB client.
38
+ */
39
+ export function toUpstreamId(documentId, idMapping = 'auto', ObjectId = BsonObjectId) {
40
+ if (idMapping === 'string')
41
+ return documentId;
42
+ try {
43
+ return ObjectId.isValid(documentId) && /^[0-9a-fA-F]{24}$/.test(documentId)
44
+ ? new ObjectId(documentId)
45
+ : documentId;
46
+ }
47
+ catch {
48
+ return documentId;
49
+ }
50
+ }
51
+ /**
52
+ * Builds the upstream write for one operation.
53
+ *
54
+ * Every form is idempotent by `_id`, which is what makes at-least-once delivery safe:
55
+ * replaying a batch reproduces the same upstream state.
56
+ *
57
+ * With versioning on, writes are additionally *conditional* on the document still being
58
+ * at the version we last saw. That is the difference between "my change is newer, so it
59
+ * wins" — which cannot detect a lost update — and "I am replacing exactly the revision
60
+ * I read", which can.
61
+ */
62
+ export function buildWriteCommand(op, options = {}) {
63
+ const { idMapping = 'auto', versioning = true, ObjectId } = options;
64
+ const _id = toUpstreamId(op.documentId, idMapping, ObjectId);
65
+ if (op.type === 'delete') {
66
+ const filter = isConditional(op) ? { _id, [VERSION_FIELD]: op.baseVersion } : { _id };
67
+ return { deleteOne: { filter } };
68
+ }
69
+ if (!versioning) {
70
+ // The local database is treated as authoritative: replace wholesale, so a field
71
+ // deleted locally disappears upstream too.
72
+ return {
73
+ replaceOne: { filter: { _id }, replacement: { ...(op.document ?? {}), _id }, upsert: true },
74
+ };
75
+ }
76
+ // Never been upstream: create it, and let a duplicate key tell us we lost the race to
77
+ // create. `$setOnInsert` alone means an existing document is left untouched.
78
+ if (!isConditional(op)) {
79
+ const document = stripReservedFields(op.document ?? {});
80
+ return {
81
+ updateOne: {
82
+ filter: { _id },
83
+ update: {
84
+ $setOnInsert: { ...document, [VERSION_FIELD]: 1 },
85
+ $currentDate: { [UPDATED_AT_FIELD]: true },
86
+ },
87
+ upsert: true,
88
+ },
89
+ };
90
+ }
91
+ // Known revision: apply only what changed, conditional on that revision.
92
+ const update = {
93
+ $inc: { [VERSION_FIELD]: 1 },
94
+ $currentDate: { [UPDATED_AT_FIELD]: true },
95
+ };
96
+ const diff = op.diff;
97
+ if (diff) {
98
+ const set = stripReservedFields(diff.set);
99
+ const unset = diff.unset.filter((path) => !isReservedPath(path));
100
+ if (Object.keys(set).length > 0)
101
+ update.$set = set;
102
+ if (unset.length > 0) {
103
+ update.$unset = Object.fromEntries(unset.map((path) => [path, '']));
104
+ }
105
+ }
106
+ else {
107
+ // No shadow to diff against — fall back to writing the whole document, minus the
108
+ // bookkeeping fields the update itself manages.
109
+ update.$set = stripReservedFields(op.document ?? {});
110
+ }
111
+ return {
112
+ updateOne: {
113
+ filter: { _id, [VERSION_FIELD]: op.baseVersion },
114
+ update,
115
+ },
116
+ };
117
+ }
118
+ //# sourceMappingURL=commands.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.js","sourceRoot":"","sources":["../../src/sync/commands.ts"],"names":[],"mappings":"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"}