@mongoosejs/studio 0.5.13 → 0.5.14

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.
Files changed (50) hide show
  1. package/AGENTS.md +7 -0
  2. package/CLAUDE.md +7 -0
  3. package/README.md +3 -0
  4. package/backend/actions/ChatMessage/executeScript.js +7 -5
  5. package/backend/actions/ChatThread/createChatMessage.js +4 -2
  6. package/backend/actions/ChatThread/getChatThread.js +5 -2
  7. package/backend/actions/ChatThread/listChatThreads.js +3 -1
  8. package/backend/actions/ChatThread/shareChatThread.js +5 -2
  9. package/backend/actions/ChatThread/streamChatMessage.js +4 -2
  10. package/backend/actions/ChatThread/toggleAgentMode.js +5 -2
  11. package/backend/actions/Dashboard/deleteDashboard.js +7 -3
  12. package/backend/actions/Dashboard/getDashboard.js +21 -10
  13. package/backend/actions/Dashboard/getDashboards.js +3 -1
  14. package/backend/actions/Dashboard/updateDashboard.js +3 -2
  15. package/backend/actions/Model/addField.js +3 -2
  16. package/backend/actions/Model/analyzeSchema.js +6 -4
  17. package/backend/actions/Model/deleteDocument.js +3 -3
  18. package/backend/actions/Model/deleteDocuments.js +3 -1
  19. package/backend/actions/Model/executeDocumentScript.js +25 -29
  20. package/backend/actions/Model/exportQueryResults.js +3 -1
  21. package/backend/actions/Model/getCollectionInfo.js +5 -3
  22. package/backend/actions/Model/getDocument.js +3 -2
  23. package/backend/actions/Model/getDocuments.js +15 -5
  24. package/backend/actions/Model/getDocumentsStream.js +28 -18
  25. package/backend/actions/Model/getEstimatedDocumentCounts.js +5 -2
  26. package/backend/actions/Model/updateDocument.js +4 -2
  27. package/backend/actions/Model/updateDocuments.js +4 -2
  28. package/backend/actions/Model/validateDocument.js +5 -2
  29. package/backend/actions/Task/cancelTask.js +6 -3
  30. package/backend/actions/Task/getTaskOverview.js +3 -2
  31. package/backend/actions/Task/getTasks.js +3 -2
  32. package/backend/actions/Task/getTasksOverTime.js +3 -2
  33. package/backend/actions/Task/rescheduleTask.js +6 -3
  34. package/backend/actions/Task/runTask.js +6 -3
  35. package/backend/helpers/omitNullish.js +5 -0
  36. package/backend/index.js +4 -4
  37. package/backend/sandbox/createSandbox.js +2 -2
  38. package/backend/sandbox/createScriptDb.js +42 -19
  39. package/frontend/public/app.js +319 -96
  40. package/frontend/public/tw.css +68 -0
  41. package/frontend/src/api.js +5 -1
  42. package/frontend/src/models/models.html +29 -8
  43. package/frontend/src/models/models.js +30 -2
  44. package/frontend/src/tooltip/tooltip.html +28 -0
  45. package/frontend/src/tooltip/tooltip.js +94 -0
  46. package/index.d.ts +1 -1
  47. package/local.js +2 -1
  48. package/nest.d.ts +1 -0
  49. package/package.json +1 -1
  50. package/vercel.d.ts +2 -0
package/AGENTS.md ADDED
@@ -0,0 +1,7 @@
1
+ # Repository guidance
2
+
3
+ The Mongoose connection passed to Studio is owned by the host application.
4
+ Do not change connection-level settings, options, configuration, models, middleware, plugins, collection objects, or methods on the application connection to implement Studio behavior.
5
+ Apply Studio-specific behavior to individual operations whenever possible.
6
+ When isolation requires a derived connection, copy any Mongoose state that `useDb()` shares with its source before changing the derived connection.
7
+ Sandbox wrappers must remain confined to sandbox-owned models and collections.
package/CLAUDE.md ADDED
@@ -0,0 +1,7 @@
1
+ # Repository guidance
2
+
3
+ The Mongoose connection passed to Studio is owned by the host application.
4
+ Do not change connection-level settings, options, configuration, models, middleware, plugins, collection objects, or methods on the application connection to implement Studio behavior.
5
+ Apply Studio-specific behavior to individual operations whenever possible.
6
+ When isolation requires a derived connection, copy any Mongoose state that `useDb()` shares with its source before changing the derived connection.
7
+ Sandbox wrappers must remain confined to sandbox-owned models and collections.
package/README.md CHANGED
@@ -39,6 +39,9 @@ opts.model = 'gpt-4o-mini';
39
39
  opts.openAIAPIKey = process.env.OPENAI_API_KEY;
