@mongoosejs/studio 0.3.11 → 0.4.1

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 (45) hide show
  1. package/README.md +9 -0
  2. package/backend/actions/ChatThread/createChatMessage.js +15 -3
  3. package/backend/actions/ChatThread/createChatThread.js +8 -1
  4. package/backend/actions/ChatThread/index.js +1 -0
  5. package/backend/actions/ChatThread/streamChatMessage.js +49 -14
  6. package/backend/actions/ChatThread/toggleAgentMode.js +37 -0
  7. package/backend/actions/Model/getDocuments.js +7 -4
  8. package/backend/actions/Model/getDocumentsStream.js +7 -4
  9. package/backend/actions/Model/streamChatMessage.js +15 -4
  10. package/backend/authorize.js +1 -0
  11. package/backend/chatAgent/agentSystemPrompt.js +45 -0
  12. package/backend/chatAgent/agentToolMetadata.js +20 -0
  13. package/backend/chatAgent/getAgentTools.js +140 -0
  14. package/backend/db/chatMessageSchema.js +5 -0
  15. package/backend/db/chatThreadSchema.js +4 -0
  16. package/backend/helpers/isBindIPConnection.js +8 -0
  17. package/backend/helpers/isLocalhostConnection.js +8 -0
  18. package/backend/helpers/normalizeBindIPOption.js +16 -0
  19. package/backend/helpers/parseProjectionParam.js +180 -0
  20. package/backend/integrations/callLLM.js +4 -2
  21. package/backend/integrations/streamLLM.js +12 -4
  22. package/eslint.config.js +2 -1
  23. package/express.js +16 -0
  24. package/frontend/public/app.js +913 -110
  25. package/frontend/public/tw.css +34 -8
  26. package/frontend/src/api.js +6 -0
  27. package/frontend/src/chat/chat-message/chat-message.html +12 -1
  28. package/frontend/src/chat/chat-message/chat-message.js +8 -0
  29. package/frontend/src/chat/chat-message-script/chat-message-script.html +1 -1
  30. package/frontend/src/chat/chat.html +91 -44
  31. package/frontend/src/chat/chat.js +119 -19
  32. package/frontend/src/create-document/create-document.js +3 -0
  33. package/frontend/src/detail-objectid/detail-objectid.html +1 -0
  34. package/frontend/src/detail-objectid/detail-objectid.js +67 -0
  35. package/frontend/src/document-details/document-property/document-property.html +8 -3
  36. package/frontend/src/document-details/document-property/document-property.js +7 -0
  37. package/frontend/src/document-details/objectid-view-mode-picker/objectid-view-mode-picker.html +12 -0
  38. package/frontend/src/document-details/objectid-view-mode-picker/objectid-view-mode-picker.js +19 -0
  39. package/frontend/src/models/document-search/document-search.js +6 -0
  40. package/frontend/src/models/models.html +6 -8
  41. package/frontend/src/models/models.js +257 -85
  42. package/frontend/src/models/projection-search/projection-search.html +27 -0
  43. package/frontend/src/models/projection-search/projection-search.js +226 -0
  44. package/index.d.ts +1 -1
  45. package/package.json +3 -2
