@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
package/AGENTS.md ADDED
@@ -0,0 +1,74 @@
1
+ # Repository guidance
2
+
3
+ The project's name is Mongoose Studio. Do not refer to the project as "Studio" - the repository is named "studio" to maintain parity between the npm package name `@mongoosejs/studio` and the GitHub repository `mongoosejs/studio`.
4
+
5
+ The Mongoose connection passed to Mongoose Studio is owned by the host application.
6
+ Do not change connection-level settings, options, configuration, models, middleware, plugins, collection objects, or methods on the application connection to implement Studio behavior.
7
+ Apply Mongoose-Studio-specific behavior to individual operations whenever possible.
8
+ When isolation requires a derived connection, copy any Mongoose state that `useDb()` shares with its source before changing the derived connection.
9
+ Sandbox wrappers must remain confined to sandbox-owned models and collections.
10
+
11
+ # Frontend Guidelines
12
+
13
+ Conceptually, the frontend's job is to render the data and provide a user interface for accessing API calls.
14
+ The frontend should minimize transformations and renaming of data provided by the backend: default to displaying the raw data as-is.
15
+
16
+ ## API Client
17
+
18
+ `src/client/api.js` exposes one wrapper function per backend action; every wrapper POSTs to `${baseUrl}/<endpoint>` and resolves with the response payload directly (`res.data`), not the axios response. The endpoint list lives in `src/endpoints.js`, which `build.js` generates from the files in `backend/actions/` — do not edit it by hand. Flat action files become flat wrappers like `api.addToCart`, while capitalized `Model/action.js` directories become namespaced wrappers like `api.quote.createQuote`, so adding an action file automatically makes its wrapper available on the next build.
19
+
20
+ ## Vue User Input
21
+
22
+ Prefer explicit DOM event handlers such as `@change`, `@input`, and `@update:modelValue` over `watch` for reacting to user input. Use `watch` only when the behavior is truly derived from state changes that do not have a single clear DOM event source.
23
+
24
+ ## Vue Templates
25
+
26
+ Prefer safe property access directly in HTML over computed properties or methods for simple display values. For example, use `{{ user?.name ?? '-' }}` instead of creating a `userName` computed property. Keep display behavior local unless the logic is reused or too complex to read inline.
27
+
28
+ Prefer using mustache interpolation (`{{ expr }}`) over `v-text` for rendering text values in Vue templates.
29
+
30
+ ## Component API Ownership
31
+
32
+ Components that present an async action must own the API request for that action. Do not emit an event to a parent solely so the parent can make the HTTP request. Emit events after the component finishes the request when the parent needs to close a modal, refresh a list, or update surrounding state.
33
+
34
+ ## Async Buttons
35
+
36
+ For a button that triggers an async action, use the `<async-button>` component instead of tracking a `saveStatus`/`loading` flag by hand. It manages the in-progress state, disables itself while running, and shows a spinner. Do not add a component-level status property, a `try/finally` to reset it, or `.finally()` on the API call just to toggle a button label.
37
+
38
+ ```html
39
+ <!-- ❌ Incorrect: hand-rolled status + disabled + label swap -->
40
+ <button :disabled="saveStatus === 'saving'" @click="save">
41
+ <span v-if="saveStatus === 'saving'">Saving...</span>
42
+ <span v-else>Save</span>
43
+ </button>
44
+
45
+ <!-- ✅ Correct -->
46
+ <async-button @click="save">Save</async-button>
47
+ ```
48
+
49
+ The click handler is just an `async` method that does the work; let errors propagate to the global handler. Keep any real `:disabled` condition (e.g. invalid input) — `async-button` ORs it with its own in-progress state.
50
+
51
+ ## Avoid trivial helpers
52
+
53
+ Minimize scattered helpers on the frontend - functions and computed properties carry real cognitive overhead and need to earn their existence.
54
+ Optimize for locality and terseness over abstraction.
55
+
56
+ For example, instead of:
57
+
58
+ ```
59
+ computed: {
60
+ displayName() {
61
+ return `${user.firstName} ${user.lastName}`;
62
+ }
63
+ }
64
+ ```
65
+
66
+ and:
67
+
68
+ <div>{{ displayName }}</div>
69
+
70
+ Write:
71
+
72
+ <div>{{ user.firstName }} {{ user.lastName }}</div>
73
+
74
+ No single-expression helper functions or computed properties unless there's heavy repetition (25+ instances).
package/README.md CHANGED
@@ -39,6 +39,10 @@ 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
+ // This is also a ceiling: a request may ask for a lower limit, never a higher one.
45
+ opts.maxTimeMS = 10000;
42
46
 
