@mongoosejs/studio 0.3.0 → 0.3.2

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 (32) hide show
  1. package/DEVGUIDE.md +8 -0
  2. package/backend/actions/ChatThread/createChatMessage.js +2 -0
  3. package/backend/actions/ChatThread/streamChatMessage.js +2 -0
  4. package/backend/actions/Dashboard/getDashboard.js +15 -11
  5. package/backend/actions/Dashboard/updateDashboard.js +2 -2
  6. package/backend/actions/Task/getTaskOverview.js +102 -0
  7. package/backend/actions/Task/getTasks.js +85 -45
  8. package/backend/actions/Task/index.js +1 -0
  9. package/frontend/public/app.js +318 -148
  10. package/frontend/public/tw.css +82 -0
  11. package/frontend/src/_util/dateRange.js +82 -0
  12. package/frontend/src/ace-editor/ace-editor.js +6 -0
  13. package/frontend/src/api.js +6 -0
  14. package/frontend/src/chat/chat-message-script/chat-message-script.html +11 -16
  15. package/frontend/src/chat/chat-message-script/chat-message-script.js +0 -6
  16. package/frontend/src/chat/chat.js +2 -0
  17. package/frontend/src/dashboard/dashboard.js +13 -2
  18. package/frontend/src/dashboard/edit-dashboard/edit-dashboard.js +4 -3
  19. package/frontend/src/dashboard-result/dashboard-result.js +3 -0
  20. package/frontend/src/dashboard-result/dashboard-table/dashboard-table.html +34 -0
  21. package/frontend/src/dashboard-result/dashboard-table/dashboard-table.js +37 -0
  22. package/frontend/src/models/models.js +9 -3
  23. package/frontend/src/navbar/navbar.js +3 -2
  24. package/frontend/src/task-by-name/task-by-name.html +77 -7
  25. package/frontend/src/task-by-name/task-by-name.js +84 -9
  26. package/frontend/src/tasks/task-details/task-details.html +8 -8
  27. package/frontend/src/tasks/task-details/task-details.js +2 -1
  28. package/frontend/src/tasks/tasks.js +25 -118
  29. package/local.js +38 -0
  30. package/package.json +4 -1
  31. package/seed/connect.js +23 -0
  32. package/seed/index.js +101 -0
package/DEVGUIDE.md ADDED
@@ -0,0 +1,8 @@
1
+ ## Running Locally
2
+
3
+ To run this repo locally:
4
+
5
+ 1. Install dependencies with `npm install`
6
+ 2. Seed the database with `npm run seed`. Make sure you have a MongoDB instance running on `localhost:27017`.
7
+ 3. Run backend on port 7777. Make sure to seed backend as well.
8
+ 4. Start the server with `npm start`
@@ -134,6 +134,8 @@ If the user's query is best answered with a chart, return a Chart.js 4 configura
134
134
 
135
135
  If the user\'s query is best answered by a map, return an object { $featureCollection } which contains a GeoJSON FeatureCollection
136
136
 
137
+ If the user's query is best answered by a table, return an object { $table: { columns: string[], rows: any[][] } }
138
+
137
139
  Example output:
138
140
 
139
141
  The following script counts the number of users which are not deleted.
