@mongoosejs/studio 0.0.118 → 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
  },
@@ -1018,6 +1070,37 @@ module.exports = app => app.component('dashboard-document', {
1018
1070
  });
1019
1071
 
1020
1072
 
1073
+ /***/ }),
1074
+
1075
+ /***/ "./frontend/src/dashboard-result/dashboard-grid/dashboard-grid.js":
1076
+ /*!************************************************************************!*\
1077
+ !*** ./frontend/src/dashboard-result/dashboard-grid/dashboard-grid.js ***!
1078
+ \************************************************************************/
1079
+ /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
1080
+
1081
+ "use strict";
1082
+
1083
+
1084
+ const template = __webpack_require__(/*! ./dashboard-grid.html */ "./frontend/src/dashboard-result/dashboard-grid/dashboard-grid.html");
1085
+
1086
+ module.exports = app => app.component('dashboard-grid', {
1087
+ template: template,
1088
+ props: ['value'],
1089
+ computed: {
1090
+ columns() {
1091
+ const grid = this.value && this.value.$grid;
1092
+ if (!Array.isArray(grid) || grid.length === 0) {
1093
+ return 1;
1094
+ }
1095
+ return Math.max(1, ...grid.map(row => Array.isArray(row) ? row.length : 0));
1096
+ },
1097
+ gridTemplateColumns() {
1098
+ return `repeat(${this.columns}, minmax(0, 1fr))`;
1099
+ }
1100
+ }
1101
+ });
1102
+
1103
+
1021
1104
  /***/ }),
1022
1105
 
1023
1106
  /***/ "./frontend/src/dashboard-result/dashboard-map/dashboard-map.js":
@@ -1139,6 +1222,9 @@ module.exports = app => app.component('dashboard-result', {
1139
1222
  if (value.$text) {
1140
1223
  return 'dashboard-text';
1141
1224
  }
1225
+ if (value.$grid) {
1226
+ return 'dashboard-grid';
1227
+ }
1142
1228
  }
1143
1229
  }
1144
1230
  });
