@coldsmirk/inkstone-mongodb 0.22.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 ADDED
@@ -0,0 +1,51 @@
1
+ # @coldsmirk/inkstone-mongodb
2
+
3
+ MongoDB command editing intelligence for Monaco: a command document in the shell's own spelling (`{ find: "orders", filter: { status: "open" }, limit: 20 }` — keys bare or quoted, `ObjectId("…")`, `ISODate("…")`, `/regex/i`), the database commands and their fields, the query, update and aggregation operators by where the caret stands, collection and database completion, the document read as the engine reads it, markers, and a drop-in React `<MongodbEditor>`.
4
+
5
+ Part of [inkstone](https://github.com/coldsmirk/inkstone). Built for the [**polyglot**](https://github.com/coldsmirk/polyglot) sidecar's MongoDB engine, which takes one command document — the command as its first key, `$db` naming the database — and parses it with the driver's own `BsonDocument.parse`.
6
+
7
+ ## Entries
8
+
9
+ - **`@coldsmirk/inkstone-mongodb`** — the framework-free core: the document reader, the command table, the operator vocabularies, the assist, the markers. Zero editor dependencies.
10
+ - **`@coldsmirk/inkstone-mongodb/monaco`** — `defineMongodbLanguage(monaco)` (the `mongodb` id with its Monarch grammar, once per monaco module), `registerMongodbLanguage(monaco, modelPath, hooks)` (completion and hover for one model), `setMongodbMarkers`. Peer: `monaco-editor`.
11
+ - **`@coldsmirk/inkstone-mongodb/react`** — `<MongodbEditor>`: a controlled command editor built on `@coldsmirk/inkstone-react`'s `<MonacoEditor>`, with a run key (Ctrl/Cmd-Enter handing over the live caret), caret tracking, an insert handle for collection trees, and the markers refreshed on every change. Peers: `monaco-editor`, `react`.
12
+
13
+ ## The shape
14
+
15
+ ```ts
16
+ import { createMongodbAssist, documentMarkers, runnableDocument } from "@coldsmirk/inkstone-mongodb";
17
+
18
+ // The catalog is YOURS to fetch — and it MUST be a cache-backed read (the tree's own list),
19
+ // never a fresh fetch: completion reads it on every keystroke in a value.
20
+ const assist = createMongodbAssist({ collections: () => cachedCollections() }); // CatalogCollection[]
21
+
22
+ // What one run should send: the selection where there is one, else the whole document.
23
+ const runnable = runnableDocument(text, from, to);
24
+
25
+ if (runnable.kind === "one") {
26
+ send(runnable.text);
27
+ }
28
+
29
+ // A code per fault — the host owns the wording.
30
+ for (const marker of documentMarkers(text)) {
31
+ console.log(marker.code, marker.severity, marker.from, marker.to);
32
+ }
33
+ ```
34
+
35
+ Completion offers the commands at the document's first key and the named command's fields after it, `$db` beside them; the collections as the command's value and the databases as `$db`'s; the query operators under a filter and the comparison operators under a field of one; the update operators under an update; the pipeline stages inside a pipeline and each stage's keys under it. Hover describes the command under its key.
36
+
37
+ ## The mirror invariant
38
+
39
+ `readDocument` reads the text as the driver's `BsonDocument.parse` does — one object, keys bare or quoted, strings in either quote, the shell's constructors with or without `new`, regular-expression literals, `undefined`, `MinKey` and `MaxKey`, no comments and no trailing commas — and refuses what the engine refuses, as a code (`"not-a-document"` with the offset it stopped at, `"no-command"` for a document holding nothing but `$db`). A change to the engine's parsing requires a matching release of this package; `document.test.ts` carries the cases.
40
+
41
+ ## The vocabulary
42
+
43
+ The command table and the operator lists are curated in `commands.ts` — the commands a bench reaches for with their fields, and the operators the query language, the update language and the aggregation pipeline take, by context. They are a hint of the language's shape, never its authority: a key the table does not know is still sent, and the server's answer is what counts.
44
+
45
+ ## Install
46
+
47
+ ```bash
48
+ pnpm add @coldsmirk/inkstone-mongodb monaco-editor
49
+ ```
50
+
51
+ Every peer is optional, because the root entry needs none of them: `/monaco` needs `monaco-editor`, and `/react` additionally needs `react` (>= 19) and `@coldsmirk/inkstone-react`. Never import `monaco-editor` statically — take the module from `onMount` / `ensureMonacoHost()`; type-only imports erase and are fine.
@@ -0,0 +1,408 @@
1
+ //#region src/document.ts
2
+ const CONSTRUCTORS = [
3
+ "ObjectId",
4
+ "ISODate",
5
+ "Date",
6
+ "NumberLong",
7
+ "NumberInt",
8
+ "NumberDecimal",
9
+ "Timestamp",
10
+ "BinData",
11
+ "UUID",
12
+ "HexData",
13
+ "RegExp",
14
+ "DBRef",
15
+ "DBPointer",
16
+ "MinKey",
17
+ "MaxKey"
18
+ ];
19
+ const CONSTRUCTOR_SET = new Set(CONSTRUCTORS);
20
+ const LITERALS = [
21
+ "true",
22
+ "false",
23
+ "null",
24
+ "undefined",
25
+ "Infinity",
26
+ "NaN",
27
+ "MinKey",
28
+ "MaxKey"
29
+ ];
30
+ const LITERAL_SET = new Set(LITERALS);
31
+ const IDENTIFIER = /[A-Za-z_$][\w$]*/uy;
32
+ const NUMBER = /-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?|-?Infinity|NaN/uy;
33
+ const ESCAPES = {
34
+ n: "\n",
35
+ r: "\r",
36
+ t: " ",
37
+ b: "\b",
38
+ f: "\f",
39
+ "\"": "\"",
40
+ "'": "'",
41
+ "\\": "\\",
42
+ "/": "/"
43
+ };
44
+ var Fault = class extends Error {
45
+ at;
46
+ constructor(at) {
47
+ super("not a document");
48
+ this.at = at;
49
+ }
50
+ };
51
+ var Reader = class {
52
+ text;
53
+ i = 0;
54
+ constructor(text) {
55
+ this.text = text;
56
+ }
57
+ blanks() {
58
+ while (this.i < this.text.length && /\s/u.test(this.text[this.i])) this.i++;
59
+ }
60
+ expect(character) {
61
+ if (this.text[this.i] !== character) throw new Fault(this.i);
62
+ this.i++;
63
+ }
64
+ key() {
65
+ const c = this.text[this.i];
66
+ if (c === "\"" || c === "'") return this.string();
67
+ IDENTIFIER.lastIndex = this.i;
68
+ const match = IDENTIFIER.exec(this.text);
69
+ if (match === null) throw new Fault(this.i);
70
+ this.i += match[0].length;
71
+ return match[0];
72
+ }
73
+ string() {
74
+ const quote = this.text[this.i];
75
+ let out = "";
76
+ this.i++;
77
+ while (true) {
78
+ if (this.i >= this.text.length) throw new Fault(this.i);
79
+ const c = this.text[this.i];
80
+ if (c === quote) {
81
+ this.i++;
82
+ return out;
83
+ }
84
+ if (c === "\\") {
85
+ const escaped = this.text[this.i + 1];
86
+ if (escaped === "u") {
87
+ const hex = this.text.slice(this.i + 2, this.i + 6);
88
+ if (!/^[0-9a-f]{4}$/iu.test(hex)) throw new Fault(this.i);
89
+ out += String.fromCodePoint(Number.parseInt(hex, 16));
90
+ this.i += 6;
91
+ continue;
92
+ }
93
+ const resolved = escaped === void 0 ? void 0 : ESCAPES[escaped];
94
+ if (resolved === void 0) throw new Fault(this.i);
95
+ out += resolved;
96
+ this.i += 2;
97
+ continue;
98
+ }
99
+ out += c;
100
+ this.i++;
101
+ }
102
+ }
103
+ value() {
104
+ const c = this.text[this.i];
105
+ if (c === void 0) throw new Fault(this.i);
106
+ if (c === "{") {
107
+ this.container("}", true);
108
+ return null;
109
+ }
110
+ if (c === "[") {
111
+ this.container("]", false);
112
+ return null;
113
+ }
114
+ if (c === "\"" || c === "'") return this.string();
115
+ if (c === "/") {
116
+ this.regex();
117
+ return null;
118
+ }
119
+ NUMBER.lastIndex = this.i;
120
+ const number = NUMBER.exec(this.text);
121
+ if (number !== null) {
122
+ this.i += number[0].length;
123
+ return null;
124
+ }
125
+ const start = this.i;
126
+ IDENTIFIER.lastIndex = start;
127
+ const word = IDENTIFIER.exec(this.text);
128
+ if (word === null) throw new Fault(start);
129
+ this.i += word[0].length;
130
+ if (word[0] === "new") {
131
+ this.blanks();
132
+ IDENTIFIER.lastIndex = this.i;
133
+ const constructed = IDENTIFIER.exec(this.text);
134
+ if (constructed === null || !CONSTRUCTOR_SET.has(constructed[0])) throw new Fault(this.i);
135
+ this.i += constructed[0].length;
136
+ this.call();
137
+ return null;
138
+ }
139
+ this.blanks();
140
+ if (this.text[this.i] === "(") {
141
+ if (!CONSTRUCTOR_SET.has(word[0])) throw new Fault(this.i);
142
+ this.call();
143
+ return null;
144
+ }
145
+ if (!LITERAL_SET.has(word[0])) throw new Fault(start);
146
+ return null;
147
+ }
148
+ container(close, keyed) {
149
+ this.i++;
150
+ this.blanks();
151
+ if (this.text[this.i] === close) {
152
+ this.i++;
153
+ return;
154
+ }
155
+ while (true) {
156
+ if (keyed) {
157
+ this.key();
158
+ this.blanks();
159
+ this.expect(":");
160
+ this.blanks();
161
+ }
162
+ this.value();
163
+ this.blanks();
164
+ if (this.text[this.i] === ",") {
165
+ this.i++;
166
+ this.blanks();
167
+ continue;
168
+ }
169
+ this.expect(close);
170
+ return;
171
+ }
172
+ }
173
+ call() {
174
+ this.expect("(");
175
+ this.blanks();
176
+ if (this.text[this.i] === ")") {
177
+ this.i++;
178
+ return;
179
+ }
180
+ while (true) {
181
+ this.value();
182
+ this.blanks();
183
+ if (this.text[this.i] === ",") {
184
+ this.i++;
185
+ this.blanks();
186
+ continue;
187
+ }
188
+ this.expect(")");
189
+ return;
190
+ }
191
+ }
192
+ regex() {
193
+ this.i++;
194
+ while (true) {
195
+ const c = this.text[this.i];
196
+ if (c === void 0 || c === "\n") throw new Fault(this.i);
197
+ if (c === "\\") {
198
+ this.i += 2;
199
+ continue;
200
+ }
201
+ this.i++;
202
+ if (c === "/") break;
203
+ }
204
+ while (this.i < this.text.length && /[a-z]/u.test(this.text[this.i])) this.i++;
205
+ }
206
+ document() {
207
+ this.blanks();
208
+ if (this.text[this.i] !== "{") throw new Fault(this.i);
209
+ const keys = [];
210
+ const spans = [];
211
+ let database = null;
212
+ this.i++;
213
+ this.blanks();
214
+ if (this.text[this.i] === "}") this.i++;
215
+ else while (true) {
216
+ const from = this.i;
217
+ const key = this.key();
218
+ keys.push(key);
219
+ spans.push({
220
+ from,
221
+ to: this.i
222
+ });
223
+ this.blanks();
224
+ this.expect(":");
225
+ this.blanks();
226
+ const value = this.value();
227
+ if (key === "$db" && typeof value === "string") database = value;
228
+ this.blanks();
229
+ if (this.text[this.i] === ",") {
230
+ this.i++;
231
+ this.blanks();
232
+ continue;
233
+ }
234
+ this.expect("}");
235
+ break;
236
+ }
237
+ this.blanks();
238
+ if (this.i < this.text.length) throw new Fault(this.i);
239
+ return {
240
+ keys,
241
+ spans,
242
+ database
243
+ };
244
+ }
245
+ };
246
+ function readDocument(text) {
247
+ if (text.trim() === "") return {
248
+ keys: [],
249
+ command: null,
250
+ database: null,
251
+ commandSpan: null,
252
+ refusal: "empty",
253
+ at: -1
254
+ };
255
+ try {
256
+ const { keys, spans, database } = new Reader(text).document();
257
+ const index = keys.findIndex((key) => key !== "$db");
258
+ return {
259
+ keys,
260
+ command: index === -1 ? null : keys[index],
261
+ database,
262
+ commandSpan: index === -1 ? null : spans[index],
263
+ refusal: index === -1 ? "no-command" : null,
264
+ at: -1
265
+ };
266
+ } catch (error) {
267
+ if (error instanceof Fault) return {
268
+ keys: [],
269
+ command: null,
270
+ database: null,
271
+ commandSpan: null,
272
+ refusal: "not-a-document",
273
+ at: error.at
274
+ };
275
+ throw error;
276
+ }
277
+ }
278
+ function runnableDocument(text, from, to) {
279
+ const [start, end] = from < to ? [from, to] : [0, text.length];
280
+ const span = text.slice(start, end);
281
+ const first = start + (span.length - span.trimStart().length);
282
+ const last = end - (span.length - span.trimEnd().length);
283
+ return first >= last ? { kind: "none" } : {
284
+ kind: "one",
285
+ text: text.slice(first, last),
286
+ from: first,
287
+ to: last
288
+ };
289
+ }
290
+ function pathOf(frames) {
291
+ const path = [];
292
+ const enclosing = frames.slice(0, -1);
293
+ for (const frame of enclosing) if (!frame.transparent && frame.key !== null) path.push(frame.key);
294
+ return path;
295
+ }
296
+ function atKey(frame) {
297
+ return frame !== void 0 && !frame.transparent && frame.expectingKey;
298
+ }
299
+ function valueKey(frame) {
300
+ return frame !== void 0 && !frame.transparent && !frame.valueStarted ? frame.key : null;
301
+ }
302
+ function keyContext(frames, top, typed, quoted) {
303
+ return {
304
+ kind: "key",
305
+ path: pathOf(frames),
306
+ siblings: top.seen,
307
+ typed,
308
+ quoted
309
+ };
310
+ }
311
+ function valueContext(frames, key, typed, quoted) {
312
+ return {
313
+ kind: "value",
314
+ path: pathOf(frames),
315
+ key,
316
+ typed,
317
+ quoted
318
+ };
319
+ }
320
+ function closingQuote(text, from, quote) {
321
+ let i = from;
322
+ while (i < text.length) {
323
+ if (text[i] === "\\") {
324
+ i += 2;
325
+ continue;
326
+ }
327
+ if (text[i] === quote) return i;
328
+ i++;
329
+ }
330
+ return -1;
331
+ }
332
+ function structure(frames, top, c, pending) {
333
+ if (c === "{" || c === "[" || c === "(") {
334
+ if (top !== void 0) top.valueStarted = true;
335
+ frames.push({
336
+ transparent: c !== "{",
337
+ key: null,
338
+ expectingKey: c === "{",
339
+ valueStarted: false,
340
+ seen: []
341
+ });
342
+ return;
343
+ }
344
+ if (c === "}" || c === "]" || c === ")") {
345
+ frames.pop();
346
+ return;
347
+ }
348
+ if (top === void 0) return;
349
+ if (!top.transparent && c === ":") {
350
+ top.key = pending;
351
+ top.expectingKey = false;
352
+ top.valueStarted = false;
353
+ top.seen.push(pending);
354
+ return;
355
+ }
356
+ if (!top.transparent && c === ",") {
357
+ top.key = null;
358
+ top.expectingKey = true;
359
+ top.valueStarted = false;
360
+ return;
361
+ }
362
+ if (!/\s/u.test(c)) top.valueStarted = true;
363
+ }
364
+ function caretContext(text, offset) {
365
+ const body = text.slice(0, offset);
366
+ const frames = [];
367
+ let pending = "";
368
+ let i = 0;
369
+ while (i < body.length) {
370
+ const c = body[i];
371
+ const top = frames.at(-1);
372
+ if (c === "\"" || c === "'") {
373
+ const end = closingQuote(body, i + 1, c);
374
+ if (end === -1) {
375
+ const typed = body.slice(i + 1);
376
+ if (top !== void 0 && atKey(top)) return keyContext(frames, top, typed, true);
377
+ const key = valueKey(top);
378
+ return key === null ? { kind: "none" } : valueContext(frames, key, typed, true);
379
+ }
380
+ if (atKey(top)) pending = body.slice(i + 1, end);
381
+ else if (top !== void 0) top.valueStarted = true;
382
+ i = end + 1;
383
+ continue;
384
+ }
385
+ IDENTIFIER.lastIndex = i;
386
+ const word = /[A-Za-z_$]/u.test(c) ? IDENTIFIER.exec(body) : null;
387
+ if (word !== null) {
388
+ const typed = word[0];
389
+ i += typed.length;
390
+ if (i >= body.length) {
391
+ if (top !== void 0 && atKey(top)) return keyContext(frames, top, typed, false);
392
+ const key = valueKey(top);
393
+ return key === null ? { kind: "none" } : valueContext(frames, key, typed, false);
394
+ }
395
+ if (atKey(top)) pending = typed;
396
+ else if (top !== void 0) top.valueStarted = true;
397
+ continue;
398
+ }
399
+ structure(frames, top, c, pending);
400
+ i++;
401
+ }
402
+ const top = frames.at(-1);
403
+ if (top !== void 0 && atKey(top)) return keyContext(frames, top, "", false);
404
+ const key = valueKey(top);
405
+ return key === null ? { kind: "none" } : valueContext(frames, key, "", false);
406
+ }
407
+ //#endregion
408
+ export { runnableDocument as a, readDocument as i, LITERALS as n, caretContext as r, CONSTRUCTORS as t };
@@ -0,0 +1,2 @@
1
+ import { C as PUSH_MODIFIERS, D as keysAt, E as UPDATE_OPERATORS, S as MongoCommand, T as STAGES, _ as MongodbHoverCard, a as CaretContext, b as FIELD_OPERATORS, c as LITERALS, d as readDocument, f as runnableDocument, g as MongodbCompletionCandidate, h as MongodbCatalog, i as CONSTRUCTORS, l as RunnableDocument, m as MongodbAssist, n as MongodbMarkerCode, o as DocumentRead, p as CatalogCollection, r as documentMarkers, s as DocumentRefusal, t as MongodbMarker, u as caretContext, v as createMongodbAssist, w as QUERY_OPERATORS, x as MONGO_COMMANDS, y as EXPRESSION_OPERATORS } from "./markers-CQOm7diu.js";
2
+ export { CONSTRUCTORS, type CaretContext, type CatalogCollection, type DocumentRead, type DocumentRefusal, EXPRESSION_OPERATORS, FIELD_OPERATORS, LITERALS, MONGO_COMMANDS, type MongoCommand, type MongodbAssist, type MongodbCatalog, type MongodbCompletionCandidate, type MongodbHoverCard, type MongodbMarker, type MongodbMarkerCode, PUSH_MODIFIERS, QUERY_OPERATORS, type RunnableDocument, STAGES, UPDATE_OPERATORS, caretContext, createMongodbAssist, documentMarkers, keysAt, readDocument, runnableDocument };
package/dist/index.js ADDED
@@ -0,0 +1,90 @@
1
+ import { a as PUSH_MODIFIERS, c as UPDATE_OPERATORS, i as MONGO_COMMANDS, l as keysAt, n as EXPRESSION_OPERATORS, o as QUERY_OPERATORS, r as FIELD_OPERATORS, s as STAGES, t as documentMarkers } from "./markers-Bdvbut2O.js";
2
+ import { a as runnableDocument, i as readDocument, n as LITERALS, r as caretContext, t as CONSTRUCTORS } from "./document-CVB9YunA.js";
3
+ //#region src/assist.ts
4
+ const NO_CATALOG = { collections: () => [] };
5
+ function documentationOf(command) {
6
+ return command.fields.length === 0 ? command.summary : `${command.summary}\n\nFields: ${command.fields.join(", ")}`;
7
+ }
8
+ function wordAt(text, offset) {
9
+ let from = offset;
10
+ let to = offset;
11
+ while (from > 0 && /[\w$]/u.test(text[from - 1])) from--;
12
+ while (to < text.length && /[\w$]/u.test(text[to])) to++;
13
+ return from === to ? null : {
14
+ word: text.slice(from, to),
15
+ from,
16
+ to
17
+ };
18
+ }
19
+ function createMongodbAssist(catalog = NO_CATALOG, commands = MONGO_COMMANDS) {
20
+ const byName = new Map(commands.map((command) => [command.name, command]));
21
+ return {
22
+ complete: (text, offset) => {
23
+ const context = caretContext(text, offset);
24
+ if (context.kind === "key") {
25
+ const named = context.path.length === 0 ? context.siblings.find((key) => key !== "$db") : void 0;
26
+ const commanding = context.path.length === 0 && (named === void 0 || !byName.has(named));
27
+ const staged = context.path.at(-1) === "pipeline";
28
+ return keysAt(context.path, context.siblings, commands).map((key) => {
29
+ const command = commanding && key !== "$db" ? byName.get(key) : void 0;
30
+ const kind = command === void 0 ? key.startsWith("$") && key !== "$db" ? staged ? "stage" : "operator" : "field" : "command";
31
+ return {
32
+ label: key,
33
+ kind,
34
+ insertText: context.quoted ? key : `"${key}": $0`,
35
+ ...!context.quoted && { snippet: true },
36
+ replace: context.typed.length,
37
+ ...named !== void 0 && !commanding && { detail: named },
38
+ ...command !== void 0 && { documentation: documentationOf(command) },
39
+ sortGroup: kind === "command" || kind === "field" ? 0 : 1
40
+ };
41
+ });
42
+ }
43
+ if (context.kind !== "value" || context.path.length > 0) return [];
44
+ const listed = catalog.collections();
45
+ const quote = (name) => context.quoted ? name : `"${name}"`;
46
+ if (context.key === "$db") return [...new Set(listed.map((collection) => collection.database))].map((database) => {
47
+ return {
48
+ label: database,
49
+ kind: "database",
50
+ insertText: quote(database),
51
+ replace: context.typed.length,
52
+ sortGroup: 0
53
+ };
54
+ });
55
+ const command = byName.get(context.key);
56
+ if (command === void 0 || !command.collection) return [];
57
+ const databases = new Set(listed.map((collection) => collection.database));
58
+ const seen = /* @__PURE__ */ new Set();
59
+ const candidates = [];
60
+ for (const collection of listed) {
61
+ if (seen.has(collection.name)) continue;
62
+ seen.add(collection.name);
63
+ candidates.push({
64
+ label: collection.name,
65
+ kind: "collection",
66
+ insertText: quote(collection.name),
67
+ replace: context.typed.length,
68
+ ...databases.size > 1 && { detail: collection.database },
69
+ sortGroup: 0
70
+ });
71
+ }
72
+ return candidates;
73
+ },
74
+ hover: (text, offset) => {
75
+ const at = wordAt(text, offset);
76
+ if (at === null) return null;
77
+ const command = byName.get(at.word);
78
+ if (command === void 0) return null;
79
+ const context = caretContext(text, at.from);
80
+ if (context.kind !== "key" || context.path.length > 0) return null;
81
+ return {
82
+ from: at.from,
83
+ to: at.to,
84
+ markdown: [`**${command.name}**`, documentationOf(command)]
85
+ };
86
+ }
87
+ };
88
+ }
89
+ //#endregion
90
+ export { CONSTRUCTORS, EXPRESSION_OPERATORS, FIELD_OPERATORS, LITERALS, MONGO_COMMANDS, PUSH_MODIFIERS, QUERY_OPERATORS, STAGES, UPDATE_OPERATORS, caretContext, createMongodbAssist, documentMarkers, keysAt, readDocument, runnableDocument };