@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.
- package/README.md +91 -20
- package/dist/bin/mongolite-debug.d.ts +6 -1
- package/dist/bin/mongolite-debug.js +6 -113
- package/dist/bin/mongolite-debug.js.map +1 -1
- package/dist/bin/mongolite.d.ts +13 -0
- package/dist/bin/mongolite.js +172 -0
- package/dist/bin/mongolite.js.map +1 -0
- package/dist/cjs/bin/mongolite-debug.js +6 -149
- package/dist/cjs/bin/mongolite.js +207 -0
- package/dist/cjs/cli/discover.js +156 -0
- package/dist/cjs/cli/explorer.js +929 -0
- package/dist/cjs/cli/filter-builder.js +276 -0
- package/dist/cjs/cli/format.js +161 -0
- package/dist/cjs/cli/schema.js +211 -0
- package/dist/cjs/cli/ui.js +431 -0
- package/dist/cjs/collection.js +8 -1
- package/dist/cjs/cursors/findCursor.js +20 -8
- 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/cli/discover.d.ts +40 -0
- package/dist/cli/discover.js +146 -0
- package/dist/cli/discover.js.map +1 -0
- package/dist/cli/explorer.d.ts +59 -0
- package/dist/cli/explorer.js +923 -0
- package/dist/cli/explorer.js.map +1 -0
- package/dist/cli/filter-builder.d.ts +52 -0
- package/dist/cli/filter-builder.js +268 -0
- package/dist/cli/filter-builder.js.map +1 -0
- package/dist/cli/format.d.ts +29 -0
- package/dist/cli/format.js +153 -0
- package/dist/cli/format.js.map +1 -0
- package/dist/cli/schema.d.ts +58 -0
- package/dist/cli/schema.js +205 -0
- package/dist/cli/schema.js.map +1 -0
- package/dist/cli/ui.d.ts +71 -0
- package/dist/cli/ui.js +379 -0
- package/dist/cli/ui.js.map +1 -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/cursors/findCursor.d.ts +12 -0
- package/dist/cursors/findCursor.js +20 -8
- package/dist/cursors/findCursor.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 +16 -1
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.PromptCancelledError = exports.style = exports.isInteractive = void 0;
|
|
37
|
+
exports.displayWidth = displayWidth;
|
|
38
|
+
exports.truncate = truncate;
|
|
39
|
+
exports.terminalWidth = terminalWidth;
|
|
40
|
+
exports.select = select;
|
|
41
|
+
exports.closePrompts = closePrompts;
|
|
42
|
+
exports.question = question;
|
|
43
|
+
exports.text = text;
|
|
44
|
+
exports.multilineText = multilineText;
|
|
45
|
+
exports.confirm = confirm;
|
|
46
|
+
exports.pause = pause;
|
|
47
|
+
exports.heading = heading;
|
|
48
|
+
exports.info = info;
|
|
49
|
+
exports.warn = warn;
|
|
50
|
+
exports.error = error;
|
|
51
|
+
exports.success = success;
|
|
52
|
+
/**
|
|
53
|
+
* Small, dependency-free terminal UI helpers used by the guided MongoLite CLI.
|
|
54
|
+
*
|
|
55
|
+
* Everything degrades gracefully:
|
|
56
|
+
* - When stdin/stdout is a TTY the prompts are interactive (arrow keys, filtering).
|
|
57
|
+
* - When it is not (pipes, CI, dumb terminals) the same prompts fall back to a
|
|
58
|
+
* numbered list read line-by-line, so the CLI stays scriptable.
|
|
59
|
+
*/
|
|
60
|
+
const readline = __importStar(require("node:readline"));
|
|
61
|
+
const NO_COLOR = process.env.NO_COLOR !== undefined || process.env.TERM === 'dumb';
|
|
62
|
+
const isInteractive = () => Boolean(process.stdin.isTTY && process.stdout.isTTY && !process.env.MONGOLITE_NON_INTERACTIVE);
|
|
63
|
+
exports.isInteractive = isInteractive;
|
|
64
|
+
const wrap = (open, close) => (text) => NO_COLOR || !process.stdout.isTTY ? text : `\u001b[${open}m${text}\u001b[${close}m`;
|
|
65
|
+
exports.style = {
|
|
66
|
+
bold: wrap('1', '22'),
|
|
67
|
+
dim: wrap('2', '22'),
|
|
68
|
+
italic: wrap('3', '23'),
|
|
69
|
+
underline: wrap('4', '24'),
|
|
70
|
+
red: wrap('31', '39'),
|
|
71
|
+
green: wrap('32', '39'),
|
|
72
|
+
yellow: wrap('33', '39'),
|
|
73
|
+
blue: wrap('34', '39'),
|
|
74
|
+
magenta: wrap('35', '39'),
|
|
75
|
+
cyan: wrap('36', '39'),
|
|
76
|
+
grey: wrap('90', '39'),
|
|
77
|
+
};
|
|
78
|
+
/** Visible width of a string, ignoring ANSI escape sequences. */
|
|
79
|
+
function displayWidth(text) {
|
|
80
|
+
return text.replace(/\u001b\[[0-9;]*m/g, '').length;
|
|
81
|
+
}
|
|
82
|
+
/** Truncate to `max` visible characters, appending an ellipsis when cut. */
|
|
83
|
+
function truncate(text, max) {
|
|
84
|
+
if (max <= 0)
|
|
85
|
+
return '';
|
|
86
|
+
if (text.length <= max)
|
|
87
|
+
return text;
|
|
88
|
+
if (max <= 1)
|
|
89
|
+
return text.slice(0, max);
|
|
90
|
+
return `${text.slice(0, max - 1)}…`;
|
|
91
|
+
}
|
|
92
|
+
function terminalWidth() {
|
|
93
|
+
return Math.max(40, process.stdout.columns || 100);
|
|
94
|
+
}
|
|
95
|
+
/** Thrown when the user aborts a prompt with Esc or Ctrl+C. */
|
|
96
|
+
class PromptCancelledError extends Error {
|
|
97
|
+
constructor() {
|
|
98
|
+
super('Prompt cancelled');
|
|
99
|
+
this.name = 'PromptCancelledError';
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
exports.PromptCancelledError = PromptCancelledError;
|
|
103
|
+
function selectableIndexes(choices) {
|
|
104
|
+
return choices.map((c, i) => (c.separator ? -1 : i)).filter((i) => i >= 0);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Ask the user to pick one of `choices`.
|
|
108
|
+
* Interactive terminals get an arrow-key menu with type-to-filter; everything
|
|
109
|
+
* else gets a numbered list.
|
|
110
|
+
*/
|
|
111
|
+
async function select(options) {
|
|
112
|
+
if (options.choices.length === 0) {
|
|
113
|
+
throw new Error(`Nothing to choose from for: ${options.message}`);
|
|
114
|
+
}
|
|
115
|
+
const selectable = selectableIndexes(options.choices);
|
|
116
|
+
if (selectable.length === 1 && options.choices.length === 1) {
|
|
117
|
+
// Nothing to decide - tell the user what was picked for them and move on.
|
|
118
|
+
const only = options.choices[selectable[0]];
|
|
119
|
+
process.stdout.write(`${exports.style.green('?')} ${exports.style.bold(options.message)} ${exports.style.cyan(only.label)} ${exports.style.grey('(only option)')}\n`);
|
|
120
|
+
return only.value;
|
|
121
|
+
}
|
|
122
|
+
return (0, exports.isInteractive)() ? interactiveSelect(options) : fallbackSelect(options);
|
|
123
|
+
}
|
|
124
|
+
async function interactiveSelect(options) {
|
|
125
|
+
const { message, choices } = options;
|
|
126
|
+
const pageSize = Math.min(options.pageSize ?? 12, Math.max(3, (process.stdout.rows || 24) - 6));
|
|
127
|
+
let filter = '';
|
|
128
|
+
let cursor = 0;
|
|
129
|
+
let renderedLines = 0;
|
|
130
|
+
const visible = () => {
|
|
131
|
+
const lowered = filter.toLowerCase();
|
|
132
|
+
return choices
|
|
133
|
+
.map((choice, index) => ({ choice, index }))
|
|
134
|
+
.filter(({ choice }) => {
|
|
135
|
+
if (!filter)
|
|
136
|
+
return true;
|
|
137
|
+
if (choice.separator)
|
|
138
|
+
return false;
|
|
139
|
+
return `${choice.label} ${choice.hint ?? ''}`.toLowerCase().includes(lowered);
|
|
140
|
+
});
|
|
141
|
+
};
|
|
142
|
+
const selectablePositions = (list) => list.map((entry, pos) => (entry.choice.separator ? -1 : pos)).filter((pos) => pos >= 0);
|
|
143
|
+
const initial = options.initialIndex ?? 0;
|
|
144
|
+
{
|
|
145
|
+
const list = visible();
|
|
146
|
+
const positions = selectablePositions(list);
|
|
147
|
+
const wanted = list.findIndex((entry) => entry.index === initial);
|
|
148
|
+
cursor = positions.includes(wanted) ? wanted : (positions[0] ?? 0);
|
|
149
|
+
}
|
|
150
|
+
const clear = () => {
|
|
151
|
+
if (renderedLines > 0) {
|
|
152
|
+
process.stdout.write(`\u001b[${renderedLines}A`);
|
|
153
|
+
process.stdout.write('\u001b[0J');
|
|
154
|
+
}
|
|
155
|
+
renderedLines = 0;
|
|
156
|
+
};
|
|
157
|
+
const render = () => {
|
|
158
|
+
clear();
|
|
159
|
+
const width = terminalWidth();
|
|
160
|
+
const list = visible();
|
|
161
|
+
const positions = selectablePositions(list);
|
|
162
|
+
if (positions.length && !positions.includes(cursor)) {
|
|
163
|
+
cursor = positions[0];
|
|
164
|
+
}
|
|
165
|
+
const lines = [];
|
|
166
|
+
const filterSuffix = filter ? ` ${exports.style.yellow(`/${filter}`)}` : '';
|
|
167
|
+
lines.push(`${exports.style.green('?')} ${exports.style.bold(message)}${filterSuffix}`);
|
|
168
|
+
lines.push(exports.style.grey(options.hint ?? ' ↑/↓ move · type to filter · enter to select · esc to go back'));
|
|
169
|
+
// Scroll window so the cursor stays visible.
|
|
170
|
+
let start = 0;
|
|
171
|
+
if (list.length > pageSize) {
|
|
172
|
+
start = Math.min(Math.max(0, cursor - Math.floor(pageSize / 2)), list.length - pageSize);
|
|
173
|
+
}
|
|
174
|
+
const window = list.slice(start, start + pageSize);
|
|
175
|
+
if (window.length === 0) {
|
|
176
|
+
lines.push(exports.style.grey(' no matches'));
|
|
177
|
+
}
|
|
178
|
+
window.forEach((entry, offset) => {
|
|
179
|
+
const pos = start + offset;
|
|
180
|
+
const { choice } = entry;
|
|
181
|
+
if (choice.separator) {
|
|
182
|
+
lines.push(exports.style.grey(` ${choice.label}`));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const active = pos === cursor;
|
|
186
|
+
const pointer = active ? exports.style.cyan('❯') : ' ';
|
|
187
|
+
const label = active ? exports.style.cyan(choice.label) : choice.label;
|
|
188
|
+
const hint = choice.hint ? ` ${exports.style.grey(choice.hint)}` : '';
|
|
189
|
+
const line = `${pointer} ${label}${hint}`;
|
|
190
|
+
lines.push(displayWidth(line) > width ? truncate(line, width) : line);
|
|
191
|
+
});
|
|
192
|
+
if (list.length > pageSize) {
|
|
193
|
+
lines.push(exports.style.grey(` … ${start + window.length}/${list.length}`));
|
|
194
|
+
}
|
|
195
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|
|
196
|
+
renderedLines = lines.length;
|
|
197
|
+
};
|
|
198
|
+
return new Promise((resolve, reject) => {
|
|
199
|
+
readline.emitKeypressEvents(process.stdin);
|
|
200
|
+
const wasRaw = process.stdin.isRaw;
|
|
201
|
+
process.stdin.setRawMode(true);
|
|
202
|
+
process.stdin.resume();
|
|
203
|
+
const finish = (fn) => {
|
|
204
|
+
process.stdin.removeListener('keypress', onKeypress);
|
|
205
|
+
process.stdin.setRawMode(Boolean(wasRaw));
|
|
206
|
+
process.stdin.pause();
|
|
207
|
+
fn();
|
|
208
|
+
};
|
|
209
|
+
const onKeypress = (str, key) => {
|
|
210
|
+
const list = visible();
|
|
211
|
+
const positions = selectablePositions(list);
|
|
212
|
+
if (key.ctrl && key.name === 'c') {
|
|
213
|
+
clear();
|
|
214
|
+
finish(() => {
|
|
215
|
+
process.stdout.write(exports.style.grey('Cancelled.\n'));
|
|
216
|
+
process.exit(0);
|
|
217
|
+
});
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
switch (key.name) {
|
|
221
|
+
case 'escape':
|
|
222
|
+
clear();
|
|
223
|
+
finish(() => reject(new PromptCancelledError()));
|
|
224
|
+
return;
|
|
225
|
+
case 'up': {
|
|
226
|
+
const at = positions.indexOf(cursor);
|
|
227
|
+
cursor = positions[(at - 1 + positions.length) % positions.length] ?? cursor;
|
|
228
|
+
render();
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
case 'down': {
|
|
232
|
+
const at = positions.indexOf(cursor);
|
|
233
|
+
cursor = positions[(at + 1) % positions.length] ?? cursor;
|
|
234
|
+
render();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
case 'pageup':
|
|
238
|
+
cursor = positions[0] ?? cursor;
|
|
239
|
+
render();
|
|
240
|
+
return;
|
|
241
|
+
case 'pagedown':
|
|
242
|
+
cursor = positions[positions.length - 1] ?? cursor;
|
|
243
|
+
render();
|
|
244
|
+
return;
|
|
245
|
+
case 'return':
|
|
246
|
+
case 'enter': {
|
|
247
|
+
const chosen = list[cursor];
|
|
248
|
+
if (!chosen || chosen.choice.separator)
|
|
249
|
+
return;
|
|
250
|
+
clear();
|
|
251
|
+
process.stdout.write(`${exports.style.green('?')} ${exports.style.bold(message)} ${exports.style.cyan(chosen.choice.label)}\n`);
|
|
252
|
+
finish(() => resolve(chosen.choice.value));
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
case 'backspace':
|
|
256
|
+
filter = filter.slice(0, -1);
|
|
257
|
+
render();
|
|
258
|
+
return;
|
|
259
|
+
default:
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
if (str && !key.ctrl && !key.meta && str >= ' ' && str <= '~') {
|
|
263
|
+
filter += str;
|
|
264
|
+
render();
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
process.stdin.on('keypress', onKeypress);
|
|
268
|
+
render();
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
async function fallbackSelect(options) {
|
|
272
|
+
const { message, choices } = options;
|
|
273
|
+
const numbered = choices
|
|
274
|
+
.map((choice, index) => ({ choice, index }))
|
|
275
|
+
.filter((e) => !e.choice.separator);
|
|
276
|
+
process.stdout.write(`\n${exports.style.bold(message)}\n`);
|
|
277
|
+
numbered.forEach((entry, position) => {
|
|
278
|
+
const hint = entry.choice.hint ? ` ${exports.style.grey(entry.choice.hint)}` : '';
|
|
279
|
+
process.stdout.write(` ${position + 1}) ${entry.choice.label}${hint}\n`);
|
|
280
|
+
});
|
|
281
|
+
for (;;) {
|
|
282
|
+
const answer = (await question(`Choose 1-${numbered.length}: `)).trim();
|
|
283
|
+
if (answer === '')
|
|
284
|
+
continue;
|
|
285
|
+
const position = Number.parseInt(answer, 10);
|
|
286
|
+
if (Number.isInteger(position) && position >= 1 && position <= numbered.length) {
|
|
287
|
+
return numbered[position - 1].choice.value;
|
|
288
|
+
}
|
|
289
|
+
// Also accept an exact (case-insensitive) label so scripted input stays readable.
|
|
290
|
+
const byLabel = numbered.find((entry) => entry.choice.label.toLowerCase() === answer.toLowerCase());
|
|
291
|
+
if (byLabel)
|
|
292
|
+
return byLabel.choice.value;
|
|
293
|
+
process.stdout.write(exports.style.red(' Not a valid choice, try again.\n'));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* A single readline interface is reused for line input when stdin is not a
|
|
298
|
+
* TTY. Creating one per prompt would work for a human typing, but a *piped*
|
|
299
|
+
* script has all its answers buffered at once: closing the interface after the
|
|
300
|
+
* first question discards every remaining line. Interactive sessions still get
|
|
301
|
+
* a throwaway interface per prompt so they can coexist with the raw-mode
|
|
302
|
+
* keypress handling in `select`.
|
|
303
|
+
*/
|
|
304
|
+
let sharedInterface = null;
|
|
305
|
+
let stdinEnded = false;
|
|
306
|
+
/** Lines that arrived before anything asked for them. */
|
|
307
|
+
const bufferedLines = [];
|
|
308
|
+
/** Callbacks waiting for the next line; `null` means stdin ended first. */
|
|
309
|
+
const lineWaiters = [];
|
|
310
|
+
function getSharedInterface() {
|
|
311
|
+
if (!sharedInterface) {
|
|
312
|
+
sharedInterface = readline.createInterface({
|
|
313
|
+
input: process.stdin,
|
|
314
|
+
output: process.stdout,
|
|
315
|
+
});
|
|
316
|
+
// Queue every line as it arrives. Piped input delivers all of its lines
|
|
317
|
+
// immediately, long before the later prompts ask for them.
|
|
318
|
+
sharedInterface.on('line', (line) => {
|
|
319
|
+
const waiter = lineWaiters.shift();
|
|
320
|
+
if (waiter)
|
|
321
|
+
waiter(line);
|
|
322
|
+
else
|
|
323
|
+
bufferedLines.push(line);
|
|
324
|
+
});
|
|
325
|
+
sharedInterface.on('close', () => {
|
|
326
|
+
stdinEnded = true;
|
|
327
|
+
while (lineWaiters.length > 0)
|
|
328
|
+
lineWaiters.shift()?.(null);
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
return sharedInterface;
|
|
332
|
+
}
|
|
333
|
+
/** Release the shared interface so the process can exit. */
|
|
334
|
+
function closePrompts() {
|
|
335
|
+
sharedInterface?.close();
|
|
336
|
+
sharedInterface = null;
|
|
337
|
+
}
|
|
338
|
+
/** Read a single line from stdin. */
|
|
339
|
+
function question(prompt) {
|
|
340
|
+
if ((0, exports.isInteractive)()) {
|
|
341
|
+
return new Promise((resolve) => {
|
|
342
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
343
|
+
rl.question(prompt, (answer) => {
|
|
344
|
+
rl.close();
|
|
345
|
+
resolve(answer);
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
getSharedInterface();
|
|
350
|
+
process.stdout.write(prompt);
|
|
351
|
+
const buffered = bufferedLines.shift();
|
|
352
|
+
if (buffered !== undefined) {
|
|
353
|
+
process.stdout.write(`${buffered}\n`);
|
|
354
|
+
return Promise.resolve(buffered);
|
|
355
|
+
}
|
|
356
|
+
if (stdinEnded) {
|
|
357
|
+
// Input is exhausted - carrying on would spin forever re-asking.
|
|
358
|
+
process.stdout.write(`\n${exports.style.grey('Input ended.')}\n`);
|
|
359
|
+
process.exit(0);
|
|
360
|
+
}
|
|
361
|
+
return new Promise((resolve) => {
|
|
362
|
+
lineWaiters.push((line) => {
|
|
363
|
+
if (line === null) {
|
|
364
|
+
process.stdout.write(`\n${exports.style.grey('Input ended.')}\n`);
|
|
365
|
+
process.exit(0);
|
|
366
|
+
}
|
|
367
|
+
resolve(line);
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
/** Ask for free-text input, re-prompting until `validate` is happy. */
|
|
372
|
+
async function text(options) {
|
|
373
|
+
const suffix = options.defaultValue ? exports.style.grey(` (${options.defaultValue})`) : '';
|
|
374
|
+
if (options.hint)
|
|
375
|
+
process.stdout.write(`${exports.style.grey(options.hint)}\n`);
|
|
376
|
+
for (;;) {
|
|
377
|
+
const raw = await question(`${exports.style.green('?')} ${exports.style.bold(options.message)}${suffix} `);
|
|
378
|
+
const value = raw.trim() === '' ? (options.defaultValue ?? '') : raw.trim();
|
|
379
|
+
if (value === '' && !options.allowEmpty && !options.defaultValue) {
|
|
380
|
+
process.stdout.write(exports.style.red(' A value is required.\n'));
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
const error = options.validate?.(value);
|
|
384
|
+
if (error) {
|
|
385
|
+
process.stdout.write(`${exports.style.red(` ${error}`)}\n`);
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
return value;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
/** Ask for multi-line input; a blank line ends the entry. */
|
|
392
|
+
async function multilineText(message) {
|
|
393
|
+
process.stdout.write(`${exports.style.green('?')} ${exports.style.bold(message)}\n`);
|
|
394
|
+
process.stdout.write(exports.style.grey(' Finish with an empty line.\n'));
|
|
395
|
+
const lines = [];
|
|
396
|
+
for (;;) {
|
|
397
|
+
const line = await question(' ');
|
|
398
|
+
if (line.trim() === '')
|
|
399
|
+
break;
|
|
400
|
+
lines.push(line);
|
|
401
|
+
}
|
|
402
|
+
return lines.join('\n');
|
|
403
|
+
}
|
|
404
|
+
async function confirm(message, defaultValue = false) {
|
|
405
|
+
const hint = defaultValue ? 'Y/n' : 'y/N';
|
|
406
|
+
const answer = (await question(`${exports.style.green('?')} ${exports.style.bold(message)} ${exports.style.grey(`(${hint})`)} `))
|
|
407
|
+
.trim()
|
|
408
|
+
.toLowerCase();
|
|
409
|
+
if (answer === '')
|
|
410
|
+
return defaultValue;
|
|
411
|
+
return answer === 'y' || answer === 'yes';
|
|
412
|
+
}
|
|
413
|
+
/** Wait for the user before scrolling more output past them. */
|
|
414
|
+
async function pause(message = 'Press enter to continue') {
|
|
415
|
+
await question(exports.style.grey(`${message} `));
|
|
416
|
+
}
|
|
417
|
+
function heading(text) {
|
|
418
|
+
process.stdout.write(`\n${exports.style.bold(exports.style.underline(text))}\n`);
|
|
419
|
+
}
|
|
420
|
+
function info(text) {
|
|
421
|
+
process.stdout.write(`${text}\n`);
|
|
422
|
+
}
|
|
423
|
+
function warn(text) {
|
|
424
|
+
process.stdout.write(`${exports.style.yellow('!')} ${text}\n`);
|
|
425
|
+
}
|
|
426
|
+
function error(text) {
|
|
427
|
+
process.stdout.write(`${exports.style.red('✖')} ${text}\n`);
|
|
428
|
+
}
|
|
429
|
+
function success(text) {
|
|
430
|
+
process.stdout.write(`${exports.style.green('✔')} ${text}\n`);
|
|
431
|
+
}
|
package/dist/cjs/collection.js
CHANGED
|
@@ -1285,7 +1285,14 @@ class MongoLiteCollection {
|
|
|
1285
1285
|
if (!upsert) {
|
|
1286
1286
|
return { acknowledged: true, matchedCount: 0, modifiedCount: 0, upsertedId: null };
|
|
1287
1287
|
}
|
|
1288
|
-
|
|
1288
|
+
// An upsert adopts the `_id` the filter pinned down, matching MongoDB. Without
|
|
1289
|
+
// this the inserted document gets a freshly generated id and the caller's
|
|
1290
|
+
// `replaceOne({ _id }, ..., { upsert: true })` silently creates the wrong row.
|
|
1291
|
+
const filterId = filter._id;
|
|
1292
|
+
const seeded = typeof filterId === 'string'
|
|
1293
|
+
? { ...replacement, _id: filterId }
|
|
1294
|
+
: replacement;
|
|
1295
|
+
const result = await this.insertOne(seeded);
|
|
1289
1296
|
return {
|
|
1290
1297
|
acknowledged: true,
|
|
1291
1298
|
matchedCount: 0,
|
|
@@ -779,6 +779,25 @@ class FindCursor {
|
|
|
779
779
|
* @returns A promise that resolves to an array of documents.
|
|
780
780
|
*/
|
|
781
781
|
async toArray() {
|
|
782
|
+
const { sql: finalSql, params: finalParams } = this.toSQL();
|
|
783
|
+
// Handle malformed json in the database
|
|
784
|
+
const rows = await this.db.all(finalSql, finalParams);
|
|
785
|
+
return rows.map((row) => {
|
|
786
|
+
const parsedData = safeJsonParse(row.data, `findCursor toArray for document ${row._id}`);
|
|
787
|
+
const restoredData = restoreDates(parsedData);
|
|
788
|
+
const doc = { _id: row._id, ...restoredData };
|
|
789
|
+
return this.applyProjection(doc);
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Returns the SQL this cursor will execute, along with its bound parameters.
|
|
794
|
+
*
|
|
795
|
+
* Useful for debugging and for tools that want to show how a MongoDB-style
|
|
796
|
+
* filter was translated. Reflects any `sort`, `limit` and `skip` applied so
|
|
797
|
+
* far; `project` is applied in JavaScript after the rows come back and so
|
|
798
|
+
* does not appear here.
|
|
799
|
+
*/
|
|
800
|
+
toSQL() {
|
|
782
801
|
let finalSql = this.queryParts.sql;
|
|
783
802
|
const finalParams = [...this.queryParts.params];
|
|
784
803
|
if (this.sortCriteria) {
|
|
@@ -805,14 +824,7 @@ class FindCursor {
|
|
|
805
824
|
finalSql += ` OFFSET ?`;
|
|
806
825
|
finalParams.push(this.skipCount);
|
|
807
826
|
}
|
|
808
|
-
|
|
809
|
-
const rows = await this.db.all(finalSql, finalParams);
|
|
810
|
-
return rows.map((row) => {
|
|
811
|
-
const parsedData = safeJsonParse(row.data, `findCursor toArray for document ${row._id}`);
|
|
812
|
-
const restoredData = restoreDates(parsedData);
|
|
813
|
-
const doc = { _id: row._id, ...restoredData };
|
|
814
|
-
return this.applyProjection(doc);
|
|
815
|
-
});
|
|
827
|
+
return { sql: finalSql, params: finalParams };
|
|
816
828
|
}
|
|
817
829
|
/**
|
|
818
830
|
* Executes the query and returns the first matching document.
|
package/dist/cjs/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.MongoLite = exports.NodeSqliteAdapter = exports.BrowserSqliteAdapter = exports.CloudflareDurableObjectAdapter = exports.ChangeStream = exports.MongoLiteCollection = void 0;
|
|
17
|
+
exports.MongoLite = exports.projectToLocalShape = exports.isEmptyDiff = exports.applyDiff = exports.computeDiff = exports.buildWriteCommand = exports.SYNC_PROTOCOL_VERSION = exports.SyncRequestRejected = exports.HttpUpstreamSink = exports.MongoUpstreamSink = exports.SyncShadow = exports.SyncOutbox = exports.SyncReplicator = exports.NodeSqliteAdapter = exports.BrowserSqliteAdapter = exports.CloudflareDurableObjectAdapter = exports.ChangeStream = exports.MongoLiteCollection = void 0;
|
|
18
18
|
const node_sqlite_js_1 = require("./adapters/node-sqlite.js");
|
|
19
19
|
const mongo_client_js_1 = require("./mongo-client.js");
|
|
20
20
|
var collection_js_1 = require("./collection.js");
|
|
@@ -28,6 +28,20 @@ var browser_js_1 = require("./adapters/browser.js");
|
|
|
28
28
|
Object.defineProperty(exports, "BrowserSqliteAdapter", { enumerable: true, get: function () { return browser_js_1.BrowserSqliteAdapter; } });
|
|
29
29
|
var node_sqlite_js_2 = require("./adapters/node-sqlite.js");
|
|
30
30
|
Object.defineProperty(exports, "NodeSqliteAdapter", { enumerable: true, get: function () { return node_sqlite_js_2.NodeSqliteAdapter; } });
|
|
31
|
+
const index_js_1 = require("./sync/index.js");
|
|
32
|
+
var index_js_2 = require("./sync/index.js");
|
|
33
|
+
Object.defineProperty(exports, "SyncReplicator", { enumerable: true, get: function () { return index_js_2.SyncReplicator; } });
|
|
34
|
+
Object.defineProperty(exports, "SyncOutbox", { enumerable: true, get: function () { return index_js_2.SyncOutbox; } });
|
|
35
|
+
Object.defineProperty(exports, "SyncShadow", { enumerable: true, get: function () { return index_js_2.SyncShadow; } });
|
|
36
|
+
Object.defineProperty(exports, "MongoUpstreamSink", { enumerable: true, get: function () { return index_js_2.MongoUpstreamSink; } });
|
|
37
|
+
Object.defineProperty(exports, "HttpUpstreamSink", { enumerable: true, get: function () { return index_js_2.HttpUpstreamSink; } });
|
|
38
|
+
Object.defineProperty(exports, "SyncRequestRejected", { enumerable: true, get: function () { return index_js_2.SyncRequestRejected; } });
|
|
39
|
+
Object.defineProperty(exports, "SYNC_PROTOCOL_VERSION", { enumerable: true, get: function () { return index_js_2.SYNC_PROTOCOL_VERSION; } });
|
|
40
|
+
Object.defineProperty(exports, "buildWriteCommand", { enumerable: true, get: function () { return index_js_2.buildWriteCommand; } });
|
|
41
|
+
Object.defineProperty(exports, "computeDiff", { enumerable: true, get: function () { return index_js_2.computeDiff; } });
|
|
42
|
+
Object.defineProperty(exports, "applyDiff", { enumerable: true, get: function () { return index_js_2.applyDiff; } });
|
|
43
|
+
Object.defineProperty(exports, "isEmptyDiff", { enumerable: true, get: function () { return index_js_2.isEmptyDiff; } });
|
|
44
|
+
Object.defineProperty(exports, "projectToLocalShape", { enumerable: true, get: function () { return index_js_2.projectToLocalShape; } });
|
|
31
45
|
/**
|
|
32
46
|
* MongoLite class is the main entry point for interacting with the SQLite-backed database.
|
|
33
47
|
*
|
|
@@ -61,5 +75,98 @@ class MongoLite extends mongo_client_js_1.MongoLite {
|
|
|
61
75
|
super(new node_sqlite_js_1.NodeSqliteAdapter(dbPathOrOptions), options);
|
|
62
76
|
}
|
|
63
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Replicates every insert, update and delete in this database to an upstream
|
|
80
|
+
* MongoDB deployment.
|
|
81
|
+
*
|
|
82
|
+
* Changes are captured by SQLite triggers into a durable outbox inside this
|
|
83
|
+
* database, in the same transaction as the write itself, then streamed
|
|
84
|
+
* upstream in batches. Replication survives process restarts and upstream
|
|
85
|
+
* outages: the outbox holds the backlog and a checkpoint records exactly how
|
|
86
|
+
* far it got.
|
|
87
|
+
*
|
|
88
|
+
* Requires the optional `mongodb` peer dependency.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```typescript
|
|
92
|
+
* const sync = client.syncToMongo({
|
|
93
|
+
* connectionString: 'mongodb+srv://user:pass@cluster.example.com/app',
|
|
94
|
+
* collections: ['users', 'orders'],
|
|
95
|
+
* });
|
|
96
|
+
*
|
|
97
|
+
* sync.on('error', (err) => console.error('replication degraded:', err));
|
|
98
|
+
* await sync.start();
|
|
99
|
+
*
|
|
100
|
+
* // …later, on shutdown
|
|
101
|
+
* await sync.stop({ flush: true });
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
syncToMongo(options) {
|
|
105
|
+
const { connectionString, database, writeConcern, driverOptions, driver, authSource, authMechanism, authMechanismProperties, username, password, tls, tlsCAFile, tlsCertificateKeyFile, tlsCertificateKeyFilePassword, tlsCRLFile, tlsAllowInvalidHostnames, tlsAllowInvalidCertificates, ...syncOptions } = options;
|
|
106
|
+
const sink = new index_js_1.MongoUpstreamSink({
|
|
107
|
+
connectionString,
|
|
108
|
+
database,
|
|
109
|
+
writeConcern,
|
|
110
|
+
driverOptions,
|
|
111
|
+
driver,
|
|
112
|
+
authSource,
|
|
113
|
+
authMechanism,
|
|
114
|
+
authMechanismProperties,
|
|
115
|
+
username,
|
|
116
|
+
password,
|
|
117
|
+
tls,
|
|
118
|
+
tlsCAFile,
|
|
119
|
+
tlsCertificateKeyFile,
|
|
120
|
+
tlsCertificateKeyFilePassword,
|
|
121
|
+
tlsCRLFile,
|
|
122
|
+
tlsAllowInvalidHostnames,
|
|
123
|
+
tlsAllowInvalidCertificates,
|
|
124
|
+
idMapping: syncOptions.idMapping,
|
|
125
|
+
// The sink builds the conditional writes, so it needs the same answer the
|
|
126
|
+
// replicator uses when deciding whether to track shadows at all.
|
|
127
|
+
versioning: syncOptions.versioning,
|
|
128
|
+
});
|
|
129
|
+
return this.createSync(sink, syncOptions);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Replicates changes to a remote HTTP API, which applies them to MongoDB on this
|
|
133
|
+
* client's behalf. For deployments where the local instance cannot reach the database
|
|
134
|
+
* directly.
|
|
135
|
+
*
|
|
136
|
+
* The wire format carries MongoDB-shaped commands along with the version predicates,
|
|
137
|
+
* so replication behaves the same as over a direct connection — the upstream stays the
|
|
138
|
+
* source of truth and concurrent writers are still detected. Mount
|
|
139
|
+
* `@semics-tech/mongolite/server` on the API to receive the messages.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```typescript
|
|
143
|
+
* const sync = client.syncToHttp({
|
|
144
|
+
* baseUrl: 'https://api.example.com',
|
|
145
|
+
* database: 'app',
|
|
146
|
+
* headers: { 'luna-environment': 'prod' },
|
|
147
|
+
* getAuthHeaders: async () => ({ Authorization: `Bearer ${await getAccessToken()}` }),
|
|
148
|
+
* });
|
|
149
|
+
*
|
|
150
|
+
* await sync.start();
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
syncToHttp(options) {
|
|
154
|
+
const { baseUrl, database, pathPrefix, getAuthHeaders, headers, cookies, fetch: fetchImpl, timeoutMs, includeCommands, ...syncOptions } = options;
|
|
155
|
+
const sink = new index_js_1.HttpUpstreamSink({
|
|
156
|
+
baseUrl,
|
|
157
|
+
database,
|
|
158
|
+
pathPrefix,
|
|
159
|
+
getAuthHeaders,
|
|
160
|
+
headers,
|
|
161
|
+
cookies,
|
|
162
|
+
fetch: fetchImpl,
|
|
163
|
+
timeoutMs,
|
|
164
|
+
includeCommands,
|
|
165
|
+
idMapping: syncOptions.idMapping,
|
|
166
|
+
versioning: syncOptions.versioning,
|
|
167
|
+
replicator: syncOptions.name,
|
|
168
|
+
});
|
|
169
|
+
return this.createSync(sink, syncOptions);
|
|
170
|
+
}
|
|
64
171
|
}
|
|
65
172
|
exports.MongoLite = MongoLite;
|
package/dist/cjs/mongo-client.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MongoLite = void 0;
|
|
4
4
|
const collection_js_1 = require("./collection.js");
|
|
5
|
+
const replicator_js_1 = require("./sync/replicator.js");
|
|
5
6
|
/**
|
|
6
7
|
* Core MongoLite client. Accepts any `IDatabaseAdapter` implementation.
|
|
7
8
|
*
|
|
@@ -42,11 +43,27 @@ class MongoLite {
|
|
|
42
43
|
listCollections() {
|
|
43
44
|
return {
|
|
44
45
|
toArray: async () => {
|
|
45
|
-
|
|
46
|
+
// `__mongolite*` tables are internal bookkeeping (change log, sync
|
|
47
|
+
// outbox and checkpoints), not user collections.
|
|
48
|
+
const result = await this.db.all(`SELECT name FROM sqlite_master
|
|
49
|
+
WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '__mongolite%'`);
|
|
46
50
|
return result.map((row) => row.name);
|
|
47
51
|
},
|
|
48
52
|
};
|
|
49
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Starts replicating this database's changes to an upstream {@link SyncSink}.
|
|
56
|
+
*
|
|
57
|
+
* Node.js users replicating to MongoDB should prefer the `syncToMongo`
|
|
58
|
+
* overload on the package-root `MongoLite`, which takes a connection string
|
|
59
|
+
* directly. This method is the backend-agnostic form — pass any sink to
|
|
60
|
+
* replicate over HTTP, into a queue, or to a test double.
|
|
61
|
+
*
|
|
62
|
+
* The returned replicator is **not** started; call `start()` on it.
|
|
63
|
+
*/
|
|
64
|
+
createSync(sink, options = {}) {
|
|
65
|
+
return new replicator_js_1.SyncReplicator(this.db, sink, { verbose: this.options.verbose, ...options });
|
|
66
|
+
}
|
|
50
67
|
/**
|
|
51
68
|
* Returns a typed collection handle.
|
|
52
69
|
* @param name The collection (table) name.
|