@mongoosejs/studio 0.5.12 → 0.5.14

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 (60) hide show
  1. package/AGENTS.md +7 -0
  2. package/CLAUDE.md +7 -0
  3. package/README.md +3 -0
  4. package/backend/actions/ChatMessage/executeScript.js +7 -5
  5. package/backend/actions/ChatThread/createChatMessage.js +4 -2
  6. package/backend/actions/ChatThread/getChatThread.js +5 -2
  7. package/backend/actions/ChatThread/listChatThreads.js +3 -1
  8. package/backend/actions/ChatThread/shareChatThread.js +5 -2
  9. package/backend/actions/ChatThread/streamChatMessage.js +4 -2
  10. package/backend/actions/ChatThread/toggleAgentMode.js +5 -2
  11. package/backend/actions/Dashboard/deleteDashboard.js +7 -3
  12. package/backend/actions/Dashboard/getDashboard.js +21 -10
  13. package/backend/actions/Dashboard/getDashboards.js +3 -1
  14. package/backend/actions/Dashboard/updateDashboard.js +3 -2
  15. package/backend/actions/Model/addField.js +3 -2
  16. package/backend/actions/Model/analyzeSchema.js +6 -4
  17. package/backend/actions/Model/deleteDocument.js +3 -3
  18. package/backend/actions/Model/deleteDocuments.js +3 -1
  19. package/backend/actions/Model/executeDocumentScript.js +25 -29
  20. package/backend/actions/Model/exportQueryResults.js +3 -1
  21. package/backend/actions/Model/getCollectionInfo.js +5 -3
  22. package/backend/actions/Model/getDocument.js +3 -2
  23. package/backend/actions/Model/getDocuments.js +15 -5
  24. package/backend/actions/Model/getDocumentsStream.js +28 -18
  25. package/backend/actions/Model/getEstimatedDocumentCounts.js +5 -2
  26. package/backend/actions/Model/updateDocument.js +4 -2
  27. package/backend/actions/Model/updateDocuments.js +4 -2
  28. package/backend/actions/Model/validateDocument.js +5 -2
  29. package/backend/actions/Task/cancelTask.js +6 -3
  30. package/backend/actions/Task/getTaskOverview.js +3 -2
  31. package/backend/actions/Task/getTasks.js +3 -2
  32. package/backend/actions/Task/getTasksOverTime.js +3 -2
  33. package/backend/actions/Task/rescheduleTask.js +6 -3
  34. package/backend/actions/Task/runTask.js +6 -3
  35. package/backend/helpers/omitNullish.js +5 -0
  36. package/backend/index.js +4 -4
  37. package/backend/sandbox/createSandbox.js +2 -2
  38. package/backend/sandbox/createScriptDb.js +42 -19
  39. package/frontend/public/app.js +3210 -2706
  40. package/frontend/public/tw.css +68 -0
  41. package/frontend/src/api.js +5 -1
  42. package/frontend/src/chat/chat-message/chat-message.js +5 -1
  43. package/frontend/src/detail-default/detail-default.html +0 -10
  44. package/frontend/src/detail-default/detail-default.js +29 -1061
  45. package/frontend/src/document-details/document-property/document-property.html +0 -11
  46. package/frontend/src/document-details/document-property/document-property.js +6 -14
  47. package/frontend/src/edit-geojson/edit-geojson.html +17 -0
  48. package/frontend/src/edit-geojson/edit-geojson.js +1104 -0
  49. package/frontend/src/models/models.html +29 -8
  50. package/frontend/src/models/models.js +30 -2
  51. package/frontend/src/tooltip/tooltip.html +28 -0
  52. package/frontend/src/tooltip/tooltip.js +94 -0
  53. package/index.d.ts +1 -1
  54. package/local.js +3 -2
  55. package/nest.d.ts +1 -0
  56. package/package.json +1 -1
  57. package/seed/connect.js +4 -0
  58. package/seed/index.js +16 -0
  59. package/vercel.d.ts +48 -0
  60. package/vercel.js +3 -0
@@ -110,7 +110,18 @@
110
110
  >
111
111
  </document-search>
112
112
  <div>
113
- <span v-if="numDocuments == null">Loading ...</span>
113
+ <tooltip
114
+ v-if="numDocumentsError"
115
+ :text="'Error counting documents: ' + numDocumentsError"
116
+ >
117
+ <span
118
+ class="inline-flex rounded-md bg-red-50 px-2 py-2 text-sm text-red-700 ring-1 ring-inset ring-red-200"
119
+ role="alert"
120
+ >
121
+ Count Error
122
+ </span>
123
+ </tooltip>
124
+ <span v-else-if="numDocuments == null">Loading...</span>
114
125
  <span v-else-if="typeof numDocuments === 'number'">{{documents.length}}/{{numDocuments === 1 ? numDocuments + ' document' : numDocuments + ' documents'}}</span>