43
47
  // Mount Mongoose Studio on '/studio'
44
48
  app.use('/studio', await studio('/studio/api', mongoose, opts));
@@ -52,6 +56,29 @@ app.use('/studio', await studio('/studio/api', mongoose, {
52
56
  }));
53
57
  ```
54
58
 
59
+ The Express integration also exposes a Streamable HTTP MCP endpoint at `/studio/mcp`.
60
+ Set `mcp: false` to turn it off, along with the OAuth protected resource metadata that advertises it.
61
+ It provides authorized Dashboard and Model actions, plus `Script.createScript`, as MCP tools.
62
+ With a Pro API key, pass the logged-in Studio access token in the `Authorization` header (either directly or as a bearer token); tools receive that user's roles and permissions.
63
+ Without workspace authentication, the MCP endpoint uses the same localhost and `bindIp` restrictions as the rest of Studio.
64
+
65
+ #### Connecting ChatGPT or Claude with OAuth
66
+
67
+ With a Pro API key, `/studio/mcp` is also an OAuth 2.1 protected resource, so MCP clients such as ChatGPT and Claude can connect to it directly.
68
+ Point the client at your `/studio/mcp` URL and it will discover the Mongoose Studio authorization server, walk the user through signing in and approving access, and receive a short-lived access token.
69
+ Users review and revoke these connections from their Mongoose Studio account page, and the access an AI client gets can never exceed the access the user who approved it already has.
70
+
71
+ Mongoose Studio advertises itself to MCP clients using its externally reachable URL.
72
+ When MCP and a Pro API key are enabled, set `publicUrl` to the URL where Mongoose Studio is mounted:
73
+
74
+ ```javascript
75
+ opts.publicUrl = 'https://app.example.com/studio';
76
+ ```
77
+
78
+ Mongoose Studio finds the authorization server through the mothership it is already configured to use, so it needs no separate authorization server configuration.
79
+
80
+ See `docs/mcp-oauth.md` for how the flow and the delegated access policy work.
81
+
55
82
  ### Next.js
56
83
 
57
84
  First, add `withMongooseStudio` to your `next.config.js` file:
@@ -4,6 +4,8 @@ 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 readOperationOptions = require('../../helpers/readOperationOptions');
8
+ const operationOptions = require('../../helpers/operationOptions');
7
9
 
8
10
  const ExecuteScriptParams = new Archetype({
9
11
  initiatedById: {
@@ -21,27 +23,35 @@ const ExecuteScriptParams = new Archetype({
21
23
  },
22
24
  roles: {
23
25
  $type: ['string']
26
+ },
27
+ maxTimeMS: {
28
+ $type: 'number'
29
+ },
30
+ readPreference: {
31
+ $type: 'string',
32
+ $enum: ['secondary', 'secondaryPreferred', 'primary']
24
33
  }
25
34
  }).compile('ExecuteScriptParams');
26
35
 
27
- module.exports = ({ db, studioConnection }) => async function executeScript(params) {
36
+ module.exports = ({ db, studioConnection, options }) => async function executeScript(params) {
28
37
  const { initiatedById, chatMessageId, script, dryRun, roles } = new ExecuteScriptParams(params);
29
38
  const ChatThread = studioConnection.model('__Studio_ChatThread');
30
39
  const ChatMessage = studioConnection.model('__Studio_ChatMessage');
31
40
 
32
41
  await authorize('ChatMessage.executeScript', roles);
33
42
 
34
- const chatMessage = await ChatMessage.findById(chatMessageId);
43
+ const dbOptions = operationOptions(options, params);
44
+ const chatMessage = await ChatMessage.findById(chatMessageId).setOptions(dbOptions);
35
45
  if (!chatMessage) {
36
46
  throw new Error('Chat message not found');
37
47
  }
38
- const chatThread = await ChatThread.findById(chatMessage.chatThreadId).orFail();
48
+ const chatThread = await ChatThread.findById(chatMessage.chatThreadId).setOptions(dbOptions).orFail();
39
49
 
40
50
  if (initiatedById && chatThread.userId?.toString() !== initiatedById.toString()) {
41
51
  throw new Error('Unauthorized');
42
52
  }
43
53
 
44
- const sandbox = createSandbox({ db });
54
+ const sandbox = createSandbox(db, { ...readOperationOptions(options, params) });
45
55
 
46
56
  let output;
47
57
  let error;
@@ -74,7 +84,7 @@ module.exports = ({ db, studioConnection }) => async function executeScript(para
74
84
  dryRun: !!dryRun
75
85
  }
76
86
  }
77
- );
87
+ ).setOptions(operationOptions(options, params));
78
88
 
79
89
  throw new Error(`Script execution failed: ${error}`);
80
90
  } 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 operationOptions = require('../../helpers/operationOptions');
11
12
 
12
13
  const CreateChatMessageParams = new Archetype({
13
14
  chatThreadId: {
@@ -25,6 +26,9 @@ const CreateChatMessageParams = new Archetype({
25
26
  },
26
27
  roles: {
27
28
  $type: ['string']
29
+ },
30
+ maxTimeMS: {
31
+ $type: 'number'
28
32
  }
29
33
  }).compile('CreateChatMessageParams');
30
34
 
@@ -36,7 +40,8 @@ module.exports = ({ db, studioConnection, options }) => async function createCha
36
40
  await authorize('ChatThread.createChatMessage', roles);
37
41
 
38
42
  // Check that the user owns the thread
39
- const chatThread = await ChatThread.findOne({ _id: chatThreadId });
43
+ const dbOptions = operationOptions(options, params);
44
+ const chatThread = await ChatThread.findOne({ _id: chatThreadId }).setOptions(dbOptions);
40
45
  if (!chatThread) {
41
46
  throw new Error('Chat thread not found');
42
47
  }
@@ -44,7 +49,7 @@ module.exports = ({ db, studioConnection, options }) => async function createCha
44
49
  throw new Error('Not authorized');
45
50
  }
46
51
 
47
- const messages = await ChatMessage.find({ chatThreadId }).sort({ createdAt: 1 });
52
+ const messages = await ChatMessage.find({ chatThreadId }).setOptions(dbOptions).sort({ createdAt: 1 });
48
53
  const llmMessages = messages.map(m => ({
49
54
  role: m.role,
50
55
  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 operationOptions = require('../../helpers/operationOptions');
6
7
 
7
8
  const GetChatThreadParams = new Archetype({
8
9
  chatThreadId: {
@@ -16,17 +17,21 @@ const GetChatThreadParams = new Archetype({
16
17
  },
17
18
  $workspaceId: {
18
19
  $type: mongoose.Types.ObjectId
20
+ },
21
+ maxTimeMS: {
22
+ $type: 'number'
19
23
  }
20
24
  }).compile('GetChatThreadParams');
21
25
 
22
- module.exports = ({ db, studioConnection }) => async function getChatThread(params) {
26
+ module.exports = ({ db, studioConnection, options }) => async function getChatThread(params) {
23
27
  const { chatThreadId, initiatedById, roles, $workspaceId } = new GetChatThreadParams(params);
24
28
  const ChatThread = studioConnection.model('__Studio_ChatThread');
25
29
  const ChatMessage = studioConnection.model('__Studio_ChatMessage');
26
30
 
27
31
  await authorize('ChatThread.getChatThread', roles);
28
32
 
29
- const chatThread = await ChatThread.findById(chatThreadId);
33
+ const dbOptions = operationOptions(options, params);
34
+ const chatThread = await ChatThread.findById(chatThreadId).setOptions(dbOptions);
30
35
 
31
36
  if (!chatThread) {
32
37
  throw new Error('Chat thread not found');
@@ -39,6 +44,7 @@ module.exports = ({ db, studioConnection }) => async function getChatThread(para
39
44
  }
40
45
 
41
46
  const chatMessages = await ChatMessage.find({ chatThreadId })
47
+ .setOptions(dbOptions)
42
48
  .sort({ createdAt: -1 });
43
49
 
44
50
  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 operationOptions = require('../../helpers/operationOptions');
6
7
 
7
8
  const ListChatThreadsParams = new Archetype({
8
9
  initiatedById: {
@@ -10,10 +11,13 @@ const ListChatThreadsParams = new Archetype({
10
11
  },
11
12
  roles: {
12
13
  $type: ['string']
14
+ },
15
+ maxTimeMS: {
16
+ $type: 'number'
13
17
  }
14
18
  }).compile('ListChatThreadsParams');
15
19
 
16
- module.exports = ({ db, studioConnection }) => async function listChatThreads(params) {
20
+ module.exports = ({ db, studioConnection, options }) => async function listChatThreads(params) {
17
21
  // Validate the params object
18
22
  const { initiatedById, roles } = new ListChatThreadsParams(params);
19
23
  const ChatThread = studioConnection.model('__Studio_ChatThread');
@@ -24,6 +28,7 @@ module.exports = ({ db, studioConnection }) => async function listChatThreads(pa
24
28
 
25
29
  // Get all chat threads
26
30
  const chatThreads = await ChatThread.find(query)
31
+ .setOptions(operationOptions(options, params))
27
32
  .sort({ updatedAt: -1 }); // Sort by most recently updated
28
33
 
29
34
  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 operationOptions = require('../../helpers/operationOptions');
6
7
 
7
8
  const ShareChatThreadParams = new Archetype({
8
9
  chatThreadId: {
@@ -19,16 +20,21 @@ const ShareChatThreadParams = new Archetype({
19
20
  },
20
21
  $workspaceId: {
21
22
  $type: mongoose.Types.ObjectId
23
+ },
24
+ maxTimeMS: {
25
+ $type: 'number'
22
26
  }
23
27
  }).compile('ShareChatThreadParams');
24
28
 
25
- module.exports = ({ studioConnection }) => async function shareChatThread(params) {
29
+ module.exports = ({ studioConnection, options }) => async function shareChatThread(params) {
26
30
  const { chatThreadId, share, initiatedById, roles, $workspaceId } = new ShareChatThreadParams(params);
27
31
  const ChatThread = studioConnection.model('__Studio_ChatThread');
28
32
 
29
33
  await authorize('ChatThread.shareChatThread', roles);
30
34
 
31
- const chatThread = await ChatThread.findById(chatThreadId).orFail();
35
+ const chatThread = await ChatThread.findById(chatThreadId).
36
+ setOptions(operationOptions(options, params)).
37
+ orFail();
32
38
  if (initiatedById != null && chatThread.userId?.toString() !== initiatedById.toString()) {
33
39
  throw new Error('Not authorized');
34
40
  }
@@ -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 operationOptions = require('../../helpers/operationOptions');
11
12
 
12
13
  const CreateChatMessageParams = new Archetype({
13
14
  chatThreadId: {
@@ -25,6 +26,9 @@ const CreateChatMessageParams = new Archetype({
25
26
  },
26
27
  roles: {
27
28
  $type: ['string']
29
+ },
30
+ maxTimeMS: {
31
+ $type: 'number'
28
32
  }
29
33
  }).compile('CreateChatMessageParams');
30
34
 
@@ -36,7 +40,8 @@ module.exports = ({ db, studioConnection, options }) => async function* streamCh
36
40
  await authorize('ChatThread.createChatMessage', roles);
37
41
 
38
42
  // Check that the user owns the thread
39
- const chatThread = await ChatThread.findOne({ _id: chatThreadId });
43
+ const dbOptions = operationOptions(options, params);
44
+ const chatThread = await ChatThread.findOne({ _id: chatThreadId }).setOptions(dbOptions);
40
45
  if (!chatThread) {
41
46
  throw new Error('Chat thread not found');
42
47
  }
@@ -44,7 +49,7 @@ module.exports = ({ db, studioConnection, options }) => async function* streamCh
44
49
  throw new Error('Not authorized');
45
50
  }
46
51
 
47
- const messages = await ChatMessage.find({ chatThreadId }).sort({ createdAt: 1 });
52
+ const messages = await ChatMessage.find({ chatThreadId }).setOptions(dbOptions).sort({ createdAt: 1 });
48
53
  const llmMessages = messages.map(m => ({
49
54
  role: m.role,
50
55
  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 operationOptions = require('../../helpers/operationOptions');
6
7
 
7
8
  const ToggleAgentModeParams = new Archetype({
8
9
  chatThreadId: {
@@ -16,16 +17,21 @@ const ToggleAgentModeParams = new Archetype({
16
17
  },
17
18
  roles: {
18
19
  $type: ['string']
20
+ },
21
+ maxTimeMS: {
22
+ $type: 'number'
19
23
  }
20
24
  }).compile('ToggleAgentModeParams');
21
25
 
22
- module.exports = ({ studioConnection }) => async function toggleAgentMode(params) {
26
+ module.exports = ({ studioConnection, options }) => async function toggleAgentMode(params) {
23
27
  const { chatThreadId, agentMode, initiatedById, roles } = new ToggleAgentModeParams(params);
24
28
  const ChatThread = studioConnection.model('__Studio_ChatThread');
25
29
 
26
30
  await authorize('ChatThread.toggleAgentMode', roles);
27
31
 
28
- const chatThread = await ChatThread.findById(chatThreadId).orFail();
32
+ const chatThread = await ChatThread.findById(chatThreadId).
33
+ setOptions(operationOptions(options, params)).
34
+ orFail();
29
35
  if (initiatedById != null && chatThread.userId?.toString() !== initiatedById.toString()) {
30
36
  throw new Error('Not authorized');
31
37
  }
@@ -37,3 +37,6 @@ module.exports = ({ studioConnection }) => async function createDashboard(params
37
37
 
38
38
  return { dashboard };
39
39
  };
40
+
41
+ module.exports.paramsType = CreateDashboardParams;
42
+ 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 DeleteDashboardParams = new Archetype({
7
8
  dashboardId: {
@@ -10,17 +11,26 @@ const DeleteDashboardParams = new Archetype({
10
11
  },
11
12
  roles: {
12
13
  $type: ['string']
14
+ },
15
+ maxTimeMS: {
16
+ $type: 'number'
13
17
  }
14
18
  }).compile('DeleteDashboardParams');
15
19
 
16
- module.exports = ({ studioConnection }) => async function deleteDashboard(params) {
20
+ module.exports = ({ studioConnection, options }) => async function deleteDashboard(params) {
17
21
  const { dashboardId, roles } = new DeleteDashboardParams(params);
18
22
  const Dashboard = studioConnection.model('__Studio_Dashboard');
19
23
  const DashboardResult = studioConnection.model('__Studio_DashboardResult');
20
24
 
21
25
  await authorize('Dashboard.deleteDashboard', roles);
22
26
 
23
- const result = await Dashboard.deleteOne({ _id: dashboardId }).orFail();
24
- await DashboardResult.deleteMany({ dashboardId });
27
+ const result = await Dashboard.deleteOne({ _id: dashboardId }).
28
+ setOptions(operationOptions(options, params)).
29
+ orFail();
30
+ await DashboardResult.deleteMany({ dashboardId }).
31
+ setOptions(operationOptions(options, params));
25
32
  return { result };
26
33
  };
34
+
35
+ module.exports.paramsType = DeleteDashboardParams;
36
+ module.exports.tags = ['destructive'];
@@ -4,6 +4,8 @@ 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 readOperationOptions = require('../../helpers/readOperationOptions');
8
+ const operationOptions = require('../../helpers/operationOptions');
7
9
  const { defaultMothershipURL } = require('../../../constants');
8
10
 
9
11
  const GetDashboardParams = new Archetype({
@@ -25,6 +27,13 @@ const GetDashboardParams = new Archetype({
25
27
  },
26
28
  roles: {
27
29
  $type: ['string']
30
+ },
31
+ maxTimeMS: {
32
+ $type: 'number'
33
+ },
34
+ readPreference: {
35
+ $type: 'string',
36
+ $enum: ['secondary', 'secondaryPreferred', 'primary']
28
37
  }
29
38
  }).compile('GetDashboardParams');
30
39
 
@@ -36,10 +45,12 @@ module.exports = ({ db, studioConnection, options }) => async function getDashbo
36
45
 
37
46
  await authorize('Dashboard.getDashboard', roles);
38
47
 
39
- const dashboard = await Dashboard.findOne({ _id: dashboardId });
48
+ const dashboardOptions = operationOptions(options, params);
49
+ const dashboard = await Dashboard.findOne({ _id: dashboardId }).
50
+ setOptions(dashboardOptions);
40
51
  if (evaluate) {
41
52
  let result = null;
42
- const sandbox = createSandbox({ db });
53
+ const sandbox = createSandbox(db, { ...readOperationOptions(options, params) });
43
54
  const startExec = startDashboardEvaluate(DashboardResult, dashboardId, $workspaceId, userId);
44
55
  startExec.catch(() => {}); // Avoid unhandled promise rejections - we will handle this error later.
45
56
  try {
@@ -56,7 +67,8 @@ module.exports = ({ db, studioConnection, options }) => async function getDashbo
56
67
  dashboardResult._id,
57
68
  null,
58
69
  { message: error.message },
59
- 'failed'
70
+ 'failed',
71
+ dashboardOptions
60
72
  );
61
73
  });
62
74
  return { dashboard, dashboardResult, error: { message: error.message } };
@@ -79,7 +91,8 @@ module.exports = ({ db, studioConnection, options }) => async function getDashbo
79
91
  dashboardResult._id,
80
92
  result,
81
93
  undefined,
82
- 'completed'
94
+ 'completed',
95
+ dashboardOptions
83
96
  );
84
97
  });
85
98
 
@@ -93,21 +106,25 @@ module.exports = ({ db, studioConnection, options }) => async function getDashbo
93
106
  dashboardId,
94
107
  $workspaceId,
95
108
  authorization,
96
- mothershipUrl
109
+ mothershipUrl,
110
+ dashboardOptions
97
111
  );
98
112
  return { dashboard, dashboardResults };
99
113
  }
100
114
  };
101
115
 
102
- async function completeDashboardEvaluate(Dashboard, DashboardResult, dashboardResultId, result, error, status) {
103
- const dashboardResult = await DashboardResult.findById(dashboardResultId).orFail();
116
+ async function completeDashboardEvaluate(Dashboard, DashboardResult, dashboardResultId, result, error, status, dashboardOptions) {
117
+ const dashboardResult = await DashboardResult.findById(dashboardResultId).
118
+ setOptions(dashboardOptions).
119
+ orFail();
104
120
  const finishedEvaluatingAt = new Date();
105
121
  dashboardResult.finishedEvaluatingAt = finishedEvaluatingAt;
106
122
  dashboardResult.result = result;
107
123
  dashboardResult.error = error;
108
124
  dashboardResult.status = status;
109
125
  await dashboardResult.save();
110
- await Dashboard.updateOne({ _id: dashboardResult.dashboardId }, { $set: { lastEvaluatedAt: finishedEvaluatingAt } });
126
+ await Dashboard.updateOne({ _id: dashboardResult.dashboardId }, { $set: { lastEvaluatedAt: finishedEvaluatingAt } }).
127
+ setOptions(dashboardOptions);
111
128
  return { dashboardResult };
112
129
  }
113
130
 
@@ -123,14 +140,17 @@ async function startDashboardEvaluate(DashboardResult, dashboardId, workspaceId,
123
140
  return { dashboardResult };
124
141
  }
125
142
 
126
- async function getDashboardResults(DashboardResult, dashboardId, workspaceId, authorization, mothershipUrl) {
143
+ async function getDashboardResults(DashboardResult, dashboardId, workspaceId, authorization, mothershipUrl, dashboardOptions) {
127
144
  const filter = { dashboardId };
128
145
  if (workspaceId != null) {
129
146
  filter.workspaceId = workspaceId;
130
147
  }
131
148
 
132
149
  const [localResultsRes, remoteResultsRes] = await Promise.allSettled([
133
- DashboardResult.find(filter).sort({ _id: -1 }).limit(10),
150
+ DashboardResult.find(filter).
151
+ sort({ _id: -1 }).
152
+ limit(10).
153
+ setOptions(dashboardOptions),
134
154
  getMothershipDashboardResults(dashboardId, workspaceId, authorization, mothershipUrl)
135
155
  ]);
136
156
 
@@ -201,3 +221,6 @@ function addDocumentSchemaPaths(result) {
201
221
  }
202
222
  result.$document.schemaPaths = schemaPaths;
203
223
  }
224
+
225
+ module.exports.paramsType = GetDashboardParams;
226
+ module.exports.tags = ['readOnly'];
@@ -2,14 +2,18 @@
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 GetDashboardParams = new Archetype({
7
8
  roles: {
8
9
  $type: ['string']
10
+ },
11
+ maxTimeMS: {
12
+ $type: 'number'
9
13
  }
10
14
  }).compile('GetDashboardParams');
11
15
 
12
- module.exports = ({ studioConnection }) => async function getDashboards(params) {
16
+ module.exports = ({ studioConnection, options }) => async function getDashboards(params) {
13
17
  const Dashboard = studioConnection.model('__Studio_Dashboard');
14
18
  const { roles } = new GetDashboardParams(params);
15
19
 
@@ -17,7 +21,11 @@ module.exports = ({ studioConnection }) => async function getDashboards(params)
17
21
 
18
22
  const dashboards = await Dashboard
19
23
  .find()
24
+ .setOptions(operationOptions(options, params))
20
25
  .sort({ isPinned: -1, createdAt: -1, _id: -1 });
21
26
 
22
27
  return { dashboards };
23
28
  };
29
+
30
+ module.exports.paramsType = GetDashboardParams;
31
+ 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 UpdateDashboardParams = new Archetype({
7
8
  dashboardId: {
@@ -22,10 +23,13 @@ const UpdateDashboardParams = new Archetype({
22
23
  },
23
24
  roles: {
24
25
  $type: ['string']
26
+ },
27
+ maxTimeMS: {
28
+ $type: 'number'
25
29
  }
26
30
  }).compile('UpdateDashboardParams');
27
31
 
28
- module.exports = ({ studioConnection }) => async function updateDashboard(params) {
32
+ module.exports = ({ studioConnection, options }) => async function updateDashboard(params) {
29
33
  const { dashboardId, code, title, description, isPinned, roles, evaluate } = new UpdateDashboardParams(params);
30
34
 
31
35
  const Dashboard = studioConnection.models['__Studio_Dashboard'];
@@ -54,7 +58,10 @@ module.exports = ({ studioConnection }) => async function updateDashboard(params
54
58
  dashboardId,
55
59
  updateObj,
56
60
  { sanitizeFilter: true, returnDocument: 'after' }
57
- );
61
+ ).setOptions(operationOptions(options, params));
58
62
 
59
63
  return { doc };
60
64
  };
65
+
66
+ module.exports.paramsType = UpdateDashboardParams;
67
+ 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 AddFieldParams = new Archetype({
7
8
  model: {
@@ -22,10 +23,13 @@ const AddFieldParams = new Archetype({
22
23
  },
23
24
  roles: {
24
25
  $type: ['string']
26
+ },
27
+ maxTimeMS: {
28
+ $type: 'number'
25
29
  }
26
30
  }).compile('AddFieldParams');
27
31
 
28
- module.exports = ({ db }) => async function addField(params) {
32
+ module.exports = ({ db, options }) => async function addField(params) {
29
33
  const { model, _id, fieldName, fieldValue, roles } = new AddFieldParams(params);
30
34
 
31
35
  await authorize('Model.updateDocument', roles);
@@ -48,7 +52,10 @@ module.exports = ({ db }) => async function addField(params) {
48
52
  runValidators: false,
49
53
  strict: false
50
54
  }
51
- );
55
+ ).setOptions(operationOptions(options, params));
52
56
 
53
57
  return { doc };
54
58
  };
59
+
60
+ module.exports.paramsType = AddFieldParams;
61
+ module.exports.tags = [];