@mongoosejs/studio 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,226 @@
1
+ 'use strict';
2
+
3
+ const template = require('./projection-search.html');
4
+
5
+ function getBraceDepth(text, pos) {
6
+ let depth = 0;
7
+ for (let i = 0; i < pos; i++) {
8
+ const ch = text[i];
9
+ if (ch === "{") {
10
+ depth++;
11
+ } else if (ch === "}") {
12
+ depth--;
13
+ }
14
+ }
15
+ return depth;
16
+ }
17
+
18
+ function collectObjectKeysBefore(text, endPos) {
19
+ const before = text.slice(0, endPos);
20
+ const keys = new Set();
21
+ const re = /([a-zA-Z_][\w.]*)\s*:/g;
22
+ let m;
23
+ while ((m = re.exec(before)) !== null) {
24
+ keys.add(m[1]);
25
+ }
26
+ return keys;
27
+ }
28
+
29
+ function getProjectionTokenContext(fullText, cursorPos) {
30
+ const before = fullText.slice(0, cursorPos);
31
+ const depth = getBraceDepth(before, before.length);
32
+
33
+ if (depth > 0) {
34
+ const m = before.match(/(?:\{|,)\s*([+-]?)([^\s:},]*)$/);
35
+ if (!m) {
36
+ return null;
37
+ }
38
+ const prefix = m[1] || '';
39
+ const raw = m[2] || '';
40
+ const term = raw.trim().toLowerCase();
41
+ if (!term) {
42
+ return null;
43
+ }
44
+ const token = `${prefix}${raw}`;
45
+ const start = cursorPos - token.length;
46
+ const selectedPaths = collectObjectKeysBefore(before, before.length);
47
+ if (raw) {
48
+ selectedPaths.delete(raw.trim());
49
+ }
50
+ return { mode: 'object', prefix, term, start, selectedPaths };
51
+ }
52
+
53
+ const tokenMatch = before.match(/(?:^|[,\s])([+-]?)([^\s,]*)$/);
54
+ if (!tokenMatch) {
55
+ return null;
56
+ }
57
+ const prefix = tokenMatch[1] || '';
58
+ const raw = tokenMatch[2] || '';
59
+ const term = raw.trim().toLowerCase();
60
+ if (!term) {
61
+ return null;
62
+ }
63
+
64
+ const token = `${prefix}${raw}`;
65
+ const after = fullText.slice(cursorPos);
66
+ const start = cursorPos - token.length;
67
+ const normalizedText = `${fullText.slice(0, start)}${token}${after}`;
68
+ const tokenCandidates = normalizedText.split(/[,\s]+/).map(s => s.trim()).filter(Boolean);
69
+ const selectedPaths = new Set(tokenCandidates.map(t => t.replace(/^[+-]/, '')).filter(Boolean));
70
+ if (raw) {
71
+ selectedPaths.delete(raw);
72
+ }
73
+
74
+ return { mode: 'list', prefix, term, start, selectedPaths };
75
+ }
76
+
77
+ module.exports = app => app.component('projection-search', {
78
+ template,
79
+ props: {
80
+ value: {
81
+ type: String,
82
+ default: ''
83
+ },
84
+ schemaPaths: {
85
+ type: Array,
86
+ default: () => []
87
+ }
88
+ },
89
+ data() {
90
+ return {
91
+ projectionText: this.value || '',
92
+ autocompleteSuggestions: [],
93
+ autocompleteIndex: 0
94
+ };
95
+ },
96
+ watch: {
97
+ value(val) {
98
+ const nextValue = val || '';
99
+ if (nextValue === this.projectionText) {
100
+ return;
101
+ }
102
+ this.projectionText = nextValue;
103
+ this.clearAutocomplete();
104
+ }
105
+ },
106
+ methods: {
107
+ focusInput() {
108
+ const input = this.$refs.projectionInput;
109
+ if (input && typeof input.focus === 'function') {
110
+ input.focus();
111
+ }
112
+ },
113
+ emitInput() {
114
+ this.$emit('projection-input', this.projectionText);
115
+ },
116
+ initProjection(ev) {
117
+ if (!this.projectionText || !this.projectionText.trim()) {
118
+ this.projectionText = '';
119
+ this.emitInput();
120
+ this.$nextTick(() => {
121
+ if (ev && ev.target) {
122
+ ev.target.setSelectionRange(0, 0);
123
+ }
124
+ });
125
+ }
126
+ },
127
+ handleInput() {
128
+ this.emitInput();
129
+ this.updateAutocomplete();
130
+ },
131
+ updateAutocomplete() {
132
+ const input = this.$refs.projectionInput;
133
+ const cursorPos = input ? input.selectionStart : 0;
134
+ const ctx = getProjectionTokenContext(this.projectionText, cursorPos);
135
+ if (!ctx) {
136
+ this.clearAutocomplete();
137
+ return;
138
+ }
139
+
140
+ const { prefix, term, selectedPaths } = ctx;
141
+
142
+ this.autocompleteSuggestions = this.schemaPaths
143
+ .map(path => path.path)
144
+ .filter(path => typeof path === 'string' && path.toLowerCase().includes(term) && !selectedPaths.has(path))
145
+ .slice(0, 10)
146
+ .map(path => (ctx.mode === 'object' ? path : `${prefix}${path}`));
147
+ this.autocompleteIndex = 0;
148
+ },
149
+ handleKeyDown(ev) {
150
+ if (this.autocompleteSuggestions.length > 0) {
151
+ if (ev.key === 'Tab') {
152
+ ev.preventDefault();
153
+ this.applySuggestion(this.autocompleteIndex);
154
+ return;
155
+ }
156
+ if (ev.key === 'Enter') {
157
+ ev.preventDefault();
158
+ this.applySuggestion(this.autocompleteIndex);
159
+ // Enter should both accept autocomplete and apply projection.
160
+ this.$nextTick(() => this.$emit('apply'));
161
+ return;
162
+ }
163
+ if (ev.key === 'ArrowDown') {
164
+ ev.preventDefault();
165
+ this.autocompleteIndex = (this.autocompleteIndex + 1) % this.autocompleteSuggestions.length;
166
+ return;
167
+ }
168
+ if (ev.key === 'ArrowUp') {
169
+ ev.preventDefault();
170
+ this.autocompleteIndex = (this.autocompleteIndex + this.autocompleteSuggestions.length - 1) %
171
+ this.autocompleteSuggestions.length;
172
+ return;
173
+ }
174
+ }
175
+
176
+ if (ev.key === 'Enter') {
177
+ ev.preventDefault();
178
+ this.$emit('apply');
179
+ } else if (ev.key === 'Escape') {
180
+ this.clearAutocomplete();
181
+ }
182
+ },
183
+ applySuggestion(index) {
184
+ const suggestion = this.autocompleteSuggestions[index];
185
+ if (!suggestion) {
186
+ return;
187
+ }
188
+ const input = this.$refs.projectionInput;
189
+ if (!input) {
190
+ return;
191
+ }
192
+
193
+ const cursorPos = input.selectionStart;
194
+ const after = this.projectionText.slice(cursorPos);
195
+ const ctx = getProjectionTokenContext(this.projectionText, cursorPos);
196
+ if (!ctx) {
197
+ return;
198
+ }
199
+
200
+ const { mode, start } = ctx;
201
+ let replacement;
202
+ if (mode === 'object') {
203
+ const colon = /^\s*:/.test(after) ? '' : ': ';
204
+ replacement = `${suggestion}${colon}`;
205
+ } else {
206
+ const needsSpace = after.length === 0 || !/^[,\s]/.test(after);
207
+ replacement = needsSpace ? `${suggestion} ` : suggestion;
208
+ }
209
+
210
+ this.projectionText = this.projectionText.slice(0, start) + replacement + after;
211
+ this.emitInput();
212
+ this.$nextTick(() => {
213
+ const newCursorPos = start + replacement.length;
214
+ input.setSelectionRange(newCursorPos, newCursorPos);
215
+ });
216
+ this.clearAutocomplete();
217
+ },
218
+ hideAutocomplete() {
219
+ window.setTimeout(() => this.clearAutocomplete(), 100);
220
+ },
221
+ clearAutocomplete() {
222
+ this.autocompleteSuggestions = [];
223
+ this.autocompleteIndex = 0;
224
+ }
225
+ }
226
+ });
package/index.d.ts CHANGED
@@ -5,7 +5,7 @@ declare module '@mongoosejs/studio' {
5
5
  const express: (
6
6
  path: string,
7
7
  connOrMongoose?: Connection | Mongoose,
8
- options?: { apiKey?: string }
8
+ options?: { apiKey?: string; bindIp?: string | string[] | null }
9
9
  ) => RequestHandler;
10
10
 
11
11
  const studio: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mongoosejs/studio",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "A Mongoose-native MongoDB UI with schema-aware autocomplete, AI-assisted queries, and dashboards that understand your models - not just your data.",
5
5
  "homepage": "https://mongoosestudio.app/",
6
6
  "repository": {