@mongoosejs/studio 0.0.119 → 0.0.121
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/backend/actions/Model/exportQueryResults.js +7 -8
- package/backend/actions/Model/getDocuments.js +8 -13
- package/backend/actions/Model/getDocumentsStream.js +117 -0
- package/backend/actions/Model/index.js +1 -0
- package/backend/authorize.js +1 -0
- package/backend/helpers/evaluateFilter.js +51 -0
- package/eslint.config.js +2 -1
- package/frontend/public/app.js +143 -56
- package/frontend/src/api.js +52 -0
- package/frontend/src/export-query-results/export-query-results.js +3 -3
- package/frontend/src/models/models.html +3 -3
- package/frontend/src/models/models.js +86 -51
- package/package.json +2 -2
|
@@ -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
|
-
|
|
14
|
-
$type:
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
43
|
+
const parsedFilter = evaluateFilter(searchText);
|
|
44
|
+
const filter = parsedFilter == null ? {} : parsedFilter;
|
|
46
45
|
|
|
47
46
|
const docs = await Model.
|
|
48
|
-
find(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
|
|
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
|
-
|
|
24
|
-
$type:
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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
|
|
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 =
|
|
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');
|
package/backend/authorize.js
CHANGED
|
@@ -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
package/frontend/public/app.js
CHANGED
|
@@ -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', '
|
|
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.
|
|
2028
|
-
params.
|
|
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:
|
|
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
|
-
|
|
2629
|
-
this.
|
|
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
|
|
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
|
-
|
|
2737
|
-
|
|
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
|
-
|
|
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.
|
|
2778
|
-
|
|
2779
|
-
this.loadedAllDocs = true;
|
|
2825
|
+
};
|
|
2826
|
+
if (typeof this.searchText === 'string' && this.searchText.trim().length > 0) {
|
|
2827
|
+
params.searchText = this.searchText;
|
|
2780
2828
|
}
|
|
2781
|
-
|
|
2782
|
-
if (
|
|
2783
|
-
|
|
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 (
|
|
2786
|
-
|
|
2849
|
+
if (event.numDocs !== undefined) {
|
|
2850
|
+
this.numDocuments = event.numDocs;
|
|
2787
2851
|
}
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
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
|
-
|
|
2793
|
-
|
|
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
|
-
|
|
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.
|
|
2807
|
-
|
|
2808
|
-
|
|
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\">×</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\">×</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];\">×</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;\">×</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;\">×</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;\">×</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\">×</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\">×</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];\">×</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;\">×</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;\">×</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;\">×</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.
|
|
15009
|
+
module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.0.121","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
|
|
package/frontend/src/api.js
CHANGED
|
@@ -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', '
|
|
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.
|
|
30
|
-
params.
|
|
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="
|
|
42
|
-
<span v-if="
|
|
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">×</div>
|
|
153
153
|
<export-query-results
|
|
154
154
|
:schemaPaths="schemaPaths"
|
|
155
|
-
:
|
|
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:
|
|
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
|
-
|
|
84
|
-
this.
|
|
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
|
|
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
|
-
|
|
192
|
-
|
|
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
|
-
|
|
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.
|
|
233
|
-
|
|
234
|
-
this.loadedAllDocs = true;
|
|
228
|
+
};
|
|
229
|
+
if (typeof this.searchText === 'string' && this.searchText.trim().length > 0) {
|
|
230
|
+
params.searchText = this.searchText;
|
|
235
231
|
}
|
|
236
|
-
|
|
237
|
-
if (
|
|
238
|
-
|
|
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 (
|
|
241
|
-
|
|
252
|
+
if (event.numDocs !== undefined) {
|
|
253
|
+
this.numDocuments = event.numDocs;
|
|
242
254
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
248
|
-
|
|
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
|
-
|
|
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.
|
|
262
|
-
|
|
263
|
-
|
|
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.
|
|
3
|
+
"version": "0.0.121",
|
|
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
|
|
14
|
+
"extrovert": "^0.1.0",
|
|
15
15
|
"marked": "15.0.12",
|
|
16
16
|
"node-inspect-extracted": "3.x",
|
|
17
17
|
"tailwindcss": "3.4.0",
|