@mongoosejs/studio 0.5.14 → 0.6.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.
Files changed (81) hide show
  1. package/AGENTS.md +69 -2
  2. package/README.md +24 -0
  3. package/backend/actions/ChatMessage/executeScript.js +14 -6
  4. package/backend/actions/ChatThread/createChatMessage.js +7 -4
  5. package/backend/actions/ChatThread/getChatThread.js +7 -4
  6. package/backend/actions/ChatThread/listChatThreads.js +5 -2
  7. package/backend/actions/ChatThread/shareChatThread.js +5 -2
  8. package/backend/actions/ChatThread/streamChatMessage.js +7 -4
  9. package/backend/actions/ChatThread/toggleAgentMode.js +5 -2
  10. package/backend/actions/Dashboard/createDashboard.js +3 -0
  11. package/backend/actions/Dashboard/deleteDashboard.js +9 -3
  12. package/backend/actions/Dashboard/getDashboard.js +23 -11
  13. package/backend/actions/Dashboard/getDashboards.js +8 -2
  14. package/backend/actions/Dashboard/updateDashboard.js +8 -2
  15. package/backend/actions/Model/addField.js +8 -2
  16. package/backend/actions/Model/analyzeSchema.js +15 -5
  17. package/backend/actions/Model/createChatMessage.js +3 -0
  18. package/backend/actions/Model/createDocument.js +3 -0
  19. package/backend/actions/Model/deleteDocument.js +8 -2
  20. package/backend/actions/Model/deleteDocuments.js +8 -2
  21. package/backend/actions/Model/dropCollection.js +3 -0
  22. package/backend/actions/Model/dropIndex.js +3 -0
  23. package/backend/actions/Model/executeDocumentScript.js +14 -3
  24. package/backend/actions/Model/exportQueryResults.js +12 -2
  25. package/backend/actions/Model/getCollectionInfo.js +14 -4
  26. package/backend/actions/Model/getDocument.js +12 -2
  27. package/backend/actions/Model/getDocuments.js +13 -3
  28. package/backend/actions/Model/getDocumentsStream.js +13 -3
  29. package/backend/actions/Model/getEstimatedDocumentCounts.js +12 -2
  30. package/backend/actions/Model/getIndexes.js +3 -0
  31. package/backend/actions/Model/getSuggestedProjection.js +3 -0
  32. package/backend/actions/Model/listModels.js +3 -0
  33. package/backend/actions/Model/streamChatMessage.js +3 -0
  34. package/backend/actions/Model/streamDocumentChanges.js +3 -0
  35. package/backend/actions/Model/updateDocument.js +8 -2
  36. package/backend/actions/Model/updateDocuments.js +8 -2
  37. package/backend/actions/Model/validateDocument.js +12 -2
  38. package/backend/actions/Script/createScript.js +36 -0
  39. package/backend/actions/Script/executeScript.js +89 -0
  40. package/backend/actions/Script/getScript.js +43 -0
  41. package/backend/actions/Script/index.js +5 -0
  42. package/backend/actions/Task/cancelTask.js +5 -2
  43. package/backend/actions/Task/getTaskOverview.js +6 -3
  44. package/backend/actions/Task/getTasks.js +6 -3
  45. package/backend/actions/Task/getTasksOverTime.js +6 -3
  46. package/backend/actions/Task/rescheduleTask.js +5 -2
  47. package/backend/actions/Task/runTask.js +5 -2
  48. package/backend/actions/index.js +1 -0
  49. package/backend/authorize.js +6 -1
  50. package/backend/db/scriptSchema.js +29 -0
  51. package/backend/helpers/operationOptions.js +26 -0
  52. package/backend/helpers/readOperationOptions.js +31 -0
  53. package/backend/index.js +2 -0
  54. package/backend/integrations/mothership.js +28 -0
  55. package/backend/mcp.js +140 -0
  56. package/backend/netlify.js +5 -3
  57. package/backend/next.js +5 -3
  58. package/backend/sandbox/createSandbox.js +2 -2
  59. package/backend/sandbox/createScriptDb.js +40 -23
  60. package/backend/util/archetypeToZodSchema.js +74 -0
  61. package/docs/mcp-oauth.md +106 -0
  62. package/eslint.config.js +1 -0
  63. package/eval/agent/run.js +1 -1
  64. package/express.js +183 -41
  65. package/frontend/index.js +3 -0
  66. package/frontend/public/app.js +441 -48
  67. package/frontend/public/tw.css +8 -0
  68. package/frontend/src/api.js +22 -0
  69. package/frontend/src/index.js +9 -1
  70. package/frontend/src/mcp/mcp.html +55 -0
  71. package/frontend/src/mcp/mcp.js +27 -0
  72. package/frontend/src/navbar/navbar.html +8 -0
  73. package/frontend/src/navbar/navbar.js +3 -0
  74. package/frontend/src/routes.js +16 -0
  75. package/frontend/src/script/script.html +105 -0
  76. package/frontend/src/script/script.js +98 -0
  77. package/index.d.ts +10 -1
  78. package/nest.d.ts +3 -0
  79. package/package.json +5 -3
  80. package/vercel.d.ts +2 -0
  81. package/CLAUDE.md +0 -7
