@mongoosejs/studio 0.5.13 → 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 (86) hide show
  1. package/AGENTS.md +74 -0
  2. package/README.md +27 -0
  3. package/backend/actions/ChatMessage/executeScript.js +15 -5
  4. package/backend/actions/ChatThread/createChatMessage.js +7 -2
  5. package/backend/actions/ChatThread/getChatThread.js +8 -2
  6. package/backend/actions/ChatThread/listChatThreads.js +6 -1
  7. package/backend/actions/ChatThread/shareChatThread.js +8 -2
  8. package/backend/actions/ChatThread/streamChatMessage.js +7 -2
  9. package/backend/actions/ChatThread/toggleAgentMode.js +8 -2
  10. package/backend/actions/Dashboard/createDashboard.js +3 -0
  11. package/backend/actions/Dashboard/deleteDashboard.js +13 -3
  12. package/backend/actions/Dashboard/getDashboard.js +33 -10
  13. package/backend/actions/Dashboard/getDashboards.js +9 -1
  14. package/backend/actions/Dashboard/updateDashboard.js +9 -2
  15. package/backend/actions/Model/addField.js +9 -2
  16. package/backend/actions/Model/analyzeSchema.js +16 -4
  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 +9 -3
  20. package/backend/actions/Model/deleteDocuments.js +9 -1
  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 +33 -26
  24. package/backend/actions/Model/exportQueryResults.js +13 -1
  25. package/backend/actions/Model/getCollectionInfo.js +15 -3
  26. package/backend/actions/Model/getDocument.js +13 -2
  27. package/backend/actions/Model/getDocuments.js +25 -5
  28. package/backend/actions/Model/getDocumentsStream.js +38 -18
  29. package/backend/actions/Model/getEstimatedDocumentCounts.js +15 -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 +10 -2
  36. package/backend/actions/Model/updateDocuments.js +10 -2
  37. package/backend/actions/Model/validateDocument.js +15 -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 +9 -3
  43. package/backend/actions/Task/getTaskOverview.js +7 -3
  44. package/backend/actions/Task/getTasks.js +7 -3
  45. package/backend/actions/Task/getTasksOverTime.js +7 -3
  46. package/backend/actions/Task/rescheduleTask.js +9 -3
  47. package/backend/actions/Task/runTask.js +9 -3
  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/omitNullish.js +5 -0
  52. package/backend/helpers/operationOptions.js +26 -0
  53. package/backend/helpers/readOperationOptions.js +31 -0
  54. package/backend/index.js +6 -4
  55. package/backend/integrations/mothership.js +28 -0
  56. package/backend/mcp.js +140 -0
  57. package/backend/netlify.js +5 -3
  58. package/backend/next.js +5 -3
  59. package/backend/sandbox/createSandbox.js +2 -2
  60. package/backend/sandbox/createScriptDb.js +60 -20
  61. package/backend/util/archetypeToZodSchema.js +74 -0
  62. package/docs/mcp-oauth.md +106 -0
  63. package/eslint.config.js +1 -0
  64. package/eval/agent/run.js +1 -1
  65. package/express.js +183 -41
  66. package/frontend/index.js +3 -0
  67. package/frontend/public/app.js +740 -124
  68. package/frontend/public/tw.css +76 -0
  69. package/frontend/src/api.js +27 -1
  70. package/frontend/src/index.js +9 -1
  71. package/frontend/src/mcp/mcp.html +55 -0
  72. package/frontend/src/mcp/mcp.js +27 -0
  73. package/frontend/src/models/models.html +29 -8
  74. package/frontend/src/models/models.js +30 -2
  75. package/frontend/src/navbar/navbar.html +8 -0
  76. package/frontend/src/navbar/navbar.js +3 -0
  77. package/frontend/src/routes.js +16 -0
  78. package/frontend/src/script/script.html +105 -0
  79. package/frontend/src/script/script.js +98 -0
  80. package/frontend/src/tooltip/tooltip.html +28 -0
  81. package/frontend/src/tooltip/tooltip.js +94 -0
  82. package/index.d.ts +10 -1
  83. package/local.js +2 -1
  84. package/nest.d.ts +4 -0
  85. package/package.json +5 -3
  86. package/vercel.d.ts +4 -0
