@mongoosejs/studio 0.5.3 → 0.5.5

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 (29) hide show
  1. package/backend/actions/Model/analyzeSchema.js +202 -0
  2. package/backend/actions/Model/index.js +2 -0
  3. package/backend/actions/Model/validateDocument.js +35 -0
  4. package/backend/authorize.js +2 -0
  5. package/backend/helpers/validateDocumentWithTimeout.js +39 -0
  6. package/frontend/index.js +1 -2
  7. package/frontend/public/app.js +689 -114
  8. package/frontend/public/tw.css +221 -28
  9. package/frontend/src/api.js +12 -0
  10. package/frontend/src/array-utils.js +60 -2
  11. package/frontend/src/chat/chat.js +2 -2
  12. package/frontend/src/detail-array/detail-array.html +98 -14
  13. package/frontend/src/detail-array/detail-array.js +73 -3
  14. package/frontend/src/document/document.html +51 -0
  15. package/frontend/src/document/document.js +13 -0
  16. package/frontend/src/document-details/document-details.html +10 -3
  17. package/frontend/src/document-details/document-details.js +28 -0
  18. package/frontend/src/document-details/document-property/document-property.html +36 -44
  19. package/frontend/src/document-details/document-property/document-property.js +78 -14
  20. package/frontend/src/edit-array/edit-array.html +143 -8
  21. package/frontend/src/edit-array/edit-array.js +104 -2
  22. package/frontend/src/index.js +45 -9
  23. package/frontend/src/models/analyze-schema-modal/analyze-schema-modal.html +169 -0
  24. package/frontend/src/models/analyze-schema-modal/analyze-schema-modal.js +149 -0
  25. package/frontend/src/models/models.html +25 -13
  26. package/frontend/src/models/models.js +5 -13
  27. package/frontend/src/navbar/navbar.html +4 -4
  28. package/frontend/src/navbar/navbar.js +2 -3
  29. package/package.json +2 -1