@@ -32,3 +32,6 @@ module.exports = ({ db }) => async function CreateDocument(params) {
32
32
 
33
33
  return { doc };
34
34
  };
35
+
36
+ module.exports.paramsType = CreateDocumentParams;
37
+ module.exports.tags = [];
@@ -2,7 +2,7 @@
2
2
 
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
- const omitNullish = require('../../helpers/omitNullish');
5
+ const operationOptions = require('../../helpers/operationOptions');
6
6
 
7
7
  const DeleteDocumentParams = new Archetype({
8
8
  model: {
@@ -15,6 +15,9 @@ const DeleteDocumentParams = new Archetype({
15
15
  },
16
16
  roles: {
17
17
  $type: ['string']
18
+ },
19
+ maxTimeMS: {
20
+ $type: 'number'
18
21
  }
19
22
  }).compile('DeleteDocumentParams');
20
23
 
@@ -31,8 +34,11 @@ module.exports = ({ db, options }) => async function DeleteDocument(params) {
31
34
 
32
35
  const doc = await Model.
33
36
  deleteOne({ _id: documentId }).
34
- setOptions(omitNullish({ sanitizeFilter: true, maxTimeMS: options?.maxTimeMS })).
37
+ setOptions(operationOptions(options, params, { sanitizeFilter: true })).
35
38
  orFail();
36
39
 
37
40
  return { doc };
38
41
  };
42
+
43
+ module.exports.paramsType = DeleteDocumentParams;
44
+ module.exports.tags = ['destructive'];
@@ -2,7 +2,7 @@
2
2
 
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
- const omitNullish = require('../../helpers/omitNullish');
5
+ const operationOptions = require('../../helpers/operationOptions');
6
6
 
7
7
  const DeleteDocumentsParams = new Archetype({
8
8
  model: {
@@ -15,6 +15,9 @@ const DeleteDocumentsParams = new Archetype({
15
15
  },
16
16
  roles: {
17
17
  $type: ['string']
18
+ },
19
+ maxTimeMS: {
20
+ $type: 'number'
18
21
  }
19
22
  }).compile('DeleteDocumentsParams');
20
23
 
@@ -31,8 +34,11 @@ module.exports = ({ db, options }) => async function DeleteDocuments(params) {
31
34
 
32
35
  await Model.
33
36
  deleteMany({ _id: { $in: documentIds } }).
34
- setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS })).
37
+ setOptions(operationOptions(options, params)).
35
38
  orFail();
36
39
 
37
40
  return { };
38
41
  };
42
+
43
+ module.exports.paramsType = DeleteDocumentsParams;
44
+ module.exports.tags = ['destructive'];
@@ -27,3 +27,6 @@ module.exports = ({ db }) => async function dropCollection(params) {
27
27
 
28
28
  return { ok: true };
29
29
  };
30
+
31
+ module.exports.paramsType = DropCollectionParams;
32
+ module.exports.tags = ['destructive'];
@@ -34,3 +34,6 @@ module.exports = ({ db }) => async function getIndexes(params) {
34
34
  mongoDBIndexes
35
35
  };
36
36
  };
37
+
38
+ module.exports.paramsType = DropIndexParams;
39
+ module.exports.tags = ['destructive'];
@@ -3,7 +3,8 @@
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
5
  const createSandbox = require('../../sandbox/createSandbox');
6
- const omitNullish = require('../../helpers/omitNullish');
6
+ const readOperationOptions = require('../../helpers/readOperationOptions');
7
+ const operationOptions = require('../../helpers/operationOptions');
7
8
 
