@mongoosejs/studio 0.5.13 → 0.6.0
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/AGENTS.md +74 -0
- package/README.md +27 -0
- package/backend/actions/ChatMessage/executeScript.js +15 -5
- package/backend/actions/ChatThread/createChatMessage.js +7 -2
- package/backend/actions/ChatThread/getChatThread.js +8 -2
- package/backend/actions/ChatThread/listChatThreads.js +6 -1
- package/backend/actions/ChatThread/shareChatThread.js +8 -2
- package/backend/actions/ChatThread/streamChatMessage.js +7 -2
- package/backend/actions/ChatThread/toggleAgentMode.js +8 -2
- package/backend/actions/Dashboard/createDashboard.js +3 -0
- package/backend/actions/Dashboard/deleteDashboard.js +13 -3
- package/backend/actions/Dashboard/getDashboard.js +33 -10
- package/backend/actions/Dashboard/getDashboards.js +9 -1
- package/backend/actions/Dashboard/updateDashboard.js +9 -2
- package/backend/actions/Model/addField.js +9 -2
- package/backend/actions/Model/analyzeSchema.js +16 -4
- package/backend/actions/Model/createChatMessage.js +3 -0
- package/backend/actions/Model/createDocument.js +3 -0
- package/backend/actions/Model/deleteDocument.js +9 -3
- package/backend/actions/Model/deleteDocuments.js +9 -1
- package/backend/actions/Model/dropCollection.js +3 -0
- package/backend/actions/Model/dropIndex.js +3 -0
- package/backend/actions/Model/executeDocumentScript.js +33 -26
- package/backend/actions/Model/exportQueryResults.js +13 -1
- package/backend/actions/Model/getCollectionInfo.js +15 -3
- package/backend/actions/Model/getDocument.js +13 -2
- package/backend/actions/Model/getDocuments.js +25 -5
- package/backend/actions/Model/getDocumentsStream.js +38 -18
- package/backend/actions/Model/getEstimatedDocumentCounts.js +15 -2
- package/backend/actions/Model/getIndexes.js +3 -0
- package/backend/actions/Model/getSuggestedProjection.js +3 -0
- package/backend/actions/Model/listModels.js +3 -0
- package/backend/actions/Model/streamChatMessage.js +3 -0
- package/backend/actions/Model/streamDocumentChanges.js +3 -0
- package/backend/actions/Model/updateDocument.js +10 -2
- package/backend/actions/Model/updateDocuments.js +10 -2
- package/backend/actions/Model/validateDocument.js +15 -2
- package/backend/actions/Script/createScript.js +36 -0
- package/backend/actions/Script/executeScript.js +89 -0
- package/backend/actions/Script/getScript.js +43 -0
- package/backend/actions/Script/index.js +5 -0
- package/backend/actions/Task/cancelTask.js +9 -3
- package/backend/actions/Task/getTaskOverview.js +7 -3
- package/backend/actions/Task/getTasks.js +7 -3
- package/backend/actions/Task/getTasksOverTime.js +7 -3
- package/backend/actions/Task/rescheduleTask.js +9 -3
- package/backend/actions/Task/runTask.js +9 -3
- package/backend/actions/index.js +1 -0
- package/backend/authorize.js +6 -1
- package/backend/db/scriptSchema.js +29 -0
- package/backend/helpers/omitNullish.js +5 -0
- package/backend/helpers/operationOptions.js +26 -0
- package/backend/helpers/readOperationOptions.js +31 -0
- package/backend/index.js +6 -4
- package/backend/integrations/mothership.js +28 -0
- package/backend/mcp.js +140 -0
- package/backend/netlify.js +5 -3
- package/backend/next.js +5 -3
- package/backend/sandbox/createSandbox.js +2 -2
- package/backend/sandbox/createScriptDb.js +60 -20
- package/backend/util/archetypeToZodSchema.js +74 -0
- package/docs/mcp-oauth.md +106 -0
- package/eslint.config.js +1 -0
- package/eval/agent/run.js +1 -1
- package/express.js +183 -41
- package/frontend/index.js +3 -0
- package/frontend/public/app.js +740 -124
- package/frontend/public/tw.css +76 -0
- package/frontend/src/api.js +27 -1
- package/frontend/src/index.js +9 -1
- package/frontend/src/mcp/mcp.html +55 -0
- package/frontend/src/mcp/mcp.js +27 -0
- package/frontend/src/models/models.html +29 -8
- package/frontend/src/models/models.js +30 -2
- package/frontend/src/navbar/navbar.html +8 -0
- package/frontend/src/navbar/navbar.js +3 -0
- package/frontend/src/routes.js +16 -0
- package/frontend/src/script/script.html +105 -0
- package/frontend/src/script/script.js +98 -0
- package/frontend/src/tooltip/tooltip.html +28 -0
- package/frontend/src/tooltip/tooltip.js +94 -0
- package/index.d.ts +10 -1
- package/local.js +2 -1
- package/nest.d.ts +4 -0
- package/package.json +5 -3
- package/vercel.d.ts +4 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const Archetype = require('archetype');
|
|
4
4
|
const authorize = require('../../authorize');
|
|
5
|
+
const operationOptions = require('../../helpers/operationOptions');
|
|
5
6
|
|
|
6
7
|
const UpdateDocumentsParams = new Archetype({
|
|
7
8
|
model: {
|
|
@@ -18,10 +19,13 @@ const UpdateDocumentsParams = new Archetype({
|
|
|
18
19
|
},
|
|
19
20
|
roles: {
|
|
20
21
|
$type: ['string']
|
|
22
|
+
},
|
|
23
|
+
maxTimeMS: {
|
|
24
|
+
$type: 'number'
|
|
21
25
|
}
|
|
22
26
|
}).compile('UpdateDocumentsParams');
|
|
23
27
|
|
|
24
|
-
module.exports = ({ db }) => async function updateDocuments(params) {
|
|
28
|
+
module.exports = ({ db, options }) => async function updateDocuments(params) {
|
|
25
29
|
const { model, _id, update, roles } = new UpdateDocumentsParams(params);
|
|
26
30
|
|
|
27
31
|
await authorize('Model.updateDocuments', roles);
|
|
@@ -60,7 +64,11 @@ module.exports = ({ db }) => async function updateDocuments(params) {
|
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
const result = await Model.
|
|
63
|
-
updateMany({ _id: { $in: _id } }, updateOperation, { overwriteImmutable: true, runValidators: false })
|
|
67
|
+
updateMany({ _id: { $in: _id } }, updateOperation, { overwriteImmutable: true, runValidators: false }).
|
|
68
|
+
setOptions(operationOptions(options, params));
|
|
64
69
|
|
|
65
70
|
return { result };
|
|
66
71
|
};
|
|
72
|
+
|
|
73
|
+
module.exports.paramsType = UpdateDocumentsParams;
|
|
74
|
+
module.exports.tags = [];
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const Archetype = require('archetype');
|
|
4
4
|
const authorize = require('../../authorize');
|
|
5
|
+
const readOperationOptions = require('../../helpers/readOperationOptions');
|
|
5
6
|
const validateDocumentWithTimeout = require('../../helpers/validateDocumentWithTimeout');
|
|
6
7
|
|
|
7
8
|
const ValidateDocumentParams = new Archetype({
|
|
@@ -15,10 +16,17 @@ const ValidateDocumentParams = new Archetype({
|
|
|
15
16
|
},
|
|
16
17
|
roles: {
|
|
17
18
|
$type: ['string']
|
|
19
|
+
},
|
|
20
|
+
maxTimeMS: {
|
|
21
|
+
$type: 'number'
|
|
22
|
+
},
|
|
23
|
+
readPreference: {
|
|
24
|
+
$type: 'string',
|
|
25
|
+
$enum: ['secondary', 'secondaryPreferred', 'primary']
|
|
18
26
|
}
|
|
19
27
|
}).compile('ValidateDocumentParams');
|
|
20
28
|
|
|
21
|
-
module.exports = ({ db }) => async function validateDocument(params) {
|
|
29
|
+
module.exports = ({ db, options }) => async function validateDocument(params) {
|
|
22
30
|
const { model, documentId, roles } = new ValidateDocumentParams(params);
|
|
23
31
|
|
|
24
32
|
await authorize('Model.validateDocument', roles);
|
|
@@ -28,8 +36,13 @@ module.exports = ({ db }) => async function validateDocument(params) {
|
|
|
28
36
|
throw new Error(`Model ${model} not found`);
|
|
29
37
|
}
|
|
30
38
|
|
|
31
|
-
const doc = await Model.findById(documentId).
|
|
39
|
+
const doc = await Model.findById(documentId).
|
|
40
|
+
setOptions(readOperationOptions(options, { sanitizeFilter: true })).
|
|
41
|
+
orFail();
|
|
32
42
|
return {
|
|
33
43
|
result: await validateDocumentWithTimeout(doc)
|
|
34
44
|
};
|
|
35
45
|
};
|
|
46
|
+
|
|
47
|
+
module.exports.paramsType = ValidateDocumentParams;
|
|
48
|
+
module.exports.tags = ['readOnly'];
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Archetype = require('archetype');
|
|
4
|
+
const mongoose = require('mongoose');
|
|
5
|
+
|
|
6
|
+
const authorize = require('../../authorize');
|
|
7
|
+
|
|
8
|
+
const CreateScriptParams = new Archetype({
|
|
9
|
+
initiatedById: {
|
|
10
|
+
$type: mongoose.Types.ObjectId
|
|
11
|
+
},
|
|
12
|
+
script: {
|
|
13
|
+
$type: 'string',
|
|
14
|
+
$required: true
|
|
15
|
+
},
|
|
16
|
+
roles: {
|
|
17
|
+
$type: ['string']
|
|
18
|
+
}
|
|
19
|
+
}).compile('CreateScriptParams');
|
|
20
|
+
|
|
21
|
+
module.exports = ({ studioConnection, options }) => async function createScript(params) {
|
|
22
|
+
const { initiatedById, script, roles } = new CreateScriptParams(params);
|
|
23
|
+
|
|
24
|
+
await authorize('Script.createScript', roles);
|
|
25
|
+
|
|
26
|
+
const Script = studioConnection.model('__Studio_Script');
|
|
27
|
+
const createdScript = await Script.create({ script, userId: initiatedById });
|
|
28
|
+
const baseUrl = options?.workspace?.baseUrl?.replace(/\/+$/, '');
|
|
29
|
+
const url = `${baseUrl ? `${baseUrl}/` : ''}#/script/${createdScript._id}`;
|
|
30
|
+
|
|
31
|
+
return { script: createdScript, url };
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
module.exports.paramsType = CreateScriptParams;
|
|
35
|
+
module.exports.tags = [];
|
|
36
|
+
module.exports.description = 'Create a script for the user to review, edit, dry run, or run in Mongoose Studio. This tool does not execute the script and returns a review URL.';
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Archetype = require('archetype');
|
|
4
|
+
const mongoose = require('mongoose');
|
|
5
|
+
|
|
6
|
+
const authorize = require('../../authorize');
|
|
7
|
+
const readOperationOptions = require('../../helpers/readOperationOptions');
|
|
8
|
+
const createSandbox = require('../../sandbox/createSandbox');
|
|
9
|
+
|
|
10
|
+
const ExecuteScriptParams = new Archetype({
|
|
11
|
+
initiatedById: {
|
|
12
|
+
$type: mongoose.Types.ObjectId
|
|
13
|
+
},
|
|
14
|
+
scriptId: {
|
|
15
|
+
$type: mongoose.Types.ObjectId,
|
|
16
|
+
$required: true
|
|
17
|
+
},
|
|
18
|
+
script: {
|
|
19
|
+
$type: 'string'
|
|
20
|
+
},
|
|
21
|
+
dryRun: {
|
|
22
|
+
$type: 'boolean',
|
|
23
|
+
$default: false
|
|
24
|
+
},
|
|
25
|
+
roles: {
|
|
26
|
+
$type: ['string']
|
|
27
|
+
},
|
|
28
|
+
maxTimeMS: {
|
|
29
|
+
$type: 'number'
|
|
30
|
+
},
|
|
31
|
+
readPreference: {
|
|
32
|
+
$type: 'string',
|
|
33
|
+
$enum: ['secondary', 'secondaryPreferred', 'primary']
|
|
34
|
+
}
|
|
35
|
+
}).compile('ExecuteScriptParams');
|
|
36
|
+
|
|
37
|
+
module.exports = ({ db, studioConnection, options }) => async function executeScript(params) {
|
|
38
|
+
const { initiatedById, scriptId, script, dryRun, roles } = new ExecuteScriptParams(params);
|
|
39
|
+
|
|
40
|
+
await authorize('Script.executeScript', roles);
|
|
41
|
+
|
|
42
|
+
const Script = studioConnection.model('__Studio_Script');
|
|
43
|
+
const scriptDoc = await Script.findById(scriptId);
|
|
44
|
+
if (scriptDoc == null) {
|
|
45
|
+
throw new Error('Script not found');
|
|
46
|
+
}
|
|
47
|
+
if (initiatedById && scriptDoc.userId?.toString() !== initiatedById.toString()) {
|
|
48
|
+
throw new Error('Unauthorized');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let output = null;
|
|
52
|
+
let error = null;
|
|
53
|
+
|
|
54
|
+
if (script != null) {
|
|
55
|
+
scriptDoc.script = script;
|
|
56
|
+
}
|
|
57
|
+
scriptDoc.dryRun = dryRun;
|
|
58
|
+
scriptDoc.executionStartedAt = new Date();
|
|
59
|
+
scriptDoc.executionFinishedAt = null;
|
|
60
|
+
scriptDoc.executionResult = null;
|
|
61
|
+
await scriptDoc.save();
|
|
62
|
+
|
|
63
|
+
const sandbox = createSandbox(db, { ...readOperationOptions(options, params) });
|
|
64
|
+
try {
|
|
65
|
+
output = await sandbox.runScript({ script: scriptDoc.script, dryRun });
|
|
66
|
+
} catch (err) {
|
|
67
|
+
error = err;
|
|
68
|
+
} finally {
|
|
69
|
+
try {
|
|
70
|
+
await sandbox.close();
|
|
71
|
+
} catch (_) {
|
|
72
|
+
// Ignore sandbox cleanup errors so they do not mask the primary result.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
scriptDoc.executionFinishedAt = new Date();
|
|
77
|
+
scriptDoc.executionResult = { output, logs: sandbox.getLogs(), error: error?.message ?? null };
|
|
78
|
+
await scriptDoc.save();
|
|
79
|
+
|
|
80
|
+
if (error != null) {
|
|
81
|
+
throw new Error(`Script execution failed: ${error.message}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { script: scriptDoc };
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
module.exports.paramsType = ExecuteScriptParams;
|
|
88
|
+
module.exports.tags = [];
|
|
89
|
+
module.exports.mcp = false;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Archetype = require('archetype');
|
|
4
|
+
const mongoose = require('mongoose');
|
|
5
|
+
|
|
6
|
+
const authorize = require('../../authorize');
|
|
7
|
+
const operationOptions = require('../../helpers/operationOptions');
|
|
8
|
+
|
|
9
|
+
const GetScriptParams = new Archetype({
|
|
10
|
+
initiatedById: {
|
|
11
|
+
$type: mongoose.Types.ObjectId
|
|
12
|
+
},
|
|
13
|
+
scriptId: {
|
|
14
|
+
$type: mongoose.Types.ObjectId,
|
|
15
|
+
$required: true
|
|
16
|
+
},
|
|
17
|
+
roles: {
|
|
18
|
+
$type: ['string']
|
|
19
|
+
},
|
|
20
|
+
maxTimeMS: {
|
|
21
|
+
$type: 'number'
|
|
22
|
+
}
|
|
23
|
+
}).compile('GetScriptParams');
|
|
24
|
+
|
|
25
|
+
module.exports = ({ studioConnection, options }) => async function getScript(params) {
|
|
26
|
+
const { initiatedById, scriptId, roles } = new GetScriptParams(params);
|
|
27
|
+
|
|
28
|
+
await authorize('Script.getScript', roles);
|
|
29
|
+
|
|
30
|
+
const Script = studioConnection.model('__Studio_Script');
|
|
31
|
+
const script = await Script.findById(scriptId).setOptions(operationOptions(options, params));
|
|
32
|
+
if (script == null) {
|
|
33
|
+
throw new Error('Script not found');
|
|
34
|
+
}
|
|
35
|
+
if (initiatedById && script.userId?.toString() !== initiatedById.toString()) {
|
|
36
|
+
throw new Error('Unauthorized');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { script };
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
module.exports.paramsType = GetScriptParams;
|
|
43
|
+
module.exports.tags = ['readOnly'];
|
|
@@ -2,23 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
const Archetype = require('archetype');
|
|
4
4
|
const mongoose = require('mongoose');
|
|
5
|
+
const operationOptions = require('../../helpers/operationOptions');
|
|
5
6
|
|
|
6
7
|
const CancelTaskParams = new Archetype({
|
|
7
8
|
taskId: {
|
|
8
9
|
$type: mongoose.Types.ObjectId,
|
|
9
10
|
$required: true
|
|
11
|
+
},
|
|
12
|
+
maxTimeMS: {
|
|
13
|
+
$type: 'number'
|
|
10
14
|
}
|
|
11
15
|
}).compile('CancelTaskParams');
|
|
12
16
|
|
|
13
|
-
module.exports = ({ db }) => async function cancelTask(params) {
|
|
17
|
+
module.exports = ({ db, options }) => async function cancelTask(params) {
|
|
14
18
|
params = new CancelTaskParams(params);
|
|
15
19
|
const { taskId } = params;
|
|
16
20
|
const { Task } = db.models;
|
|
17
21
|
|
|
18
|
-
const task = await Task.findOne({ _id: taskId }).
|
|
22
|
+
const task = await Task.findOne({ _id: taskId }).
|
|
23
|
+
setOptions(operationOptions(options, params)).
|
|
24
|
+
orFail();
|
|
19
25
|
|
|
20
26
|
const cancelledTask = await Task.cancelTask({ _id: taskId });
|
|
21
27
|
return {
|
|
22
28
|
task: cancelledTask
|
|
23
29
|
};
|
|
24
|
-
};
|
|
30
|
+
};
|
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
const Archetype = require('archetype');
|
|
4
4
|
const escape = require('regexp.escape');
|
|
5
|
+
const operationOptions = require('../../helpers/operationOptions');
|
|
5
6
|
|
|
6
7
|
const GetTaskOverviewParams = new Archetype({
|
|
7
8
|
start: { $type: Date },
|
|
8
9
|
end: { $type: Date },
|
|
9
10
|
status: { $type: 'string' },
|
|
10
|
-
name: { $type: 'string' }
|
|
11
|
+
name: { $type: 'string' },
|
|
12
|
+
maxTimeMS: {
|
|
13
|
+
$type: 'number'
|
|
14
|
+
}
|
|
11
15
|
}).compile('GetTaskOverviewParams');
|
|
12
16
|
|
|
13
17
|
/** Statuses shown on the Task overview page. */
|
|
@@ -34,7 +38,7 @@ function buildMatch(params) {
|
|
|
34
38
|
return match;
|
|
35
39
|
}
|
|
36
40
|
|
|
37
|
-
module.exports = ({ db }) => async function getTaskOverview(params) {
|
|
41
|
+
module.exports = ({ db, options }) => async function getTaskOverview(params) {
|
|
38
42
|
params = new GetTaskOverviewParams(params);
|
|
39
43
|
if (typeof params.status === 'string') params.status = params.status.trim();
|
|
40
44
|
if (typeof params.name === 'string') params.name = params.name.trim();
|
|
@@ -93,7 +97,7 @@ module.exports = ({ db }) => async function getTaskOverview(params) {
|
|
|
93
97
|
}
|
|
94
98
|
];
|
|
95
99
|
|
|
96
|
-
const [result] = await Task.aggregate(pipeline);
|
|
100
|
+
const [result] = await Task.aggregate(pipeline).option(operationOptions(options, params));
|
|
97
101
|
|
|
98
102
|
return {
|
|
99
103
|
statusCounts: result.statusCounts?.[0] ?? {},
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const Archetype = require('archetype');
|
|
4
4
|
const escape = require('regexp.escape');
|
|
5
|
+
const operationOptions = require('../../helpers/operationOptions');
|
|
5
6
|
|
|
6
7
|
const GetTasksParams = new Archetype({
|
|
7
8
|
start: { $type: Date },
|
|
@@ -9,7 +10,10 @@ const GetTasksParams = new Archetype({
|
|
|
9
10
|
status: { $type: 'string' },
|
|
10
11
|
name: { $type: 'string' },
|
|
11
12
|
skip: { $type: 'number', $default: 0 },
|
|
12
|
-
limit: { $type: 'number', $default: 100 }
|
|
13
|
+
limit: { $type: 'number', $default: 100 },
|
|
14
|
+
maxTimeMS: {
|
|
15
|
+
$type: 'number'
|
|
16
|
+
}
|
|
13
17
|
}).compile('GetTasksParams');
|
|
14
18
|
|
|
15
19
|
const ALL_STATUSES = ['pending', 'in_progress', 'succeeded', 'failed', 'cancelled', 'unknown'];
|
|
@@ -55,7 +59,7 @@ const TASK_PROJECT_STAGE = {
|
|
|
55
59
|
parameters: '$payload'
|
|
56
60
|
};
|
|
57
61
|
|
|
58
|
-
module.exports = ({ db }) => async function getTasks(params) {
|
|
62
|
+
module.exports = ({ db, options }) => async function getTasks(params) {
|
|
59
63
|
params = new GetTasksParams(params);
|
|
60
64
|
if (typeof params.status === 'string') params.status = params.status.trim();
|
|
61
65
|
if (typeof params.name === 'string') params.name = params.name.trim();
|
|
@@ -96,7 +100,7 @@ module.exports = ({ db }) => async function getTasks(params) {
|
|
|
96
100
|
}
|
|
97
101
|
];
|
|
98
102
|
|
|
99
|
-
const [result] = await Task.aggregate(pipeline);
|
|
103
|
+
const [result] = await Task.aggregate(pipeline).option(operationOptions(options, params));
|
|
100
104
|
const tasks = result.tasks || [];
|
|
101
105
|
const numDocs = (result.count && result.count[0] && result.count[0].total) || 0;
|
|
102
106
|
const statusCounts = result.statusCounts?.[0] ?? {};
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const Archetype = require('archetype');
|
|
4
|
+
const operationOptions = require('../../helpers/operationOptions');
|
|
4
5
|
|
|
5
6
|
const GetTasksOverTimeParams = new Archetype({
|
|
6
7
|
start: { $type: Date },
|
|
7
8
|
end: { $type: Date },
|
|
8
|
-
bucketSizeMs: { $type: 'number' }
|
|
9
|
+
bucketSizeMs: { $type: 'number' },
|
|
10
|
+
maxTimeMS: {
|
|
11
|
+
$type: 'number'
|
|
12
|
+
}
|
|
9
13
|
}).compile('GetTasksOverTimeParams');
|
|
10
14
|
|
|
11
15
|
const TRACKED_STATUSES = ['succeeded', 'failed', 'cancelled'];
|
|
12
16
|
|
|
13
|
-
module.exports = ({ db }) => async function getTasksOverTime(params) {
|
|
17
|
+
module.exports = ({ db, options }) => async function getTasksOverTime(params) {
|
|
14
18
|
params = new GetTasksOverTimeParams(params);
|
|
15
19
|
const { Task } = db.models;
|
|
16
20
|
const { start, end, bucketSizeMs } = params;
|
|
@@ -54,7 +58,7 @@ module.exports = ({ db }) => async function getTasksOverTime(params) {
|
|
|
54
58
|
{ $sort: { _id: 1 } }
|
|
55
59
|
];
|
|
56
60
|
|
|
57
|
-
const results = await Task.aggregate(pipeline);
|
|
61
|
+
const results = await Task.aggregate(pipeline).option(operationOptions(options, params));
|
|
58
62
|
|
|
59
63
|
return results.map(r => {
|
|
60
64
|
const bucket = { timestamp: r._id, succeeded: 0, failed: 0, cancelled: 0 };
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const Archetype = require('archetype');
|
|
4
4
|
const mongoose = require('mongoose');
|
|
5
|
+
const operationOptions = require('../../helpers/operationOptions');
|
|
5
6
|
|
|
6
7
|
const RescheduleTaskParams = new Archetype({
|
|
7
8
|
taskId: {
|
|
@@ -11,15 +12,20 @@ const RescheduleTaskParams = new Archetype({
|
|
|
11
12
|
scheduledAt: {
|
|
12
13
|
$type: Date,
|
|
13
14
|
$required: true
|
|
15
|
+
},
|
|
16
|
+
maxTimeMS: {
|
|
17
|
+
$type: 'number'
|
|
14
18
|
}
|
|
15
19
|
}).compile('RescheduleTaskParams');
|
|
16
20
|
|
|
17
|
-
module.exports = ({ db }) => async function rescheduleTask(params) {
|
|
21
|
+
module.exports = ({ db, options }) => async function rescheduleTask(params) {
|
|
18
22
|
params = new RescheduleTaskParams(params);
|
|
19
23
|
const { taskId, scheduledAt } = params;
|
|
20
24
|
const { Task } = db.models;
|
|
21
25
|
|
|
22
|
-
const task = await Task.findOne({ _id: taskId }).
|
|
26
|
+
const task = await Task.findOne({ _id: taskId }).
|
|
27
|
+
setOptions(operationOptions(options, params)).
|
|
28
|
+
orFail();
|
|
23
29
|
|
|
24
30
|
if (scheduledAt < Date.now()) {
|
|
25
31
|
throw new Error('Cannot reschedule a task for the past');
|
|
@@ -36,4 +42,4 @@ module.exports = ({ db }) => async function rescheduleTask(params) {
|
|
|
36
42
|
return {
|
|
37
43
|
task
|
|
38
44
|
};
|
|
39
|
-
};
|
|
45
|
+
};
|
|
@@ -2,24 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
const Archetype = require('archetype');
|
|
4
4
|
const mongoose = require('mongoose');
|
|
5
|
+
const operationOptions = require('../../helpers/operationOptions');
|
|
5
6
|
|
|
6
7
|
const RunTaskParams = new Archetype({
|
|
7
8
|
taskId: {
|
|
8
9
|
$type: mongoose.Types.ObjectId,
|
|
9
10
|
$required: true
|
|
11
|
+
},
|
|
12
|
+
maxTimeMS: {
|
|
13
|
+
$type: 'number'
|
|
10
14
|
}
|
|
11
15
|
}).compile('RunTaskParams');
|
|
12
16
|
|
|
13
|
-
module.exports = ({ db }) => async function runTask(params) {
|
|
17
|
+
module.exports = ({ db, options }) => async function runTask(params) {
|
|
14
18
|
params = new RunTaskParams(params);
|
|
15
19
|
const { taskId } = params;
|
|
16
20
|
const { Task } = db.models;
|
|
17
21
|
|
|
18
|
-
const task = await Task.findOne({ _id: taskId }).
|
|
22
|
+
const task = await Task.findOne({ _id: taskId }).
|
|
23
|
+
setOptions(operationOptions(options, params)).
|
|
24
|
+
orFail();
|
|
19
25
|
|
|
20
26
|
const executedTask = await Task.execute(task);
|
|
21
27
|
|
|
22
28
|
return {
|
|
23
29
|
task: executedTask
|
|
24
30
|
};
|
|
25
|
-
};
|
|
31
|
+
};
|
package/backend/actions/index.js
CHANGED
|
@@ -5,5 +5,6 @@ exports.ChatThread = require('./ChatThread');
|
|
|
5
5
|
exports.Dashboard = require('./Dashboard');
|
|
6
6
|
exports.getCapabilities = require('./getCapabilities');
|
|
7
7
|
exports.Model = require('./Model');
|
|
8
|
+
exports.Script = require('./Script');
|
|
8
9
|
exports.status = require('./status');
|
|
9
10
|
exports.Task = require('./Task');
|
package/backend/authorize.js
CHANGED
|
@@ -14,6 +14,7 @@ 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.createChatMessage': ['owner', 'admin', 'member'],
|
|
17
18
|
'Model.analyzeSchema': ['owner', 'admin', 'member', 'readonly'],
|
|
18
19
|
'Model.updateDocument': ['owner', 'admin', 'member'],
|
|
19
20
|
'Model.validateDocument': ['owner', 'admin', 'member', 'readonly'],
|
|
@@ -24,6 +25,7 @@ const actionsToRequiredRoles = {
|
|
|
24
25
|
'Model.executeDocumentScript': ['owner', 'admin', 'member'],
|
|
25
26
|
'Model.exportQueryResults': ['owner', 'admin', 'member', 'readonly'],
|
|
26
27
|
'Model.getDocument': ['owner', 'admin', 'member', 'readonly'],
|
|
28
|
+
'Model.getCollectionInfo': ['owner', 'admin', 'member', 'readonly'],
|
|
27
29
|
'Model.getDocuments': ['owner', 'admin', 'member', 'readonly'],
|
|
28
30
|
'Model.getDocumentsStream': ['owner', 'admin', 'member', 'readonly'],
|
|
29
31
|
'Model.getSuggestedProjection': ['owner', 'admin', 'member', 'readonly'],
|
|
@@ -32,7 +34,10 @@ const actionsToRequiredRoles = {
|
|
|
32
34
|
'Model.listModels': ['owner', 'admin', 'member', 'readonly'],
|
|
33
35
|
'Model.streamDocumentChanges': ['owner', 'admin', 'member', 'readonly'],
|
|
34
36
|
'Model.streamChatMessage': ['owner', 'admin', 'member', 'readonly'],
|
|
35
|
-
'Model.updateDocuments': ['owner', 'admin', 'member']
|
|
37
|
+
'Model.updateDocuments': ['owner', 'admin', 'member'],
|
|
38
|
+
'Script.createScript': ['owner', 'admin', 'member', 'readonly'],
|
|
39
|
+
'Script.executeScript': ['owner', 'admin', 'member', 'readonly'],
|
|
40
|
+
'Script.getScript': ['owner', 'admin', 'member', 'readonly']
|
|
36
41
|
};
|
|
37
42
|
|
|
38
43
|
module.exports = function authorize(action, roles) {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const mongoose = require('mongoose');
|
|
4
|
+
|
|
5
|
+
const scriptSchema = new mongoose.Schema({
|
|
6
|
+
script: {
|
|
7
|
+
type: String,
|
|
8
|
+
required: true
|
|
9
|
+
},
|
|
10
|
+
executionResult: new mongoose.Schema({
|
|
11
|
+
output: mongoose.Schema.Types.Mixed,
|
|
12
|
+
logs: String,
|
|
13
|
+
error: String
|
|
14
|
+
}, { _id: false }),
|
|
15
|
+
userId: {
|
|
16
|
+
type: mongoose.Schema.Types.ObjectId
|
|
17
|
+
},
|
|
18
|
+
dryRun: {
|
|
19
|
+
type: Boolean
|
|
20
|
+
},
|
|
21
|
+
executionStartedAt: {
|
|
22
|
+
type: Date
|
|
23
|
+
},
|
|
24
|
+
executionFinishedAt: {
|
|
25
|
+
type: Date
|
|
26
|
+
}
|
|
27
|
+
}, { timestamps: true });
|
|
28
|
+
|
|
29
|
+
module.exports = scriptSchema;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const omitNullish = require('./omitNullish');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Options to apply to a MongoDB operation: the configured query time limit,
|
|
7
|
+
* narrowed by any limit the request itself carries.
|
|
8
|
+
*
|
|
9
|
+
* Per-request limits arrive as params, which trusted middleware sets from the
|
|
10
|
+
* caller's authorization: an MCP OAuth grant, for example, caps every operation
|
|
11
|
+
* the AI client runs. They are intersected with the host application's
|
|
12
|
+
* configuration rather than replacing it, so a request can only ever ask for
|
|
13
|
+
* less than the host allows, never more.
|
|
14
|
+
*/
|
|
15
|
+
module.exports = function operationOptions(options, params, extra) {
|
|
16
|
+
return omitNullish({ ...extra, maxTimeMS: resolveMaxTimeMS(options, params) });
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
module.exports.resolveMaxTimeMS = resolveMaxTimeMS;
|
|
20
|
+
|
|
21
|
+
function resolveMaxTimeMS(options, params) {
|
|
22
|
+
const requested = Number(params?.maxTimeMS);
|
|
23
|
+
const limits = [options?.maxTimeMS, Number.isFinite(requested) && requested > 0 ? requested : null].
|
|
24
|
+
filter(limit => limit != null);
|
|
25
|
+
return limits.length === 0 ? null : Math.min(...limits);
|
|
26
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const operationOptions = require('./operationOptions');
|
|
4
|
+
|
|
5
|
+
const readPreferenceRank = {
|
|
6
|
+
secondary: 0,
|
|
7
|
+
secondaryPreferred: 1,
|
|
8
|
+
primary: 2
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Options to apply to a read against the application database: everything
|
|
13
|
+
* `operationOptions()` resolves, plus the read preference. MCP OAuth grants use
|
|
14
|
+
* the read preference to keep AI clients off the primary, so like the query time
|
|
15
|
+
* limit it is resolved to whichever of the configured and requested values is
|
|
16
|
+
* more restrictive.
|
|
17
|
+
*/
|
|
18
|
+
module.exports = function readOperationOptions(options, params, extra) {
|
|
19
|
+
return operationOptions(options, params, {
|
|
20
|
+
...extra,
|
|
21
|
+
readPreference: resolveReadPreference(options, params)
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function resolveReadPreference(options, params) {
|
|
26
|
+
const preferences = [options?.readPreference, params?.readPreference].
|
|
27
|
+
filter(preference => readPreferenceRank[preference] != null);
|
|
28
|
+
return preferences.length === 0 ?
|
|
29
|
+
null :
|
|
30
|
+
preferences.reduce((result, preference) => readPreferenceRank[preference] < readPreferenceRank[result] ? preference : result);
|
|
31
|
+
}
|
package/backend/index.js
CHANGED
|
@@ -8,6 +8,7 @@ const chatMessageSchema = require('./db/chatMessageSchema');
|
|
|
8
8
|
const chatThreadSchema = require('./db/chatThreadSchema');
|
|
9
9
|
const dashboardSchema = require('./db/dashboardSchema');
|
|
10
10
|
const dashboardResultSchema = require('./db/dashboardResultSchema');
|
|
11
|
+
const scriptSchema = require('./db/scriptSchema');
|
|
11
12
|
|
|
12
13
|
module.exports = function backend(db, studioConnection, options) {
|
|
13
14
|
db = db || mongoose.connection;
|
|
@@ -16,10 +17,11 @@ module.exports = function backend(db, studioConnection, options) {
|
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
studioConnection = studioConnection ?? db;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
studioConnection.model('__Studio_Dashboard', dashboardSchema, 'studio__dashboards');
|
|
21
|
+
studioConnection.model('__Studio_DashboardResult', dashboardResultSchema, 'studio__dashboardResults');
|
|
22
|
+
studioConnection.model('__Studio_ChatMessage', chatMessageSchema, 'studio__chatMessages');
|
|
23
|
+
studioConnection.model('__Studio_ChatThread', chatThreadSchema, 'studio__chatThreads');
|
|
24
|
+
studioConnection.model('__Studio_Script', scriptSchema, 'studio__scripts');
|
|
23
25
|
|
|
24
26
|
let changeStream = null;
|
|
25
27
|
if (options?.changeStream) {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { defaultMothershipURL } = require('../../constants');
|
|
4
|
+
|
|
5
|
+
async function post(path, params, options) {
|
|
6
|
+
const mothershipUrl = options?._mothershipUrl || defaultMothershipURL;
|
|
7
|
+
const mcpOAuthUrl = `${new URL(mothershipUrl).origin}/mcp-oauth`;
|
|
8
|
+
const response = await fetch(`${mcpOAuthUrl}/${path}`, {
|
|
9
|
+
method: 'POST',
|
|
10
|
+
headers: { 'Content-Type': 'application/json' },
|
|
11
|
+
body: JSON.stringify(params)
|
|
12
|
+
});
|
|
13
|
+
const data = await response.json().catch(() => ({}));
|
|
14
|
+
if (!response.ok) {
|
|
15
|
+
const error = new Error(data?.message || `Mothership request failed with status ${response.status}`);
|
|
16
|
+
error.status = response.status;
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
return data;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
exports.MCPOAuthToken = {
|
|
23
|
+
introspectMCPAccessToken: (params, options) => post('MCPOAuthToken/introspectMCPAccessToken', params, options)
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
exports.Workspace = {
|
|
27
|
+
registerMCPResource: (params, options) => post('Workspace/registerMCPResource', params, options)
|
|
28
|
+
};
|