@@ -3,6 +3,7 @@
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
5
  const { Buffer } = require('buffer');
6
+ const readOperationOptions = require('../../helpers/readOperationOptions');
6
7
  const validateDocumentWithTimeout = require('../../helpers/validateDocumentWithTimeout');
7
8
 
8
9
  const SAMPLE_SIZE = 1000;
@@ -15,10 +16,17 @@ const AnalyzeSchemaParams = new Archetype({
15
16
  },
16
17
  roles: {
17
18
  $type: ['string']
19
+ },
20
+ maxTimeMS: {
21
+ $type: 'number'
22
+ },
23
+ readPreference: {
24
+ $type: 'string',
25
+ $enum: ['secondary', 'secondaryPreferred', 'primary']
18
26
  }
19
27
  }).compile('AnalyzeSchemaParams');
20
28
 
21
- module.exports = ({ db }) => async function analyzeSchema(params) {
29
+ module.exports = ({ db, options }) => async function analyzeSchema(params) {
22
30
  const { model, roles } = new AnalyzeSchemaParams(params);
23
31
 
24
32
  await authorize('Model.analyzeSchema', roles);
@@ -28,10 +36,11 @@ module.exports = ({ db }) => async function analyzeSchema(params) {
28
36
  throw new Error(`Model ${model} not found`);
29
37
  }
30
38
 
31
- const documentCount = await Model.collection.countDocuments();
39
+ const dbOptions = readOperationOptions(options, params);
40
+ const documentCount = await Model.collection.countDocuments({}, dbOptions);
32
41
  const rawDocs = documentCount > SAMPLE_SIZE ?
33
- await Model.collection.aggregate([{ $sample: { size: SAMPLE_SIZE } }]).toArray() :
34
- await Model.collection.find({}).toArray();
42
+ await Model.collection.aggregate([{ $sample: { size: SAMPLE_SIZE } }], dbOptions).toArray() :
43
+ await Model.collection.find({}, dbOptions).toArray();
35
44
 
36
45
  const paths = getSchemaPaths(Model.schema);
37
46
  const pathTypeCounts = {};
@@ -200,3 +209,6 @@ function getType(value) {
200
209
  }
201
210
  return typeof value;
202
211
  }
212
+
213
+ module.exports.paramsType = AnalyzeSchemaParams;
214
+ module.exports.tags = ['readOnly'];
@@ -62,3 +62,6 @@ const systemPrompt = `
62
62
 
63
63
  Return only the updated document body as a JavaScript object literal. Do not use Markdown or code fences.
64
64
  `.trim();
65
+
66
+ module.exports.paramsType = CreateChatMessageParams;
67
+ module.exports.tags = ['readOnly'];
@@ -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,6 +2,7 @@
2
2
 
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
+ const operationOptions = require('../../helpers/operationOptions');
5
6
 
6
7
  const DeleteDocumentParams = new Archetype({
7
8
  model: {
@@ -14,10 +15,13 @@ const DeleteDocumentParams = new Archetype({
14
15
  },
15
16
  roles: {
16
17
  $type: ['string']
18
+ },
19
+ maxTimeMS: {
20
+ $type: 'number'
17
21
  }
18
22
  }).compile('DeleteDocumentParams');
19
23
 
20
- module.exports = ({ db }) => async function DeleteDocument(params) {
24
+ module.exports = ({ db, options }) => async function DeleteDocument(params) {
21
25
  const { model, documentId, roles } = new DeleteDocumentParams(params);
22
26
 
23
27
  const Model = db.models[model];
@@ -30,9 +34,11 @@ module.exports = ({ db }) => async function DeleteDocument(params) {
30
34
 
31
35
  const doc = await Model.
32
36
  deleteOne({ _id: documentId }).
33
- setOptions({ sanitizeFilter: true }).
37
+ setOptions(operationOptions(options, params, { sanitizeFilter: true })).
34
38
  orFail();
35
- console.log('what is doc', doc);
36
39
 
37
40
  return { doc };
38
41
  };
42
+
43
+ module.exports.paramsType = DeleteDocumentParams;
44
+ module.exports.tags = ['destructive'];
@@ -2,6 +2,7 @@
2
2
 
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
+ const operationOptions = require('../../helpers/operationOptions');
5
6
 
6
7
  const DeleteDocumentsParams = new Archetype({
7
8
  model: {
@@ -14,10 +15,13 @@ const DeleteDocumentsParams = new Archetype({
14
15
  },
15
16
  roles: {
16
17
  $type: ['string']
18
+ },
19
+ maxTimeMS: {
20
+ $type: 'number'
17
21
  }
18
22
  }).compile('DeleteDocumentsParams');
19
23
 
20
- module.exports = ({ db }) => async function DeleteDocuments(params) {
24
+ module.exports = ({ db, options }) => async function DeleteDocuments(params) {
21
25
  const { model, documentIds, roles } = new DeleteDocumentsParams(params);
22
26
 
23
27
  const Model = db.models[model];
@@ -30,7 +34,11 @@ module.exports = ({ db }) => async function DeleteDocuments(params) {
30
34
 
31
35
  await Model.
32
36
  deleteMany({ _id: { $in: documentIds } }).
37
+ setOptions(operationOptions(options, params)).
33
38
  orFail();
34
39
 
35
40
  return { };
36
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'];
@@ -2,9 +2,9 @@
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 readOperationOptions = require('../../helpers/readOperationOptions');
7
+ const operationOptions = require('../../helpers/operationOptions');
8
8
 
9
9
  const ExecuteDocumentScriptParams = new Archetype({
10
10
  model: {
@@ -21,41 +21,48 @@ const ExecuteDocumentScriptParams = new Archetype({
21
21
  },
22
22
  roles: {
23
23
  $type: ['string']
24
+ },
25
+ maxTimeMS: {
26
+ $type: 'number'
27
+ },
28
+ readPreference: {
29
+ $type: 'string',
30
+ $enum: ['secondary', 'secondaryPreferred', 'primary']
24
31
  }
25
32
  }).compile('ExecuteDocumentScriptParams');
26
33
 
27
- module.exports = ({ db }) => async function executeDocumentScript(params) {
34
+ module.exports = ({ db, options }) => async function executeDocumentScript(params) {
28
35
  const { model, documentId, script, roles } = new ExecuteDocumentScriptParams(params);
29
36
 
30
37
  await authorize('Model.executeDocumentScript', roles);
31
38
 
32
- const Model = db.models[model];
33
- if (Model == null) {
39
+ if (db.models[model] == null) {
34
40
  throw new Error(`Model ${model} not found`);
35
41
  }
36
42
 
37
- const doc = await Model.findById(documentId).setOptions({ sanitizeFilter: true }).orFail();
43
+ const sandbox = createSandbox(db, { ...readOperationOptions(options, params) });
38
44
 
39
- const logs = [];
40
- if (!db.Types) {
41
- db.Types = mongoose.Types;
42
- }
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
- };
45
+ try {
46
+ const Model = sandbox.db.models[model];
47
+ const doc = await Model.findById(documentId).
48
+ setOptions(operationOptions(options, params, { sanitizeFilter: true })).
49
+ orFail();
50
+ sandbox.context.doc = doc;
49
51
 
50
- const context = vm.createContext(sandbox);
51
- const result = await vm.runInContext(wrappedScript(script), context);
52
+ const result = await sandbox.runScript({ script });
52
53
 
53
- return {
54
- result,
55
- logs: logs.join('\n')
56
- };
54
+ return {
55
+ result,
56
+ logs: sandbox.getLogs()
57
+ };
58
+ } finally {
59
+ try {
60
+ await sandbox.close();
61
+ } catch (_) {
62
+ // Ignore sandbox cleanup errors so they do not mask the primary result.
63
+ }
64
+ }
57
65
  };
58
66
 
59
- const wrappedScript = script => `(async () => {
60
- ${script}
61
- })()`;
67
+ module.exports.paramsType = ExecuteDocumentScriptParams;
68
+ module.exports.tags = [];
@@ -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 readOperationOptions = require('../../helpers/readOperationOptions');
8
9
 
9
10
  const GetDocumentsParams = new Archetype({
10
11
  model: {
@@ -26,10 +27,17 @@ const GetDocumentsParams = new Archetype({
26
27
  },
27
28
  roles: {
28
29
  $type: ['string']
30
+ },
31
+ maxTimeMS: {
32
+ $type: 'number'
33
+ },
34
+ readPreference: {
35
+ $type: 'string',
36
+ $enum: ['secondary', 'secondaryPreferred', 'primary']
29
37
  }
30
38
  }).compile('GetDocumentsParams');
31
39
 
32
- module.exports = ({ db }) => async function exportQueryResults(params, req, res) {
40
+ module.exports = ({ db, options }) => async function exportQueryResults(params, req, res) {
33
41
  params = new GetDocumentsParams(params);
34
42
  const { model, propertiesToInclude, roles, searchText } = params;
35
43
 
@@ -45,6 +53,7 @@ module.exports = ({ db }) => async function exportQueryResults(params, req, res)
45
53
 
46
54
  const docs = await Model.
47
55
  find(filter).
56
+ setOptions(readOperationOptions(options, params)).
48
57
  sort({ _id: -1 });
49
58
 
50
59
  const rows = [propertiesToInclude];
@@ -66,3 +75,6 @@ module.exports = ({ db }) => async function exportQueryResults(params, req, res)
66
75
  res.setHeader('Content-Disposition', `attachment; filename="${model.toLowerCase()}-export.csv"`);
67
76
  return csv;
68
77
  };
78
+
79
+ module.exports.paramsType = GetDocumentsParams;
80
+ module.exports.tags = ['readOnly'];
@@ -2,6 +2,7 @@
2
2
 
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
+ const readOperationOptions = require('../../helpers/readOperationOptions');
5
6
 
6
7
  const GetCollectionInfoParams = new Archetype({
7
8
  model: {
@@ -10,10 +11,17 @@ const GetCollectionInfoParams = new Archetype({
10
11
  },
11
12
  roles: {
12
13
  $type: ['string']
14
+ },
15
+ maxTimeMS: {
16
+ $type: 'number'
17
+ },
18
+ readPreference: {
19
+ $type: 'string',
20
+ $enum: ['secondary', 'secondaryPreferred', 'primary']
13
21
  }
14
22
  }).compile('GetCollectionInfoParams');
15
23
 
16
- module.exports = ({ db }) => async function getCollectionInfo(params) {
24
+ module.exports = ({ db, options }) => async function getCollectionInfo(params) {
17
25
  const { model, roles } = new GetCollectionInfoParams(params);
18
26
 
19
27
  await authorize('Model.getCollectionInfo', roles);
@@ -23,8 +31,9 @@ module.exports = ({ db }) => async function getCollectionInfo(params) {
23
31
  throw new Error(`Model ${model} not found`);
24
32
  }
25
33
 
34
+ const dbOptions = readOperationOptions(options, params);
26
35
  const [collectionOptions, stats] = await Promise.all([
27
- Model.collection.options(),
36
+ Model.collection.options(dbOptions),
28
37
  Model.aggregate([
29
38
  {
30
39
  $collStats: {
@@ -32,7 +41,7 @@ module.exports = ({ db }) => async function getCollectionInfo(params) {
32
41
  count: {}
33
42
  }
34
43
  }
35
- ]).then(res => res[0] ?? {})
44
+ ]).option(dbOptions).then(res => res[0] ?? {})
36
45
  ]);
37
46
 
38
47
  return {
@@ -47,3 +56,6 @@ module.exports = ({ db }) => async function getCollectionInfo(params) {
47
56
  }
48
57
  };
49
58
  };
59
+
60
+ module.exports.paramsType = GetCollectionInfoParams;
61
+ module.exports.tags = ['readOnly'];
@@ -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 readOperationOptions = require('../../helpers/readOperationOptions');
7
8
 
8
9
  const GetDocumentParams = new Archetype({
9
10
  model: {
@@ -16,10 +17,17 @@ const GetDocumentParams = new Archetype({
16
17
  },
17
18
  roles: {
18
19
  $type: ['string']
20
+ },
21
+ maxTimeMS: {
22
+ $type: 'number'
23
+ },
24
+ readPreference: {
25
+ $type: 'string',
26
+ $enum: ['secondary', 'secondaryPreferred', 'primary']
19
27
  }
20
28
  }).compile('GetDocumentParams');
21
29
 
22
- module.exports = ({ db }) => async function getDocument(params) {
30
+ module.exports = ({ db, options }) => async function getDocument(params) {
23
31
  const { model, documentId, roles } = new GetDocumentParams(params);
24
32
 
25
33
  await authorize('Model.getDocument', roles);
@@ -31,7 +39,7 @@ module.exports = ({ db }) => async function getDocument(params) {
31
39
 
32
40
  const doc = await Model.
33
41
  findById(documentId).
34
- setOptions({ sanitizeFilter: true }).
42
+ setOptions(readOperationOptions(options, { sanitizeFilter: true })).
35
43
  orFail();
36
44
  const schemaPaths = {};
37
45
  for (const path of Object.keys(Model.schema.paths)) {
@@ -49,3 +57,6 @@ module.exports = ({ db }) => async function getDocument(params) {
49
57
 
50
58
  return { doc: doc.toJSON({ virtuals: true, getters: false, transform: false }), schemaPaths, virtualPaths };
51
59
  };
60
+
61
+ module.exports.paramsType = GetDocumentParams;
62
+ module.exports.tags = ['readOnly'];
@@ -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 readOperationOptions = require('../../helpers/readOperationOptions');
10
11
 
11
12
  const GetDocumentsParams = new Archetype({
12
13
  model: {
@@ -37,10 +38,17 @@ const GetDocumentsParams = new Archetype({
37
38
  },
38
39
  roles: {
39
40
  $type: ['string']
41
+ },
42
+ maxTimeMS: {
43
+ $type: 'number'
44
+ },
45
+ readPreference: {
46
+ $type: 'string',
47
+ $enum: ['secondary', 'secondaryPreferred', 'primary']
40
48
  }
41
49
  }).compile('GetDocumentsParams');
42
50
 
43
- module.exports = ({ db }) => async function getDocuments(params) {
51
+ module.exports = ({ db, options }) => async function getDocuments(params) {
44
52
  params = new GetDocumentsParams(params);
45
53
  const { roles } = params;
46
54
  await authorize('Model.getDocuments', roles);
@@ -75,6 +83,7 @@ module.exports = ({ db }) => async function getDocuments(params) {
75
83
  if (projection != null) {
76
84
  query = query.select(projection);
77
85
  }
86
+ query.setOptions(readOperationOptions(options, params));
78
87
  const cursor = await query.cursor();
79
88
  const docs = [];
80
89
  for (let doc = await cursor.next(); doc != null; doc = await cursor.next()) {
@@ -106,9 +115,16 @@ module.exports = ({ db }) => async function getDocuments(params) {
106
115
  }
107
116
  removeSpecifiedPaths(schemaPaths, '.$*');
108
117
 
109
- const numDocuments = parsedFilter == null ?
110
- await Model.estimatedDocumentCount() :
111
- await Model.countDocuments(filter);
118
+ const countQuery = parsedFilter == null ?
119
+ Model.estimatedDocumentCount() :
120
+ Model.countDocuments(filter);
121
+ let numDocuments = null;
122
+ let numDocsError = null;
123
+ try {
124
+ numDocuments = await countQuery.setOptions(readOperationOptions(options, params));
125
+ } catch (err) {
126
+ numDocsError = err?.message;
127
+ }
112
128
 
113
129
  const suggestedFields = getSuggestedProjection(Model);
114
130
 
@@ -116,6 +132,10 @@ module.exports = ({ db }) => async function getDocuments(params) {
116
132
  docs: docs.map(doc => doc.toJSON({ virtuals: false, getters: false, transform: false })),
117
133
  schemaPaths,
118
134
  suggestedFields,
119
- numDocs: numDocuments
135
+ numDocs: numDocuments,
136
+ numDocsError
120
137
  };
121
138
  };
139
+
140
+ module.exports.paramsType = GetDocumentsParams;
141
+ module.exports.tags = ['readOnly'];
@@ -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 readOperationOptions = require('../../helpers/readOperationOptions');
10
11
 
11
12
  const GetDocumentsParams = new Archetype({
12
13
  model: {
@@ -37,10 +38,17 @@ const GetDocumentsParams = new Archetype({
37
38
  },
38
39
  roles: {
39
40
  $type: ['string']
41
+ },
42
+ maxTimeMS: {
43
+ $type: 'number'
44
+ },
45
+ readPreference: {
46
+ $type: 'string',
47
+ $enum: ['secondary', 'secondaryPreferred', 'primary']
40
48
  }
41
49
  }).compile('GetDocumentsParams');
42
50
 
43
- module.exports = ({ db }) => async function* getDocumentsStream(params) {
51
+ module.exports = ({ db, options }) => async function* getDocumentsStream(params) {
44
52
  params = new GetDocumentsParams(params);
45
53
  const { roles } = params;
46
54
  await authorize('Model.getDocumentsStream', roles);
@@ -75,6 +83,7 @@ module.exports = ({ db }) => async function* getDocumentsStream(params) {
75
83
  if (projection != null) {
76
84
  query = query.select(projection);
77
85
  }
86
+ query.setOptions(readOperationOptions(options, params));
78
87
 
79
88
  const schemaPaths = {};
80
89
  for (const path of Object.keys(Model.schema.paths)) {
@@ -106,32 +115,36 @@ module.exports = ({ db }) => async function* getDocumentsStream(params) {
106
115
  yield { schemaPaths, suggestedFields };
107
116
 
108
117
  // 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();
118
+ const countQuery = (parsedFilter == null)
119
+ ? Model.estimatedDocumentCount()
120
+ : Model.countDocuments(filter);
121
+ const numDocsPromise = countQuery.
122
+ setOptions(readOperationOptions(options, params)).
123
+ exec();
114
124
 
115
125
  let numDocsYielded = false;
116
126
  let numDocumentsPromiseResolved = false;
117
127
  let numDocumentsValue;
118
128
  let numDocumentsError;
119
129
 
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
- });
130
+ // Attach both handlers immediately so a fast count failure cannot become
131
+ // an unhandled rejection while the document cursor is still opening.
132
+ numDocsPromise.then(num => {
133
+ numDocumentsPromiseResolved = true;
134
+ numDocumentsValue = num;
135
+ }).catch(err => {
136
+ numDocumentsPromiseResolved = true;
137
+ numDocumentsError = err;
138
+ });
139
+
140
+ const cursor = await query.cursor();
129
141
 
142
+ try {
130
143
  for (let doc = await cursor.next(); doc != null; doc = await cursor.next()) {
131
144
  // If numDocsPromise has resolved and not yet yielded, yield it first
132
145
  if (numDocumentsPromiseResolved && !numDocsYielded) {
133
146
  if (numDocumentsError) {
134
- yield { error: numDocumentsError };
147
+ yield { numDocsError: numDocumentsError.message };
135
148
  } else {
136
149
  yield { numDocs: numDocumentsValue };
137
150
  }
@@ -142,10 +155,17 @@ module.exports = ({ db }) => async function* getDocumentsStream(params) {
142
155
 
143
156
  // If numDocsPromise hasn't resolved yet, wait for it and yield
144
157
  if (!numDocsYielded) {
145
- const numDocuments = await numDocsPromise;
146
- yield { numDocs: numDocuments };
158
+ try {
159
+ const numDocuments = await numDocsPromise;
160
+ yield { numDocs: numDocuments };
161
+ } catch (err) {
162
+ yield { numDocsError: err.message };
163
+ }
147
164
  }
148
165
  } finally {
149
166
  await cursor.close();
150
167
  }
151
168
  };
169
+
170
+ module.exports.paramsType = GetDocumentsParams;
171
+ module.exports.tags = ['readOnly'];
@@ -2,14 +2,22 @@
2
2
 
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
+ const readOperationOptions = require('../../helpers/readOperationOptions');
5
6
 
6
7
  const GetEstimatedDocumentCountsParams = new Archetype({
7
8
  roles: {
8
9
  $type: ['string']
10
+ },
11
+ maxTimeMS: {
12
+ $type: 'number'
13
+ },
14
+ readPreference: {
15
+ $type: 'string',
16
+ $enum: ['secondary', 'secondaryPreferred', 'primary']
9
17
  }
10
18
  }).compile('GetEstimatedDocumentCountsParams');
11
19
 
12
- module.exports = ({ db }) => async function getEstimatedDocumentCounts(params) {
20
+ module.exports = ({ db, options }) => async function getEstimatedDocumentCounts(params) {
13
21
  const { roles } = new GetEstimatedDocumentCountsParams(params);
14
22
  await authorize('Model.getEstimatedDocumentCounts', roles);
15
23
 
@@ -20,7 +28,9 @@ module.exports = ({ db }) => async function getEstimatedDocumentCounts(params) {
20
28
  const results = await Promise.allSettled(
21
29
  modelNames.map(name => {
22
30
  const Model = db.models[name];
23
- return Model.estimatedDocumentCount().exec();
31
+ return Model.estimatedDocumentCount().
32
+ setOptions(readOperationOptions(options, params)).
33
+ exec();
24
34
  })
25
35
  );
26
36
 
@@ -36,3 +46,6 @@ module.exports = ({ db }) => async function getEstimatedDocumentCounts(params) {
36
46
 
37
47
  return { counts };
38
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,6 +2,7 @@
2
2
 
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
+ const operationOptions = require('../../helpers/operationOptions');
5
6
 
6
7
  const UpdateDocumentsParams = new Archetype({
7
8
  model: {
@@ -21,10 +22,13 @@ const UpdateDocumentsParams = new Archetype({
21
22
  },
22
23
  roles: {
23
24
  $type: ['string']
25
+ },
26
+ maxTimeMS: {
27
+ $type: 'number'
24
28
  }
25
29
  }).compile('UpdateDocumentsParams');
26
30
 
27
- module.exports = ({ db }) => async function updateDocument(params) {
31
+ module.exports = ({ db, options }) => async function updateDocument(params) {
28
32
  const { model, _id, update, unset, roles } = new UpdateDocumentsParams(params);
29
33
 
30
34
  await authorize('Model.updateDocument', roles);
@@ -58,6 +62,10 @@ module.exports = ({ db }) => async function updateDocument(params) {
58
62
  }
59
63
 
60
64
  const doc = await Model.
61
- findByIdAndUpdate(_id, updateOperation, { sanitizeFilter: true, returnDocument: 'after', overwriteImmutable: true, runValidators: false });
65
+ findByIdAndUpdate(_id, updateOperation, { sanitizeFilter: true, returnDocument: 'after', overwriteImmutable: true, runValidators: false }).
66
+ setOptions(operationOptions(options, params));
62
67
  return { doc };
63
68
  };
69
+
70
+ module.exports.paramsType = UpdateDocumentsParams;
71
+ module.exports.tags = [];