8
9
  const ExecuteDocumentScriptParams = new Archetype({
9
10
  model: {
@@ -20,6 +21,13 @@ const ExecuteDocumentScriptParams = new Archetype({
20
21
  },
21
22
  roles: {
22
23
  $type: ['string']
24
+ },
25
+ maxTimeMS: {
26
+ $type: 'number'
27
+ },
28
+ readPreference: {
29
+ $type: 'string',
30
+ $enum: ['secondary', 'secondaryPreferred', 'primary']
23
31
  }
24
32
  }).compile('ExecuteDocumentScriptParams');
25
33
 
@@ -32,12 +40,12 @@ module.exports = ({ db, options }) => async function executeDocumentScript(param
32
40
  throw new Error(`Model ${model} not found`);
33
41
  }
34
42
 
35
- const sandbox = createSandbox({ db, maxTimeMS: options?.maxTimeMS });
43
+ const sandbox = createSandbox(db, { ...readOperationOptions(options, params) });
36
44
 
37
45
  try {
38
46
  const Model = sandbox.db.models[model];
39
47
  const doc = await Model.findById(documentId).
40
- setOptions(omitNullish({ sanitizeFilter: true, maxTimeMS: options?.maxTimeMS })).
48
+ setOptions(operationOptions(options, params, { sanitizeFilter: true })).
41
49
  orFail();
42
50
  sandbox.context.doc = doc;
43
51
 
@@ -55,3 +63,6 @@ module.exports = ({ db, options }) => async function executeDocumentScript(param
55
63
  }
56
64
  }
57
65
  };
66
+
67
+ module.exports.paramsType = ExecuteDocumentScriptParams;
68
+ module.exports.tags = [];
@@ -5,7 +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
+ const readOperationOptions = require('../../helpers/readOperationOptions');
9
9
 
10
10
  const GetDocumentsParams = new Archetype({
11
11
  model: {
@@ -27,6 +27,13 @@ const GetDocumentsParams = new Archetype({
27
27
  },
28
28
  roles: {
29
29
  $type: ['string']
30
+ },
31
+ maxTimeMS: {
32
+ $type: 'number'
33
+ },
34
+ readPreference: {
35
+ $type: 'string',
36
+ $enum: ['secondary', 'secondaryPreferred', 'primary']
30
37
  }
31
38
  }).compile('GetDocumentsParams');
32
39
 
@@ -46,7 +53,7 @@ module.exports = ({ db, options }) => async function exportQueryResults(params,
46
53
 
47
54
  const docs = await Model.
48
55
  find(filter).
49
- setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS })).
56
+ setOptions(readOperationOptions(options, params)).
50
57
  sort({ _id: -1 });
51
58
 
52
59
  const rows = [propertiesToInclude];
@@ -68,3 +75,6 @@ module.exports = ({ db, options }) => async function exportQueryResults(params,
68
75
  res.setHeader('Content-Disposition', `attachment; filename="${model.toLowerCase()}-export.csv"`);
69
76
  return csv;
70
77
  };
78
+
79
+ module.exports.paramsType = GetDocumentsParams;
80
+ module.exports.tags = ['readOnly'];
@@ -2,7 +2,7 @@
2
2
 
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
- const omitNullish = require('../../helpers/omitNullish');
5
+ const readOperationOptions = require('../../helpers/readOperationOptions');
6
6
 
7
7
  const GetCollectionInfoParams = new Archetype({
8
8
  model: {
@@ -11,6 +11,13 @@ const GetCollectionInfoParams = new Archetype({
11
11
  },
12
12
  roles: {
13
13
  $type: ['string']
14
+ },
15
+ maxTimeMS: {
16
+ $type: 'number'
17
+ },
18
+ readPreference: {
19
+ $type: 'string',
20
+ $enum: ['secondary', 'secondaryPreferred', 'primary']
14
21
  }
15
22
  }).compile('GetCollectionInfoParams');
16
23
 
@@ -24,9 +31,9 @@ module.exports = ({ db, options }) => async function getCollectionInfo(params) {
24
31
  throw new Error(`Model ${model} not found`);
25
32
  }
26
33
 
27
- const operationOptions = omitNullish({ maxTimeMS: options?.maxTimeMS });
34
+ const dbOptions = readOperationOptions(options, params);
28
35
  const [collectionOptions, stats] = await Promise.all([
29
- Model.collection.options(operationOptions),
36
+ Model.collection.options(dbOptions),
30
37
  Model.aggregate([
31
38
  {
32
39
  $collStats: {
@@ -34,7 +41,7 @@ module.exports = ({ db, options }) => async function getCollectionInfo(params) {
34
41
  count: {}
35
42
  }
36
43
  }
37
- ]).option(operationOptions).then(res => res[0] ?? {})
44
+ ]).option(dbOptions).then(res => res[0] ?? {})
38
45
  ]);
39
46
 
40
47
  return {
@@ -49,3 +56,6 @@ module.exports = ({ db, options }) => async function getCollectionInfo(params) {
49
56
  }
50
57
  };
51
58
  };
59
+
60
+ module.exports.paramsType = GetCollectionInfoParams;
61
+ module.exports.tags = ['readOnly'];
@@ -4,7 +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
+ const readOperationOptions = require('../../helpers/readOperationOptions');
8
8
 
9
9
  const GetDocumentParams = new Archetype({
10
10
  model: {
@@ -17,6 +17,13 @@ const GetDocumentParams = new Archetype({
17
17
  },
18
18
  roles: {
19
19
  $type: ['string']
20
+ },
21
+ maxTimeMS: {
22
+ $type: 'number'
23
+ },
24
+ readPreference: {
25
+ $type: 'string',
26
+ $enum: ['secondary', 'secondaryPreferred', 'primary']
20
27
  }
21
28
  }).compile('GetDocumentParams');
22
29
 
@@ -32,7 +39,7 @@ module.exports = ({ db, options }) => async function getDocument(params) {
32
39
 
33
40
  const doc = await Model.
34
41
  findById(documentId).
35
- setOptions(omitNullish({ sanitizeFilter: true, maxTimeMS: options?.maxTimeMS })).
42
+ setOptions(readOperationOptions(options, { sanitizeFilter: true })).
36
43
  orFail();
37
44
  const schemaPaths = {};
38
45
  for (const path of Object.keys(Model.schema.paths)) {
@@ -50,3 +57,6 @@ module.exports = ({ db, options }) => async function getDocument(params) {
50
57
 
51
58
  return { doc: doc.toJSON({ virtuals: true, getters: false, transform: false }), schemaPaths, virtualPaths };
52
59
  };
60
+
61
+ module.exports.paramsType = GetDocumentParams;
62
+ module.exports.tags = ['readOnly'];
@@ -7,7 +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
+ const readOperationOptions = require('../../helpers/readOperationOptions');
11
11
 
12
12
  const GetDocumentsParams = new Archetype({
13
13
  model: {
@@ -38,6 +38,13 @@ const GetDocumentsParams = new Archetype({
38
38
  },
39
39
  roles: {
40
40
  $type: ['string']
41
+ },
42
+ maxTimeMS: {
43
+ $type: 'number'
44
+ },
45
+ readPreference: {
46
+ $type: 'string',
47
+ $enum: ['secondary', 'secondaryPreferred', 'primary']
41
48
  }
42
49
  }).compile('GetDocumentsParams');
43
50
 
@@ -76,7 +83,7 @@ module.exports = ({ db, options }) => async function getDocuments(params) {
76
83
  if (projection != null) {
77
84
  query = query.select(projection);
78
85
  }
79
- query.setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS }));
86
+ query.setOptions(readOperationOptions(options, params));
80
87
  const cursor = await query.cursor();
81
88
  const docs = [];
82
89
  for (let doc = await cursor.next(); doc != null; doc = await cursor.next()) {
@@ -114,7 +121,7 @@ module.exports = ({ db, options }) => async function getDocuments(params) {
114
121
  let numDocuments = null;
115
122
  let numDocsError = null;
116
123
  try {
117
- numDocuments = await countQuery.setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS }));
124
+ numDocuments = await countQuery.setOptions(readOperationOptions(options, params));
118
125
  } catch (err) {
119
126
  numDocsError = err?.message;
120
127
  }
@@ -129,3 +136,6 @@ module.exports = ({ db, options }) => async function getDocuments(params) {
129
136
  numDocsError
130
137
  };
131
138
  };
139
+
140
+ module.exports.paramsType = GetDocumentsParams;
141
+ module.exports.tags = ['readOnly'];
@@ -7,7 +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
+ const readOperationOptions = require('../../helpers/readOperationOptions');
11
11
 
12
12
  const GetDocumentsParams = new Archetype({
13
13
  model: {
@@ -38,6 +38,13 @@ const GetDocumentsParams = new Archetype({
38
38
  },
39
39
  roles: {
40
40
  $type: ['string']
41
+ },
42
+ maxTimeMS: {
43
+ $type: 'number'
44
+ },
45
+ readPreference: {
46
+ $type: 'string',
47
+ $enum: ['secondary', 'secondaryPreferred', 'primary']
41
48
  }
42
49
  }).compile('GetDocumentsParams');
43
50
 
@@ -76,7 +83,7 @@ module.exports = ({ db, options }) => async function* getDocumentsStream(params)
76
83
  if (projection != null) {
77
84
  query = query.select(projection);
78
85
  }
79
- query.setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS }));
86
+ query.setOptions(readOperationOptions(options, params));
80
87
 
81
88
  const schemaPaths = {};
82
89
  for (const path of Object.keys(Model.schema.paths)) {
@@ -112,7 +119,7 @@ module.exports = ({ db, options }) => async function* getDocumentsStream(params)
112
119
  ? Model.estimatedDocumentCount()
113
120
  : Model.countDocuments(filter);
114
121
  const numDocsPromise = countQuery.
115
- setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS })).
122
+ setOptions(readOperationOptions(options, params)).
116
123
  exec();
