@mongoosejs/studio 0.5.4 → 0.5.6

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.
Files changed (36) hide show
  1. package/backend/actions/Dashboard/createDashboard.js +12 -2
  2. package/backend/actions/Dashboard/getDashboard.js +6 -2
  3. package/backend/actions/Dashboard/getDashboards.js +3 -1
  4. package/backend/actions/Dashboard/updateDashboard.js +19 -6
  5. package/backend/actions/Model/analyzeSchema.js +202 -0
  6. package/backend/actions/Model/index.js +2 -0
  7. package/backend/actions/Model/validateDocument.js +35 -0
  8. package/backend/authorize.js +2 -0
  9. package/backend/db/dashboardSchema.js +31 -0
  10. package/backend/helpers/validateDocumentWithTimeout.js +39 -0
  11. package/backend/netlify.js +1 -0
  12. package/backend/next.js +1 -0
  13. package/express.js +1 -0
  14. package/frontend/public/app.js +859 -102
  15. package/frontend/public/images/pin-off.svg +1 -0
  16. package/frontend/public/images/pin.svg +1 -0
  17. package/frontend/public/tw.css +249 -28
  18. package/frontend/src/api.js +12 -0
  19. package/frontend/src/array-utils.js +60 -2
  20. package/frontend/src/dashboards/dashboards.html +52 -4
  21. package/frontend/src/dashboards/dashboards.js +96 -1
  22. package/frontend/src/detail-array/detail-array.html +98 -14
  23. package/frontend/src/detail-array/detail-array.js +73 -3
  24. package/frontend/src/document/document.html +51 -0
  25. package/frontend/src/document/document.js +13 -0
  26. package/frontend/src/document-details/document-details.html +10 -3
  27. package/frontend/src/document-details/document-details.js +28 -0
  28. package/frontend/src/document-details/document-property/document-property.html +36 -44
  29. package/frontend/src/document-details/document-property/document-property.js +78 -14
  30. package/frontend/src/edit-array/edit-array.html +143 -8
  31. package/frontend/src/edit-array/edit-array.js +104 -2
  32. package/frontend/src/models/analyze-schema-modal/analyze-schema-modal.html +169 -0
  33. package/frontend/src/models/analyze-schema-modal/analyze-schema-modal.js +149 -0
  34. package/frontend/src/models/models.html +25 -13
  35. package/frontend/src/models/models.js +5 -13
  36. package/package.json +2 -1
@@ -2,6 +2,7 @@
2
2
 
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
+ const mongoose = require('mongoose');
5
6
 
6
7
  const CreateDashboardParams = new Archetype({
7
8
  title: {
@@ -12,18 +13,27 @@ const CreateDashboardParams = new Archetype({
12
13
  $type: 'string',
13
14
  $required: true
14
15
  },
16
+ initiatedById: {
17
+ $type: mongoose.Types.ObjectId,
18
+ $required: false
19
+ },
15
20
  roles: {
16
21
  $type: ['string']
17
22
  }
18
23
  }).compile('CreateDashboardParams');
19
24
 
20
25
  module.exports = ({ studioConnection }) => async function createDashboard(params) {
21
- const { title, code, roles } = new CreateDashboardParams(params);
26
+ const { title, code, initiatedById, roles } = new CreateDashboardParams(params);
22
27
  const Dashboard = studioConnection.model('__Studio_Dashboard');
23
28
 
24
29
  await authorize('Dashboard.createDashboard', roles);
25
30
 
26
- const dashboard = await Dashboard.create({ title, code });
31
+ const dashboard = await Dashboard.create({
32
+ title,
33
+ code,
34
+ createdById: initiatedById,
35
+ createdBy: params.initiatedBy
36
+ });
27
37
 
28
38
  return { dashboard };
29
39
  };
@@ -51,6 +51,7 @@ module.exports = ({ db, studioConnection, options }) => async function getDashbo
51
51
  return {};
52
52
  }
53
53
  return completeDashboardEvaluate(
54
+ Dashboard,
54
55
  DashboardResult,
55
56
  dashboardResult._id,
56
57
  null,
@@ -73,6 +74,7 @@ module.exports = ({ db, studioConnection, options }) => async function getDashbo
73
74
  return {};
74
75
  }
75
76
  return completeDashboardEvaluate(
77
+ Dashboard,
76
78
  DashboardResult,
77
79
  dashboardResult._id,
78
80
  result,
@@ -97,13 +99,15 @@ module.exports = ({ db, studioConnection, options }) => async function getDashbo
97
99
  }
98
100
  };