@@ -0,0 +1,169 @@
1
+ <div>
2
+ <div class="modal-exit" @click="close">&times;</div>
3
+ <div class="text-xl font-bold mb-2">Analyze Schema</div>
4
+ <div class="space-y-4">
5
+ <div class="grid grid-cols-2 gap-3 text-sm md:grid-cols-4">
6
+ <div>
7
+ <div class="font-semibold text-content-secondary">Documents</div>
8
+ <div v-if="schemaAnalysis" class="text-content">{{ formatNumber(schemaAnalysis.documentCount) }}</div>
9
+ <div v-else class="mt-1 h-4 w-20 animate-pulse rounded bg-muted"></div>
10
+ </div>
11
+ <div>
12
+ <div class="font-semibold text-content-secondary">Analyzed</div>
13
+ <div v-if="schemaAnalysis" class="text-content">{{ formatNumber(schemaAnalysis.sampleSize) }}{{ schemaAnalysis.sampled ? ' sampled' : '' }}</div>
14
+ <div v-else class="mt-1 h-4 w-24 animate-pulse rounded bg-muted"></div>
15
+ </div>
16
+ <div>
17
+ <div class="font-semibold text-content-secondary">Valid</div>
18
+ <div v-if="schemaAnalysis" class="text-content">
19
+ {{ formatNumber(schemaAnalysis.validDocumentCount) }} ({{ formatPercent(schemaAnalysis.validDocumentCount, schemaAnalysis.sampleSize) }})
20
+ <router-link
21
+ v-if="schemaAnalysis.firstValidDocumentId"
22
+ :to="documentRoute(schemaAnalysis.firstValidDocumentId)"
23
+ class="ml-1 text-xs text-primary underline"
24
+ >[Example]</router-link>
25
+ </div>
26
+ <div v-else class="mt-1 h-4 w-28 animate-pulse rounded bg-muted"></div>
27
+ </div>
28
+ <div>
29
+ <div class="flex items-center gap-2 font-semibold text-content-secondary">
30
+ <span>Invalid</span>
31
+ <button
32
+ v-if="schemaAnalysis && schemaAnalysis.invalidDocuments.length > 0"
33
+ type="button"
34
+ class="text-xs font-semibold text-primary underline"
35
+ @click="showInvalidDocuments = !showInvalidDocuments"
36
+ >{{ showInvalidDocuments ? 'Hide' : 'Show' }}</button>
37
+ </div>
38
+ <div v-if="schemaAnalysis" class="text-content">
39
+ {{ formatNumber(schemaAnalysis.invalidDocumentCount) }} ({{ formatPercent(schemaAnalysis.invalidDocumentCount, schemaAnalysis.sampleSize) }})
40
+ <router-link
41
+ v-if="schemaAnalysis.firstInvalidDocumentId"
42
+ :to="documentRoute(schemaAnalysis.firstInvalidDocumentId)"
43
+ class="ml-1 text-xs text-primary underline"
44
+ >[Example]</router-link>
45
+ </div>
46
+ <div v-else class="mt-1 h-4 w-28 animate-pulse rounded bg-muted"></div>
47
+ </div>
48
+ </div>
49
+ <div v-if="schemaAnalysis && showInvalidDocuments && schemaAnalysis.invalidDocuments.length > 0" class="rounded border border-edge">
50
+ <div class="max-h-48 overflow-auto">
51
+ <div
52
+ v-for="invalidDocument in schemaAnalysis.invalidDocuments"
53
+ :key="invalidDocument.documentId"
54
+ class="grid gap-2 border-b border-edge px-3 py-2 text-sm last:border-b-0 md:grid-cols-[minmax(180px,240px)_1fr]"
55
+ >
56
+ <router-link
57
+ :to="documentRoute(invalidDocument.documentId)"
58
+ class="font-mono text-primary underline"
59
+ >{{ invalidDocument.documentId }}</router-link>
60
+ <div class="text-content-secondary">{{ invalidDocument.error }}</div>
61
+ </div>
62
+ </div>
63
+ </div>
64
+ <div class="px-px py-px">
65
+ <input
66
+ ref="fieldSearch"
67
+ v-model="fieldSearch"
68
+ type="text"
69
+ :disabled="!schemaAnalysis"
70
+ placeholder="Filter fields..."
71
+ class="w-full rounded border border-edge bg-surface px-3 py-2 text-sm text-content placeholder:text-content-tertiary focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary disabled:cursor-wait disabled:bg-muted disabled:text-content-tertiary"
72
+ />
73
+ </div>
74
+ <div class="max-h-[55vh] overflow-auto rounded border border-edge">
75
+ <table class="min-w-full table-fixed border-collapse text-sm">
76
+ <colgroup>
77
+ <col class="w-[40%]" />
78
+ <col class="w-[60%]" />
79
+ </colgroup>
80
+ <thead class="sticky top-0 bg-slate-100 border-b border-edge">
81
+ <tr>
82
+ <th class="px-3 py-2 text-left font-medium text-content border-r border-edge">Property</th>
83
+ <th class="px-3 py-2 text-left font-medium text-content">
84
+ <div class="flex flex-wrap items-center justify-between gap-2">
85
+ <span>Types</span>
86
+ <div class="isolate inline-flex shrink-0 rounded-md shadow-sm">
87
+ <button
88
+ type="button"
89
+ class="relative inline-flex items-center rounded-l-md px-2.5 py-1 text-xs font-semibold ring-1 ring-inset ring-edge-strong focus:z-10"
90
+ :class="typeDisplayMode === 'badges' ? 'bg-merlot-700 text-white' : 'bg-surface text-content-secondary hover:bg-muted'"
91
+ @click="typeDisplayMode = 'badges'"
92
+ >
93
+ Badges
94
+ </button>
95
+ <button
96
+ type="button"
97
+ class="relative -ml-px inline-flex items-center rounded-r-md px-2.5 py-1 text-xs font-semibold ring-1 ring-inset ring-edge-strong focus:z-10"
98
+ :class="typeDisplayMode === 'bars' ? 'bg-merlot-700 text-white' : 'bg-surface text-content-secondary hover:bg-muted'"
99
+ @click="typeDisplayMode = 'bars'"
100
+ >
101
+ Bars
102
+ </button>
103
+ </div>
104
+ </div>
105
+ </th>
106
+ </tr>
107
+ </thead>
108
+ <tbody class="bg-surface">
109
+ <template v-if="!schemaAnalysis">
110
+ <tr v-for="index in 6" :key="'skeleton-' + index" class="border-b border-edge last:border-b-0">
111
+ <td class="px-3 py-3 align-top border-r border-edge">
112
+ <div class="h-4 w-44 animate-pulse rounded bg-muted"></div>
113
+ </td>
114
+ <td class="px-3 py-3 align-top">
115
+ <div class="flex flex-wrap gap-1.5">
116
+ <div class="h-6 w-24 animate-pulse rounded bg-muted"></div>
117
+ <div class="h-6 w-20 animate-pulse rounded bg-muted"></div>
118
+ <div class="h-6 w-28 animate-pulse rounded bg-muted"></div>
119
+ </div>
120
+ </td>
121
+ </tr>
122
+ </template>
123
+ <tr v-for="path in filteredPaths" :key="path.path" class="border-b border-edge last:border-b-0">
124
+ <td class="px-3 py-2 align-top border-r border-edge text-xs text-content">
125
+ <div class="flex flex-wrap items-center gap-1.5">
126
+ <span class="font-mono" v-html="highlightPath(path.path)"></span>
127
+ <span
128
+ v-if="primaryType(path)"
129
+ class="rounded px-1.5 py-0.5 text-[11px] font-semibold"
130
+ :style="typeBadgeStyle(primaryType(path).type)"
131
+ >{{ primaryType(path).type }}</span>
132
+ <span v-if="path.required" class="rounded bg-red-50 px-1.5 py-0.5 text-[11px] font-semibold text-red-700">required</span>
133
+ </div>
134
+ </td>
135
+ <td class="px-3 py-2 align-top">
136
+ <div v-if="typeDisplayMode === 'badges'" class="flex flex-wrap gap-1.5">
137
+ <span
138
+ v-for="typeInfo in path.types"
139
+ :key="typeInfo.type"
140
+ class="rounded px-2 py-1 text-xs font-semibold"
141
+ :style="typeBadgeStyle(typeInfo.type)"
142
+ >
143
+ <span class="font-mono">{{ typeInfo.type }}</span>:
144
+ {{ formatPercent(typeInfo.count, pathValueCount(path)) }}
145
+ </span>
146
+ </div>
147
+ <div v-else class="min-w-[260px]">
148
+ <div class="flex h-7 overflow-hidden rounded-lg border border-edge bg-surface">
149
+ <div
150
+ v-for="typeInfo in path.types"
151
+ :key="typeInfo.type"
152
+ :title="`${typeInfo.type}: ${formatPercent(typeInfo.count, pathValueCount(path))}`"
153
+ class="flex h-full items-center overflow-hidden px-2 text-xs font-semibold"
154
+ :style="typeSegmentStyle(typeInfo.type, typeInfo.count, pathValueCount(path))"
155
+ >
156
+ <span class="truncate">{{ typeInfo.type }}</span>
157
+ </div>
158
+ </div>
159
+ </div>
160
+ </td>
161
+ </tr>
162
+ <tr v-if="schemaAnalysis && filteredPaths.length === 0">
163
+ <td colspan="2" class="px-3 py-4 text-sm text-content-tertiary">No fields match "{{ fieldSearch }}".</td>
164
+ </tr>
165
+ </tbody>
166
+ </table>
167
+ </div>
168
+ </div>
169
+ </div>
@@ -0,0 +1,149 @@
1
+ 'use strict';
2
+
3
+ const api = require('../../api');
4
+ const template = require('./analyze-schema-modal.html');
5
+ const xss = require('xss');
6
+
7
+ const TYPE_COLORS = {
8
+ string: '#315f5b',
9
+ number: '#465b86',
10
+ objectid: '#2f6381',
11
+ boolean: '#5b6540',
12
+ date: '#5a5278',
13
+ array: '#6b573d',
14
+ object: '#3f6648',
15
+ mixed: '#62546a',
16
+ buffer: '#5a5f50',
17
+ decimal128: '#4a6474',
18
+ map: '#4e665d',
19
+ null: '#3f4650',
20
+ undefined: '#5b626b'
21
+ };
22
+
23
+ const FALLBACK_TYPE_COLORS = [
24
+ '#315f5b',
25
+ '#465b86',
26
+ '#2f6381',
27
+ '#5b6540',
28
+ '#5a5278',
29
+ '#6b573d',
30
+ '#3f6648',
31
+ '#62546a'
32
+ ];
33
+
34
+ module.exports = app => app.component('analyze-schema-modal', {
35
+ template,
36
+ props: ['currentModel'],
37
+ emits: ['close'],
38
+ data: () => ({
39
+ schemaAnalysis: null,
40
+ fieldSearch: '',
41
+ showInvalidDocuments: false,
42
+ typeDisplayMode: 'bars'
43
+ }),
44
+ computed: {
45
+ filteredPaths() {
46
+ if (!this.schemaAnalysis) {
47
+ return [];
48
+ }
49
+ const search = this.fieldSearch.trim().toLowerCase();
50
+ if (!search) {
51
+ return this.schemaAnalysis.paths;
52
+ }
53
+ return this.schemaAnalysis.paths.filter(path => path.path.toLowerCase().includes(search));
54
+ }
55
+ },
56
+ async mounted() {
57
+ window.analyzeSchemaModal = this;
58
+ const { analysis } = await api.Model.analyzeSchema({ model: this.currentModel });
59
+ this.schemaAnalysis = analysis;
60
+ this.$nextTick(() => {
61
+ this.$refs.fieldSearch?.focus();
62
+ });
63
+ },
64
+ unmounted() {
65
+ window.analyzeSchemaModal = null;
66
+ },
67
+ methods: {
68
+ close() {
69
+ this.$emit('close');
70
+ },
71
+ formatNumber(value) {
72
+ if (typeof value !== 'number') {
73
+ return 'Unknown';
74
+ }
75
+
76
+ return value.toLocaleString();
77
+ },
78
+ formatPercent(value, total) {
79
+ if (!total) {
80
+ return '0%';
81
+ }
82
+ return `${Math.round((value / total) * 1000) / 10}%`;
83
+ },
84
+ typePercentValue(value, total) {
85
+ if (!total) {
86
+ return 0;
87
+ }
88
+ return Math.round((value / total) * 1000) / 10;
89
+ },
90
+ pathValueCount(path) {
91
+ return typeof path?.valueCount === 'number' && path.valueCount > 0 ?
92
+ path.valueCount :
93
+ this.schemaAnalysis.sampleSize;
94
+ },
95
+ primaryType(path) {
96
+ if (!path || !Array.isArray(path.types) || path.types.length === 0) {
97
+ return null;
98
+ }
99
+
100
+ return path.types.find(typeInfo => typeInfo.type !== 'null' && typeInfo.type !== 'undefined') || path.types[0];
101
+ },
102
+ typeColor(type) {
103
+ const normalizedType = String(type || '').toLowerCase();
104
+ if (TYPE_COLORS[normalizedType]) {
105
+ return TYPE_COLORS[normalizedType];
106
+ }
107
+
108
+ let hash = 0;
109
+ for (let i = 0; i < normalizedType.length; ++i) {
110
+ hash = ((hash << 5) - hash) + normalizedType.charCodeAt(i);
111
+ hash |= 0;
112
+ }
113
+ const index = Math.abs(hash) % FALLBACK_TYPE_COLORS.length;
114
+ return FALLBACK_TYPE_COLORS[index % FALLBACK_TYPE_COLORS.length];
115
+ },
116
+ typeBadgeStyle(type) {
117
+ return {
118
+ backgroundColor: this.typeColor(type),
119
+ color: '#fff'
120
+ };
121
+ },
122
+ typeSegmentStyle(type, count, total) {
123
+ return {
124
+ width: `${this.typePercentValue(count, total)}%`,
125
+ backgroundColor: this.typeColor(type),
126
+ color: '#fff'
127
+ };
128
+ },
129
+ documentRoute(documentId) {
130
+ return `/model/${this.currentModel}/document/${documentId}`;
131
+ },
132
+ highlightPath(path) {
133
+ const search = this.fieldSearch.trim();
134
+ if (!search) {
135
+ return xss(path);
136
+ }
137
+
138
+ const index = path.toLowerCase().indexOf(search.toLowerCase());
139
+ if (index === -1) {
140
+ return xss(path);
141
+ }
142
+
143
+ const before = path.slice(0, index);
144
+ const match = path.slice(index, index + search.length);
145
+ const after = path.slice(index + search.length);
146
+ return `${xss(before)}<strong>${xss(match)}</strong>${xss(after)}`;
147
+ }
148
+ }
149
+ });
@@ -177,37 +177,34 @@
177
177
  Indexes
