@mongoosejs/studio 0.0.124 → 0.0.126

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.
package/README.md CHANGED
@@ -34,6 +34,8 @@ If you have a Mongoose Studio Pro API key, you can set it as follows:
34
34
  const opts = process.env.MONGOOSE_STUDIO_API_KEY ? { apiKey: process.env.MONGOOSE_STUDIO_API_KEY } : {};
35
35
  // Optionally specify which ChatGPT model to use for chat messages
36
36
  opts.model = 'gpt-4o-mini';
37
+ // Provide your own OpenAI API key to run chat completions locally
38
+ opts.openAIAPIKey = process.env.OPENAI_API_KEY;
37
39
 
38
40
  // Mount Mongoose Studio on '/studio'
39
41
  app.use('/studio', await studio('/studio/api', mongoose, opts));
@@ -52,7 +54,9 @@ const { execSync } = require('child_process');
52
54
  const opts = {
53
55
  apiKey: process.env.MONGOOSE_STUDIO_API_KEY,
54
56
  // Optionally specify which ChatGPT model to use for chat messages
55
- model: 'gpt-4o-mini'
57
+ model: 'gpt-4o-mini',
58
+ // Provide your own OpenAI API key to run chat completions locally
59
+ openAIAPIKey: process.env.OPENAI_API_KEY
56
60
  };
57
61
  console.log('Creating Mongoose studio', opts);
