@mongoosejs/studio 0.1.5 → 0.1.7

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.
@@ -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() {
@@ -108,17 +74,18 @@ module.exports = app => app.component('models', {
108
74
 
109
75
  await this.initSearchFromUrl();
110
76
  },
111
- methods: {
112
- buildAutocompleteTrie() {
113
- this.autocompleteTrie = new Trie();
114
- this.autocompleteTrie.bulkInsert(QUERY_SELECTORS, 5);
115
- if (Array.isArray(this.schemaPaths) && this.schemaPaths.length > 0) {
116
- const paths = this.schemaPaths
117
- .map(path => path?.path)
118
- .filter(path => typeof path === 'string' && path.length > 0);
119
- this.autocompleteTrie.bulkInsert(paths, 10);
77
+ computed: {
78
+ referenceMap() {
79
+ const map = {};
80
+ for (const path of this.filteredPaths) {
81
+ if (path?.ref) {
82
+ map[path.path] = path.ref;
83
+ }
120
84
  }
121
- },
85
+ return map;
86
+ }
87
+ },
88
+ methods: {
122
89
  loadOutputPreference() {
123
90
  if (typeof window === 'undefined' || !window.localStorage) {
124
91
  return;
@@ -194,141 +161,6 @@ module.exports = app => app.component('models', {
194
161
  const { mongoDBIndexes } = await api.Model.dropIndex({ model: this.currentModel, name });
195
162
  this.mongoDBIndexes = mongoDBIndexes;
196
163
  },
197
- initFilter(ev) {
198
- if (!this.searchText) {
199
- this.searchText = '{}';
200
- this.$nextTick(() => {
201
- ev.target.setSelectionRange(1, 1);
202
- });
203
- }
204
- },
205
- updateAutocomplete() {
206
- const input = this.$refs.searchInput;
207
- const cursorPos = input ? input.selectionStart : 0;
208
- const before = this.searchText.slice(0, cursorPos);
209
- const match = before.match(/(?:\{|,)\s*([^:\s]*)$/);
210
- if (match && match[1]) {
211
- const token = match[1];
212
- const leadingQuoteMatch = token.match(/^["']/);
213
- const trailingQuoteMatch = token.length > 1 && /["']$/.test(token)
214
- ? token[token.length - 1]
215
- : '';
216
- const term = token
217
- .replace(/^["']/, '')
218
- .replace(trailingQuoteMatch ? new RegExp(`[${trailingQuoteMatch}]$`) : '', '')
219
- .trim();
220
- if (!term) {
221
- this.autocompleteSuggestions = [];
222
- return;
223
- }
224
- if (this.autocompleteTrie) {
225
- const primarySuggestions = this.autocompleteTrie.getSuggestions(term, 10);
226
- const suggestionsSet = new Set(primarySuggestions);
227
- if (Array.isArray(this.schemaPaths) && this.schemaPaths.length > 0) {
228
- for (const schemaPath of this.schemaPaths) {
229
- const path = schemaPath?.path;
230
- if (
231
- typeof path === 'string' &&
232
- path.startsWith(`${term}.`) &&
233
- !suggestionsSet.has(path)
234
- ) {
235
- suggestionsSet.add(path);
236
- if (suggestionsSet.size >= 10) {
237
- break;
238
- }
239
- }
240
- }
241
- }
242
- let suggestions = Array.from(suggestionsSet);
243
- if (leadingQuoteMatch) {
244
- const leadingQuote = leadingQuoteMatch[0];
245
- suggestions = suggestions.map(suggestion => `${leadingQuote}${suggestion}`);
246
- }
247
- if (trailingQuoteMatch) {
248
- suggestions = suggestions.map(suggestion =>
249
- suggestion.endsWith(trailingQuoteMatch) ? suggestion : `${suggestion}${trailingQuoteMatch}`
250
- );
251
- }
252
- this.autocompleteSuggestions = suggestions;
253
- this.autocompleteIndex = 0;
254
- return;
255
- }
256
- }
257
- this.autocompleteSuggestions = [];
258
- },
259
- handleKeyDown(ev) {
260
- if (this.autocompleteSuggestions.length === 0) {
261
- return;
262
- }
263
- if (ev.key === 'Tab' || ev.key === 'Enter') {
264
- ev.preventDefault();
265
- this.applySuggestion(this.autocompleteIndex);
266
- } else if (ev.key === 'ArrowDown') {
267
- ev.preventDefault();
268
- this.autocompleteIndex = (this.autocompleteIndex + 1) % this.autocompleteSuggestions.length;
269
- } else if (ev.key === 'ArrowUp') {
270
- ev.preventDefault();
271
- this.autocompleteIndex = (this.autocompleteIndex + this.autocompleteSuggestions.length - 1) % this.autocompleteSuggestions.length;
272
- }
273
- },
274
- applySuggestion(index) {
275
- const suggestion = this.autocompleteSuggestions[index];
276
- if (!suggestion) {
277
- return;
278
- }
279
- const input = this.$refs.searchInput;
280
- const cursorPos = input.selectionStart;
281
- const before = this.searchText.slice(0, cursorPos);
282
- const after = this.searchText.slice(cursorPos);
283
- const match = before.match(/(?:\{|,)\s*([^:\s]*)$/);
284
- const colonNeeded = !/^\s*:/.test(after);
285
- if (!match) {
286
- return;
287
- }
288
- const token = match[1];
289
- const start = cursorPos - token.length;
290
- let replacement = suggestion;
291
- const leadingQuote = token.startsWith('"') || token.startsWith('\'') ? token[0] : '';
292
- const trailingQuote = token.length > 1 && (token.endsWith('"') || token.endsWith('\'')) ? token[token.length - 1] : '';
293
- if (leadingQuote && !replacement.startsWith(leadingQuote)) {
294
- replacement = `${leadingQuote}${replacement}`;
295
- }
296
- if (trailingQuote && !replacement.endsWith(trailingQuote)) {
297
- replacement = `${replacement}${trailingQuote}`;
298
- }
299
- // Only insert : if we know the user isn't entering in a nested path
300
- if (colonNeeded && (!leadingQuote || trailingQuote)) {
301
- replacement = `${replacement}:`;
302
- }
303
- this.searchText = this.searchText.slice(0, start) + replacement + after;
304
- this.$nextTick(() => {
305
- const pos = start + replacement.length;
306
- input.setSelectionRange(pos, pos);
307
- });
308
- this.autocompleteSuggestions = [];
309
- },
310
- clickFilter(path) {
311
- if (this.searchText) {
312
- if (this.searchText.endsWith('}')) {
313
- this.searchText = this.searchText.slice(0, -1) + `, ${path}: }`;
314
- } else {
315
- this.searchText += `, ${path}: }`;
316
- }
317
-
318
- } else {
319
- // If this.searchText is empty or undefined, initialize it with a new object
320
- this.searchText = `{ ${path}: }`;
321
- }
322
-
323
-
324
- this.$nextTick(() => {
325
- const input = this.$refs.searchInput;
326
- const cursorIndex = this.searchText.lastIndexOf(':') + 2; // Move cursor after ": "
327
-
328
- input.focus();
329
- input.setSelectionRange(cursorIndex, cursorIndex);
330
- });
331
- },
332
164
  async closeCreationModal() {
333
165
  this.shouldShowCreateModal = false;
334
166
  await this.getDocuments();
@@ -338,9 +170,10 @@ module.exports = app => app.component('models', {
338
170
  },
339
171
  filterDocument(doc) {
340
172
  const filteredDoc = {};
341
- console.log(doc, this.filteredPaths);
342
173
  for (let i = 0; i < this.filteredPaths.length; i++) {
343
- filteredDoc[this.filteredPaths[i].path] = doc[this.filteredPaths[i].path];
174
+ const path = this.filteredPaths[i].path;
175
+ const value = mpath.get(path, doc);
176
+ mpath.set(path, value, filteredDoc);
344
177
  }
345
178
  return filteredDoc;
346
179
  },
@@ -377,7 +210,8 @@ module.exports = app => app.component('models', {
377
210
  }
378
211
  await this.loadMoreDocuments();
379
212
  },
380
- async search() {
213
+ async search(searchText) {
214
+ this.searchText = searchText;
381
215
  const hasSearch = typeof this.searchText === 'string' && this.searchText.trim().length > 0;
382
216
  if (hasSearch) {
383
217
  this.query.search = this.searchText;
@@ -392,6 +226,11 @@ module.exports = app => app.component('models', {
392
226
  await this.loadMoreDocuments();
393
227
  this.status = 'loaded';
394
228
  },
229
+ addPathFilter(path) {
230
+ if (this.$refs.documentSearch?.addPathFilter) {
231
+ this.$refs.documentSearch.addPathFilter(path);
232
+ }
233
+ },
395
234
  async openIndexModal() {
396
235
  this.shouldShowIndexModal = true;
397
236
  const { mongoDBIndexes, schemaIndexes } = await api.Model.getIndexes({ model: this.currentModel });
@@ -411,7 +250,6 @@ module.exports = app => app.component('models', {
411
250
  // Clear previous data
412
251
  this.documents = [];
413
252
  this.schemaPaths = [];
414
- this.buildAutocompleteTrie();
415
253
  this.numDocuments = null;
416
254
  this.loadedAllDocs = false;
417
255
  this.lastSelectedIndex = null;
@@ -439,7 +277,6 @@ module.exports = app => app.component('models', {
439
277
  }
440
278
  this.filteredPaths = [...this.schemaPaths];
441
279
  this.selectedPaths = [...this.schemaPaths];
442
- this.buildAutocompleteTrie();
443
280
  schemaPathsReceived = true;
444
281
  }
445
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.5",
3
+ "version": "0.1.7",
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": {
@@ -12,7 +12,7 @@
12
12
  "archetype": "0.13.1",
13
13
  "csv-stringify": "6.3.0",
14
14
  "ejson": "^2.2.3",
15
- "extrovert": "^0.1.0",
15
+ "extrovert": "^0.2.0",
16
16
  "marked": "15.0.12",
17
17
  "node-inspect-extracted": "3.x",
18
18
  "tailwindcss": "3.4.0",
@@ -32,7 +32,7 @@
32
32
  "eslint": "9.30.0",
33
33
  "express": "4.x",
34
34
  "mocha": "10.2.0",
35
- "mongoose": "8.x"
35
+ "mongoose": "9.x"
36
36
  },
37
37
  "scripts": {
38
38
  "lint": "eslint .",