@mongoosejs/studio 0.1.6 → 0.1.8

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.
@@ -0,0 +1,23 @@
1
+ <form @submit.prevent="emitSearch" class="relative flex-grow m-0">
2
+ <input
3
+ ref="searchInput"
4
+ class="w-full font-mono rounded-md p-1 border border-gray-300 outline-gray-300 text-lg focus:ring-1 focus:ring-ultramarine-200 focus:ring-offset-0 focus:outline-none"
5
+ type="text"
6
+ placeholder="Filter"
7
+ v-model="searchText"
8
+ @click="initFilter"
9
+ @input="updateAutocomplete"
10
+ @keydown="handleKeyDown"
11
+ />
12
+ <ul v-if="autocompleteSuggestions.length" class="absolute z-[9999] bg-white border border-gray-300 rounded mt-1 w-full max-h-40 overflow-y-auto shadow">
13
+ <li
14
+ v-for="(suggestion, index) in autocompleteSuggestions"
15
+ :key="suggestion"
16
+ class="px-2 py-1 cursor-pointer"
17
+ :class="{ 'bg-ultramarine-100': index === autocompleteIndex }"
18
+ @mousedown.prevent="applySuggestion(index)"
19
+ >
20
+ {{ suggestion }}
21
+ </li>
22
+ </ul>
23
+ </form>
@@ -0,0 +1,227 @@
1
+ 'use strict';
2
+
3
+ const template = require('./document-search.html');
4
+ const { Trie } = require('../trie');
5
+
6
+ const QUERY_SELECTORS = [
7
+ '$eq',
8
+ '$ne',
9
+ '$gt',
10
+ '$gte',
11
+ '$lt',
12
+ '$lte',
13
+ '$in',
14
+ '$nin',
15
+ '$exists',
16
+ '$regex',
17
+ '$options',
18
+ '$text',
19
+ '$search',
20
+ '$and',
21
+ '$or',
22
+ '$nor',
23
+ '$not',
24
+ '$elemMatch',
25
+ '$size',
26
+ '$all',
27
+ '$type',
28
+ '$expr',
29
+ '$jsonSchema',
30
+ '$mod'
31
+ ];
32
+
33
+ module.exports = app => app.component('document-search', {
34
+ template,
35
+ props: {
36
+ value: {
37
+ type: String,
38
+ default: ''
39
+ },
40
+ schemaPaths: {
41
+ type: Array,
42
+ default: () => []
43
+ }
44
+ },
45
+ data() {
46
+ return {
47
+ autocompleteSuggestions: [],
48
+ autocompleteIndex: 0,
49
+ autocompleteTrie: null,
50
+ searchText: this.value || ''
51
+ };
52
+ },
53
+ watch: {
54
+ value(val) {
55
+ this.searchText = val || '';
56
+ },
57
+ schemaPaths: {
58
+ handler() {
59
+ this.buildAutocompleteTrie();
60
+ },
61
+ deep: true
62
+ }
63
+ },
64
+ created() {
65
+ this.buildAutocompleteTrie();
66
+ },
67
+ methods: {
68
+ emitSearch() {
69
+ this.$emit('input', this.searchText);
70
+ this.$emit('search', this.searchText);
71
+ },
72
+ buildAutocompleteTrie() {
73
+ this.autocompleteTrie = new Trie();
74
+ this.autocompleteTrie.bulkInsert(QUERY_SELECTORS, 5, 'operator');
75
+ if (Array.isArray(this.schemaPaths) && this.schemaPaths.length > 0) {
76
+ const paths = this.schemaPaths
77
+ .map(path => path?.path)
78
+ .filter(path => typeof path === 'string' && path.length > 0);
79
+ for (const path of this.schemaPaths) {
80
+ if (path.schema) {
81
+ paths.push(...Object.keys(path.schema).map(subpath => `${path.path}.${subpath}`));
82
+ }
83
+ }
84
+ this.autocompleteTrie.bulkInsert(paths, 10, 'fieldName');
85
+ }
86
+ },
87
+ initFilter(ev) {
88
+ if (!this.searchText) {
89
+ this.searchText = '{}';
90
+ this.$nextTick(() => {
91
+ ev.target.setSelectionRange(1, 1);
92
+ });
93
+ }
94
+ },
95
+ updateAutocomplete() {
96
+ const input = this.$refs.searchInput;
97
+ const cursorPos = input ? input.selectionStart : 0;
98
+ const before = this.searchText.slice(0, cursorPos);
99
+ const match = before.match(/(?:\{|,)\s*([^:\s]*)$/);
100
+ if (match && match[1]) {
101
+ const token = match[1];
102
+ const leadingQuoteMatch = token.match(/^["']/);
103
+ const trailingQuoteMatch = token.length > 1 && /["']$/.test(token)
104
+ ? token[token.length - 1]
105
+ : '';
106
+ const term = token
107
+ .replace(/^["']/, '')
108
+ .replace(trailingQuoteMatch ? new RegExp(`[${trailingQuoteMatch}]$`) : '', '')
109
+ .trim();
110
+ if (!term) {
111
+ this.autocompleteSuggestions = [];
112
+ return;
113
+ }
114
+
115
+ const colonMatch = before.match(/:\s*([^,\}\]]*)$/);
116
+ const role = colonMatch ? 'operator' : 'fieldName';
117
+
118
+ if (this.autocompleteTrie) {
119
+ const primarySuggestions = this.autocompleteTrie.getSuggestions(term, 10, role);
120
+ const suggestionsSet = new Set(primarySuggestions);
121
+ if (Array.isArray(this.schemaPaths) && this.schemaPaths.length > 0) {
122
+ for (const schemaPath of this.schemaPaths) {
123
+ const path = schemaPath?.path;
124
+ if (
125
+ typeof path === 'string' &&
126
+ path.startsWith(`${term}.`) &&
127
+ !suggestionsSet.has(path)
128
+ ) {
129
+ suggestionsSet.add(path);
130
+ if (suggestionsSet.size >= 10) {
131
+ break;
132
+ }
133
+ }
134
+ }
135
+ }
136
+ let suggestions = Array.from(suggestionsSet);
137
+ if (leadingQuoteMatch) {
138
+ const leadingQuote = leadingQuoteMatch[0];
139
+ suggestions = suggestions.map(suggestion => `${leadingQuote}${suggestion}`);
140
+ }
141
+ if (trailingQuoteMatch) {
142
+ suggestions = suggestions.map(suggestion =>
143
+ suggestion.endsWith(trailingQuoteMatch) ? suggestion : `${suggestion}${trailingQuoteMatch}`
144
+ );
145
+ }
146
+ this.autocompleteSuggestions = suggestions;
147
+ this.autocompleteIndex = 0;
148
+ return;
149
+ }
150
+ }
151
+ this.autocompleteSuggestions = [];
152
+ },
153
+ handleKeyDown(ev) {
154
+ if (this.autocompleteSuggestions.length === 0) {
155
+ return;
156
+ }
157
+ if (ev.key === 'Tab' || ev.key === 'Enter') {
158
+ ev.preventDefault();
159
+ this.applySuggestion(this.autocompleteIndex);
160
+ } else if (ev.key === 'ArrowDown') {
161
+ ev.preventDefault();
162
+ this.autocompleteIndex = (this.autocompleteIndex + 1) % this.autocompleteSuggestions.length;
163
+ } else if (ev.key === 'ArrowUp') {
164
+ ev.preventDefault();
165
+ this.autocompleteIndex = (this.autocompleteIndex + this.autocompleteSuggestions.length - 1) % this.autocompleteSuggestions.length;
166
+ }
167
+ },
168
+ applySuggestion(index) {
169
+ const suggestion = this.autocompleteSuggestions[index];
170
+ if (!suggestion) {
171
+ return;
172
+ }
173
+ const input = this.$refs.searchInput;
174
+ const cursorPos = input.selectionStart;
175
+ const before = this.searchText.slice(0, cursorPos);
176
+ const after = this.searchText.slice(cursorPos);
177
+ const match = before.match(/(?:\{|,)\s*([^:\s]*)$/);
178
+ const colonNeeded = !/^\s*:/.test(after);
179
+ if (!match) {
180
+ return;
181
+ }
182
+ const token = match[1];
183
+ const start = cursorPos - token.length;
184
+ let replacement = suggestion;
185
+ const leadingQuote = token.startsWith('"') || token.startsWith('\'') ? token[0] : '';
186
+ const trailingQuote = token.length > 1 && (token.endsWith('"') || token.endsWith('\'')) ? token[token.length - 1] : '';
187
+ if (leadingQuote && !replacement.startsWith(leadingQuote)) {
188
+ replacement = `${leadingQuote}${replacement}`;
189
+ }
190
+ if (trailingQuote && !replacement.endsWith(trailingQuote)) {
191
+ replacement = `${replacement}${trailingQuote}`;
192
+ }
193
+ // Only insert : if we know the user isn't entering in a nested path
194
+ if (colonNeeded && (!leadingQuote || trailingQuote)) {
195
+ replacement = `${replacement}:`;
196
+ }
197
+ this.searchText = this.searchText.slice(0, start) + replacement + after;
198
+ this.$nextTick(() => {
199
+ const pos = start + replacement.length;
200
+ input.setSelectionRange(pos, pos);
201
+ });
202
+ this.autocompleteSuggestions = [];
203
+ },
204
+ addPathFilter(path) {
205
+ if (this.searchText) {
206
+ if (this.searchText.endsWith('}')) {
207
+ this.searchText = this.searchText.slice(0, -1) + `, ${path}: }`;
208
+ } else {
209
+ this.searchText += `, ${path}: }`;
210
+ }
211
+
212
+ } else {
213
+ // If this.searchText is empty or undefined, initialize it with a new object
214
+ this.searchText = `{ ${path}: }`;
215
+ }
216
+
217
+
218
+ this.$nextTick(() => {
219
+ const input = this.$refs.searchInput;
220
+ const cursorIndex = this.searchText.lastIndexOf(':') + 2; // Move cursor after ": "
221
+
222
+ input.focus();
223
+ input.setSelectionRange(cursorIndex, cursorIndex);
224
+ });
225
+ }
226
+ }
227
+ });
@@ -37,12 +37,13 @@
37
37
  <div class="relative h-[42px] z-10">
38
38
  <div class="documents-menu">
39
39
  <div class="flex flex-row items-center w-full gap-2">
40
- <form @submit.prevent="search" class="relative flex-grow m-0">
41
- <input ref="searchInput" class="w-full font-mono rounded-md p-1 border border-gray-300 outline-gray-300 text-lg focus:ring-1 focus:ring-ultramarine-200 focus:ring-offset-0 focus:outline-none" type="text" placeholder="Filter" v-model="searchText" @click="initFilter" @input="updateAutocomplete" @keydown="handleKeyDown" />
42
- <ul v-if="autocompleteSuggestions.length" class="absolute z-[9999] bg-white border border-gray-300 rounded mt-1 w-full max-h-40 overflow-y-auto shadow">
43
- <li v-for="(suggestion, index) in autocompleteSuggestions" :key="suggestion" class="px-2 py-1 cursor-pointer" :class="{ 'bg-ultramarine-100': index === autocompleteIndex }" @mousedown.prevent="applySuggestion(index)">{{ suggestion }}</li>
44
- </ul>
45
- </form>
40
+ <document-search
41
+ ref="documentSearch"
42
+ :value="searchText"
43
+ :schema-paths="schemaPaths"
44
+ @search="search"
45
+ >
46
+ </document-search>
46
47
  <div>
47
48
  <span v-if="numDocuments == null">Loading ...</span>
48
49
  <span v-else-if="typeof numDocuments === 'number'">{{numDocuments === 1 ? numDocuments+ ' document' : numDocuments + ' documents'}}</span>
@@ -127,7 +128,7 @@
127
128
  </div>
128
129
  <table v-else-if="outputType === 'table'">
129
130
  <thead>
130
- <th v-for="path in filteredPaths" @click="clickFilter(path.path)" class="cursor-pointer">
131
+ <th v-for="path in filteredPaths" @click="addPathFilter(path.path)" class="cursor-pointer">
131
132
  {{path.path}}
132
133
  <span class="path-type">
133
134
  ({{(path.instance || 'unknown')}})
@@ -5,36 +5,6 @@ const template = require('./models.html');
5
5
  const mpath = require('mpath');
6
6
 
7
7
  const appendCSS = require('../appendCSS');
8
- const { Trie } = require('./trie');
9
-
10
- const QUERY_SELECTORS = [
11
- '$eq',
12
- '$ne',
13
- '$gt',
14
- '$gte',
15
- '$lt',
16
- '$lte',
17
- '$in',
18
- '$nin',
19
- '$exists',
20
- '$regex',
21
- '$options',
22
- '$text',
23
- '$search',
24
- '$and',
25
- '$or',
26
- '$nor',
27
- '$not',
28
- '$elemMatch',
29
- '$size',
30
- '$all',
31
- '$type',
32
- '$expr',
33
- '$jsonSchema',
34
- '$mod'
35
- ];
36
-
37
-
38
8
  appendCSS(require('./models.css'));
39
9
 
40
10
  const limit = 20;
@@ -60,9 +30,6 @@ module.exports = app => app.component('models', {
60
30
  selectMultiple: false,
61
31
  selectedDocuments: [],
62
32
  searchText: '',
63
- autocompleteSuggestions: [],
64
- autocompleteIndex: 0,
65
- autocompleteTrie: null,
66
33
  shouldShowExportModal: false,
67
34
  shouldShowCreateModal: false,
68
35
  shouldShowFieldModal: false,
@@ -81,7 +48,6 @@ module.exports = app => app.component('models', {
81
48
  }),
82
49
  created() {
83
50
  this.currentModel = this.model;
84
- this.buildAutocompleteTrie();
85
51
  this.loadOutputPreference();
86
52
  },
87
53
  beforeDestroy() {
@@ -120,16 +86,6 @@ module.exports = app => app.component('models', {
120
86
  }
121
87
  },
122
88
  methods: {
123
- buildAutocompleteTrie() {
124
- this.autocompleteTrie = new Trie();
125
- this.autocompleteTrie.bulkInsert(QUERY_SELECTORS, 5);
126
- if (Array.isArray(this.schemaPaths) && this.schemaPaths.length > 0) {
127
- const paths = this.schemaPaths
128
- .map(path => path?.path)
129
- .filter(path => typeof path === 'string' && path.length > 0);
130
- this.autocompleteTrie.bulkInsert(paths, 10);
131
- }
132
- },
133
89
  loadOutputPreference() {
134
90
  if (typeof window === 'undefined' || !window.localStorage) {
135
91
  return;
@@ -205,141 +161,6 @@ module.exports = app => app.component('models', {
205
161
  const { mongoDBIndexes } = await api.Model.dropIndex({ model: this.currentModel, name });
206
162
  this.mongoDBIndexes = mongoDBIndexes;
207
163
  },
208
- initFilter(ev) {
209
- if (!this.searchText) {
210
- this.searchText = '{}';
211
- this.$nextTick(() => {
212
- ev.target.setSelectionRange(1, 1);
213
- });
214
- }
215
- },
216
- updateAutocomplete() {
217
- const input = this.$refs.searchInput;
218
- const cursorPos = input ? input.selectionStart : 0;
219
- const before = this.searchText.slice(0, cursorPos);
220
- const match = before.match(/(?:\{|,)\s*([^:\s]*)$/);
221
- if (match && match[1]) {
222
- const token = match[1];
223
- const leadingQuoteMatch = token.match(/^["']/);
224
- const trailingQuoteMatch = token.length > 1 && /["']$/.test(token)
225
- ? token[token.length - 1]
226
- : '';
227
- const term = token
228
- .replace(/^["']/, '')
229
- .replace(trailingQuoteMatch ? new RegExp(`[${trailingQuoteMatch}]$`) : '', '')
230
- .trim();
231
- if (!term) {
232
- this.autocompleteSuggestions = [];
233
- return;
234
- }
235
- if (this.autocompleteTrie) {
236
- const primarySuggestions = this.autocompleteTrie.getSuggestions(term, 10);
237
- const suggestionsSet = new Set(primarySuggestions);
238
- if (Array.isArray(this.schemaPaths) && this.schemaPaths.length > 0) {
239
- for (const schemaPath of this.schemaPaths) {
240
- const path = schemaPath?.path;
241
- if (
242
- typeof path === 'string' &&
243
- path.startsWith(`${term}.`) &&
244
- !suggestionsSet.has(path)
245
- ) {
246
- suggestionsSet.add(path);
247
- if (suggestionsSet.size >= 10) {
248
- break;
249
- }
250
- }
251
- }
252
- }
253
- let suggestions = Array.from(suggestionsSet);
254
- if (leadingQuoteMatch) {
255
- const leadingQuote = leadingQuoteMatch[0];
256
- suggestions = suggestions.map(suggestion => `${leadingQuote}${suggestion}`);
257
- }
258
- if (trailingQuoteMatch) {
259
- suggestions = suggestions.map(suggestion =>
260
- suggestion.endsWith(trailingQuoteMatch) ? suggestion : `${suggestion}${trailingQuoteMatch}`
261
- );
262
- }
263
- this.autocompleteSuggestions = suggestions;
264
- this.autocompleteIndex = 0;
265
- return;
266
- }
267
- }
268
- this.autocompleteSuggestions = [];
269
- },
270
- handleKeyDown(ev) {
271
- if (this.autocompleteSuggestions.length === 0) {
272
- return;
273
- }
274
- if (ev.key === 'Tab' || ev.key === 'Enter') {
275
- ev.preventDefault();
276
- this.applySuggestion(this.autocompleteIndex);
277
- } else if (ev.key === 'ArrowDown') {
278
- ev.preventDefault();
279
- this.autocompleteIndex = (this.autocompleteIndex + 1) % this.autocompleteSuggestions.length;
280
- } else if (ev.key === 'ArrowUp') {
281
- ev.preventDefault();
282
- this.autocompleteIndex = (this.autocompleteIndex + this.autocompleteSuggestions.length - 1) % this.autocompleteSuggestions.length;
283
- }
284
- },
285
- applySuggestion(index) {
286
- const suggestion = this.autocompleteSuggestions[index];
287
- if (!suggestion) {
288
- return;
289
- }
290
- const input = this.$refs.searchInput;
291
- const cursorPos = input.selectionStart;
292
- const before = this.searchText.slice(0, cursorPos);
293
- const after = this.searchText.slice(cursorPos);
294
- const match = before.match(/(?:\{|,)\s*([^:\s]*)$/);
295
- const colonNeeded = !/^\s*:/.test(after);
296
- if (!match) {
297
- return;
298
- }
299
- const token = match[1];
300
- const start = cursorPos - token.length;
301
- let replacement = suggestion;
302
- const leadingQuote = token.startsWith('"') || token.startsWith('\'') ? token[0] : '';
303
- const trailingQuote = token.length > 1 && (token.endsWith('"') || token.endsWith('\'')) ? token[token.length - 1] : '';
304
- if (leadingQuote && !replacement.startsWith(leadingQuote)) {
305
- replacement = `${leadingQuote}${replacement}`;
306
- }
307
- if (trailingQuote && !replacement.endsWith(trailingQuote)) {
308
- replacement = `${replacement}${trailingQuote}`;
309
- }
310
- // Only insert : if we know the user isn't entering in a nested path
311
- if (colonNeeded && (!leadingQuote || trailingQuote)) {
312
- replacement = `${replacement}:`;
313
- }
314
- this.searchText = this.searchText.slice(0, start) + replacement + after;
315
- this.$nextTick(() => {
316
- const pos = start + replacement.length;
317
- input.setSelectionRange(pos, pos);
318
- });
319
- this.autocompleteSuggestions = [];
320
- },
321
- clickFilter(path) {
322
- if (this.searchText) {
323
- if (this.searchText.endsWith('}')) {
324
- this.searchText = this.searchText.slice(0, -1) + `, ${path}: }`;
325
- } else {
326
- this.searchText += `, ${path}: }`;
327
- }
328
-
329
- } else {
330
- // If this.searchText is empty or undefined, initialize it with a new object
331
- this.searchText = `{ ${path}: }`;
332
- }
333
-
334
-
335
- this.$nextTick(() => {
336
- const input = this.$refs.searchInput;
337
- const cursorIndex = this.searchText.lastIndexOf(':') + 2; // Move cursor after ": "
338
-
339
- input.focus();
340
- input.setSelectionRange(cursorIndex, cursorIndex);
341
- });
342
- },
343
164
  async closeCreationModal() {
344
165
  this.shouldShowCreateModal = false;
345
166
  await this.getDocuments();
@@ -389,7 +210,8 @@ module.exports = app => app.component('models', {
389
210
  }
390
211
  await this.loadMoreDocuments();
391
212
  },
392
- async search() {
213
+ async search(searchText) {
214
+ this.searchText = searchText;
393
215
  const hasSearch = typeof this.searchText === 'string' && this.searchText.trim().length > 0;
394
216
  if (hasSearch) {
395
217
  this.query.search = this.searchText;
@@ -404,6 +226,11 @@ module.exports = app => app.component('models', {
404
226
  await this.loadMoreDocuments();
405
227
  this.status = 'loaded';
406
228
  },
229
+ addPathFilter(path) {
230
+ if (this.$refs.documentSearch?.addPathFilter) {
231
+ this.$refs.documentSearch.addPathFilter(path);
232
+ }
233
+ },
407
234
  async openIndexModal() {
408
235
  this.shouldShowIndexModal = true;
409
236
  const { mongoDBIndexes, schemaIndexes } = await api.Model.getIndexes({ model: this.currentModel });
@@ -423,7 +250,6 @@ module.exports = app => app.component('models', {
423
250
  // Clear previous data
424
251
  this.documents = [];
425
252
  this.schemaPaths = [];
426
- this.buildAutocompleteTrie();
427
253
  this.numDocuments = null;
428
254
  this.loadedAllDocs = false;
429
255
  this.lastSelectedIndex = null;
@@ -451,7 +277,6 @@ module.exports = app => app.component('models', {
451
277
  }
452
278
  this.filteredPaths = [...this.schemaPaths];
453
279
  this.selectedPaths = [...this.schemaPaths];
454
- this.buildAutocompleteTrie();
455
280
  schemaPathsReceived = true;
456
281
  }
457
282
  if (event.numDocs !== undefined) {
@@ -5,6 +5,7 @@ class TrieNode {
5
5
  this.children = Object.create(null);
6
6
  this.isEnd = false;
7
7
  this.freq = 0;
8
+ this.roles = new Set(); // semantic roles like 'fieldName', 'operator'
8
9
  }
9
10
  }
10
11
 
@@ -13,7 +14,7 @@ class Trie {
13
14
  this.root = new TrieNode();
14
15
  }
15
16
 
16
- insert(word, freq = 1) {
17
+ insert(word, freq = 1, role = null) {
17
18
  if (!word) {
18
19
  return;
19
20
  }
@@ -26,27 +27,25 @@ class Trie {
26
27
  }
27
28
  node.isEnd = true;
28
29
  node.freq += freq;
30
+ if (role) {
31
+ node.roles.add(role);
32
+ }
29
33
  }
30
34
 
31
- bulkInsert(words, freq = 1) {
32
- if (!Array.isArray(words)) {
33
- return;
34
- }
35
- for (const word of words) {
36
- this.insert(word, freq);
37
- }
35
+ bulkInsert(words, freq = 1, role = null) {
36
+ for (const word of words) this.insert(word, freq, role);
38
37
  }
39
38
 
40
- collect(node, prefix, out) {
41
- if (node.isEnd) {
39
+ collect(node, prefix, out, role) {
40
+ if (node.isEnd && (role == null || node.roles.has(role))) {
42
41
  out.push([prefix, node.freq]);
43
42
  }
44
43
  for (const [ch, child] of Object.entries(node.children)) {
45
- this.collect(child, prefix + ch, out);
44
+ this.collect(child, prefix + ch, out, role);
46
45
  }
47
46
  }
48
47
 
49
- suggest(prefix, limit = 10) {
48
+ suggest(prefix, limit = 10, role = null) {
50
49
  let node = this.root;
51
50
  for (const ch of prefix) {
52
51
  if (!node.children[ch]) {
@@ -55,19 +54,19 @@ class Trie {
55
54
  node = node.children[ch];
56
55
  }
57
56
  const results = [];
58
- this.collect(node, prefix, results);
57
+ this.collect(node, prefix, results, role);
59
58
  results.sort((a, b) => b[1] - a[1]);
60
59
  return results.slice(0, limit).map(([word]) => word);
61
60
  }
62
61
 
63
- fuzzySuggest(prefix, limit = 10) {
62
+ fuzzySuggest(prefix, limit = 10, role = null) {
64
63
  const results = new Set();
65
64
 
66
65
  const dfs = (node, path, edits) => {
67
66
  if (edits > 1) {
68
67
  return;
69
68
  }
70
- if (node.isEnd && Math.abs(path.length - prefix.length) <= 1) {
69
+ if (node.isEnd && Math.abs(path.length - prefix.length) <= 1 && (role == null || node.roles.has(role))) {
71
70
  const dist = levenshtein(prefix, path);
72
71
  if (dist <= 1) {
73
72
  results.add(path);
@@ -83,17 +82,44 @@ class Trie {
83
82
  return Array.from(results).slice(0, limit);
84
83
  }
85
84
 
86
- getSuggestions(prefix, limit = 10) {
85
+ getSuggestions(prefix, limit = 10, role = null) {
87
86
  if (!prefix) {
88
87
  return [];
89
88
  }
90
- const exact = this.suggest(prefix, limit);
89
+ const exact = this.suggest(prefix, limit, role);
91
90
  if (exact.length >= limit) {
92
91
  return exact;
93
92
  }
94
- const fuzzy = this.fuzzySuggest(prefix, limit - exact.length);
93
+ const fuzzy = this.fuzzySuggest(prefix, limit - exact.length, role);
95
94
  return [...exact, ...fuzzy];
96
95
  }
96
+
97
+ toString() {
98
+ const lines = [];
99
+ function dfs(node, prefix, depth) {
100
+ let line = ' '.repeat(depth);
101
+ if (prefix.length > 0) {
102
+ line += prefix[prefix.length - 1];
103
+ } else {
104
+ line += '(root)';
105
+ }
106
+ if (node.isEnd) {
107
+ line += ' *';
108
+ }
109
+ if (node.roles.size > 0) {
110
+ line += ' [' + Array.from(node.roles).join(',') + ']';
111
+ }
112
+ if (node.freq > 0) {
113
+ line += ` {freq:${node.freq}}`;
114
+ }
115
+ lines.push(line);
116
+ for (const ch of Object.keys(node.children).sort()) {
117
+ dfs(node.children[ch], prefix + ch, depth + 1);
118
+ }
119
+ }
120
+ dfs(this.root, '', 0);
121
+ return lines.join('\n');
122
+ }
97
123
  }
98
124
 
99
125
  function levenshtein(a, b) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mongoosejs/studio",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "A sleek, powerful MongoDB UI with built-in dashboarding and auth, seamlessly integrated with your Express, Vercel, or Netlify app.",
5
5
  "homepage": "https://studio.mongoosejs.io/",
6
6
  "repository": {
@@ -9,6 +9,9 @@
9
9
  },
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
+ "@ai-sdk/openai": "2.x",
13
+ "@ai-sdk/anthropic": "2.x",
14
+ "ai": "5.x",
12
15
  "archetype": "0.13.1",
13
16
  "csv-stringify": "6.3.0",
14
17
  "ejson": "^2.2.3",