117
124
 
118
125
  let numDocsYielded = false;
@@ -159,3 +166,6 @@ module.exports = ({ db, options }) => async function* getDocumentsStream(params)
159
166
  await cursor.close();
160
167
  }
161
168
  };
169
+
170
+ module.exports.paramsType = GetDocumentsParams;
171
+ module.exports.tags = ['readOnly'];
@@ -2,11 +2,18 @@
2
2
 
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
- const omitNullish = require('../../helpers/omitNullish');
5
+ const readOperationOptions = require('../../helpers/readOperationOptions');
6
6
 
7
7
  const GetEstimatedDocumentCountsParams = new Archetype({
8
8
  roles: {
9
9
  $type: ['string']
10
+ },
11
+ maxTimeMS: {
12
+ $type: 'number'
13
+ },
14
+ readPreference: {
15
+ $type: 'string',
16
+ $enum: ['secondary', 'secondaryPreferred', 'primary']
10
17
  }
11
18
  }).compile('GetEstimatedDocumentCountsParams');
12
19
 
@@ -22,7 +29,7 @@ module.exports = ({ db, options }) => async function getEstimatedDocumentCounts(
22
29
  modelNames.map(name => {
23
30
  const Model = db.models[name];
24
31
  return Model.estimatedDocumentCount().
25
- setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS })).
32
+ setOptions(readOperationOptions(options, params)).
26
33
  exec();
