@mongoosejs/studio 0.5.0 → 0.5.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.
@@ -2,6 +2,7 @@
2
2
 
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
+ const createSandbox = require('../../sandbox/createSandbox');
5
6
  const mongoose = require('mongoose');
6
7
  const { defaultMothershipURL } = require('../../../constants');
7
8
 
@@ -27,7 +28,7 @@ const GetDashboardParams = new Archetype({
27
28
  }
28
29
  }).compile('GetDashboardParams');
29
30
 
30
- module.exports = ({ studioConnection, options }) => async function getDashboard(params) {
31
+ module.exports = ({ db, studioConnection, options }) => async function getDashboard(params) {
31
32
  const { $workspaceId, authorization, userId, dashboardId, evaluate, roles } = new GetDashboardParams(params);
32
33
  const Dashboard = studioConnection.model('__Studio_Dashboard');
33
34
  const DashboardResult = studioConnection.model('__Studio_DashboardResult');
@@ -38,10 +39,12 @@ module.exports = ({ studioConnection, options }) => async function getDashboard(
38
39
  const dashboard = await Dashboard.findOne({ _id: dashboardId });
39
40
  if (evaluate) {
40
41
  let result = null;
42
+ const sandbox = createSandbox({ db });
41
43
  const startExec = startDashboardEvaluate(DashboardResult, dashboardId, $workspaceId, userId);
42
44
  startExec.catch(() => {}); // Avoid unhandled promise rejections - we will handle this error later.
43
45
  try {
44
- result = await dashboard.evaluate();
46
+ result = await sandbox.runScript({ script: dashboard.code });
47
+ addDocumentSchemaPaths(result);
45
48
  } catch (error) {
46
49
  const { dashboardResult } = await startExec.then(({ dashboardResult }) => {
47
50
  if (!dashboardResult) {
@@ -56,6 +59,12 @@ module.exports = ({ studioConnection, options }) => async function getDashboard(
56
59
  );
57
60
  });
58
61
  return { dashboard, dashboardResult, error: { message: error.message } };
62
+ } finally {
63
+ try {
64
+ await sandbox.close();
65
+ } catch (_) {
66
+ // Ignore sandbox cleanup errors so they do not mask the primary result.
67
+ }
59
68
  }
60
69
 
61
70
  try {
@@ -170,3 +179,21 @@ function getSortTime(result) {
170
179
  const time = new Date(candidate).getTime();
171
180
  return Number.isNaN(time) ? 0 : time;
172
181
  }
182
+
183
+ function addDocumentSchemaPaths(result) {
184
+ const Model = result?.$document?.constructor;
185
+ if (!Model?.modelName || !Model.schema?.paths) {
186
+ return;
187
+ }
188
+
189
+ const schemaPaths = {};
190
+ for (const path of Object.keys(Model.schema.paths)) {
191
+ schemaPaths[path] = {
192
+ instance: Model.schema.paths[path].instance,
193
+ path,
194
+ ref: Model.schema.paths[path].options?.ref,
195
+ required: Model.schema.paths[path].options?.required
196
+ };
197
+ }
198
+ result.$document.schemaPaths = schemaPaths;
199
+ }
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+
3
+ const Archetype = require('archetype');
4
+ const authorize = require('../../authorize');
5
+
6
+ const DropCollectionParams = new Archetype({
7
+ model: {
8
+ $type: 'string',
9
+ $required: true
10
+ },
11
+ roles: {
12
+ $type: ['string']
13
+ }
14
+ }).compile('DropCollectionParams');
15
+
16
+ module.exports = ({ db }) => async function dropCollection(params) {
17
+ const { model, roles } = new DropCollectionParams(params);
18
+
19
+ await authorize('Model.dropCollection', roles);
20
+
21
+ const Model = db.models[model];
22
+ if (Model == null) {
23
+ throw new Error(`Model ${model} not found`);
24
+ }
25
+
26
+ await Model.collection.drop();
27
+
28
+ return { ok: true };
29
+ };
@@ -62,9 +62,7 @@ module.exports = ({ db }) => async function exportQueryResults(params, req, res)
62
62
  }
63
63
  const csv = stringify(rows);
64
64
 
65
- res.set({
66
- 'Content-Type': 'text/csv',
67
- 'Content-Disposition': `attachment; filename="${model.toLowerCase()}-export.csv"`
68
- });
65
+ res.setHeader('Content-Type', 'text/csv');
66
+ res.setHeader('Content-Disposition', `attachment; filename="${model.toLowerCase()}-export.csv"`);
69
67
  return csv;
70
68
  };
@@ -5,6 +5,7 @@ exports.createChatMessage = require('./createChatMessage');
5
5
  exports.createDocument = require('./createDocument');
6
6
  exports.deleteDocument = require('./deleteDocument');
7
7
  exports.deleteDocuments = require('./deleteDocuments');
8
+ exports.dropCollection = require('./dropCollection');
8
9
  exports.dropIndex = require('./dropIndex');
9
10
  exports.executeDocumentScript = require('./executeDocumentScript');
10
11
  exports.exportQueryResults = require('./exportQueryResults');
@@ -17,6 +17,7 @@ const actionsToRequiredRoles = {
17
17
  'Model.updateDocument': ['owner', 'admin', 'member'],
18
18
  'Model.deleteDocument': ['owner', 'admin', 'member'],
19
19
  'Model.deleteDocuments': ['owner', 'admin', 'member'],
20
+ 'Model.dropCollection': ['owner', 'admin'],
20
21
  'Model.dropIndex': ['owner', 'admin'],
21
22
  'Model.executeDocumentScript': ['owner', 'admin', 'member'],
22
23
  'Model.exportQueryResults': ['owner', 'admin', 'member', 'readonly'],
@@ -1,8 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const MongooseStudioChartColors = require('../constants/mongooseStudioChartColors');
4
3
  const mongoose = require('mongoose');
5
- const vm = require('vm');
6
4
 
7
5
  const dashboardSchema = new mongoose.Schema({
8
6
  title: {
@@ -18,34 +16,4 @@ const dashboardSchema = new mongoose.Schema({
18
16
  }
19
17
  });
20
18
 
21
- dashboardSchema.methods.evaluate = async function evaluate() {
22
- const context = vm.createContext({
23
- db: this.constructor.db,
24
- setTimeout,
25
- ObjectId: mongoose.Types.ObjectId,
26
- MongooseStudioChartColors
27
- });
28
- let result = null;
29
- result = await vm.runInContext(formatFunction(this.code), context);
30
- if (result.$document?.constructor?.modelName) {
31
- const schemaPaths = {};
32
- const Model = this.constructor.db.model(result.$document?.constructor?.modelName);
33
- for (const path of Object.keys(Model.schema.paths)) {
34
- schemaPaths[path] = {
35
- instance: Model.schema.paths[path].instance,
36
- path,
37
- ref: Model.schema.paths[path].options?.ref,
38
- required: Model.schema.paths[path].options?.required
39
- };
40
- }
41
- result.$document.schemaPaths = schemaPaths;
42
- }
43
-
44
- return result;
45
- };
46
-
47
19
  module.exports = dashboardSchema;
48
-
49
- const formatFunction = code => `(async function() {
50
- ${code}
51
- })();`;
package/backend/index.js CHANGED
@@ -20,7 +20,7 @@ module.exports = function backend(db, studioConnection, options) {
20
20
 
21
21
  let changeStream = null;
22
22
  if (options?.changeStream) {
23
- const conn = db instanceof mongoose.Mongoose ? db.connection : db;
23
+ const conn = db.connection ? db.connection : db;
24
24
  if (conn.readyState !== mongoose.Connection.STATES.connected) {
25
25
  conn._waitForConnect().then(() => {
26
26
  changeStream = conn.watch();
@@ -4,7 +4,6 @@ const { createAnthropic } = require('@ai-sdk/anthropic');
4
4
  const { createGoogleGenerativeAI } = require('@ai-sdk/google');
5
5
  const { createOpenAI } = require('@ai-sdk/openai');
6
6
  const { generateText, stepCountIs } = require('ai');
7
- const { defaultMothershipURL } = require('../../constants');
8
7
 
9
8
  module.exports = async function callLLM(messages, system, options) {
10
9
  let provider = null;
@@ -13,6 +12,7 @@ module.exports = async function callLLM(messages, system, options) {
13
12
 
14
13
  if (options?.openAIAPIKey) {
15
14
  providers.push({
15
+ name: 'OpenAI',
16
16
  provider: createOpenAI({ apiKey: options.openAIAPIKey }),
17
17
  model: options?.model ?? 'gpt-4o-mini'
18
18
  });
@@ -20,6 +20,7 @@ module.exports = async function callLLM(messages, system, options) {
20
20
 
21
21
  if (options?.anthropicAPIKey) {
22
22
  providers.push({
23
+ name: 'Anthropic',
23
24
  provider: createAnthropic({ apiKey: options.anthropicAPIKey }),
24
25
  model: options?.model ?? 'claude-haiku-4-5-20251001'
25
26
  });
@@ -27,13 +28,14 @@ module.exports = async function callLLM(messages, system, options) {
27
28
 
28
29
  if (options?.googleGeminiAPIKey) {
29
30
  providers.push({
31
+ name: 'Gemini',
30
32
  provider: createGoogleGenerativeAI({ apiKey: options.googleGeminiAPIKey }),
31
33
  model: options?.model ?? 'gemini-2.5-flash'
32
34
  });
33
35
  }
34
36
 
35
37
  if (providers.length > 1) {
36
- throw new Error('Cannot set multiple LLM API keys');
38
+ throw new Error(`Cannot set multiple LLM API keys, found ${providers.map(p => p.name).join(', ')}`);
37
39
  }
38
40
 
39
41
  if (providers.length > 0) {
@@ -52,22 +54,5 @@ module.exports = async function callLLM(messages, system, options) {
52
54
  });
53
55
  }
54
56
 
55
- const headers = { 'Content-Type': 'application/json' };
56
- const response = await fetch(`${defaultMothershipURL}/getChatCompletion`, {
57
- method: 'POST',
58
- headers,
59
- body: JSON.stringify({
60
- messages: [{ role: 'system', content: { type: 'text', text: system } }, ...messages],
61
- model: options?.model
62
- })
63
- }).then(response => {
64
- if (!response.ok) {
65
- return response.json().then(data => {
66
- throw new Error(`Mongoose Studio chat completion error: ${data.message}`);
67
- });
68
- }
69
- return response;
70
- });
71
-
72
- return await response.json().then(res => ({ text: res.response }));
57
+ throw new Error('No LLM API key configured. Set one of anthropicAPIKey, googleGeminiAPIKey, or openAIAPIKey.');
73
58
  };
@@ -4,7 +4,6 @@ const { createAnthropic } = require('@ai-sdk/anthropic');
4
4
  const { createGoogleGenerativeAI } = require('@ai-sdk/google');
5
5
  const { createOpenAI } = require('@ai-sdk/openai');
6
6
  const { streamText, stepCountIs } = require('ai');
7
- const { defaultMothershipURL } = require('../../constants');
8
7
 
9
8
  module.exports = async function* streamLLM(messages, system, options) {
10
9
  let provider = null;
@@ -13,6 +12,7 @@ module.exports = async function* streamLLM(messages, system, options) {
13
12
 
14
13
  if (options?.openAIAPIKey) {
15
14
  providers.push({
15
+ name: 'OpenAI',
16
16
  provider: createOpenAI({ apiKey: options.openAIAPIKey }),
17
17
  model: options?.model ?? 'gpt-4o-mini'
18
18
  });
@@ -20,6 +20,7 @@ module.exports = async function* streamLLM(messages, system, options) {
20
20
 
21
21
  if (options?.anthropicAPIKey) {
22
22
  providers.push({
23
+ name: 'Anthropic',
23
24
  provider: createAnthropic({ apiKey: options.anthropicAPIKey }),
24
25
  model: options?.model ?? 'claude-haiku-4-5-20251001'
25
26
  });
@@ -27,13 +28,14 @@ module.exports = async function* streamLLM(messages, system, options) {
27
28
 
28
29
  if (options?.googleGeminiAPIKey) {
29
30
  providers.push({
31
+ name: 'Gemini',
30
32
  provider: createGoogleGenerativeAI({ apiKey: options.googleGeminiAPIKey }),
31
33
  model: options?.model ?? 'gemini-2.5-flash'
32
34
  });
33
35
  }
34
36
 
35
37
  if (providers.length > 1) {
36
- throw new Error('Cannot set multiple LLM API keys');
38
+ throw new Error(`Cannot set multiple LLM API keys, found ${providers.map(p => p.name).join(', ')}`);
37
39
  }
38
40
 
39
41
  if (providers.length > 0) {
@@ -69,26 +71,5 @@ module.exports = async function* streamLLM(messages, system, options) {
69
71
  return;
70
72
  }
71
73
 
72
- // If not using OpenAI, Anthropic, or Google Gemini, fallback to Mongoose (no streaming)
73
- const headers = { 'Content-Type': 'application/json' };
74
- const response = await fetch(`${defaultMothershipURL}/getChatCompletion`, {
75
- method: 'POST',
76
- headers,
77
- body: JSON.stringify({
78
- messages: [{ role: 'system', content: { type: 'text', text: system } }, ...messages],
79
- model: options?.model
80
- })
81
- }).then(response => {
82
- if (!response.ok) {
83
- return response.json().then(data => {
84
- throw new Error(`Mongoose Studio chat completion error: ${data.message}`);
85
- });
86
- }
87
- return response;
88
- });
89
-
90
- const data = await response.json();
91
-
92
- // Simulate streaming by yielding the whole response at once as a single chunk
93
- yield data.response;
74
+ throw new Error('No LLM API key configured. Set one of anthropicAPIKey, googleGeminiAPIKey, or openAIAPIKey.');
94
75
  };
package/backend/next.js CHANGED
@@ -81,8 +81,12 @@ module.exports = function next(conn, options) {
81
81
  throw new Error(`Action ${actionName} not found`);
82
82
  }
83
83
 
84
- return actionFn(params)
84
+ return actionFn(params, req, res)
85
85
  .then(result => {
86
+ if (typeof result === 'string') {
87
+ res.status(200).send(result);
88
+ return result;
89
+ }
86
90
  res.status(200).json(result);
87
91
  return result;
88
92
  })
@@ -7,6 +7,12 @@ const { createScriptDb } = require('./createScriptDb');
7
7
 
8
8
  module.exports = function createSandbox({ db }) {
9
9
  const logs = [];
10
+
11
+ // db must be a connection, not a Mongoose instance
12
+ if (db.connection && db.connections) {
13
+ db = db.connection;
14
+ }
15
+
10
16
  const scriptDb = createScriptDb(db);
11
17
  if (!scriptDb.db.Types) {
12
18
  scriptDb.db.Types = mongoose.Types;
@@ -0,0 +1,3 @@
1
+ 'use strict';
2
+
3
+ module.exports = require('./next');
package/frontend/index.js CHANGED
@@ -35,7 +35,8 @@ module.exports = async function frontend(apiUrl, isLambda, options, workspace) {
35
35
  baseURL: apiUrl,
36
36
  isLambda,
37
37
  mothershipUrl: apiKey ? mothershipUrl : null,
38
- workspace: workspaceData
38
+ workspace: workspaceData,
39
+ hasLLMAPIKey: [options?.anthropicAPIKey, options?.googleGeminiAPIKey, options?.openAIAPIKey].some(v => v != null)
39
40
  };
40
41
 
41
42
  if (isLambda) {