@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
@@ -4,6 +4,7 @@ const Archetype = require('archetype');
4
4
  const getRefFromSchemaType = require('../../helpers/getRefFromSchemaType');
5
5
  const removeSpecifiedPaths = require('../../helpers/removeSpecifiedPaths');
6
6
  const authorize = require('../../authorize');
7
+ const omitNullish = require('../../helpers/omitNullish');
7
8
 
8
9
  const GetDocumentParams = new Archetype({
9
10
  model: {
@@ -19,7 +20,7 @@ const GetDocumentParams = new Archetype({
19
20
  }
20
21
  }).compile('GetDocumentParams');
21
22
 
22
- module.exports = ({ db }) => async function getDocument(params) {
23
+ module.exports = ({ db, options }) => async function getDocument(params) {
23
24
  const { model, documentId, roles } = new GetDocumentParams(params);
24
25
 
25
26
  await authorize('Model.getDocument', roles);
@@ -31,7 +32,7 @@ module.exports = ({ db }) => async function getDocument(params) {
31
32
 
32
33
  const doc = await Model.
33
34
  findById(documentId).
34
- setOptions({ sanitizeFilter: true }).
35
+ setOptions(omitNullish({ sanitizeFilter: true, maxTimeMS: options?.maxTimeMS })).
35
36
  orFail();
36
37
  const schemaPaths = {};
37
38
  for (const path of Object.keys(Model.schema.paths)) {
@@ -7,6 +7,7 @@ const getRefFromSchemaType = require('../../helpers/getRefFromSchemaType');
7
7
  const getSuggestedProjection = require('../../helpers/getSuggestedProjection');
8
8
  const parseProjectionParam = require('../../helpers/parseProjectionParam');
9
9
  const authorize = require('../../authorize');
10
+ const omitNullish = require('../../helpers/omitNullish');
10
11
 
11
12
  const GetDocumentsParams = new Archetype({
12
13
  model: {
@@ -40,7 +41,7 @@ const GetDocumentsParams = new Archetype({
40
41
  }
41
42
  }).compile('GetDocumentsParams');
42
43
 
43
- module.exports = ({ db }) => async function getDocuments(params) {
44
+ module.exports = ({ db, options }) => async function getDocuments(params) {
44
45
  params = new GetDocumentsParams(params);
45
46
  const { roles } = params;
46
47
  await authorize('Model.getDocuments', roles);
@@ -75,6 +76,7 @@ module.exports = ({ db }) => async function getDocuments(params) {
75
76
  if (projection != null) {
76
77
  query = query.select(projection);
77
78
  }
79
+ query.setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS }));
78
80
  const cursor = await query.cursor();
79
81
  const docs = [];
80
82
  for (let doc = await cursor.next(); doc != null; doc = await cursor.next()) {
@@ -106,9 +108,16 @@ module.exports = ({ db }) => async function getDocuments(params) {
106
108
  }
107
109
  removeSpecifiedPaths(schemaPaths, '.$*');
108
110
 
109
- const numDocuments = parsedFilter == null ?
110
- await Model.estimatedDocumentCount() :
111
- await Model.countDocuments(filter);
111
+ const countQuery = parsedFilter == null ?
112
+ Model.estimatedDocumentCount() :
113
+ Model.countDocuments(filter);
114
+ let numDocuments = null;
115
+ let numDocsError = null;
116
+ try {
117
+ numDocuments = await countQuery.setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS }));
118
+ } catch (err) {
119
+ numDocsError = err?.message;
120
+ }
112
121
 
113
122
  const suggestedFields = getSuggestedProjection(Model);
114
123
 
@@ -116,6 +125,7 @@ module.exports = ({ db }) => async function getDocuments(params) {
116
125
  docs: docs.map(doc => doc.toJSON({ virtuals: false, getters: false, transform: false })),
117
126
  schemaPaths,
118
127
  suggestedFields,
119
- numDocs: numDocuments
128
+ numDocs: numDocuments,
129
+ numDocsError
120
130
  };
121
131
  };
@@ -7,6 +7,7 @@ const getRefFromSchemaType = require('../../helpers/getRefFromSchemaType');
7
7
  const getSuggestedProjection = require('../../helpers/getSuggestedProjection');
8
8
  const parseProjectionParam = require('../../helpers/parseProjectionParam');
9
9
  const authorize = require('../../authorize');
10
+ const omitNullish = require('../../helpers/omitNullish');
10
11
 
11
12
  const GetDocumentsParams = new Archetype({
12
13
  model: {
@@ -40,7 +41,7 @@ const GetDocumentsParams = new Archetype({
40
41
  }
41
42
  }).compile('GetDocumentsParams');
42
43
 
43
- module.exports = ({ db }) => async function* getDocumentsStream(params) {
44
+ module.exports = ({ db, options }) => async function* getDocumentsStream(params) {
44
45
  params = new GetDocumentsParams(params);
45
46
  const { roles } = params;
46
47
  await authorize('Model.getDocumentsStream', roles);
@@ -75,6 +76,7 @@ module.exports = ({ db }) => async function* getDocumentsStream(params) {
75
76
  if (projection != null) {
76
77
  query = query.select(projection);
77
78
  }
79
+ query.setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS }));
78
80
 
79
81
  const schemaPaths = {};
80
82
  for (const path of Object.keys(Model.schema.paths)) {
@@ -106,32 +108,36 @@ module.exports = ({ db }) => async function* getDocumentsStream(params) {
106
108
  yield { schemaPaths, suggestedFields };
107
109
 
108
110
  // Start counting documents in parallel with streaming documents
109
- const numDocsPromise = (parsedFilter == null)
110
- ? Model.estimatedDocumentCount().exec()
111
- : Model.countDocuments(filter).exec();
112
-
113
- const cursor = await query.cursor();
111
+ const countQuery = (parsedFilter == null)
112
+ ? Model.estimatedDocumentCount()
113
+ : Model.countDocuments(filter);
114
+ const numDocsPromise = countQuery.
115
+ setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS })).
116
+ exec();
114
117
 
115
118
  let numDocsYielded = false;
116
119
  let numDocumentsPromiseResolved = false;
117
120
  let numDocumentsValue;
118
121
  let numDocumentsError;
119
122
 
120
- try {
121
- // Start listening for numDocsPromise resolution
122
- numDocsPromise.then(num => {
123
- numDocumentsPromiseResolved = true;
124
- numDocumentsValue = num;
125
- }).catch(err => {
126
- numDocumentsPromiseResolved = true;
127
- numDocumentsError = err;
128
- });
123
+ // Attach both handlers immediately so a fast count failure cannot become
124
+ // an unhandled rejection while the document cursor is still opening.
125
+ numDocsPromise.then(num => {
126
+ numDocumentsPromiseResolved = true;
127
+ numDocumentsValue = num;
128
+ }).catch(err => {
129
+ numDocumentsPromiseResolved = true;
130
+ numDocumentsError = err;
131
+ });
129
132
 
133
+ const cursor = await query.cursor();
134
+
135
+ try {
130
136
  for (let doc = await cursor.next(); doc != null; doc = await cursor.next()) {
131
137
  // If numDocsPromise has resolved and not yet yielded, yield it first
132
138
  if (numDocumentsPromiseResolved && !numDocsYielded) {
133
139
  if (numDocumentsError) {
134
- yield { error: numDocumentsError };
140
+ yield { numDocsError: numDocumentsError.message };
135
141
  } else {
136
142
  yield { numDocs: numDocumentsValue };
137
143
  }
@@ -142,8 +148,12 @@ module.exports = ({ db }) => async function* getDocumentsStream(params) {
142
148
 
143
149
  // If numDocsPromise hasn't resolved yet, wait for it and yield
144
150
  if (!numDocsYielded) {
145
- const numDocuments = await numDocsPromise;
146
- yield { numDocs: numDocuments };
151
+ try {
152
+ const numDocuments = await numDocsPromise;
153
+ yield { numDocs: numDocuments };
154
+ } catch (err) {
155
+ yield { numDocsError: err.message };
156
+ }
147
157
  }
148
158
  } finally {
149
159
  await cursor.close();
@@ -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 GetEstimatedDocumentCountsParams = new Archetype({
7
8
  roles: {
@@ -9,7 +10,7 @@ const GetEstimatedDocumentCountsParams = new Archetype({
9
10
  }
10
11
  }).compile('GetEstimatedDocumentCountsParams');
11
12
 
12
- module.exports = ({ db }) => async function getEstimatedDocumentCounts(params) {
13
+ module.exports = ({ db, options }) => async function getEstimatedDocumentCounts(params) {
13
14
  const { roles } = new GetEstimatedDocumentCountsParams(params);
14
15
  await authorize('Model.getEstimatedDocumentCounts', roles);
15
16
 
@@ -20,7 +21,9 @@ module.exports = ({ db }) => async function getEstimatedDocumentCounts(params) {
20
21
  const results = await Promise.allSettled(
21
22
  modelNames.map(name => {
22
23
  const Model = db.models[name];
23
- return Model.estimatedDocumentCount().exec();
24
+ return Model.estimatedDocumentCount().
25
+ setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS })).
26
+ exec();
24
27
  })
25
28
  );
26
29
 
@@ -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 UpdateDocumentsParams = new Archetype({
7
8
  model: {
@@ -24,7 +25,7 @@ const UpdateDocumentsParams = new Archetype({
24
25
  }
25
26
  }).compile('UpdateDocumentsParams');
26
27
 
27
- module.exports = ({ db }) => async function updateDocument(params) {
28
+ module.exports = ({ db, options }) => async function updateDocument(params) {
28
29
  const { model, _id, update, unset, roles } = new UpdateDocumentsParams(params);
29
30
 
30
31
  await authorize('Model.updateDocument', roles);
@@ -58,6 +59,7 @@ module.exports = ({ db }) => async function updateDocument(params) {
58
59
  }
59
60
 
60
61
  const doc = await Model.
61
- findByIdAndUpdate(_id, updateOperation, { sanitizeFilter: true, returnDocument: 'after', overwriteImmutable: true, runValidators: false });
62
+ findByIdAndUpdate(_id, updateOperation, { sanitizeFilter: true, returnDocument: 'after', overwriteImmutable: true, runValidators: false }).
63
+ setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS }));
62
64
  return { doc };
63
65
  };
@@ -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 UpdateDocumentsParams = new Archetype({
7
8
  model: {
@@ -21,7 +22,7 @@ const UpdateDocumentsParams = new Archetype({
21
22
  }
22
23
  }).compile('UpdateDocumentsParams');
23
24
 
24
- module.exports = ({ db }) => async function updateDocuments(params) {
25
+ module.exports = ({ db, options }) => async function updateDocuments(params) {
25
26
  const { model, _id, update, roles } = new UpdateDocumentsParams(params);
26
27
 
27
28
  await authorize('Model.updateDocuments', roles);
@@ -60,7 +61,8 @@ module.exports = ({ db }) => async function updateDocuments(params) {
60
61
  }
61
62
 
62
63
  const result = await Model.
63
- updateMany({ _id: { $in: _id } }, updateOperation, { overwriteImmutable: true, runValidators: false });
64
+ updateMany({ _id: { $in: _id } }, updateOperation, { overwriteImmutable: true, runValidators: false }).
65
+ setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS }));
64
66
 
65
67
  return { result };
66
68
  };
@@ -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
  const validateDocumentWithTimeout = require('../../helpers/validateDocumentWithTimeout');
6
7
 
7
8
  const ValidateDocumentParams = new Archetype({
@@ -18,7 +19,7 @@ const ValidateDocumentParams = new Archetype({
18
19
  }
19
20
  }).compile('ValidateDocumentParams');
20
21
 
21
- module.exports = ({ db }) => async function validateDocument(params) {
22
+ module.exports = ({ db, options }) => async function validateDocument(params) {
22
23
  const { model, documentId, roles } = new ValidateDocumentParams(params);
23
24
 
24
25
  await authorize('Model.validateDocument', roles);
@@ -28,7 +29,9 @@ module.exports = ({ db }) => async function validateDocument(params) {
28
29
  throw new Error(`Model ${model} not found`);
29
30
  }
30
31
 
31
- const doc = await Model.findById(documentId).setOptions({ sanitizeFilter: true }).orFail();
32
+ const doc = await Model.findById(documentId).
33
+ setOptions(omitNullish({ sanitizeFilter: true, maxTimeMS: options?.maxTimeMS })).
34
+ orFail();
32
35
  return {
33
36
  result: await validateDocumentWithTimeout(doc)
34
37
  };
@@ -2,6 +2,7 @@
2
2
 
3
3
  const Archetype = require('archetype');
4
4
  const mongoose = require('mongoose');
5
+ const omitNullish = require('../../helpers/omitNullish');
5
6
 
6
7
  const CancelTaskParams = new Archetype({
7
8
  taskId: {
@@ -10,15 +11,17 @@ const CancelTaskParams = new Archetype({
10
11
  }
11
12
  }).compile('CancelTaskParams');
12
13
 
13
- module.exports = ({ db }) => async function cancelTask(params) {
14
+ module.exports = ({ db, options }) => async function cancelTask(params) {
14
15
  params = new CancelTaskParams(params);
15
16
  const { taskId } = params;
16
17
  const { Task } = db.models;
17
18
 
18
- const task = await Task.findOne({ _id: taskId }).orFail();
19
+ const task = await Task.findOne({ _id: taskId }).
20
+ setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS })).
21
+ orFail();
19
22
 
20
23
  const cancelledTask = await Task.cancelTask({ _id: taskId });
21
24
  return {
22
25
  task: cancelledTask
23
26
  };
24
- };
27
+ };
@@ -2,6 +2,7 @@
2
2
 
3
3
  const Archetype = require('archetype');
4
4
  const escape = require('regexp.escape');
5
+ const omitNullish = require('../../helpers/omitNullish');
5
6
 
6
7
  const GetTaskOverviewParams = new Archetype({
7
8
  start: { $type: Date },
@@ -34,7 +35,7 @@ function buildMatch(params) {
34
35
  return match;
35
36
  }
36
37
 
37
- module.exports = ({ db }) => async function getTaskOverview(params) {
38
+ module.exports = ({ db, options }) => async function getTaskOverview(params) {
38
39
  params = new GetTaskOverviewParams(params);
39
40
  if (typeof params.status === 'string') params.status = params.status.trim();
40
41
  if (typeof params.name === 'string') params.name = params.name.trim();
@@ -93,7 +94,7 @@ module.exports = ({ db }) => async function getTaskOverview(params) {
93
94
  }
94
95
  ];
95
96
 
96
- const [result] = await Task.aggregate(pipeline);
97
+ const [result] = await Task.aggregate(pipeline).option(omitNullish({ maxTimeMS: options?.maxTimeMS }));
97
98
 
98
99
  return {
99
100
  statusCounts: result.statusCounts?.[0] ?? {},
@@ -2,6 +2,7 @@
2
2
 
3
3
  const Archetype = require('archetype');
4
4
  const escape = require('regexp.escape');
5
+ const omitNullish = require('../../helpers/omitNullish');
5
6
 
6
7
  const GetTasksParams = new Archetype({
7
8
  start: { $type: Date },
@@ -55,7 +56,7 @@ const TASK_PROJECT_STAGE = {
55
56
  parameters: '$payload'
56
57
  };
57
58
 
58
- module.exports = ({ db }) => async function getTasks(params) {
59
+ module.exports = ({ db, options }) => async function getTasks(params) {
59
60
  params = new GetTasksParams(params);
60
61
  if (typeof params.status === 'string') params.status = params.status.trim();
61
62
  if (typeof params.name === 'string') params.name = params.name.trim();
@@ -96,7 +97,7 @@ module.exports = ({ db }) => async function getTasks(params) {
96
97
  }
97
98
  ];
98
99
 
99
- const [result] = await Task.aggregate(pipeline);
100
+ const [result] = await Task.aggregate(pipeline).option(omitNullish({ maxTimeMS: options?.maxTimeMS }));
100
101
  const tasks = result.tasks || [];
101
102
  const numDocs = (result.count && result.count[0] && result.count[0].total) || 0;
102
103
  const statusCounts = result.statusCounts?.[0] ?? {};
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const Archetype = require('archetype');
4
+ const omitNullish = require('../../helpers/omitNullish');
4
5
 
5
6
  const GetTasksOverTimeParams = new Archetype({
6
7
  start: { $type: Date },
@@ -10,7 +11,7 @@ const GetTasksOverTimeParams = new Archetype({
10
11
 
11
12
  const TRACKED_STATUSES = ['succeeded', 'failed', 'cancelled'];
12
13
 
13
- module.exports = ({ db }) => async function getTasksOverTime(params) {
14
+ module.exports = ({ db, options }) => async function getTasksOverTime(params) {
14
15
  params = new GetTasksOverTimeParams(params);
15
16
  const { Task } = db.models;
16
17
  const { start, end, bucketSizeMs } = params;
@@ -54,7 +55,7 @@ module.exports = ({ db }) => async function getTasksOverTime(params) {
54
55
  { $sort: { _id: 1 } }
55
56
  ];
56
57
 
57
- const results = await Task.aggregate(pipeline);
58
+ const results = await Task.aggregate(pipeline).option(omitNullish({ maxTimeMS: options?.maxTimeMS }));
58
59
 
59
60
  return results.map(r => {
60
61
  const bucket = { timestamp: r._id, succeeded: 0, failed: 0, cancelled: 0 };
@@ -2,6 +2,7 @@
2
2
 
3
3
  const Archetype = require('archetype');
4
4
  const mongoose = require('mongoose');
5
+ const omitNullish = require('../../helpers/omitNullish');
5
6
 
6
7
  const RescheduleTaskParams = new Archetype({
7
8
  taskId: {
@@ -14,12 +15,14 @@ const RescheduleTaskParams = new Archetype({
14
15
  }
15
16
  }).compile('RescheduleTaskParams');
16
17
 
17
- module.exports = ({ db }) => async function rescheduleTask(params) {
18
+ module.exports = ({ db, options }) => async function rescheduleTask(params) {
18
19
  params = new RescheduleTaskParams(params);
19
20
  const { taskId, scheduledAt } = params;
20
21
  const { Task } = db.models;
21
22
 
22
- const task = await Task.findOne({ _id: taskId }).orFail();
23
+ const task = await Task.findOne({ _id: taskId }).
24
+ setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS })).
25
+ orFail();
23
26
 
24
27
  if (scheduledAt < Date.now()) {
25
28
  throw new Error('Cannot reschedule a task for the past');
@@ -36,4 +39,4 @@ module.exports = ({ db }) => async function rescheduleTask(params) {
36
39
  return {
37
40
  task
38
41
  };
39
- };
42
+ };
@@ -2,6 +2,7 @@
2
2
 
3
3
  const Archetype = require('archetype');
4
4
  const mongoose = require('mongoose');
5
+ const omitNullish = require('../../helpers/omitNullish');
5
6
 
6
7
  const RunTaskParams = new Archetype({
7
8
  taskId: {
@@ -10,16 +11,18 @@ const RunTaskParams = new Archetype({
10
11
  }
11
12
  }).compile('RunTaskParams');
12
13
 
13
- module.exports = ({ db }) => async function runTask(params) {
14
+ module.exports = ({ db, options }) => async function runTask(params) {
14
15
  params = new RunTaskParams(params);
15
16
  const { taskId } = params;
16
17
  const { Task } = db.models;
17
18
 
18
- const task = await Task.findOne({ _id: taskId }).orFail();
19
+ const task = await Task.findOne({ _id: taskId }).
20
+ setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS })).
21
+ orFail();
19
22
 
20
23
  const executedTask = await Task.execute(task);
21
24
 
22
25
  return {
23
26
  task: executedTask
24
27
  };
25
- };
28
+ };
@@ -0,0 +1,5 @@
1
+ 'use strict';
2
+
3
+ module.exports = function omitNullish(obj) {
4
+ return Object.fromEntries(Object.entries(obj).filter(([, value]) => value != null));
5
+ };
package/backend/index.js CHANGED
@@ -16,10 +16,10 @@ module.exports = function backend(db, studioConnection, options) {
16
16
  }
17
17
 
18
18
  studioConnection = studioConnection ?? db;
19
- const Dashboard = studioConnection.model('__Studio_Dashboard', dashboardSchema, 'studio__dashboards');
20
- const DashboardResult = studioConnection.model('__Studio_DashboardResult', dashboardResultSchema, 'studio__dashboardResults');
21
- const ChatMessage = studioConnection.model('__Studio_ChatMessage', chatMessageSchema, 'studio__chatMessages');
22
- const ChatThread = studioConnection.model('__Studio_ChatThread', chatThreadSchema, 'studio__chatThreads');
19
+ studioConnection.model('__Studio_Dashboard', dashboardSchema, 'studio__dashboards');
20
+ studioConnection.model('__Studio_DashboardResult', dashboardResultSchema, 'studio__dashboardResults');
21
+ studioConnection.model('__Studio_ChatMessage', chatMessageSchema, 'studio__chatMessages');
22
+ studioConnection.model('__Studio_ChatThread', chatThreadSchema, 'studio__chatThreads');
23
23
 
24
24
  let changeStream = null;
25
25
  if (options?.changeStream) {
@@ -5,7 +5,7 @@ const mongoose = require('mongoose');
5
5
  const vm = require('vm');
6
6
  const { createScriptDb } = require('./createScriptDb');
7
7
 
8
- module.exports = function createSandbox({ db }) {
8
+ module.exports = function createSandbox({ db, maxTimeMS }) {
9
9
  const logs = [];
10
10
 
11
11
  // db must be a connection, not a Mongoose instance
@@ -13,7 +13,7 @@ module.exports = function createSandbox({ db }) {
13
13
  db = db.connection;
14
14
  }
15
15
 
16
- const scriptDb = createScriptDb(db);
16
+ const scriptDb = createScriptDb(db, { maxTimeMS });
17
17
  if (!scriptDb.db.Types) {
18
18
  scriptDb.db.Types = mongoose.Types;
19
19
  }
@@ -1,8 +1,11 @@
1
1
  'use strict';
2
2
 
3
+ const omitNullish = require('../helpers/omitNullish');
4
+
3
5
  const wrappedCollections = new WeakSet();
4
6
 
5
7
  const collectionMethodOptionsIndex = new Map([
8
+ ['aggregate', 1],
6
9
  ['bulkWrite', 1],
7
10
  ['countDocuments', 1],
8
11
  ['createIndex', 1],
@@ -21,26 +24,34 @@ const collectionMethodOptionsIndex = new Map([
21
24
  ['findOneAndUpdate', 2],
22
25
  ['insertMany', 1],
23
26
  ['insertOne', 1],
27
+ ['listIndexes', 0],
24
28
  ['rename', 1],
25
29
  ['replaceOne', 2],
26
30
  ['updateMany', 2],
27
31
  ['updateOne', 2]
28
32
  ]);
29
33
 
30
- function createScriptDb(db) {
34
+ function createScriptDb(db, options = {}) {
31
35
  const sourceConnection = db;
32
36
 
33
37
  const scriptConnection = sourceConnection.useDb(sourceConnection.name, { useCache: false });
38
+ // Mongoose useDb() shares the source connection's options object. Copy it
39
+ // before setting sandbox-only options so the application connection remains untouched.
40
+ scriptConnection.options = { ...scriptConnection.options };
34
41
  scriptConnection.config = {
35
42
  ...scriptConnection.config,
36
43
  autoCreate: false,
37
44
  autoIndex: false
38
45
  };
46
+ if (options.maxTimeMS != null) {
47
+ scriptConnection.set('maxTimeMS', options.maxTimeMS);
48
+ }
39
49
 
40
50
  let dryRunSession = null;
41
51
  const getSession = () => dryRunSession;
42
- cloneModels(sourceConnection, scriptConnection, getSession);
43
- wrapCollectionAccessors(scriptConnection, getSession);
52
+ const getMaxTimeMS = () => options.maxTimeMS;
53
+ cloneModels(sourceConnection, scriptConnection, getSession, getMaxTimeMS);
54
+ wrapCollectionAccessors(scriptConnection, getSession, getMaxTimeMS);
44
55
 
45
56
  const originalDebug = sourceConnection.options?.debug;
46
57
  scriptConnection.set('debug', function() {
@@ -68,14 +79,14 @@ function createScriptDb(db) {
68
79
  };
69
80
  }
70
81
 
71
- function cloneModels(sourceConnection, scriptConnection, getSession) {
82
+ function cloneModels(sourceConnection, scriptConnection, getSession, getMaxTimeMS) {
72
83
  for (const Model of Object.values(sourceConnection.models ?? {})) {
73
84
  if (Model?.schema == null || typeof scriptConnection.model !== 'function') {
74
85
  continue;
75
86
  }
76
87
 
77
88
  const ClonedModel = scriptConnection.model(Model.modelName, Model.schema, getCollectionName(Model));
78
- wrapModelCollections(ClonedModel, getSession);
89
+ wrapModelCollections(ClonedModel, getSession, getMaxTimeMS);
79
90
  }
80
91
  }
81
92
 
@@ -83,13 +94,13 @@ function getCollectionName(Model) {
83
94
  return Model.collection?.collectionName ?? Model.collection?.name ?? Model.$__collection?.collectionName;
84
95
  }
85
96
 
86
- function wrapModelCollections(Model, getSession) {
97
+ function wrapModelCollections(Model, getSession, getMaxTimeMS) {
87
98
  const collection = Model?.collection ?? Model?.$__collection;
88
99
  if (collection == null) {
89
100
  return;
90
101
  }
91
102
 
92
- const wrappedCollection = wrapCollection(collection, getSession);
103
+ const wrappedCollection = wrapCollection(collection, getSession, getMaxTimeMS);
93
104
  Model.collection = wrappedCollection;
94
105
  Model.$__collection = wrappedCollection;
95
106
 
@@ -108,7 +119,7 @@ function setModelCollectionSymbols(target, collection) {
108
119
  }
109
120
  }
110
121
 
111
- function wrapCollectionAccessors(scriptConnection, getSession) {
122
+ function wrapCollectionAccessors(scriptConnection, getSession, getMaxTimeMS) {
112
123
  const wrappedCollectionCache = new WeakMap();
113
124
 
114
125
  const nativeDb = scriptConnection.db;
@@ -125,19 +136,19 @@ function wrapCollectionAccessors(scriptConnection, getSession) {
125
136
  collection.collection = originalDbCollection.call(nativeDb, collection.name);
126
137
  }
127
138
  }
128
- return getOrCreateWrappedCollection(wrappedCollectionCache, collection, getSession);
139
+ return getOrCreateWrappedCollection(wrappedCollectionCache, collection, getSession, getMaxTimeMS);
129
140
  };
130
141
  }
131
142
 
132
143
  if (originalDbCollection != null) {
133
144
  nativeDb.collection = function() {
134
145
  const collection = originalDbCollection.apply(this, arguments);
135
- return getOrCreateWrappedCollection(wrappedCollectionCache, collection, getSession);
146
+ return getOrCreateWrappedCollection(wrappedCollectionCache, collection, getSession, getMaxTimeMS);
136
147
  };
137
148
  }
138
149
  }
139
150
 
140
- function getOrCreateWrappedCollection(cache, collection, getSession) {
151
+ function getOrCreateWrappedCollection(cache, collection, getSession, getMaxTimeMS) {
141
152
  if (collection == null || (typeof collection !== 'object' && typeof collection !== 'function')) {
142
153
  return collection;
143
154
  }
@@ -147,12 +158,12 @@ function getOrCreateWrappedCollection(cache, collection, getSession) {
147
158
  if (cache.has(collection)) {
148
159
  return cache.get(collection);
149
160
  }
150
- const wrapped = wrapCollection(collection, getSession);
161
+ const wrapped = wrapCollection(collection, getSession, getMaxTimeMS);
151
162
  cache.set(collection, wrapped);
152
163
  return wrapped;
153
164
  }
154
165
 
155
- function wrapCollection(collection, getSession) {
166
+ function wrapCollection(collection, getSession, getMaxTimeMS) {
156
167
  if (wrappedCollections.has(collection)) {
157
168
  return collection;
158
169
  }
@@ -165,15 +176,20 @@ function wrapCollection(collection, getSession) {
165
176
  }
166
177
 
167
178
  wrapped[methodName] = function() {
168
- const args = addSessionOption(Array.from(arguments), optionsIndex, getSession());
179
+ const args = addOperationOptions(
180
+ Array.from(arguments),
181
+ optionsIndex,
182
+ getSession(),
183
+ getMaxTimeMS()
184
+ );
169
185
  return method.apply(collection, args);
170
186
  };
171
187
  }
172
188
  return wrapped;
173
189
  }
174
190
 
175
- function addSessionOption(args, optionsIndex, session) {
176
- if (session == null) {
191
+ function addOperationOptions(args, optionsIndex, session, maxTimeMS) {
192
+ if (session == null && maxTimeMS == null) {
177
193
  return args;
178
194
  }
179
195
 
@@ -184,14 +200,21 @@ function addSessionOption(args, optionsIndex, session) {
184
200
  const options = args[optionsIndex];
185
201
 
186
202
  if (options != null && typeof options !== 'object') {
187
- throw new Error('Cannot run dry run on script where options arg is a non-object');
203
+ if (session != null) {
204
+ throw new Error('Cannot run dry run on script where options arg is a non-object');
205
+ }
206
+ throw new Error('Cannot apply maxTimeMS to script where options arg is a non-object');
188
207
  }
189
208
 
190
- if (options?.session != null) {
209
+ if (session != null && options?.session != null) {
191
210
  throw new Error('Cannot run dry run on script that uses sessions');
192
211
  }
193
212
 
194
- args[optionsIndex] = { ...(options ?? {}), session };
213
+ args[optionsIndex] = omitNullish({
214
+ ...(options ?? {}),
215
+ maxTimeMS: maxTimeMS ?? options?.maxTimeMS,
216
+ session: session ?? options?.session
217
+ });
195
218
  return args;
196
219
  }
197
220