@mongoosejs/studio 0.0.119 → 0.0.120

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.
@@ -4,14 +4,15 @@ const Archetype = require('archetype');
4
4
  const mongoose = require('mongoose');
5
5
  const { stringify } = require('csv-stringify/sync');
6
6
  const authorize = require('../../authorize');
7
+ const evaluateFilter = require('../../helpers/evaluateFilter');
7
8
 
8
9
  const GetDocumentsParams = new Archetype({
9
10
  model: {
10
11
  $type: 'string',
11
12
  $required: true
12
13
  },
13
- filter: {
14
- $type: Archetype.Any
14
+ searchText: {
15
+ $type: 'string'
15
16
  },
16
17
  propertiesToInclude: {
17
18
  $type: ['string'],
@@ -30,8 +31,7 @@ const GetDocumentsParams = new Archetype({
30
31
 
31
32
  module.exports = ({ db }) => async function exportQueryResults(params, req, res) {
32
33
  params = new GetDocumentsParams(params);
33
- let { filter } = params;
34
- const { model, propertiesToInclude, roles } = params;
34
+ const { model, propertiesToInclude, roles, searchText } = params;
35
35
 
36
36
  await authorize('Model.exportQueryResults', roles);
37
37
 
@@ -40,12 +40,11 @@ module.exports = ({ db }) => async function exportQueryResults(params, req, res)
40
40
  throw new Error(`Model ${model} not found`);
41
41
  }
42
42
 
43
- if (typeof filter === 'string') {
44
- filter = { '$**': filter };
45
- }
43
+ const parsedFilter = evaluateFilter(searchText);
44
+ const filter = parsedFilter == null ? {} : parsedFilter;
46
45
 
47
46
  const docs = await Model.
48
- find(filter == null ? {} : filter).
47
+ find(filter).
49
48
  setOptions({ sanitizeFilter: true }).
50
49
  sort({ _id: -1 });
51
50
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  const Archetype = require('archetype');
4
4
  const removeSpecifiedPaths = require('../../helpers/removeSpecifiedPaths');
5
- const { EJSON } = require('bson');
5
+ const evaluateFilter = require('../../helpers/evaluateFilter');
6
6
  const authorize = require('../../authorize');
7
7
 
8
8
  const GetDocumentsParams = new Archetype({
@@ -20,8 +20,8 @@ const GetDocumentsParams = new Archetype({
20
20
  $required: true,
21
21
  $default: 0
22
22
  },
23
- filter: {
24
- $type: Archetype.Any
23
+ searchText: {
24
+ $type: 'string'
25
25
  },
26
26
  sort: {
27
27
  $type: Archetype.Any
@@ -36,20 +36,15 @@ module.exports = ({ db }) => async function getDocuments(params) {
36
36
  const { roles } = params;
37
37
  await authorize('Model.getDocuments', roles);
38
38
 
39
- let { filter } = params;
40
- if (filter != null && Object.keys(filter).length > 0) {
41
- filter = EJSON.parse(filter);
42
- }
43
- const { model, limit, skip, sort } = params;
39
+ const { model, limit, skip, sort, searchText } = params;
44
40
 
45
41
  const Model = db.models[model];
46
42
  if (Model == null) {
47
43
  throw new Error(`Model ${model} not found`);
48
44
  }
49
45
 
50
- if (typeof filter === 'string') {
51
- filter = { '$**': filter };
52
- }
46
+ const parsedFilter = evaluateFilter(searchText);
47
+ const filter = parsedFilter == null ? {} : parsedFilter;
53
48
 
54
49
  const hasSort = typeof sort === 'object' && sort != null && Object.keys(sort).length > 0;
55
50
  const sortObj = hasSort ? { ...sort } : {};
@@ -57,7 +52,7 @@ module.exports = ({ db }) => async function getDocuments(params) {
57
52
  sortObj._id = -1;
58
53
  }
59
54
  const cursor = await Model.
60
- find(filter == null ? {} : filter).
55
+ find(filter).
61
56
  limit(limit).
62
57
  skip(skip).
63
58
  sort(sortObj).
@@ -78,7 +73,7 @@ module.exports = ({ db }) => async function getDocuments(params) {
78
73
  }
79
74
  removeSpecifiedPaths(schemaPaths, '.$*');
80
75
 
81
- const numDocuments = filter == null ?
76
+ const numDocuments = parsedFilter == null ?
82
77
  await Model.estimatedDocumentCount() :
83
78
  await Model.countDocuments(filter);
84
79
 
@@ -0,0 +1,117 @@
1
+ 'use strict';
2
+
3
+ const Archetype = require('archetype');
4
+ const removeSpecifiedPaths = require('../../helpers/removeSpecifiedPaths');
5
+ const evaluateFilter = require('../../helpers/evaluateFilter');
6
+ const authorize = require('../../authorize');
7
+
8
+ const GetDocumentsParams = new Archetype({
9
+ model: {
10
+ $type: 'string',
11
+ $required: true
12
+ },
13
+ limit: {
14
+ $type: 'number',
15
+ $required: true,
16
+ $default: 20
17
+ },
18
+ skip: {
19
+ $type: 'number',
20
+ $required: true,
21
+ $default: 0
22
+ },
23
+ searchText: {
24
+ $type: 'string'
25
+ },
26
+ sort: {
27
+ $type: Archetype.Any
28
+ },
29
+ roles: {
30
+ $type: ['string']
31
+ }
32
+ }).compile('GetDocumentsParams');
33
+
34
+ module.exports = ({ db }) => async function* getDocumentsStream(params) {
35
+ params = new GetDocumentsParams(params);
36
+ const { roles } = params;
37
+ await authorize('Model.getDocumentsStream', roles);
38
+
39
+ const { model, limit, skip, sort, searchText } = params;
40
+
41
+ const Model = db.models[model];
42
+ if (Model == null) {
43
+ throw new Error(`Model ${model} not found`);
44
+ }
45
+
46
+ const parsedFilter = evaluateFilter(searchText);
47
+ const filter = parsedFilter == null ? {} : parsedFilter;
48
+
49
+ const hasSort = typeof sort === 'object' && sort != null && Object.keys(sort).length > 0;
50
+ const sortObj = hasSort ? { ...sort } : {};
51
+ if (!sortObj.hasOwnProperty('_id')) {
52
+ sortObj._id = -1;
53
+ }
54
+
55
+ const schemaPaths = {};
56
+ for (const path of Object.keys(Model.schema.paths)) {
57
+ schemaPaths[path] = {
58
+ instance: Model.schema.paths[path].instance,
59
+ path,
60
+ ref: Model.schema.paths[path].options?.ref,
61
+ required: Model.schema.paths[path].options?.required
62
+ };
63
+ }
64
+ removeSpecifiedPaths(schemaPaths, '.$*');
65
+
66
+ yield { schemaPaths };
67
+
68
+ // Start counting documents in parallel with streaming documents
69
+ const numDocsPromise = (parsedFilter == null)
70
+ ? Model.estimatedDocumentCount().exec()
71
+ : Model.countDocuments(filter).exec();
72
+
73
+ const cursor = await Model.
74
+ find(filter).
75
+ limit(limit).
76
+ skip(skip).
77
+ sort(sortObj).
78
+ batchSize(1).
79
+ cursor();
80
+
81
+ let numDocsYielded = false;
82
+ let numDocumentsPromiseResolved = false;
83
+ let numDocumentsValue;
84
+ let numDocumentsError;
85
+
86
+ try {
87
+ // Start listening for numDocsPromise resolution
88
+ numDocsPromise.then(num => {
89
+ numDocumentsPromiseResolved = true;
90
+ numDocumentsValue = num;
91
+ }).catch(err => {
92
+ numDocumentsPromiseResolved = true;
93
+ numDocumentsError = err;
94
+ });
95
+
96
+ for (let doc = await cursor.next(); doc != null; doc = await cursor.next()) {
97
+ // If numDocsPromise has resolved and not yet yielded, yield it first
98
+ if (numDocumentsPromiseResolved && !numDocsYielded) {
99
+ if (numDocumentsError) {
100
+ yield { error: numDocumentsError };
101
+ } else {
102
+ yield { numDocs: numDocumentsValue };
103
+ }
104
+ numDocsYielded = true;
105
+ }
106
+ yield { document: doc.toJSON({ virtuals: false, getters: false, transform: false }) };
107
+ }
108
+
109
+ // If numDocsPromise hasn't resolved yet, wait for it and yield
110
+ if (!numDocsYielded) {
111
+ const numDocuments = await numDocsPromise;
112
+ yield { numDocs: numDocuments };
113
+ }
114
+ } finally {
115
+ await cursor.close();
116
+ }
117
+ };
@@ -7,6 +7,7 @@ exports.dropIndex = require('./dropIndex');
7
7
  exports.exportQueryResults = require('./exportQueryResults');
8
8
  exports.getDocument = require('./getDocument');
9
9
  exports.getDocuments = require('./getDocuments');
10
+ exports.getDocumentsStream = require('./getDocumentsStream');
10
11
  exports.getIndexes = require('./getIndexes');
11
12
  exports.listModels = require('./listModels');
12
13
  exports.updateDocument = require('./updateDocument');
@@ -20,6 +20,7 @@ const actionsToRequiredRoles = {
20
20
  'Model.exportQueryResults': ['owner', 'admin', 'member', 'readonly'],
21
21
  'Model.getDocument': ['owner', 'admin', 'member', 'readonly'],
22
22
  'Model.getDocuments': ['owner', 'admin', 'member', 'readonly'],
23
+ 'Model.getDocumentsStream': ['owner', 'admin', 'member', 'readonly'],
23
24
  'Model.getIndexes': ['owner', 'admin', 'member', 'readonly'],
24
25
  'Model.listModels': ['owner', 'admin', 'member', 'readonly'],
25
26
  'Model.updateDocuments': ['owner', 'admin', 'member']
@@ -0,0 +1,51 @@
1
+ 'use strict';
2
+
3
+ const mongoose = require('mongoose');
4
+ const vm = require('vm');
5
+
6
+ const evaluate = typeof vm.evaluate === 'function' ?
7
+ vm.evaluate.bind(vm) :
8
+ (code, context) => {
9
+ const script = new vm.Script(code, { displayErrors: true });
10
+ return script.runInContext(context, { timeout: 1000 });
11
+ };
12
+
13
+ const ObjectId = new Proxy(mongoose.Types.ObjectId, {
14
+ apply(target, thisArg, argumentsList) {
15
+ return new target(...argumentsList);
16
+ }
17
+ });
18
+
19
+ module.exports = function evaluateFilter(searchText) {
20
+ if (searchText == null) {
21
+ return null;
22
+ }
23
+
24
+ const normalized = String(searchText);
25
+ if (normalized.trim().length === 0) {
26
+ return null;
27
+ }
28
+
29
+ const context = vm.createContext({
30
+ ObjectId,
31
+ Date,
32
+ Math
33
+ });
34
+
35
+ let result;
36
+ try {
37
+ result = evaluate(`(${normalized})`, context);
38
+ } catch (err) {
39
+ throw new Error(`Invalid search filter: ${err.message}`);
40
+ }
41
+
42
+ if (result == null) {
43
+ return result;
44
+ }
45
+
46
+ if (typeof result === 'object') {
47
+ return result;
48
+ }
49
+
50
+ throw new Error('Invalid search filter: must evaluate to an object');
51
+ };
package/eslint.config.js CHANGED
@@ -37,7 +37,8 @@ module.exports = defineConfig([
37
37
  __dirname: true,
38
38
  process: true,
39
39
  setTimeout: true,
40
- navigator: true
40
+ navigator: true,
41
+ TextDecoder: true
41
42
  },
42
43
  sourceType: 'commonjs'
43
44
  },
@@ -124,6 +124,9 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
124
124
  getDocuments: function getDocuments(params) {
125
125
  return client.post('', { action: 'Model.getDocuments', ...params }).then(res => res.data);
126
126
  },
127
+ getDocumentsStream: function getDocumentsStream(params) {
128
+ return client.post('', { action: 'Model.getDocumentsStream', ...params }).then(res => res.data);
129
+ },
127
130
  getIndexes: function getIndexes(params) {
128
131
  return client.post('', { action: 'Model.getIndexes', ...params }).then(res => res.data);
129
132
  },
@@ -229,6 +232,55 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
229
232
  getDocuments: function getDocuments(params) {
230
233
  return client.post('/Model/getDocuments', params).then(res => res.data);
231
234
  },
235
+ getDocumentsStream: async function* getDocumentsStream(params) {
236
+ const accessToken = window.localStorage.getItem('_mongooseStudioAccessToken') || null;
237
+ const url = window.MONGOOSE_STUDIO_CONFIG.baseURL + '/Model/getDocumentsStream?' + new URLSearchParams(params).toString();
238
+
239
+ const response = await fetch(url, {
240
+ method: 'GET',
241
+ headers: {
242
+ Authorization: `${accessToken}`,
243
+ Accept: 'text/event-stream'
244
+ }
245
+ });
246
+
247
+ if (!response.ok) {
248
+ throw new Error(`HTTP error! Status: ${response.status}`);
249
+ }
250
+
251
+ const reader = response.body.getReader();
252
+ const decoder = new TextDecoder('utf-8');
253
+ let buffer = '';
254
+
255
+ while (true) {
256
+ const { done, value } = await reader.read();
257
+ if (done) break;
258
+ buffer += decoder.decode(value, { stream: true });
259
+
260
+ let eventEnd;
261
+ while ((eventEnd = buffer.indexOf('\n\n')) !== -1) {
262
+ const eventStr = buffer.slice(0, eventEnd);
263
+ buffer = buffer.slice(eventEnd + 2);
264
+
265
+ // Parse SSE event
266
+ const lines = eventStr.split('\n');
267
+ let data = '';
268
+ for (const line of lines) {
269
+ if (line.startsWith('data:')) {
270
+ data += line.slice(5).trim();
271
+ }
272
+ }
273
+ if (data) {
274
+ try {
275
+ yield JSON.parse(data);
276
+ } catch (err) {
277
+ // If not JSON, yield as string
278
+ yield data;
279
+ }
280
+ }
281
+ }
282
+ }
283
+ },
232
284
  getIndexes: function getIndexes(params) {
233
285
  return client.post('/Model/getIndexes', params).then(res => res.data);
234
286
  },
@@ -2007,7 +2059,7 @@ appendCSS(__webpack_require__(/*! ./export-query-results.css */ "./frontend/src/
2007
2059
 
2008
2060
  module.exports = app => app.component('export-query-results', {
2009
2061
  template: template,
2010
- props: ['schemaPaths', 'filter', 'currentModel'],
2062
+ props: ['schemaPaths', 'searchText', 'currentModel'],
2011
2063
  emits: ['done'],
2012
2064
  data: () => ({
2013
2065
  shouldExport: {}
@@ -2024,8 +2076,8 @@ module.exports = app => app.component('export-query-results', {
2024
2076
  model: this.currentModel,
2025
2077
  propertiesToInclude: Object.keys(this.shouldExport).filter(key => this.shouldExport[key])
2026
2078
  };
2027
- if (this.filter) {
2028
- params.filter = this.filter;
2079
+ if (typeof this.searchText === 'string' && this.searchText.trim().length > 0) {
2080
+ params.searchText = this.searchText;
2029
2081
  }
2030
2082
  await api.Model.exportQueryResults(params);
2031
2083
 
@@ -2548,15 +2600,6 @@ module.exports = app => app.component('modal', {
2548
2600
  const api = __webpack_require__(/*! ../api */ "./frontend/src/api.js");
2549
2601
  const template = __webpack_require__(/*! ./models.html */ "./frontend/src/models/models.html");
2550
2602
  const mpath = __webpack_require__(/*! mpath */ "./node_modules/mpath/index.js");
2551
- const { BSON, EJSON } = __webpack_require__(/*! bson */ "./node_modules/bson/lib/bson.mjs");
2552
-
2553
-
2554
-
2555
- const ObjectId = new Proxy(BSON.ObjectId, {
2556
- apply(target, thisArg, argumentsList) {
2557
- return new target(...argumentsList);
2558
- }
2559
- });
2560
2603
 
2561
2604
  const appendCSS = __webpack_require__(/*! ../appendCSS */ "./frontend/src/appendCSS.js");
2562
2605
 
@@ -2575,14 +2618,13 @@ module.exports = app => app.component('models', {
2575
2618
  schemaPaths: [],
2576
2619
  filteredPaths: [],
2577
2620
  selectedPaths: [],
2578
- numDocuments: 0,
2621
+ numDocuments: null,
2579
2622
  mongoDBIndexes: [],
2580
2623
  schemaIndexes: [],
2581
2624
  status: 'loading',
2582
2625
  loadedAllDocs: false,
2583
2626
  edittingDoc: null,
2584
2627
  docEdits: null,
2585
- filter: null,
2586
2628
  selectMultiple: false,
2587
2629
  selectedDocuments: [],
2588
2630
  searchText: '',
@@ -2625,8 +2667,8 @@ module.exports = app => app.component('models', {
2625
2667
  this.query = Object.assign({}, this.$route.query); // important that this is here before the if statements
2626
2668
  if (this.$route.query?.search) {
2627
2669
  this.searchText = this.$route.query.search;
2628
- this.filter = eval(`(${this.$route.query.search})`);
2629
- this.filter = EJSON.stringify(this.filter);
2670
+ } else {
2671
+ this.searchText = '';
2630
2672
  }
2631
2673
  if (this.$route.query?.sort) {
2632
2674
  const sort = eval(`(${this.$route.query.sort})`);
@@ -2701,13 +2743,16 @@ module.exports = app => app.component('models', {
2701
2743
  const container = this.$refs.documentsList;
2702
2744
  if (container.scrollHeight - container.clientHeight - 100 < container.scrollTop) {
2703
2745
  this.status = 'loading';
2704
- const { docs } = await api.Model.getDocuments({
2746
+ const params = {
2705
2747
  model: this.currentModel,
2706
- filter: this.filter,
2707
2748
  sort: this.sortBy,
2708
2749
  skip: this.documents.length,
2709
2750
  limit
2710
- });
2751
+ };
2752
+ if (typeof this.searchText === 'string' && this.searchText.trim().length > 0) {
2753
+ params.searchText = this.searchText;
2754
+ }
2755
+ const { docs } = await api.Model.getDocuments(params);
2711
2756
  if (docs.length < limit) {
2712
2757
  this.loadedAllDocs = true;
2713
2758
  }
@@ -2733,21 +2778,16 @@ module.exports = app => app.component('models', {
2733
2778
  await this.loadMoreDocuments();
2734
2779
  },
2735
2780
  async search() {
2736
- if (this.searchText && Object.keys(this.searchText).length) {
2737
- this.filter = eval(`(${this.searchText})`);
2738
- this.filter = EJSON.stringify(this.filter);
2781
+ const hasSearch = typeof this.searchText === 'string' && this.searchText.trim().length > 0;
2782
+ if (hasSearch) {
2739
2783
  this.query.search = this.searchText;
2740
- const query = this.query;
2741
- const newUrl = this.$router.resolve({ query }).href;
2742
- this.$router.push({ query });
2743
2784
  } else {
2744
- this.filter = {};
2745
2785
  delete this.query.search;
2746
- const query = this.query;
2747
- const newUrl = this.$router.resolve({ query }).href;
2748
- this.$router.push({ query });
2749
2786
  }
2787
+ const query = this.query;
2788
+ this.$router.push({ query });
2750
2789
  this.documents = [];
2790
+ this.loadedAllDocs = false;
2751
2791
  this.status = 'loading';
2752
2792
  await this.loadMoreDocuments();
2753
2793
  this.status = 'loaded';
@@ -2768,44 +2808,91 @@ module.exports = app => app.component('models', {
2768
2808
  }
2769
2809
  },
2770
2810
  async getDocuments() {
2771
- const { docs, schemaPaths, numDocs } = await api.Model.getDocuments({
2811
+ // Clear previous data
2812
+ this.documents = [];
2813
+ this.schemaPaths = [];
2814
+ this.numDocuments = null;
2815
+ this.loadedAllDocs = false;
2816
+
2817
+ let docsCount = 0;
2818
+ let schemaPathsReceived = false;
2819
+
2820
+ // Use async generator to stream SSEs
2821
+ const params = {
2772
2822
  model: this.currentModel,
2773
- filter: this.filter,
2774
2823
  sort: this.sortBy,
2775
2824
  limit
2776
- });
2777
- this.documents = docs;
2778
- if (docs.length < limit) {
2779
- this.loadedAllDocs = true;
2825
+ };
2826
+ if (typeof this.searchText === 'string' && this.searchText.trim().length > 0) {
2827
+ params.searchText = this.searchText;
2780
2828
  }
2781
- this.schemaPaths = Object.keys(schemaPaths).sort((k1, k2) => {
2782
- if (k1 === '_id' && k2 !== '_id') {
2783
- return -1;
2829
+ for await (const event of api.Model.getDocumentsStream(params)) {
2830
+ if (event.schemaPaths && !schemaPathsReceived) {
2831
+ // Sort schemaPaths with _id first
2832
+ this.schemaPaths = Object.keys(event.schemaPaths).sort((k1, k2) => {
2833
+ if (k1 === '_id' && k2 !== '_id') {
2834
+ return -1;
2835
+ }
2836
+ if (k1 !== '_id' && k2 === '_id') {
2837
+ return 1;
2838
+ }
2839
+ return 0;
2840
+ }).map(key => event.schemaPaths[key]);
2841
+ this.shouldExport = {};
2842
+ for (const { path } of this.schemaPaths) {
2843
+ this.shouldExport[path] = true;
2844
+ }
2845
+ this.filteredPaths = [...this.schemaPaths];
2846
+ this.selectedPaths = [...this.schemaPaths];
2847
+ schemaPathsReceived = true;
2784
2848
  }
2785
- if (k1 !== '_id' && k2 === '_id') {
2786
- return 1;
2849
+ if (event.numDocs !== undefined) {
2850
+ this.numDocuments = event.numDocs;
2787
2851
  }
2788
- return 0;
2789
- }).map(key => schemaPaths[key]);
2790
- this.numDocuments = numDocs;
2852
+ if (event.document) {
2853
+ this.documents.push(event.document);
2854
+ docsCount++;
2855
+ }
2856
+ if (event.message) {
2857
+ this.status = 'loaded';
2858
+ throw new Error(event.message);
2859
+ }
2860
+ }
2791
2861
 
2792
- this.shouldExport = {};
2793
- for (const { path } of this.schemaPaths) {
2794
- this.shouldExport[path] = true;
2862
+ if (docsCount < limit) {
2863
+ this.loadedAllDocs = true;
2795
2864
  }
2796
- this.filteredPaths = [...this.schemaPaths];
2797
- this.selectedPaths = [...this.schemaPaths];
2798
2865
  },
2799
2866
  async loadMoreDocuments() {
2800
- const { docs, numDocs } = await api.Model.getDocuments({
2867
+ let docsCount = 0;
2868
+ let numDocsReceived = false;
2869
+
2870
+ // Use async generator to stream SSEs
2871
+ const params = {
2801
2872
  model: this.currentModel,
2802
- filter: this.filter,
2803
2873
  sort: this.sortBy,
2874
+ skip: this.documents.length,
2804
2875
  limit
2805
- });
2806
- this.documents = docs;
2807
- this.numDocuments = numDocs;
2808
- if (docs.length < limit) {
2876
+ };
2877
+ if (typeof this.searchText === 'string' && this.searchText.trim().length > 0) {
2878
+ params.searchText = this.searchText;
2879
+ }
2880
+ for await (const event of api.Model.getDocumentsStream(params)) {
2881
+ if (event.numDocs !== undefined && !numDocsReceived) {
2882
+ this.numDocuments = event.numDocs;
2883
+ numDocsReceived = true;
2884
+ }
2885
+ if (event.document) {
2886
+ this.documents.push(event.document);
2887
+ docsCount++;
2888
+ }
2889
+ if (event.message) {
2890
+ this.status = 'loaded';
2891
+ throw new Error(event.message);
2892
+ }
2893
+ }
2894
+
2895
+ if (docsCount < limit) {
2809
2896
  this.loadedAllDocs = true;
2810
2897
  }
2811
2898
  },
@@ -4801,7 +4888,7 @@ module.exports = ".models {\n position: relative;\n display: flex;\n flex-dir
4801
4888
  /***/ ((module) => {
4802
4889
 
4803
4890
  "use strict";
4804
- 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=\"status === 'loading'\">Loading ...</span>\n <span v-if=\"status === 'loaded'\">{{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 :filter=\"filter\"\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";
4891
+ 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";
4805
4892
 
4806
4893
  /***/ }),
4807
4894
 
@@ -14919,7 +15006,7 @@ var bson = /*#__PURE__*/Object.freeze({
14919
15006
  /***/ ((module) => {
14920
15007
 
14921
15008
  "use strict";
14922
- module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.0.119","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.0.26","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"}}');
15009
+ module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.0.120","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"}}');
14923
15010
 
14924
15011
  /***/ })
14925
15012
 
@@ -114,6 +114,9 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
114
114
  getDocuments: function getDocuments(params) {
115
115
  return client.post('', { action: 'Model.getDocuments', ...params }).then(res => res.data);
116
116
  },
117
+ getDocumentsStream: function getDocumentsStream(params) {
118
+ return client.post('', { action: 'Model.getDocumentsStream', ...params }).then(res => res.data);
119
+ },
117
120
  getIndexes: function getIndexes(params) {
118
121
  return client.post('', { action: 'Model.getIndexes', ...params }).then(res => res.data);
119
122
  },
@@ -219,6 +222,55 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
219
222
  getDocuments: function getDocuments(params) {
220
223
  return client.post('/Model/getDocuments', params).then(res => res.data);
221
224
  },
225
+ getDocumentsStream: async function* getDocumentsStream(params) {
226
+ const accessToken = window.localStorage.getItem('_mongooseStudioAccessToken') || null;
227
+ const url = window.MONGOOSE_STUDIO_CONFIG.baseURL + '/Model/getDocumentsStream?' + new URLSearchParams(params).toString();
228
+
229
+ const response = await fetch(url, {
230
+ method: 'GET',
231
+ headers: {
232
+ Authorization: `${accessToken}`,
233
+ Accept: 'text/event-stream'
234
+ }
235
+ });
236
+
237
+ if (!response.ok) {
238
+ throw new Error(`HTTP error! Status: ${response.status}`);
239
+ }
240
+
241
+ const reader = response.body.getReader();
242
+ const decoder = new TextDecoder('utf-8');
243
+ let buffer = '';
244
+
245
+ while (true) {
246
+ const { done, value } = await reader.read();
247
+ if (done) break;
248
+ buffer += decoder.decode(value, { stream: true });
249
+
250
+ let eventEnd;
251
+ while ((eventEnd = buffer.indexOf('\n\n')) !== -1) {
252
+ const eventStr = buffer.slice(0, eventEnd);
253
+ buffer = buffer.slice(eventEnd + 2);
254
+
255
+ // Parse SSE event
256
+ const lines = eventStr.split('\n');
257
+ let data = '';
258
+ for (const line of lines) {
259
+ if (line.startsWith('data:')) {
260
+ data += line.slice(5).trim();
261
+ }
262
+ }
263
+ if (data) {
264
+ try {
265
+ yield JSON.parse(data);
266
+ } catch (err) {
267
+ // If not JSON, yield as string
268
+ yield data;
269
+ }
270
+ }
271
+ }
272
+ }
273
+ },
222
274
  getIndexes: function getIndexes(params) {
223
275
  return client.post('/Model/getIndexes', params).then(res => res.data);
224
276
  },
@@ -9,7 +9,7 @@ appendCSS(require('./export-query-results.css'));
9
9
 
10
10
  module.exports = app => app.component('export-query-results', {
11
11
  template: template,
12
- props: ['schemaPaths', 'filter', 'currentModel'],
12
+ props: ['schemaPaths', 'searchText', 'currentModel'],
13
13
  emits: ['done'],
14
14
  data: () => ({
15
15
  shouldExport: {}
@@ -26,8 +26,8 @@ module.exports = app => app.component('export-query-results', {
26
26
  model: this.currentModel,
27
27
  propertiesToInclude: Object.keys(this.shouldExport).filter(key => this.shouldExport[key])
28
28
  };
29
- if (this.filter) {
30
- params.filter = this.filter;
29
+ if (typeof this.searchText === 'string' && this.searchText.trim().length > 0) {
30
+ params.searchText = this.searchText;
31
31
  }
32
32
  await api.Model.exportQueryResults(params);
33
33
 
@@ -38,8 +38,8 @@
38
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" />
39
39
  </form>
40
40
  <div>
41
- <span v-if="status === 'loading'">Loading ...</span>
42
- <span v-if="status === 'loaded'">{{numDocuments === 1 ? numDocuments+ ' document' : numDocuments + ' documents'}}</span>
41
+ <span v-if="numDocuments == null">Loading ...</span>
42
+ <span v-else-if="typeof numDocuments === 'number'">{{numDocuments === 1 ? numDocuments+ ' document' : numDocuments + ' documents'}}</span>
43
43
  </div>
44
44
  <button
45
45
  @click="shouldShowExportModal = true"
@@ -152,7 +152,7 @@
152
152
  <div class="modal-exit" @click="shouldShowExportModal = false">&times;</div>
153
153
  <export-query-results
154
154
  :schemaPaths="schemaPaths"
155
- :filter="filter"
155
+ :search-text="searchText"
156
156
  :currentModel="currentModel"
157
157
  @done="shouldShowExportModal = false">
158
158
  </export-query-results>
@@ -3,15 +3,6 @@
3
3
  const api = require('../api');
4
4
  const template = require('./models.html');
5
5
  const mpath = require('mpath');
6
- const { BSON, EJSON } = require('bson');
7
-
8
-
9
-
10
- const ObjectId = new Proxy(BSON.ObjectId, {
11
- apply(target, thisArg, argumentsList) {
12
- return new target(...argumentsList);
13
- }
14
- });
15
6
 
16
7
  const appendCSS = require('../appendCSS');
17
8
 
@@ -30,14 +21,13 @@ module.exports = app => app.component('models', {
30
21
  schemaPaths: [],
31
22
  filteredPaths: [],
32
23
  selectedPaths: [],
33
- numDocuments: 0,
24
+ numDocuments: null,
34
25
  mongoDBIndexes: [],
35
26
  schemaIndexes: [],
36
27
  status: 'loading',
37
28
  loadedAllDocs: false,
38
29
  edittingDoc: null,
39
30
  docEdits: null,
40
- filter: null,
41
31
  selectMultiple: false,
42
32
  selectedDocuments: [],
43
33
  searchText: '',
@@ -80,8 +70,8 @@ module.exports = app => app.component('models', {
80
70
  this.query = Object.assign({}, this.$route.query); // important that this is here before the if statements
81
71
  if (this.$route.query?.search) {
82
72
  this.searchText = this.$route.query.search;
83
- this.filter = eval(`(${this.$route.query.search})`);
84
- this.filter = EJSON.stringify(this.filter);
73
+ } else {
74
+ this.searchText = '';
85
75
  }
86
76
  if (this.$route.query?.sort) {
87
77
  const sort = eval(`(${this.$route.query.sort})`);
@@ -156,13 +146,16 @@ module.exports = app => app.component('models', {
156
146
  const container = this.$refs.documentsList;
157
147
  if (container.scrollHeight - container.clientHeight - 100 < container.scrollTop) {
158
148
  this.status = 'loading';
159
- const { docs } = await api.Model.getDocuments({
149
+ const params = {
160
150
  model: this.currentModel,
161
- filter: this.filter,
162
151
  sort: this.sortBy,
163
152
  skip: this.documents.length,
164
153
  limit
165
- });
154
+ };
155
+ if (typeof this.searchText === 'string' && this.searchText.trim().length > 0) {
156
+ params.searchText = this.searchText;
157
+ }
158
+ const { docs } = await api.Model.getDocuments(params);
166
159
  if (docs.length < limit) {
167
160
  this.loadedAllDocs = true;
168
161
  }
@@ -188,21 +181,16 @@ module.exports = app => app.component('models', {
188
181
  await this.loadMoreDocuments();
189
182
  },
190
183
  async search() {
191
- if (this.searchText && Object.keys(this.searchText).length) {
192
- this.filter = eval(`(${this.searchText})`);
193
- this.filter = EJSON.stringify(this.filter);
184
+ const hasSearch = typeof this.searchText === 'string' && this.searchText.trim().length > 0;
185
+ if (hasSearch) {
194
186
  this.query.search = this.searchText;
195
- const query = this.query;
196
- const newUrl = this.$router.resolve({ query }).href;
197
- this.$router.push({ query });
198
187
  } else {
199
- this.filter = {};
200
188
  delete this.query.search;
201
- const query = this.query;
202
- const newUrl = this.$router.resolve({ query }).href;
203
- this.$router.push({ query });
204
189
  }
190
+ const query = this.query;
191
+ this.$router.push({ query });
205
192
  this.documents = [];
193
+ this.loadedAllDocs = false;
206
194
  this.status = 'loading';
207
195
  await this.loadMoreDocuments();
208
196
  this.status = 'loaded';
@@ -223,44 +211,91 @@ module.exports = app => app.component('models', {
223
211
  }
224
212
  },
225
213
  async getDocuments() {
226
- const { docs, schemaPaths, numDocs } = await api.Model.getDocuments({
214
+ // Clear previous data
215
+ this.documents = [];
216
+ this.schemaPaths = [];
217
+ this.numDocuments = null;
218
+ this.loadedAllDocs = false;
219
+
220
+ let docsCount = 0;
221
+ let schemaPathsReceived = false;
222
+
223
+ // Use async generator to stream SSEs
224
+ const params = {
227
225
  model: this.currentModel,
228
- filter: this.filter,
229
226
  sort: this.sortBy,
230
227
  limit
231
- });
232
- this.documents = docs;
233
- if (docs.length < limit) {
234
- this.loadedAllDocs = true;
228
+ };
229
+ if (typeof this.searchText === 'string' && this.searchText.trim().length > 0) {
230
+ params.searchText = this.searchText;
235
231
  }
236
- this.schemaPaths = Object.keys(schemaPaths).sort((k1, k2) => {
237
- if (k1 === '_id' && k2 !== '_id') {
238
- return -1;
232
+ for await (const event of api.Model.getDocumentsStream(params)) {
233
+ if (event.schemaPaths && !schemaPathsReceived) {
234
+ // Sort schemaPaths with _id first
235
+ this.schemaPaths = Object.keys(event.schemaPaths).sort((k1, k2) => {
236
+ if (k1 === '_id' && k2 !== '_id') {
237
+ return -1;
238
+ }
239
+ if (k1 !== '_id' && k2 === '_id') {
240
+ return 1;
241
+ }
242
+ return 0;
243
+ }).map(key => event.schemaPaths[key]);
244
+ this.shouldExport = {};
245
+ for (const { path } of this.schemaPaths) {
246
+ this.shouldExport[path] = true;
247
+ }
248
+ this.filteredPaths = [...this.schemaPaths];
249
+ this.selectedPaths = [...this.schemaPaths];
250
+ schemaPathsReceived = true;
239
251
  }
240
- if (k1 !== '_id' && k2 === '_id') {
241
- return 1;
252
+ if (event.numDocs !== undefined) {
253
+ this.numDocuments = event.numDocs;
242
254
  }
243
- return 0;
244
- }).map(key => schemaPaths[key]);
245
- this.numDocuments = numDocs;
255
+ if (event.document) {
256
+ this.documents.push(event.document);
257
+ docsCount++;
258
+ }
259
+ if (event.message) {
260
+ this.status = 'loaded';
261
+ throw new Error(event.message);
262
+ }
263
+ }
246
264
 
247
- this.shouldExport = {};
248
- for (const { path } of this.schemaPaths) {
249
- this.shouldExport[path] = true;
265
+ if (docsCount < limit) {
266
+ this.loadedAllDocs = true;
250
267
  }
251
- this.filteredPaths = [...this.schemaPaths];
252
- this.selectedPaths = [...this.schemaPaths];
253
268
  },
254
269
  async loadMoreDocuments() {
255
- const { docs, numDocs } = await api.Model.getDocuments({
270
+ let docsCount = 0;
271
+ let numDocsReceived = false;
272
+
273
+ // Use async generator to stream SSEs
274
+ const params = {
256
275
  model: this.currentModel,
257
- filter: this.filter,
258
276
  sort: this.sortBy,
277
+ skip: this.documents.length,
259
278
  limit
260
- });
261
- this.documents = docs;
262
- this.numDocuments = numDocs;
263
- if (docs.length < limit) {
279
+ };
280
+ if (typeof this.searchText === 'string' && this.searchText.trim().length > 0) {
281
+ params.searchText = this.searchText;
282
+ }
283
+ for await (const event of api.Model.getDocumentsStream(params)) {
284
+ if (event.numDocs !== undefined && !numDocsReceived) {
285
+ this.numDocuments = event.numDocs;
286
+ numDocsReceived = true;
287
+ }
288
+ if (event.document) {
289
+ this.documents.push(event.document);
290
+ docsCount++;
291
+ }
292
+ if (event.message) {
293
+ this.status = 'loaded';
294
+ throw new Error(event.message);
295
+ }
296
+ }
297
+
298
+ if (docsCount < limit) {
264
299
  this.loadedAllDocs = true;
265
300
  }
266
301
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mongoosejs/studio",
3
- "version": "0.0.119",
3
+ "version": "0.0.120",
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": {
@@ -11,7 +11,7 @@
11
11
  "archetype": "0.13.1",
12
12
  "csv-stringify": "6.3.0",
13
13
  "ejson": "^2.2.3",
14
- "extrovert": "0.0.26",
14
+ "extrovert": "0.1.0",
15
15
  "marked": "15.0.12",
16
16
  "node-inspect-extracted": "3.x",
17
17
  "tailwindcss": "3.4.0",