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