@mongoosejs/studio 0.3.2 → 0.3.4

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.
@@ -2,6 +2,7 @@
2
2
 
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
+ const MongooseStudioChartColors = require('../../constants/mongooseStudioChartColors');
5
6
  const mongoose = require('mongoose');
6
7
  const vm = require('vm');
7
8
 
@@ -43,10 +44,6 @@ module.exports = ({ db, studioConnection }) => async function executeScript(para
43
44
  if (!db.Types) {
44
45
  db.Types = mongoose.Types;
45
46
  }
46
- const MongooseStudioChartColors = [
47
- '#4e79a7', '#e15759', '#59a14f', '#9c755f',
48
- '#f28e2b', '#b07aa1', '#76b7b2', '#edc948'
49
- ];
50
47
  const sandbox = { db, mongoose, console: {}, ObjectId: mongoose.Types.ObjectId, MongooseStudioChartColors };
51
48
 
52
49
  // Capture console logs
@@ -4,6 +4,8 @@ const Archetype = require('archetype');
4
4
  const removeSpecifiedPaths = require('../../helpers/removeSpecifiedPaths');
5
5
  const evaluateFilter = require('../../helpers/evaluateFilter');
6
6
  const getRefFromSchemaType = require('../../helpers/getRefFromSchemaType');
7
+ const getSuggestedProjection = require('../../helpers/getSuggestedProjection');
8
+ const parseFieldsParam = require('../../helpers/parseFieldsParam');
7
9
  const authorize = require('../../authorize');
8
10
 
9
11
  const GetDocumentsParams = new Archetype({
@@ -30,6 +32,9 @@ const GetDocumentsParams = new Archetype({
30
32
  sortDirection: {
31
33
  $type: 'number'
32
34
  },
35
+ fields: {
36
+ $type: 'string'
37
+ },
33
38
  roles: {
34
39
  $type: ['string']
35
40
  }
@@ -40,7 +45,7 @@ module.exports = ({ db }) => async function getDocuments(params) {
40
45
  const { roles } = params;
41
46
  await authorize('Model.getDocuments', roles);
42
47
 
43
- const { model, limit, skip, sortKey, sortDirection, searchText } = params;
48
+ const { model, limit, skip, sortKey, sortDirection, searchText, fields } = params;
44
49
 
45
50
  const Model = db.models[model];
46
51
  if (Model == null) {
@@ -61,12 +66,13 @@ module.exports = ({ db }) => async function getDocuments(params) {
61
66
  if (!sortObj.hasOwnProperty('_id')) {
62
67
  sortObj._id = -1;
63
68
  }
64
- const cursor = await Model.
65
- find(filter).
66
- limit(limit).
67
- skip(skip).
68
- sort(sortObj).
69
- cursor();
69
+
70
+ let query = Model.find(filter).limit(limit).skip(skip).sort(sortObj);
71
+ const projection = parseFieldsParam(fields);
72
+ if (projection != null) {
73
+ query = query.select(projection);
74
+ }
75
+ const cursor = await query.cursor();
70
76
  const docs = [];
71
77
  for (let doc = await cursor.next(); doc != null; doc = await cursor.next()) {
72
78
  docs.push(doc);
@@ -101,9 +107,12 @@ module.exports = ({ db }) => async function getDocuments(params) {
101
107
  await Model.estimatedDocumentCount() :
102
108
  await Model.countDocuments(filter);
103
109
 
110
+ const suggestedFields = getSuggestedProjection(Model);
111
+
104
112
  return {
105
113
  docs: docs.map(doc => doc.toJSON({ virtuals: false, getters: false, transform: false })),
106
114
  schemaPaths,
115
+ suggestedFields,
107
116
  numDocs: numDocuments
108
117
  };
109
118
  };
@@ -4,6 +4,8 @@ const Archetype = require('archetype');
4
4
  const removeSpecifiedPaths = require('../../helpers/removeSpecifiedPaths');
5
5
  const evaluateFilter = require('../../helpers/evaluateFilter');
6
6
  const getRefFromSchemaType = require('../../helpers/getRefFromSchemaType');
7
+ const getSuggestedProjection = require('../../helpers/getSuggestedProjection');
8
+ const parseFieldsParam = require('../../helpers/parseFieldsParam');
7
9
  const authorize = require('../../authorize');
8
10
 
9
11
  const GetDocumentsParams = new Archetype({
@@ -30,6 +32,9 @@ const GetDocumentsParams = new Archetype({
30
32
  sortDirection: {
31
33
  $type: 'number'
32
34
  },
35
+ fields: {
36
+ $type: 'string'
37
+ },
33
38
  roles: {
34
39
  $type: ['string']
35
40
  }
@@ -40,7 +45,7 @@ module.exports = ({ db }) => async function* getDocumentsStream(params) {
40
45
  const { roles } = params;
41
46
  await authorize('Model.getDocumentsStream', roles);
42
47
 
43
- const { model, limit, skip, sortKey, sortDirection, searchText } = params;
48
+ const { model, limit, skip, sortKey, sortDirection, searchText, fields } = params;
44
49
 
45
50
  const Model = db.models[model];
46
51
  if (Model == null) {
@@ -62,6 +67,12 @@ module.exports = ({ db }) => async function* getDocumentsStream(params) {
62
67
  sortObj._id = -1;
63
68
  }
64
69
 
70
+ let query = Model.find(filter).limit(limit).skip(skip).sort(sortObj).batchSize(1);
71
+ const projection = parseFieldsParam(fields);
72
+ if (projection != null) {
73
+ query = query.select(projection);
74
+ }
75
+
65
76
  const schemaPaths = {};
66
77
  for (const path of Object.keys(Model.schema.paths)) {
67
78
  const schemaType = Model.schema.paths[path];
@@ -87,20 +98,16 @@ module.exports = ({ db }) => async function* getDocumentsStream(params) {
87
98
  }
88
99
  removeSpecifiedPaths(schemaPaths, '.$*');
89
100
 
90
- yield { schemaPaths };
101
+ const suggestedFields = getSuggestedProjection(Model);
102
+
103
+ yield { schemaPaths, suggestedFields };
91
104
 
92
105
  // Start counting documents in parallel with streaming documents
93
106
  const numDocsPromise = (parsedFilter == null)
94
107
  ? Model.estimatedDocumentCount().exec()
95
108
  : Model.countDocuments(filter).exec();
96
109
 
97
- const cursor = await Model.
98
- find(filter).
99
- limit(limit).
100
- skip(skip).
101
- sort(sortObj).
102
- batchSize(1).
103
- cursor();
110
+ const cursor = await query.cursor();
104
111
 
105
112
  let numDocsYielded = false;
106
113
  let numDocumentsPromiseResolved = false;
@@ -0,0 +1,33 @@
1
+ 'use strict';
2
+
3
+ const Archetype = require('archetype');
4
+ const getSuggestedProjection = require('../../helpers/getSuggestedProjection');
5
+ const authorize = require('../../authorize');
6
+
7
+ const GetSuggestedProjectionParams = new Archetype({
8
+ model: {
9
+ $type: 'string',
10
+ $required: true
11
+ },
12
+ roles: {
13
+ $type: ['string']
14
+ }
15
+ }).compile('GetSuggestedProjectionParams');
16
+
17
+ module.exports = ({ db }) => async function getSuggestedProjectionAction(params) {
18
+ params = new GetSuggestedProjectionParams(params);
19
+ const { roles } = params;
20
+ await authorize('Model.getSuggestedProjection', roles);
21
+
22
+ const { model } = params;
23
+
24
+ const Model = db.models[model];
25
+ if (Model == null) {
26
+ throw new Error(`Model ${model} not found`);
27
+ }
28
+
29
+ // Default columns: first N schema paths (no scoring).
30
+ const suggestedFields = getSuggestedProjection(Model);
31
+
32
+ return { suggestedFields };
33
+ };
@@ -11,6 +11,7 @@ exports.exportQueryResults = require('./exportQueryResults');
11
11
  exports.getDocument = require('./getDocument');
12
12
  exports.getDocuments = require('./getDocuments');
13
13
  exports.getDocumentsStream = require('./getDocumentsStream');
14
+ exports.getSuggestedProjection = require('./getSuggestedProjection');
14
15
  exports.getCollectionInfo = require('./getCollectionInfo');
15
16
  exports.getIndexes = require('./getIndexes');
16
17
  exports.getEstimatedDocumentCounts = require('./getEstimatedDocumentCounts');
@@ -22,6 +22,7 @@ const actionsToRequiredRoles = {
22
22
  'Model.getDocument': ['owner', 'admin', 'member', 'readonly'],
23
23
  'Model.getDocuments': ['owner', 'admin', 'member', 'readonly'],
24
24
  'Model.getDocumentsStream': ['owner', 'admin', 'member', 'readonly'],
25
+ 'Model.getSuggestedProjection': ['owner', 'admin', 'member', 'readonly'],
25
26
  'Model.getEstimatedDocumentCounts': ['owner', 'admin', 'member', 'readonly'],
26
27
  'Model.getIndexes': ['owner', 'admin', 'member', 'readonly'],
27
28
  'Model.listModels': ['owner', 'admin', 'member', 'readonly'],
@@ -0,0 +1,6 @@
1
+ 'use strict';
2
+
3
+ module.exports = Object.freeze([
4
+ '#4e79a7', '#e15759', '#59a14f', '#9c755f',
5
+ '#f28e2b', '#b07aa1', '#76b7b2', '#edc948'
6
+ ]);
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ const MongooseStudioChartColors = require('../constants/mongooseStudioChartColors');
3
4
  const mongoose = require('mongoose');
4
5
  const vm = require('vm');
5
6
 
@@ -18,7 +19,12 @@ const dashboardSchema = new mongoose.Schema({
18
19
  });
19
20
 
20
21
  dashboardSchema.methods.evaluate = async function evaluate() {
21
- const context = vm.createContext({ db: this.constructor.db, setTimeout, ObjectId: mongoose.Types.ObjectId });
22
+ const context = vm.createContext({
23
+ db: this.constructor.db,
24
+ setTimeout,
25
+ ObjectId: mongoose.Types.ObjectId,
26
+ MongooseStudioChartColors
27
+ });
22
28
  let result = null;
23
29
  result = await vm.runInContext(formatFunction(this.code), context);
24
30
  if (result.$document?.constructor?.modelName) {
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+
3
+ /** Max number of paths to use for the default table projection. */
4
+ const DEFAULT_SUGGESTED_LIMIT = 6;
5
+
6
+ /**
7
+ * Default projection for the models table: the first N schema paths (definition order),
8
+ * excluding Mongoose internals. No scoring — stable and predictable.
9
+ *
10
+ * @param {import('mongoose').Model} Model - Mongoose model
11
+ * @param {{ limit?: number }} options - max paths returned
12
+ * @returns {string[]} Path names in schema order
13
+ */
14
+ function getSuggestedProjection(Model, options = {}) {
15
+ const limit = typeof options.limit === 'number' && options.limit > 0
16
+ ? options.limit
17
+ : DEFAULT_SUGGESTED_LIMIT;
18
+
19
+ const pathNames = Object.keys(Model.schema.paths).filter(key =>
20
+ !key.includes('.$*') &&
21
+ key !== '__v'
22
+ );
23
+
24
+ pathNames.sort((k1, k2) => {
25
+ if (k1 === '_id' && k2 !== '_id') {
26
+ return -1;
27
+ }
28
+ if (k1 !== '_id' && k2 === '_id') {
29
+ return 1;
30
+ }
31
+ return 0;
32
+ });
33
+
34
+ return pathNames.slice(0, limit);
35
+ }
36
+
37
+ module.exports = getSuggestedProjection;
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Parse the `fields` request param for Model.getDocuments / getDocumentsStream.
5
+ * Expects JSON: either `["a","b"]` (inclusion list) or `{"a":1,"b":1}` (Mongo projection).
6
+ *
7
+ * @param {string|undefined} fields
8
+ * @returns {string|object|null} Argument suitable for Query.select(), or null when unset/invalid.
9
+ */
10
+ function parseFieldsParam(fields) {
11
+ if (fields == null || typeof fields !== 'string') {
12
+ return null;
13
+ }
14
+ const trimmed = fields.trim();
15
+ if (!trimmed) {
16
+ return null;
17
+ }
18
+
19
+ let parsed;
20
+ try {
21
+ parsed = JSON.parse(trimmed);
22
+ } catch (e) {
23
+ return null;
24
+ }
25
+
26
+ if (Array.isArray(parsed)) {
27
+ const list = parsed.map(x => String(x).trim()).filter(Boolean);
28
+ return list.length > 0 ? list.join(' ') : null;
29
+ }
30
+ if (parsed != null && typeof parsed === 'object' && !Array.isArray(parsed)) {
31
+ return Object.keys(parsed).length > 0 ? parsed : null;
32
+ }
33
+ return null;
34
+ }
35
+
36
+ module.exports = parseFieldsParam;