@mongoosejs/studio 0.2.9 → 0.2.11

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 (35) hide show
  1. package/backend/actions/Model/executeDocumentScript.js +61 -0
  2. package/backend/actions/Model/index.js +2 -0
  3. package/backend/actions/Model/listModels.js +36 -1
  4. package/backend/actions/Model/streamDocumentChanges.js +123 -0
  5. package/backend/actions/Task/cancelTask.js +24 -0
  6. package/backend/actions/Task/createTask.js +33 -0
  7. package/backend/actions/Task/getTasks.js +62 -0
  8. package/backend/actions/Task/index.js +7 -0
  9. package/backend/actions/Task/rescheduleTask.js +39 -0
  10. package/backend/actions/Task/runTask.js +25 -0
  11. package/backend/actions/index.js +1 -0
  12. package/backend/authorize.js +2 -0
  13. package/backend/index.js +7 -1
  14. package/eslint.config.js +4 -1
  15. package/express.js +4 -2
  16. package/frontend/public/app.js +14590 -13420
  17. package/frontend/public/tw.css +357 -4
  18. package/frontend/src/api.js +100 -0
  19. package/frontend/src/dashboard-result/dashboard-document/dashboard-document.html +4 -5
  20. package/frontend/src/dashboard-result/dashboard-document/dashboard-document.js +13 -14
  21. package/frontend/src/document/document.html +80 -0
  22. package/frontend/src/document/document.js +206 -19
  23. package/frontend/src/document/execute-script/execute-script.css +35 -0
  24. package/frontend/src/document/execute-script/execute-script.html +67 -0
  25. package/frontend/src/document/execute-script/execute-script.js +142 -0
  26. package/frontend/src/index.js +48 -4
  27. package/frontend/src/navbar/navbar.html +15 -2
  28. package/frontend/src/navbar/navbar.js +11 -0
  29. package/frontend/src/routes.js +13 -5
  30. package/frontend/src/tasks/task-details/task-details.html +284 -0
  31. package/frontend/src/tasks/task-details/task-details.js +182 -0
  32. package/frontend/src/tasks/tasks.css +0 -0
  33. package/frontend/src/tasks/tasks.html +220 -0
  34. package/frontend/src/tasks/tasks.js +372 -0
  35. package/package.json +4 -1
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+
3
+ const Archetype = require('archetype');
4
+ const authorize = require('../../authorize');
5
+ const mongoose = require('mongoose');
6
+ const util = require('util');
7
+ const vm = require('vm');
8
+
9
+ const ExecuteDocumentScriptParams = new Archetype({
10
+ model: {
11
+ $type: 'string',
12
+ $required: true
13
+ },
14
+ documentId: {
15
+ $type: 'string',
16
+ $required: true
17
+ },
18
+ script: {
19
+ $type: 'string',
20
+ $required: true
21
+ },
22
+ roles: {
23
+ $type: ['string']
24
+ }
25
+ }).compile('ExecuteDocumentScriptParams');
26
+
27
+ module.exports = ({ db }) => async function executeDocumentScript(params) {
28
+ const { model, documentId, script, roles } = new ExecuteDocumentScriptParams(params);
29
+
30
+ await authorize('Model.executeDocumentScript', roles);
31
+
32
+ const Model = db.models[model];
33
+ if (Model == null) {
34
+ throw new Error(`Model ${model} not found`);
35
+ }
36
+
37
+ const doc = await Model.findById(documentId).setOptions({ sanitizeFilter: true }).orFail();
38
+
39
+ const logs = [];
40
+ if (!db.Types) {
41
+ db.Types = mongoose.Types;
42
+ }
43
+ const sandbox = { db, mongoose, doc, console: {}, ObjectId: mongoose.Types.ObjectId };
44
+
45
+ sandbox.console.log = function() {
46
+ const args = Array.from(arguments);
47
+ logs.push(args.map(arg => typeof arg === 'object' ? util.inspect(arg) : arg).join(' '));
48
+ };
49
+
50
+ const context = vm.createContext(sandbox);
51
+ const result = await vm.runInContext(wrappedScript(script), context);
52
+
53
+ return {
54
+ result,
55
+ logs: logs.join('\n')
56
+ };
57
+ };
58
+
59
+ const wrappedScript = script => `(async () => {
60
+ ${script}
61
+ })()`;
@@ -6,6 +6,7 @@ exports.createDocument = require('./createDocument');
6
6
  exports.deleteDocument = require('./deleteDocument');