178
178
  </button>
179
179
  <button
180
- @click="openCollectionInfo"
180
+ @click="openAnalyzeSchemaModal"
181
181
  type="button"
182
182
  class="block w-full px-4 py-2 text-left text-sm text-content-secondary hover:bg-muted"
183
183
  >
184
- Collection Info
184
+ Analyze Schema
185
185
  </button>
186
186
  <button
187
- @click="findOldestDocument"
187
+ @click="openCollectionInfo"
188
188
  type="button"
189
189
  class="block w-full px-4 py-2 text-left text-sm text-content-secondary hover:bg-muted"
190
190
  >
191
- Find oldest document
192
- </button>
193
- <button
194
- @click="openDropCollectionModal"
195
- type="button"
196
- class="block w-full px-4 py-2 text-left text-sm text-red-700 hover:bg-red-50"
197
- >
198
- Drop Collection
191
+ Collection Info
199
192
  </button>
200
193
  <button
201
194
  @click="toggleRowNumbers()"
195
+ :disabled="outputType !== 'table'"
202
196
  type="button"
203
- class="block w-full px-4 py-2 text-left text-sm text-content-secondary hover:bg-muted"
197
+ class="block w-full px-4 py-2 text-left text-sm"
198
+ :class="outputType === 'table' ? 'text-content-secondary hover:bg-muted' : 'text-content-tertiary cursor-not-allowed opacity-50'"
204
199
  >
