@openenthrium/oe-runtime-sdk 1.7.5
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/README.md +139 -0
- package/index.js +58 -0
- package/package.json +64 -0
- package/src/data/connectionTypes.json +18587 -0
- package/src/engine/index.js +183 -0
- package/src/engine/llm.js +71 -0
- package/src/engine/promptBuilder.js +38 -0
- package/src/index.js +156 -0
- package/src/middleware/auth.js +63 -0
- package/src/providers/embedding/index.js +69 -0
- package/src/providers/llm/index.js +136 -0
- package/src/routes/admin.js +686 -0
- package/src/routes/agents.js +193 -0
- package/src/routes/apiKeys.js +55 -0
- package/src/routes/audio.js +111 -0
- package/src/routes/auth.js +83 -0
- package/src/routes/chat.js +381 -0
- package/src/routes/connectors.js +435 -0
- package/src/routes/dashboard.js +244 -0
- package/src/routes/documents.js +443 -0
- package/src/routes/embed.js +103 -0
- package/src/routes/marketplace.js +49 -0
- package/src/routes/models.js +81 -0
- package/src/routes/oauth.js +423 -0
- package/src/routes/projects.js +238 -0
- package/src/routes/settings.js +45 -0
- package/src/routes/setup.js +42 -0
- package/src/routes/sso.js +218 -0
- package/src/routes/superadmin.js +51 -0
- package/src/routes/templates.js +75 -0
- package/src/routes/threads.js +53 -0
- package/src/routes/workspaces.js +464 -0
- package/src/telemetry/bootstrap.js +39 -0
- package/src/telemetry/registration.js +16 -0
- package/src/utils/activityLog.js +16 -0
- package/src/utils/agentChain.js +141 -0
- package/src/utils/buildVisualization.js +102 -0
- package/src/utils/dlpScanner.js +57 -0
- package/src/utils/ingestionQueue.js +240 -0
- package/src/utils/prepareConnectors.js +66 -0
- package/src/utils/rag/_settings.js +6 -0
- package/src/utils/rag/chroma.js +82 -0
- package/src/utils/rag/lancedb.js +103 -0
- package/src/utils/rag/milvus.js +116 -0
- package/src/utils/rag/pgvector.js +104 -0
- package/src/utils/rag/pinecone.js +71 -0
- package/src/utils/rag/qdrant.js +106 -0
- package/src/utils/rag/weaviate.js +137 -0
- package/src/utils/rag/zilliz.js +115 -0
- package/src/utils/scheduler.js +168 -0
- package/src/utils/tier.js +106 -0
- package/src/utils/tools/adapters/_template.js +74 -0
- package/src/utils/tools/adapters/box.js +71 -0
- package/src/utils/tools/adapters/confluence.js +80 -0
- package/src/utils/tools/adapters/database.js +215 -0
- package/src/utils/tools/adapters/dropbox.js +74 -0
- package/src/utils/tools/adapters/elasticsearch.js +90 -0
- package/src/utils/tools/adapters/filesystem.js +197 -0
- package/src/utils/tools/adapters/freshdesk.js +87 -0
- package/src/utils/tools/adapters/gdrive.js +326 -0
- package/src/utils/tools/adapters/github.js +169 -0
- package/src/utils/tools/adapters/gmail.js +157 -0
- package/src/utils/tools/adapters/graphql.js +73 -0
- package/src/utils/tools/adapters/hubspot.js +101 -0
- package/src/utils/tools/adapters/image-gen.js +120 -0
- package/src/utils/tools/adapters/jira.js +86 -0
- package/src/utils/tools/adapters/kafka.js +126 -0
- package/src/utils/tools/adapters/ldap.js +118 -0
- package/src/utils/tools/adapters/mcp-client.js +138 -0
- package/src/utils/tools/adapters/mongodb.js +119 -0
- package/src/utils/tools/adapters/mqtt.js +106 -0
- package/src/utils/tools/adapters/music-gen.js +118 -0
- package/src/utils/tools/adapters/notion.js +93 -0
- package/src/utils/tools/adapters/ocr.js +107 -0
- package/src/utils/tools/adapters/onedrive.js +72 -0
- package/src/utils/tools/adapters/redis.js +104 -0
- package/src/utils/tools/adapters/rest-api.js +119 -0
- package/src/utils/tools/adapters/s3.js +142 -0
- package/src/utils/tools/adapters/search.js +80 -0
- package/src/utils/tools/adapters/sftp.js +121 -0
- package/src/utils/tools/adapters/shell.js +97 -0
- package/src/utils/tools/adapters/slack.js +83 -0
- package/src/utils/tools/adapters/speech.js +160 -0
- package/src/utils/tools/adapters/ssh.js +113 -0
- package/src/utils/tools/adapters/video-gen.js +152 -0
- package/src/utils/tools/adapters/web3.js +111 -0
- package/src/utils/tools/adapters/zendesk.js +82 -0
- package/src/utils/tools/adapters/zoho-mail.js +246 -0
- package/src/utils/tools/registry.js +229 -0
- package/src/utils/vectorStore.js +68 -0
- package/src/utils/workflowEngine.js +4 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
const router = require("express").Router();
|
|
2
|
+
const { authenticate, requireAdmin, requireManagerOrAdmin } = require("../middleware/auth");
|
|
3
|
+
const { canAddConnector, getTierFromDB } = require("../utils/tier");
|
|
4
|
+
const { logActivity } = require("../utils/activityLog");
|
|
5
|
+
|
|
6
|
+
const SUPPORTED_TYPES = [
|
|
7
|
+
"postgresql", "mysql", "mssql", "oracle", "mongodb",
|
|
8
|
+
"redis", "sqlite", "snowflake", "bigquery", "cockroachdb", "elasticsearch",
|
|
9
|
+
"rest-api", "gmail", "slack", "jira", "confluence", "notion", "hubspot",
|
|
10
|
+
"freshdesk", "zendesk", "github", "zoho-mail", "gdrive", "ssh",
|
|
11
|
+
"onedrive", "dropbox", "box",
|
|
12
|
+
// Search
|
|
13
|
+
"perplexity-search", "google-search", "bing-search",
|
|
14
|
+
// OCR
|
|
15
|
+
"azure-vision", "google-vision", "aws-textract", "tesseract-ocr",
|
|
16
|
+
// Image generation
|
|
17
|
+
"openai-image", "flux", "stable-diffusion", "ideogram",
|
|
18
|
+
// Speech & audio
|
|
19
|
+
"elevenlabs", "openai-tts", "azure-speech", "google-tts",
|
|
20
|
+
// Video generation
|
|
21
|
+
"runway", "kling", "pika",
|
|
22
|
+
// Music generation
|
|
23
|
+
"suno", "udio",
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
function generateSlug(name) {
|
|
27
|
+
return (name || "").toLowerCase().replace(/[^a-z0-9]/g, "") || "connector";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Public-to-all-authenticated: connector type catalog (needed by workspace users too)
|
|
31
|
+
router.get("/connection-masters", authenticate, async (req, res) => {
|
|
32
|
+
try {
|
|
33
|
+
const { implementedTypes } = require("../utils/tools/registry");
|
|
34
|
+
const masters = await req.db.connectionMaster.findMany({
|
|
35
|
+
orderBy: [{ category: "asc" }, { label: "asc" }],
|
|
36
|
+
});
|
|
37
|
+
res.json({ masters: masters.map(m => ({ ...m, implemented: implementedTypes.has(m.key) })) });
|
|
38
|
+
} catch (err) {
|
|
39
|
+
res.status(500).json({ error: err.message });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
router.use(authenticate, requireManagerOrAdmin);
|
|
44
|
+
|
|
45
|
+
const SENSITIVE_AUTH_KEYS = new Set([
|
|
46
|
+
"password", "privateKey", "apiToken", "apiKey", "botToken", "appPassword",
|
|
47
|
+
"bearerToken", "integrationToken", "privateAppToken", "personalAccessToken",
|
|
48
|
+
"keyFileJson", "refreshToken", "accessToken", "clientSecret",
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
// List connectors for a workspace
|
|
52
|
+
router.get("/workspaces/:workspaceId/connectors", async (req, res) => {
|
|
53
|
+
try {
|
|
54
|
+
const workspaceId = parseInt(req.params.workspaceId);
|
|
55
|
+
const [connectors, totalConnectors, tier] = await Promise.all([
|
|
56
|
+
req.db.connector.findMany({
|
|
57
|
+
where: { workspaceId },
|
|
58
|
+
orderBy: { createdAt: "asc" },
|
|
59
|
+
select: { id: true, name: true, slug: true, type: true, config: true, authConfig: true, status: true, lastTestedAt: true, createdAt: true }
|
|
60
|
+
}),
|
|
61
|
+
req.db.connector.count(),
|
|
62
|
+
getTierFromDB(req.db),
|
|
63
|
+
]);
|
|
64
|
+
// Return non-sensitive auth fields (host, port, username, email, etc.) for edit pre-fill
|
|
65
|
+
const sanitized = connectors.map(({ authConfig, ...c }) => {
|
|
66
|
+
const auth = authConfig ? JSON.parse(authConfig) : {};
|
|
67
|
+
const publicAuth = Object.fromEntries(Object.entries(auth).filter(([k]) => !SENSITIVE_AUTH_KEYS.has(k)));
|
|
68
|
+
return { ...c, publicAuth }; // slug is already in ...c
|
|
69
|
+
});
|
|
70
|
+
res.json({ connectors: sanitized, totalConnectors, tier: { maxConnectors: isFinite(tier.maxConnectors) ? tier.maxConnectors : null } });
|
|
71
|
+
} catch (err) {
|
|
72
|
+
console.error("[connectors] GET failed:", err.message);
|
|
73
|
+
res.status(500).json({ error: err.message });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Check slug availability (legacy — kept for backward compat)
|
|
78
|
+
router.get("/connectors/check-slug", async (req, res) => {
|
|
79
|
+
const { slug, excludeId } = req.query;
|
|
80
|
+
if (!slug) return res.json({ available: false });
|
|
81
|
+
const clean = slug.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
82
|
+
if (!clean) return res.json({ available: false });
|
|
83
|
+
const existing = await req.db.connector.findUnique({ where: { slug: clean }, select: { id: true } });
|
|
84
|
+
const available = !existing || (excludeId && existing.id === parseInt(excludeId));
|
|
85
|
+
res.json({ available });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Check name availability (globally unique across all workspaces)
|
|
89
|
+
router.get("/connectors/check-name", async (req, res) => {
|
|
90
|
+
const { name, excludeId } = req.query;
|
|
91
|
+
const clean = name?.trim();
|
|
92
|
+
if (!clean) return res.json({ available: false, suggestion: null });
|
|
93
|
+
const existing = await req.db.connector.findFirst({ where: { name: clean }, select: { id: true } });
|
|
94
|
+
const available = !existing || (excludeId && existing.id === parseInt(excludeId));
|
|
95
|
+
let suggestion = null;
|
|
96
|
+
if (!available) {
|
|
97
|
+
let suffix = 1;
|
|
98
|
+
while (await req.db.connector.findFirst({ where: { name: `${clean}-${suffix}` } })) suffix++;
|
|
99
|
+
suggestion = `${clean}-${suffix}`;
|
|
100
|
+
}
|
|
101
|
+
res.json({ available, suggestion });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Create connector
|
|
105
|
+
router.post("/workspaces/:workspaceId/connectors", async (req, res) => {
|
|
106
|
+
const workspaceId = parseInt(req.params.workspaceId);
|
|
107
|
+
const { name, slug: providedSlug, type, config, authConfig } = req.body;
|
|
108
|
+
|
|
109
|
+
if (!name?.trim()) return res.status(400).json({ error: "Name required" });
|
|
110
|
+
let typeIsValid = SUPPORTED_TYPES.includes(type);
|
|
111
|
+
if (!typeIsValid) {
|
|
112
|
+
const master = await req.db.connectionMaster.findUnique({ where: { key: type }, select: { fields: true } });
|
|
113
|
+
typeIsValid = !!master?.fields;
|
|
114
|
+
}
|
|
115
|
+
if (!typeIsValid) return res.status(400).json({ error: `Unsupported connector type: ${type}` });
|
|
116
|
+
|
|
117
|
+
const existing = await req.db.connector.count();
|
|
118
|
+
if (!await canAddConnector(existing, req.db)) {
|
|
119
|
+
const tier = await getTierFromDB(req.db);
|
|
120
|
+
return res.status(403).json({ error: `Tier limit reached. Max ${tier.maxConnectors} connector(s) allowed across the instance.` });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Auto-suffix name to keep it globally unique (mysql → mysql-1 → mysql-2 …)
|
|
124
|
+
let finalName = name.trim();
|
|
125
|
+
const baseName = finalName;
|
|
126
|
+
let nameSuffix = 1;
|
|
127
|
+
while (await req.db.connector.findFirst({ where: { name: finalName } })) {
|
|
128
|
+
finalName = `${baseName}-${nameSuffix++}`;
|
|
129
|
+
}
|
|
130
|
+
const slug = finalName; // slug mirrors name
|
|
131
|
+
|
|
132
|
+
const connector = await req.db.connector.create({
|
|
133
|
+
data: {
|
|
134
|
+
workspaceId,
|
|
135
|
+
name: finalName,
|
|
136
|
+
slug,
|
|
137
|
+
type,
|
|
138
|
+
config: config ? JSON.stringify(config) : null,
|
|
139
|
+
authConfig: authConfig ? JSON.stringify(authConfig) : null,
|
|
140
|
+
status: "active",
|
|
141
|
+
},
|
|
142
|
+
select: { id: true, name: true, slug: true, type: true, config: true, status: true, createdAt: true }
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
await logActivity(req.db, req.user, "connector.created", { workspaceId, name: connector.name, type });
|
|
146
|
+
res.json({ connector });
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Update connector
|
|
150
|
+
router.put("/workspaces/:workspaceId/connectors/:id", async (req, res) => {
|
|
151
|
+
try {
|
|
152
|
+
const id = parseInt(req.params.id);
|
|
153
|
+
const { name, slug, config, authConfig, status } = req.body;
|
|
154
|
+
const data = {};
|
|
155
|
+
if (name !== undefined) data.name = name.trim();
|
|
156
|
+
if (slug !== undefined) data.slug = slug ? slug.toLowerCase().replace(/[^a-z0-9]/g, "") || null : null;
|
|
157
|
+
if (config !== undefined) data.config = config ? JSON.stringify(config) : null;
|
|
158
|
+
if (status !== undefined) data.status = status;
|
|
159
|
+
|
|
160
|
+
if (authConfig !== undefined) {
|
|
161
|
+
if (!authConfig) {
|
|
162
|
+
data.authConfig = null;
|
|
163
|
+
} else {
|
|
164
|
+
// Merge with existing: blank fields in the edit form mean "keep existing"
|
|
165
|
+
const existing = await req.db.connector.findUnique({ where: { id }, select: { authConfig: true } });
|
|
166
|
+
const existingAuth = existing?.authConfig ? JSON.parse(existing.authConfig) : {};
|
|
167
|
+
const merged = { ...existingAuth };
|
|
168
|
+
for (const [k, v] of Object.entries(authConfig)) {
|
|
169
|
+
if (v !== "" && v !== null && v !== undefined) merged[k] = v;
|
|
170
|
+
}
|
|
171
|
+
data.authConfig = JSON.stringify(merged);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const connector = await req.db.connector.update({
|
|
176
|
+
where: { id },
|
|
177
|
+
data,
|
|
178
|
+
select: { id: true, name: true, slug: true, type: true, config: true, status: true, lastTestedAt: true }
|
|
179
|
+
});
|
|
180
|
+
res.json({ connector });
|
|
181
|
+
} catch (err) {
|
|
182
|
+
if (err.code === "P2002") {
|
|
183
|
+
return res.status(400).json({ error: "Slug is already in use by another connector. Choose a different slug." });
|
|
184
|
+
}
|
|
185
|
+
console.error("[connectors] PUT failed:", err.message);
|
|
186
|
+
res.status(500).json({ error: err.message });
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Test connector connection
|
|
191
|
+
router.post("/workspaces/:workspaceId/connectors/:id/test", async (req, res) => {
|
|
192
|
+
const id = parseInt(req.params.id);
|
|
193
|
+
const connector = await req.db.connector.findUnique({ where: { id } });
|
|
194
|
+
if (!connector) return res.status(404).json({ error: "Connector not found" });
|
|
195
|
+
|
|
196
|
+
const cfg = connector.config ? JSON.parse(connector.config) : {};
|
|
197
|
+
const auth = connector.authConfig ? JSON.parse(connector.authConfig) : {};
|
|
198
|
+
|
|
199
|
+
let success = false;
|
|
200
|
+
let message = "";
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
if (connector.type === "postgresql") {
|
|
204
|
+
const { Pool } = require("pg");
|
|
205
|
+
const pool = new Pool({
|
|
206
|
+
connectionString: cfg.url || undefined,
|
|
207
|
+
host: cfg.host || "localhost",
|
|
208
|
+
port: parseInt(cfg.port || "5432"),
|
|
209
|
+
database: cfg.database || undefined,
|
|
210
|
+
user: auth.username || undefined,
|
|
211
|
+
password: auth.password || undefined,
|
|
212
|
+
ssl: cfg.ssl ? { rejectUnauthorized: false } : false,
|
|
213
|
+
connectionTimeoutMillis: 8000,
|
|
214
|
+
});
|
|
215
|
+
await pool.query("SELECT 1");
|
|
216
|
+
await pool.end();
|
|
217
|
+
success = true; message = "Connected successfully.";
|
|
218
|
+
} else if (connector.type === "mysql") {
|
|
219
|
+
const mysql = require("mysql2/promise");
|
|
220
|
+
const TIMEOUT_MS = 8000;
|
|
221
|
+
const connPromise = mysql.createConnection({
|
|
222
|
+
host: cfg.host || "localhost",
|
|
223
|
+
port: parseInt(cfg.port || "3306"),
|
|
224
|
+
database: cfg.database || undefined,
|
|
225
|
+
user: auth.username || undefined,
|
|
226
|
+
password: auth.password || undefined,
|
|
227
|
+
connectTimeout: TIMEOUT_MS,
|
|
228
|
+
});
|
|
229
|
+
const timeoutPromise = new Promise((_, reject) =>
|
|
230
|
+
setTimeout(() => reject(new Error("Connection timed out")), TIMEOUT_MS)
|
|
231
|
+
);
|
|
232
|
+
const conn = await Promise.race([connPromise, timeoutPromise]);
|
|
233
|
+
await conn.ping();
|
|
234
|
+
await conn.execute("SELECT 1");
|
|
235
|
+
await conn.end();
|
|
236
|
+
success = true; message = "Connected successfully.";
|
|
237
|
+
} else if (connector.type === "mssql") {
|
|
238
|
+
const mssql = require("mssql");
|
|
239
|
+
const pool = await mssql.connect({
|
|
240
|
+
server: cfg.host || "localhost",
|
|
241
|
+
port: parseInt(cfg.port || "1433"),
|
|
242
|
+
database: cfg.database || undefined,
|
|
243
|
+
user: auth.username || undefined,
|
|
244
|
+
password: auth.password || undefined,
|
|
245
|
+
options: {
|
|
246
|
+
encrypt: cfg.encrypt !== false,
|
|
247
|
+
trustServerCertificate: cfg.trustServerCertificate || false,
|
|
248
|
+
},
|
|
249
|
+
connectionTimeout: 8000,
|
|
250
|
+
requestTimeout: 8000,
|
|
251
|
+
});
|
|
252
|
+
await pool.request().query("SELECT 1 AS ok");
|
|
253
|
+
await pool.close();
|
|
254
|
+
success = true; message = "Connected successfully.";
|
|
255
|
+
} else if (connector.type === "oracle") {
|
|
256
|
+
const oracledb = require("oracledb");
|
|
257
|
+
const connectString = cfg.connectString ||
|
|
258
|
+
`${cfg.host || "localhost"}:${cfg.port || "1521"}/${cfg.serviceName || cfg.sid || "ORCL"}`;
|
|
259
|
+
const conn = await oracledb.getConnection({
|
|
260
|
+
user: auth.username || undefined, password: auth.password || undefined, connectString,
|
|
261
|
+
});
|
|
262
|
+
await conn.execute("SELECT 1 FROM DUAL");
|
|
263
|
+
await conn.close();
|
|
264
|
+
success = true; message = "Connected successfully.";
|
|
265
|
+
} else if (connector.type === "mongodb") {
|
|
266
|
+
const { MongoClient } = require("mongodb");
|
|
267
|
+
const { buildUri } = require("../utils/tools/adapters/mongodb");
|
|
268
|
+
const uri = buildUri(cfg, auth);
|
|
269
|
+
const client = new MongoClient(uri, { serverSelectionTimeoutMS: 8000, connectTimeoutMS: 8000 });
|
|
270
|
+
await client.connect();
|
|
271
|
+
await client.db(cfg.database || "admin").command({ ping: 1 });
|
|
272
|
+
await client.close();
|
|
273
|
+
success = true; message = "Connected successfully.";
|
|
274
|
+
} else if (connector.type === "gmail") {
|
|
275
|
+
const { google } = require("googleapis");
|
|
276
|
+
const { makeOAuth2Client } = require("../utils/tools/adapters/gmail");
|
|
277
|
+
if (!auth.refreshToken) throw new Error("Not connected — please reconnect via the Integrations tab.");
|
|
278
|
+
const oauth2 = await makeOAuth2Client(req.db);
|
|
279
|
+
oauth2.setCredentials({ access_token: auth.accessToken, refresh_token: auth.refreshToken, expiry_date: auth.expiresAt });
|
|
280
|
+
const gmail = google.gmail({ version: "v1", auth: oauth2 });
|
|
281
|
+
const profile = await gmail.users.getProfile({ userId: "me" });
|
|
282
|
+
success = true; message = `Connected as ${profile.data.emailAddress}`;
|
|
283
|
+
} else if (connector.type === "gdrive") {
|
|
284
|
+
const { google } = require("googleapis");
|
|
285
|
+
const { makeOAuth2Client } = require("../utils/tools/adapters/gmail");
|
|
286
|
+
if (!auth.refreshToken) throw new Error("Not connected — please reconnect via the Integrations tab.");
|
|
287
|
+
const oauth2 = await makeOAuth2Client(req.db, connector.workspaceId);
|
|
288
|
+
oauth2.setCredentials({ access_token: auth.accessToken, refresh_token: auth.refreshToken, expiry_date: auth.expiresAt });
|
|
289
|
+
const drive = google.drive({ version: "v3", auth: oauth2 });
|
|
290
|
+
const res = await drive.about.get({ fields: "user" });
|
|
291
|
+
success = true; message = `Connected as ${res.data.user.emailAddress}`;
|
|
292
|
+
} else if (["slack","jira","confluence","notion","hubspot","freshdesk","zendesk","github"].includes(connector.type)) {
|
|
293
|
+
const { ADAPTERS } = require("../utils/tools/registry");
|
|
294
|
+
const adapter = ADAPTERS[connector.type];
|
|
295
|
+
const ok = await adapter.testConnection(auth);
|
|
296
|
+
if (!ok) throw new Error("Connection test failed — check your credentials.");
|
|
297
|
+
success = true; message = "Connected successfully.";
|
|
298
|
+
} else if (connector.type === "zoho-mail") {
|
|
299
|
+
const nodemailer = require("nodemailer");
|
|
300
|
+
const port = parseInt(auth.smtpPort || "465");
|
|
301
|
+
const transport = nodemailer.createTransport({
|
|
302
|
+
host: auth.smtpHost || "smtp.zoho.com",
|
|
303
|
+
port,
|
|
304
|
+
secure: port === 465,
|
|
305
|
+
auth: { user: auth.email, pass: auth.appPassword },
|
|
306
|
+
connectionTimeout: 8000,
|
|
307
|
+
});
|
|
308
|
+
await transport.verify();
|
|
309
|
+
success = true; message = `Connected as ${auth.email}`;
|
|
310
|
+
} else if (connector.type === "ssh") {
|
|
311
|
+
const { Client } = require("ssh2");
|
|
312
|
+
await new Promise((resolve, reject) => {
|
|
313
|
+
const conn = new Client();
|
|
314
|
+
const timer = setTimeout(() => { conn.end(); reject(new Error("Connection timed out")); }, 10000);
|
|
315
|
+
conn.on("ready", () => { clearTimeout(timer); conn.end(); resolve(); });
|
|
316
|
+
conn.on("error", err => { clearTimeout(timer); reject(err); });
|
|
317
|
+
const cfg2 = { host: auth.host || cfg.host, port: parseInt(auth.port || cfg.port || "22"), username: auth.username || cfg.username };
|
|
318
|
+
if (auth.privateKey || cfg.privateKey) cfg2.privateKey = auth.privateKey || cfg.privateKey;
|
|
319
|
+
conn.connect(cfg2);
|
|
320
|
+
});
|
|
321
|
+
success = true; message = `Connected to ${auth.host || cfg.host} as ${auth.username || cfg.username}`;
|
|
322
|
+
} else if (connector.type === "redis") {
|
|
323
|
+
const Redis = require("ioredis");
|
|
324
|
+
const client = new Redis({
|
|
325
|
+
host: cfg.host || "localhost",
|
|
326
|
+
port: parseInt(cfg.port || "6379"),
|
|
327
|
+
password: auth.password || undefined,
|
|
328
|
+
db: parseInt(cfg.db || "0"),
|
|
329
|
+
tls: cfg.tls ? {} : undefined,
|
|
330
|
+
connectTimeout: 8000,
|
|
331
|
+
lazyConnect: true,
|
|
332
|
+
});
|
|
333
|
+
await client.connect();
|
|
334
|
+
await client.ping();
|
|
335
|
+
await client.quit();
|
|
336
|
+
success = true; message = "Connected successfully.";
|
|
337
|
+
} else if (connector.type === "sqlite") {
|
|
338
|
+
const Database = require("better-sqlite3");
|
|
339
|
+
const db = new Database(cfg.filename || ":memory:", { timeout: 8000 });
|
|
340
|
+
db.prepare("SELECT 1").get();
|
|
341
|
+
db.close();
|
|
342
|
+
success = true; message = `Connected to ${cfg.filename || ":memory:"}`;
|
|
343
|
+
} else if (connector.type === "cockroachdb") {
|
|
344
|
+
const { Pool } = require("pg");
|
|
345
|
+
const pool = new Pool({
|
|
346
|
+
host: cfg.host || "localhost",
|
|
347
|
+
port: parseInt(cfg.port || "26257"),
|
|
348
|
+
database: cfg.database || "defaultdb",
|
|
349
|
+
user: auth.username || undefined,
|
|
350
|
+
password: auth.password || undefined,
|
|
351
|
+
ssl: cfg.ssl ? { rejectUnauthorized: false } : false,
|
|
352
|
+
connectionTimeoutMillis: 8000,
|
|
353
|
+
});
|
|
354
|
+
await pool.query("SELECT 1");
|
|
355
|
+
await pool.end();
|
|
356
|
+
success = true; message = "Connected successfully.";
|
|
357
|
+
} else if (connector.type === "snowflake") {
|
|
358
|
+
const snowflake = require("snowflake-sdk");
|
|
359
|
+
await new Promise((resolve, reject) => {
|
|
360
|
+
const c = snowflake.createConnection({
|
|
361
|
+
account: cfg.account,
|
|
362
|
+
username: auth.username,
|
|
363
|
+
password: auth.password,
|
|
364
|
+
database: cfg.database,
|
|
365
|
+
schema: cfg.schema || "PUBLIC",
|
|
366
|
+
warehouse: cfg.warehouse,
|
|
367
|
+
role: cfg.role || undefined,
|
|
368
|
+
});
|
|
369
|
+
c.connect(err => {
|
|
370
|
+
if (err) { reject(err); return; }
|
|
371
|
+
c.execute({ sqlText: "SELECT 1", complete: (err2) => { c.destroy(() => {}); err2 ? reject(err2) : resolve(); } });
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
success = true; message = "Connected successfully.";
|
|
375
|
+
} else if (connector.type === "bigquery") {
|
|
376
|
+
const { BigQuery } = require("@google-cloud/bigquery");
|
|
377
|
+
const credentials = auth.keyFileJson ? JSON.parse(auth.keyFileJson) : undefined;
|
|
378
|
+
const bq = new BigQuery({ projectId: cfg.projectId, credentials });
|
|
379
|
+
await bq.query({ query: "SELECT 1", timeoutMs: 8000 });
|
|
380
|
+
success = true; message = `Connected to project ${cfg.projectId}`;
|
|
381
|
+
} else if (connector.type === "elasticsearch") {
|
|
382
|
+
const { Client } = require("@elastic/elasticsearch");
|
|
383
|
+
const esAuth = auth.apiKey
|
|
384
|
+
? { auth: { apiKey: auth.apiKey } }
|
|
385
|
+
: (auth.username ? { auth: { username: auth.username, password: auth.password || "" } } : {});
|
|
386
|
+
const esClient = new Client({ node: cfg.node || "http://localhost:9200", ...esAuth, requestTimeout: 8000 });
|
|
387
|
+
const info = await esClient.info();
|
|
388
|
+
await esClient.close();
|
|
389
|
+
success = true; message = `Connected — Elasticsearch ${info.version?.number || ""}`;
|
|
390
|
+
} else if (connector.type === "onedrive") {
|
|
391
|
+
if (!auth.refreshToken && !auth.accessToken) throw new Error("Not connected — complete OAuth flow first.");
|
|
392
|
+
const { data } = await require("axios").get("https://graph.microsoft.com/v1.0/me/drive", { headers: { Authorization: `Bearer ${auth.accessToken}` }, timeout: 8000 });
|
|
393
|
+
success = true; message = `Connected to OneDrive (${data.owner?.user?.displayName || data.driveType || "personal"})`;
|
|
394
|
+
} else if (connector.type === "dropbox") {
|
|
395
|
+
if (!auth.accessToken) throw new Error("Not connected — complete OAuth flow first.");
|
|
396
|
+
const { data } = await require("axios").post("https://api.dropboxapi.com/2/users/get_current_account", null, { headers: { Authorization: `Bearer ${auth.accessToken}` }, timeout: 8000 });
|
|
397
|
+
success = true; message = `Connected as ${data.email || data.name?.display_name || "Dropbox user"}`;
|
|
398
|
+
} else if (connector.type === "box") {
|
|
399
|
+
if (!auth.accessToken) throw new Error("Not connected — complete OAuth flow first.");
|
|
400
|
+
const { data } = await require("axios").get("https://api.box.com/2.0/users/me", { headers: { Authorization: `Bearer ${auth.accessToken}` }, timeout: 8000 });
|
|
401
|
+
success = true; message = `Connected as ${data.login || data.name || "Box user"}`;
|
|
402
|
+
} else if (connector.type === "rest-api") {
|
|
403
|
+
const axios = require("axios");
|
|
404
|
+
const baseUrl = (cfg.baseUrl || "").replace(/\/$/, "");
|
|
405
|
+
if (!baseUrl) throw new Error("Base URL not configured");
|
|
406
|
+
const headers = {};
|
|
407
|
+
if (auth.apiKey) headers[auth.headerName || "X-API-Key"] = auth.apiKey;
|
|
408
|
+
if (auth.bearerToken) headers["Authorization"] = `Bearer ${auth.bearerToken}`;
|
|
409
|
+
await axios.get(baseUrl + (cfg.healthPath || "/"), { headers, timeout: 8000 });
|
|
410
|
+
success = true; message = "API reachable.";
|
|
411
|
+
}
|
|
412
|
+
} catch (err) {
|
|
413
|
+
message = err.message || err.errors?.[0]?.message || err.code || String(err) || "Connection failed";
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
await req.db.connector.update({
|
|
417
|
+
where: { id },
|
|
418
|
+
data: { status: success ? "active" : "error", lastTestedAt: new Date() }
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
res.json({ success, message });
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// Delete connector
|
|
425
|
+
router.delete("/workspaces/:workspaceId/connectors/:id", async (req, res) => {
|
|
426
|
+
const id = parseInt(req.params.id);
|
|
427
|
+
const connector = await req.db.connector.findUnique({ where: { id }, select: { name: true, type: true } });
|
|
428
|
+
if (!connector) return res.status(404).json({ error: "Not found" });
|
|
429
|
+
await req.db.connector.delete({ where: { id } });
|
|
430
|
+
await logActivity(req.db, req.user, "connector.deleted", { name: connector.name, type: connector.type });
|
|
431
|
+
res.json({ success: true });
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
module.exports = router;
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
const router = require("express").Router();
|
|
2
|
+
const { authenticate, requireCommercial } = require("../middleware/auth");
|
|
3
|
+
const { getTierFromDB, getAgentRunsThisMonth } = require("../utils/tier");
|
|
4
|
+
|
|
5
|
+
const EMBEDDING_RATES = {
|
|
6
|
+
"text-embedding-3-small": 0.02 / 1_000_000,
|
|
7
|
+
"text-embedding-3-large": 0.13 / 1_000_000,
|
|
8
|
+
"text-embedding-ada-002": 0.10 / 1_000_000,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const LLM_RATES = {
|
|
12
|
+
"gpt-4o": { in: 2.50 / 1e6, out: 10.00 / 1e6 },
|
|
13
|
+
"gpt-4o-mini": { in: 0.15 / 1e6, out: 0.60 / 1e6 },
|
|
14
|
+
"gpt-4-turbo": { in: 10.00 / 1e6, out: 30.00 / 1e6 },
|
|
15
|
+
"claude-3-5-sonnet-20241022": { in: 3.00 / 1e6, out: 15.00 / 1e6 },
|
|
16
|
+
"claude-3-5-haiku-20241022": { in: 0.80 / 1e6, out: 4.00 / 1e6 },
|
|
17
|
+
"claude-3-opus-20240229": { in: 15.00 / 1e6, out: 75.00 / 1e6 },
|
|
18
|
+
"claude-3-haiku-20240307": { in: 0.25 / 1e6, out: 1.25 / 1e6 },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function calcEmbeddingCost(tokens, model) {
|
|
22
|
+
return (tokens || 0) * (EMBEDDING_RATES[model] || 0.10 / 1_000_000);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function calcLLMCost(inputTokens, outputTokens, model) {
|
|
26
|
+
const r = LLM_RATES[model] || { in: 2.50 / 1e6, out: 10.00 / 1e6 };
|
|
27
|
+
return (inputTokens || 0) * r.in + (outputTokens || 0) * r.out;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function last30Days() {
|
|
31
|
+
return Array.from({ length: 30 }, (_, i) => {
|
|
32
|
+
const d = new Date();
|
|
33
|
+
d.setDate(d.getDate() - (29 - i));
|
|
34
|
+
return d.toISOString().slice(0, 10);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function groupByDay(items, dateField) {
|
|
39
|
+
const map = {};
|
|
40
|
+
for (const item of items) {
|
|
41
|
+
const day = new Date(item[dateField]).toISOString().slice(0, 10);
|
|
42
|
+
map[day] = (map[day] || 0) + 1;
|
|
43
|
+
}
|
|
44
|
+
return last30Days().map(d => ({ date: d, count: map[d] || 0 }));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function statusSeries(counts) {
|
|
48
|
+
return [
|
|
49
|
+
{ name: "Ready", value: counts.ready || 0, color: "#22c55e" },
|
|
50
|
+
{ name: "Failed", value: counts.failed || 0, color: "#ef4444" },
|
|
51
|
+
{ name: "Partial", value: counts.partial || 0, color: "#f59e0b" },
|
|
52
|
+
{ name: "Active", value: (counts.ingesting || 0) + (counts.queued || 0), color: "#6366f1" },
|
|
53
|
+
].filter(s => s.value > 0);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Manager stats — scoped to user's workspaces (admin sees all)
|
|
57
|
+
router.get("/manager", authenticate, async (req, res) => {
|
|
58
|
+
const db = req.db;
|
|
59
|
+
|
|
60
|
+
let workspaceIds;
|
|
61
|
+
if (req.user.role === "admin") {
|
|
62
|
+
const all = await db.workspace.findMany({ select: { id: true } });
|
|
63
|
+
workspaceIds = all.map(w => w.id);
|
|
64
|
+
} else {
|
|
65
|
+
const memberships = await db.workspaceUser.findMany({
|
|
66
|
+
where: { userId: req.user.id },
|
|
67
|
+
select: { workspaceId: true }
|
|
68
|
+
});
|
|
69
|
+
workspaceIds = memberships.map(m => m.workspaceId);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const thirtyDaysAgo = new Date();
|
|
73
|
+
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 29);
|
|
74
|
+
|
|
75
|
+
const agentIds = (await db.agent.findMany({ where: { workspaceId: { in: workspaceIds } }, select: { id: true } })).map(a => a.id);
|
|
76
|
+
|
|
77
|
+
const [workspaces, documents, agentRuns] = await Promise.all([
|
|
78
|
+
db.workspace.findMany({
|
|
79
|
+
where: { id: { in: workspaceIds } },
|
|
80
|
+
include: { _count: { select: { documents: true, chats: true } } }
|
|
81
|
+
}),
|
|
82
|
+
db.document.findMany({
|
|
83
|
+
where: { workspaceId: { in: workspaceIds } },
|
|
84
|
+
select: { status: true, chunkCount: true, createdAt: true }
|
|
85
|
+
}),
|
|
86
|
+
agentIds.length ? db.agentRun.findMany({
|
|
87
|
+
where: { agentId: { in: agentIds }, startedAt: { gte: thirtyDaysAgo } },
|
|
88
|
+
select: { startedAt: true, status: true }
|
|
89
|
+
}) : Promise.resolve([]),
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
const statusCounts = {};
|
|
93
|
+
let totalVectors = 0;
|
|
94
|
+
for (const d of documents) {
|
|
95
|
+
statusCounts[d.status] = (statusCounts[d.status] || 0) + 1;
|
|
96
|
+
totalVectors += d.chunkCount || 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
res.json({
|
|
100
|
+
workspaceCount: workspaces.length,
|
|
101
|
+
documentCount: documents.length,
|
|
102
|
+
vectorCount: totalVectors,
|
|
103
|
+
activeIngestions: (statusCounts.ingesting || 0) + (statusCounts.queued || 0),
|
|
104
|
+
agentRunCount: agentRuns.length,
|
|
105
|
+
agentRunSuccess: agentRuns.filter(r => r.status === "success").length,
|
|
106
|
+
agentRunErrors: agentRuns.filter(r => r.status === "error").length,
|
|
107
|
+
agentRunsByTrigger: [
|
|
108
|
+
{ name: "Runs", value: agentRuns.length, color: "#6366f1" },
|
|
109
|
+
].filter(s => s.value > 0),
|
|
110
|
+
documentsByStatus: statusSeries(statusCounts),
|
|
111
|
+
agentRunActivity: groupByDay(agentRuns, "startedAt"),
|
|
112
|
+
docsByWorkspace: workspaces
|
|
113
|
+
.map(ws => ({
|
|
114
|
+
name: ws.name.length > 18 ? ws.name.slice(0, 18) + "…" : ws.name,
|
|
115
|
+
documents: ws._count.documents,
|
|
116
|
+
chats: ws._count.chats,
|
|
117
|
+
}))
|
|
118
|
+
.sort((a, b) => b.documents - a.documents)
|
|
119
|
+
.slice(0, 8),
|
|
120
|
+
ingestActivity: groupByDay(
|
|
121
|
+
documents.filter(d => new Date(d.createdAt) >= thirtyDaysAgo),
|
|
122
|
+
"createdAt"
|
|
123
|
+
),
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Admin stats — platform-wide
|
|
128
|
+
router.get("/admin", authenticate, async (req, res) => {
|
|
129
|
+
if (req.user.role !== "admin") return res.status(403).json({ error: "Admin only" });
|
|
130
|
+
const db = req.db;
|
|
131
|
+
|
|
132
|
+
const thirtyDaysAgo = new Date();
|
|
133
|
+
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 29);
|
|
134
|
+
|
|
135
|
+
const tier = await getTierFromDB(db);
|
|
136
|
+
|
|
137
|
+
const [users, workspaces, documents, recentUserChats, allAssistantChats, agentRunsThisMonth, recentAgentRuns] = await Promise.all([
|
|
138
|
+
db.user.findMany({ select: { id: true, role: true, createdAt: true } }),
|
|
139
|
+
db.workspace.findMany({
|
|
140
|
+
select: { id: true, name: true, _count: { select: { documents: true, chats: true } } }
|
|
141
|
+
}),
|
|
142
|
+
db.document.findMany({
|
|
143
|
+
select: { status: true, chunkCount: true, embeddingTokens: true, embeddingModel: true, createdAt: true, size: true }
|
|
144
|
+
}),
|
|
145
|
+
db.chat.findMany({
|
|
146
|
+
where: { role: "user", createdAt: { gte: thirtyDaysAgo } },
|
|
147
|
+
select: { createdAt: true }
|
|
148
|
+
}),
|
|
149
|
+
db.chat.findMany({
|
|
150
|
+
where: { role: "assistant" },
|
|
151
|
+
select: { inputTokens: true, outputTokens: true, model: true }
|
|
152
|
+
}),
|
|
153
|
+
getAgentRunsThisMonth(db),
|
|
154
|
+
db.agentRun.findMany({
|
|
155
|
+
where: { startedAt: { gte: thirtyDaysAgo } },
|
|
156
|
+
select: { startedAt: true, status: true }
|
|
157
|
+
}),
|
|
158
|
+
]);
|
|
159
|
+
|
|
160
|
+
// Document stats
|
|
161
|
+
const statusCounts = {};
|
|
162
|
+
let totalVectors = 0, totalEmbedTokens = 0, totalEmbedCost = 0, totalStorageBytes = 0;
|
|
163
|
+
for (const d of documents) {
|
|
164
|
+
statusCounts[d.status] = (statusCounts[d.status] || 0) + 1;
|
|
165
|
+
totalVectors += d.chunkCount || 0;
|
|
166
|
+
totalEmbedTokens += d.embeddingTokens || 0;
|
|
167
|
+
totalEmbedCost += calcEmbeddingCost(d.embeddingTokens, d.embeddingModel);
|
|
168
|
+
totalStorageBytes += d.size || 0;
|
|
169
|
+
}
|
|
170
|
+
const storageUsedGb = totalStorageBytes / (1024 ** 3);
|
|
171
|
+
|
|
172
|
+
// LLM stats
|
|
173
|
+
let llmInputTokens = 0, llmOutputTokens = 0, llmCost = 0;
|
|
174
|
+
for (const c of allAssistantChats) {
|
|
175
|
+
llmInputTokens += c.inputTokens || 0;
|
|
176
|
+
llmOutputTokens += c.outputTokens || 0;
|
|
177
|
+
llmCost += calcLLMCost(c.inputTokens, c.outputTokens, c.model);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
res.json({
|
|
181
|
+
// Summary counts
|
|
182
|
+
userCount: users.length,
|
|
183
|
+
workspaceCount: workspaces.length,
|
|
184
|
+
documentCount: documents.length,
|
|
185
|
+
vectorCount: totalVectors,
|
|
186
|
+
chatCount: allAssistantChats.length,
|
|
187
|
+
activeIngestions: (statusCounts.ingesting || 0) + (statusCounts.queued || 0),
|
|
188
|
+
// Cost
|
|
189
|
+
embeddingTokens: totalEmbedTokens,
|
|
190
|
+
embeddingCostUsd: totalEmbedCost,
|
|
191
|
+
llmInputTokens,
|
|
192
|
+
llmOutputTokens,
|
|
193
|
+
llmCostUsd: llmCost,
|
|
194
|
+
totalCostUsd: totalEmbedCost + llmCost,
|
|
195
|
+
// Charts
|
|
196
|
+
documentsByStatus: statusSeries(statusCounts),
|
|
197
|
+
usersByRole: [
|
|
198
|
+
{ name: "Admin", value: users.filter(u => u.role === "admin").length, color: "#6366f1" },
|
|
199
|
+
{ name: "Manager", value: users.filter(u => u.role === "manager").length, color: "#f59e0b" },
|
|
200
|
+
{ name: "User", value: users.filter(u => u.role === "user").length, color: "#22c55e" },
|
|
201
|
+
].filter(u => u.value > 0),
|
|
202
|
+
topWorkspaces: workspaces
|
|
203
|
+
.sort((a, b) => b._count.chats - a._count.chats)
|
|
204
|
+
.slice(0, 8)
|
|
205
|
+
.map(ws => ({
|
|
206
|
+
name: ws.name.length > 16 ? ws.name.slice(0, 16) + "…" : ws.name,
|
|
207
|
+
chats: ws._count.chats,
|
|
208
|
+
documents: ws._count.documents,
|
|
209
|
+
})),
|
|
210
|
+
// Usage vs limits — commercial only; null means unlimited (Infinity can't serialize to JSON)
|
|
211
|
+
usage: (process.env.LICENSE_TYPE === "enterprise" && process.env.LICENSE_EDITION === "Open Enthrium Commercial" && process.env.LICENSE_PRICE === "custom") ? {
|
|
212
|
+
tierName: tier.name,
|
|
213
|
+
workspaceCount: workspaces.length,
|
|
214
|
+
workspaceLimit: isFinite(tier.maxWorkspaces) ? tier.maxWorkspaces : null,
|
|
215
|
+
userCount: users.length,
|
|
216
|
+
userLimit: isFinite(tier.maxUsers) ? tier.maxUsers : null,
|
|
217
|
+
connectorCount: 0,
|
|
218
|
+
connectorLimit: null,
|
|
219
|
+
agentRunsThisMonth,
|
|
220
|
+
agentRunsLimit: isFinite(tier.maxAgentRunsPerMonth) ? tier.maxAgentRunsPerMonth : null,
|
|
221
|
+
storageUsedGb: parseFloat(storageUsedGb.toFixed(3)),
|
|
222
|
+
storageUsedBytes: totalStorageBytes,
|
|
223
|
+
storageLimitGb: isFinite(tier.ingestionSpaceGb) ? tier.ingestionSpaceGb : null,
|
|
224
|
+
} : null,
|
|
225
|
+
agentRunCount: recentAgentRuns.length,
|
|
226
|
+
agentRunSuccess: recentAgentRuns.filter(r => r.status === "success").length,
|
|
227
|
+
agentRunErrors: recentAgentRuns.filter(r => r.status === "error").length,
|
|
228
|
+
agentRunsByTrigger: [
|
|
229
|
+
{ name: "Runs", value: recentAgentRuns.length, color: "#6366f1" },
|
|
230
|
+
].filter(s => s.value > 0),
|
|
231
|
+
chatActivity: groupByDay(recentUserChats, "createdAt"),
|
|
232
|
+
agentRunActivity: groupByDay(recentAgentRuns, "startedAt"),
|
|
233
|
+
ingestActivity: groupByDay(
|
|
234
|
+
documents.filter(d => new Date(d.createdAt) >= thirtyDaysAgo),
|
|
235
|
+
"createdAt"
|
|
236
|
+
),
|
|
237
|
+
userGrowth: groupByDay(
|
|
238
|
+
users.filter(u => new Date(u.createdAt) >= thirtyDaysAgo),
|
|
239
|
+
"createdAt"
|
|
240
|
+
),
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
module.exports = router;
|