40
40
  opts.anthropicAPIKey = process.env.ANTHROPIC_API_KEY;
41
41
  opts.googleGeminiAPIKey = process.env.GOOGLE_GEMINI_API_KEY;
42
+ // Apply a maximum execution time to all read operations, including reads in scripts.
43
+ // MongoDB does not support maxTimeMS for inserts and index operations.
44
+ opts.maxTimeMS = 10000;
42
45
 
43
46
  // Mount Mongoose Studio on '/studio'
44
47
  app.use('/studio', await studio('/studio/api', mongoose, opts));
@@ -4,6 +4,7 @@ const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
5
  const createSandbox = require('../../sandbox/createSandbox');
6
6
  const mongoose = require('mongoose');
7
+ const omitNullish = require('../../helpers/omitNullish');
7
8
 
8
9
  const ExecuteScriptParams = new Archetype({
9
10
  initiatedById: {
@@ -24,24 +25,25 @@ const ExecuteScriptParams = new Archetype({
24
25
  }
25
26
  }).compile('ExecuteScriptParams');
26
27
 
27
- module.exports = ({ db, studioConnection }) => async function executeScript(params) {
28
+ module.exports = ({ db, studioConnection, options }) => async function executeScript(params) {
28
29
  const { initiatedById, chatMessageId, script, dryRun, roles } = new ExecuteScriptParams(params);
29
30
  const ChatThread = studioConnection.model('__Studio_ChatThread');
30
31
  const ChatMessage = studioConnection.model('__Studio_ChatMessage');
31
32
 
32
33
  await authorize('ChatMessage.executeScript', roles);
33
34
 
34
- const chatMessage = await ChatMessage.findById(chatMessageId);
35
+ const operationOptions = omitNullish({ maxTimeMS: options?.maxTimeMS });
36
+ const chatMessage = await ChatMessage.findById(chatMessageId).setOptions(operationOptions);
35
37
  if (!chatMessage) {
36
38
  throw new Error('Chat message not found');
37
39
  }
38
- const chatThread = await ChatThread.findById(chatMessage.chatThreadId).orFail();
40
+ const chatThread = await ChatThread.findById(chatMessage.chatThreadId).setOptions(operationOptions).orFail();
39
41
 
40
42
  if (initiatedById && chatThread.userId?.toString() !== initiatedById.toString()) {
41
43
  throw new Error('Unauthorized');
42
44
  }
43
45
 
44
- const sandbox = createSandbox({ db });
46
+ const sandbox = createSandbox({ db, maxTimeMS: options?.maxTimeMS });
45
47
 
46
48
  let output;
47
49
  let error;
@@ -74,7 +76,7 @@ module.exports = ({ db, studioConnection }) => async function executeScript(para
74
76
  dryRun: !!dryRun
75
77
  }
76
78
  }
77
- );
79
+ ).setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS }));
78
80
 
79
81
  throw new Error(`Script execution failed: ${error}`);