@@ -1973,7 +2059,7 @@ appendCSS(__webpack_require__(/*! ./export-query-results.css */ "./frontend/src/
1973
2059
 
1974
2060
  module.exports = app => app.component('export-query-results', {
1975
2061
  template: template,
1976
- props: ['schemaPaths', 'filter', 'currentModel'],
2062
+ props: ['schemaPaths', 'searchText', 'currentModel'],
1977
2063
  emits: ['done'],
1978
2064
  data: () => ({
1979
2065
  shouldExport: {}
@@ -1990,8 +2076,8 @@ module.exports = app => app.component('export-query-results', {
1990
2076
  model: this.currentModel,
1991
2077
  propertiesToInclude: Object.keys(this.shouldExport).filter(key => this.shouldExport[key])
1992
2078
  };
1993
- if (this.filter) {
1994
- params.filter = this.filter;
2079
+ if (typeof this.searchText === 'string' && this.searchText.trim().length > 0) {
2080
+ params.searchText = this.searchText;
1995
2081
  }
1996
2082
  await api.Model.exportQueryResults(params);
1997
2083
 
@@ -2514,15 +2600,6 @@ module.exports = app => app.component('modal', {
2514
2600
  const api = __webpack_require__(/*! ../api */ "./frontend/src/api.js");
2515
2601
  const template = __webpack_require__(/*! ./models.html */ "./frontend/src/models/models.html");
2516
2602
  const mpath = __webpack_require__(/*! mpath */ "./node_modules/mpath/index.js");
2517
- const { BSON, EJSON } = __webpack_require__(/*! bson */ "./node_modules/bson/lib/bson.mjs");
2518
-
2519
-
2520
-
2521
- const ObjectId = new Proxy(BSON.ObjectId, {
2522
- apply(target, thisArg, argumentsList) {
2523
- return new target(...argumentsList);
2524
- }
2525
- });
2526
2603
 
2527
2604
  const appendCSS = __webpack_require__(/*! ../appendCSS */ "./frontend/src/appendCSS.js");
2528
2605
 
@@ -2541,14 +2618,13 @@ module.exports = app => app.component('models', {
2541
2618
  schemaPaths: [],
2542
2619
  filteredPaths: [],
2543
2620
  selectedPaths: [],
2544
- numDocuments: 0,
2621
+ numDocuments: null,
2545
2622
  mongoDBIndexes: [],
2546
2623
  schemaIndexes: [],
2547
2624
  status: 'loading',
2548
2625
  loadedAllDocs: false,
2549
2626
  edittingDoc: null,
2550
2627
  docEdits: null,
2551
- filter: null,
2552
2628
  selectMultiple: false,
2553
2629
  selectedDocuments: [],
2554
2630
  searchText: '',
@@ -2591,8 +2667,8 @@ module.exports = app => app.component('models', {
2591
2667
  this.query = Object.assign({}, this.$route.query); // important that this is here before the if statements
2592
2668
  if (this.$route.query?.search) {
2593
2669
  this.searchText = this.$route.query.search;
2594
- this.filter = eval(`(${this.$route.query.search})`);
2595
- this.filter = EJSON.stringify(this.filter);
2670
+ } else {
2671
+ this.searchText = '';
2596
2672
  }
2597
2673
  if (this.$route.query?.sort) {
2598
2674
  const sort = eval(`(${this.$route.query.sort})`);
@@ -2667,13 +2743,16 @@ module.exports = app => app.component('models', {
2667
2743
  const container = this.$refs.documentsList;
2668
2744
  if (container.scrollHeight - container.clientHeight - 100 < container.scrollTop) {
2669
2745
  this.status = 'loading';
2670
- const { docs } = await api.Model.getDocuments({
2746
+ const params = {
2671
2747
  model: this.currentModel,
2672
- filter: this.filter,
2673
2748
  sort: this.sortBy,
2674
2749
  skip: this.documents.length,
2675
2750
  limit
2676
- });
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);
2677
2756
  if (docs.length < limit) {
2678
2757
  this.loadedAllDocs = true;
2679
2758
  }
@@ -2699,21 +2778,16 @@ module.exports = app => app.component('models', {
2699
2778
  await this.loadMoreDocuments();
2700
2779
  },
2701
2780
  async search() {
2702
- if (this.searchText && Object.keys(this.searchText).length) {
2703
- this.filter = eval(`(${this.searchText})`);
2704
- this.filter = EJSON.stringify(this.filter);
2781
+ const hasSearch = typeof this.searchText === 'string' && this.searchText.trim().length > 0;
2782
+ if (hasSearch) {
2705
2783
  this.query.search = this.searchText;
2706
- const query = this.query;
2707
- const newUrl = this.$router.resolve({ query }).href;
2708
- this.$router.push({ query });
2709
2784
  } else {
2710
- this.filter = {};
2711
2785
  delete this.query.search;
2712
- const query = this.query;
2713
- const newUrl = this.$router.resolve({ query }).href;
2714
- this.$router.push({ query });
2715
2786
  }
2787
+ const query = this.query;
2788
+ this.$router.push({ query });
2716
2789
  this.documents = [];
2790
+ this.loadedAllDocs = false;
2717
2791
  this.status = 'loading';
2718
2792
  await this.loadMoreDocuments();
2719
2793
  this.status = 'loaded';
@@ -2734,44 +2808,91 @@ module.exports = app => app.component('models', {
2734
2808
  }
2735
2809
  },
2736
2810
  async getDocuments() {
2737
- 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 = {
2738
2822
  model: this.currentModel,
2739
- filter: this.filter,
2740
2823
  sort: this.sortBy,
2741
2824
  limit
2742
- });
2743
- this.documents = docs;
2744
- if (docs.length < limit) {
2745
- this.loadedAllDocs = true;
2825
+ };
2826
+ if (typeof this.searchText === 'string' && this.searchText.trim().length > 0) {
2827
+ params.searchText = this.searchText;
2746
2828
  }
2747
- this.schemaPaths = Object.keys(schemaPaths).sort((k1, k2) => {
2748
- if (k1 === '_id' && k2 !== '_id') {
2749
- 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;
2750
2848
  }
2751
- if (k1 !== '_id' && k2 === '_id') {
2752
- return 1;
2849
+ if (event.numDocs !== undefined) {
2850
+ this.numDocuments = event.numDocs;
2753
2851
  }
2754
- return 0;
2755
- }).map(key => schemaPaths[key]);
2756
- 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
+ }
2757
2861
 
2758
- this.shouldExport = {};
2759
- for (const { path } of this.schemaPaths) {
2760
- this.shouldExport[path] = true;
2862
+ if (docsCount < limit) {
2863
+ this.loadedAllDocs = true;
2761
2864
  }
2762
- this.filteredPaths = [...this.schemaPaths];
2763
- this.selectedPaths = [...this.schemaPaths];
2764
2865
  },
2765
2866
  async loadMoreDocuments() {
2766
- 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 = {
2767
2872
  model: this.currentModel,
2768
- filter: this.filter,
2769
2873
  sort: this.sortBy,
2874
+ skip: this.documents.length,
2770
2875
  limit
2771
- });
2772
- this.documents = docs;
2773
- this.numDocuments = numDocs;
2774
- 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) {
2775
2896
  this.loadedAllDocs = true;
2776
2897
  }
2777
2898
  },
@@ -3439,6 +3560,9 @@ var map = {
3439
3560
  "./dashboard-result/dashboard-document/dashboard-document": "./frontend/src/dashboard-result/dashboard-document/dashboard-document.js",
3440
3561
  "./dashboard-result/dashboard-document/dashboard-document.html": "./frontend/src/dashboard-result/dashboard-document/dashboard-document.html",
3441
3562
  "./dashboard-result/dashboard-document/dashboard-document.js": "./frontend/src/dashboard-result/dashboard-document/dashboard-document.js",
3563
+ "./dashboard-result/dashboard-grid/dashboard-grid": "./frontend/src/dashboard-result/dashboard-grid/dashboard-grid.js",
3564
+ "./dashboard-result/dashboard-grid/dashboard-grid.html": "./frontend/src/dashboard-result/dashboard-grid/dashboard-grid.html",
3565
+ "./dashboard-result/dashboard-grid/dashboard-grid.js": "./frontend/src/dashboard-result/dashboard-grid/dashboard-grid.js",
3442
3566
  "./dashboard-result/dashboard-map/dashboard-map": "./frontend/src/dashboard-result/dashboard-map/dashboard-map.js",
3443
3567
  "./dashboard-result/dashboard-map/dashboard-map.html": "./frontend/src/dashboard-result/dashboard-map/dashboard-map.html",
3444
3568
  "./dashboard-result/dashboard-map/dashboard-map.js": "./frontend/src/dashboard-result/dashboard-map/dashboard-map.js",
@@ -4306,6 +4430,17 @@ module.exports = "<div class=\"py-2\">\n <div v-if=\"header\" class=\"border-b
4306
4430
 
4307
4431
  /***/ }),
4308
4432
 
4433
+ /***/ "./frontend/src/dashboard-result/dashboard-grid/dashboard-grid.html":
4434
+ /*!**************************************************************************!*\
4435
+ !*** ./frontend/src/dashboard-result/dashboard-grid/dashboard-grid.html ***!
4436
+ \**************************************************************************/
4437
+ /***/ ((module) => {
4438
+
4439
+ "use strict";
4440
+ module.exports = "<div class=\"grid gap-2\" :style=\"{ gridTemplateColumns: gridTemplateColumns }\">\n <template v-for=\"(row, rowIndex) in value.$grid\" :key=\"rowIndex\">\n <dashboard-result\n v-for=\"(cell, colIndex) in row\"\n :key=\"rowIndex + '-' + colIndex\"\n :result=\"cell\">\n </dashboard-result>\n </template>\n</div>\n";
4441
+
4442
+ /***/ }),
4443
+
4309
4444
  /***/ "./frontend/src/dashboard-result/dashboard-map/dashboard-map.html":
4310
4445
  /*!************************************************************************!*\
4311
4446
  !*** ./frontend/src/dashboard-result/dashboard-map/dashboard-map.html ***!
@@ -4753,7 +4888,7 @@ module.exports = ".models {\n position: relative;\n display: flex;\n flex-dir
4753
4888
  /***/ ((module) => {
4754
4889
 
4755
4890
  "use strict";
4756
- 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";
4757
4892
 
4758
4893
  /***/ }),
4759
4894
 
@@ -14871,7 +15006,7 @@ var bson = /*#__PURE__*/Object.freeze({
14871
15006
  /***/ ((module) => {
14872
15007
 
14873
15008
  "use strict";
14874
- module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.0.118","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"}}');
14875
15010
 
14876
15011
  /***/ })
14877
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
  },
@@ -0,0 +1,9 @@
1
+ <div class="grid gap-2" :style="{ gridTemplateColumns: gridTemplateColumns }">
2
+ <template v-for="(row, rowIndex) in value.$grid" :key="rowIndex">
3
+ <dashboard-result
4
+ v-for="(cell, colIndex) in row"
5
+ :key="rowIndex + '-' + colIndex"
6
+ :result="cell">
7
+ </dashboard-result>
8
+ </template>
9
+ </div>
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+
3
+ const template = require('./dashboard-grid.html');
4
+
5
+ module.exports = app => app.component('dashboard-grid', {
6
+ template: template,
7
+ props: ['value'],
8
+ computed: {
9
+ columns() {
10
+ const grid = this.value && this.value.$grid;
11
+ if (!Array.isArray(grid) || grid.length === 0) {
12
+ return 1;
13
+ }
14
+ return Math.max(1, ...grid.map(row => Array.isArray(row) ? row.length : 0));
15
+ },
16
+ gridTemplateColumns() {
17
+ return `repeat(${this.columns}, minmax(0, 1fr))`;
18
+ }
19
+ }
20
+ });
@@ -31,6 +31,9 @@ module.exports = app => app.component('dashboard-result', {
31
31
  if (value.$text) {
32
32
  return 'dashboard-text';
33
33
  }
34
+ if (value.$grid) {
35
+ return 'dashboard-grid';
36
+ }
34
37
  }
35
38
  }
36
39
  });
@@ -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.118",
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",