99
101
 
100
- async function completeDashboardEvaluate(DashboardResult, dashboardResultId, result, error, status) {
102
+ async function completeDashboardEvaluate(Dashboard, DashboardResult, dashboardResultId, result, error, status) {
101
103
  const dashboardResult = await DashboardResult.findById(dashboardResultId).orFail();
102
- dashboardResult.finishedEvaluatingAt = new Date();
104
+ const finishedEvaluatingAt = new Date();
105
+ dashboardResult.finishedEvaluatingAt = finishedEvaluatingAt;
103
106
  dashboardResult.result = result;
104
107
  dashboardResult.error = error;
105
108
  dashboardResult.status = status;
106
109
  await dashboardResult.save();
110
+ await Dashboard.updateOne({ _id: dashboardResult.dashboardId }, { $set: { lastEvaluatedAt: finishedEvaluatingAt } });
107
111
  return { dashboardResult };
108
112
  }
109
113
 
@@ -15,7 +15,9 @@ module.exports = ({ studioConnection }) => async function getDashboards(params)
15
15
 
16
16
  await authorize('Dashboard.getDashboards', roles);
17
17
 
18
- const dashboards = await Dashboard.find();
18
+ const dashboards = await Dashboard
19
+ .find()
20
+ .sort({ isPinned: -1, createdAt: -1, _id: -1 });
19
21
 
20
22
  return { dashboards };
21
23
  };
