@mongoosejs/studio 0.4.1 → 0.5.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/backend/actions/ChatMessage/executeScript.js +17 -25
- package/backend/actions/getCapabilities.js +42 -0
- package/backend/actions/index.js +1 -1
- package/backend/db/chatMessageSchema.js +2 -1
- package/backend/sandbox/createSandbox.js +83 -0
- package/backend/sandbox/createScriptDb.js +201 -0
- package/frontend/public/app.js +107 -31
- package/frontend/public/tw.css +4 -0
- package/frontend/src/api.js +6 -0
- package/frontend/src/chat/chat-message/chat-message.html +1 -0
- package/frontend/src/chat/chat-message/chat-message.js +4 -3
- package/frontend/src/chat/chat-message-script/chat-message-script.html +98 -16
- package/frontend/src/chat/chat-message-script/chat-message-script.js +77 -17
- package/frontend/src/chat/chat.html +5 -1
- package/frontend/src/chat/chat.js +11 -2
- package/frontend/src/modal/modal.css +0 -1
- package/frontend/src/models/models.js +2 -2
- package/frontend/src/models/projection-search/projection-search.js +2 -2
- package/package.json +2 -2
- package/backend/actions/Script/execute.js +0 -20
- package/backend/actions/Script/index.js +0 -3
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const Archetype = require('archetype');
|
|
4
4
|
const authorize = require('../../authorize');
|
|
5
|
-
const
|
|
5
|
+
const createSandbox = require('../../sandbox/createSandbox');
|
|
6
6
|
const mongoose = require('mongoose');
|
|
7
|
-
const vm = require('vm');
|
|
8
7
|
|
|
9
8
|
const ExecuteScriptParams = new Archetype({
|
|
10
9
|
initiatedById: {
|
|
@@ -17,13 +16,16 @@ const ExecuteScriptParams = new Archetype({
|
|
|
17
16
|
script: {
|
|
18
17
|
$type: 'string'
|
|
19
18
|
},
|
|
19
|
+
dryRun: {
|
|
20
|
+
$type: 'boolean'
|
|
21
|
+
},
|
|
20
22
|
roles: {
|
|
21
23
|
$type: ['string']
|
|
22
24
|
}
|
|
23
25
|
}).compile('ExecuteScriptParams');
|
|
24
26
|
|
|
25
27
|
module.exports = ({ db, studioConnection }) => async function executeScript(params) {
|
|
26
|
-
const { initiatedById, chatMessageId, script, roles } = new ExecuteScriptParams(params);
|
|
28
|
+
const { initiatedById, chatMessageId, script, dryRun, roles } = new ExecuteScriptParams(params);
|
|
27
29
|
const ChatThread = studioConnection.model('__Studio_ChatThread');
|
|
28
30
|
const ChatMessage = studioConnection.model('__Studio_ChatMessage');
|
|
29
31
|
|
|
@@ -39,32 +41,19 @@ module.exports = ({ db, studioConnection }) => async function executeScript(para
|
|
|
39
41
|
throw new Error('Unauthorized');
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
const logs = [];
|
|
44
|
-
if (!db.Types) {
|
|
45
|
-
db.Types = mongoose.Types;
|
|
46
|
-
}
|
|
47
|
-
const sandbox = { db, mongoose, console: {}, ObjectId: mongoose.Types.ObjectId, MongooseStudioChartColors };
|
|
48
|
-
|
|
49
|
-
// Capture console logs
|
|
50
|
-
sandbox.console.log = function() {
|
|
51
|
-
const args = Array.from(arguments);
|
|
52
|
-
logs.push(args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : arg).join(' '));
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
const context = vm.createContext(sandbox);
|
|
44
|
+
const sandbox = createSandbox({ db });
|
|
56
45
|
|
|
57
46
|
let output;
|
|
58
47
|
let error;
|
|
59
48
|
|
|
60
49
|
try {
|
|
61
50
|
// Execute the script in the sandbox
|
|
62
|
-
output = await
|
|
51
|
+
output = await sandbox.runScript({ script, dryRun });
|
|
63
52
|
|
|
64
53
|
const updatedContent = updateContentWithScript(chatMessage.content, chatMessage.script, script);
|
|
65
54
|
chatMessage.script = script;
|
|
66
55
|
chatMessage.content = updatedContent;
|
|
67
|
-
chatMessage.executionResult = { output, logs:
|
|
56
|
+
chatMessage.executionResult = { output, logs: sandbox.getLogs(), error: null, dryRun: !!dryRun };
|
|
68
57
|
await chatMessage.save();
|
|
69
58
|
|
|
70
59
|
return { chatMessage };
|
|
@@ -80,20 +69,23 @@ module.exports = ({ db, studioConnection }) => async function executeScript(para
|
|
|
80
69
|
content: updatedContent,
|
|
81
70
|
executionResult: {
|
|
82
71
|
output: null,
|
|
83
|
-
logs:
|
|
84
|
-
error
|
|
72
|
+
logs: sandbox.getLogs(),
|
|
73
|
+
error,
|
|
74
|
+
dryRun: !!dryRun
|
|
85
75
|
}
|
|
86
76
|
}
|
|
87
77
|
);
|
|
88
78
|
|
|
89
79
|
throw new Error(`Script execution failed: ${error}`);
|
|
80
|
+
} finally {
|
|
81
|
+
try {
|
|
82
|
+
await sandbox.close();
|
|
83
|
+
} catch (_) {
|
|
84
|
+
// Ignore sandbox cleanup errors so they do not mask the primary result.
|
|
85
|
+
}
|
|
90
86
|
}
|
|
91
87
|
};
|
|
92
88
|
|
|
93
|
-
const wrappedScript = script => `(async () => {
|
|
94
|
-
${script}
|
|
95
|
-
})()`;
|
|
96
|
-
|
|
97
89
|
function updateContentWithScript(content, previousScript, newScript) {
|
|
98
90
|
if (typeof content !== 'string') {
|
|
99
91
|
return content;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
module.exports = ({ db, options }) => async function getCapabilities() {
|
|
4
|
+
const connection = db?.connection ?? db;
|
|
5
|
+
const admin = typeof connection?.db?.admin === 'function' ? connection.db.admin() : null;
|
|
6
|
+
const supportsAI = hasAIProviderKey(options);
|
|
7
|
+
|
|
8
|
+
if (admin == null || typeof admin.command !== 'function') {
|
|
9
|
+
return {
|
|
10
|
+
supportsChangeStreams: false,
|
|
11
|
+
supportsTransactions: false,
|
|
12
|
+
supportsAI
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let hello;
|
|
17
|
+
try {
|
|
18
|
+
hello = await admin.command({ hello: 1 });
|
|
19
|
+
} catch (err) {
|
|
20
|
+
hello = await admin.command({ isMaster: 1 });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const maxWireVersion = hello?.maxWireVersion ?? 0;
|
|
24
|
+
const supportsModernMongoFeatures = maxWireVersion >= 7;
|
|
25
|
+
const isReplicaSet = !!hello?.setName;
|
|
26
|
+
const isMongos = hello?.msg === 'isdbgrid';
|
|
27
|
+
const hasSessions = hello?.logicalSessionTimeoutMinutes != null;
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
supportsChangeStreams: supportsModernMongoFeatures && (isReplicaSet || isMongos),
|
|
31
|
+
supportsTransactions: supportsModernMongoFeatures && hasSessions && (isReplicaSet || isMongos),
|
|
32
|
+
supportsAI
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function hasAIProviderKey(options) {
|
|
37
|
+
return [
|
|
38
|
+
options?.openAIAPIKey,
|
|
39
|
+
options?.anthropicAPIKey,
|
|
40
|
+
options?.googleGeminiAPIKey
|
|
41
|
+
].some(value => typeof value === 'string' ? value.trim().length > 0 : !!value);
|
|
42
|
+
}
|
package/backend/actions/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
exports.ChatMessage = require('./ChatMessage');
|
|
4
4
|
exports.ChatThread = require('./ChatThread');
|
|
5
5
|
exports.Dashboard = require('./Dashboard');
|
|
6
|
+
exports.getCapabilities = require('./getCapabilities');
|
|
6
7
|
exports.Model = require('./Model');
|
|
7
|
-
exports.Script = require('./Script');
|
|
8
8
|
exports.status = require('./status');
|
|
9
9
|
exports.Task = require('./Task');
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const MongooseStudioChartColors = require('../constants/mongooseStudioChartColors');
|
|
4
|
+
const mongoose = require('mongoose');
|
|
5
|
+
const vm = require('vm');
|
|
6
|
+
const { createScriptDb } = require('./createScriptDb');
|
|
7
|
+
|
|
8
|
+
module.exports = function createSandbox({ db }) {
|
|
9
|
+
const logs = [];
|
|
10
|
+
const scriptDb = createScriptDb(db);
|
|
11
|
+
if (!scriptDb.db.Types) {
|
|
12
|
+
scriptDb.db.Types = mongoose.Types;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const sandbox = {
|
|
16
|
+
db: scriptDb.db,
|
|
17
|
+
mongoose,
|
|
18
|
+
console: {},
|
|
19
|
+
ObjectId: mongoose.Types.ObjectId,
|
|
20
|
+
MongooseStudioChartColors
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
sandbox.console.log = function() {
|
|
24
|
+
const args = Array.from(arguments);
|
|
25
|
+
logs.push(args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : arg).join(' '));
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
context: vm.createContext(sandbox),
|
|
30
|
+
db: scriptDb.db,
|
|
31
|
+
getLogs() {
|
|
32
|
+
return logs.join('\n');
|
|
33
|
+
},
|
|
34
|
+
setDryRunSession: scriptDb.setDryRunSession,
|
|
35
|
+
async runScript({ script, dryRun }) {
|
|
36
|
+
const wrappedScript = `(async () => {
|
|
37
|
+
${script}
|
|
38
|
+
})()`;
|
|
39
|
+
|
|
40
|
+
if (!dryRun) {
|
|
41
|
+
return await vm.runInContext(wrappedScript, this.context);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return await runDryRunScript({
|
|
45
|
+
db: this.db,
|
|
46
|
+
context: this.context,
|
|
47
|
+
setDryRunSession: this.setDryRunSession,
|
|
48
|
+
wrappedScript
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
close: scriptDb.close
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
async function runDryRunScript({ db, context, setDryRunSession, wrappedScript }) {
|
|
56
|
+
if (typeof db?.startSession !== 'function') {
|
|
57
|
+
throw new Error('Dry run mode requires MongoDB sessions support');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const session = await db.startSession();
|
|
61
|
+
setDryRunSession(session);
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
session.startTransaction();
|
|
65
|
+
const result = await vm.runInContext(wrappedScript, context);
|
|
66
|
+
await abortTransaction(session);
|
|
67
|
+
return result;
|
|
68
|
+
} catch (err) {
|
|
69
|
+
await abortTransaction(session);
|
|
70
|
+
throw err;
|
|
71
|
+
} finally {
|
|
72
|
+
setDryRunSession(null);
|
|
73
|
+
await session.endSession().catch(() => {});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function abortTransaction(session) {
|
|
78
|
+
if (typeof session?.inTransaction === 'function' && !session.inTransaction()) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
await session.abortTransaction().catch(() => {});
|
|
83
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const wrappedCollections = new WeakSet();
|
|
4
|
+
|
|
5
|
+
const collectionMethodOptionsIndex = new Map([
|
|
6
|
+
['bulkWrite', 1],
|
|
7
|
+
['countDocuments', 1],
|
|
8
|
+
['createIndex', 1],
|
|
9
|
+
['createIndexes', 1],
|
|
10
|
+
['deleteMany', 1],
|
|
11
|
+
['deleteOne', 1],
|
|
12
|
+
['distinct', 2],
|
|
13
|
+
['drop', 0],
|
|
14
|
+
['dropIndex', 1],
|
|
15
|
+
['dropIndexes', 0],
|
|
16
|
+
['estimatedDocumentCount', 0],
|
|
17
|
+
['find', 1],
|
|
18
|
+
['findOne', 1],
|
|
19
|
+
['findOneAndDelete', 1],
|
|
20
|
+
['findOneAndReplace', 2],
|
|
21
|
+
['findOneAndUpdate', 2],
|
|
22
|
+
['insertMany', 1],
|
|
23
|
+
['insertOne', 1],
|
|
24
|
+
['rename', 1],
|
|
25
|
+
['replaceOne', 2],
|
|
26
|
+
['updateMany', 2],
|
|
27
|
+
['updateOne', 2]
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
function createScriptDb(db) {
|
|
31
|
+
const sourceConnection = db;
|
|
32
|
+
|
|
33
|
+
const scriptConnection = sourceConnection.useDb(sourceConnection.name, { useCache: false });
|
|
34
|
+
scriptConnection.config = {
|
|
35
|
+
...scriptConnection.config,
|
|
36
|
+
autoCreate: false,
|
|
37
|
+
autoIndex: false
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
let dryRunSession = null;
|
|
41
|
+
const getSession = () => dryRunSession;
|
|
42
|
+
cloneModels(sourceConnection, scriptConnection, getSession);
|
|
43
|
+
wrapCollectionAccessors(scriptConnection, getSession);
|
|
44
|
+
|
|
45
|
+
const originalDebug = sourceConnection.options?.debug;
|
|
46
|
+
scriptConnection.set('debug', function() {
|
|
47
|
+
if (typeof originalDebug === 'function') {
|
|
48
|
+
return originalDebug.apply(this, arguments);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
db: scriptConnection,
|
|
54
|
+
setDryRunSession(session) {
|
|
55
|
+
dryRunSession = session;
|
|
56
|
+
},
|
|
57
|
+
close() {
|
|
58
|
+
if (Array.isArray(sourceConnection.otherDbs)) {
|
|
59
|
+
sourceConnection.otherDbs = sourceConnection.otherDbs.filter(db => db !== scriptConnection);
|
|
60
|
+
}
|
|
61
|
+
if (Array.isArray(scriptConnection.otherDbs)) {
|
|
62
|
+
scriptConnection.otherDbs = [];
|
|
63
|
+
}
|
|
64
|
+
if (sourceConnection.relatedDbs?.[scriptConnection.name] === scriptConnection) {
|
|
65
|
+
delete sourceConnection.relatedDbs[scriptConnection.name];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function cloneModels(sourceConnection, scriptConnection, getSession) {
|
|
72
|
+
for (const Model of Object.values(sourceConnection.models ?? {})) {
|
|
73
|
+
if (Model?.schema == null || typeof scriptConnection.model !== 'function') {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const ClonedModel = scriptConnection.model(Model.modelName, Model.schema, getCollectionName(Model));
|
|
78
|
+
wrapModelCollections(ClonedModel, getSession);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getCollectionName(Model) {
|
|
83
|
+
return Model.collection?.collectionName ?? Model.collection?.name ?? Model.$__collection?.collectionName;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function wrapModelCollections(Model, getSession) {
|
|
87
|
+
const collection = Model?.collection ?? Model?.$__collection;
|
|
88
|
+
if (collection == null) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const wrappedCollection = wrapCollection(collection, getSession);
|
|
93
|
+
Model.collection = wrappedCollection;
|
|
94
|
+
Model.$__collection = wrappedCollection;
|
|
95
|
+
|
|
96
|
+
if (Model.prototype != null) {
|
|
97
|
+
Model.prototype.collection = wrappedCollection;
|
|
98
|
+
Model.prototype.$collection = wrappedCollection;
|
|
99
|
+
setModelCollectionSymbols(Model.prototype, wrappedCollection);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function setModelCollectionSymbols(target, collection) {
|
|
104
|
+
for (const symbol of Object.getOwnPropertySymbols(target)) {
|
|
105
|
+
if (symbol.description === 'mongoose#Model#collection') {
|
|
106
|
+
target[symbol] = collection;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function wrapCollectionAccessors(scriptConnection, getSession) {
|
|
112
|
+
const wrappedCollectionCache = new WeakMap();
|
|
113
|
+
|
|
114
|
+
const nativeDb = scriptConnection.db;
|
|
115
|
+
const originalDbCollection = nativeDb != null && typeof nativeDb.collection === 'function'
|
|
116
|
+
? nativeDb.collection
|
|
117
|
+
: null;
|
|
118
|
+
|
|
119
|
+
if (typeof scriptConnection.collection === 'function') {
|
|
120
|
+
const originalConnectionCollection = scriptConnection.collection;
|
|
121
|
+
scriptConnection.collection = function() {
|
|
122
|
+
const collection = originalConnectionCollection.apply(this, arguments);
|
|
123
|
+
if (collection != null && originalDbCollection != null && collection.name != null) {
|
|
124
|
+
if (collection.collection == null || wrappedCollections.has(collection.collection)) {
|
|
125
|
+
collection.collection = originalDbCollection.call(nativeDb, collection.name);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return getOrCreateWrappedCollection(wrappedCollectionCache, collection, getSession);
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (originalDbCollection != null) {
|
|
133
|
+
nativeDb.collection = function() {
|
|
134
|
+
const collection = originalDbCollection.apply(this, arguments);
|
|
135
|
+
return getOrCreateWrappedCollection(wrappedCollectionCache, collection, getSession);
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function getOrCreateWrappedCollection(cache, collection, getSession) {
|
|
141
|
+
if (collection == null || (typeof collection !== 'object' && typeof collection !== 'function')) {
|
|
142
|
+
return collection;
|
|
143
|
+
}
|
|
144
|
+
if (wrappedCollections.has(collection)) {
|
|
145
|
+
return collection;
|
|
146
|
+
}
|
|
147
|
+
if (cache.has(collection)) {
|
|
148
|
+
return cache.get(collection);
|
|
149
|
+
}
|
|
150
|
+
const wrapped = wrapCollection(collection, getSession);
|
|
151
|
+
cache.set(collection, wrapped);
|
|
152
|
+
return wrapped;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function wrapCollection(collection, getSession) {
|
|
156
|
+
if (wrappedCollections.has(collection)) {
|
|
157
|
+
return collection;
|
|
158
|
+
}
|
|
159
|
+
const wrapped = Object.create(collection);
|
|
160
|
+
wrappedCollections.add(wrapped);
|
|
161
|
+
for (const [methodName, optionsIndex] of collectionMethodOptionsIndex) {
|
|
162
|
+
const method = collection[methodName];
|
|
163
|
+
if (typeof method !== 'function') {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
wrapped[methodName] = function() {
|
|
168
|
+
const args = addSessionOption(Array.from(arguments), optionsIndex, getSession());
|
|
169
|
+
return method.apply(collection, args);
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return wrapped;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function addSessionOption(args, optionsIndex, session) {
|
|
176
|
+
if (session == null) {
|
|
177
|
+
return args;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
while (args.length < optionsIndex) {
|
|
181
|
+
args.push(undefined);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const options = args[optionsIndex];
|
|
185
|
+
|
|
186
|
+
if (options != null && typeof options !== 'object') {
|
|
187
|
+
throw new Error('Cannot run dry run on script where options arg is a non-object');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (options?.session != null) {
|
|
191
|
+
throw new Error('Cannot run dry run on script that uses sessions');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
args[optionsIndex] = { ...(options ?? {}), session };
|
|
195
|
+
return args;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
module.exports = {
|
|
199
|
+
createScriptDb,
|
|
200
|
+
collectionMethodOptionsIndex
|
|
201
|
+
};
|