@@ -143,6 +143,8 @@ const systemPrompt = `
143
143
 
144
144
  If the user\'s query is best answered by a map, return an object { $featureCollection } which contains a GeoJSON FeatureCollection
145
145
 
146
+ If the user's query is best answered by a table, return an object { $table: { columns: string[], rows: any[][] } }
147
+
146
148
  Example output:
147
149
 
148
150
  The following script counts the number of users which are not deleted.
@@ -23,16 +23,17 @@ const GetDashboardParams = new Archetype({
23
23
  }
24
24
  }).compile('GetDashboardParams');
25
25
 
26
- module.exports = ({ db }) => async function getDashboard(params) {
26
+ module.exports = ({ db, options }) => async function getDashboard(params) {
27
27
  const { $workspaceId, authorization, dashboardId, evaluate, roles } = new GetDashboardParams(params);
28
28
  const Dashboard = db.model('__Studio_Dashboard');
29
+ const mothershipUrl = options?._mothershipUrl ?? 'https://mongoose-js.netlify.app/.netlify/functions';
29
30
 
30
31
  await authorize('Dashboard.getDashboard', roles);
31
32
 
32
33
  const dashboard = await Dashboard.findOne({ _id: dashboardId });
33
34
  if (evaluate) {
34
35
  let result = null;
35
- const startExec = startDashboardEvaluate(dashboardId, $workspaceId, authorization);
36
+ const startExec = startDashboardEvaluate(dashboardId, $workspaceId, authorization, mothershipUrl);
36
37
  // Avoid unhandled promise rejection since we handle the promise later.
37
38
  startExec.catch(() => {});
38
39
  try {
@@ -47,7 +48,8 @@ module.exports = ({ db }) => async function getDashboard(params) {
47
48
  $workspaceId,
48
49
  authorization,
49
50
  null,
50
- { message: error.message }
51
+ { message: error.message },
52
+ mothershipUrl
51
53
  );
52
54
  });
53
55
  return { dashboard, dashboardResult, error: { message: error.message } };
@@ -62,7 +64,9 @@ module.exports = ({ db }) => async function getDashboard(params) {
62
64
  dashboardResult._id,
63
65
  $workspaceId,
64
66
  authorization,
65
- result
67
+ result,
68
+ undefined,
69
+ mothershipUrl
66
70
  );
67
71
  });
68
72
 
@@ -71,12 +75,12 @@ module.exports = ({ db }) => async function getDashboard(params) {
71
75
  return { dashboard, error: { message: error.message } };
72
76
  }
73
77
  } else {
74
- const { dashboardResults } = await getDashboardResults(dashboardId, $workspaceId, authorization);
78
+ const { dashboardResults } = await getDashboardResults(dashboardId, $workspaceId, authorization, mothershipUrl);
75
79
  return { dashboard, dashboardResults };
76
80
  }
77
81
  };
78
82
 
79
- async function completeDashboardEvaluate(dashboardResultId, workspaceId, authorization, result, error) {
83
+ async function completeDashboardEvaluate(dashboardResultId, workspaceId, authorization, result, error, mothershipUrl) {
80
84
  if (!workspaceId) {
81
85
  return {};
82
86
  }
@@ -84,7 +88,7 @@ async function completeDashboardEvaluate(dashboardResultId, workspaceId, authori
84
88
  if (authorization) {
85
89
  headers.Authorization = authorization;
86
90
  }
87
- const response = await fetch('https://mongoose-js.netlify.app/.netlify/functions/completeDashboardEvaluate', {
91
+ const response = await fetch(`${mothershipUrl}/completeDashboardEvaluate`, {
88
92
  method: 'POST',
89
93
  headers,
90
94
  body: JSON.stringify({
@@ -106,7 +110,7 @@ async function completeDashboardEvaluate(dashboardResultId, workspaceId, authori
106
110
  return await response.json();
107
111
  }
108
112
 
109
- async function startDashboardEvaluate(dashboardId, workspaceId, authorization) {
113
+ async function startDashboardEvaluate(dashboardId, workspaceId, authorization, _mothershipUrl) {
110
114
  if (!workspaceId) {
111
115
  return {};
112
116
  }
@@ -114,7 +118,7 @@ async function startDashboardEvaluate(dashboardId, workspaceId, authorization) {
114
118
  if (authorization) {
115
119
  headers.Authorization = authorization;
116
120
  }
117
- const response = await fetch('https://mongoose-js.netlify.app/.netlify/functions/startDashboardEvaluate', {
121
+ const response = await fetch(`${_mothershipUrl}/startDashboardEvaluate`, {
118
122
  method: 'POST',
119
123
  headers,
120
124
  body: JSON.stringify({
@@ -134,7 +138,7 @@ async function startDashboardEvaluate(dashboardId, workspaceId, authorization) {
134
138
  return await response.json();
135
139
  }
136
140
 
137
- async function getDashboardResults(dashboardId, workspaceId, authorization) {
141
+ async function getDashboardResults(dashboardId, workspaceId, authorization, mothershipUrl) {
138
142
  if (!workspaceId) {
139
143
  return {};
140
144
  }
@@ -142,7 +146,7 @@ async function getDashboardResults(dashboardId, workspaceId, authorization) {
142
146
  if (authorization) {
143
147
  headers.Authorization = authorization;
144
148
  }
145
- const response = await fetch('https://mongoose-js.netlify.app/.netlify/functions/getDashboardResults', {
149
+ const response = await fetch(`${mothershipUrl}/getDashboardResults`, {
146
150
  method: 'POST',
147
151
  headers,
148
152
  body: JSON.stringify({
@@ -32,11 +32,11 @@ module.exports = ({ db }) => async function updateDashboard(params) {
32
32
 
33
33
  const updateObj = { code };
34
34
 
35
- if (title) {
35
+ if (title != null) {
36
36
  updateObj.title = title;
37
37
  }
38
38
 
39
- if (description) {
39
+ if (description != null) {
40
40
  updateObj.description = description;
41
41
  }
42
42
 
@@ -0,0 +1,102 @@
1
+ 'use strict';
2
+
3
+ const Archetype = require('archetype');
4
+ const escape = require('regexp.escape');
5
+
6
+ const GetTaskOverviewParams = new Archetype({
7
+ start: { $type: Date },
8
+ end: { $type: Date },
9
+ status: { $type: 'string' },
10
+ name: { $type: 'string' }
11
+ }).compile('GetTaskOverviewParams');
12
+
13
+ /** Statuses shown on the Task overview page. */
14
+ const OVERVIEW_STATUSES = ['pending', 'succeeded', 'failed', 'cancelled'];
15
+
16
+ function buildMatch(params) {
17
+ const { start, end, status, name } = params;
18
+ const match = {};
19
+ if (start != null && end != null) {
20
+ match.scheduledAt = { $gte: start, $lt: end };
21
+ } else if (start != null) {
22
+ match.scheduledAt = { $gte: start };
23
+ }
24
+ const statusVal = typeof status === 'string' ? status.trim() : status;
25
+ if (statusVal != null && statusVal !== '') {
26
+ match.status = statusVal;
27
+ } else {
28
+ match.status = { $in: ['pending', 'in_progress', 'succeeded', 'failed', 'cancelled', 'unknown'] };
29
+ }
30
+ if (name != null && name !== '') {
31
+ const nameStr = typeof name === 'string' ? name.trim() : String(name);
32
+ match.name = { $regex: escape(nameStr), $options: 'i' };
33
+ }
34
+ return match;
35
+ }
36
+
37
+ module.exports = ({ db }) => async function getTaskOverview(params) {
38
+ params = new GetTaskOverviewParams(params);
39
+ if (typeof params.status === 'string') params.status = params.status.trim();
40
+ if (typeof params.name === 'string') params.name = params.name.trim();
41
+ const { Task } = db.models;
42
+ const match = buildMatch(params);
43
+
44
+ const defaultCounts = OVERVIEW_STATUSES.map(s => ({ k: s, v: 0 }));
45
+
46
+ const pipeline = [
47
+ { $match: match },
48
+ {
49
+ $facet: {
50
+ statusCounts: [
51
+ { $group: { _id: { $ifNull: ['$status', 'unknown'] }, count: { $sum: 1 } } },
52
+ { $group: { _id: null, counts: { $push: { k: '$_id', v: '$count' } } } },
53
+ {
54
+ $project: {
55
+ statusCounts: {
56
+ $arrayToObject: {
57
+ $concatArrays: [{ $literal: defaultCounts }, '$counts']
58
+ }
59
+ }
60
+ }
61
+ },
62
+ { $replaceRoot: { newRoot: '$statusCounts' } }
63
+ ],
64
+ tasksByName: [
65
+ {
66
+ $group: {
67
+ _id: '$name',
68
+ totalCount: { $sum: 1 },
69
+ lastRun: { $max: '$scheduledAt' },
70
+ pending: { $sum: { $cond: [{ $eq: ['$status', 'pending'] }, 1, 0] } },
71
+ succeeded: { $sum: { $cond: [{ $eq: ['$status', 'succeeded'] }, 1, 0] } },
72
+ failed: { $sum: { $cond: [{ $eq: ['$status', 'failed'] }, 1, 0] } },
73
+ cancelled: { $sum: { $cond: [{ $eq: ['$status', 'cancelled'] }, 1, 0] } }
74
+ }
75
+ },
76
+ {
77
+ $project: {
78
+ _id: 0,
79
+ name: '$_id',
80
+ totalCount: 1,
81
+ lastRun: 1,
82
+ statusCounts: {
83
+ pending: '$pending',
84
+ succeeded: '$succeeded',
85
+ failed: '$failed',
86
+ cancelled: '$cancelled'
87
+ }
88
+ }
89
+ },
90
+ { $sort: { name: 1 } }
91
+ ]
92
+ }
93
+ }
94
+ ];
95
+
96
+ const [result] = await Task.aggregate(pipeline);
97
+
98
+ return {
99
+ statusCounts: result.statusCounts?.[0] ?? {},
100
+ tasksByName: result.tasksByName || []
101
+ };
102
+ };
@@ -1,65 +1,105 @@
1
1
  'use strict';
2
2
 
3
3
  const Archetype = require('archetype');
4
+ const escape = require('regexp.escape');
4
5
 
5
6
  const GetTasksParams = new Archetype({
6
- start: {
7
- $type: Date,
8
- $required: true
9
- },
10
- end: {
11
- $type: Date
12
- },
13
- status: {
14
- $type: 'string'
15
- },
16
- name: {
17
- $type: 'string'
18
- }
7
+ start: { $type: Date },
8
+ end: { $type: Date },
9
+ status: { $type: 'string' },
10
+ name: { $type: 'string' },
11
+ skip: { $type: 'number', $default: 0 },
12
+ limit: { $type: 'number', $default: 100 }
19
13
  }).compile('GetTasksParams');
20
14
 
21
- module.exports = ({ db }) => async function getTasks(params) {
22
- params = new GetTasksParams(params);
23
- const { start, end, status, name } = params;
24
- const { Task } = db.models;
15
+ const ALL_STATUSES = ['pending', 'in_progress', 'succeeded', 'failed', 'cancelled', 'unknown'];
16
+
17
+ /** Status keys for statusCounts (same shape as getTaskOverview). */
18
+ const STATUS_COUNT_KEYS = ['pending', 'succeeded', 'failed', 'cancelled'];
25
19
 
26
- const filter = {};
20
+ /** Max documents per request to avoid excessive memory and response size. */
21
+ const MAX_LIMIT = 2000;
27
22
 
28
- if (start && end) {
29
- filter.scheduledAt = { $gte: start, $lt: end };
30
- } else if (start) {
31
- filter.scheduledAt = { $gte: start };
23
+ function buildMatch(params) {
24
+ const { start, end, status, name } = params;
25
+ const match = {};
26
+ if (start != null && end != null) {
27
+ match.scheduledAt = { $gte: start, $lt: end };
28
+ } else if (start != null) {
29
+ match.scheduledAt = { $gte: start };
32
30
  }
33
- if (status) {
34
- filter.status = status;
31
+ const statusVal = typeof status === 'string' ? status.trim() : status;
32
+ if (statusVal != null && statusVal !== '') {
33
+ match.status = statusVal;
35
34
  } else {
36
- filter.status = { $in: ['pending', 'in_progress', 'succeeded', 'failed', 'cancelled', 'unknown'] };
35
+ match.status = { $in: ALL_STATUSES };
37
36
  }
38
- if (name) {
39
- filter.name = { $regex: name, $options: 'i' };
37
+ if (name != null && name !== '') {
38
+ const nameStr = typeof name === 'string' ? name.trim() : String(name);
39
+ match.name = { $regex: escape(nameStr), $options: 'i' };
40
40
  }
41
+ return match;
42
+ }
41
43
 
42
- const tasks = await Task.find(filter);
44
+ /** Projection done in aggregation: only fields needed by frontend, payload → parameters, _id → id. */
45
+ const TASK_PROJECT_STAGE = {
46
+ _id: 1,
47
+ id: '$_id',
48
+ name: 1,
49
+ status: 1,
50
+ scheduledAt: 1,
51
+ createdAt: 1,
52
+ startedAt: 1,
53
+ completedAt: 1,
54
+ error: 1,
55
+ parameters: '$payload'
56
+ };
57
+
58
+ module.exports = ({ db }) => async function getTasks(params) {
59
+ params = new GetTasksParams(params);
60
+ if (typeof params.status === 'string') params.status = params.status.trim();
61
+ if (typeof params.name === 'string') params.name = params.name.trim();
43
62
 
44
- // Define all possible statuses
45
- const allStatuses = ['pending', 'in_progress', 'succeeded', 'failed', 'cancelled', 'unknown'];
63
+ const skip = Math.max(0, Number(params.skip) || 0);
64
+ const limit = Math.min(MAX_LIMIT, Math.max(1, Number(params.limit) || 100));
65
+ const { Task } = db.models;
66
+ const match = buildMatch(params);
46
67
 
47
- // Initialize groupedTasks with all statuses
48
- const groupedTasks = allStatuses.reduce((groups, status) => {
49
- groups[status] = [];
50
- return groups;
51
- }, {});
68
+ const defaultCounts = STATUS_COUNT_KEYS.map(s => ({ k: s, v: 0 }));
52
69
 
53
- // Group tasks by status
54
- tasks.forEach(task => {
55
- const taskStatus = task.status || 'unknown';
56
- if (groupedTasks.hasOwnProperty(taskStatus)) {
57
- groupedTasks[taskStatus].push(task);
70
+ const pipeline = [
71
+ { $match: match },
72
+ {
73
+ $facet: {
74
+ tasks: [
75
+ { $sort: { scheduledAt: -1 } },
76
+ { $skip: skip },
77
+ { $limit: limit },
78
+ { $project: TASK_PROJECT_STAGE }
79
+ ],
80
+ count: [{ $count: 'total' }],
81
+ statusCounts: [
82
+ { $group: { _id: { $ifNull: ['$status', 'unknown'] }, count: { $sum: 1 } } },
83
+ { $group: { _id: null, counts: { $push: { k: '$_id', v: '$count' } } } },
84
+ {
85
+ $project: {
86
+ statusCounts: {
87
+ $arrayToObject: {
88
+ $concatArrays: [{ $literal: defaultCounts }, '$counts']
89
+ }
90
+ }
91
+ }
92
+ },
93
+ { $replaceRoot: { newRoot: '$statusCounts' } }
94
+ ]
95
+ }
58
96
  }
59
- });
97
+ ];
98
+
99
+ const [result] = await Task.aggregate(pipeline);
100
+ const tasks = result.tasks || [];
101
+ const numDocs = (result.count && result.count[0] && result.count[0].total) || 0;
102
+ const statusCounts = result.statusCounts?.[0] ?? {};
60
103
 
61
- return {
62
- tasks,
63
- groupedTasks
64
- };
104
+ return { tasks, numDocs, statusCounts };
65
105
  };
@@ -3,5 +3,6 @@
3
3
  exports.cancelTask = require('./cancelTask');
4
4
  exports.createTask = require('./createTask');
5
5
  exports.getTasks = require('./getTasks');
6
+ exports.getTaskOverview = require('./getTaskOverview');
6
7
  exports.rescheduleTask = require('./rescheduleTask');
7
8
  exports.runTask = require('./runTask');