115
126
  </div>
116
127
  <button
@@ -453,11 +464,16 @@
453
464
  </tbody>
454
465
  </table>
455
466
  </div>
456
- <div v-if="outputType === 'table' && (loadingMore || (status === 'loading' && documents.length > 0))" class="flex items-center justify-center gap-2 py-3 text-sm text-content-tertiary border-t border-edge bg-surface">
457
- <img src="images/loader.gif" alt="" class="h-5 w-5">
458
- <span>Loading documents…</span>
467
+ <div
468
+ v-if="outputType === 'table' && (documentsError || loadingMore || (status === 'loading' && documents.length > 0))"
469
+ class="flex items-center justify-center gap-2 py-3 text-sm border-t"
470
+ :class="documentsError ? 'border-red-200 bg-red-50 text-red-700' : 'border-edge bg-surface text-content-tertiary'"
471
+ :role="documentsError ? 'alert' : null"
472
+ >
473
+ <img v-if="!documentsError" src="images/loader.gif" alt="" class="h-5 w-5">
474
+ <span>{{documentsError || 'Loading documents…'}}</span>
459
475
  </div>
460
- <p v-if="outputType === 'table' && documents.length === 0 && status === 'loaded'" class="mt-2 text-sm text-content-tertiary px-1">
476
+ <p v-if="outputType === 'table' && documents.length === 0 && status === 'loaded' && !documentsError" class="mt-2 text-sm text-content-tertiary px-1">
461
477
  No documents to show. Use Projection in the menu to choose columns.
462
478
  </p>
463
479
  </div>
@@ -483,9 +499,14 @@
483
499
  <list-json :value="filterDocument(document)" :references="referenceMap">
484
500
  </list-json>
485
501
  </div>
486
- <div v-if="outputType === 'json' && (loadingMore || (status === 'loading' && documents.length > 0))" class="flex items-center justify-center gap-2 py-3 text-sm text-content-tertiary">
487
- <img src="images/loader.gif" alt="" class="h-5 w-5">
488
- <span>Loading documents…</span>
502
+ <div
503
+ v-if="outputType === 'json' && (documentsError || loadingMore || (status === 'loading' && documents.length > 0))"
504
+ class="flex items-center justify-center gap-2 py-3 text-sm"
505
+ :class="documentsError ? 'rounded-md border border-red-200 bg-red-50 text-red-700' : 'text-content-tertiary'"
506
+ :role="documentsError ? 'alert' : null"
507
+ >
508
+ <img v-if="!documentsError" src="images/loader.gif" alt="" class="h-5 w-5">
509
+ <span>{{documentsError || 'Loading documents…'}}</span>
489
510
  </div>
490
511
  </div>
491
512
  <div v-else-if="outputType === 'map'" class="flex flex-col h-full">