205
200
  {{ showRowNumbers ? 'Hide row numbers' : 'Show row numbers' }}
206
201
  </button>
207
202
  <button
208
203
  @click="resetFilter()"
204
+ :disabled="searchText.trim().length === 0"
209
205
  type="button"
210
- class="block w-full px-4 py-2 text-left text-sm text-content-secondary hover:bg-muted"
206
+ class="block w-full px-4 py-2 text-left text-sm"
207
+ :class="searchText.trim().length > 0 ? 'text-content-secondary hover:bg-muted' : 'text-content-tertiary cursor-not-allowed opacity-50'"
211
208
  >
212
209
  Reset Filter
213
210
  </button>
@@ -235,6 +232,13 @@
235
232
  >
236
233
  Default Projection
237
234
  </async-button>
235
+ <button
236
+ @click="openDropCollectionModal"
237
+ type="button"
238
+ class="block w-full px-4 py-2 text-left text-sm text-red-700 hover:bg-red-50"
239
+ >
240
+ Drop Collection
241
+ </button>
238
242
  </div>
239
243
  </div>
240
244
  </div>
@@ -551,6 +555,14 @@
551
555
  </div>
552
556
  </template>
553
557
  </modal>
558
+ <modal v-if="shouldShowAnalyzeSchemaModal" containerClass="md:!w-[70vw]">
559
+ <template v-slot:body>
560
+ <analyze-schema-modal
561
+ :current-model="currentModel"
562
+ @close="shouldShowAnalyzeSchemaModal = false"
563
+ ></analyze-schema-modal>
564
+ </template>
565
+ </modal>
554
566
  <modal v-if="shouldShowCollectionInfoModal">