7
7
  exports.deleteDocuments = require('./deleteDocuments');
8
8
  exports.dropIndex = require('./dropIndex');
9
+ exports.executeDocumentScript = require('./executeDocumentScript');
9
10
  exports.exportQueryResults = require('./exportQueryResults');
10
11
  exports.getDocument = require('./getDocument');
11
12
  exports.getDocuments = require('./getDocuments');
@@ -13,6 +14,7 @@ exports.getDocumentsStream = require('./getDocumentsStream');
13
14
  exports.getCollectionInfo = require('./getCollectionInfo');
14
15
  exports.getIndexes = require('./getIndexes');
15
16
  exports.listModels = require('./listModels');
17
+ exports.streamDocumentChanges = require('./streamDocumentChanges');
16
18
  exports.streamChatMessage = require('./streamChatMessage');
17
19
  exports.updateDocument = require('./updateDocument');
18
20
  exports.updateDocuments = require('./updateDocuments');
@@ -2,6 +2,8 @@
2
2
 
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
+ const getRefFromSchemaType = require('../../helpers/getRefFromSchemaType');
6
+ const removeSpecifiedPaths = require('../../helpers/removeSpecifiedPaths');
5
7
 
6
8
  const ListModelsParams = new Archetype({
7
9
  roles: {
@@ -15,8 +17,41 @@ module.exports = ({ db }) => async function listModels(params) {
15
17
 
16
18
  const readyState = db.connection?.readyState ?? db.readyState;
17
19
 
20
+ const models = Object.keys(db.models).filter(key => !key.startsWith('__Studio_')).sort();
21
+
22
+ const modelSchemaPaths = {};
23
+ for (const modelName of models) {
24
+ const Model = db.models[modelName];
25
+ const schemaPaths = {};
26
+ modelSchemaPaths[modelName] = schemaPaths;
27
+ for (const path of Object.keys(Model.schema.paths)) {
28
+ const schemaType = Model.schema.paths[path];
29
+ schemaPaths[path] = {
30
+ instance: schemaType.instance,
31
+ path,
32
+ ref: getRefFromSchemaType(schemaType),
33
+ required: schemaType.options?.required,
34
+ enum: schemaType.options?.enum
35
+ };
36
+ if (schemaType.schema) {
37
+ schemaPaths[path].schema = {};
38
+ for (const subpath of Object.keys(schemaType.schema.paths)) {
39
+ schemaPaths[path].schema[subpath] = {
40
+ instance: schemaType.schema.paths[subpath].instance,
41
+ path: subpath,
42
+ ref: getRefFromSchemaType(schemaType.schema.paths[subpath]),
43
+ required: schemaType.schema.paths[subpath].options?.required,
44
+ enum: schemaType.schema.paths[subpath].options?.enum
45
+ };
46
+ }
47
+ }
48
+ }
49
+ removeSpecifiedPaths(schemaPaths, '.$*');
50
+ }
51
+
18
52
  return {
19
- models: Object.keys(db.models).filter(key => !key.startsWith('__Studio_')).sort(),
53
+ models,
54
+ modelSchemaPaths,
20
55
  readyState
21
56
  };
22
57
  };
@@ -0,0 +1,123 @@
1
+ 'use strict';
2
+
3
+ const Archetype = require('archetype');
4
+ const authorize = require('../../authorize');
5
+
6
+ const StreamDocumentChangesParams = new Archetype({
7
+ model: {
8
+ $type: 'string',
9
+ $required: true
10
+ },
11
+ documentId: {
12
+ $type: 'string',
13
+ $required: true
14
+ },
15
+ roles: {
16
+ $type: ['string']
17
+ }
18
+ }).compile('StreamDocumentChangesParams');
19
+
20
+ module.exports = ({ db, changeStream }) => async function* streamDocumentChanges(params) {
21
+ const { model, documentId, roles } = new StreamDocumentChangesParams(params);
22
+
23
+ await authorize('Model.streamDocumentChanges', roles);
24
+
25
+ const Model = db.models[model];
26
+ if (Model == null) {
27
+ throw new Error(`Model ${model} not found`);
28
+ }
29
+
30
+ if (!changeStream) {
31
+ throw new Error('Change streams are not enabled');
32
+ }
33
+
34
+ const collectionName = Model.collection.name;
35
+ const targetId = String(documentId);
36
+
37
+ const queue = [];
38
+ let resolveQueue = null;
39
+ let streamError = null;
40
+ let streamEnded = false;
41
+
42
+ function enqueue(payload) {
43
+ queue.push(payload);
44
+ if (resolveQueue) {
45
+ const resolve = resolveQueue;
46
+ resolveQueue = null;
47
+ resolve();
48
+ }
49
+ }
50
+
51
+ function handleChange(change) {
52
+ if (!change || change.ns?.coll !== collectionName) {
53
+ return;
54
+ }
55
+ if (!change.documentKey || change.documentKey._id == null) {
56
+ return;
57
+ }
58
+ if (String(change.documentKey._id) !== targetId) {
59
+ return;
60
+ }
61
+
62
+ enqueue({
63
+ type: 'change',
64
+ operationType: change.operationType,
65
+ documentKey: change.documentKey,
66
+ ns: change.ns,
67
+ updateDescription: change.updateDescription,
68
+ clusterTime: change.clusterTime
69
+ });
70
+ }
71
+
72
+ function handleError(err) {
73
+ streamError = err || new Error('Change stream error');
74
+ enqueue({ type: 'error', message: streamError.message });
75
+ }
76
+
77
+ function handleEnd() {
78
+ streamEnded = true;
79
+ enqueue({ type: 'end' });
80
+ }
81
+
82
+ changeStream.on('change', handleChange);
83
+ changeStream.on('error', handleError);
84
+ changeStream.on('end', handleEnd);
85
+
86
+ try {
87
+ while (true) {
88
+ if (streamError) {
89
+ throw streamError;
90
+ }
91
+
92
+ if (queue.length === 0) {
93
+ await new Promise(resolve => {
94
+ resolveQueue = resolve;
95
+ });
96
+ }
97
+
98
+ if (streamError) {
99
+ throw streamError;
100
+ }
101
+
102
+ while (queue.length > 0) {
103
+ const payload = queue.shift();
104
+ if (payload?.type === 'end') {
105
+ return;
106
+ }
107
+ yield payload;
108
+ }
109
+
110
+ if (streamEnded) {
111
+ return;
112
+ }
113
+ }
114
+ } finally {
115
+ changeStream.off('change', handleChange);
116
+ changeStream.off('error', handleError);
117
+ changeStream.off('end', handleEnd);
118
+ if (resolveQueue) {
119
+ resolveQueue();
120
+ resolveQueue = null;
121
+ }
122
+ }
123
+ };
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ const Archetype = require('archetype');
4
+ const mongoose = require('mongoose');
5
+
6
+ const CancelTaskParams = new Archetype({
7
+ taskId: {
8
+ $type: mongoose.Types.ObjectId,
9
+ $required: true
10
+ }
11
+ }).compile('CancelTaskParams');
12
+
13
+ module.exports = ({ db }) => async function cancelTask(params) {
14
+ params = new CancelTaskParams(params);
15
+ const { taskId } = params;
16
+ const { Task } = db.models;
17
+
18
+ const task = await Task.findOne({ _id: taskId }).orFail();
19
+
20
+ const cancelledTask = await Task.cancelTask({ _id: taskId });
21
+ return {
22
+ task: cancelledTask
23
+ };
24
+ };
@@ -0,0 +1,33 @@
1
+ 'use strict';
2
+
3
+ const Archetype = require('archetype');
4
+
5
+ const CreateTaskParams = new Archetype({
6
+ name: {
7
+ $type: 'string',
8
+ $required: true
9
+ },
10
+ scheduledAt: {
11
+ $type: Date,
12
+ $required: true
13
+ },
14
+ repeatAfterMS: {
15
+ $type: 'number'
16
+ },
17
+ payload: {
18
+ $type: Archetype.Any
19
+ }
20
+ }).compile('CreateTaskParams');
21
+
22
+ module.exports = ({ db }) => async function createTask(params) {
23
+ params = new CreateTaskParams(params);
24
+
25
+ const { name, scheduledAt, payload, repeatAfterMS } = params;
26
+ const { Task } = db.models;
27
+
28
+ const task = await Task.schedule(name, scheduledAt, payload, repeatAfterMS);
29
+
30
+ return {
31
+ task
32
+ };
33
+ };
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+
3
+ const Archetype = require('archetype');
4
+
5
+ const GetTasksParams = new Archetype({
6
+ start: {
7
+ $type: Date
8
+ },
9
+ end: {
10
+ $type: Date
11
+ },
12
+ status: {
13
+ $type: 'string'
14
+ },
15
+ name: {
16
+ $type: 'string'
17
+ }
18
+ }).compile('GetTasksParams');
19
+
20
+ module.exports = ({ db }) => async function getTasks(params) {
21
+ params = new GetTasksParams(params);
22
+ const { start, end, status, name } = params;
23
+ const { Task } = db.models;
24
+
25
+ const filter = {};
26
+
27
+ if (start && end) {
28
+ filter.scheduledAt = { $gte: start, $lt: end };
29
+ } else if (start) {
30
+ filter.scheduledAt = { $gte: start };
31
+ }
32
+ if (status) {
33
+ filter.status = status;
34
+ }
35
+ if (name) {
36
+ filter.name = { $regex: name, $options: 'i' };
37
+ }
38
+
39
+ const tasks = await Task.find(filter);
40
+
41
+ // Define all possible statuses
42
+ const allStatuses = ['pending', 'in_progress', 'succeeded', 'failed', 'cancelled', 'unknown'];
43
+
44
+ // Initialize groupedTasks with all statuses
45
+ const groupedTasks = allStatuses.reduce((groups, status) => {
46
+ groups[status] = [];
47
+ return groups;
48
+ }, {});
49
+
50
+ // Group tasks by status
51
+ tasks.forEach(task => {
52
+ const taskStatus = task.status || 'unknown';
53
+ if (groupedTasks.hasOwnProperty(taskStatus)) {
54
+ groupedTasks[taskStatus].push(task);
55
+ }
56
+ });
57
+
58
+ return {
59
+ tasks,
60
+ groupedTasks
61
+ };
62
+ };
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ exports.cancelTask = require('./cancelTask');
4
+ exports.createTask = require('./createTask');
5
+ exports.getTasks = require('./getTasks');
6
+ exports.rescheduleTask = require('./rescheduleTask');
7
+ exports.runTask = require('./runTask');
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+
3
+ const Archetype = require('archetype');
4
+ const mongoose = require('mongoose');
5
+
6
+ const RescheduleTaskParams = new Archetype({
7
+ taskId: {
8
+ $type: mongoose.Types.ObjectId,
9
+ $required: true
10
+ },
11
+ scheduledAt: {
12
+ $type: Date,
13
+ $required: true
14
+ }
15
+ }).compile('RescheduleTaskParams');
16
+
17
+ module.exports = ({ db }) => async function rescheduleTask(params) {
18
+ params = new RescheduleTaskParams(params);
19
+ const { taskId, scheduledAt } = params;
20
+ const { Task } = db.models;
21
+
22
+ const task = await Task.findOne({ _id: taskId }).orFail();
23
+
24
+ if (scheduledAt < Date.now()) {
25
+ throw new Error('Cannot reschedule a task for the past');
26
+ }
27
+
28
+ if (task.status != 'pending') {
29
+ throw new Error('Cannot reschedule a task that is not pending');
30
+ }
31
+
32
+ task.scheduledAt = scheduledAt;
33
+
34
+ await task.save();
35
+
36
+ return {
37
+ task
38
+ };
39
+ };
@@ -0,0 +1,25 @@
1
+ 'use strict';
2
+
3
+ const Archetype = require('archetype');
4
+ const mongoose = require('mongoose');
5
+
6
+ const RunTaskParams = new Archetype({
7
+ taskId: {
8
+ $type: mongoose.Types.ObjectId,
9
+ $required: true
10
+ }
11
+ }).compile('RunTaskParams');
12
+
13
+ module.exports = ({ db }) => async function runTask(params) {
14
+ params = new RunTaskParams(params);
15
+ const { taskId } = params;
16
+ const { Task } = db.models;
17
+
18
+ const task = await Task.findOne({ _id: taskId }).orFail();
19
+
20
+ const executedTask = await Task.execute(task);
21
+
22
+ return {
23
+ task: executedTask
24
+ };
25
+ };
@@ -6,3 +6,4 @@ exports.Dashboard = require('./Dashboard');
6
6
  exports.Model = require('./Model');
7
7
  exports.Script = require('./Script');
8
8
  exports.status = require('./status');
9
+ exports.Task = require('./Task');
@@ -17,12 +17,14 @@ const actionsToRequiredRoles = {
17
17
  'Model.deleteDocument': ['owner', 'admin', 'member'],
18
18
  'Model.deleteDocuments': ['owner', 'admin', 'member'],
19
19
  'Model.dropIndex': ['owner', 'admin'],
20
+ 'Model.executeDocumentScript': ['owner', 'admin', 'member'],
20
21
  'Model.exportQueryResults': ['owner', 'admin', 'member', 'readonly'],
21
22
  'Model.getDocument': ['owner', 'admin', 'member', 'readonly'],
22
23
  'Model.getDocuments': ['owner', 'admin', 'member', 'readonly'],
23
24
  'Model.getDocumentsStream': ['owner', 'admin', 'member', 'readonly'],
24
25
  'Model.getIndexes': ['owner', 'admin', 'member', 'readonly'],
25
26
  'Model.listModels': ['owner', 'admin', 'member', 'readonly'],
27
+ 'Model.streamDocumentChanges': ['owner', 'admin', 'member', 'readonly'],
26
28
  'Model.streamChatMessage': ['owner', 'admin', 'member', 'readonly'],
27
29
  'Model.updateDocuments': ['owner', 'admin', 'member']
28
30
  };
package/backend/index.js CHANGED
@@ -16,6 +16,12 @@ module.exports = function backend(db, studioConnection, options) {
16
16
  const ChatMessage = studioConnection.model('__Studio_ChatMessage', chatMessageSchema, 'studio__chatMessages');
17
17
  const ChatThread = studioConnection.model('__Studio_ChatThread', chatThreadSchema, 'studio__chatThreads');
18
18
 
19
- const actions = applySpec(Actions, { db, studioConnection, options });
19
+ let changeStream = null;
20
+ if (options?.changeStream) {
21
+ changeStream = db.watch();
22
+ }
23
+
24
+ const actions = applySpec(Actions, { db, studioConnection, options, changeStream });
25
+ actions.services = { changeStream };
20
26
  return actions;
21
27
  };
package/eslint.config.js CHANGED
@@ -37,9 +37,12 @@ module.exports = defineConfig([
37
37
  fetch: true,
38
38
  __dirname: true,
39
39
  process: true,
40
+ clearTimeout: true,
40
41
  setTimeout: true,
41
42
  navigator: true,
42
- TextDecoder: true
43
+ TextDecoder: true,
44
+ AbortController: true,
45
+ clearTimeout: true
43
46
  },
44
47
  sourceType: 'commonjs'
45
48
  },
package/express.js CHANGED
@@ -7,8 +7,9 @@ const { toRoute, objectRouter } = require('extrovert');
7
7
 
8
8
  module.exports = async function mongooseStudioExpressApp(apiUrl, conn, options) {
9
9
  const router = express.Router();
10
+ options = options ? { changeStream: true, ...options } : { changeStream: true };
10
11
 
11
- const mothershipUrl = options?._mothershipUrl || 'https://mongoose-js.netlify.app/.netlify/functions';
12
+ const mothershipUrl = options._mothershipUrl || 'https://mongoose-js.netlify.app/.netlify/functions';
12
13
  let workspace = null;
13
14
  if (options?.apiKey) {
14
15
  ({ workspace } = await fetch(`${mothershipUrl}/getWorkspace`, {
@@ -31,7 +32,7 @@ module.exports = async function mongooseStudioExpressApp(apiUrl, conn, options)
31
32
  }
32
33
 
33
34
  apiUrl = apiUrl || 'api';
34
- const backend = Backend(conn, options?.studioConnection, options);
35
+ const backend = Backend(conn, options.studioConnection, options);
35
36
 
36
37
  router.use(
37
38
  '/api',
@@ -80,6 +81,7 @@ module.exports = async function mongooseStudioExpressApp(apiUrl, conn, options)
80
81
  );
81
82
 
82
83
  const { config } = await frontend(apiUrl, false, options, workspace);
84
+ config.enableTaskVisualizer = options.enableTaskVisualizer;
83
85
  router.get('/config.js', function (req, res) {
84
86
  res.setHeader('Content-Type', 'application/javascript');
85
87
  res.end(`window.MONGOOSE_STUDIO_CONFIG = ${JSON.stringify(config, null, 2)};`);