@mongoosejs/studio 0.4.1 → 0.5.1

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.
@@ -29,6 +29,9 @@ client.interceptors.response.use(
29
29
  );
30
30
 
31
31
  if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
32
+ exports.getCapabilities = function getCapabilities() {
33
+ return client.post('', { action: 'getCapabilities' }).then(res => res.data);
34
+ };
32
35
  exports.status = function status() {
33
36
  return client.post('', { action: 'status' }).then(res => res.data);
34
37
  };
@@ -205,6 +208,9 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
205
208
  }
206
209
  };
207
210
  } else {
211
+ exports.getCapabilities = function getCapabilities() {
212
+ return client.get('/getCapabilities').then(res => res.data);
213
+ };
208
214
  exports.status = function status() {
209
215
  return client.get('/status').then(res => res.data);
210
216
  };
@@ -24,6 +24,7 @@
24
24
  :script="part.content"
25
25
  :language="part.language"
26
26
  :target-dashboard-id="targetDashboardId"
27
+ :capabilities="capabilities"
27
28
  @copyMessage="copyMessage"></chat-message-script>
28
29
  </div>
29
30
  </div>
@@ -6,7 +6,7 @@ const template = require('./chat-message.html');
6
6
 
7
7
  module.exports = app => app.component('chat-message', {
8
8
  template: template,
9
- props: ['message', 'targetDashboardId'],
9
+ props: ['message', 'targetDashboardId', 'capabilities'],
10
10
  computed: {
11
11
  styleForMessage() {
12
12
  return this.message.role === 'user' ? 'p-3 bg-muted' : 'py-3 pr-3';
@@ -62,10 +62,11 @@ module.exports = app => app.component('chat-message', {
62
62
  if (input.limit && input.limit !== 10) parts.push('limit=' + input.limit);
63
63
  return parts.length ? '(' + parts.join(', ') + ')' : '';
64
64
  },
65
- async executeScript(message, script) {
65
+ async executeScript(message, script, dryRun) {
66
66
  const { chatMessage } = await api.ChatMessage.executeScript({
67
67
  chatMessageId: message._id,
68
- script
68
+ script,
69
+ dryRun
69
70
  });
70
71
  message.executionResult = chatMessage.executionResult;
71
72
  console.log(message);
@@ -20,17 +20,19 @@
20
20
  : 'bg-muted hover:bg-muted text-gray-600'
21
21
  ]"
22
22
  @click="activeTab = 'output'">
23
- Output
23
+ <span>Output</span>
24
+ <span v-if="isDryRunResult" class="ml-1">[Dry Run]</span>
25
+ <span
26
+ v-if="isDryRunResult"
27
+ class="ml-1 inline-flex align-middle cursor-pointer"
28
+ title="This output was produced in dry run mode"
29
+ @click.stop="openDryRunInfoModal">
30
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor">
31
+ <path fill-rule="evenodd" d="M18 10A8 8 0 112 10a8 8 0 0116 0zm-7-3a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 102 0v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
32
+ </svg>
33
+ </span>
24
34
  </button>
25
35
  <div class="ml-auto mr-1 flex items-center">
26
- <button
27
- v-if="activeTab === 'output'"
28
- class="px-2 py-1 mr-1 text-xs bg-gray-700 text-white border-none rounded cursor-pointer hover:bg-primary-hover transition-colors flex items-center"
29
- @click="openDetailModal">
30
- <svg xmlns="http://www.w3.org/2000/svg" style="height: 1.2em;" fill="none" viewBox="0 0 24 24" stroke="currentColor">
31
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 1v4m0 0h-4m4 0l-5-5" />
32
- </svg>
33
- </button>
34
36
  <button
35
37
  v-if="activeTab === 'code' && !isEditing"
36
38
  class="px-2 py-1 mr-1 text-xs bg-gray-700 text-white border-none rounded cursor-pointer hover:bg-gray-600 transition-colors flex items-center"
@@ -39,12 +41,58 @@
39
41
  <path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM4 13.5V16h2.5l7.086-7.086-2.828-2.828L4 13.5z" />
40
42
  </svg>
41
43
  </button>
42
- <async-button
43
- v-if="!isEditing"
44
- class="px-2 py-1 text-xs bg-primary hover:bg-primary-hover text-primary-text border-none rounded cursor-pointer transition-colors disabled:bg-gray-400"
45
- @click="executeScript">
46
- Run
47
- </async-button>
44
+ <button
45
+ v-if="activeTab === 'output'"
46
+ class="px-2 py-1 mr-1 text-xs bg-gray-700 text-white border-none rounded cursor-pointer hover:bg-primary-hover transition-colors flex items-center"
47
+ @click="openDetailModal">
48
+ <svg xmlns="http://www.w3.org/2000/svg" style="height: 1.2em;" fill="none" viewBox="0 0 24 24" stroke="currentColor">
49
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 1v4m0 0h-4m4 0l-5-5" />
50
+ </svg>
51
+ </button>
52
+ <div v-if="!isEditing" class="relative mr-1" ref="runDropdown">
53
+ <div class="flex items-center">
54
+ <button
55
+ class="px-2 py-1 text-xs bg-primary hover:bg-primary-hover text-primary-text border-none rounded-l-md cursor-pointer transition-colors disabled:bg-gray-400 disabled:cursor-not-allowed"
56
+ :disabled="isExecuting"
57
+ @click="executeScript()">
58
+ {{ selectedRunLabel }}
59
+ </button>
60
+ <button
61
+ class="px-1 py-1 text-xs bg-primary hover:bg-primary-hover text-primary-text border-l border-black/10 rounded-r-md cursor-pointer transition-colors disabled:bg-gray-400 disabled:cursor-not-allowed"
62
+ :disabled="isExecuting"
63
+ @click.stop="toggleRunDropdown">
64
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
65
+ <path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd" />
66
+ </svg>
67
+ </button>
68
+ <button
69
+ class="ml-1 px-1 py-1 text-content-secondary hover:bg-muted rounded cursor-pointer transition-colors"
70
+ @click="openRunInfoModal"
71
+ title="How run works">
72
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
73
+ <path fill-rule="evenodd" d="M18 10A8 8 0 112 10a8 8 0 0116 0zm-7-3a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 102 0v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
74
+ </svg>
75
+ </button>
76
+ </div>
77
+ <div
78
+ v-if="showRunDropdown"
79
+ class="absolute right-0 z-10 mt-1 w-40 origin-top-right rounded-md bg-surface py-1 shadow-lg ring-1 ring-black/5">
80
+ <button
81
+ class="block w-full text-left px-4 py-2 text-xs text-content-secondary hover:bg-muted"
82
+ :disabled="isExecuting"
83
+ @click="selectRunMode('run')">
84
+ Run
85
+ </button>
86
+ <button
87
+ class="block w-full text-left px-4 py-2 text-xs"
88
+ :class="isExecuting ? 'cursor-not-allowed text-gray-400' : canUseDryRun ? 'text-content-secondary hover:bg-muted' : 'cursor-not-allowed text-gray-400'"
89
+ :aria-disabled="!canUseDryRun"
90
+ :title="dryRunDisabledTitle"
91
+ @click="selectRunMode('dryRun')">
92
+ Dry Run
93
+ </button>
94
+ </div>
95
+ </div>
48
96
  <div class="relative ml-1" ref="dropdown">
49
97
  <button
50
98
  @click.stop="toggleDropdown"
@@ -100,7 +148,7 @@
100
148
  </button>
101
149
  <async-button
102
150
  class="px-2 py-1 text-xs bg-blue-600 text-white border-none rounded cursor-pointer hover:bg-blue-700 transition-colors disabled:bg-gray-400"
103
- @click="executeScript">
151
+ @click="executeScript(false)">
104
152
  Execute
105
153
  </async-button>
106
154
  </div>
@@ -134,6 +182,40 @@
134
182
  </div>
135
183
  </template>
136
184
  </modal>
185
+ <modal v-if="showRunInfoModal">
186
+ <template #body>
187
+ <div class="modal-exit" @click="showRunInfoModal = false">&times;</div>
188
+ <div>
189
+ <div class="mt-4 text-content font-semibold">How Run Works</div>
190
+ <p class="mt-4 text-sm text-content-secondary">
191
+ Run executes the script shown in this message on the server with access to your Mongoose models through <code class="bg-muted px-1 rounded">db.models</code>. The latest script is saved back onto the chat message.
192
+ </p>
193
+ <p class="mt-2 text-sm text-content-secondary">
194
+ Output and console logs are stored on the message, and the Output tab updates to show the latest result.
195
+ </p>
196
+ <p class="mt-2 text-sm text-content-secondary">
197
+ Dry Run executes the same script inside a MongoDB transaction. Studio attaches the transaction session to Mongoose model and collection calls, captures the output and logs, then aborts the transaction so writes are rolled back.
198
+ </p>
199
+ <p class="mt-2 text-sm text-content-secondary">
200
+ Dry Run requires MongoDB transaction support. It is intended for previewing what a script would return before persisting changes.
201
+ </p>
202
+ </div>
203
+ </template>
204
+ </modal>
205
+ <modal v-if="showDryRunInfoModal">
206
+ <template #body>
207
+ <div class="modal-exit" @click="showDryRunInfoModal = false">&times;</div>
208
+ <div>
209
+ <div class="mt-4 text-content font-semibold">Dry Run Output</div>
210
+ <p class="mt-4 text-sm text-content-secondary">
211
+ This output was produced in dry run mode. The script ran on the server inside a MongoDB transaction, and Studio aborted that transaction after the script finished.
212
+ </p>
213
+ <p class="mt-2 text-sm text-content-secondary">
214
+ No data changes from the script were persisted. The displayed output and console logs were saved on the chat message so you can review what the script returned.
215
+ </p>
216
+ </div>
217
+ </template>
218
+ </modal>
137
219
  <modal v-if="showCreateDashboardModal">
138
220
  <template #body>
139
221
  <div class="modal-exit" @click="showCreateDashboardModal = false">&times;</div>
@@ -6,15 +6,20 @@ const template = require('./chat-message-script.html');
6
6
 
7
7
  module.exports = app => app.component('chat-message-script', {
8
8
  template,
9
- props: ['message', 'script', 'language', 'targetDashboardId'],
9
+ props: ['message', 'script', 'language', 'targetDashboardId', 'capabilities'],
10
10
  emits: ['copyMessage'],
11
11
  data() {
12
12
  return {
13
13
  activeTab: 'code',
14
+ selectedRunMode: 'run',
14
15
  showDetailModal: false,
16
+ showRunInfoModal: false,
17
+ showDryRunInfoModal: false,
15
18
  showCreateDashboardModal: false,
16
19
  showOverwriteDashboardConfirmationModal: false,
20
+ showRunDropdown: false,
17
21
  showDropdown: false,
22
+ isExecuting: false,
18
23
  newDashboardTitle: '',
19
24
  dashboardCode: '',
20
25
  createError: null,
@@ -30,32 +35,60 @@ module.exports = app => app.component('chat-message-script', {
30
35
  },
31
36
  canOverwriteDashboard() {
32
37
  return !!this.targetDashboardId;
38
+ },
39
+ canUseDryRun() {
40
+ return this.capabilities?.supportsTransactions !== false;
41
+ },
42
+ dryRunDisabledTitle() {
43
+ return this.canUseDryRun ? null : 'dry run mode requires MongoDB transactions support';
44
+ },
45
+ isDryRunResult() {
46
+ return !!this.message.executionResult?.dryRun;
47
+ },
48
+ selectedRunLabel() {
49
+ return this.selectedRunMode === 'dryRun' ? 'Dry Run' : 'Run';
33
50
  }
34
51
  },
35
52
  methods: {
36
- async executeScript() {
53
+ async executeScript(dryRun = this.selectedRunMode === 'dryRun') {
37
54
  const scriptToRun = this.isEditing ? this.editedScript : this.script;
55
+ if (this.isExecuting) {
56
+ return;
57
+ }
58
+ this.showRunDropdown = false;
38
59
  this.editedScript = scriptToRun;
39
- const { chatMessage } = await api.ChatMessage.executeScript({
40
- chatMessageId: this.message._id,
41
- script: scriptToRun
42
- });
43
- this.message.executionResult = chatMessage.executionResult;
44
- this.message.script = chatMessage.script;
45
- this.message.content = chatMessage.content;
46
- this.editedScript = chatMessage.script;
47
- if (this.isEditing) {
48
- this.finishEditing();
49
- } else {
50
- this.highlightCode();
60
+ this.isExecuting = true;
61
+ try {
62
+ const { chatMessage } = await api.ChatMessage.executeScript({
63
+ chatMessageId: this.message._id,
64
+ script: scriptToRun,
65
+ dryRun
66
+ });
67
+ this.message.executionResult = chatMessage.executionResult;
68
+ this.message.script = chatMessage.script;
69
+ this.message.content = chatMessage.content;
70
+ this.editedScript = chatMessage.script;
71
+ if (this.isEditing) {
72
+ this.finishEditing();
73
+ } else {
74
+ this.highlightCode();
75
+ }
76
+ this.activeTab = 'output';
77
+ this.$toast.success('Script executed successfully!');
78
+ return chatMessage;
79
+ } finally {
80
+ this.isExecuting = false;
51
81
  }
52
- this.activeTab = 'output';
53
- this.$toast.success('Script executed successfully!');
54
- return chatMessage;
55
82
  },
56
83
  openDetailModal() {
57
84
  this.showDetailModal = true;
58
85
  },
86
+ openRunInfoModal() {
87
+ this.showRunInfoModal = true;
88
+ },
89
+ openDryRunInfoModal() {
90
+ this.showDryRunInfoModal = true;
91
+ },
59
92
  openCreateDashboardModal() {
60
93
  this.newDashboardTitle = '';
61
94
  this.dashboardCode = this.script;
@@ -72,6 +105,21 @@ module.exports = app => app.component('chat-message-script', {
72
105
  },
73
106
  toggleDropdown() {
74
107
  this.showDropdown = !this.showDropdown;
108
+ this.showRunDropdown = false;
109
+ },
110
+ toggleRunDropdown() {
111
+ this.showRunDropdown = !this.showRunDropdown;
112
+ this.showDropdown = false;
113
+ },
114
+ selectRunMode(mode) {
115
+ if (this.isExecuting) {
116
+ return;
117
+ }
118
+ if (mode === 'dryRun' && !this.canUseDryRun) {
119
+ return;
120
+ }
121
+ this.selectedRunMode = mode;
122
+ this.showRunDropdown = false;
75
123
  },
76
124
  startEditing() {
77
125
  this.isEditing = true;
@@ -97,7 +145,11 @@ module.exports = app => app.component('chat-message-script', {
97
145
  });
98
146
  },
99
147
  handleBodyClick(event) {
148
+ const runDropdown = this.$refs.runDropdown;
100
149
  const dropdown = this.$refs.dropdown;
150
+ if (runDropdown && typeof runDropdown.contains === 'function' && !runDropdown.contains(event.target)) {
151
+ this.showRunDropdown = false;
152
+ }
101
153
  if (dropdown && typeof dropdown.contains === 'function' && !dropdown.contains(event.target)) {
102
154
  this.showDropdown = false;
103
155
  }
@@ -172,6 +224,14 @@ module.exports = app => app.component('chat-message-script', {
172
224
  this.editedScript = newScript;
173
225
  this.highlightCode();
174
226
  }
227
+ },
228
+ capabilities: {
229
+ immediate: true,
230
+ handler(capabilities) {
231
+ if (capabilities?.supportsTransactions === false && this.selectedRunMode === 'dryRun') {
232
+ this.selectedRunMode = 'run';
233
+ }
234
+ }
175
235
  }
176
236
  },
177
237
  mounted() {
@@ -112,7 +112,11 @@
112
112
  </div>
113
113
  <ul role="list" class="space-y-4" v-else>
114
114
  <li v-for="message in chatMessages" :key="message._id">
115
- <chat-message :message="message" :target-dashboard-id="currentThread?.dashboardId"></chat-message>
115
+ <chat-message
116
+ :message="message"
117
+ :target-dashboard-id="currentThread?.dashboardId"
118
+ :capabilities="capabilities">
119
+ </chat-message>
116
120
  </li>
117
121
  </ul>
118
122
  </div>
@@ -23,7 +23,12 @@ module.exports = {
23
23
  sharingThread: false,
24
24
  threadSearch: '',
25
25
  showProUpgradeModal: false,
26
- agentTools: agentToolMetadata
26
+ agentTools: agentToolMetadata,
27
+ capabilities: {
28
+ supportsChangeStreams: false,
29
+ supportsTransactions: true,
30
+ supportsAI: false
31
+ }
27
32
  }),
28
33
  methods: {
29
34
  async sendMessage() {
@@ -300,8 +305,12 @@ module.exports = {
300
305
 
301
306
  this.draftAgentMode = this.getAgentModePreference();
302
307
  this.chatThreadId = this.threadId;
303
- const { chatThreads } = await api.ChatThread.listChatThreads();
308
+ const [{ chatThreads }, { capabilities }] = await Promise.all([
309
+ api.ChatThread.listChatThreads(),
310
+ api.getCapabilities()
311
+ ]);
304
312
  this.chatThreads = chatThreads;
313
+ this.capabilities = capabilities;
305
314
  if (this.chatThreadId) {
306
315
  await this.syncCurrentThreadAgentMode();
307
316
  const { chatMessages } = await api.ChatThread.getChatThread({ chatThreadId: this.chatThreadId });
@@ -90,7 +90,6 @@
90
90
  display: flex;
91
91
  align-items: center;
92
92
  justify-content: center;
93
- padding-bottom: 0.25em;
94
93
  }
95
94
 
96
95
  .modal-container .modal-exit:hover {
@@ -1058,8 +1058,8 @@ module.exports = app => app.component('models', {
1058
1058
  const projectionPaths = this.normalizeProjectionPathsForDisplay(routeProjectionInput);
1059
1059
  const fromProjectionInput = Array.isArray(projectionPaths) ?
1060
1060
  projectionPaths
1061
- .map(path => this.schemaPaths.find(p => p.path === path))
1062
- .filter(Boolean) :
1061
+ .map(path => this.schemaPaths.find(p => p.path === path))
1062
+ .filter(Boolean) :
1063
1063
  [];
1064
1064
  this.filteredPaths = fromProjectionInput;
1065
1065
  } else {
@@ -6,9 +6,9 @@ function getBraceDepth(text, pos) {
6
6
  let depth = 0;
7
7
  for (let i = 0; i < pos; i++) {
8
8
  const ch = text[i];
9
- if (ch === "{") {
9
+ if (ch === '{') {
10
10
  depth++;
11
- } else if (ch === "}") {
11
+ } else if (ch === '}') {
12
12
  depth--;
13
13
  }
14
14
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mongoosejs/studio",
3
- "version": "0.4.1",
4
- "description": "A Mongoose-native MongoDB UI with schema-aware autocomplete, AI-assisted queries, and dashboards that understand your models - not just your data.",
3
+ "version": "0.5.1",
4
+ "description": "AI-Powered Data Workspace for MongoDB: Turn your MongoDB data into dashboards, maps, and interactive workflows - powered by your Mongoose models and AI.",
5
5
  "homepage": "https://mongoosestudio.app/",
6
6
  "repository": {
7
7
  "type": "git",
@@ -1,20 +0,0 @@
1
- 'use strict';
2
-
3
- const Archetype = require('archetype');
4
-
5
- const ExecuteParams = new Archetype({
6
- code: {
7
- $type: 'string',
8
- $required: true
9
- }
10
- }).compile('ExecuteParams');
11
-
12
- module.exports = ({ db }) => async function execute(params) {
13
- const { code } = new ExecuteParams(params);
14
-
15
- const res = eval(code);
16
-
17
- return {
18
- res
19
- };
20
- };
@@ -1,3 +0,0 @@
1
- 'use strict';
2
-
3
- exports.execute = require('./execute');