555
567
  <template v-slot:body>
556
568
  <div class="modal-exit" @click="shouldShowCollectionInfoModal = false">&times;</div>
@@ -167,6 +167,7 @@ module.exports = app => app.component('models', {
167
167
  addFieldFilterText: '',
168
168
  showAddFieldDropdown: false,
169
169
  shouldShowIndexModal: false,
170
+ shouldShowAnalyzeSchemaModal: false,
170
171
  shouldShowCollectionInfoModal: false,
171
172
  shouldShowDropCollectionModal: false,
172
173
  dropCollectionConfirmName: '',
@@ -889,6 +890,10 @@ module.exports = app => app.component('models', {
889
890
  this.mongoDBIndexes = mongoDBIndexes;
890
891
  this.schemaIndexes = schemaIndexes;
891
892
  },
893
+ openAnalyzeSchemaModal() {
894
+ this.closeActionsMenu();
895
+ this.shouldShowAnalyzeSchemaModal = true;
896
+ },
892
897
  toggleActionsMenu() {
893
898
  this.showActionsMenu = !this.showActionsMenu;
894
899
  },
@@ -949,19 +954,6 @@ module.exports = app => app.component('models', {
949
954
  this.$toast.success(`Collection ${this.currentModel} dropped.`);
950
955
  await this.getDocuments();
951
956
  },
952
- async findOldestDocument() {
953
- this.closeActionsMenu();
954
- const { docs } = await api.Model.getDocuments({
955
- model: this.currentModel,
956
- limit: 1,
957
- sortKey: '_id',
958
- sortDirection: 1
959
- });
960
- if (!Array.isArray(docs) || docs.length === 0) {
961
- throw new Error('No documents found');
962
- }
963
- this.openDocument(docs[0]);
964
- },
965
957
  isTTLIndex(index) {
966
958
  return index != null && index.expireAfterSeconds != null;
967
959
  },
@@ -32,11 +32,11 @@
32
32
  <path fill-rule="evenodd" d="M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z" clip-rule="evenodd" />
33
33
  </svg>
34
34
  </span>
35
- <a v-if="hasAccess(roles, 'chat') && hasLLMAPIKey"
35
+ <a v-if="hasAccess(roles, 'chat') && supportsAI"
36
36
  href="#/chat"
37
37
  class="inline-flex items-center px-1 border-b-2 text-sm font-medium"
38
38
  :class="chatView ? 'text-content border-primary' : 'border-transparent text-content-tertiary hover:text-content'">Chat</a>
39
- <span v-else-if="hasAccess(roles, 'chat') && !hasLLMAPIKey"
39
+ <span v-else-if="hasAccess(roles, 'chat') && !supportsAI"
40
40
  class="inline-flex items-center px-1 border-b-2 border-transparent text-sm font-medium text-gray-300 cursor-not-allowed"
41
41
  :title="chatDisabledReason">
42
42
  Chat
@@ -236,11 +236,11 @@
236
236
  <path fill-rule="evenodd" d="M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z" clip-rule="evenodd" />
237
237
  </svg>
238
238
  </span>
239
- <a v-if="hasAccess(roles, 'chat') && hasLLMAPIKey"
239
+ <a v-if="hasAccess(roles, 'chat') && supportsAI"
240
240
  href="#/chat"
241
241
  class="block px-3 py-2 rounded-md text-base font-medium"
242
242
  :class="chatView ? 'text-content bg-primary-subtle' : 'text-content-secondary hover:bg-muted'">Chat</a>
243
- <span v-else-if="hasAccess(roles, 'chat') && !hasLLMAPIKey"
243
+ <span v-else-if="hasAccess(roles, 'chat') && !supportsAI"
244
244
  class="block px-3 py-2 rounded-md text-base font-medium text-gray-300 cursor-not-allowed"
245
245
  :title="chatDisabledReason">
246
246
  Chat
@@ -1,6 +1,5 @@
1
1
  'use strict';
2
2
 
3
- const api = require('../api');
4
3
  const mothership = require('../mothership');
5
4
  const template = require('./navbar.html');
6
5
  const { routes, hasAccess } = require('../routes');
@@ -107,8 +106,8 @@ module.exports = app => app.component('navbar', {
107
106
  recentPagesList() {
108
107
  return this.localRecentPages;
109
108
  },
110
- hasLLMAPIKey() {
111
- return !!window.MONGOOSE_STUDIO_CONFIG.hasLLMAPIKey;
109
+ supportsAI() {
110
+ return this.state.capabilities == null || this.state.capabilities.supportsAI !== false;
112
111
  },
113
112
  chatDisabledReason() {
114
113
  return 'Chat requires an Anthropic, Gemini, or OpenAI API key.';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mongoosejs/studio",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
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": {
@@ -19,6 +19,7 @@
19
19
  "ejson": "^2.2.3",
20
20
  "extrovert": "^0.2.0",
21
21
  "marked": "15.0.12",
22
+ "mpath": "^0.9.0",
22
23
  "node-inspect-extracted": "3.x",
23
24
  "regexp.escape": "^2.0.1",
24
25
  "tailwindcss": "3.4.0",