27
34
  })
28
35
  );
@@ -39,3 +46,6 @@ module.exports = ({ db, options }) => async function getEstimatedDocumentCounts(
39
46
 
40
47
  return { counts };
41
48
  };
49
+
50
+ module.exports.paramsType = GetEstimatedDocumentCountsParams;
51
+ module.exports.tags = ['readOnly'];
@@ -35,3 +35,6 @@ module.exports = ({ db }) => async function getIndexes(params) {
35
35
  diffIndexes
36
36
  };
37
37
  };
38
+
39
+ module.exports.paramsType = GetDocumentsParams;
40
+ module.exports.tags = ['readOnly'];
@@ -31,3 +31,6 @@ module.exports = ({ db }) => async function getSuggestedProjectionAction(params)
31
31
 
32
32
  return { suggestedFields };
33
33
  };
34
+
35
+ module.exports.paramsType = GetSuggestedProjectionParams;
36
+ module.exports.tags = ['readOnly'];
@@ -55,3 +55,6 @@ module.exports = ({ db }) => async function listModels(params) {
55
55
  readyState
56
56
  };
57
57
  };
58
+
59
+ module.exports.paramsType = ListModelsParams;
60
+ module.exports.tags = ['readOnly'];
@@ -78,3 +78,6 @@ const systemPrompt = `
78
78
 
79
79
  Return only the updated document body as a JavaScript object literal. Do not use Markdown or code fences.
80
80
  `.trim();
81
+
82
+ module.exports.paramsType = StreamChatMessageParams;
83
+ module.exports.tags = ['readOnly'];
@@ -122,3 +122,6 @@ module.exports = ({ db, changeStream }) => async function* streamDocumentChanges
122
122
  }