@@ -151,6 +151,8 @@ module.exports = app => app.component('models', {
151
151
  filteredPaths: [],
152
152
  selectedPaths: [],
153
153
  numDocuments: null,
154
+ numDocumentsError: null,
155
+ documentsError: null,
154
156
  mongoDBIndexes: [],
155
157
  schemaIndexes: [],
156
158
  status: 'loading',
@@ -838,7 +840,8 @@ module.exports = app => app.component('models', {
838
840
  return filteredDoc;
839
841
  },
840
842
  async checkIfScrolledToBottom() {
841
- if (this.status === 'loading' || this.loadedAllDocs) {
843
+ // Stop infinite scroll if the last load failed, otherwise we would retry the same failing request forever.
844
+ if (this.status === 'loading' || this.loadedAllDocs || this.documentsError) {
842
845
  return;
843
846
  }
844
847
  // Infinite scroll only applies to table/json views.
@@ -864,11 +867,20 @@ module.exports = app => app.component('models', {
864
867
  try {
865
868
  const skip = this.documents.length;
866
869
  const params = this.buildDocumentFetchParams({ skip });
867
- const { docs } = await api.Model.getDocuments(params);
870
+ const { docs, numDocs, numDocsError } = await api.Model.getDocuments(params);
871
+ if (numDocsError) {
872
+ this.numDocumentsError = numDocsError;
873
+ } else if (numDocs !== undefined) {
874
+ this.numDocuments = numDocs;
875
+ this.numDocumentsError = null;
876
+ }
868
877
  if (docs.length < limit) {
869
878
  this.loadedAllDocs = true;
870
879
  }
871
880
  this.documents.push(...docs);
881
+ this.documentsError = null;
882
+ } catch (err) {
883
+ this.documentsError = err?.message;
872
884
  } finally {
873
885
  this.loadingMore = false;
874
886
  this.status = 'loaded';
@@ -1081,6 +1093,8 @@ module.exports = app => app.component('models', {
1081
1093
  // Clear previous data
1082
1094
  this.documents = [];
1083
1095
  this.numDocuments = null;
1096
+ this.numDocumentsError = null;
1097
+ this.documentsError = null;
1084
1098
  this.loadedAllDocs = false;
1085
1099
  this.lastSelectedIndex = null;
1086
1100
 
@@ -1130,6 +1144,10 @@ module.exports = app => app.component('models', {
1130
1144
  }
1131
1145
  if (event.numDocs !== undefined) {
1132
1146
  this.numDocuments = event.numDocs;
1147
+ this.numDocumentsError = null;
1148
+ }
1149
+ if (event.numDocsError) {
1150
+ this.numDocumentsError = event.numDocsError;
1133
1151
  }
1134
1152
  if (event.document) {
1135
1153
  this.documents.push(event.document);
@@ -1143,6 +1161,9 @@ module.exports = app => app.component('models', {
1143
1161
  if (docsCount < limit) {
1144
1162
  this.loadedAllDocs = true;
1145
1163
  }
1164
+ this.documentsError = null;
1165
+ } catch (err) {
1166
+ this.documentsError = err?.message;
1146
1167
  } finally {
1147
1168
  this.status = 'loaded';
1148
1169
  }
@@ -1169,8 +1190,12 @@ module.exports = app => app.component('models', {
1169
1190
  for await (const event of api.Model.getDocumentsStream(params)) {
1170
1191
  if (event.numDocs !== undefined && !numDocsReceived) {
1171
1192
  this.numDocuments = event.numDocs;
1193
+ this.numDocumentsError = null;
1172
1194
  numDocsReceived = true;
1173
1195
  }
1196
+ if (event.numDocsError) {
1197
+ this.numDocumentsError = event.numDocsError;
1198
+ }
1174
1199
  if (event.document) {
1175
1200
  this.documents.push(event.document);
1176
1201
  docsCount++;
@@ -1183,6 +1208,9 @@ module.exports = app => app.component('models', {
1183
1208
  if (docsCount < limit) {
1184
1209
  this.loadedAllDocs = true;
1185
1210
  }
1211
+ this.documentsError = null;
1212
+ } catch (err) {
1213
+ this.documentsError = err?.message;
1186
1214
  } finally {
1187
1215
  this.loadingMore = false;
1188
1216
  this.status = 'loaded';
@@ -0,0 +1,28 @@
1
+ <span
2
+ ref="trigger"
3
+ class="inline-flex outline-none focus-visible:rounded-md focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
4
+ tabindex="0"
5
+ :aria-describedby="isOpen ? tooltipId : null"
6
+ @mouseenter="show"
7
+ @mouseleave="hideUnlessPinned"
8
+ @focusin="show"
9
+ @focusout="handleFocusOut"
10
+ @click="toggle"
11
+ @keydown.esc.stop="hide"
12
+ >
13
+ <slot></slot>
14
+ <span
15
+ v-show="isOpen"
16
+ ref="tooltip"
17
+ :id="tooltipId"
18
+ class="pointer-events-none fixed z-[1000] w-max max-w-[min(20rem,calc(100vw-1rem))] whitespace-normal rounded-md bg-gray-900 px-2.5 py-1.5 text-xs font-normal leading-4 text-white shadow-lg"
19
+ :style="tooltipStyle"
20
+ role="tooltip"
21
+ >
22
+ {{ text }}
23
+ <span
24
+ class="absolute left-1/2 h-2 w-2 -translate-x-1/2 rotate-45 bg-gray-900"
25
+ :class="placement === 'bottom' ? '-top-1' : '-bottom-1'"
26
+ ></span>
27
+ </span>
28
+ </span>
@@ -0,0 +1,94 @@
1
+ 'use strict';
2
+
3
+ const template = require('./tooltip.html');
4
+
5
+ let nextTooltipId = 0;
6
+
7
+ module.exports = app => app.component('tooltip', {
8
+ props: {
9
+ text: {
10
+ type: String,
11
+ required: true
12
+ }
13
+ },
14
+ template,
15
+ data() {
16
+ return {
17
+ isOpen: false,
18
+ isPinned: false,
19
+ placement: 'bottom',
20
+ tooltipId: `tooltip-${++nextTooltipId}`,
21
+ tooltipStyle: {
22
+ visibility: 'hidden'
23
+ }
24
+ };
25
+ },
26
+ mounted() {
27
+ document.addEventListener('click', this.handleOutsideClick);
28
+ window.addEventListener('resize', this.updatePosition);
29
+ window.addEventListener('scroll', this.updatePosition, true);
30
+ },
31
+ beforeUnmount() {
32
+ document.removeEventListener('click', this.handleOutsideClick);
33
+ window.removeEventListener('resize', this.updatePosition);
34
+ window.removeEventListener('scroll', this.updatePosition, true);
35
+ },
36
+ methods: {
37
+ show() {
38
+ this.isOpen = true;
39
+ this.tooltipStyle = { visibility: 'hidden' };
40
+ this.$nextTick(this.updatePosition);
41
+ },
42
+ hide() {
43
+ this.isOpen = false;
44
+ this.isPinned = false;
45
+ },
46
+ hideUnlessPinned() {
47
+ if (!this.isPinned) {
48
+ this.isOpen = false;
49
+ }
50
+ },
51
+ toggle() {
52
+ if (this.isPinned) {
53
+ this.hide();
54
+ return;
55
+ }
56
+
57
+ this.isPinned = true;
58
+ this.show();
59
+ },
60
+ handleFocusOut(event) {
61
+ if (!this.$el.contains(event.relatedTarget)) {
62
+ this.hideUnlessPinned();
63
+ }
64
+ },
65
+ handleOutsideClick(event) {
66
+ if (!this.$el.contains(event.target)) {
67
+ this.hide();
68
+ }
69
+ },
70
+ updatePosition() {
71
+ if (!this.isOpen || !this.$refs.trigger || !this.$refs.tooltip) {
72
+ return;
73
+ }
74
+
75
+ const gap = 8;
76
+ const viewportPadding = 8;
77
+ const triggerRect = this.$refs.trigger.getBoundingClientRect();
78
+ const tooltipRect = this.$refs.tooltip.getBoundingClientRect();
79
+ const fitsBelow = triggerRect.bottom + gap + tooltipRect.height <= window.innerHeight - viewportPadding;
80
+ const top = fitsBelow ?
81
+ triggerRect.bottom + gap :
82
+ Math.max(viewportPadding, triggerRect.top - gap - tooltipRect.height);
83
+ const centeredLeft = triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2;
84
+ const maxLeft = Math.max(viewportPadding, window.innerWidth - tooltipRect.width - viewportPadding);
85
+ const left = Math.min(Math.max(viewportPadding, centeredLeft), maxLeft);
86
+
87
+ this.placement = fitsBelow ? 'bottom' : 'top';
88
+ this.tooltipStyle = {
89
+ left: `${Math.round(left)}px`,
90
+ top: `${Math.round(top)}px`
91
+ };
92
+ }
93
+ }
94
+ });
package/index.d.ts CHANGED
@@ -5,7 +5,7 @@ declare module '@mongoosejs/studio' {
5
5
  const express: (
6
6
  path?: string,
7
7
  connOrMongoose?: Connection | Mongoose,
8
- options?: { apiKey?: string; bindIp?: string | string[] | null }
8
+ options?: { apiKey?: string; bindIp?: string | string[] | null; maxTimeMS?: number }
9
9
  ) => Promise<RequestHandler>;
10
10
 
11
11
  const studio: {
package/local.js CHANGED
@@ -28,9 +28,10 @@ async function run() {
28
28
  __build: true,
29
29
  __watch: process.env.WATCH,
30
30
  _mothershipUrl: 'http://localhost:7777/.netlify/functions',
31
- apiKey: 'TACO',
31
+ // apiKey: 'TACO',
32
32
  openAIAPIKey: process.env.OPENAI_API_KEY,
33
- googleGeminiAPIKey: process.env.GEMINI_API_KEY
33
+ googleGeminiAPIKey: process.env.GEMINI_API_KEY,
34
+ maxTimeMS: 1000
34
35
  })
35
36
  );
36
37
 
package/nest.d.ts CHANGED
@@ -9,6 +9,7 @@ declare module '@mongoosejs/studio/nest' {
9
9
  connectionToken?: string | symbol | Function;
10
10
  apiKey?: string;
11
11
  bindIp?: string | string[] | null;
12
+ maxTimeMS?: number;
12
13
  [key: string]: any;
13
14
  }
14
15
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mongoosejs/studio",
3
- "version": "0.5.12",
3
+ "version": "0.5.14",
4
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": {
package/seed/connect.js CHANGED
@@ -10,6 +10,10 @@ module.exports = async function connect() {
10
10
  mongoose.model('User', new mongoose.Schema({
11
11
  name: String,
12
12
  email: String,
13
+ location: new mongoose.Schema({
14
+ type: { type: String },
15
+ coordinates: [Number]
16
+ }),
13
17
  role: String,
14
18
  plan: String,
15
19
  status: String,
package/seed/index.js CHANGED
@@ -20,6 +20,10 @@ async function run() {
20
20
  {
21
21
  name: 'Ada Lovelace',
22
22
  email: 'ada@example.com',
23
+ location: {
24
+ type: 'Point',
25
+ coordinates: [-0.1276, 51.5072]
26
+ },
23
27
  role: 'admin',
24
28
  plan: 'enterprise',
25
29
  status: 'active',
@@ -36,6 +40,10 @@ async function run() {
36
40
  {
37
41
  name: 'Grace Hopper',
38
42
  email: 'grace@example.com',
43
+ location: {
44
+ type: 'Point',
45
+ coordinates: [-74.006, 40.7128]
46
+ },
39
47
  role: 'analyst',
40
48
  plan: 'pro',
41
49
  status: 'active',
@@ -52,6 +60,10 @@ async function run() {
52
60
  {
53
61
  name: 'Linus Torvalds',
54
62
  email: 'linus@example.com',
63
+ location: {
64
+ type: 'Point',
65
+ coordinates: [24.9384, 60.1699]
66
+ },
55
67
  role: 'editor',
56
68
  plan: 'starter',
57
69
  status: 'invited',
@@ -68,6 +80,10 @@ async function run() {
68
80
  {
69
81
  name: 'Margaret Hamilton',
70
82
  email: 'margaret@example.com',
83
+ location: {
84
+ type: 'Point',
85
+ coordinates: [-71.1097, 42.3736]
86
+ },
71
87
  role: 'viewer',
72
88
  plan: 'pro',
73
89
  status: 'inactive',
package/vercel.d.ts ADDED
@@ -0,0 +1,48 @@
1
+ declare module '@mongoosejs/studio/vercel' {
2
+ import { Connection, Mongoose } from 'mongoose';
3
+
4
+ export interface VercelAPIRouteHandlerOptions {
5
+ /** Mongoose Studio Pro API key. When set, requests must be authorized. */
6
+ apiKey?: string;
7
+ /** Separate connection for Studio's own collections (dashboards, chat threads). */
8
+ studioConnection?: Connection;
9
+ /** Watch the connection for changes and stream them to the Studio frontend. */
10
+ changeStream?: boolean;
11
+ /** Maximum execution time in milliseconds for MongoDB operations. */
12
+ maxTimeMS?: number;
13
+ /** Chat model name, e.g. 'gpt-4o-mini' */
14
+ model?: string;
15
+ openAIAPIKey?: string;
16
+ anthropicAPIKey?: string;
17
+ googleGeminiAPIKey?: string;
18
+ [key: string]: any;
19
+ }
20
+
21
+ /**
22
+ * Minimal request shape: satisfied by Next.js API routes (NextApiRequest),
23
+ * Express, and other Node server frameworks with query/body parsing.
24
+ */
25
+ export interface VercelAPIRouteHandlerRequest {
26
+ query?: Record<string, any>;
27
+ body?: any;
28
+ params?: Record<string, any>;
29
+ headers: { authorization?: string | null; [key: string]: any };
30
+ }
31
+
32
+ /** Minimal response shape: satisfied by NextApiResponse and Express responses. */
33
+ export interface VercelAPIRouteHandlerResponse {
34
+ status(code: number): any;
35
+ json(body: any): any;
36
+ send(body: any): any;
37
+ }
38
+
39
+ /**
40
+ * Creates a Mongoose Studio API handler for Next.js Pages Router API routes
41
+ * (`pages/api/studio.js`) and other Vercel-style serverless functions.
42
+ * Pass `null` to use the default Mongoose global.
43
+ */
44
+ export function vercelAPIRouteHandler(
45
+ conn?: Connection | Mongoose | null,
46
+ options?: VercelAPIRouteHandlerOptions
47
+ ): (req: VercelAPIRouteHandlerRequest, res: VercelAPIRouteHandlerResponse) => Promise<any>;
48
+ }
package/vercel.js ADDED
@@ -0,0 +1,3 @@
1
+ 'use strict';
2
+
3
+ exports.vercelAPIRouteHandler = require('./backend/vercel');