@mongoosejs/studio 0.4.0 → 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/README.md +9 -0
- package/backend/actions/ChatMessage/executeScript.js +17 -25
- package/backend/actions/Model/getDocuments.js +7 -4
- package/backend/actions/Model/getDocumentsStream.js +7 -4
- package/backend/actions/getCapabilities.js +42 -0
- package/backend/actions/index.js +1 -1
- package/backend/db/chatMessageSchema.js +2 -1
- package/backend/helpers/isBindIPConnection.js +8 -0
- package/backend/helpers/isLocalhostConnection.js +8 -0
- package/backend/helpers/normalizeBindIPOption.js +16 -0
- package/backend/helpers/parseProjectionParam.js +180 -0
- package/backend/sandbox/createSandbox.js +83 -0
- package/backend/sandbox/createScriptDb.js +201 -0
- package/express.js +16 -0
- package/frontend/public/app.js +845 -114
- 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/detail-objectid/detail-objectid.html +1 -0
- package/frontend/src/detail-objectid/detail-objectid.js +67 -0
- package/frontend/src/document-details/document-property/document-property.html +8 -3
- package/frontend/src/document-details/document-property/document-property.js +7 -0
- package/frontend/src/document-details/objectid-view-mode-picker/objectid-view-mode-picker.html +12 -0
- package/frontend/src/document-details/objectid-view-mode-picker/objectid-view-mode-picker.js +19 -0
- package/frontend/src/modal/modal.css +0 -1
- package/frontend/src/models/document-search/document-search.js +6 -0
- package/frontend/src/models/models.html +6 -8
- package/frontend/src/models/models.js +257 -85
- package/frontend/src/models/projection-search/projection-search.html +27 -0
- package/frontend/src/models/projection-search/projection-search.js +226 -0
- package/index.d.ts +1 -1
- package/package.json +2 -2
- package/backend/actions/Script/execute.js +0 -20
- package/backend/actions/Script/index.js +0 -3
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ For local dev, if your app runs on `localhost:3000`, Studio will be on `localhos
|
|
|
12
12
|
|
|
13
13
|
By default, Mongoose Studio does **not** provide any authentication or authorization.
|
|
14
14
|
You can use Mongoose Studio for free for local development, but we recommend [Mongoose Studio Pro](https://studio.mongoosejs.io/#pricing) for when you want to go into production.
|
|
15
|
+
When you omit an API key, Mongoose Studio only accepts localhost connections by default.
|
|
15
16
|
|
|
16
17
|
First, `npm install @mongoosejs/studio`.
|
|
17
18
|
|
|
@@ -43,6 +44,14 @@ opts.googleGeminiAPIKey = process.env.GOOGLE_GEMINI_API_KEY;
|
|
|
43
44
|
app.use('/studio', await studio('/studio/api', mongoose, opts));
|
|
44
45
|
```
|
|
45
46
|
|
|
47
|
+
Without an API key, you can allow access from additional IP addresses with `bindIp`, similar to MongoDB's `bindIp` option. `bindIp` may be a comma-separated string or an array of exact IP addresses. Set `bindIp` to `null` to allow unauthenticated connections from anywhere.
|
|
48
|
+
|
|
49
|
+
```javascript
|
|
50
|
+
app.use('/studio', await studio('/studio/api', mongoose, {
|
|
51
|
+
bindIp: '127.0.0.1,192.168.0.10'
|
|
52
|
+
}));
|
|
53
|
+
```
|
|
54
|
+
|
|
46
55
|
### Next.js
|
|
47
56
|
|
|
48
57
|
First, add `withMongooseStudio` to your `next.config.js` file:
|
|
@@ -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;
|
|
@@ -5,7 +5,7 @@ const removeSpecifiedPaths = require('../../helpers/removeSpecifiedPaths');
|
|
|
5
5
|
const evaluateFilter = require('../../helpers/evaluateFilter');
|
|
6
6
|
const getRefFromSchemaType = require('../../helpers/getRefFromSchemaType');
|
|
7
7
|
const getSuggestedProjection = require('../../helpers/getSuggestedProjection');
|
|
8
|
-
const
|
|
8
|
+
const parseProjectionParam = require('../../helpers/parseProjectionParam');
|
|
9
9
|
const authorize = require('../../authorize');
|
|
10
10
|
|
|
11
11
|
const GetDocumentsParams = new Archetype({
|
|
@@ -32,7 +32,7 @@ const GetDocumentsParams = new Archetype({
|
|
|
32
32
|
sortDirection: {
|
|
33
33
|
$type: 'number'
|
|
34
34
|
},
|
|
35
|
-
|
|
35
|
+
projectionInput: {
|
|
36
36
|
$type: 'string'
|
|
37
37
|
},
|
|
38
38
|
roles: {
|
|
@@ -45,7 +45,7 @@ module.exports = ({ db }) => async function getDocuments(params) {
|
|
|
45
45
|
const { roles } = params;
|
|
46
46
|
await authorize('Model.getDocuments', roles);
|
|
47
47
|
|
|
48
|
-
const { model, limit, skip, sortKey, sortDirection, searchText,
|
|
48
|
+
const { model, limit, skip, sortKey, sortDirection, searchText, projectionInput } = params;
|
|
49
49
|
|
|
50
50
|
const Model = db.models[model];
|
|
51
51
|
if (Model == null) {
|
|
@@ -68,7 +68,10 @@ module.exports = ({ db }) => async function getDocuments(params) {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
let query = Model.find(filter).limit(limit).skip(skip).sort(sortObj);
|
|
71
|
-
const projection =
|
|
71
|
+
const projection = parseProjectionParam({
|
|
72
|
+
projectionInput,
|
|
73
|
+
schemaPaths: Object.keys(Model.schema.paths)
|
|
74
|
+
});
|
|
72
75
|
if (projection != null) {
|
|
73
76
|
query = query.select(projection);
|
|
74
77
|
}
|
|
@@ -5,7 +5,7 @@ const removeSpecifiedPaths = require('../../helpers/removeSpecifiedPaths');
|
|
|
5
5
|
const evaluateFilter = require('../../helpers/evaluateFilter');
|
|
6
6
|
const getRefFromSchemaType = require('../../helpers/getRefFromSchemaType');
|
|
7
7
|
const getSuggestedProjection = require('../../helpers/getSuggestedProjection');
|
|
8
|
-
const
|
|
8
|
+
const parseProjectionParam = require('../../helpers/parseProjectionParam');
|
|
9
9
|
const authorize = require('../../authorize');
|
|
10
10
|
|
|
11
11
|
const GetDocumentsParams = new Archetype({
|
|
@@ -32,7 +32,7 @@ const GetDocumentsParams = new Archetype({
|
|
|
32
32
|
sortDirection: {
|
|
33
33
|
$type: 'number'
|
|
34
34
|
},
|
|
35
|
-
|
|
35
|
+
projectionInput: {
|
|
36
36
|
$type: 'string'
|
|
37
37
|
},
|
|
38
38
|
roles: {
|
|
@@ -45,7 +45,7 @@ module.exports = ({ db }) => async function* getDocumentsStream(params) {
|
|
|
45
45
|
const { roles } = params;
|
|
46
46
|
await authorize('Model.getDocumentsStream', roles);
|
|
47
47
|
|
|
48
|
-
const { model, limit, skip, sortKey, sortDirection, searchText,
|
|
48
|
+
const { model, limit, skip, sortKey, sortDirection, searchText, projectionInput } = params;
|
|
49
49
|
|
|
50
50
|
const Model = db.models[model];
|
|
51
51
|
if (Model == null) {
|
|
@@ -68,7 +68,10 @@ module.exports = ({ db }) => async function* getDocumentsStream(params) {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
let query = Model.find(filter).limit(limit).skip(skip).sort(sortObj).batchSize(1);
|
|
71
|
-
const projection =
|
|
71
|
+
const projection = parseProjectionParam({
|
|
72
|
+
projectionInput,
|
|
73
|
+
schemaPaths: Object.keys(Model.schema.paths)
|
|
74
|
+
});
|
|
72
75
|
if (projection != null) {
|
|
73
76
|
query = query.select(projection);
|
|
74
77
|
}
|
|
@@ -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,16 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
module.exports = function normalizeBindIPOption(bindIp) {
|
|
4
|
+
if (bindIp === null) {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
if (bindIp === undefined) {
|
|
8
|
+
return ['localhost'];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const bindIps = Array.isArray(bindIp) ? bindIp : `${bindIp}`.split(',');
|
|
12
|
+
return bindIps.
|
|
13
|
+
flatMap(bindIp => `${bindIp}`.split(',')).
|
|
14
|
+
map(bindIp => bindIp.trim()).
|
|
15
|
+
filter(bindIp => bindIp.length > 0);
|
|
16
|
+
};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parse a raw projection input string into a Query.select()-compatible projection.
|
|
5
|
+
*
|
|
6
|
+
* Supports:
|
|
7
|
+
* - `name email`
|
|
8
|
+
* - `-password`
|
|
9
|
+
* - `+email`
|
|
10
|
+
* - `{ deletedAt: 1 }`
|
|
11
|
+
* - `{ password: 0 }`
|
|
12
|
+
*
|
|
13
|
+
* @param {Object} options
|
|
14
|
+
* @param {string|undefined} options.projectionInput
|
|
15
|
+
* @param {string[]} options.schemaPaths
|
|
16
|
+
* @returns {string|object|null}
|
|
17
|
+
*/
|
|
18
|
+
function parseProjectionParam({ projectionInput, schemaPaths }) {
|
|
19
|
+
return parseProjectionInput(projectionInput, schemaPaths);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseProjectionInput(projectionInput, schemaPaths) {
|
|
23
|
+
if (projectionInput == null || typeof projectionInput !== 'string') {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const trimmed = projectionInput.trim();
|
|
27
|
+
if (!trimmed) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
|
|
32
|
+
return parseProjectionObjectNotation(trimmed, schemaPaths);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const tokens = normalizeProjectionTokens(trimmed);
|
|
36
|
+
if (tokens === null || tokens.length === 0) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const includeKeys = [];
|
|
41
|
+
const excludeKeys = [];
|
|
42
|
+
|
|
43
|
+
for (const rawToken of tokens) {
|
|
44
|
+
const token = rawToken.trim();
|
|
45
|
+
if (!token) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const prefix = token[0];
|
|
50
|
+
if (prefix === '-') {
|
|
51
|
+
const path = token.slice(1).trim();
|
|
52
|
+
if (!path) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
excludeKeys.push(path);
|
|
56
|
+
} else if (prefix === '+') {
|
|
57
|
+
const path = token.slice(1).trim();
|
|
58
|
+
if (!path) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
includeKeys.push(path);
|
|
62
|
+
} else {
|
|
63
|
+
includeKeys.push(token);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (includeKeys.length > 0 && excludeKeys.length > 0) {
|
|
68
|
+
const includeSet = new Set(includeKeys);
|
|
69
|
+
for (const path of excludeKeys) {
|
|
70
|
+
const ex = path;
|
|
71
|
+
for (const key of Array.from(includeSet)) {
|
|
72
|
+
if (key.toLowerCase() === ex.toLowerCase()) {
|
|
73
|
+
includeSet.delete(key);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (includeSet.size === 0) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return Array.from(includeSet).join(' ');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (excludeKeys.length > 0) {
|
|
84
|
+
return excludeKeys.map(path => `-${path}`).join(' ');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return includeKeys.join(' ');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function normalizeProjectionTokens(trimmed) {
|
|
91
|
+
const rawTokens = trimmed.split(/[,\s]+/).filter(Boolean);
|
|
92
|
+
const tokens = [];
|
|
93
|
+
for (let i = 0; i < rawTokens.length; i++) {
|
|
94
|
+
const token = rawTokens[i];
|
|
95
|
+
if (token === '-' || token === '+') {
|
|
96
|
+
if (i + 1 >= rawTokens.length) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
tokens.push(token + rawTokens[++i]);
|
|
100
|
+
} else {
|
|
101
|
+
tokens.push(token);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return tokens;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function parseProjectionObjectNotation(trimmed, schemaPaths) {
|
|
108
|
+
const body = trimmed.slice(1, -1).trim();
|
|
109
|
+
if (!body) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const pairRe = /(?:^|,)\s*(?:"([^"]+)"|'([^']+)'|([a-zA-Z_$][\w.$]*))\s*:\s*("[^"]*"|'[^']*'|[^,]+?)\s*(?=,|$)/g;
|
|
114
|
+
const includeKeys = [];
|
|
115
|
+
const excludeKeys = [];
|
|
116
|
+
let matchedChars = '';
|
|
117
|
+
let match;
|
|
118
|
+
|
|
119
|
+
while ((match = pairRe.exec(body)) !== null) {
|
|
120
|
+
matchedChars += match[0];
|
|
121
|
+
const key = (match[1] || match[2] || match[3] || '').trim();
|
|
122
|
+
const rawValue = (match[4] || '').trim();
|
|
123
|
+
if (!key || !rawValue) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
if (key === '__proto__' || key === 'constructor') {
|
|
127
|
+
throw new Error(`Invalid key: ${key}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const valueLower = rawValue.replace(/^['"]|['"]$/g, '').trim().toLowerCase();
|
|
131
|
+
const isInclude = valueLower === '1' || valueLower === 'true';
|
|
132
|
+
const isExclude = valueLower === '0' || valueLower === 'false';
|
|
133
|
+
if (!isInclude && !isExclude) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
if (isInclude) {
|
|
137
|
+
includeKeys.push(key);
|
|
138
|
+
} else {
|
|
139
|
+
excludeKeys.push(key);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const normalizedBody = body.replace(/\s+/g, '');
|
|
144
|
+
const normalizedMatched = matchedChars.replace(/\s+/g, '').replace(/^,/, '');
|
|
145
|
+
if (!normalizedMatched || normalizedMatched !== normalizedBody) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (includeKeys.length > 0 && excludeKeys.length > 0) {
|
|
150
|
+
const includeSet = new Set(includeKeys);
|
|
151
|
+
for (const path of excludeKeys) {
|
|
152
|
+
const ex = path;
|
|
153
|
+
for (const key of Array.from(includeSet)) {
|
|
154
|
+
if (key.toLowerCase() === ex.toLowerCase()) {
|
|
155
|
+
includeSet.delete(key);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (includeSet.size === 0) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
return Array.from(includeSet).join(' ');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (excludeKeys.length > 0) {
|
|
166
|
+
const projection = {};
|
|
167
|
+
for (const path of excludeKeys) {
|
|
168
|
+
projection[path] = 0;
|
|
169
|
+
}
|
|
170
|
+
return Object.keys(projection).length > 0 ? projection : null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const projection = {};
|
|
174
|
+
for (const path of includeKeys) {
|
|
175
|
+
projection[path] = 1;
|
|
176
|
+
}
|
|
177
|
+
return Object.keys(projection).length > 0 ? projection : null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
module.exports = parseProjectionParam;
|
|
@@ -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
|
+
}
|