123
123
  }
124
124
  };
125
+
126
+ module.exports.paramsType = StreamDocumentChangesParams;
127
+ module.exports.tags = ['readOnly'];
@@ -2,7 +2,7 @@
2
2
 
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
- const omitNullish = require('../../helpers/omitNullish');
5
+ const operationOptions = require('../../helpers/operationOptions');
6
6
 
7
7
  const UpdateDocumentsParams = new Archetype({
8
8
  model: {
@@ -22,6 +22,9 @@ const UpdateDocumentsParams = new Archetype({
22
22
  },
23
23
  roles: {
24
24
  $type: ['string']
25
+ },
26
+ maxTimeMS: {
27
+ $type: 'number'
25
28
  }
26
29
  }).compile('UpdateDocumentsParams');
27
30
 
@@ -60,6 +63,9 @@ module.exports = ({ db, options }) => async function updateDocument(params) {
60
63
 
61
64
  const doc = await Model.
62
65
  findByIdAndUpdate(_id, updateOperation, { sanitizeFilter: true, returnDocument: 'after', overwriteImmutable: true, runValidators: false }).
63
- setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS }));
66
+ setOptions(operationOptions(options, params));
64
67
  return { doc };
65
68
  };
69
+
70
+ module.exports.paramsType = UpdateDocumentsParams;
71
+ module.exports.tags = [];
@@ -2,7 +2,7 @@
2
2
 
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
- const omitNullish = require('../../helpers/omitNullish');
5
+ const operationOptions = require('../../helpers/operationOptions');
6
6
 
7
7
  const UpdateDocumentsParams = new Archetype({
8
8
  model: {
@@ -19,6 +19,9 @@ const UpdateDocumentsParams = new Archetype({
19
19
  },
20
20
  roles: {
21
21
  $type: ['string']
22
+ },
23
+ maxTimeMS: {
24
+ $type: 'number'
22
25
  }
23
26
  }).compile('UpdateDocumentsParams');
24
27
 
@@ -62,7 +65,10 @@ module.exports = ({ db, options }) => async function updateDocuments(params) {
62
65
 
63
66
  const result = await Model.
64
67
  updateMany({ _id: { $in: _id } }, updateOperation, { overwriteImmutable: true, runValidators: false }).
65
- setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS }));
68
+ setOptions(operationOptions(options, params));
66
69
 
67
70
  return { result };
68
71
  };
72
+
73
+ module.exports.paramsType = UpdateDocumentsParams;
74
+ module.exports.tags = [];
@@ -2,7 +2,7 @@
2
2
 
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
- const omitNullish = require('../../helpers/omitNullish');
5
+ const readOperationOptions = require('../../helpers/readOperationOptions');
6
6
  const validateDocumentWithTimeout = require('../../helpers/validateDocumentWithTimeout');
7
7
 
8
8
  const ValidateDocumentParams = new Archetype({
@@ -16,6 +16,13 @@ const ValidateDocumentParams = new Archetype({
16
16
  },
17
17
  roles: {
18
18
  $type: ['string']
19
+ },
20
+ maxTimeMS: {
21
+ $type: 'number'
22
+ },
23
+ readPreference: {
24
+ $type: 'string',
25
+ $enum: ['secondary', 'secondaryPreferred', 'primary']
19
26
  }
20
27
  }).compile('ValidateDocumentParams');
21
28
 
@@ -30,9 +37,12 @@ module.exports = ({ db, options }) => async function validateDocument(params) {
30
37
  }
31
38
 
32
39
  const doc = await Model.findById(documentId).
33
- setOptions(omitNullish({ sanitizeFilter: true, maxTimeMS: options?.maxTimeMS })).
40
+ setOptions(readOperationOptions(options, { sanitizeFilter: true })).
34
41
  orFail();
35
42
  return {
36
43
  result: await validateDocumentWithTimeout(doc)
37
44
  };
38
45
  };
