@semics-tech/mongolite 1.3.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.
- package/README.md +79 -21
- package/dist/cjs/collection.js +8 -1
- package/dist/cjs/index.js +108 -1
- package/dist/cjs/mongo-client.js +18 -1
- package/dist/cjs/server.js +164 -0
- package/dist/cjs/sync/commands.js +126 -0
- package/dist/cjs/sync/diff.js +174 -0
- package/dist/cjs/sync/httpSink.js +159 -0
- package/dist/cjs/sync/index.js +40 -0
- package/dist/cjs/sync/mongoExecutor.js +306 -0
- package/dist/cjs/sync/mongoSink.js +161 -0
- package/dist/cjs/sync/outbox.js +358 -0
- package/dist/cjs/sync/protocol.js +177 -0
- package/dist/cjs/sync/replicator.js +747 -0
- package/dist/cjs/sync/shadow.js +178 -0
- package/dist/cjs/sync/types.js +2 -0
- package/dist/cloudflare.d.ts +1 -1
- package/dist/cloudflare.js.map +1 -1
- package/dist/collection.js +8 -1
- package/dist/collection.js.map +1 -1
- package/dist/index.d.ts +68 -2
- package/dist/index.js +96 -1
- package/dist/index.js.map +1 -1
- package/dist/mongo-client.d.ts +13 -0
- package/dist/mongo-client.js +18 -1
- package/dist/mongo-client.js.map +1 -1
- package/dist/server.d.ts +97 -0
- package/dist/server.js +158 -0
- package/dist/server.js.map +1 -0
- package/dist/sync/commands.d.ts +49 -0
- package/dist/sync/commands.js +118 -0
- package/dist/sync/commands.js.map +1 -0
- package/dist/sync/diff.d.ts +55 -0
- package/dist/sync/diff.js +169 -0
- package/dist/sync/diff.js.map +1 -0
- package/dist/sync/httpSink.d.ts +93 -0
- package/dist/sync/httpSink.js +155 -0
- package/dist/sync/httpSink.js.map +1 -0
- package/dist/sync/index.d.ts +17 -0
- package/dist/sync/index.js +10 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/mongoExecutor.d.ts +37 -0
- package/dist/sync/mongoExecutor.js +302 -0
- package/dist/sync/mongoExecutor.js.map +1 -0
- package/dist/sync/mongoSink.d.ts +109 -0
- package/dist/sync/mongoSink.js +123 -0
- package/dist/sync/mongoSink.js.map +1 -0
- package/dist/sync/outbox.d.ts +120 -0
- package/dist/sync/outbox.js +355 -0
- package/dist/sync/outbox.js.map +1 -0
- package/dist/sync/protocol.d.ts +93 -0
- package/dist/sync/protocol.js +168 -0
- package/dist/sync/protocol.js.map +1 -0
- package/dist/sync/replicator.d.ts +177 -0
- package/dist/sync/replicator.js +744 -0
- package/dist/sync/replicator.js.map +1 -0
- package/dist/sync/shadow.d.ts +54 -0
- package/dist/sync/shadow.js +173 -0
- package/dist/sync/shadow.js.map +1 -0
- package/dist/sync/types.d.ts +388 -0
- package/dist/sync/types.js +2 -0
- package/dist/sync/types.js.map +1 -0
- package/package.json +15 -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
|
|
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,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/mongo-client.d.ts
CHANGED
|
@@ -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.
|
package/dist/mongo-client.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
package/dist/mongo-client.js.map
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/server.d.ts
ADDED
|
@@ -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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"}
|