@documentdb-js/shell-runtime 0.8.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/LICENSE.md +21 -0
- package/README.md +94 -0
- package/dist/CommandInterceptor.d.ts +55 -0
- package/dist/CommandInterceptor.d.ts.map +1 -0
- package/dist/CommandInterceptor.js +87 -0
- package/dist/CommandInterceptor.js.map +1 -0
- package/dist/CommandInterceptor.test.d.ts +2 -0
- package/dist/CommandInterceptor.test.d.ts.map +1 -0
- package/dist/CommandInterceptor.test.js +195 -0
- package/dist/CommandInterceptor.test.js.map +1 -0
- package/dist/DocumentDBServiceProvider.d.ts +48 -0
- package/dist/DocumentDBServiceProvider.d.ts.map +1 -0
- package/dist/DocumentDBServiceProvider.js +59 -0
- package/dist/DocumentDBServiceProvider.js.map +1 -0
- package/dist/DocumentDBShellRuntime.d.ts +144 -0
- package/dist/DocumentDBShellRuntime.d.ts.map +1 -0
- package/dist/DocumentDBShellRuntime.js +521 -0
- package/dist/DocumentDBShellRuntime.js.map +1 -0
- package/dist/DocumentDBShellRuntime.test.d.ts +2 -0
- package/dist/DocumentDBShellRuntime.test.d.ts.map +1 -0
- package/dist/DocumentDBShellRuntime.test.js +200 -0
- package/dist/DocumentDBShellRuntime.test.js.map +1 -0
- package/dist/HelpProvider.d.ts +45 -0
- package/dist/HelpProvider.d.ts.map +1 -0
- package/dist/HelpProvider.js +179 -0
- package/dist/HelpProvider.js.map +1 -0
- package/dist/HelpProvider.test.d.ts +2 -0
- package/dist/HelpProvider.test.d.ts.map +1 -0
- package/dist/HelpProvider.test.js +133 -0
- package/dist/HelpProvider.test.js.map +1 -0
- package/dist/ResultTransformer.d.ts +58 -0
- package/dist/ResultTransformer.d.ts.map +1 -0
- package/dist/ResultTransformer.js +96 -0
- package/dist/ResultTransformer.js.map +1 -0
- package/dist/ResultTransformer.test.d.ts +2 -0
- package/dist/ResultTransformer.test.d.ts.map +1 -0
- package/dist/ResultTransformer.test.js +166 -0
- package/dist/ResultTransformer.test.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +65 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/package.json +30 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { type MongoClient } from 'mongodb';
|
|
2
|
+
import { type ShellEvalOptions, type ShellEvaluationResult, type ShellRuntimeCallbacks, type ShellRuntimeOptions } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Shell runtime abstraction for DocumentDB.
|
|
5
|
+
*
|
|
6
|
+
* Wraps the @mongosh evaluation pipeline behind a clean API that both the
|
|
7
|
+
* query playground (scratchpad) and future interactive shell (Step 9) consume.
|
|
8
|
+
*
|
|
9
|
+
* The runtime:
|
|
10
|
+
* - Intercepts known commands (help) before they reach @mongosh
|
|
11
|
+
* - Creates a fresh @mongosh evaluation context per `evaluate()` call
|
|
12
|
+
* - Transforms raw @mongosh ShellResult into a protocol-agnostic result type
|
|
13
|
+
* - Delegates to `DocumentDBServiceProvider` for database operations
|
|
14
|
+
*
|
|
15
|
+
* The caller owns the `MongoClient` lifecycle — the runtime only uses it.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* const runtime = new DocumentDBShellRuntime(mongoClient, {
|
|
20
|
+
* onConsoleOutput: (output) => console.log(output),
|
|
21
|
+
* onLog: (level, msg) => logger[level](msg),
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* const result = await runtime.evaluate('db.users.find({})', 'myDatabase');
|
|
25
|
+
* console.log(result.type, result.printable);
|
|
26
|
+
*
|
|
27
|
+
* runtime.dispose();
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare class DocumentDBShellRuntime {
|
|
31
|
+
private readonly _mongoClient;
|
|
32
|
+
private readonly _callbacks;
|
|
33
|
+
private readonly _options;
|
|
34
|
+
private readonly _commandInterceptor;
|
|
35
|
+
private readonly _resultTransformer;
|
|
36
|
+
private _disposed;
|
|
37
|
+
private _persistent;
|
|
38
|
+
constructor(mongoClient: MongoClient, callbacks?: ShellRuntimeCallbacks, options?: ShellRuntimeOptions);
|
|
39
|
+
/**
|
|
40
|
+
* Evaluate shell code against the specified database.
|
|
41
|
+
*
|
|
42
|
+
* Creates a fresh @mongosh context per call — no variable leakage between
|
|
43
|
+
* evaluations. The target database is pre-selected via `use()` before
|
|
44
|
+
* executing user code.
|
|
45
|
+
*
|
|
46
|
+
* @param code - JavaScript/shell code string to evaluate
|
|
47
|
+
* @param databaseName - Target database name for execution
|
|
48
|
+
* @param evalOptions - Per-eval overrides (e.g. displayBatchSize from user settings)
|
|
49
|
+
* @returns Evaluation result with type, printable value, and timing
|
|
50
|
+
* @throws Error if the runtime has been disposed
|
|
51
|
+
* @throws Error if @mongosh evaluation fails (syntax error, runtime error, etc.)
|
|
52
|
+
*/
|
|
53
|
+
evaluate(code: string, databaseName: string, evalOptions?: ShellEvalOptions): Promise<ShellEvaluationResult>;
|
|
54
|
+
/**
|
|
55
|
+
* Fresh-context evaluation (playground mode).
|
|
56
|
+
* Creates a new @mongosh context per call — no variable leakage between evaluations.
|
|
57
|
+
*/
|
|
58
|
+
private evaluateFresh;
|
|
59
|
+
/**
|
|
60
|
+
* Persistent-context evaluation (interactive shell mode).
|
|
61
|
+
* Reuses the same @mongosh context across calls — variables, cursor state,
|
|
62
|
+
* and the `db` reference persist between evaluations.
|
|
63
|
+
*/
|
|
64
|
+
private evaluatePersistent;
|
|
65
|
+
/**
|
|
66
|
+
* Dispose the runtime. After disposal, `evaluate()` calls will throw.
|
|
67
|
+
* Does NOT close the MongoClient — the caller owns its lifecycle.
|
|
68
|
+
*/
|
|
69
|
+
dispose(): void;
|
|
70
|
+
/**
|
|
71
|
+
* Apply the display batch size to the instance state.
|
|
72
|
+
* Uses @mongosh's displayBatchSizeFromDBQuery property which takes
|
|
73
|
+
* precedence over config.get('displayBatchSize') in cursor iteration.
|
|
74
|
+
*/
|
|
75
|
+
private applyBatchSize;
|
|
76
|
+
/**
|
|
77
|
+
* Register the console output listener on the @mongosh instance state.
|
|
78
|
+
* Routes `print()`, `printjson()`, and `console.log()` output to the
|
|
79
|
+
* caller-provided callback.
|
|
80
|
+
*/
|
|
81
|
+
private registerConsoleOutputListener;
|
|
82
|
+
private log;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Rewrites bare `use <name>` / `show <name>` direct shell commands into
|
|
86
|
+
* function-call form (`use("<name>");` / `show("<name>");`).
|
|
87
|
+
*
|
|
88
|
+
* ## Why this is needed
|
|
89
|
+
*
|
|
90
|
+
* Direct shell commands like `use mydb` are detected by the evaluation
|
|
91
|
+
* pipeline as special tokens. When they appear as the first line of a
|
|
92
|
+
* multi-line code block, the pipeline processes only the direct command
|
|
93
|
+
* and silently discards all subsequent statements. Converting them to
|
|
94
|
+
* function-call form bypasses that short-circuit so the entire block is
|
|
95
|
+
* evaluated normally.
|
|
96
|
+
*
|
|
97
|
+
* ## How it works
|
|
98
|
+
*
|
|
99
|
+
* A single linear scan of the input tracks whether each character sits in
|
|
100
|
+
* plain code, a `//` line comment, a `/* ... *\/` block comment, a `'...'`
|
|
101
|
+
* or `"..."` string literal, a `` `...` `` template literal (including
|
|
102
|
+
* `${...}` expression nesting), or a `/.../` regex literal. Only line
|
|
103
|
+
* starts that fall in the plain-code state are considered candidates for
|
|
104
|
+
* rewriting. The per-line regex extracts the argument once the context
|
|
105
|
+
* check has passed.
|
|
106
|
+
*
|
|
107
|
+
* This avoids the collateral rewrites a naive regex would produce in:
|
|
108
|
+
*
|
|
109
|
+
* - single- and double-quoted string literals
|
|
110
|
+
* - template literals (`` `use mydb` `` as string content)
|
|
111
|
+
* - line and block comments
|
|
112
|
+
* - regular-expression literals (`/use mydb/`)
|
|
113
|
+
*
|
|
114
|
+
* ### Scope note — why a scanner, not a parser
|
|
115
|
+
*
|
|
116
|
+
* This is a lexical scanner, not a syntactic parser. It recognizes the
|
|
117
|
+
* literal forms listed above, which covers every collateral-rewrite
|
|
118
|
+
* failure mode reported so far. It intentionally does **not** understand
|
|
119
|
+
* JavaScript statement structure, so a handful of exotic cases are not
|
|
120
|
+
* distinguished:
|
|
121
|
+
*
|
|
122
|
+
* - `use` / `show` as a declared identifier rather than a statement
|
|
123
|
+
* starter (e.g. `const use = mydb; use` — runtime-invalid anyway).
|
|
124
|
+
* - `use mydb` nested inside a block such as `if (x) { use mydb }`
|
|
125
|
+
* (the bare form was never valid inside a block either; rewriting it
|
|
126
|
+
* to `use("mydb");` is still a legal expression statement).
|
|
127
|
+
* - Contextual keywords used where a regex is legal but the scanner
|
|
128
|
+
* guessed division, or vice-versa.
|
|
129
|
+
*
|
|
130
|
+
* Getting 100% of these right would require a real JS parser (e.g.
|
|
131
|
+
* `acorn` / `acorn-loose`) or the TypeScript compiler. We deliberately
|
|
132
|
+
* avoid adding that dependency: `@documentdb-js/shell-runtime`
|
|
133
|
+
* is intended to also ship as a lightweight standalone runtime for CLI
|
|
134
|
+
* tooling, and pulling in a full JS parser would dominate its footprint
|
|
135
|
+
* for an edge case that is not observed in real user input.
|
|
136
|
+
*
|
|
137
|
+
* ## ASI safety
|
|
138
|
+
*
|
|
139
|
+
* The emitted replacement always ends with `;` so a following line that
|
|
140
|
+
* begins with `[`, `(`, `+`, `-`, or `/` starts a fresh statement instead
|
|
141
|
+
* of binding to the call expression.
|
|
142
|
+
*/
|
|
143
|
+
export declare function normalizeDirectCommands(code: string): string;
|
|
144
|
+
//# sourceMappingURL=DocumentDBShellRuntime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DocumentDBShellRuntime.d.ts","sourceRoot":"","sources":["../src/DocumentDBShellRuntime.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAM3C,OAAO,EACH,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EAC3B,MAAM,SAAS,CAAC;AAgBjB;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,qBAAa,sBAAsB;IAC/B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAc;IAC3C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAwB;IACnD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAsB;IAC/C,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAqB;IACzD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAoB;IACvD,OAAO,CAAC,SAAS,CAAS;IAG1B,OAAO,CAAC,WAAW,CAOH;gBAEJ,WAAW,EAAE,WAAW,EAAE,SAAS,CAAC,EAAE,qBAAqB,EAAE,OAAO,CAAC,EAAE,mBAAmB;IAStG;;;;;;;;;;;;;OAaG;IACG,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAgClH;;;OAGG;YACW,aAAa;IAoD3B;;;;OAIG;YACW,kBAAkB;IAiEhC;;;OAGG;IACH,OAAO,IAAI,IAAI;IAKf;;;;OAIG;IACH,OAAO,CAAC,cAAc;IAMtB;;;;OAIG;IACH,OAAO,CAAC,6BAA6B;IAyBrC,OAAO,CAAC,GAAG;CAGd;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA6C5D"}
|
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*---------------------------------------------------------------------------------------------
|
|
3
|
+
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
4
|
+
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
5
|
+
*--------------------------------------------------------------------------------------------*/
|
|
6
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
|
+
};
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.DocumentDBShellRuntime = void 0;
|
|
11
|
+
exports.normalizeDirectCommands = normalizeDirectCommands;
|
|
12
|
+
const shell_api_1 = require("@mongosh/shell-api");
|
|
13
|
+
const shell_evaluator_1 = require("@mongosh/shell-evaluator");
|
|
14
|
+
const vm_1 = __importDefault(require("vm"));
|
|
15
|
+
const CommandInterceptor_1 = require("./CommandInterceptor");
|
|
16
|
+
const DocumentDBServiceProvider_1 = require("./DocumentDBServiceProvider");
|
|
17
|
+
const HelpProvider_1 = require("./HelpProvider");
|
|
18
|
+
const ResultTransformer_1 = require("./ResultTransformer");
|
|
19
|
+
/**
|
|
20
|
+
* Matches `<cmd> <arg>` on a single line. Used to extract the argument text
|
|
21
|
+
* after the scanner has already confirmed that `<cmd>` starts a real top-level
|
|
22
|
+
* `use`/`show` statement (not inside a string, comment, or regex literal).
|
|
23
|
+
*/
|
|
24
|
+
const DIRECT_COMMAND_LINE_RE = /^(\s*)(use|show)\s+([^(\s][^;]*?)\s*;?\s*$/;
|
|
25
|
+
const DEFAULT_OPTIONS = {
|
|
26
|
+
productName: 'DocumentDB for VS Code',
|
|
27
|
+
productDocsLink: 'https://github.com/microsoft/vscode-documentdb',
|
|
28
|
+
displayBatchSize: 50,
|
|
29
|
+
persistent: false,
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Shell runtime abstraction for DocumentDB.
|
|
33
|
+
*
|
|
34
|
+
* Wraps the @mongosh evaluation pipeline behind a clean API that both the
|
|
35
|
+
* query playground (scratchpad) and future interactive shell (Step 9) consume.
|
|
36
|
+
*
|
|
37
|
+
* The runtime:
|
|
38
|
+
* - Intercepts known commands (help) before they reach @mongosh
|
|
39
|
+
* - Creates a fresh @mongosh evaluation context per `evaluate()` call
|
|
40
|
+
* - Transforms raw @mongosh ShellResult into a protocol-agnostic result type
|
|
41
|
+
* - Delegates to `DocumentDBServiceProvider` for database operations
|
|
42
|
+
*
|
|
43
|
+
* The caller owns the `MongoClient` lifecycle — the runtime only uses it.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* const runtime = new DocumentDBShellRuntime(mongoClient, {
|
|
48
|
+
* onConsoleOutput: (output) => console.log(output),
|
|
49
|
+
* onLog: (level, msg) => logger[level](msg),
|
|
50
|
+
* });
|
|
51
|
+
*
|
|
52
|
+
* const result = await runtime.evaluate('db.users.find({})', 'myDatabase');
|
|
53
|
+
* console.log(result.type, result.printable);
|
|
54
|
+
*
|
|
55
|
+
* runtime.dispose();
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
class DocumentDBShellRuntime {
|
|
59
|
+
_mongoClient;
|
|
60
|
+
_callbacks;
|
|
61
|
+
_options;
|
|
62
|
+
_commandInterceptor;
|
|
63
|
+
_resultTransformer;
|
|
64
|
+
_disposed = false;
|
|
65
|
+
// Persistent mode state — reused across evaluate() calls when options.persistent is true
|
|
66
|
+
_persistent;
|
|
67
|
+
constructor(mongoClient, callbacks, options) {
|
|
68
|
+
this._mongoClient = mongoClient;
|
|
69
|
+
this._callbacks = callbacks ?? {};
|
|
70
|
+
this._options = { ...DEFAULT_OPTIONS, ...options };
|
|
71
|
+
const helpSurface = this._options.persistent ? 'shell' : 'playground';
|
|
72
|
+
this._commandInterceptor = new CommandInterceptor_1.CommandInterceptor(new HelpProvider_1.HelpProvider(helpSurface));
|
|
73
|
+
this._resultTransformer = new ResultTransformer_1.ResultTransformer();
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Evaluate shell code against the specified database.
|
|
77
|
+
*
|
|
78
|
+
* Creates a fresh @mongosh context per call — no variable leakage between
|
|
79
|
+
* evaluations. The target database is pre-selected via `use()` before
|
|
80
|
+
* executing user code.
|
|
81
|
+
*
|
|
82
|
+
* @param code - JavaScript/shell code string to evaluate
|
|
83
|
+
* @param databaseName - Target database name for execution
|
|
84
|
+
* @param evalOptions - Per-eval overrides (e.g. displayBatchSize from user settings)
|
|
85
|
+
* @returns Evaluation result with type, printable value, and timing
|
|
86
|
+
* @throws Error if the runtime has been disposed
|
|
87
|
+
* @throws Error if @mongosh evaluation fails (syntax error, runtime error, etc.)
|
|
88
|
+
*/
|
|
89
|
+
async evaluate(code, databaseName, evalOptions) {
|
|
90
|
+
if (this._disposed) {
|
|
91
|
+
throw new Error('Shell runtime has been disposed');
|
|
92
|
+
}
|
|
93
|
+
// Check for intercepted commands (help, etc.)
|
|
94
|
+
const intercepted = this._commandInterceptor.tryIntercept(code);
|
|
95
|
+
if (intercepted) {
|
|
96
|
+
return intercepted;
|
|
97
|
+
}
|
|
98
|
+
// Normalize bare direct commands (`use dbName`, `show dbs`) into function-call
|
|
99
|
+
// form (`use("dbName")`, `show("dbs")`) so they go through the async rewriter
|
|
100
|
+
// instead of short-circuiting. Without this, a bare `use` as the first token
|
|
101
|
+
// of a multi-line block consumes the entire input and silently drops subsequent
|
|
102
|
+
// statements.
|
|
103
|
+
code = normalizeDirectCommands(code);
|
|
104
|
+
this.log('trace', `Evaluating code (${code.split('\n').length} lines, ${code.length} chars, db: ${databaseName})`);
|
|
105
|
+
const startTime = Date.now();
|
|
106
|
+
if (this._options.persistent) {
|
|
107
|
+
return this.evaluatePersistent(code, databaseName, evalOptions, startTime);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
return this.evaluateFresh(code, databaseName, evalOptions, startTime);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Fresh-context evaluation (playground mode).
|
|
115
|
+
* Creates a new @mongosh context per call — no variable leakage between evaluations.
|
|
116
|
+
*/
|
|
117
|
+
async evaluateFresh(code, databaseName, evalOptions, startTime) {
|
|
118
|
+
// Create fresh shell context per execution
|
|
119
|
+
const { serviceProvider, bus } = DocumentDBServiceProvider_1.DocumentDBServiceProvider.createForDocumentDB(this._mongoClient, this._options.productName, this._options.productDocsLink);
|
|
120
|
+
const instanceState = new shell_api_1.ShellInstanceState(serviceProvider, bus);
|
|
121
|
+
try {
|
|
122
|
+
const evaluator = new shell_evaluator_1.ShellEvaluator(instanceState);
|
|
123
|
+
this.applyBatchSize(instanceState, evalOptions);
|
|
124
|
+
this.registerConsoleOutputListener(instanceState);
|
|
125
|
+
// Set up eval context with shell globals (db, ObjectId, ISODate, etc.)
|
|
126
|
+
const context = {};
|
|
127
|
+
instanceState.setCtx(context);
|
|
128
|
+
// Custom eval function using vm.runInContext for sandboxed execution
|
|
129
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
130
|
+
const customEvalFn = async (evalCode, ctx) => {
|
|
131
|
+
const vmContext = vm_1.default.isContext(ctx) ? ctx : vm_1.default.createContext(ctx);
|
|
132
|
+
return vm_1.default.runInContext(evalCode, vmContext);
|
|
133
|
+
};
|
|
134
|
+
// Pre-select the target database (fresh context each time)
|
|
135
|
+
await evaluator.customEval(customEvalFn, `use(${JSON.stringify(databaseName)})`, context, 'playground');
|
|
136
|
+
// Evaluate user code
|
|
137
|
+
const result = await evaluator.customEval(customEvalFn, code, context, 'playground');
|
|
138
|
+
const durationMs = Date.now() - startTime;
|
|
139
|
+
this.log('trace', `Evaluation complete (${durationMs}ms)`);
|
|
140
|
+
return this._resultTransformer.transform(result, durationMs);
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
await instanceState.close();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Persistent-context evaluation (interactive shell mode).
|
|
148
|
+
* Reuses the same @mongosh context across calls — variables, cursor state,
|
|
149
|
+
* and the `db` reference persist between evaluations.
|
|
150
|
+
*/
|
|
151
|
+
async evaluatePersistent(code, databaseName, evalOptions, startTime) {
|
|
152
|
+
// Initialize persistent state on first call
|
|
153
|
+
if (!this._persistent) {
|
|
154
|
+
const { serviceProvider, bus } = DocumentDBServiceProvider_1.DocumentDBServiceProvider.createForDocumentDB(this._mongoClient, this._options.productName, this._options.productDocsLink);
|
|
155
|
+
const instanceState = new shell_api_1.ShellInstanceState(serviceProvider, bus);
|
|
156
|
+
const evaluator = new shell_evaluator_1.ShellEvaluator(instanceState);
|
|
157
|
+
const context = {};
|
|
158
|
+
instanceState.setCtx(context);
|
|
159
|
+
const vmContext = vm_1.default.createContext(context);
|
|
160
|
+
this.registerConsoleOutputListener(instanceState);
|
|
161
|
+
// Pre-select the initial database
|
|
162
|
+
await evaluator.customEval(
|
|
163
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
164
|
+
async (evalCode, _ctx) => {
|
|
165
|
+
return vm_1.default.runInContext(evalCode, vmContext);
|
|
166
|
+
}, `use(${JSON.stringify(databaseName)})`, context, 'shell');
|
|
167
|
+
this._persistent = { instanceState, evaluator, context, vmContext };
|
|
168
|
+
}
|
|
169
|
+
const { instanceState, evaluator, context, vmContext } = this._persistent;
|
|
170
|
+
// Apply batch size per-eval (may change between evaluations via settings)
|
|
171
|
+
this.applyBatchSize(instanceState, evalOptions);
|
|
172
|
+
// Evaluate user code using the persistent context
|
|
173
|
+
const result = await evaluator.customEval(
|
|
174
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
175
|
+
async (evalCode, _ctx) => {
|
|
176
|
+
return vm_1.default.runInContext(evalCode, vmContext);
|
|
177
|
+
}, code, context, 'shell');
|
|
178
|
+
const durationMs = Date.now() - startTime;
|
|
179
|
+
this.log('trace', `Evaluation complete (${durationMs}ms)`);
|
|
180
|
+
return this._resultTransformer.transform(result, durationMs);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Dispose the runtime. After disposal, `evaluate()` calls will throw.
|
|
184
|
+
* Does NOT close the MongoClient — the caller owns its lifecycle.
|
|
185
|
+
*/
|
|
186
|
+
dispose() {
|
|
187
|
+
this._disposed = true;
|
|
188
|
+
this._persistent = undefined;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Apply the display batch size to the instance state.
|
|
192
|
+
* Uses @mongosh's displayBatchSizeFromDBQuery property which takes
|
|
193
|
+
* precedence over config.get('displayBatchSize') in cursor iteration.
|
|
194
|
+
*/
|
|
195
|
+
applyBatchSize(instanceState, evalOptions) {
|
|
196
|
+
const batchSize = evalOptions?.displayBatchSize ?? this._options.displayBatchSize ?? DEFAULT_OPTIONS.displayBatchSize;
|
|
197
|
+
instanceState.displayBatchSizeFromDBQuery = batchSize;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Register the console output listener on the @mongosh instance state.
|
|
201
|
+
* Routes `print()`, `printjson()`, and `console.log()` output to the
|
|
202
|
+
* caller-provided callback.
|
|
203
|
+
*/
|
|
204
|
+
registerConsoleOutputListener(instanceState) {
|
|
205
|
+
const onConsoleOutput = this._callbacks.onConsoleOutput;
|
|
206
|
+
if (!onConsoleOutput) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
instanceState.setEvaluationListener({
|
|
210
|
+
onPrint(values, _type) {
|
|
211
|
+
const output = values
|
|
212
|
+
.map((v) => {
|
|
213
|
+
if (typeof v.printable === 'string') {
|
|
214
|
+
return v.printable;
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
return JSON.stringify(v.printable, null, 2);
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return String(v.printable);
|
|
221
|
+
}
|
|
222
|
+
})
|
|
223
|
+
.join(' ');
|
|
224
|
+
onConsoleOutput(output + '\n');
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
log(level, message) {
|
|
229
|
+
this._callbacks.onLog?.(level, message);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
exports.DocumentDBShellRuntime = DocumentDBShellRuntime;
|
|
233
|
+
/**
|
|
234
|
+
* Rewrites bare `use <name>` / `show <name>` direct shell commands into
|
|
235
|
+
* function-call form (`use("<name>");` / `show("<name>");`).
|
|
236
|
+
*
|
|
237
|
+
* ## Why this is needed
|
|
238
|
+
*
|
|
239
|
+
* Direct shell commands like `use mydb` are detected by the evaluation
|
|
240
|
+
* pipeline as special tokens. When they appear as the first line of a
|
|
241
|
+
* multi-line code block, the pipeline processes only the direct command
|
|
242
|
+
* and silently discards all subsequent statements. Converting them to
|
|
243
|
+
* function-call form bypasses that short-circuit so the entire block is
|
|
244
|
+
* evaluated normally.
|
|
245
|
+
*
|
|
246
|
+
* ## How it works
|
|
247
|
+
*
|
|
248
|
+
* A single linear scan of the input tracks whether each character sits in
|
|
249
|
+
* plain code, a `//` line comment, a `/* ... *\/` block comment, a `'...'`
|
|
250
|
+
* or `"..."` string literal, a `` `...` `` template literal (including
|
|
251
|
+
* `${...}` expression nesting), or a `/.../` regex literal. Only line
|
|
252
|
+
* starts that fall in the plain-code state are considered candidates for
|
|
253
|
+
* rewriting. The per-line regex extracts the argument once the context
|
|
254
|
+
* check has passed.
|
|
255
|
+
*
|
|
256
|
+
* This avoids the collateral rewrites a naive regex would produce in:
|
|
257
|
+
*
|
|
258
|
+
* - single- and double-quoted string literals
|
|
259
|
+
* - template literals (`` `use mydb` `` as string content)
|
|
260
|
+
* - line and block comments
|
|
261
|
+
* - regular-expression literals (`/use mydb/`)
|
|
262
|
+
*
|
|
263
|
+
* ### Scope note — why a scanner, not a parser
|
|
264
|
+
*
|
|
265
|
+
* This is a lexical scanner, not a syntactic parser. It recognizes the
|
|
266
|
+
* literal forms listed above, which covers every collateral-rewrite
|
|
267
|
+
* failure mode reported so far. It intentionally does **not** understand
|
|
268
|
+
* JavaScript statement structure, so a handful of exotic cases are not
|
|
269
|
+
* distinguished:
|
|
270
|
+
*
|
|
271
|
+
* - `use` / `show` as a declared identifier rather than a statement
|
|
272
|
+
* starter (e.g. `const use = mydb; use` — runtime-invalid anyway).
|
|
273
|
+
* - `use mydb` nested inside a block such as `if (x) { use mydb }`
|
|
274
|
+
* (the bare form was never valid inside a block either; rewriting it
|
|
275
|
+
* to `use("mydb");` is still a legal expression statement).
|
|
276
|
+
* - Contextual keywords used where a regex is legal but the scanner
|
|
277
|
+
* guessed division, or vice-versa.
|
|
278
|
+
*
|
|
279
|
+
* Getting 100% of these right would require a real JS parser (e.g.
|
|
280
|
+
* `acorn` / `acorn-loose`) or the TypeScript compiler. We deliberately
|
|
281
|
+
* avoid adding that dependency: `@documentdb-js/shell-runtime`
|
|
282
|
+
* is intended to also ship as a lightweight standalone runtime for CLI
|
|
283
|
+
* tooling, and pulling in a full JS parser would dominate its footprint
|
|
284
|
+
* for an edge case that is not observed in real user input.
|
|
285
|
+
*
|
|
286
|
+
* ## ASI safety
|
|
287
|
+
*
|
|
288
|
+
* The emitted replacement always ends with `;` so a following line that
|
|
289
|
+
* begins with `[`, `(`, `+`, `-`, or `/` starts a fresh statement instead
|
|
290
|
+
* of binding to the call expression.
|
|
291
|
+
*/
|
|
292
|
+
function normalizeDirectCommands(code) {
|
|
293
|
+
if (!code.includes('\n')) {
|
|
294
|
+
return code;
|
|
295
|
+
}
|
|
296
|
+
// Cheap early exit: if neither token appears anywhere, skip scanning.
|
|
297
|
+
if (!/\b(use|show)\b/.test(code)) {
|
|
298
|
+
return code;
|
|
299
|
+
}
|
|
300
|
+
const candidateLineStarts = findCodeLineStarts(code);
|
|
301
|
+
if (candidateLineStarts.length === 0) {
|
|
302
|
+
return code;
|
|
303
|
+
}
|
|
304
|
+
const edits = [];
|
|
305
|
+
for (const lineStart of candidateLineStarts) {
|
|
306
|
+
const nextNewline = code.indexOf('\n', lineStart);
|
|
307
|
+
const lineEnd = nextNewline === -1 ? code.length : nextNewline;
|
|
308
|
+
const line = code.slice(lineStart, lineEnd);
|
|
309
|
+
const match = DIRECT_COMMAND_LINE_RE.exec(line);
|
|
310
|
+
if (!match)
|
|
311
|
+
continue;
|
|
312
|
+
const [, indent, cmd, arg] = match;
|
|
313
|
+
edits.push({
|
|
314
|
+
lineStart,
|
|
315
|
+
lineEnd,
|
|
316
|
+
replacement: `${indent}${cmd}(${JSON.stringify(arg)});`,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
if (edits.length === 0) {
|
|
320
|
+
return code;
|
|
321
|
+
}
|
|
322
|
+
// Apply right-to-left so earlier offsets stay valid.
|
|
323
|
+
edits.sort((a, b) => b.lineStart - a.lineStart);
|
|
324
|
+
let result = code;
|
|
325
|
+
for (const edit of edits) {
|
|
326
|
+
result = result.slice(0, edit.lineStart) + edit.replacement + result.slice(edit.lineEnd);
|
|
327
|
+
}
|
|
328
|
+
return result;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Scan `code` once and return the offsets of every line start that falls in
|
|
332
|
+
* plain-code state (i.e., outside any string, template, comment, or regex
|
|
333
|
+
* literal). The returned offsets are candidates for direct-command rewriting.
|
|
334
|
+
*
|
|
335
|
+
* The scanner covers exactly what we need to avoid false rewrites:
|
|
336
|
+
*
|
|
337
|
+
* - `//` line comments and `/* *\/` block comments
|
|
338
|
+
* - single- and double-quoted strings with `\` escapes
|
|
339
|
+
* - template literals, including nested `${ ... }` expressions (which are
|
|
340
|
+
* themselves code and can contain further strings/templates)
|
|
341
|
+
* - regex literals, disambiguated from division by tracking whether a `/`
|
|
342
|
+
* can begin an expression at its position
|
|
343
|
+
*
|
|
344
|
+
* It is **lexical** only; it does not build an AST or understand statement
|
|
345
|
+
* structure. A full parser (e.g. `acorn` / `acorn-loose`) would be needed
|
|
346
|
+
* for 100% syntactic accuracy — see the note on `normalizeDirectCommands`
|
|
347
|
+
* for why that trade-off is intentional here.
|
|
348
|
+
*/
|
|
349
|
+
function findCodeLineStarts(code) {
|
|
350
|
+
const starts = [];
|
|
351
|
+
// `${...}` nesting inside template literals: each element counts the
|
|
352
|
+
// currently-open `{` inside that expression so we know when to pop back
|
|
353
|
+
// into template-literal state.
|
|
354
|
+
const templateStack = [];
|
|
355
|
+
let inLineComment = false;
|
|
356
|
+
let inBlockComment = false;
|
|
357
|
+
let stringQuote = null;
|
|
358
|
+
let inTemplate = false;
|
|
359
|
+
let inRegex = false;
|
|
360
|
+
let regexCharClass = false;
|
|
361
|
+
// Whether a `/` at the current cursor may start a regex literal.
|
|
362
|
+
let canRegex = true;
|
|
363
|
+
// True only at the FIRST offset of a line (offset 0, or the position
|
|
364
|
+
// right after a newline). Cleared as soon as we consume that offset,
|
|
365
|
+
// so we never record the same line twice.
|
|
366
|
+
let atLineStart = true;
|
|
367
|
+
const len = code.length;
|
|
368
|
+
for (let i = 0; i < len; i++) {
|
|
369
|
+
const ch = code[i];
|
|
370
|
+
const next = i + 1 < len ? code[i + 1] : '';
|
|
371
|
+
// Record plain-code line starts (at most once per line).
|
|
372
|
+
if (atLineStart &&
|
|
373
|
+
!inLineComment &&
|
|
374
|
+
!inBlockComment &&
|
|
375
|
+
stringQuote === null &&
|
|
376
|
+
!inTemplate &&
|
|
377
|
+
!inRegex &&
|
|
378
|
+
templateStack.length === 0) {
|
|
379
|
+
starts.push(i);
|
|
380
|
+
}
|
|
381
|
+
atLineStart = false;
|
|
382
|
+
if (inLineComment) {
|
|
383
|
+
if (ch === '\n') {
|
|
384
|
+
inLineComment = false;
|
|
385
|
+
atLineStart = true;
|
|
386
|
+
canRegex = true;
|
|
387
|
+
}
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
if (inBlockComment) {
|
|
391
|
+
if (ch === '*' && next === '/') {
|
|
392
|
+
inBlockComment = false;
|
|
393
|
+
i++;
|
|
394
|
+
canRegex = true;
|
|
395
|
+
}
|
|
396
|
+
else if (ch === '\n') {
|
|
397
|
+
atLineStart = true;
|
|
398
|
+
}
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
if (stringQuote !== null) {
|
|
402
|
+
if (ch === '\\' && next !== '') {
|
|
403
|
+
i++;
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
if (ch === stringQuote) {
|
|
407
|
+
stringQuote = null;
|
|
408
|
+
canRegex = false;
|
|
409
|
+
}
|
|
410
|
+
else if (ch === '\n') {
|
|
411
|
+
// Unterminated string at newline: recover by exiting string
|
|
412
|
+
// state so we don't swallow the rest of the input.
|
|
413
|
+
stringQuote = null;
|
|
414
|
+
atLineStart = true;
|
|
415
|
+
canRegex = true;
|
|
416
|
+
}
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
if (inTemplate) {
|
|
420
|
+
if (ch === '\\' && next !== '') {
|
|
421
|
+
i++;
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
if (ch === '`') {
|
|
425
|
+
inTemplate = false;
|
|
426
|
+
canRegex = false;
|
|
427
|
+
}
|
|
428
|
+
else if (ch === '$' && next === '{') {
|
|
429
|
+
templateStack.push(1);
|
|
430
|
+
inTemplate = false;
|
|
431
|
+
i++;
|
|
432
|
+
canRegex = true;
|
|
433
|
+
}
|
|
434
|
+
else if (ch === '\n') {
|
|
435
|
+
atLineStart = true;
|
|
436
|
+
}
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
if (inRegex) {
|
|
440
|
+
if (ch === '\\' && next !== '') {
|
|
441
|
+
i++;
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
if (ch === '[') {
|
|
445
|
+
regexCharClass = true;
|
|
446
|
+
}
|
|
447
|
+
else if (ch === ']') {
|
|
448
|
+
regexCharClass = false;
|
|
449
|
+
}
|
|
450
|
+
else if (ch === '/' && !regexCharClass) {
|
|
451
|
+
inRegex = false;
|
|
452
|
+
// Consume optional flags.
|
|
453
|
+
while (i + 1 < len && /[a-z]/i.test(code[i + 1]))
|
|
454
|
+
i++;
|
|
455
|
+
canRegex = false;
|
|
456
|
+
}
|
|
457
|
+
else if (ch === '\n') {
|
|
458
|
+
// Unterminated regex: recover.
|
|
459
|
+
inRegex = false;
|
|
460
|
+
regexCharClass = false;
|
|
461
|
+
atLineStart = true;
|
|
462
|
+
canRegex = true;
|
|
463
|
+
}
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
// Plain-code state (outer) or template-expression state (inner).
|
|
467
|
+
if (ch === '/' && next === '/') {
|
|
468
|
+
inLineComment = true;
|
|
469
|
+
i++;
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
if (ch === '/' && next === '*') {
|
|
473
|
+
inBlockComment = true;
|
|
474
|
+
i++;
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
if (ch === '"' || ch === "'") {
|
|
478
|
+
stringQuote = ch;
|
|
479
|
+
canRegex = false;
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
if (ch === '`') {
|
|
483
|
+
inTemplate = true;
|
|
484
|
+
canRegex = false;
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
if (ch === '/' && canRegex) {
|
|
488
|
+
inRegex = true;
|
|
489
|
+
regexCharClass = false;
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
if (ch === '{' && templateStack.length > 0) {
|
|
493
|
+
templateStack[templateStack.length - 1]++;
|
|
494
|
+
}
|
|
495
|
+
if (ch === '}' && templateStack.length > 0) {
|
|
496
|
+
templateStack[templateStack.length - 1]--;
|
|
497
|
+
if (templateStack[templateStack.length - 1] === 0) {
|
|
498
|
+
templateStack.pop();
|
|
499
|
+
inTemplate = true;
|
|
500
|
+
canRegex = false;
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (ch === '\n') {
|
|
505
|
+
atLineStart = true;
|
|
506
|
+
canRegex = true;
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
// Heuristic for the regex/division ambiguity: letters/digits/closers
|
|
510
|
+
// disallow a regex at the next `/`; other punctuation allows one.
|
|
511
|
+
// Adequate for line-start candidates, which is all we care about.
|
|
512
|
+
if (/[A-Za-z0-9_$)\]]/.test(ch)) {
|
|
513
|
+
canRegex = false;
|
|
514
|
+
}
|
|
515
|
+
else if (!/\s/.test(ch)) {
|
|
516
|
+
canRegex = true;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return starts;
|
|
520
|
+
}
|
|
521
|
+
//# sourceMappingURL=DocumentDBShellRuntime.js.map
|