@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.
- package/backend/actions/Model/getDocuments.js +17 -4
- package/backend/actions/Model/getDocumentsStream.js +17 -4
- package/frontend/public/app.js +386 -286
- package/frontend/public/tw.css +21 -0
- package/frontend/src/dashboard/dashboard.js +23 -0
- package/frontend/src/list-json/json-node.html +118 -0
- package/frontend/src/list-json/list-json.html +1 -0
- package/frontend/src/list-json/list-json.js +38 -88
- package/frontend/src/models/document-search/document-search.html +23 -0
- package/frontend/src/models/document-search/document-search.js +227 -0
- package/frontend/src/models/models.html +9 -8
- package/frontend/src/models/models.js +21 -184
- package/frontend/src/models/trie.js +44 -18
- package/package.json +3 -3
|
@@ -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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
this.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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": "
|
|
35
|
+
"mongoose": "9.x"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"lint": "eslint .",
|