@@ -0,0 +1,180 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Parse a raw projection input string into a Query.select()-compatible projection.
5
+ *
6
+ * Supports:
7
+ * - `name email`
8
+ * - `-password`
9
+ * - `+email`
10
+ * - `{ deletedAt: 1 }`
11
+ * - `{ password: 0 }`
12
+ *
13
+ * @param {Object} options
14
+ * @param {string|undefined} options.projectionInput
15
+ * @param {string[]} options.schemaPaths
16
+ * @returns {string|object|null}
17
+ */
18
+ function parseProjectionParam({ projectionInput, schemaPaths }) {
19
+ return parseProjectionInput(projectionInput, schemaPaths);
20
+ }
21
+
22
+ function parseProjectionInput(projectionInput, schemaPaths) {
23
+ if (projectionInput == null || typeof projectionInput !== 'string') {
24
+ return null;
25
+ }
26
+ const trimmed = projectionInput.trim();
27
+ if (!trimmed) {
28
+ return null;
29
+ }
30
+
31
+ if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
32
+ return parseProjectionObjectNotation(trimmed, schemaPaths);
33
+ }
34
+
35
+ const tokens = normalizeProjectionTokens(trimmed);
36
+ if (tokens === null || tokens.length === 0) {
37
+ return null;
38
+ }
39
+
40
+ const includeKeys = [];
41
+ const excludeKeys = [];
42
+
43
+ for (const rawToken of tokens) {
44
+ const token = rawToken.trim();
45
+ if (!token) {
46
+ continue;
47
+ }
48
+
49
+ const prefix = token[0];
50
+ if (prefix === '-') {
51
+ const path = token.slice(1).trim();
52
+ if (!path) {
53
+ return null;
54
+ }
55
+ excludeKeys.push(path);
56
+ } else if (prefix === '+') {
57
+ const path = token.slice(1).trim();
58
+ if (!path) {
59
+ return null;
60
+ }
61
+ includeKeys.push(path);
62
+ } else {
63
+ includeKeys.push(token);
64
+ }
65
+ }
66
+
67
+ if (includeKeys.length > 0 && excludeKeys.length > 0) {
68
+ const includeSet = new Set(includeKeys);
69
+ for (const path of excludeKeys) {
70
+ const ex = path;
71
+ for (const key of Array.from(includeSet)) {
72
+ if (key.toLowerCase() === ex.toLowerCase()) {
73
+ includeSet.delete(key);
74
+ }
75
+ }
76
+ }
77
+ if (includeSet.size === 0) {
78
+ return null;
79
+ }
80
+ return Array.from(includeSet).join(' ');
81
+ }
82
+
83
+ if (excludeKeys.length > 0) {
84
+ return excludeKeys.map(path => `-${path}`).join(' ');
85
+ }
86
+
87
+ return includeKeys.join(' ');
88
+ }
89
+
90
+ function normalizeProjectionTokens(trimmed) {
91
+ const rawTokens = trimmed.split(/[,\s]+/).filter(Boolean);
92
+ const tokens = [];
93
+ for (let i = 0; i < rawTokens.length; i++) {
94
+ const token = rawTokens[i];
95
+ if (token === '-' || token === '+') {
96
+ if (i + 1 >= rawTokens.length) {
97
+ return null;
98
+ }
99
+ tokens.push(token + rawTokens[++i]);
100
+ } else {
101
+ tokens.push(token);
102
+ }
103
+ }
104
+ return tokens;
105
+ }
106
+
107
+ function parseProjectionObjectNotation(trimmed, schemaPaths) {
108
+ const body = trimmed.slice(1, -1).trim();
109
+ if (!body) {
110
+ return null;
111
+ }
112
+
113
+ const pairRe = /(?:^|,)\s*(?:"([^"]+)"|'([^']+)'|([a-zA-Z_$][\w.$]*))\s*:\s*("[^"]*"|'[^']*'|[^,]+?)\s*(?=,|$)/g;
114
+ const includeKeys = [];
115
+ const excludeKeys = [];
116
+ let matchedChars = '';
117
+ let match;
118
+
119
+ while ((match = pairRe.exec(body)) !== null) {
120
+ matchedChars += match[0];
121
+ const key = (match[1] || match[2] || match[3] || '').trim();
122
+ const rawValue = (match[4] || '').trim();
123
+ if (!key || !rawValue) {
124
+ return null;
125
+ }
126
+ if (key === '__proto__' || key === 'constructor') {
127
+ throw new Error(`Invalid key: ${key}`);
128
+ }
129
+
130
+ const valueLower = rawValue.replace(/^['"]|['"]$/g, '').trim().toLowerCase();
131
+ const isInclude = valueLower === '1' || valueLower === 'true';
132
+ const isExclude = valueLower === '0' || valueLower === 'false';
133
+ if (!isInclude && !isExclude) {
134
+ return null;
135
+ }
136
+ if (isInclude) {
137
+ includeKeys.push(key);
138
+ } else {
139
+ excludeKeys.push(key);
140
+ }
141
+ }
142
+
143
+ const normalizedBody = body.replace(/\s+/g, '');
144
+ const normalizedMatched = matchedChars.replace(/\s+/g, '').replace(/^,/, '');
145
+ if (!normalizedMatched || normalizedMatched !== normalizedBody) {
146
+ return null;
147
+ }
148
+
149
+ if (includeKeys.length > 0 && excludeKeys.length > 0) {
150
+ const includeSet = new Set(includeKeys);
151
+ for (const path of excludeKeys) {
152
+ const ex = path;
153
+ for (const key of Array.from(includeSet)) {
154
+ if (key.toLowerCase() === ex.toLowerCase()) {
155
+ includeSet.delete(key);
156
+ }
157
+ }
158
+ }
159
+ if (includeSet.size === 0) {
160
+ return null;
161
+ }
162
+ return Array.from(includeSet).join(' ');
163
+ }
164
+
165
+ if (excludeKeys.length > 0) {
166
+ const projection = {};
167
+ for (const path of excludeKeys) {
168
+ projection[path] = 0;
169
+ }
170
+ return Object.keys(projection).length > 0 ? projection : null;
171
+ }
172
+
173
+ const projection = {};
174
+ for (const path of includeKeys) {
175
+ projection[path] = 1;
176
+ }
177
+ return Object.keys(projection).length > 0 ? projection : null;
178
+ }
179
+
180
+ module.exports = parseProjectionParam;
@@ -3,7 +3,7 @@
3
3
  const { createAnthropic } = require('@ai-sdk/anthropic');
4
4
  const { createGoogleGenerativeAI } = require('@ai-sdk/google');
5
5
  const { createOpenAI } = require('@ai-sdk/openai');
6
- const { generateText } = require('ai');
6
+ const { generateText, stepCountIs } = require('ai');
7
7
  const { defaultMothershipURL } = require('../../constants');
8
8
 
9
9
  module.exports = async function callLLM(messages, system, options) {
@@ -46,7 +46,9 @@ module.exports = async function callLLM(messages, system, options) {
46
46
  return generateText({
47
47
  model: provider(model),
48
48
  system,
49
- messages
49
+ messages,
50
+ tools: options?.tools,
51
+ stopWhen: options?.tools ? stepCountIs(10) : undefined
50
52
  });
51
53
  }
52
54
 
@@ -3,7 +3,7 @@
3
3
  const { createAnthropic } = require('@ai-sdk/anthropic');
4
4
  const { createGoogleGenerativeAI } = require('@ai-sdk/google');
5
5
  const { createOpenAI } = require('@ai-sdk/openai');
6
- const { streamText } = require('ai');
6
+ const { streamText, stepCountIs } = require('ai');
7
7
  const { defaultMothershipURL } = require('../../constants');
8
8
 
9
9
  module.exports = async function* streamLLM(messages, system, options) {
@@ -44,16 +44,24 @@ module.exports = async function* streamLLM(messages, system, options) {
44
44
 
45
45
  if (provider) {
46
46
  let error = null;
47
- const { textStream } = streamText({
47
+ const { fullStream } = streamText({
48
48
  model: provider(model),
49
49
  system,
50
50
  messages,
51
+ tools: options?.tools,
52
+ stopWhen: options?.tools ? stepCountIs(10) : undefined,
51
53
  onError(err) {
52
54
  error = err.error;
53
55
  }
54
56
  });
55
- for await (const chunk of textStream) {
56
- yield chunk;
57
+ for await (const chunk of fullStream) {
58
+ if (chunk.type === 'text-delta') {
59
+ yield chunk.text;
60
+ } else if (chunk.type === 'tool-call') {
61
+ yield { toolCall: { toolName: chunk.toolName, input: chunk.input } };
62
+ } else if (chunk.type === 'tool-result') {
63
+ yield { toolResult: { toolName: chunk.toolName, output: chunk.output } };
64
+ }
57
65
  }
58
66
  if (error) {
59
67
  throw error;
package/eslint.config.js CHANGED
@@ -15,7 +15,8 @@ module.exports = defineConfig([
15
15
  fetch: true,
16
16
  setTimeout: true,
17
17
  process: true,
18
- console: true
18
+ console: true,
19
+ __dirname: true
19
20
  }
20
21
  },
21
22
  extends: [config]
package/express.js CHANGED
@@ -3,12 +3,17 @@
3
3
  const Backend = require('./backend');
4
4
  const express = require('express');
5
5
  const frontend = require('./frontend');
6
+ const isBindIPConnection = require('./backend/helpers/isBindIPConnection');
7
+ const isLocalhostConnection = require('./backend/helpers/isLocalhostConnection');
8
+ const normalizeBindIPOption = require('./backend/helpers/normalizeBindIPOption');
6
9
  const { toRoute, objectRouter } = require('extrovert');
7
10
  const { defaultMothershipURL } = require('./constants');
8
11
 
9
12
  module.exports = async function mongooseStudioExpressApp(apiUrl, conn, options) {
10
13
  const router = express.Router();
11
14
  options = options ? { changeStream: true, ...options } : { changeStream: true };
15
+ const hasBindIpOption = Object.prototype.hasOwnProperty.call(options, 'bindIp');
16
+ const bindIp = normalizeBindIPOption(options.bindIp);
12
17
 
13
18
  const mothershipUrl = options._mothershipUrl || defaultMothershipURL;
14
19
  let workspace = null;
@@ -32,6 +37,17 @@ module.exports = async function mongooseStudioExpressApp(apiUrl, conn, options)
32
37
  .then(res => res.json()));
33
38
  }
34
39
 
40
+ if (!workspace && bindIp !== null) {
41
+ router.use((req, res, next) => {
42
+ const allowed = hasBindIpOption ? isBindIPConnection(req, bindIp) : isLocalhostConnection(req);
43
+ if (!allowed) {
44
+ return res.status(403).json({ message: 'Mongoose Studio without an API key only accepts localhost or configured bindIp connections' });
45
+ }
46
+
47
+ next();
48
+ });
49
+ }
50
+
35
51
  apiUrl = apiUrl || 'api';
36
52
  const backend = Backend(conn, options.studioConnection, options);
37
53
  delete backend.services;