80
82
  } finally {
@@ -8,6 +8,7 @@ const callLLM = require('../../integrations/callLLM');
8
8
  const getAgentTools = require('../../chatAgent/getAgentTools');
9
9
  const getModelDescriptions = require('../../helpers/getModelDescriptions');
10
10
  const mongoose = require('mongoose');
11
+ const omitNullish = require('../../helpers/omitNullish');
11
12
 
12
13
  const CreateChatMessageParams = new Archetype({
13
14
  chatThreadId: {
@@ -36,7 +37,8 @@ module.exports = ({ db, studioConnection, options }) => async function createCha
36
37
  await authorize('ChatThread.createChatMessage', roles);
37
38
 
38
39
  // Check that the user owns the thread
39
- const chatThread = await ChatThread.findOne({ _id: chatThreadId });
40
+ const operationOptions = omitNullish({ maxTimeMS: options?.maxTimeMS });
41
+ const chatThread = await ChatThread.findOne({ _id: chatThreadId }).setOptions(operationOptions);
40
42
  if (!chatThread) {
41
43
  throw new Error('Chat thread not found');
42
44
  }
@@ -44,7 +46,7 @@ module.exports = ({ db, studioConnection, options }) => async function createCha
44
46
  throw new Error('Not authorized');
45
47
  }
46
48
 
47
- const messages = await ChatMessage.find({ chatThreadId }).sort({ createdAt: 1 });
49
+ const messages = await ChatMessage.find({ chatThreadId }).setOptions(operationOptions).sort({ createdAt: 1 });
48
50
  const llmMessages = messages.map(m => ({
49
51
  role: m.role,
50
52
  content: [{
@@ -3,6 +3,7 @@
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
5
  const mongoose = require('mongoose');
6
+ const omitNullish = require('../../helpers/omitNullish');
6
7
 
7
8
  const GetChatThreadParams = new Archetype({
8
9
  chatThreadId: {
@@ -19,14 +20,15 @@ const GetChatThreadParams = new Archetype({
19
20
  }
20
21
  }).compile('GetChatThreadParams');
21
22
 
22
- module.exports = ({ db, studioConnection }) => async function getChatThread(params) {
23
+ module.exports = ({ db, studioConnection, options }) => async function getChatThread(params) {
23
24
  const { chatThreadId, initiatedById, roles, $workspaceId } = new GetChatThreadParams(params);
24
25
  const ChatThread = studioConnection.model('__Studio_ChatThread');
25
26
  const ChatMessage = studioConnection.model('__Studio_ChatMessage');
26
27
 
27
28
  await authorize('ChatThread.getChatThread', roles);
28
29
 
29
- const chatThread = await ChatThread.findById(chatThreadId);
30
+ const operationOptions = omitNullish({ maxTimeMS: options?.maxTimeMS });
31
+ const chatThread = await ChatThread.findById(chatThreadId).setOptions(operationOptions);
30
32
 
31
33
  if (!chatThread) {
32
34
  throw new Error('Chat thread not found');
@@ -39,6 +41,7 @@ module.exports = ({ db, studioConnection }) => async function getChatThread(para
39
41
  }
40
42
 
41
43
  const chatMessages = await ChatMessage.find({ chatThreadId })
44
+ .setOptions(operationOptions)
42
45
  .sort({ createdAt: -1 });
43
46
 
44
47
  return {
@@ -3,6 +3,7 @@
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
5
  const mongoose = require('mongoose');
6
+ const omitNullish = require('../../helpers/omitNullish');
6
7
 
7
8
  const ListChatThreadsParams = new Archetype({
8
9
  initiatedById: {
@@ -13,7 +14,7 @@ const ListChatThreadsParams = new Archetype({
13
14
  }
14
15
  }).compile('ListChatThreadsParams');
15
16
 
16
- module.exports = ({ db, studioConnection }) => async function listChatThreads(params) {
17
+ module.exports = ({ db, studioConnection, options }) => async function listChatThreads(params) {
17
18
  // Validate the params object
18
19
  const { initiatedById, roles } = new ListChatThreadsParams(params);
19
20
  const ChatThread = studioConnection.model('__Studio_ChatThread');
@@ -24,6 +25,7 @@ module.exports = ({ db, studioConnection }) => async function listChatThreads(pa
24
25
 
25
26
  // Get all chat threads
26
27
  const chatThreads = await ChatThread.find(query)
28
+ .setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS }))
27
29
  .sort({ updatedAt: -1 }); // Sort by most recently updated
28
30
 
29
31
  return {
@@ -3,6 +3,7 @@
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
5
  const mongoose = require('mongoose');
6
+ const omitNullish = require('../../helpers/omitNullish');
6
7
 
7
8
  const ShareChatThreadParams = new Archetype({
8
9
  chatThreadId: {
@@ -22,13 +23,15 @@ const ShareChatThreadParams = new Archetype({
22
23
  }
23
24
  }).compile('ShareChatThreadParams');
24
25
 
25
- module.exports = ({ studioConnection }) => async function shareChatThread(params) {
26
+ module.exports = ({ studioConnection, options }) => async function shareChatThread(params) {
26
27
  const { chatThreadId, share, initiatedById, roles, $workspaceId } = new ShareChatThreadParams(params);
27
28
  const ChatThread = studioConnection.model('__Studio_ChatThread');
28
29
 
29
30
  await authorize('ChatThread.shareChatThread', roles);
30
31
 
31
- const chatThread = await ChatThread.findById(chatThreadId).orFail();
32
+ const chatThread = await ChatThread.findById(chatThreadId).
33
+ setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS })).
34
+ orFail();
32
35
  if (initiatedById != null && chatThread.userId?.toString() !== initiatedById.toString()) {
33
36
  throw new Error('Not authorized');
34
37
  }
@@ -8,6 +8,7 @@ const runChatAgent = require('../../chatAgent/runChatAgent');
8
8
  const streamLLM = require('../../integrations/streamLLM');
9
9
  const getModelDescriptions = require('../../helpers/getModelDescriptions');
10
10
  const mongoose = require('mongoose');
11
+ const omitNullish = require('../../helpers/omitNullish');
11
12
 
12
13
  const CreateChatMessageParams = new Archetype({
13
14
  chatThreadId: {
@@ -36,7 +37,8 @@ module.exports = ({ db, studioConnection, options }) => async function* streamCh
36
37
  await authorize('ChatThread.createChatMessage', roles);
37
38
 
38
39
  // Check that the user owns the thread
39
- const chatThread = await ChatThread.findOne({ _id: chatThreadId });
40
+ const operationOptions = omitNullish({ maxTimeMS: options?.maxTimeMS });
41
+ const chatThread = await ChatThread.findOne({ _id: chatThreadId }).setOptions(operationOptions);
40
42
  if (!chatThread) {
41
43
  throw new Error('Chat thread not found');
42
44
  }
@@ -44,7 +46,7 @@ module.exports = ({ db, studioConnection, options }) => async function* streamCh
44
46
  throw new Error('Not authorized');
45
47
  }
46
48
 
47
- const messages = await ChatMessage.find({ chatThreadId }).sort({ createdAt: 1 });
49
+ const messages = await ChatMessage.find({ chatThreadId }).setOptions(operationOptions).sort({ createdAt: 1 });
48
50
  const llmMessages = messages.map(m => ({
49
51
  role: m.role,
50
52
  content: [{
@@ -3,6 +3,7 @@
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
5
  const mongoose = require('mongoose');
6
+ const omitNullish = require('../../helpers/omitNullish');
6
7
 
7
8
  const ToggleAgentModeParams = new Archetype({
8
9
  chatThreadId: {
@@ -19,13 +20,15 @@ const ToggleAgentModeParams = new Archetype({
19
20
  }
20
21
  }).compile('ToggleAgentModeParams');
21
22
 
22
- module.exports = ({ studioConnection }) => async function toggleAgentMode(params) {
23
+ module.exports = ({ studioConnection, options }) => async function toggleAgentMode(params) {
23
24
  const { chatThreadId, agentMode, initiatedById, roles } = new ToggleAgentModeParams(params);
24
25
  const ChatThread = studioConnection.model('__Studio_ChatThread');
25
26
 
26
27
  await authorize('ChatThread.toggleAgentMode', roles);
27
28
 
28
- const chatThread = await ChatThread.findById(chatThreadId).orFail();
29
+ const chatThread = await ChatThread.findById(chatThreadId).
30
+ setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS })).
31
+ orFail();
29
32
  if (initiatedById != null && chatThread.userId?.toString() !== initiatedById.toString()) {
30
33
  throw new Error('Not authorized');
31
34
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
+ const omitNullish = require('../../helpers/omitNullish');
5
6
 
6
7
  const DeleteDashboardParams = new Archetype({
7
8
  dashboardId: {
@@ -13,14 +14,17 @@ const DeleteDashboardParams = new Archetype({
13
14
  }
14
15
  }).compile('DeleteDashboardParams');
15
16
 
16
- module.exports = ({ studioConnection }) => async function deleteDashboard(params) {
17
+ module.exports = ({ studioConnection, options }) => async function deleteDashboard(params) {
17
18
  const { dashboardId, roles } = new DeleteDashboardParams(params);
18
19
  const Dashboard = studioConnection.model('__Studio_Dashboard');
19
20
  const DashboardResult = studioConnection.model('__Studio_DashboardResult');
20
21
 
21
22
  await authorize('Dashboard.deleteDashboard', roles);
22
23
 
23
- const result = await Dashboard.deleteOne({ _id: dashboardId }).orFail();
24
- await DashboardResult.deleteMany({ dashboardId });
24
+ const result = await Dashboard.deleteOne({ _id: dashboardId }).
25
+ setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS })).
26
+ orFail();
27
+ await DashboardResult.deleteMany({ dashboardId }).
28
+ setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS }));
25
29
  return { result };
26
30
  };
@@ -4,6 +4,7 @@ const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
5
  const createSandbox = require('../../sandbox/createSandbox');
6
6
  const mongoose = require('mongoose');
7
+ const omitNullish = require('../../helpers/omitNullish');
7
8
  const { defaultMothershipURL } = require('../../../constants');
8
9
 
9
10
  const GetDashboardParams = new Archetype({
@@ -36,10 +37,11 @@ module.exports = ({ db, studioConnection, options }) => async function getDashbo
36
37
 
37
38
  await authorize('Dashboard.getDashboard', roles);
38
39
 
39
- const dashboard = await Dashboard.findOne({ _id: dashboardId });
40
+ const dashboard = await Dashboard.findOne({ _id: dashboardId }).
41
+ setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS }));
40
42
  if (evaluate) {
41
43
  let result = null;
42
- const sandbox = createSandbox({ db });
44
+ const sandbox = createSandbox({ db, maxTimeMS: options?.maxTimeMS });
43
45
  const startExec = startDashboardEvaluate(DashboardResult, dashboardId, $workspaceId, userId);
44
46
  startExec.catch(() => {}); // Avoid unhandled promise rejections - we will handle this error later.
45
47
  try {
@@ -56,7 +58,8 @@ module.exports = ({ db, studioConnection, options }) => async function getDashbo
56
58
  dashboardResult._id,
57
59
  null,
58
60
  { message: error.message },
59
- 'failed'
61
+ 'failed',
62
+ options
60
63
  );
61
64
  });
62
65
  return { dashboard, dashboardResult, error: { message: error.message } };
@@ -79,7 +82,8 @@ module.exports = ({ db, studioConnection, options }) => async function getDashbo
79
82
  dashboardResult._id,
80
83
  result,
81
84
  undefined,
82
- 'completed'
85
+ 'completed',
86
+ options
83
87
  );
84
88
  });
85
89
 
@@ -93,21 +97,25 @@ module.exports = ({ db, studioConnection, options }) => async function getDashbo
93
97
  dashboardId,
94
98
  $workspaceId,
95
99
  authorization,
96
- mothershipUrl
100
+ mothershipUrl,
101
+ options
97
102
  );
98
103
  return { dashboard, dashboardResults };
99
104
  }
100
105
  };
101
106
 
102
- async function completeDashboardEvaluate(Dashboard, DashboardResult, dashboardResultId, result, error, status) {
103
- const dashboardResult = await DashboardResult.findById(dashboardResultId).orFail();
107
+ async function completeDashboardEvaluate(Dashboard, DashboardResult, dashboardResultId, result, error, status, options) {
108
+ const dashboardResult = await DashboardResult.findById(dashboardResultId).
109
+ setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS })).
110
+ orFail();
104
111
  const finishedEvaluatingAt = new Date();
105
112
  dashboardResult.finishedEvaluatingAt = finishedEvaluatingAt;
106
113
  dashboardResult.result = result;
107
114
  dashboardResult.error = error;
108
115
  dashboardResult.status = status;
109
116
  await dashboardResult.save();
110
- await Dashboard.updateOne({ _id: dashboardResult.dashboardId }, { $set: { lastEvaluatedAt: finishedEvaluatingAt } });
117
+ await Dashboard.updateOne({ _id: dashboardResult.dashboardId }, { $set: { lastEvaluatedAt: finishedEvaluatingAt } }).
118
+ setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS }));
111
119
  return { dashboardResult };
112
120
  }
113
121
 
@@ -123,14 +131,17 @@ async function startDashboardEvaluate(DashboardResult, dashboardId, workspaceId,
123
131
  return { dashboardResult };
124
132
  }
125
133
 
126
- async function getDashboardResults(DashboardResult, dashboardId, workspaceId, authorization, mothershipUrl) {
134
+ async function getDashboardResults(DashboardResult, dashboardId, workspaceId, authorization, mothershipUrl, options) {
127
135
  const filter = { dashboardId };
128
136
  if (workspaceId != null) {
129
137
  filter.workspaceId = workspaceId;
130
138
  }
131
139
 
132
140
  const [localResultsRes, remoteResultsRes] = await Promise.allSettled([
133
- DashboardResult.find(filter).sort({ _id: -1 }).limit(10),
141
+ DashboardResult.find(filter).
142
+ sort({ _id: -1 }).
143
+ limit(10).
144
+ setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS })),
134
145
  getMothershipDashboardResults(dashboardId, workspaceId, authorization, mothershipUrl)
135
146
  ]);
136
147
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
+ const omitNullish = require('../../helpers/omitNullish');
5
6
 
6
7
  const GetDashboardParams = new Archetype({
7
8
  roles: {
@@ -9,7 +10,7 @@ const GetDashboardParams = new Archetype({
9
10
  }
10
11
  }).compile('GetDashboardParams');
11
12
 
12
- module.exports = ({ studioConnection }) => async function getDashboards(params) {
13
+ module.exports = ({ studioConnection, options }) => async function getDashboards(params) {
13
14
  const Dashboard = studioConnection.model('__Studio_Dashboard');
14
15
  const { roles } = new GetDashboardParams(params);
15
16
 
@@ -17,6 +18,7 @@ module.exports = ({ studioConnection }) => async function getDashboards(params)
17
18
 
18
19
  const dashboards = await Dashboard
19
20
  .find()
21
+ .setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS }))
20
22
  .sort({ isPinned: -1, createdAt: -1, _id: -1 });
21
23
 
22
24
  return { dashboards };
@@ -2,6 +2,7 @@
2
2
 
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
+ const omitNullish = require('../../helpers/omitNullish');
5
6
 
6
7
  const UpdateDashboardParams = new Archetype({
7
8
  dashboardId: {
@@ -25,7 +26,7 @@ const UpdateDashboardParams = new Archetype({
25
26
  }
26
27
  }).compile('UpdateDashboardParams');
27
28
 
28
- module.exports = ({ studioConnection }) => async function updateDashboard(params) {
29
+ module.exports = ({ studioConnection, options }) => async function updateDashboard(params) {
29
30
  const { dashboardId, code, title, description, isPinned, roles, evaluate } = new UpdateDashboardParams(params);
30
31
 
31
32
  const Dashboard = studioConnection.models['__Studio_Dashboard'];
@@ -54,7 +55,7 @@ module.exports = ({ studioConnection }) => async function updateDashboard(params
54
55
  dashboardId,
55
56
  updateObj,
56
57
  { sanitizeFilter: true, returnDocument: 'after' }
57
- );
58
+ ).setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS }));
58
59
 
59
60
  return { doc };
60
61
  };
@@ -2,6 +2,7 @@
2
2
 
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
+ const omitNullish = require('../../helpers/omitNullish');
5
6
 
6
7
  const AddFieldParams = new Archetype({
7
8
  model: {
@@ -25,7 +26,7 @@ const AddFieldParams = new Archetype({
25
26
  }
26
27
  }).compile('AddFieldParams');
27
28
 
28
- module.exports = ({ db }) => async function addField(params) {
29
+ module.exports = ({ db, options }) => async function addField(params) {
29
30
  const { model, _id, fieldName, fieldValue, roles } = new AddFieldParams(params);
30
31
 
31
32
  await authorize('Model.updateDocument', roles);
@@ -48,7 +49,7 @@ module.exports = ({ db }) => async function addField(params) {
48
49
  runValidators: false,
49
50
  strict: false
50
51
  }
51
- );
52
+ ).setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS }));
52
53
 
53
54
  return { doc };
54
55
  };
@@ -3,6 +3,7 @@
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
5
  const { Buffer } = require('buffer');
6
+ const omitNullish = require('../../helpers/omitNullish');
6
7
  const validateDocumentWithTimeout = require('../../helpers/validateDocumentWithTimeout');
7
8
 
8
9
  const SAMPLE_SIZE = 1000;
@@ -18,7 +19,7 @@ const AnalyzeSchemaParams = new Archetype({
18
19
  }
19
20
  }).compile('AnalyzeSchemaParams');
20
21
 
21
- module.exports = ({ db }) => async function analyzeSchema(params) {
22
+ module.exports = ({ db, options }) => async function analyzeSchema(params) {
22
23
  const { model, roles } = new AnalyzeSchemaParams(params);
23
24
 
24
25
  await authorize('Model.analyzeSchema', roles);
@@ -28,10 +29,11 @@ module.exports = ({ db }) => async function analyzeSchema(params) {
28
29
  throw new Error(`Model ${model} not found`);
29
30
  }
30
31
 
31
- const documentCount = await Model.collection.countDocuments();
32
+ const operationOptions = omitNullish({ maxTimeMS: options?.maxTimeMS });
33
+ const documentCount = await Model.collection.countDocuments({}, operationOptions);
32
34
  const rawDocs = documentCount > SAMPLE_SIZE ?
33
- await Model.collection.aggregate([{ $sample: { size: SAMPLE_SIZE } }]).toArray() :
34
- await Model.collection.find({}).toArray();
35
+ await Model.collection.aggregate([{ $sample: { size: SAMPLE_SIZE } }], operationOptions).toArray() :
36
+ await Model.collection.find({}, operationOptions).toArray();
35
37
 
36
38
  const paths = getSchemaPaths(Model.schema);
37
39
  const pathTypeCounts = {};
@@ -2,6 +2,7 @@
2
2
 
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
+ const omitNullish = require('../../helpers/omitNullish');
5
6
 
6
7
  const DeleteDocumentParams = new Archetype({
7
8
  model: {
@@ -17,7 +18,7 @@ const DeleteDocumentParams = new Archetype({
17
18
  }
18
19
  }).compile('DeleteDocumentParams');
19
20
 
20
- module.exports = ({ db }) => async function DeleteDocument(params) {
21
+ module.exports = ({ db, options }) => async function DeleteDocument(params) {
21
22
  const { model, documentId, roles } = new DeleteDocumentParams(params);
22
23
 
23
24
  const Model = db.models[model];
@@ -30,9 +31,8 @@ module.exports = ({ db }) => async function DeleteDocument(params) {
30
31
 
31
32
  const doc = await Model.
32
33
  deleteOne({ _id: documentId }).
33
- setOptions({ sanitizeFilter: true }).
34
+ setOptions(omitNullish({ sanitizeFilter: true, maxTimeMS: options?.maxTimeMS })).
34
35
  orFail();
35
- console.log('what is doc', doc);
36
36
 
37
37
  return { doc };
38
38
  };
@@ -2,6 +2,7 @@
2
2
 
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
+ const omitNullish = require('../../helpers/omitNullish');
5
6
 
6
7
  const DeleteDocumentsParams = new Archetype({
7
8
  model: {
@@ -17,7 +18,7 @@ const DeleteDocumentsParams = new Archetype({
17
18
  }
18
19
  }).compile('DeleteDocumentsParams');
19
20
 
20
- module.exports = ({ db }) => async function DeleteDocuments(params) {
21
+ module.exports = ({ db, options }) => async function DeleteDocuments(params) {
21
22
  const { model, documentIds, roles } = new DeleteDocumentsParams(params);
22
23
 
23
24
  const Model = db.models[model];
@@ -30,6 +31,7 @@ module.exports = ({ db }) => async function DeleteDocuments(params) {
30
31
 
31
32
  await Model.
32
33
  deleteMany({ _id: { $in: documentIds } }).
34
+ setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS })).
33
35
  orFail();
34
36
 
35
37
  return { };
@@ -2,9 +2,8 @@
2
2
 
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
- const mongoose = require('mongoose');
6
- const util = require('util');
7
- const vm = require('vm');
5
+ const createSandbox = require('../../sandbox/createSandbox');
6
+ const omitNullish = require('../../helpers/omitNullish');
8
7
 
9
8
  const ExecuteDocumentScriptParams = new Archetype({
10
9
  model: {
@@ -24,38 +23,35 @@ const ExecuteDocumentScriptParams = new Archetype({
24
23
  }
25
24
  }).compile('ExecuteDocumentScriptParams');
26
25
 
27
- module.exports = ({ db }) => async function executeDocumentScript(params) {
26
+ module.exports = ({ db, options }) => async function executeDocumentScript(params) {
28
27
  const { model, documentId, script, roles } = new ExecuteDocumentScriptParams(params);
29
28
 
30
29
  await authorize('Model.executeDocumentScript', roles);
31
30
 
32
- const Model = db.models[model];
33
- if (Model == null) {
31
+ if (db.models[model] == null) {
34
32
  throw new Error(`Model ${model} not found`);
35
33
  }
36
34
 
37
- const doc = await Model.findById(documentId).setOptions({ sanitizeFilter: true }).orFail();
38
-
39
- const logs = [];
40
- if (!db.Types) {
41
- db.Types = mongoose.Types;
35
+ const sandbox = createSandbox({ db, maxTimeMS: options?.maxTimeMS });
36
+
37
+ try {
38
+ const Model = sandbox.db.models[model];
39
+ const doc = await Model.findById(documentId).
40
+ setOptions(omitNullish({ sanitizeFilter: true, maxTimeMS: options?.maxTimeMS })).
41
+ orFail();
42
+ sandbox.context.doc = doc;
43
+
44
+ const result = await sandbox.runScript({ script });
45
+
46
+ return {
47
+ result,
48
+ logs: sandbox.getLogs()
49
+ };
50
+ } finally {
51
+ try {
52
+ await sandbox.close();
53
+ } catch (_) {
54
+ // Ignore sandbox cleanup errors so they do not mask the primary result.
55
+ }
42
56
  }
43
- const sandbox = { db, mongoose, doc, console: {}, ObjectId: mongoose.Types.ObjectId };
44
-
45
- sandbox.console.log = function() {
46
- const args = Array.from(arguments);
47
- logs.push(args.map(arg => typeof arg === 'object' ? util.inspect(arg) : arg).join(' '));
48
- };
49
-
50
- const context = vm.createContext(sandbox);
51
- const result = await vm.runInContext(wrappedScript(script), context);
52
-
53
- return {
54
- result,
55
- logs: logs.join('\n')
56
- };
57
57
  };
58
-
59
- const wrappedScript = script => `(async () => {
60
- ${script}
61
- })()`;
@@ -5,6 +5,7 @@ const mongoose = require('mongoose');
5
5
  const { stringify } = require('csv-stringify/sync');
6
6
  const authorize = require('../../authorize');
7
7
  const evaluateFilter = require('../../helpers/evaluateFilter');
8
+ const omitNullish = require('../../helpers/omitNullish');
8
9
 
9
10
  const GetDocumentsParams = new Archetype({
10
11
  model: {
@@ -29,7 +30,7 @@ const GetDocumentsParams = new Archetype({
29
30
  }
30
31
  }).compile('GetDocumentsParams');
31
32
 
32
- module.exports = ({ db }) => async function exportQueryResults(params, req, res) {
33
+ module.exports = ({ db, options }) => async function exportQueryResults(params, req, res) {
33
34
  params = new GetDocumentsParams(params);
34
35
  const { model, propertiesToInclude, roles, searchText } = params;
35
36
 
@@ -45,6 +46,7 @@ module.exports = ({ db }) => async function exportQueryResults(params, req, res)
45
46
 
46
47
  const docs = await Model.
47
48
  find(filter).
49
+ setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS })).
48
50
  sort({ _id: -1 });
49
51
 
50
52
  const rows = [propertiesToInclude];
@@ -2,6 +2,7 @@
2
2
 
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
+ const omitNullish = require('../../helpers/omitNullish');
5
6
 
6
7
  const GetCollectionInfoParams = new Archetype({
7
8
  model: {
@@ -13,7 +14,7 @@ const GetCollectionInfoParams = new Archetype({
13
14
  }
14
15
  }).compile('GetCollectionInfoParams');
15
16
 
16
- module.exports = ({ db }) => async function getCollectionInfo(params) {
17
+ module.exports = ({ db, options }) => async function getCollectionInfo(params) {
17
18
  const { model, roles } = new GetCollectionInfoParams(params);
18
19
 
19
20
  await authorize('Model.getCollectionInfo', roles);
@@ -23,8 +24,9 @@ module.exports = ({ db }) => async function getCollectionInfo(params) {
23
24
  throw new Error(`Model ${model} not found`);
24
25
  }
25
26
 
27
+ const operationOptions = omitNullish({ maxTimeMS: options?.maxTimeMS });
26
28
  const [collectionOptions, stats] = await Promise.all([
27
- Model.collection.options(),
29
+ Model.collection.options(operationOptions),
28
30
  Model.aggregate([
29
31
  {
30
32
  $collStats: {
@@ -32,7 +34,7 @@ module.exports = ({ db }) => async function getCollectionInfo(params) {
32
34
  count: {}
33
35
  }
34
36
  }
35
- ]).then(res => res[0] ?? {})
37
+ ]).option(operationOptions).then(res => res[0] ?? {})
36
38
  ]);
37
39
 
38
40
  return {