58
62
  require('@mongoosejs/studio/frontend')(`/.netlify/functions/studio`, true, opts).then(() => {
@@ -70,7 +74,8 @@ const mongoose = require('mongoose');
70
74
 
71
75
  const handler = require('@mongoosejs/studio/backend/netlify')({
72
76
  apiKey: process.env.MONGOOSE_STUDIO_API_KEY,
73
- model: 'gpt-4o-mini'
77
+ model: 'gpt-4o-mini',
78
+ openAIAPIKey: process.env.OPENAI_API_KEY
74
79
  }).handler;
75
80
 
76
81
  let conn = null;
@@ -48,7 +48,7 @@ module.exports = ({ db, studioConnection, options }) => async function createCha
48
48
 
49
49
  let summarizePromise = Promise.resolve();
50
50
  if (chatThread.title == null) {
51
- summarizePromise = summarizeChatThread(llmMessages, authorization).then(res => {
51
+ summarizePromise = summarizeChatThread(llmMessages, authorization, options).then(res => {
52
52
  const title = res.response;
53
53
  chatThread.title = title;
54
54
  return chatThread.save();
@@ -73,7 +73,7 @@ module.exports = ({ db, studioConnection, options }) => async function createCha
73
73
  script,
74
74
  executionResult: null
75
75
  }),
76
- createChatMessageCore(llmMessages, modelDescriptions, options?.model, authorization).then(res => {
76
+ createChatMessageCore(llmMessages, modelDescriptions, options?.model, authorization, options).then(res => {
77
77
  const content = res.response;
78
78
  return ChatMessage.create({
79
79
  chatThreadId,
@@ -87,7 +87,23 @@ module.exports = ({ db, studioConnection, options }) => async function createCha
87
87
  return { chatMessages, chatThread };
88
88
  };
89
89
 
90
- async function summarizeChatThread(messages, authorization) {
90
+ async function summarizeChatThread(messages, authorization, options) {
91
+ if (options?.openAIAPIKey) {
92
+ const response = await callOpenAI({
93
+ apiKey: options.openAIAPIKey,
94
+ model: options.model,
95
+ messages: [
96
+ {
97
+ role: 'system',
98
+ content: 'Summarize the following conversation into a concise title of at most 7 words. Respond with the title only.'
99
+ },
100
+ ...messages
101
+ ]
102
+ });
103
+
104
+ return { response };
105
+ }
106
+
91
107
  const headers = { 'Content-Type': 'application/json' };
92
108
  if (authorization) {
93
109
  headers.Authorization = authorization;
@@ -110,7 +126,26 @@ async function summarizeChatThread(messages, authorization) {
110
126
  return await response.json();
111
127
  }
112
128
 
113
- async function createChatMessageCore(messages, modelDescriptions, model, authorization) {
129
+ async function createChatMessageCore(messages, modelDescriptions, model, authorization, options) {
130
+ if (options?.openAIAPIKey) {
131
+ const openAIMessages = [];
132
+ if (modelDescriptions) {
133
+ openAIMessages.push({
134
+ role: 'system',
135
+ content: `Here are the Mongoose model descriptions you can refer to:\n\n${modelDescriptions}`
136
+ });
137
+ }
138
+ openAIMessages.push(...messages);
139
+
140
+ const response = await callOpenAI({
141
+ apiKey: options.openAIAPIKey,
142
+ model,
143
+ messages: openAIMessages
144
+ });
145
+
146
+ return { response };
147
+ }
148
+
114
149
  const headers = { 'Content-Type': 'application/json' };
115
150
  if (authorization) {
116
151
  headers.Authorization = authorization;
@@ -134,3 +169,34 @@ async function createChatMessageCore(messages, modelDescriptions, model, authori
134
169
 
135
170
  return await response.json();
136
171
  }
172
+
173
+ async function callOpenAI({ apiKey, model, messages }) {
174
+ if (!apiKey) {
175
+ throw new Error('OpenAI API key required');
176
+ }
177
+
178
+ const response = await fetch('https://api.openai.com/v1/chat/completions', {
179
+ method: 'POST',
180
+ headers: {
181
+ 'Content-Type': 'application/json',
182
+ Authorization: `Bearer ${apiKey}`
183
+ },
184
+ body: JSON.stringify({
185
+ model: model || 'gpt-4o-mini',
186
+ messages
187
+ })
188
+ });
189
+
190
+ const data = await response.json();
191
+
192
+ if (response.status < 200 || response.status >= 400) {
193
+ throw new Error(`OpenAI chat completion error ${response.status}: ${data.error?.message || data.message || 'Unknown error'}`);
194
+ }
195
+
196
+ const content = data?.choices?.[0]?.message?.content;
197
+ if (!content) {
198
+ throw new Error('OpenAI chat completion error: missing response content');
199
+ }
200
+
201
+ return content.trim();
202
+ }
@@ -94,7 +94,7 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
94
94
  exportQueryResults(params) {
95
95
  const accessToken = window.localStorage.getItem('_mongooseStudioAccessToken') || null;
96
96
 
97
- return fetch(window.MONGOOSE_STUDIO_CONFIG.baseURL + new URLSearchParams({ ...params, action: 'Model.exportQueryResults' }).toString(), {
97
+ return fetch(window.MONGOOSE_STUDIO_CONFIG.baseURL + '?' + new URLSearchParams({ ...params, action: 'Model.exportQueryResults' }).toString(), {
98
98
  method: 'GET',
99
99
  headers: {
100
100
  Authorization: `${accessToken}`, // Set your authorization token here
@@ -2607,6 +2607,34 @@ const template = __webpack_require__(/*! ./models.html */ "./frontend/src/models
2607
2607
  const mpath = __webpack_require__(/*! mpath */ "./node_modules/mpath/index.js");
2608
2608
 
2609
2609
  const appendCSS = __webpack_require__(/*! ../appendCSS */ "./frontend/src/appendCSS.js");
2610
+ const { Trie } = __webpack_require__(/*! ./trie */ "./frontend/src/models/trie.js");
2611
+
2612
+ const QUERY_SELECTORS = [
2613
+ '$eq',
2614
+ '$ne',
2615
+ '$gt',
2616
+ '$gte',
2617
+ '$lt',
2618
+ '$lte',
2619
+ '$in',
2620
+ '$nin',
2621
+ '$exists',
2622
+ '$regex',
2623
+ '$options',
2624
+ '$text',
2625
+ '$search',
2626
+ '$and',
2627
+ '$or',
2628
+ '$nor',
2629
+ '$not',
2630
+ '$elemMatch',
2631
+ '$size',
2632
+ '$all',
2633
+ '$type',
2634
+ '$expr',
2635
+ '$jsonSchema',
2636
+ '$mod'
2637
+ ];
2610
2638
 
2611
2639
 
2612
2640
  appendCSS(__webpack_require__(/*! ./models.css */ "./frontend/src/models/models.css"));
@@ -2633,6 +2661,9 @@ module.exports = app => app.component('models', {
2633
2661
  selectMultiple: false,
2634
2662
  selectedDocuments: [],
2635
2663
  searchText: '',
2664
+ autocompleteSuggestions: [],
2665
+ autocompleteIndex: 0,
2666
+ autocompleteTrie: null,
2636
2667
  shouldShowExportModal: false,
2637
2668
  shouldShowCreateModal: false,
2638
2669
  shouldShowFieldModal: false,
@@ -2645,10 +2676,12 @@ module.exports = app => app.component('models', {
2645
2676
  scrollHeight: 0,
2646
2677
  interval: null,
2647
2678
  outputType: 'table', // json, table
2648
- hideSidebar: null
2679
+ hideSidebar: null,
2680
+ lastSelectedIndex: null
2649
2681
  }),
2650
2682
  created() {
2651
2683
  this.currentModel = this.model;
2684
+ this.buildAutocompleteTrie();
2652
2685
  },
2653
2686
  beforeDestroy() {
2654
2687
  document.removeEventListener('scroll', this.onScroll, true);
@@ -2667,6 +2700,16 @@ module.exports = app => app.component('models', {
2667
2700
  await this.initSearchFromUrl();
2668
2701
  },
2669
2702
  methods: {
2703
+ buildAutocompleteTrie() {
2704
+ this.autocompleteTrie = new Trie();
2705
+ this.autocompleteTrie.bulkInsert(QUERY_SELECTORS, 5);
2706
+ if (Array.isArray(this.schemaPaths) && this.schemaPaths.length > 0) {
2707
+ const paths = this.schemaPaths
2708
+ .map(path => path?.path)
2709
+ .filter(path => typeof path === 'string' && path.length > 0);
2710
+ this.autocompleteTrie.bulkInsert(paths, 10);
2711
+ }
2712
+ },
2670
2713
  async initSearchFromUrl() {
2671
2714
  this.status = 'loading';
2672
2715
  this.query = Object.assign({}, this.$route.query); // important that this is here before the if statements
@@ -2704,6 +2747,62 @@ module.exports = app => app.component('models', {
2704
2747
  });
2705
2748
  }
2706
2749
  },
2750
+ updateAutocomplete() {
2751
+ const input = this.$refs.searchInput;
2752
+ const cursorPos = input ? input.selectionStart : 0;
2753
+ const before = this.searchText.slice(0, cursorPos);
2754
+ const match = before.match(/(?:\{|,)\s*([^:\s]*)$/);
2755
+ if (match && match[1]) {
2756
+ const term = match[1].replace(/["']/g, '');
2757
+ if (!term) {
2758
+ this.autocompleteSuggestions = [];
2759
+ return;
2760
+ }
2761
+ if (this.autocompleteTrie) {
2762
+ this.autocompleteSuggestions = this.autocompleteTrie.getSuggestions(term, 10);
2763
+ this.autocompleteIndex = 0;
2764
+ return;
2765
+ }
2766
+ }
2767
+ this.autocompleteSuggestions = [];
2768
+ },
2769
+ handleKeyDown(ev) {
2770
+ if (this.autocompleteSuggestions.length === 0) {
2771
+ return;
2772
+ }
2773
+ if (ev.key === 'Tab' || ev.key === 'Enter') {
2774
+ ev.preventDefault();
2775
+ this.applySuggestion(this.autocompleteIndex);
2776
+ } else if (ev.key === 'ArrowDown') {
2777
+ ev.preventDefault();
2778
+ this.autocompleteIndex = (this.autocompleteIndex + 1) % this.autocompleteSuggestions.length;
2779
+ } else if (ev.key === 'ArrowUp') {
2780
+ ev.preventDefault();
2781
+ this.autocompleteIndex = (this.autocompleteIndex + this.autocompleteSuggestions.length - 1) % this.autocompleteSuggestions.length;
2782
+ }
2783
+ },
2784
+ applySuggestion(index) {
2785
+ const suggestion = this.autocompleteSuggestions[index];
2786
+ if (!suggestion) {
2787
+ return;
2788
+ }
2789
+ const input = this.$refs.searchInput;
2790
+ const cursorPos = input.selectionStart;
2791
+ const before = this.searchText.slice(0, cursorPos);
2792
+ const after = this.searchText.slice(cursorPos);
2793
+ const match = before.match(/(?:\{|,)\s*([^:\s]*)$/);
2794
+ if (!match) {
2795
+ return;
2796
+ }
2797
+ const token = match[1];
2798
+ const start = cursorPos - token.length;
2799
+ this.searchText = this.searchText.slice(0, start) + suggestion + after;
2800
+ this.$nextTick(() => {
2801
+ const pos = start + suggestion.length;
2802
+ input.setSelectionRange(pos, pos);
2803
+ });
2804
+ this.autocompleteSuggestions = [];
2805
+ },
2707
2806
  clickFilter(path) {
2708
2807
  if (this.searchText) {
2709
2808
  if (this.searchText.endsWith('}')) {
@@ -2816,8 +2915,10 @@ module.exports = app => app.component('models', {
2816
2915
  // Clear previous data
2817
2916
  this.documents = [];
2818
2917
  this.schemaPaths = [];
2918
+ this.buildAutocompleteTrie();
2819
2919
  this.numDocuments = null;
2820
2920
  this.loadedAllDocs = false;
2921
+ this.lastSelectedIndex = null;
2821
2922
 
2822
2923
  let docsCount = 0;
2823
2924
  let schemaPathsReceived = false;
@@ -2849,6 +2950,7 @@ module.exports = app => app.component('models', {
2849
2950
  }
2850
2951
  this.filteredPaths = [...this.schemaPaths];
2851
2952
  this.selectedPaths = [...this.schemaPaths];
2953
+ this.buildAutocompleteTrie();
2852
2954
  schemaPathsReceived = true;
2853
2955
  }
2854
2956
  if (event.numDocs !== undefined) {
@@ -2991,17 +3093,38 @@ module.exports = app => app.component('models', {
2991
3093
  }
2992
3094
  this.edittingDoc = null;
2993
3095
  },
2994
- handleDocumentClick(document) {
2995
- console.log(this.selectedDocuments);
3096
+ handleDocumentClick(document, event) {
2996
3097
  if (this.selectMultiple) {
2997
- const exists = this.selectedDocuments.find(x => x._id.toString() == document._id.toString());
2998
- if (exists) {
2999
- const index = this.selectedDocuments.findIndex(x => x._id.toString() == document._id.toString());
3000
- if (index !== -1) {
3001
- this.selectedDocuments.splice(index, 1);
3098
+ const documentIndex = this.documents.findIndex(doc => doc._id.toString() == document._id.toString());
3099
+ if (event?.shiftKey && this.selectedDocuments.length > 0) {
3100
+ const anchorIndex = this.lastSelectedIndex;
3101
+ if (anchorIndex != null && anchorIndex !== -1 && documentIndex !== -1) {
3102
+ const start = Math.min(anchorIndex, documentIndex);
3103
+ const end = Math.max(anchorIndex, documentIndex);
3104
+ const selectedDocumentIds = new Set(this.selectedDocuments.map(doc => doc._id.toString()));
3105
+ for (let i = start; i <= end; i++) {
3106
+ const docInRange = this.documents[i];
3107
+ const existsInRange = selectedDocumentIds.has(docInRange._id.toString());
3108
+ if (!existsInRange) {
3109
+ this.selectedDocuments.push(docInRange);
3110
+ }
3111
+ }
3112
+ this.lastSelectedIndex = documentIndex;
3113
+ return;
3114
+ }
3115
+ }
3116
+ const index = this.selectedDocuments.findIndex(x => x._id.toString() == document._id.toString());
3117
+ if (index !== -1) {
3118
+ this.selectedDocuments.splice(index, 1);
3119
+ if (this.selectedDocuments.length === 0) {
3120
+ this.lastSelectedIndex = null;
3121
+ } else {
3122
+ const lastDoc = this.selectedDocuments[this.selectedDocuments.length - 1];
3123
+ this.lastSelectedIndex = this.documents.findIndex(doc => doc._id.toString() == lastDoc._id.toString());
3002
3124
  }
3003
3125
  } else {
3004
3126
  this.selectedDocuments.push(document);
3127
+ this.lastSelectedIndex = documentIndex;
3005
3128
  }
3006
3129
  } else {
3007
3130
  this.$router.push('/model/' + this.currentModel + '/document/' + document._id);
@@ -3015,18 +3138,21 @@ module.exports = app => app.component('models', {
3015
3138
  });
3016
3139
  await this.getDocuments();
3017
3140
  this.selectedDocuments.length = 0;
3141
+ this.lastSelectedIndex = null;
3018
3142
  this.shouldShowDeleteMultipleModal = false;
3019
3143
  this.selectMultiple = false;
3020
3144
  },
3021
3145
  async updateDocuments() {
3022
3146
  await this.getDocuments();
3023
3147
  this.selectedDocuments.length = 0;
3148
+ this.lastSelectedIndex = null;
3024
3149
  this.selectMultiple = false;
3025
3150
  },
3026
3151
  stagingSelect() {
3027
3152
  if (this.selectMultiple) {
3028
3153
  this.selectMultiple = false;
3029
3154
  this.selectedDocuments.length = 0;
3155
+ this.lastSelectedIndex = null;
3030
3156
  } else {
3031
3157
  this.selectMultiple = true;
3032
3158
  }
@@ -3035,6 +3161,134 @@ module.exports = app => app.component('models', {
3035
3161
  });
3036
3162
 
3037
3163
 
3164
+ /***/ }),
3165
+
3166
+ /***/ "./frontend/src/models/trie.js":
3167
+ /*!*************************************!*\
3168
+ !*** ./frontend/src/models/trie.js ***!
3169
+ \*************************************/
3170
+ /***/ ((module) => {
3171
+
3172
+ "use strict";
3173
+
3174
+
3175
+ class TrieNode {
3176
+ constructor() {
3177
+ this.children = Object.create(null);
3178
+ this.isEnd = false;
3179
+ this.freq = 0;
3180
+ }
3181
+ }
3182
+
3183
+ class Trie {
3184
+ constructor() {
3185
+ this.root = new TrieNode();
3186
+ }
3187
+
3188
+ insert(word, freq = 1) {
3189
+ if (!word) {
3190
+ return;
3191
+ }
3192
+ let node = this.root;
3193
+ for (const ch of word) {
3194
+ if (!node.children[ch]) {
3195
+ node.children[ch] = new TrieNode();
3196
+ }
3197
+ node = node.children[ch];
3198
+ }
3199
+ node.isEnd = true;
3200
+ node.freq += freq;
3201
+ }
3202
+
3203
+ bulkInsert(words, freq = 1) {
3204
+ if (!Array.isArray(words)) {
3205
+ return;
3206
+ }
3207
+ for (const word of words) {
3208
+ this.insert(word, freq);
3209
+ }
3210
+ }
3211
+
3212
+ collect(node, prefix, out) {
3213
+ if (node.isEnd) {
3214
+ out.push([prefix, node.freq]);
3215
+ }
3216
+ for (const [ch, child] of Object.entries(node.children)) {
3217
+ this.collect(child, prefix + ch, out);
3218
+ }
3219
+ }
3220
+
3221
+ suggest(prefix, limit = 10) {
3222
+ let node = this.root;
3223
+ for (const ch of prefix) {
3224
+ if (!node.children[ch]) {
3225
+ return [];
3226
+ }
3227
+ node = node.children[ch];
3228
+ }
3229
+ const results = [];
3230
+ this.collect(node, prefix, results);
3231
+ results.sort((a, b) => b[1] - a[1]);
3232
+ return results.slice(0, limit).map(([word]) => word);
3233
+ }
3234
+
3235
+ fuzzySuggest(prefix, limit = 10) {
3236
+ const results = new Set();
3237
+
3238
+ const dfs = (node, path, edits) => {
3239
+ if (edits > 1) {
3240
+ return;
3241
+ }
3242
+ if (node.isEnd && Math.abs(path.length - prefix.length) <= 1) {
3243
+ const dist = levenshtein(prefix, path);
3244
+ if (dist <= 1) {
3245
+ results.add(path);
3246
+ }
3247
+ }
3248
+ for (const [ch, child] of Object.entries(node.children)) {
3249
+ const nextEdits = ch === prefix[path.length] ? edits : edits + 1;
3250
+ dfs(child, path + ch, nextEdits);
3251
+ }
3252
+ };
3253
+
3254
+ dfs(this.root, '', 0);
3255
+ return Array.from(results).slice(0, limit);
3256
+ }
3257
+
3258
+ getSuggestions(prefix, limit = 10) {
3259
+ if (!prefix) {
3260
+ return [];
3261
+ }
3262
+ const exact = this.suggest(prefix, limit);
3263
+ if (exact.length >= limit) {
3264
+ return exact;
3265
+ }
3266
+ const fuzzy = this.fuzzySuggest(prefix, limit - exact.length);
3267
+ return [...exact, ...fuzzy];
3268
+ }
3269
+ }
3270
+
3271
+ function levenshtein(a, b) {
3272
+ const dp = Array.from({ length: a.length + 1 }, (_, i) =>
3273
+ Array.from({ length: b.length + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0))
3274
+ );
3275
+ for (let i = 1; i <= a.length; i++) {
3276
+ for (let j = 1; j <= b.length; j++) {
3277
+ dp[i][j] =
3278
+ a[i - 1] === b[j - 1]
3279
+ ? dp[i - 1][j - 1]
3280
+ : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
3281
+ }
3282
+ }
3283
+ return dp[a.length][b.length];
3284
+ }
3285
+
3286
+ module.exports = {
3287
+ Trie,
3288
+ TrieNode
3289
+ };
3290
+
3291
+
3038
3292
  /***/ }),
3039
3293
 
3040
3294
  /***/ "./frontend/src/mothership.js":
@@ -3669,6 +3923,8 @@ var map = {
3669
3923
  "./models/models.css": "./frontend/src/models/models.css",
3670
3924
  "./models/models.html": "./frontend/src/models/models.html",
3671
3925
  "./models/models.js": "./frontend/src/models/models.js",
3926
+ "./models/trie": "./frontend/src/models/trie.js",
3927
+ "./models/trie.js": "./frontend/src/models/trie.js",
3672
3928
  "./mothership": "./frontend/src/mothership.js",
3673
3929
  "./mothership.js": "./frontend/src/mothership.js",
3674
3930
  "./navbar/navbar": "./frontend/src/navbar/navbar.js",
@@ -4893,7 +5149,7 @@ module.exports = ".models {\n position: relative;\n display: flex;\n flex-dir
4893
5149
  /***/ ((module) => {
4894
5150
 
4895
5151
  "use strict";
4896
- module.exports = "<div class=\"models flex\" style=\"height: calc(100vh - 55px); height: calc(100dvh - 55px)\">\n <div class=\"fixed top-[65px] cursor-pointer bg-gray-100 rounded-r-md z-10\" @click=\"hideSidebar = false\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" style=\"h-5 w-5\" viewBox=\"0 -960 960 960\" class=\"w-5\" fill=\"#5f6368\"><path d=\"M360-120v-720h80v720h-80Zm160-160v-400l200 200-200 200Z\"/></svg>\n </div>\n <aside class=\"bg-white border-r overflow-y-auto overflow-x-hidden h-full transition-all duration-300 ease-in-out z-20 w-0 lg:w-48 fixed lg:relative shrink-0\" :class=\"hideSidebar === true ? '!w-0' : hideSidebar === false ? '!w-48' : ''\">\n <div class=\"flex items-center border-b border-gray-100 w-48 overflow-x-hidden\">\n <div class=\"p-4 font-bold text-lg\">Models</div>\n <button\n @click=\"hideSidebar = true\"\n class=\"ml-auto mr-2 p-2 rounded hover:bg-gray-200 focus:outline-none\"\n aria-label=\"Close sidebar\"\n >\n <svg xmlns=\"http://www.w3.org/2000/svg\" style=\"h-5 w-5\" viewBox=\"0 -960 960 960\" class=\"w-5\" fill=\"currentColor\"><path d=\"M660-320v-320L500-480l160 160ZM200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm120-80v-560H200v560h120Zm80 0h360v-560H400v560Zm-80 0H200h120Z\"/></svg>\n </button>\n </div>\n <nav class=\"flex flex-1 flex-col\">\n <ul role=\"list\" class=\"flex flex-1 flex-col gap-y-7\">\n <li>\n <ul role=\"list\">\n <li v-for=\"model in models\">\n <router-link\n :to=\"'/model/' + model\"\n class=\"block truncate rounded-md py-2 pr-2 pl-2 text-sm font-semibold text-gray-700\"\n :class=\"model === currentModel ? 'bg-ultramarine-100 font-bold' : 'hover:bg-ultramarine-100'\">\n {{model}}\n </router-link>\n </li>\n </ul>\n </li>\n </ul>\n </nav>\n </aside>\n <div class=\"documents\" ref=\"documentsList\">\n <div class=\"relative h-[42px]\">\n <div class=\"documents-menu\">\n <div class=\"flex flex-row items-center w-full gap-2\">\n <form @submit.prevent=\"search\" class=\"flex-grow m-0\">\n <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\" />\n </form>\n <div>\n <span v-if=\"numDocuments == null\">Loading ...</span>\n <span v-else-if=\"typeof numDocuments === 'number'\">{{numDocuments === 1 ? numDocuments+ ' document' : numDocuments + ' documents'}}</span>\n </div>\n <button\n @click=\"shouldShowExportModal = true\"\n type=\"button\"\n v-show=\"!selectMultiple\"\n class=\"rounded bg-ultramarine-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600\">\n Export\n </button>\n <button\n @click=\"stagingSelect\"\n type=\"button\"\n :class=\"{ 'bg-ultramarine-500 ring-inset ring-2 ring-gray-300 hover:bg-ultramarine-600': selectMultiple }\"\n class=\"rounded bg-ultramarine-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600\"\n >\n Select\n </button>\n <button\n v-show=\"selectMultiple\"\n @click=\"shouldShowUpdateMultipleModal=true;\"\n type=\"button\"\n class=\"rounded bg-green-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600\"\n >\n Update\n </button>\n <button\n @click=\"shouldShowDeleteMultipleModal=true;\"\n type=\"button\"\n v-show=\"selectMultiple\"\n class=\"rounded bg-red-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500\"\n >\n Delete\n </button>\n <button\n @click=\"openIndexModal\"\n type=\"button\"\n v-show=\"!selectMultiple\"\n class=\"rounded bg-ultramarine-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600\">\n Indexes\n </button>\n <button\n @click=\"shouldShowCreateModal = true;\"\n type=\"button\"\n v-show=\"!selectMultiple\"\n class=\"rounded bg-ultramarine-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600\">\n Create\n </button>\n <button\n @click=\"openFieldSelection\"\n type=\"button\"\n v-show=\"!selectMultiple\"\n class=\"rounded bg-ultramarine-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600\">\n Fields\n </button>\n <span class=\"isolate inline-flex rounded-md shadow-sm\">\n <button\n @click=\"outputType = 'table'\"\n type=\"button\"\n class=\"relative inline-flex items-center rounded-none rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10\"\n :class=\"outputType === 'table' ? 'bg-gray-200' : 'bg-white'\">\n <img class=\"h-5 w-5\" src=\"images/table.svg\">\n </button>\n <button\n @click=\"outputType = 'json'\"\n type=\"button\"\n class=\"relative -ml-px inline-flex items-center rounded-none rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10\"\n :class=\"outputType === 'json' ? 'bg-gray-200' : 'bg-white'\">\n <img class=\"h-5 w-5\" src=\"images/json.svg\">\n </button>\n </span>\n </div>\n </div>\n </div>\n <div class=\"documents-container relative\">\n <table v-if=\"outputType === 'table'\">\n <thead>\n <th v-for=\"path in filteredPaths\" @click=\"clickFilter(path.path)\" class=\"cursor-pointer\">\n {{path.path}}\n <span class=\"path-type\">\n ({{(path.instance || 'unknown')}})\n </span>\n <span class=\"sort-arrow\" @click=\"sortDocs(1, path.path)\">{{sortBy[path.path] == 1 ? 'X' : '↑'}}</span>\n <span class=\"sort-arrow\" @click=\"sortDocs(-1, path.path)\">{{sortBy[path.path] == -1 ? 'X' : '↓'}}</span>\n </th>\n </thead>\n <tbody>\n <tr v-for=\"document in documents\" @click=\"handleDocumentClick(document)\" :key=\"document._id\">\n <td v-for=\"schemaPath in filteredPaths\" :class=\"{ 'bg-blue-200': selectedDocuments.some(x => x._id.toString() === document._id.toString()) }\">\n <component\n :is=\"getComponentForPath(schemaPath)\"\n :value=\"getValueForPath(document, schemaPath.path)\"\n :allude=\"getReferenceModel(schemaPath)\">\n </component>\n </td>\n </tr>\n </tbody>\n </table>\n <div v-if=\"outputType === 'json'\">\n <div v-for=\"document in documents\" @click=\"handleDocumentClick(document)\" :key=\"document._id\" :class=\"{ 'bg-blue-200': selectedDocuments.some(x => x._id.toString() === document._id.toString()) }\">\n <list-json :value=\"filterDocument(document)\">\n </list-json>\n </div>\n </div>\n <div v-if=\"status === 'loading'\" class=\"loader\">\n <img src=\"images/loader.gif\">\n </div>\n </div>\n </div>\n <modal v-if=\"shouldShowExportModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowExportModal = false\">&times;</div>\n <export-query-results\n :schemaPaths=\"schemaPaths\"\n :search-text=\"searchText\"\n :currentModel=\"currentModel\"\n @done=\"shouldShowExportModal = false\">\n </export-query-results>\n </template>\n </modal>\n <modal v-if=\"shouldShowIndexModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowIndexModal = false\">&times;</div>\n <div class=\"text-xl font-bold mb-2\">Indexes</div>\n <div v-for=\"index in mongoDBIndexes\" class=\"w-full flex items-center\">\n <div class=\"grow shrink text-left flex justify-between items-center\" v-if=\"index.name != '_id_'\">\n <div>\n <div class=\"font-bold\">{{ index.name }}</div>\n <div class=\"text-sm font-mono\">{{ JSON.stringify(index.key) }}</div>\n </div>\n <div>\n <async-button\n type=\"button\"\n @click=\"dropIndex(index.name)\"\n class=\"rounded-md bg-valencia-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-valencia-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600 disabled:bg-gray-400 disabled:cursor-not-allowed\">\n Drop\n </async-button>\n </div>\n </div>\n </div>\n </template>\n </modal>\n <modal v-if=\"shouldShowFieldModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowFieldModal = false; selectedPaths = [...filteredPaths];\">&times;</div>\n <div v-for=\"(path, index) in schemaPaths\" :key=\"index\" class=\"w-5 flex items-center\">\n <input class=\"mt-0 h-4 w-4 rounded border-gray-300 text-sky-600 focus:ring-sky-600 accent-sky-600\" type=\"checkbox\" :id=\"'path.path'+index\" @change=\"addOrRemove(path)\" :value=\"path.path\" :checked=\"isSelected(path.path)\" />\n <div class=\"ml-2 text-gray-700 grow shrink text-left\">\n <label :for=\"'path.path' + index\">{{path.path}}</label>\n </div>\n </div>\n <div class=\"mt-4 flex gap-2\">\n <button type=\"button\" @click=\"filterDocuments()\" class=\"rounded-md bg-ultramarine-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-600\">Filter Selection</button>\n <button type=\"button\" @click=\"selectAll()\" class=\"rounded-md bg-forest-green-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600\">Select All</button>\n <button type=\"button\" @click=\"deselectAll()\" class=\"rounded-md bg-valencia-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-valencia-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600\">Deselect All</button>\n <button type=\"button\" @click=\"resetDocuments()\" class=\"rounded-md bg-gray-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-gray-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600\" >Cancel</button>\n </div>\n </template>\n </modal>\n <modal v-if=\"shouldShowCreateModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowCreateModal = false;\">&times;</div>\n <create-document :currentModel=\"currentModel\" :paths=\"schemaPaths\" @close=\"closeCreationModal\"></create-document>\n </template>\n </modal>\n <modal v-if=\"shouldShowUpdateMultipleModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowUpdateMultipleModal = false;\">&times;</div>\n <update-document :currentModel=\"currentModel\" :document=\"selectedDocuments\" :multiple=\"true\" @update=\"updateDocuments\" @close=\"shouldShowUpdateMultipleModal=false;\"></update-document>\n </template>\n </modal>\n <modal v-if=\"shouldShowDeleteMultipleModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowDeleteMultipleModal = false;\">&times;</div>\n <h2>Are you sure you want to delete {{selectedDocuments.length}} documents?</h2>\n <div>\n <list-json :value=\"selectedDocuments\"></list-json>\n </div>\n <div class=\"flex gap-4\">\n <async-button @click=\"deleteDocuments\" class=\"rounded bg-red-500 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600\">\n Confirm\n </async-button>\n <button @click=\"shouldShowDeleteMultipleModal = false;\" class=\"rounded bg-gray-400 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-gray-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500\">\n Cancel\n </button>\n </div>\n </template>\n </modal>\n</div>\n";
5152
+ module.exports = "<div class=\"models flex\" style=\"height: calc(100vh - 55px); height: calc(100dvh - 55px)\">\n <div class=\"fixed top-[65px] cursor-pointer bg-gray-100 rounded-r-md z-10\" @click=\"hideSidebar = false\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" style=\"h-5 w-5\" viewBox=\"0 -960 960 960\" class=\"w-5\" fill=\"#5f6368\"><path d=\"M360-120v-720h80v720h-80Zm160-160v-400l200 200-200 200Z\"/></svg>\n </div>\n <aside class=\"bg-white border-r overflow-y-auto overflow-x-hidden h-full transition-all duration-300 ease-in-out z-20 w-0 lg:w-48 fixed lg:relative shrink-0\" :class=\"hideSidebar === true ? '!w-0' : hideSidebar === false ? '!w-48' : ''\">\n <div class=\"flex items-center border-b border-gray-100 w-48 overflow-x-hidden\">\n <div class=\"p-4 font-bold text-lg\">Models</div>\n <button\n @click=\"hideSidebar = true\"\n class=\"ml-auto mr-2 p-2 rounded hover:bg-gray-200 focus:outline-none\"\n aria-label=\"Close sidebar\"\n >\n <svg xmlns=\"http://www.w3.org/2000/svg\" style=\"h-5 w-5\" viewBox=\"0 -960 960 960\" class=\"w-5\" fill=\"currentColor\"><path d=\"M660-320v-320L500-480l160 160ZM200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm120-80v-560H200v560h120Zm80 0h360v-560H400v560Zm-80 0H200h120Z\"/></svg>\n </button>\n </div>\n <nav class=\"flex flex-1 flex-col\">\n <ul role=\"list\" class=\"flex flex-1 flex-col gap-y-7\">\n <li>\n <ul role=\"list\">\n <li v-for=\"model in models\">\n <router-link\n :to=\"'/model/' + model\"\n class=\"block truncate rounded-md py-2 pr-2 pl-2 text-sm font-semibold text-gray-700\"\n :class=\"model === currentModel ? 'bg-ultramarine-100 font-bold' : 'hover:bg-ultramarine-100'\">\n {{model}}\n </router-link>\n </li>\n </ul>\n </li>\n </ul>\n </nav>\n </aside>\n <div class=\"documents\" ref=\"documentsList\">\n <div class=\"relative h-[42px] z-10\">\n <div class=\"documents-menu\">\n <div class=\"flex flex-row items-center w-full gap-2\">\n <form @submit.prevent=\"search\" class=\"relative flex-grow m-0\">\n <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\" />\n <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\">\n <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>\n </ul>\n </form>\n <div>\n <span v-if=\"numDocuments == null\">Loading ...</span>\n <span v-else-if=\"typeof numDocuments === 'number'\">{{numDocuments === 1 ? numDocuments+ ' document' : numDocuments + ' documents'}}</span>\n </div>\n <button\n @click=\"shouldShowExportModal = true\"\n type=\"button\"\n v-show=\"!selectMultiple\"\n class=\"rounded bg-ultramarine-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600\">\n Export\n </button>\n <button\n @click=\"stagingSelect\"\n type=\"button\"\n :class=\"{ 'bg-gray-500 ring-inset ring-2 ring-gray-300 hover:bg-gray-600': selectMultiple, 'bg-ultramarine-600 hover:bg-ultramarine-500' : !selectMultiple }\"\n class=\"rounded px-2 py-2 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600\"\n >\n {{ selectMultiple ? 'Cancel' : 'Select' }}\n </button>\n <button\n v-show=\"selectMultiple\"\n @click=\"shouldShowUpdateMultipleModal=true;\"\n type=\"button\"\n class=\"rounded bg-green-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600\"\n >\n Update\n </button>\n <button\n @click=\"shouldShowDeleteMultipleModal=true;\"\n type=\"button\"\n v-show=\"selectMultiple\"\n class=\"rounded bg-red-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500\"\n >\n Delete\n </button>\n <button\n @click=\"openIndexModal\"\n type=\"button\"\n v-show=\"!selectMultiple\"\n class=\"rounded bg-ultramarine-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600\">\n Indexes\n </button>\n <button\n @click=\"shouldShowCreateModal = true;\"\n type=\"button\"\n v-show=\"!selectMultiple\"\n class=\"rounded bg-ultramarine-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600\">\n Create\n </button>\n <button\n @click=\"openFieldSelection\"\n type=\"button\"\n v-show=\"!selectMultiple\"\n class=\"rounded bg-ultramarine-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600\">\n Fields\n </button>\n <span class=\"isolate inline-flex rounded-md shadow-sm\">\n <button\n @click=\"outputType = 'table'\"\n type=\"button\"\n class=\"relative inline-flex items-center rounded-none rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10\"\n :class=\"outputType === 'table' ? 'bg-gray-200' : 'bg-white'\">\n <img class=\"h-5 w-5\" src=\"images/table.svg\">\n </button>\n <button\n @click=\"outputType = 'json'\"\n type=\"button\"\n class=\"relative -ml-px inline-flex items-center rounded-none rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10\"\n :class=\"outputType === 'json' ? 'bg-gray-200' : 'bg-white'\">\n <img class=\"h-5 w-5\" src=\"images/json.svg\">\n </button>\n </span>\n </div>\n </div>\n </div>\n <div class=\"documents-container relative\">\n <table v-if=\"outputType === 'table'\">\n <thead>\n <th v-for=\"path in filteredPaths\" @click=\"clickFilter(path.path)\" class=\"cursor-pointer\">\n {{path.path}}\n <span class=\"path-type\">\n ({{(path.instance || 'unknown')}})\n </span>\n <span class=\"sort-arrow\" @click=\"sortDocs(1, path.path)\">{{sortBy[path.path] == 1 ? 'X' : '↑'}}</span>\n <span class=\"sort-arrow\" @click=\"sortDocs(-1, path.path)\">{{sortBy[path.path] == -1 ? 'X' : '↓'}}</span>\n </th>\n </thead>\n <tbody>\n <tr v-for=\"document in documents\" @click=\"handleDocumentClick(document, $event)\" :key=\"document._id\">\n <td v-for=\"schemaPath in filteredPaths\" :class=\"{ 'bg-blue-200': selectedDocuments.some(x => x._id.toString() === document._id.toString()) }\">\n <component\n :is=\"getComponentForPath(schemaPath)\"\n :value=\"getValueForPath(document, schemaPath.path)\"\n :allude=\"getReferenceModel(schemaPath)\">\n </component>\n </td>\n </tr>\n </tbody>\n </table>\n <div v-if=\"outputType === 'json'\">\n <div v-for=\"document in documents\" @click=\"handleDocumentClick(document, $event)\" :key=\"document._id\" :class=\"{ 'bg-blue-200': selectedDocuments.some(x => x._id.toString() === document._id.toString()) }\">\n <list-json :value=\"filterDocument(document)\">\n </list-json>\n </div>\n </div>\n <div v-if=\"status === 'loading'\" class=\"loader\">\n <img src=\"images/loader.gif\">\n </div>\n </div>\n </div>\n <modal v-if=\"shouldShowExportModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowExportModal = false\">&times;</div>\n <export-query-results\n :schemaPaths=\"schemaPaths\"\n :search-text=\"searchText\"\n :currentModel=\"currentModel\"\n @done=\"shouldShowExportModal = false\">\n </export-query-results>\n </template>\n </modal>\n <modal v-if=\"shouldShowIndexModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowIndexModal = false\">&times;</div>\n <div class=\"text-xl font-bold mb-2\">Indexes</div>\n <div v-for=\"index in mongoDBIndexes\" class=\"w-full flex items-center\">\n <div class=\"grow shrink text-left flex justify-between items-center\" v-if=\"index.name != '_id_'\">\n <div>\n <div class=\"font-bold\">{{ index.name }}</div>\n <div class=\"text-sm font-mono\">{{ JSON.stringify(index.key) }}</div>\n </div>\n <div>\n <async-button\n type=\"button\"\n @click=\"dropIndex(index.name)\"\n class=\"rounded-md bg-valencia-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-valencia-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600 disabled:bg-gray-400 disabled:cursor-not-allowed\">\n Drop\n </async-button>\n </div>\n </div>\n </div>\n </template>\n </modal>\n <modal v-if=\"shouldShowFieldModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowFieldModal = false; selectedPaths = [...filteredPaths];\">&times;</div>\n <div v-for=\"(path, index) in schemaPaths\" :key=\"index\" class=\"w-5 flex items-center\">\n <input class=\"mt-0 h-4 w-4 rounded border-gray-300 text-sky-600 focus:ring-sky-600 accent-sky-600\" type=\"checkbox\" :id=\"'path.path'+index\" @change=\"addOrRemove(path)\" :value=\"path.path\" :checked=\"isSelected(path.path)\" />\n <div class=\"ml-2 text-gray-700 grow shrink text-left\">\n <label :for=\"'path.path' + index\">{{path.path}}</label>\n </div>\n </div>\n <div class=\"mt-4 flex gap-2\">\n <button type=\"button\" @click=\"filterDocuments()\" class=\"rounded-md bg-ultramarine-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-600\">Filter Selection</button>\n <button type=\"button\" @click=\"selectAll()\" class=\"rounded-md bg-forest-green-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600\">Select All</button>\n <button type=\"button\" @click=\"deselectAll()\" class=\"rounded-md bg-valencia-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-valencia-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600\">Deselect All</button>\n <button type=\"button\" @click=\"resetDocuments()\" class=\"rounded-md bg-gray-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-gray-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600\" >Cancel</button>\n </div>\n </template>\n </modal>\n <modal v-if=\"shouldShowCreateModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowCreateModal = false;\">&times;</div>\n <create-document :currentModel=\"currentModel\" :paths=\"schemaPaths\" @close=\"closeCreationModal\"></create-document>\n </template>\n </modal>\n <modal v-if=\"shouldShowUpdateMultipleModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowUpdateMultipleModal = false;\">&times;</div>\n <update-document :currentModel=\"currentModel\" :document=\"selectedDocuments\" :multiple=\"true\" @update=\"updateDocuments\" @close=\"shouldShowUpdateMultipleModal=false;\"></update-document>\n </template>\n </modal>\n <modal v-if=\"shouldShowDeleteMultipleModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowDeleteMultipleModal = false;\">&times;</div>\n <h2>Are you sure you want to delete {{selectedDocuments.length}} documents?</h2>\n <div>\n <list-json :value=\"selectedDocuments\"></list-json>\n </div>\n <div class=\"flex gap-4\">\n <async-button @click=\"deleteDocuments\" class=\"rounded bg-red-500 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600\">\n Confirm\n </async-button>\n <button @click=\"shouldShowDeleteMultipleModal = false;\" class=\"rounded bg-gray-400 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-gray-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500\">\n Cancel\n </button>\n </div>\n </template>\n </modal>\n</div>\n";
4897
5153
 
4898
5154
  /***/ }),
4899
5155
 
@@ -15011,7 +15267,7 @@ var bson = /*#__PURE__*/Object.freeze({
15011
15267
  /***/ ((module) => {
15012
15268
 
15013
15269
  "use strict";
15014
- module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.0.124","description":"A sleek, powerful MongoDB UI with built-in dashboarding and auth, seamlessly integrated with your Express, Vercel, or Netlify app.","homepage":"https://studio.mongoosejs.io/","repository":{"type":"git","url":"https://github.com/mongoosejs/studio"},"dependencies":{"archetype":"0.13.1","csv-stringify":"6.3.0","ejson":"^2.2.3","extrovert":"^0.1.0","marked":"15.0.12","node-inspect-extracted":"3.x","tailwindcss":"3.4.0","vanillatoasts":"^1.6.0","vue":"3.x","webpack":"5.x"},"peerDependencies":{"bson":"^5.5.1 || 6.x","express":"4.x","mongoose":"7.x || 8.x"},"devDependencies":{"@masteringjs/eslint-config":"0.1.1","axios":"1.2.2","dedent":"^1.6.0","eslint":"9.30.0","express":"4.x","mocha":"10.2.0","mongoose":"8.x"},"scripts":{"lint":"eslint .","tailwind":"tailwindcss -o ./frontend/public/tw.css","tailwind:watch":"tailwindcss -o ./frontend/public/tw.css --watch","test":"mocha test/*.test.js"}}');
15270
+ module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.0.126","description":"A sleek, powerful MongoDB UI with built-in dashboarding and auth, seamlessly integrated with your Express, Vercel, or Netlify app.","homepage":"https://studio.mongoosejs.io/","repository":{"type":"git","url":"https://github.com/mongoosejs/studio"},"dependencies":{"archetype":"0.13.1","csv-stringify":"6.3.0","ejson":"^2.2.3","extrovert":"^0.1.0","marked":"15.0.12","node-inspect-extracted":"3.x","tailwindcss":"3.4.0","vanillatoasts":"^1.6.0","vue":"3.x","webpack":"5.x"},"peerDependencies":{"bson":"^5.5.1 || 6.x","express":"4.x","mongoose":"7.x || 8.x"},"devDependencies":{"@masteringjs/eslint-config":"0.1.1","axios":"1.2.2","dedent":"^1.6.0","eslint":"9.30.0","express":"4.x","mocha":"10.2.0","mongoose":"8.x"},"scripts":{"lint":"eslint .","tailwind":"tailwindcss -o ./frontend/public/tw.css","tailwind:watch":"tailwindcss -o ./frontend/public/tw.css --watch","test":"mocha test/*.test.js"}}');
15015
15271
 
15016
15272
  /***/ })
15017
15273
 
@@ -662,6 +662,10 @@ video {
662
662
  z-index: 50;
663
663
  }
664
664
 
665
+ .z-\[9999\] {
666
+ z-index: 9999;
667
+ }
668
+
665
669
  .col-start-1 {
666
670
  grid-column-start: 1;
667
671
  }
@@ -902,6 +906,10 @@ video {
902
906
  height: 1px;
903
907
  }
904
908
 
909
+ .max-h-40 {
910
+ max-height: 10rem;
911
+ }
912
+
905
913
  .max-h-\[50vh\] {
906
914
  max-height: 50vh;
907
915
  }
@@ -1474,11 +1482,6 @@ video {
1474
1482
  background-color: rgb(229 234 255 / var(--tw-bg-opacity));
1475
1483
  }
1476
1484
 
1477
- .bg-ultramarine-500 {
1478
- --tw-bg-opacity: 1;
1479
- background-color: rgb(63 83 255 / var(--tw-bg-opacity));
1480
- }
1481
-
1482
1485
  .bg-ultramarine-600 {
1483
1486
  --tw-bg-opacity: 1;
1484
1487
  background-color: rgb(24 35 255 / var(--tw-bg-opacity));
@@ -2086,11 +2089,6 @@ video {
2086
2089
  background-color: rgb(63 83 255 / var(--tw-bg-opacity));
2087
2090
  }
2088
2091
 
2089
- .hover\:bg-ultramarine-600:hover {
2090
- --tw-bg-opacity: 1;
2091
- background-color: rgb(24 35 255 / var(--tw-bg-opacity));
2092
- }
2093
-
2094
2092
  .hover\:bg-valencia-400:hover {
2095
2093
  --tw-bg-opacity: 1;
2096
2094
  background-color: rgb(235 126 126 / var(--tw-bg-opacity));
@@ -84,7 +84,7 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
84
84
  exportQueryResults(params) {
85
85
  const accessToken = window.localStorage.getItem('_mongooseStudioAccessToken') || null;
86
86
 
87
- return fetch(window.MONGOOSE_STUDIO_CONFIG.baseURL + new URLSearchParams({ ...params, action: 'Model.exportQueryResults' }).toString(), {
87
+ return fetch(window.MONGOOSE_STUDIO_CONFIG.baseURL + '?' + new URLSearchParams({ ...params, action: 'Model.exportQueryResults' }).toString(), {
88
88
  method: 'GET',
89
89
  headers: {
90
90
  Authorization: `${accessToken}`, // Set your authorization token here
@@ -31,11 +31,14 @@
31
31
  </nav>
32
32
  </aside>
33
33
  <div class="documents" ref="documentsList">
34
- <div class="relative h-[42px]">
34
+ <div class="relative h-[42px] z-10">
35
35
  <div class="documents-menu">
36
36
  <div class="flex flex-row items-center w-full gap-2">
37
- <form @submit.prevent="search" class="flex-grow m-0">
38
- <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" />
37
+ <form @submit.prevent="search" class="relative flex-grow m-0">
38
+ <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" />
39
+ <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">
40
+ <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>
41
+ </ul>
39
42
  </form>
40
43
  <div>
41
44
  <span v-if="numDocuments == null">Loading ...</span>
@@ -51,10 +54,10 @@
51
54
  <button
52
55
  @click="stagingSelect"
53
56
  type="button"
54
- :class="{ 'bg-ultramarine-500 ring-inset ring-2 ring-gray-300 hover:bg-ultramarine-600': selectMultiple }"
55
- class="rounded bg-ultramarine-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600"
57
+ :class="{ 'bg-gray-500 ring-inset ring-2 ring-gray-300 hover:bg-gray-600': selectMultiple, 'bg-ultramarine-600 hover:bg-ultramarine-500' : !selectMultiple }"
58
+ class="rounded px-2 py-2 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600"
56
59
  >
57
- Select
60
+ {{ selectMultiple ? 'Cancel' : 'Select' }}
58
61
  </button>
59
62
  <button
60
63
  v-show="selectMultiple"
@@ -125,7 +128,7 @@
125
128
  </th>
126
129
  </thead>
127
130
  <tbody>
128
- <tr v-for="document in documents" @click="handleDocumentClick(document)" :key="document._id">
131
+ <tr v-for="document in documents" @click="handleDocumentClick(document, $event)" :key="document._id">
129
132
  <td v-for="schemaPath in filteredPaths" :class="{ 'bg-blue-200': selectedDocuments.some(x => x._id.toString() === document._id.toString()) }">
130
133
  <component
131
134
  :is="getComponentForPath(schemaPath)"
@@ -137,7 +140,7 @@
137
140
  </tbody>
138
141
  </table>
139
142
  <div v-if="outputType === 'json'">
140
- <div v-for="document in documents" @click="handleDocumentClick(document)" :key="document._id" :class="{ 'bg-blue-200': selectedDocuments.some(x => x._id.toString() === document._id.toString()) }">
143
+ <div v-for="document in documents" @click="handleDocumentClick(document, $event)" :key="document._id" :class="{ 'bg-blue-200': selectedDocuments.some(x => x._id.toString() === document._id.toString()) }">
141
144
  <list-json :value="filterDocument(document)">
142
145
  </list-json>
143
146
  </div>
@@ -5,6 +5,34 @@ 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
+ ];
8
36
 
9
37
 
10
38
  appendCSS(require('./models.css'));
@@ -31,6 +59,9 @@ module.exports = app => app.component('models', {
31
59
  selectMultiple: false,
32
60
  selectedDocuments: [],
33
61
  searchText: '',
62
+ autocompleteSuggestions: [],
63
+ autocompleteIndex: 0,
64
+ autocompleteTrie: null,
34
65
  shouldShowExportModal: false,
35
66
  shouldShowCreateModal: false,
36
67
  shouldShowFieldModal: false,
@@ -43,10 +74,12 @@ module.exports = app => app.component('models', {
43
74
  scrollHeight: 0,
44
75
  interval: null,
45
76
  outputType: 'table', // json, table
46
- hideSidebar: null
77
+ hideSidebar: null,
78
+ lastSelectedIndex: null
47
79
  }),
48
80
  created() {
49
81
  this.currentModel = this.model;
82
+ this.buildAutocompleteTrie();
50
83
  },
51
84
  beforeDestroy() {
52
85
  document.removeEventListener('scroll', this.onScroll, true);
@@ -65,6 +98,16 @@ module.exports = app => app.component('models', {
65
98
  await this.initSearchFromUrl();
66
99
  },
67
100
  methods: {
101
+ buildAutocompleteTrie() {
102
+ this.autocompleteTrie = new Trie();
103
+ this.autocompleteTrie.bulkInsert(QUERY_SELECTORS, 5);
104
+ if (Array.isArray(this.schemaPaths) && this.schemaPaths.length > 0) {
105
+ const paths = this.schemaPaths
106
+ .map(path => path?.path)
107
+ .filter(path => typeof path === 'string' && path.length > 0);
108
+ this.autocompleteTrie.bulkInsert(paths, 10);
109
+ }
110
+ },
68
111
  async initSearchFromUrl() {
69
112
  this.status = 'loading';
70
113
  this.query = Object.assign({}, this.$route.query); // important that this is here before the if statements
@@ -102,6 +145,62 @@ module.exports = app => app.component('models', {
102
145
  });
103
146
  }
104
147
  },
148
+ updateAutocomplete() {
149
+ const input = this.$refs.searchInput;
150
+ const cursorPos = input ? input.selectionStart : 0;
151
+ const before = this.searchText.slice(0, cursorPos);
152
+ const match = before.match(/(?:\{|,)\s*([^:\s]*)$/);
153
+ if (match && match[1]) {
154
+ const term = match[1].replace(/["']/g, '');
155
+ if (!term) {
156
+ this.autocompleteSuggestions = [];
157
+ return;
158
+ }
159
+ if (this.autocompleteTrie) {
160
+ this.autocompleteSuggestions = this.autocompleteTrie.getSuggestions(term, 10);
161
+ this.autocompleteIndex = 0;
162
+ return;
163
+ }
164
+ }
165
+ this.autocompleteSuggestions = [];
166
+ },
167
+ handleKeyDown(ev) {
168
+ if (this.autocompleteSuggestions.length === 0) {
169
+ return;
170
+ }
171
+ if (ev.key === 'Tab' || ev.key === 'Enter') {
172
+ ev.preventDefault();
173
+ this.applySuggestion(this.autocompleteIndex);
174
+ } else if (ev.key === 'ArrowDown') {
175
+ ev.preventDefault();
176
+ this.autocompleteIndex = (this.autocompleteIndex + 1) % this.autocompleteSuggestions.length;
177
+ } else if (ev.key === 'ArrowUp') {
178
+ ev.preventDefault();
179
+ this.autocompleteIndex = (this.autocompleteIndex + this.autocompleteSuggestions.length - 1) % this.autocompleteSuggestions.length;
180
+ }
181
+ },
182
+ applySuggestion(index) {
183
+ const suggestion = this.autocompleteSuggestions[index];
184
+ if (!suggestion) {
185
+ return;
186
+ }
187
+ const input = this.$refs.searchInput;
188
+ const cursorPos = input.selectionStart;
189
+ const before = this.searchText.slice(0, cursorPos);
190
+ const after = this.searchText.slice(cursorPos);
191
+ const match = before.match(/(?:\{|,)\s*([^:\s]*)$/);
192
+ if (!match) {
193
+ return;
194
+ }
195
+ const token = match[1];
196
+ const start = cursorPos - token.length;
197
+ this.searchText = this.searchText.slice(0, start) + suggestion + after;
198
+ this.$nextTick(() => {
199
+ const pos = start + suggestion.length;
200
+ input.setSelectionRange(pos, pos);
201
+ });
202
+ this.autocompleteSuggestions = [];
203
+ },
105
204
  clickFilter(path) {
106
205
  if (this.searchText) {
107
206
  if (this.searchText.endsWith('}')) {
@@ -214,8 +313,10 @@ module.exports = app => app.component('models', {
214
313
  // Clear previous data
215
314
  this.documents = [];
216
315
  this.schemaPaths = [];
316
+ this.buildAutocompleteTrie();
217
317
  this.numDocuments = null;
218
318
  this.loadedAllDocs = false;
319
+ this.lastSelectedIndex = null;
219
320
 
220
321
  let docsCount = 0;
221
322
  let schemaPathsReceived = false;
@@ -247,6 +348,7 @@ module.exports = app => app.component('models', {
247
348
  }
248
349
  this.filteredPaths = [...this.schemaPaths];
249
350
  this.selectedPaths = [...this.schemaPaths];
351
+ this.buildAutocompleteTrie();
250
352
  schemaPathsReceived = true;
251
353
  }
252
354
  if (event.numDocs !== undefined) {
@@ -389,17 +491,38 @@ module.exports = app => app.component('models', {
389
491
  }
390
492
  this.edittingDoc = null;
391
493
  },
392
- handleDocumentClick(document) {
393
- console.log(this.selectedDocuments);
494
+ handleDocumentClick(document, event) {
394
495
  if (this.selectMultiple) {
395
- const exists = this.selectedDocuments.find(x => x._id.toString() == document._id.toString());
396
- if (exists) {
397
- const index = this.selectedDocuments.findIndex(x => x._id.toString() == document._id.toString());
398
- if (index !== -1) {
399
- this.selectedDocuments.splice(index, 1);
496
+ const documentIndex = this.documents.findIndex(doc => doc._id.toString() == document._id.toString());
497
+ if (event?.shiftKey && this.selectedDocuments.length > 0) {
498
+ const anchorIndex = this.lastSelectedIndex;
499
+ if (anchorIndex != null && anchorIndex !== -1 && documentIndex !== -1) {
500
+ const start = Math.min(anchorIndex, documentIndex);
501
+ const end = Math.max(anchorIndex, documentIndex);
502
+ const selectedDocumentIds = new Set(this.selectedDocuments.map(doc => doc._id.toString()));
503
+ for (let i = start; i <= end; i++) {
504
+ const docInRange = this.documents[i];
505
+ const existsInRange = selectedDocumentIds.has(docInRange._id.toString());
506
+ if (!existsInRange) {
507
+ this.selectedDocuments.push(docInRange);
508
+ }
509
+ }
510
+ this.lastSelectedIndex = documentIndex;
511
+ return;
512
+ }
513
+ }
514
+ const index = this.selectedDocuments.findIndex(x => x._id.toString() == document._id.toString());
515
+ if (index !== -1) {
516
+ this.selectedDocuments.splice(index, 1);
517
+ if (this.selectedDocuments.length === 0) {
518
+ this.lastSelectedIndex = null;
519
+ } else {
520
+ const lastDoc = this.selectedDocuments[this.selectedDocuments.length - 1];
521
+ this.lastSelectedIndex = this.documents.findIndex(doc => doc._id.toString() == lastDoc._id.toString());
400
522
  }
401
523
  } else {
402
524
  this.selectedDocuments.push(document);
525
+ this.lastSelectedIndex = documentIndex;
403
526
  }
404
527
  } else {
405
528
  this.$router.push('/model/' + this.currentModel + '/document/' + document._id);
@@ -413,18 +536,21 @@ module.exports = app => app.component('models', {
413
536
  });
414
537
  await this.getDocuments();
415
538
  this.selectedDocuments.length = 0;
539
+ this.lastSelectedIndex = null;
416
540
  this.shouldShowDeleteMultipleModal = false;
417
541
  this.selectMultiple = false;
418
542
  },
419
543
  async updateDocuments() {
420
544
  await this.getDocuments();
421
545
  this.selectedDocuments.length = 0;
546
+ this.lastSelectedIndex = null;
422
547
  this.selectMultiple = false;
423
548
  },
424
549
  stagingSelect() {
425
550
  if (this.selectMultiple) {
426
551
  this.selectMultiple = false;
427
552
  this.selectedDocuments.length = 0;
553
+ this.lastSelectedIndex = null;
428
554
  } else {
429
555
  this.selectMultiple = true;
430
556
  }
@@ -0,0 +1,117 @@
1
+ 'use strict';
2
+
3
+ class TrieNode {
4
+ constructor() {
5
+ this.children = Object.create(null);
6
+ this.isEnd = false;
7
+ this.freq = 0;
8
+ }
9
+ }
10
+
11
+ class Trie {
12
+ constructor() {
13
+ this.root = new TrieNode();
14
+ }
15
+
16
+ insert(word, freq = 1) {
17
+ if (!word) {
18
+ return;
19
+ }
20
+ let node = this.root;
21
+ for (const ch of word) {
22
+ if (!node.children[ch]) {
23
+ node.children[ch] = new TrieNode();
24
+ }
25
+ node = node.children[ch];
26
+ }
27
+ node.isEnd = true;
28
+ node.freq += freq;
29
+ }
30
+
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
+ }
38
+ }
39
+
40
+ collect(node, prefix, out) {
41
+ if (node.isEnd) {
42
+ out.push([prefix, node.freq]);
43
+ }
44
+ for (const [ch, child] of Object.entries(node.children)) {
45
+ this.collect(child, prefix + ch, out);
46
+ }
47
+ }
48
+
49
+ suggest(prefix, limit = 10) {
50
+ let node = this.root;
51
+ for (const ch of prefix) {
52
+ if (!node.children[ch]) {
53
+ return [];
54
+ }
55
+ node = node.children[ch];
56
+ }
57
+ const results = [];
58
+ this.collect(node, prefix, results);
59
+ results.sort((a, b) => b[1] - a[1]);
60
+ return results.slice(0, limit).map(([word]) => word);
61
+ }
62
+
63
+ fuzzySuggest(prefix, limit = 10) {
64
+ const results = new Set();
65
+
66
+ const dfs = (node, path, edits) => {
67
+ if (edits > 1) {
68
+ return;
69
+ }
70
+ if (node.isEnd && Math.abs(path.length - prefix.length) <= 1) {
71
+ const dist = levenshtein(prefix, path);
72
+ if (dist <= 1) {
73
+ results.add(path);
74
+ }
75
+ }
76
+ for (const [ch, child] of Object.entries(node.children)) {
77
+ const nextEdits = ch === prefix[path.length] ? edits : edits + 1;
78
+ dfs(child, path + ch, nextEdits);
79
+ }
80
+ };
81
+
82
+ dfs(this.root, '', 0);
83
+ return Array.from(results).slice(0, limit);
84
+ }
85
+
86
+ getSuggestions(prefix, limit = 10) {
87
+ if (!prefix) {
88
+ return [];
89
+ }
90
+ const exact = this.suggest(prefix, limit);
91
+ if (exact.length >= limit) {
92
+ return exact;
93
+ }
94
+ const fuzzy = this.fuzzySuggest(prefix, limit - exact.length);
95
+ return [...exact, ...fuzzy];
96
+ }
97
+ }
98
+
99
+ function levenshtein(a, b) {
100
+ const dp = Array.from({ length: a.length + 1 }, (_, i) =>
101
+ Array.from({ length: b.length + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0))
102
+ );
103
+ for (let i = 1; i <= a.length; i++) {
104
+ for (let j = 1; j <= b.length; j++) {
105
+ dp[i][j] =
106
+ a[i - 1] === b[j - 1]
107
+ ? dp[i - 1][j - 1]
108
+ : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
109
+ }
110
+ }
111
+ return dp[a.length][b.length];
112
+ }
113
+
114
+ module.exports = {
115
+ Trie,
116
+ TrieNode
117
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mongoosejs/studio",
3
- "version": "0.0.124",
3
+ "version": "0.0.126",
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": {