@@ -9,8 +9,7 @@ const UpdateDashboardParams = new Archetype({
9
9
  $required: true
10
10
  },
11
11
  code: {
12
- $type: 'string',
13
- $required: true
12
+ $type: 'string'
14
13
  },
15
14
  title: {
16
15
  $type: 'string'
@@ -18,19 +17,26 @@ const UpdateDashboardParams = new Archetype({
18
17
  description: {
19
18
  $type: 'string'
20
19
  },
20
+ isPinned: {
21
+ $type: 'boolean'
22
+ },
21
23
  roles: {
22
24
  $type: ['string']
23
25
  }
24
26
  }).compile('UpdateDashboardParams');
25
27
 
26
28
  module.exports = ({ studioConnection }) => async function updateDashboard(params) {
27
- const { dashboardId, code, title, description, roles, evaluate } = new UpdateDashboardParams(params);
29
+ const { dashboardId, code, title, description, isPinned, roles, evaluate } = new UpdateDashboardParams(params);
28
30
 
29
31
  const Dashboard = studioConnection.models['__Studio_Dashboard'];
30
32
 
31
33
  await authorize('Dashboard.updateDashboard', roles);
32
34
 
33
- const updateObj = { code };
35
+ const updateObj = {};
36
+
37
+ if (code != null) {
38
+ updateObj.code = code;
39
+ }
34
40
 
35
41
  if (title != null) {
36
42
  updateObj.title = title;
@@ -40,8 +46,15 @@ module.exports = ({ studioConnection }) => async function updateDashboard(params
40
46
  updateObj.description = description;
41
47
  }
42
48
 
43
- const doc = await Dashboard.
44
- findByIdAndUpdate(dashboardId, updateObj, { sanitizeFilter: true, returnDocument: 'after', overwriteImmutable: true });
49
+ if (isPinned != null) {
50
+ updateObj.isPinned = isPinned;
51
+ }
52
+
53
+ const doc = await Dashboard.findByIdAndUpdate(
54
+ dashboardId,
55
+ updateObj,
56
+ { sanitizeFilter: true, returnDocument: 'after' }
57
+ );
45
58
 
46
59
  return { doc };
47
60
  };
@@ -0,0 +1,202 @@
1
+ 'use strict';
2
+
3
+ const Archetype = require('archetype');
4
+ const authorize = require('../../authorize');
5
+ const { Buffer } = require('buffer');
6
+ const validateDocumentWithTimeout = require('../../helpers/validateDocumentWithTimeout');
7
+
8
+ const SAMPLE_SIZE = 1000;
9
+ const MAX_SCHEMA_PATH_DEPTH = 4;
10
+
11
+ const AnalyzeSchemaParams = new Archetype({
12
+ model: {
13
+ $type: 'string',
14
+ $required: true
15
+ },
16
+ roles: {
17
+ $type: ['string']
18
+ }
19
+ }).compile('AnalyzeSchemaParams');
20
+
21
+ module.exports = ({ db }) => async function analyzeSchema(params) {
22
+ const { model, roles } = new AnalyzeSchemaParams(params);
23
+
24
+ await authorize('Model.analyzeSchema', roles);
25
+
26
+ const Model = db.models[model];
27
+ if (Model == null) {
28
+ throw new Error(`Model ${model} not found`);
29
+ }
30
+
31
+ const documentCount = await Model.collection.countDocuments();
32
+ const rawDocs = documentCount > SAMPLE_SIZE ?
33
+ await Model.collection.aggregate([{ $sample: { size: SAMPLE_SIZE } }]).toArray() :
34
+ await Model.collection.find({}).toArray();
35
+
36
+ const paths = getSchemaPaths(Model.schema);
37
+ const pathTypeCounts = {};
38
+ const pathValueCounts = {};
39
+
40
+ for (const path of paths) {
41
+ pathTypeCounts[path.path] = {};
42
+ pathValueCounts[path.path] = 0;
43
+ }
44
+
45
+ const validationResults = await Promise.all(rawDocs.map(rawDoc => validateDocumentWithTimeout(Model.hydrate(rawDoc))));
46
+ const validDocumentCount = validationResults.filter(result => result.valid).length;
47
+ const firstValidDocument = validationResults.find(result => result.valid);
48
+ const firstInvalidDocument = validationResults.find(result => !result.valid);
49
+
50
+ for (const rawDoc of rawDocs) {
51
+ for (const path of paths) {
52
+ for (const value of getPathValues(rawDoc, path.path, path.arrayParents)) {
53
+ const types = path.arrayPath ? [getType(value)] : getTypes(value);
54
+ pathValueCounts[path.path] += types.length;
55
+ for (const type of types) {
56
+ pathTypeCounts[path.path][type] = (pathTypeCounts[path.path][type] || 0) + 1;
57
+ }
58
+ }
59
+ }
60
+ }
61
+
62
+ return {
63
+ analysis: {
64
+ documentCount,
65
+ sampleSize: rawDocs.length,
66
+ sampled: documentCount > SAMPLE_SIZE,
67
+ validDocumentCount,
68
+ invalidDocumentCount: rawDocs.length - validDocumentCount,
69
+ firstValidDocumentId: firstValidDocument?.documentId || null,
70
+ firstInvalidDocumentId: firstInvalidDocument?.documentId || null,
71
+ invalidDocuments: validationResults.filter(result => !result.valid).map(result => ({
72
+ documentId: result.documentId,
73
+ error: result.error,
74
+ errors: result.errors
75
+ })),
76
+ paths: paths.map(path => ({
77
+ path: path.path,
78
+ valueCount: pathValueCounts[path.path],
79
+ ...(path.required ? { required: true } : {}),
80
+ types: Object.keys(pathTypeCounts[path.path]).map(type => ({
81
+ type,
82
+ count: pathTypeCounts[path.path][type]
83
+ })).sort((type1, type2) => type2.count - type1.count)
84
+ }))
85
+ }
86
+ };
87
+ };
88
+
89
+ function getSchemaPaths(schema, prefix, parentArrayParents) {
90
+ prefix = prefix || '';
91
+ parentArrayParents = parentArrayParents || [];
92
+
93
+ const paths = [];
94
+ for (const path of Object.keys(schema.paths)) {
95
+ const fullPath = prefix ? `${prefix}.${path}` : path;
96
+ const pathDepth = fullPath.split('.').length;
97
+ if (pathDepth > MAX_SCHEMA_PATH_DEPTH) {
98
+ continue;
99
+ }
100
+ const schemaType = schema.paths[path];
101
+ const arrayParents = parentArrayParents.concat(getSchemaArrayParents(schemaType, fullPath));
102
+ paths.push({
103
+ path: fullPath,
104
+ arrayParents,
105
+ arrayPath: isSchemaArrayPath(schemaType),
106
+ required: schemaType.options?.required === true
107
+ });
108
+
109
+ if (schemaType.schema && pathDepth < MAX_SCHEMA_PATH_DEPTH) {
110
+ paths.push(...getSchemaPaths(schemaType.schema, fullPath, arrayParents));
111
+ }
112
+ }
113
+ return paths;
114
+ }
115
+
116
+ function getSchemaArrayParents(schemaType, fullPath) {
117
+ if (isSchemaArrayPath(schemaType)) {
118
+ return [fullPath];
119
+ }
120
+ const parent = schemaType.$parentSchemaDocArray;
121
+ if (parent?.path) {
122
+ return [parent.path];
123
+ }
124
+ return [];
125
+ }
126
+
127
+ function isSchemaArrayPath(schemaType) {
128
+ return schemaType.$isMongooseDocumentArray || schemaType.$isMongooseArray || schemaType.instance === 'Array';
129
+ }
130
+
131
+ function getPathValues(doc, path, arrayParents) {
132
+ return getPathValuesRecursive(doc, path.split('.'), '', new Set(arrayParents));
133
+ }
134
+
135
+ function getPathValuesRecursive(value, parts, currentPath, arrayParents) {
136
+ if (parts.length === 0) {
137
+ return [value];
138
+ }
139
+
140
+ if (Array.isArray(value)) {
141
+ return value.flatMap(element => getPathValuesRecursive(element, parts, currentPath, arrayParents));
142
+ }
143
+
144
+ if (value == null) {
145
+ return [undefined];
146
+ }
147
+
148
+ const nextPart = parts[0];
149
+ const nextPath = currentPath ? `${currentPath}.${nextPart}` : nextPart;
150
+ const nextValue = getPathPartValue(value, nextPart);
151
+ if (nextValue == null && arrayParents.has(nextPath)) {
152
+ return [];
153
+ }
154
+
155
+ return getPathValuesRecursive(nextValue, parts.slice(1), nextPath, arrayParents);
156
+ }
157
+
158
+ function getPathPartValue(value, pathPart) {
159
+ if (value instanceof Map) {
160
+ return value.get(pathPart);
161
+ }
162
+ return value[pathPart];
163
+ }
164
+
165
+ function getTypes(value) {
166
+ if (!Array.isArray(value)) {
167
+ return [getType(value)];
168
+ }
169
+ if (value.length === 0) {
170
+ return ['array'];
171
+ }
172
+ const types = new Set();
173
+ for (const element of value) {
174
+ types.add(getType(element));
175
+ }
176
+ return Array.from(types);
177
+ }
178
+
179
+ function getType(value) {
180
+ if (value === undefined) {
181
+ return 'undefined';
182
+ }
183
+ if (value === null) {
184
+ return 'null';
185
+ }
186
+ if (Array.isArray(value)) {
187
+ return 'array';
188
+ }
189
+ if (value instanceof Date) {
190
+ return 'date';
191
+ }
192
+ if (Buffer.isBuffer(value)) {
193
+ return 'buffer';
194
+ }
195
+ if (value?._bsontype === 'ObjectId') {
196
+ return 'objectId';
197
+ }
198
+ if (value?._bsontype === 'Decimal128') {
199
+ return 'decimal128';
200
+ }
201
+ return typeof value;
202
+ }
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  exports.addField = require('./addField');
4
+ exports.analyzeSchema = require('./analyzeSchema');
4
5
  exports.createChatMessage = require('./createChatMessage');
5
6
  exports.createDocument = require('./createDocument');
6
7
  exports.deleteDocument = require('./deleteDocument');
@@ -21,3 +22,4 @@ exports.streamDocumentChanges = require('./streamDocumentChanges');
21
22
  exports.streamChatMessage = require('./streamChatMessage');
22
23
  exports.updateDocument = require('./updateDocument');
23
24
  exports.updateDocuments = require('./updateDocuments');
25
+ exports.validateDocument = require('./validateDocument');
@@ -0,0 +1,35 @@
1
+ 'use strict';
2
+
3
+ const Archetype = require('archetype');
4
+ const authorize = require('../../authorize');
5
+ const validateDocumentWithTimeout = require('../../helpers/validateDocumentWithTimeout');
6
+
7
+ const ValidateDocumentParams = new Archetype({
8
+ model: {
9
+ $type: 'string',
10
+ $required: true
11
+ },
12
+ documentId: {
13
+ $type: 'string',
14
+ $required: true
15
+ },
16
+ roles: {
17
+ $type: ['string']
18
+ }
19
+ }).compile('ValidateDocumentParams');
20
+
21
+ module.exports = ({ db }) => async function validateDocument(params) {
22
+ const { model, documentId, roles } = new ValidateDocumentParams(params);
23
+
24
+ await authorize('Model.validateDocument', roles);
25
+
26
+ const Model = db.models[model];
27
+ if (Model == null) {
28
+ throw new Error(`Model ${model} not found`);
29
+ }
30
+
31
+ const doc = await Model.findById(documentId).setOptions({ sanitizeFilter: true }).orFail();
32
+ return {
33
+ result: await validateDocumentWithTimeout(doc)
34
+ };
35
+ };
@@ -14,7 +14,9 @@ const actionsToRequiredRoles = {
14
14
  'Dashboard.getDashboards': ['owner', 'admin', 'member', 'readonly', 'dashboards'],
15
15
  'Dashboard.updateDashboard': ['owner', 'admin', 'member'],
16
16
  'Model.createDocument': ['owner', 'admin', 'member'],
17
+ 'Model.analyzeSchema': ['owner', 'admin', 'member', 'readonly'],
17
18
  'Model.updateDocument': ['owner', 'admin', 'member'],
19
+ 'Model.validateDocument': ['owner', 'admin', 'member', 'readonly'],
18
20
  'Model.deleteDocument': ['owner', 'admin', 'member'],
19
21
  'Model.deleteDocuments': ['owner', 'admin', 'member'],
20
22
  'Model.dropCollection': ['owner', 'admin'],
@@ -13,6 +13,37 @@ const dashboardSchema = new mongoose.Schema({
13
13
  },
14
14
  description: {
15
15
  type: String
16
+ },
17
+ isPinned: {
18
+ type: Boolean
19
+ },
20
+ createdById: {
21
+ type: mongoose.Schema.Types.ObjectId
22
+ },
23
+ createdBy: {
24
+ name: String,
25
+ email: String
26
+ },
27
+ lastEvaluatedAt: {
28
+ type: Date
29
+ }
30
+ }, { timestamps: true });
31
+
32
+ dashboardSchema.post(['find', 'findOne'], async function(docs) {
33
+ const dashboards = Array.isArray(docs) ? docs : [docs];
34
+ for (const dashboard of dashboards) {
35
+ if (dashboard != null && dashboard.createdAt == null && dashboard._id?.getTimestamp) {
36
+ const createdAt = dashboard._id.getTimestamp();
37
+ if (!createdAt.valueOf()) {
38
+ continue;
39
+ }
40
+ dashboard.set('createdAt', createdAt, { overwriteImmutable: true });
41
+ await this.model.db.model('__Studio_Dashboard').updateOne(
42
+ { _id: dashboard._id },
43
+ { createdAt },
44
+ { overwriteImmutable: true }
45
+ );
46
+ }
16
47
  }
17
48
  });
18
49
 
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+
3
+ const VALIDATION_TIMEOUT_MS = 1000;
4
+
5
+ module.exports = async function validateDocumentWithTimeout(doc) {
6
+ const documentId = doc._id?.toString() || null;
7
+ try {
8
+ await Promise.race([
9
+ doc.validate(),
10
+ new Promise((resolve, reject) => setTimeout(() => reject(new Error('Validation timed out')), VALIDATION_TIMEOUT_MS))
11
+ ]);
12
+ return { valid: true, documentId, error: null, errors: null };
13
+ } catch (err) {
14
+ return {
15
+ valid: false,
16
+ documentId,
17
+ error: err?.message || String(err),
18
+ errors: formatValidationErrors(err)
19
+ };
20
+ }
21
+ };
22
+
23
+ function formatValidationErrors(err) {
24
+ if (err?.name !== 'ValidationError' || err.errors == null) {
25
+ return null;
26
+ }
27
+
28
+ const errors = {};
29
+ for (const [path, pathError] of Object.entries(err.errors)) {
30
+ errors[path] = {
31
+ message: pathError?.message || String(pathError),
32
+ name: pathError?.name || null,
33
+ kind: pathError?.kind || null,
34
+ path: pathError?.path || path,
35
+ value: pathError?.value
36
+ };
37
+ }
38
+ return errors;
39
+ }
@@ -63,6 +63,7 @@ module.exports = function netlify(conn, options) {
63
63
  params.roles = roles;
64
64
  params.userId = user._id;
65
65
  params.initiatedById = user._id;
66
+ params.initiatedBy = user;
66
67
  }
67
68
 
68
69
  if (typeof actionName !== 'string') {
package/backend/next.js CHANGED
@@ -64,6 +64,7 @@ module.exports = function next(conn, options) {
64
64
  params.roles = roles;
65
65
  params.userId = user._id;
66
66
  params.initiatedById = user._id;
67
+ params.initiatedBy = user;
67
68
  }
68
69
 
69
70
  if (typeof actionName !== 'string') {
package/express.js CHANGED
@@ -87,6 +87,7 @@ module.exports = async function mongooseStudioExpressApp(apiUrl, conn, options)
87
87
  req._internals.initiatedById = user._id;
88
88
  req._internals.roles = roles;
89
89
  req._internals.$workspaceId = workspace._id;
90
+ req._internals.initiatedBy = user;
90
91
 
91
92
  next();
92
93
  })