@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
package/AGENTS.md CHANGED
@@ -1,7 +1,74 @@
1
1
  # Repository guidance
2
2
 
3
- The Mongoose connection passed to Studio is owned by the host application.
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.
4
6
  Do not change connection-level settings, options, configuration, models, middleware, plugins, collection objects, or methods on the application connection to implement Studio behavior.
5
- Apply Studio-specific behavior to individual operations whenever possible.
7
+ Apply Mongoose-Studio-specific behavior to individual operations whenever possible.
6
8
  When isolation requires a derived connection, copy any Mongoose state that `useDb()` shares with its source before changing the derived connection.
7
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
@@ -41,6 +41,7 @@ opts.anthropicAPIKey = process.env.ANTHROPIC_API_KEY;
41
41
  opts.googleGeminiAPIKey = process.env.GOOGLE_GEMINI_API_KEY;
42
42
  // Apply a maximum execution time to all read operations, including reads in scripts.
43
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.
44
45
  opts.maxTimeMS = 10000;
45
46
 
46
47
  // Mount Mongoose Studio on '/studio'
@@ -55,6 +56,29 @@ app.use('/studio', await studio('/studio/api', mongoose, {
55
56
  }));
56
57
  ```
57
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
+
58
82
  ### Next.js
59
83
 
60
84
  First, add `withMongooseStudio` to your `next.config.js` file:
@@ -4,7 +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 omitNullish = require('../../helpers/omitNullish');
7
+ const readOperationOptions = require('../../helpers/readOperationOptions');
8
+ const operationOptions = require('../../helpers/operationOptions');
8
9
 
9
10
  const ExecuteScriptParams = new Archetype({
10
11
  initiatedById: {
@@ -22,6 +23,13 @@ const ExecuteScriptParams = new Archetype({
22
23
  },
23
24
  roles: {
24
25
  $type: ['string']
26
+ },
27
+ maxTimeMS: {
28
+ $type: 'number'
29
+ },
30
+ readPreference: {
31
+ $type: 'string',
32
+ $enum: ['secondary', 'secondaryPreferred', 'primary']
25
33
  }
26
34
  }).compile('ExecuteScriptParams');
27
35
 
@@ -32,18 +40,18 @@ module.exports = ({ db, studioConnection, options }) => async function executeSc
32
40
 
33
41
  await authorize('ChatMessage.executeScript', roles);
34
42
 
35
- const operationOptions = omitNullish({ maxTimeMS: options?.maxTimeMS });
36
- const chatMessage = await ChatMessage.findById(chatMessageId).setOptions(operationOptions);
43
+ const dbOptions = operationOptions(options, params);
44
+ const chatMessage = await ChatMessage.findById(chatMessageId).setOptions(dbOptions);
37
45
  if (!chatMessage) {
38
46
  throw new Error('Chat message not found');
39
47
  }
40
- const chatThread = await ChatThread.findById(chatMessage.chatThreadId).setOptions(operationOptions).orFail();
48
+ const chatThread = await ChatThread.findById(chatMessage.chatThreadId).setOptions(dbOptions).orFail();
41
49
 
42
50
  if (initiatedById && chatThread.userId?.toString() !== initiatedById.toString()) {
43
51
  throw new Error('Unauthorized');
44
52
  }
45
53
 
46
- const sandbox = createSandbox({ db, maxTimeMS: options?.maxTimeMS });
54
+ const sandbox = createSandbox(db, { ...readOperationOptions(options, params) });
47
55
 
48
56
  let output;
49
57
  let error;
@@ -76,7 +84,7 @@ module.exports = ({ db, studioConnection, options }) => async function executeSc
76
84
  dryRun: !!dryRun
77
85
  }
78
86
  }
79
- ).setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS }));
87
+ ).setOptions(operationOptions(options, params));
80
88
 
81
89
  throw new Error(`Script execution failed: ${error}`);
82
90
  } finally {
@@ -8,7 +8,7 @@ const callLLM = require('../../integrations/callLLM');
8
8
  const getAgentTools = require('../../chatAgent/getAgentTools');
9
9
  const getModelDescriptions = require('../../helpers/getModelDescriptions');
10
10
  const mongoose = require('mongoose');
11
- const omitNullish = require('../../helpers/omitNullish');
11
+ const operationOptions = require('../../helpers/operationOptions');
12
12
 
13
13
  const CreateChatMessageParams = new Archetype({
14
14
  chatThreadId: {
@@ -26,6 +26,9 @@ const CreateChatMessageParams = new Archetype({
26
26
  },
27
27
  roles: {
28
28
  $type: ['string']
29
+ },
30
+ maxTimeMS: {
31
+ $type: 'number'
29
32
  }
30
33
  }).compile('CreateChatMessageParams');
31
34
 
@@ -37,8 +40,8 @@ module.exports = ({ db, studioConnection, options }) => async function createCha
37
40
  await authorize('ChatThread.createChatMessage', roles);
38
41
 
39
42
  // Check that the user owns the thread
40
- const operationOptions = omitNullish({ maxTimeMS: options?.maxTimeMS });
41
- const chatThread = await ChatThread.findOne({ _id: chatThreadId }).setOptions(operationOptions);
43
+ const dbOptions = operationOptions(options, params);
44
+ const chatThread = await ChatThread.findOne({ _id: chatThreadId }).setOptions(dbOptions);
42
45
  if (!chatThread) {
43
46
  throw new Error('Chat thread not found');
44
47
  }
@@ -46,7 +49,7 @@ module.exports = ({ db, studioConnection, options }) => async function createCha
46
49
  throw new Error('Not authorized');
47
50
  }
48
51
 
49
- const messages = await ChatMessage.find({ chatThreadId }).setOptions(operationOptions).sort({ createdAt: 1 });
52
+ const messages = await ChatMessage.find({ chatThreadId }).setOptions(dbOptions).sort({ createdAt: 1 });
50
53
  const llmMessages = messages.map(m => ({
51
54
  role: m.role,
52
55
  content: [{
@@ -3,7 +3,7 @@
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
5
  const mongoose = require('mongoose');
6
- const omitNullish = require('../../helpers/omitNullish');
6
+ const operationOptions = require('../../helpers/operationOptions');
7
7
 
8
8
  const GetChatThreadParams = new Archetype({
9
9
  chatThreadId: {
@@ -17,6 +17,9 @@ const GetChatThreadParams = new Archetype({
17
17
  },
18
18
  $workspaceId: {
19
19
  $type: mongoose.Types.ObjectId
20
+ },
21
+ maxTimeMS: {
22
+ $type: 'number'
20
23
  }
21
24
  }).compile('GetChatThreadParams');
22
25
 
@@ -27,8 +30,8 @@ module.exports = ({ db, studioConnection, options }) => async function getChatTh
27
30
 
28
31
  await authorize('ChatThread.getChatThread', roles);
29
32
 
30
- const operationOptions = omitNullish({ maxTimeMS: options?.maxTimeMS });
31
- const chatThread = await ChatThread.findById(chatThreadId).setOptions(operationOptions);
33
+ const dbOptions = operationOptions(options, params);
34
+ const chatThread = await ChatThread.findById(chatThreadId).setOptions(dbOptions);
32
35
 
33
36
  if (!chatThread) {
34
37
  throw new Error('Chat thread not found');
@@ -41,7 +44,7 @@ module.exports = ({ db, studioConnection, options }) => async function getChatTh
41
44
  }
42
45
 
43
46
  const chatMessages = await ChatMessage.find({ chatThreadId })
44
- .setOptions(operationOptions)
47
+ .setOptions(dbOptions)
45
48
  .sort({ createdAt: -1 });
46
49
 
47
50
  return {
@@ -3,7 +3,7 @@
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
5
  const mongoose = require('mongoose');
6
- const omitNullish = require('../../helpers/omitNullish');
6
+ const operationOptions = require('../../helpers/operationOptions');
7
7
 
8
8
  const ListChatThreadsParams = new Archetype({
9
9
  initiatedById: {
@@ -11,6 +11,9 @@ const ListChatThreadsParams = new Archetype({
11
11
  },
12
12
  roles: {
13
13
  $type: ['string']
14
+ },
15
+ maxTimeMS: {
16
+ $type: 'number'
14
17
  }
15
18
  }).compile('ListChatThreadsParams');
16
19
 
@@ -25,7 +28,7 @@ module.exports = ({ db, studioConnection, options }) => async function listChatT
25
28
 
26
29
  // Get all chat threads
27
30
  const chatThreads = await ChatThread.find(query)
28
- .setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS }))
31
+ .setOptions(operationOptions(options, params))
29
32
  .sort({ updatedAt: -1 }); // Sort by most recently updated
30
33
 
31
34
  return {
@@ -3,7 +3,7 @@
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
5
  const mongoose = require('mongoose');
6
- const omitNullish = require('../../helpers/omitNullish');
6
+ const operationOptions = require('../../helpers/operationOptions');
7
7
 
8
8
  const ShareChatThreadParams = new Archetype({
9
9
  chatThreadId: {
@@ -20,6 +20,9 @@ const ShareChatThreadParams = new Archetype({
20
20
  },
21
21
  $workspaceId: {
22
22
  $type: mongoose.Types.ObjectId
23
+ },
24
+ maxTimeMS: {
25
+ $type: 'number'
23
26
  }
24
27
  }).compile('ShareChatThreadParams');
25
28
 
@@ -30,7 +33,7 @@ module.exports = ({ studioConnection, options }) => async function shareChatThre
30
33
  await authorize('ChatThread.shareChatThread', roles);
31
34
 
32
35
  const chatThread = await ChatThread.findById(chatThreadId).
33
- setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS })).
36
+ setOptions(operationOptions(options, params)).
34
37
  orFail();
35
38
  if (initiatedById != null && chatThread.userId?.toString() !== initiatedById.toString()) {
36
39
  throw new Error('Not authorized');
@@ -8,7 +8,7 @@ const runChatAgent = require('../../chatAgent/runChatAgent');
8
8
  const streamLLM = require('../../integrations/streamLLM');
9
9
  const getModelDescriptions = require('../../helpers/getModelDescriptions');
10
10
  const mongoose = require('mongoose');
11
- const omitNullish = require('../../helpers/omitNullish');
11
+ const operationOptions = require('../../helpers/operationOptions');
12
12
 
13
13
  const CreateChatMessageParams = new Archetype({
14
14
  chatThreadId: {
@@ -26,6 +26,9 @@ const CreateChatMessageParams = new Archetype({
26
26
  },
27
27
  roles: {
28
28
  $type: ['string']
29
+ },
30
+ maxTimeMS: {
31
+ $type: 'number'
29
32
  }
30
33
  }).compile('CreateChatMessageParams');
31
34
 
@@ -37,8 +40,8 @@ module.exports = ({ db, studioConnection, options }) => async function* streamCh
37
40
  await authorize('ChatThread.createChatMessage', roles);
38
41
 
39
42
  // Check that the user owns the thread
40
- const operationOptions = omitNullish({ maxTimeMS: options?.maxTimeMS });
41
- const chatThread = await ChatThread.findOne({ _id: chatThreadId }).setOptions(operationOptions);
43
+ const dbOptions = operationOptions(options, params);
44
+ const chatThread = await ChatThread.findOne({ _id: chatThreadId }).setOptions(dbOptions);
42
45
  if (!chatThread) {
43
46
  throw new Error('Chat thread not found');
44
47
  }
@@ -46,7 +49,7 @@ module.exports = ({ db, studioConnection, options }) => async function* streamCh
46
49
  throw new Error('Not authorized');
47
50
  }
48
51
 
49
- const messages = await ChatMessage.find({ chatThreadId }).setOptions(operationOptions).sort({ createdAt: 1 });
52
+ const messages = await ChatMessage.find({ chatThreadId }).setOptions(dbOptions).sort({ createdAt: 1 });
50
53
  const llmMessages = messages.map(m => ({
51
54
  role: m.role,
52
55
  content: [{
@@ -3,7 +3,7 @@
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
5
  const mongoose = require('mongoose');
6
- const omitNullish = require('../../helpers/omitNullish');
6
+ const operationOptions = require('../../helpers/operationOptions');
7
7
 
8
8
  const ToggleAgentModeParams = new Archetype({
9
9
  chatThreadId: {
@@ -17,6 +17,9 @@ const ToggleAgentModeParams = new Archetype({
17
17
  },
18
18
  roles: {
19
19
  $type: ['string']
20
+ },
21
+ maxTimeMS: {
22
+ $type: 'number'
20
23
  }
21
24
  }).compile('ToggleAgentModeParams');
22
25
 
@@ -27,7 +30,7 @@ module.exports = ({ studioConnection, options }) => async function toggleAgentMo
27
30
  await authorize('ChatThread.toggleAgentMode', roles);
28
31
 
29
32
  const chatThread = await ChatThread.findById(chatThreadId).
30
- setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS })).
33
+ setOptions(operationOptions(options, params)).
31
34
  orFail();
32
35
  if (initiatedById != null && chatThread.userId?.toString() !== initiatedById.toString()) {
33
36
  throw new Error('Not authorized');
@@ -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,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 DeleteDashboardParams = new Archetype({
8
8
  dashboardId: {
@@ -11,6 +11,9 @@ const DeleteDashboardParams = new Archetype({
11
11
  },
12
12
  roles: {
13
13
  $type: ['string']
14
+ },
15
+ maxTimeMS: {
16
+ $type: 'number'
14
17
  }
15
18
  }).compile('DeleteDashboardParams');
16
19
 
@@ -22,9 +25,12 @@ module.exports = ({ studioConnection, options }) => async function deleteDashboa
22
25
  await authorize('Dashboard.deleteDashboard', roles);
23
26
 
24
27
  const result = await Dashboard.deleteOne({ _id: dashboardId }).
25
- setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS })).
28
+ setOptions(operationOptions(options, params)).
26
29
  orFail();
27
30
  await DashboardResult.deleteMany({ dashboardId }).
28
- setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS }));
31
+ setOptions(operationOptions(options, params));
29
32
  return { result };
30
33
  };
34
+
35
+ module.exports.paramsType = DeleteDashboardParams;
36
+ module.exports.tags = ['destructive'];
@@ -4,7 +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 omitNullish = require('../../helpers/omitNullish');
7
+ const readOperationOptions = require('../../helpers/readOperationOptions');
8
+ const operationOptions = require('../../helpers/operationOptions');
8
9
  const { defaultMothershipURL } = require('../../../constants');
9
10
 
10
11
  const GetDashboardParams = new Archetype({
@@ -26,6 +27,13 @@ const GetDashboardParams = 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('GetDashboardParams');
31
39
 
@@ -37,11 +45,12 @@ module.exports = ({ db, studioConnection, options }) => async function getDashbo
37
45
 
38
46
  await authorize('Dashboard.getDashboard', roles);
39
47
 
48
+ const dashboardOptions = operationOptions(options, params);
40
49
  const dashboard = await Dashboard.findOne({ _id: dashboardId }).
41
- setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS }));
50
+ setOptions(dashboardOptions);
42
51
  if (evaluate) {
43
52
  let result = null;
44
- const sandbox = createSandbox({ db, maxTimeMS: options?.maxTimeMS });
53
+ const sandbox = createSandbox(db, { ...readOperationOptions(options, params) });
45
54
  const startExec = startDashboardEvaluate(DashboardResult, dashboardId, $workspaceId, userId);
46
55
  startExec.catch(() => {}); // Avoid unhandled promise rejections - we will handle this error later.
47
56
  try {
@@ -59,7 +68,7 @@ module.exports = ({ db, studioConnection, options }) => async function getDashbo
59
68
  null,
60
69
  { message: error.message },
61
70
  'failed',
62
- options
71
+ dashboardOptions
63
72
  );
64
73
  });
65
74
  return { dashboard, dashboardResult, error: { message: error.message } };
@@ -83,7 +92,7 @@ module.exports = ({ db, studioConnection, options }) => async function getDashbo
83
92
  result,
84
93
  undefined,
85
94
  'completed',
86
- options
95
+ dashboardOptions
87
96
  );
88
97
  });
89
98
 
@@ -98,15 +107,15 @@ module.exports = ({ db, studioConnection, options }) => async function getDashbo
98
107
  $workspaceId,
99
108
  authorization,
100
109
  mothershipUrl,
101
- options
110
+ dashboardOptions
102
111
  );
103
112
  return { dashboard, dashboardResults };
104
113
  }
105
114
  };
106
115
 
107
- async function completeDashboardEvaluate(Dashboard, DashboardResult, dashboardResultId, result, error, status, options) {
116
+ async function completeDashboardEvaluate(Dashboard, DashboardResult, dashboardResultId, result, error, status, dashboardOptions) {
108
117
  const dashboardResult = await DashboardResult.findById(dashboardResultId).
109
- setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS })).
118
+ setOptions(dashboardOptions).
110
119
  orFail();
111
120
  const finishedEvaluatingAt = new Date();
112
121
  dashboardResult.finishedEvaluatingAt = finishedEvaluatingAt;
@@ -115,7 +124,7 @@ async function completeDashboardEvaluate(Dashboard, DashboardResult, dashboardRe
115
124
  dashboardResult.status = status;
116
125
  await dashboardResult.save();
117
126
  await Dashboard.updateOne({ _id: dashboardResult.dashboardId }, { $set: { lastEvaluatedAt: finishedEvaluatingAt } }).
118
- setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS }));
127
+ setOptions(dashboardOptions);
119
128
  return { dashboardResult };
120
129
  }
121
130
 
@@ -131,7 +140,7 @@ async function startDashboardEvaluate(DashboardResult, dashboardId, workspaceId,
131
140
  return { dashboardResult };
132
141
  }
133
142
 
134
- async function getDashboardResults(DashboardResult, dashboardId, workspaceId, authorization, mothershipUrl, options) {
143
+ async function getDashboardResults(DashboardResult, dashboardId, workspaceId, authorization, mothershipUrl, dashboardOptions) {
135
144
  const filter = { dashboardId };
136
145
  if (workspaceId != null) {
137
146
  filter.workspaceId = workspaceId;
@@ -141,7 +150,7 @@ async function getDashboardResults(DashboardResult, dashboardId, workspaceId, au
141
150
  DashboardResult.find(filter).
142
151
  sort({ _id: -1 }).
143
152
  limit(10).
144
- setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS })),
153
+ setOptions(dashboardOptions),
145
154
  getMothershipDashboardResults(dashboardId, workspaceId, authorization, mothershipUrl)
146
155
  ]);
147
156
 
@@ -212,3 +221,6 @@ function addDocumentSchemaPaths(result) {
212
221
  }
213
222
  result.$document.schemaPaths = schemaPaths;
214
223
  }
224
+
225
+ module.exports.paramsType = GetDashboardParams;
226
+ module.exports.tags = ['readOnly'];
@@ -2,11 +2,14 @@
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 GetDashboardParams = new Archetype({
8
8
  roles: {
9
9
  $type: ['string']
10
+ },
11
+ maxTimeMS: {
12
+ $type: 'number'
10
13
  }
11
14
  }).compile('GetDashboardParams');
12
15
 
@@ -18,8 +21,11 @@ module.exports = ({ studioConnection, options }) => async function getDashboards
18
21
 
19
22
  const dashboards = await Dashboard
20
23
  .find()
21
- .setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS }))
24
+ .setOptions(operationOptions(options, params))
22
25
  .sort({ isPinned: -1, createdAt: -1, _id: -1 });
23
26
 
24
27
  return { dashboards };
25
28
  };
29
+
30
+ module.exports.paramsType = GetDashboardParams;
31
+ 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 UpdateDashboardParams = new Archetype({
8
8
  dashboardId: {
@@ -23,6 +23,9 @@ const UpdateDashboardParams = new Archetype({
23
23
  },
24
24
  roles: {
25
25
  $type: ['string']
26
+ },
27
+ maxTimeMS: {
28
+ $type: 'number'
26
29
  }
27
30
  }).compile('UpdateDashboardParams');
28
31
 
@@ -55,7 +58,10 @@ module.exports = ({ studioConnection, options }) => async function updateDashboa
55
58
  dashboardId,
56
59
  updateObj,
57
60
  { sanitizeFilter: true, returnDocument: 'after' }
58
- ).setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS }));
61
+ ).setOptions(operationOptions(options, params));
59
62
 
60
63
  return { doc };
61
64
  };
65
+
66
+ module.exports.paramsType = UpdateDashboardParams;
67
+ 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 AddFieldParams = new Archetype({
8
8
  model: {
@@ -23,6 +23,9 @@ const AddFieldParams = new Archetype({
23
23
  },
24
24
  roles: {
25
25
  $type: ['string']
26
+ },
27
+ maxTimeMS: {
28
+ $type: 'number'
26
29
  }
27
30
  }).compile('AddFieldParams');
28
31
 
@@ -49,7 +52,10 @@ module.exports = ({ db, options }) => async function addField(params) {
49
52
  runValidators: false,
50
53
  strict: false
51
54
  }
52
- ).setOptions(omitNullish({ maxTimeMS: options?.maxTimeMS }));
55
+ ).setOptions(operationOptions(options, params));
53
56
 
54
57
  return { doc };
55
58
  };
59
+
60
+ module.exports.paramsType = AddFieldParams;
61
+ module.exports.tags = [];
@@ -3,7 +3,7 @@
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
5
  const { Buffer } = require('buffer');
6
- const omitNullish = require('../../helpers/omitNullish');
6
+ const readOperationOptions = require('../../helpers/readOperationOptions');
7
7
  const validateDocumentWithTimeout = require('../../helpers/validateDocumentWithTimeout');
8
8
 
9
9
  const SAMPLE_SIZE = 1000;
@@ -16,6 +16,13 @@ const AnalyzeSchemaParams = 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('AnalyzeSchemaParams');
21
28
 
@@ -29,11 +36,11 @@ module.exports = ({ db, options }) => async function analyzeSchema(params) {
29
36
  throw new Error(`Model ${model} not found`);
30
37
  }
31
38
 
32
- const operationOptions = omitNullish({ maxTimeMS: options?.maxTimeMS });
33
- const documentCount = await Model.collection.countDocuments({}, operationOptions);
39
+ const dbOptions = readOperationOptions(options, params);
40
+ const documentCount = await Model.collection.countDocuments({}, dbOptions);
34
41
  const rawDocs = documentCount > SAMPLE_SIZE ?
35
- await Model.collection.aggregate([{ $sample: { size: SAMPLE_SIZE } }], operationOptions).toArray() :
36
- await Model.collection.find({}, operationOptions).toArray();
42
+ await Model.collection.aggregate([{ $sample: { size: SAMPLE_SIZE } }], dbOptions).toArray() :
43
+ await Model.collection.find({}, dbOptions).toArray();
37
44
 
38
45
  const paths = getSchemaPaths(Model.schema);
39
46
  const pathTypeCounts = {};
@@ -202,3 +209,6 @@ function getType(value) {
202
209
  }
203
210
  return typeof value;
204
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'];