46
+
47
+ module.exports.paramsType = ValidateDocumentParams;
48
+ module.exports.tags = ['readOnly'];
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ const Archetype = require('archetype');
4
+ const mongoose = require('mongoose');
5
+
6
+ const authorize = require('../../authorize');
7
+
8
+ const CreateScriptParams = new Archetype({
9
+ initiatedById: {
10
+ $type: mongoose.Types.ObjectId
11
+ },
12
+ script: {
13
+ $type: 'string',
14
+ $required: true
15
+ },
16
+ roles: {
17
+ $type: ['string']
18
+ }
19
+ }).compile('CreateScriptParams');
20
+
21
+ module.exports = ({ studioConnection, options }) => async function createScript(params) {
22
+ const { initiatedById, script, roles } = new CreateScriptParams(params);
23
+
24
+ await authorize('Script.createScript', roles);
25
+
26
+ const Script = studioConnection.model('__Studio_Script');
27
+ const createdScript = await Script.create({ script, userId: initiatedById });
28
+ const baseUrl = options?.workspace?.baseUrl?.replace(/\/+$/, '');
29
+ const url = `${baseUrl ? `${baseUrl}/` : ''}#/script/${createdScript._id}`;
30
+
31
+ return { script: createdScript, url };
32
+ };
33
+
34
+ module.exports.paramsType = CreateScriptParams;
35
+ module.exports.tags = [];
36
+ module.exports.description = 'Create a script for the user to review, edit, dry run, or run in Mongoose Studio. This tool does not execute the script and returns a review URL.';
@@ -0,0 +1,89 @@
1
+ 'use strict';
2
+
3
+ const Archetype = require('archetype');
4
+ const mongoose = require('mongoose');
5
+
6
+ const authorize = require('../../authorize');
7
+ const readOperationOptions = require('../../helpers/readOperationOptions');
8
+ const createSandbox = require('../../sandbox/createSandbox');
9
+
10
+ const ExecuteScriptParams = new Archetype({
11
+ initiatedById: {
12
+ $type: mongoose.Types.ObjectId
13
+ },
14
+ scriptId: {
15
+ $type: mongoose.Types.ObjectId,
16
+ $required: true
17
+ },
18
+ script: {
19
+ $type: 'string'
20
+ },
21
+ dryRun: {
22
+ $type: 'boolean',
23
+ $default: false
24
+ },
25
+ roles: {
26
+ $type: ['string']
27
+ },
28
+ maxTimeMS: {
29
+ $type: 'number'
30
+ },
31
+ readPreference: {
32
+ $type: 'string',
33
+ $enum: ['secondary', 'secondaryPreferred', 'primary']
34
+ }
35
+ }).compile('ExecuteScriptParams');
36
+
37
+ module.exports = ({ db, studioConnection, options }) => async function executeScript(params) {
38
+ const { initiatedById, scriptId, script, dryRun, roles } = new ExecuteScriptParams(params);
39
+
40
+ await authorize('Script.executeScript', roles);
41
+
42
+ const Script = studioConnection.model('__Studio_Script');
43
+ const scriptDoc = await Script.findById(scriptId);
44
+ if (scriptDoc == null) {
45
+ throw new Error('Script not found');
46
+ }
47
+ if (initiatedById && scriptDoc.userId?.toString() !== initiatedById.toString()) {
48
+ throw new Error('Unauthorized');
49
+ }
50
+
51
+ let output = null;
52
+ let error = null;
53
+
54
+ if (script != null) {
55
+ scriptDoc.script = script;
56
+ }
57
+ scriptDoc.dryRun = dryRun;
58
+ scriptDoc.executionStartedAt = new Date();
59
+ scriptDoc.executionFinishedAt = null;
60
+ scriptDoc.executionResult = null;
61
+ await scriptDoc.save();
62
+
63
+ const sandbox = createSandbox(db, { ...readOperationOptions(options, params) });
64
+ try {
65
+ output = await sandbox.runScript({ script: scriptDoc.script, dryRun });
66
+ } catch (err) {
67
+ error = err;
68
+ } finally {
69
+ try {
70
+ await sandbox.close();
71
+ } catch (_) {
72
+ // Ignore sandbox cleanup errors so they do not mask the primary result.
73
+ }
74
+ }
75
+
76
+ scriptDoc.executionFinishedAt = new Date();
77
+ scriptDoc.executionResult = { output, logs: sandbox.getLogs(), error: error?.message ?? null };
78
+ await scriptDoc.save();
79
+
80
+ if (error != null) {
81
+ throw new Error(`Script execution failed: ${error.message}`);
82
+ }
83
+
84
+ return { script: scriptDoc };
85
+ };
86
+
87
+ module.exports.paramsType = ExecuteScriptParams;
88
+ module.exports.tags = [];
89
+ module.exports.mcp = false;