@intranefr/superbackend 1.4.3
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/.commiat +4 -0
- package/.env.example +47 -0
- package/README.md +110 -0
- package/index.js +94 -0
- package/package.json +67 -0
- package/public/css/styles.css +139 -0
- package/public/js/animations.js +41 -0
- package/sdk/error-tracking/browser/package.json +16 -0
- package/sdk/error-tracking/browser/src/core.js +270 -0
- package/sdk/error-tracking/browser/src/embed.js +18 -0
- package/sdk/error-tracking/browser/src/index.js +1 -0
- package/server.js +5 -0
- package/src/admin/endpointRegistry.js +300 -0
- package/src/controllers/admin.controller.js +321 -0
- package/src/controllers/adminAssets.controller.js +530 -0
- package/src/controllers/adminAssetsStorage.controller.js +260 -0
- package/src/controllers/adminEjsVirtual.controller.js +354 -0
- package/src/controllers/adminFeatureFlags.controller.js +155 -0
- package/src/controllers/adminHeadless.controller.js +1071 -0
- package/src/controllers/adminI18n.controller.js +604 -0
- package/src/controllers/adminJsonConfigs.controller.js +97 -0
- package/src/controllers/adminLlm.controller.js +273 -0
- package/src/controllers/adminMigration.controller.js +257 -0
- package/src/controllers/adminSeoConfig.controller.js +515 -0
- package/src/controllers/adminStats.controller.js +121 -0
- package/src/controllers/adminUploadNamespaces.controller.js +208 -0
- package/src/controllers/assets.controller.js +248 -0
- package/src/controllers/auth.controller.js +93 -0
- package/src/controllers/billing.controller.js +223 -0
- package/src/controllers/featureFlags.controller.js +35 -0
- package/src/controllers/forms.controller.js +217 -0
- package/src/controllers/globalSettings.controller.js +252 -0
- package/src/controllers/headlessCrud.controller.js +126 -0
- package/src/controllers/i18n.controller.js +12 -0
- package/src/controllers/invite.controller.js +249 -0
- package/src/controllers/jsonConfigs.controller.js +19 -0
- package/src/controllers/metrics.controller.js +149 -0
- package/src/controllers/notificationAdmin.controller.js +264 -0
- package/src/controllers/notifications.controller.js +131 -0
- package/src/controllers/org.controller.js +357 -0
- package/src/controllers/orgAdmin.controller.js +491 -0
- package/src/controllers/stripeAdmin.controller.js +410 -0
- package/src/controllers/user.controller.js +361 -0
- package/src/controllers/userAdmin.controller.js +277 -0
- package/src/controllers/waitingList.controller.js +167 -0
- package/src/controllers/webhook.controller.js +200 -0
- package/src/middleware/auth.js +66 -0
- package/src/middleware/errorCapture.js +170 -0
- package/src/middleware/headlessApiTokenAuth.js +57 -0
- package/src/middleware/org.js +108 -0
- package/src/middleware.js +901 -0
- package/src/models/ActionEvent.js +31 -0
- package/src/models/ActivityLog.js +41 -0
- package/src/models/Asset.js +84 -0
- package/src/models/AuditEvent.js +93 -0
- package/src/models/EmailLog.js +28 -0
- package/src/models/ErrorAggregate.js +72 -0
- package/src/models/FormSubmission.js +41 -0
- package/src/models/GlobalSetting.js +38 -0
- package/src/models/HeadlessApiToken.js +24 -0
- package/src/models/HeadlessModelDefinition.js +41 -0
- package/src/models/I18nEntry.js +77 -0
- package/src/models/I18nLocale.js +33 -0
- package/src/models/Invite.js +70 -0
- package/src/models/JsonConfig.js +46 -0
- package/src/models/Notification.js +60 -0
- package/src/models/Organization.js +57 -0
- package/src/models/OrganizationMember.js +43 -0
- package/src/models/StripeCatalogItem.js +77 -0
- package/src/models/StripeWebhookEvent.js +57 -0
- package/src/models/User.js +89 -0
- package/src/models/VirtualEjsFile.js +60 -0
- package/src/models/VirtualEjsFileVersion.js +43 -0
- package/src/models/VirtualEjsGroupChange.js +32 -0
- package/src/models/WaitingList.js +41 -0
- package/src/models/Webhook.js +63 -0
- package/src/models/Workflow.js +29 -0
- package/src/models/WorkflowExecution.js +12 -0
- package/src/routes/admin.routes.js +26 -0
- package/src/routes/adminAssets.routes.js +28 -0
- package/src/routes/adminAssetsStorage.routes.js +13 -0
- package/src/routes/adminAudit.routes.js +196 -0
- package/src/routes/adminEjsVirtual.routes.js +17 -0
- package/src/routes/adminErrors.routes.js +164 -0
- package/src/routes/adminFeatureFlags.routes.js +12 -0
- package/src/routes/adminHeadless.routes.js +38 -0
- package/src/routes/adminI18n.routes.js +22 -0
- package/src/routes/adminJsonConfigs.routes.js +15 -0
- package/src/routes/adminLlm.routes.js +12 -0
- package/src/routes/adminMigration.routes.js +81 -0
- package/src/routes/adminSeoConfig.routes.js +20 -0
- package/src/routes/adminUploadNamespaces.routes.js +13 -0
- package/src/routes/assets.routes.js +21 -0
- package/src/routes/auth.routes.js +12 -0
- package/src/routes/billing.routes.js +11 -0
- package/src/routes/errorTracking.routes.js +31 -0
- package/src/routes/featureFlags.routes.js +9 -0
- package/src/routes/forms.routes.js +9 -0
- package/src/routes/formsAdmin.routes.js +13 -0
- package/src/routes/globalSettings.routes.js +18 -0
- package/src/routes/headless.routes.js +15 -0
- package/src/routes/i18n.routes.js +8 -0
- package/src/routes/invite.routes.js +9 -0
- package/src/routes/jsonConfigs.routes.js +8 -0
- package/src/routes/log.routes.js +111 -0
- package/src/routes/metrics.routes.js +9 -0
- package/src/routes/notificationAdmin.routes.js +15 -0
- package/src/routes/notifications.routes.js +12 -0
- package/src/routes/org.routes.js +31 -0
- package/src/routes/orgAdmin.routes.js +20 -0
- package/src/routes/publicAssets.routes.js +7 -0
- package/src/routes/stripeAdmin.routes.js +20 -0
- package/src/routes/user.routes.js +22 -0
- package/src/routes/userAdmin.routes.js +15 -0
- package/src/routes/waitingList.routes.js +13 -0
- package/src/routes/waitingListAdmin.routes.js +9 -0
- package/src/routes/webhook.routes.js +32 -0
- package/src/routes/workflowWebhook.routes.js +54 -0
- package/src/routes/workflows.routes.js +110 -0
- package/src/services/assets.service.js +110 -0
- package/src/services/audit.service.js +62 -0
- package/src/services/auditLogger.js +165 -0
- package/src/services/ejsVirtual.service.js +614 -0
- package/src/services/email.service.js +351 -0
- package/src/services/errorLogger.js +221 -0
- package/src/services/featureFlags.service.js +202 -0
- package/src/services/forms.service.js +214 -0
- package/src/services/globalSettings.service.js +49 -0
- package/src/services/headlessApiTokens.service.js +158 -0
- package/src/services/headlessCrypto.service.js +31 -0
- package/src/services/headlessModels.service.js +356 -0
- package/src/services/i18n.service.js +314 -0
- package/src/services/i18nInferredKeys.service.js +337 -0
- package/src/services/jsonConfigs.service.js +392 -0
- package/src/services/llm.service.js +749 -0
- package/src/services/migration.service.js +581 -0
- package/src/services/migrationAssets/fsLocal.js +58 -0
- package/src/services/migrationAssets/index.js +134 -0
- package/src/services/migrationAssets/s3.js +75 -0
- package/src/services/migrationAssets/sftp.js +92 -0
- package/src/services/notification.service.js +212 -0
- package/src/services/objectStorage.service.js +514 -0
- package/src/services/seoConfig.service.js +402 -0
- package/src/services/storage.js +150 -0
- package/src/services/stripe.service.js +185 -0
- package/src/services/stripeHelper.service.js +264 -0
- package/src/services/uploadNamespaces.service.js +326 -0
- package/src/services/webhook.service.js +157 -0
- package/src/services/workflow.service.js +271 -0
- package/src/utils/asyncHandler.js +5 -0
- package/src/utils/encryption.js +80 -0
- package/src/utils/jwt.js +40 -0
- package/src/utils/orgRoles.js +156 -0
- package/src/utils/validation.js +26 -0
- package/src/utils/webhookRetry.js +93 -0
- package/views/admin-assets.ejs +444 -0
- package/views/admin-audit.ejs +283 -0
- package/views/admin-coolify-deploy.ejs +207 -0
- package/views/admin-dashboard-home.ejs +291 -0
- package/views/admin-dashboard.ejs +397 -0
- package/views/admin-ejs-virtual.ejs +280 -0
- package/views/admin-errors.ejs +368 -0
- package/views/admin-feature-flags.ejs +390 -0
- package/views/admin-forms.ejs +526 -0
- package/views/admin-global-settings.ejs +436 -0
- package/views/admin-headless.ejs +2020 -0
- package/views/admin-i18n-locales.ejs +221 -0
- package/views/admin-i18n.ejs +728 -0
- package/views/admin-json-configs.ejs +410 -0
- package/views/admin-llm.ejs +884 -0
- package/views/admin-metrics.ejs +274 -0
- package/views/admin-migration.ejs +814 -0
- package/views/admin-notifications.ejs +430 -0
- package/views/admin-organizations.ejs +984 -0
- package/views/admin-seo-config.ejs +673 -0
- package/views/admin-stripe-pricing.ejs +558 -0
- package/views/admin-test.ejs +342 -0
- package/views/admin-users.ejs +452 -0
- package/views/admin-waiting-list.ejs +547 -0
- package/views/admin-webhooks.ejs +329 -0
- package/views/admin-workflows.ejs +310 -0
- package/views/partials/admin-assets-script.ejs +2022 -0
- package/views/partials/admin-test-sidebar.ejs +14 -0
- package/views/partials/dashboard/nav-items.ejs +66 -0
- package/views/partials/dashboard/palette.ejs +63 -0
- package/views/partials/dashboard/sidebar.ejs +21 -0
- package/views/partials/dashboard/tab-bar.ejs +26 -0
- package/views/partials/footer.ejs +3 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
const GlobalSetting = require("../models/GlobalSetting");
|
|
2
|
+
const AuditEvent = require("../models/AuditEvent");
|
|
3
|
+
const llmService = require("../services/llm.service");
|
|
4
|
+
const { encryptString } = require("../utils/encryption");
|
|
5
|
+
|
|
6
|
+
const PROVIDERS_KEY = "llm.providers";
|
|
7
|
+
const PROMPTS_KEY = "llm.prompts";
|
|
8
|
+
|
|
9
|
+
async function getJsonSetting(key, defaultValue) {
|
|
10
|
+
const doc = await GlobalSetting.findOne({ key }).lean();
|
|
11
|
+
if (!doc || !doc.value) return defaultValue;
|
|
12
|
+
try {
|
|
13
|
+
const parsed = JSON.parse(doc.value);
|
|
14
|
+
return parsed || defaultValue;
|
|
15
|
+
} catch (e) {
|
|
16
|
+
return defaultValue;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function setJsonSetting(key, value) {
|
|
21
|
+
const descriptions = {
|
|
22
|
+
[PROVIDERS_KEY]: "LLM providers configuration (JSON)",
|
|
23
|
+
[PROMPTS_KEY]: "LLM prompts configuration (JSON)",
|
|
24
|
+
};
|
|
25
|
+
const description = descriptions[key] || `LLM setting ${key}`;
|
|
26
|
+
const stringValue = JSON.stringify(value || {});
|
|
27
|
+
const existing = await GlobalSetting.findOne({ key });
|
|
28
|
+
if (existing) {
|
|
29
|
+
existing.value = stringValue;
|
|
30
|
+
existing.type = "json";
|
|
31
|
+
if (!existing.description) {
|
|
32
|
+
existing.description = description;
|
|
33
|
+
}
|
|
34
|
+
await existing.save();
|
|
35
|
+
return existing;
|
|
36
|
+
}
|
|
37
|
+
const created = new GlobalSetting({
|
|
38
|
+
key,
|
|
39
|
+
value: stringValue,
|
|
40
|
+
type: "json",
|
|
41
|
+
description,
|
|
42
|
+
});
|
|
43
|
+
await created.save();
|
|
44
|
+
return created;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function getConfig(req, res) {
|
|
48
|
+
try {
|
|
49
|
+
const providers = await getJsonSetting(PROVIDERS_KEY, {});
|
|
50
|
+
const prompts = await getJsonSetting(PROMPTS_KEY, {});
|
|
51
|
+
const safeProviders = {};
|
|
52
|
+
if (providers && typeof providers === "object") {
|
|
53
|
+
for (const [key, value] of Object.entries(providers)) {
|
|
54
|
+
if (!value || typeof value !== "object") continue;
|
|
55
|
+
const { apiKey, ...rest } = value;
|
|
56
|
+
safeProviders[key] = rest;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
res.json({ providers: safeProviders, prompts });
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.error("[adminLlm] getConfig error", error);
|
|
62
|
+
res.status(500).json({ error: "Failed to load LLM configuration" });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function saveConfig(req, res) {
|
|
67
|
+
try {
|
|
68
|
+
const body = req.body || {};
|
|
69
|
+
const providers = body.providers && typeof body.providers === "object" ? body.providers : {};
|
|
70
|
+
const prompts = body.prompts && typeof body.prompts === "object" ? body.prompts : {};
|
|
71
|
+
|
|
72
|
+
const storedProviders = {};
|
|
73
|
+
for (const [key, value] of Object.entries(providers)) {
|
|
74
|
+
if (!value || typeof value !== "object") continue;
|
|
75
|
+
const clone = { ...value };
|
|
76
|
+
const rawApiKey = typeof clone.apiKey === "string" ? clone.apiKey.trim() : "";
|
|
77
|
+
delete clone.apiKey;
|
|
78
|
+
storedProviders[key] = clone;
|
|
79
|
+
|
|
80
|
+
if (rawApiKey) {
|
|
81
|
+
const encrypted = encryptString(rawApiKey);
|
|
82
|
+
const settingKey = `llm.provider.${key}.apiKey`;
|
|
83
|
+
let doc = await GlobalSetting.findOne({ key: settingKey });
|
|
84
|
+
if (!doc) {
|
|
85
|
+
doc = new GlobalSetting({
|
|
86
|
+
key: settingKey,
|
|
87
|
+
description: `Encrypted API key for LLM provider ${key}`,
|
|
88
|
+
type: "encrypted",
|
|
89
|
+
value: JSON.stringify(encrypted),
|
|
90
|
+
});
|
|
91
|
+
} else {
|
|
92
|
+
if (!doc.description) {
|
|
93
|
+
doc.description = `Encrypted API key for LLM provider ${key}`;
|
|
94
|
+
}
|
|
95
|
+
doc.type = "encrypted";
|
|
96
|
+
doc.value = JSON.stringify(encrypted);
|
|
97
|
+
}
|
|
98
|
+
await doc.save();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
await setJsonSetting(PROVIDERS_KEY, storedProviders);
|
|
103
|
+
await setJsonSetting(PROMPTS_KEY, prompts);
|
|
104
|
+
|
|
105
|
+
res.json({ success: true });
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error("[adminLlm] saveConfig error", error);
|
|
108
|
+
res.status(500).json({ error: "Failed to save LLM configuration" });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function testPrompt(req, res) {
|
|
113
|
+
try {
|
|
114
|
+
const promptKey = String(req.params.key || "");
|
|
115
|
+
const { variables, options } = req.body || {};
|
|
116
|
+
|
|
117
|
+
const result = await llmService.testPrompt(
|
|
118
|
+
{ key: promptKey },
|
|
119
|
+
variables || {},
|
|
120
|
+
options || {},
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
res.json({ result });
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.error("[adminLlm] testPrompt error", error);
|
|
126
|
+
res.status(500).json({ error: error.message || "Failed to test prompt" });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function listAudit(req, res) {
|
|
131
|
+
try {
|
|
132
|
+
const page = Math.max(parseInt(req.query.page, 10) || 1, 1);
|
|
133
|
+
const pageSize = Math.min(Math.max(parseInt(req.query.pageSize, 10) || 20, 1), 200);
|
|
134
|
+
const promptKey = String(req.query.promptKey || "").trim();
|
|
135
|
+
const providerKey = String(req.query.providerKey || "").trim();
|
|
136
|
+
const status = String(req.query.status || "").trim();
|
|
137
|
+
|
|
138
|
+
const query = { action: "llm.completion" };
|
|
139
|
+
|
|
140
|
+
if (status === "success" || status === "failure") {
|
|
141
|
+
query.outcome = status;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (promptKey) {
|
|
145
|
+
query["meta.promptKey"] = promptKey;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (providerKey) {
|
|
149
|
+
query["meta.providerKey"] = providerKey;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const total = await AuditEvent.countDocuments(query);
|
|
153
|
+
const items = await AuditEvent.find(query)
|
|
154
|
+
.sort({ createdAt: -1 })
|
|
155
|
+
.skip((page - 1) * pageSize)
|
|
156
|
+
.limit(pageSize)
|
|
157
|
+
.lean();
|
|
158
|
+
|
|
159
|
+
res.json({
|
|
160
|
+
page,
|
|
161
|
+
pageSize,
|
|
162
|
+
total,
|
|
163
|
+
items,
|
|
164
|
+
});
|
|
165
|
+
} catch (error) {
|
|
166
|
+
console.error("[adminLlm] listAudit error", error);
|
|
167
|
+
res.status(500).json({ error: "Failed to load LLM audit entries" });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function computeCostFromPricing({ prompt_tokens, completion_tokens }, modelPricing) {
|
|
172
|
+
if (!modelPricing || typeof modelPricing !== "object") return null;
|
|
173
|
+
|
|
174
|
+
const promptTokens = Number(prompt_tokens);
|
|
175
|
+
const completionTokens = Number(completion_tokens);
|
|
176
|
+
if (!Number.isFinite(promptTokens) && !Number.isFinite(completionTokens)) return null;
|
|
177
|
+
|
|
178
|
+
const inRate = Number(modelPricing.costPerMillionIn);
|
|
179
|
+
const outRate = Number(modelPricing.costPerMillionOut);
|
|
180
|
+
if (!Number.isFinite(inRate) && !Number.isFinite(outRate)) return null;
|
|
181
|
+
|
|
182
|
+
const costIn = Number.isFinite(promptTokens) && Number.isFinite(inRate)
|
|
183
|
+
? (promptTokens / 1_000_000) * inRate
|
|
184
|
+
: 0;
|
|
185
|
+
const costOut = Number.isFinite(completionTokens) && Number.isFinite(outRate)
|
|
186
|
+
? (completionTokens / 1_000_000) * outRate
|
|
187
|
+
: 0;
|
|
188
|
+
|
|
189
|
+
const total = costIn + costOut;
|
|
190
|
+
return Number.isFinite(total) ? total : null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function listCosts(req, res) {
|
|
194
|
+
try {
|
|
195
|
+
const page = Math.max(parseInt(req.query.page, 10) || 1, 1);
|
|
196
|
+
const pageSize = Math.min(Math.max(parseInt(req.query.pageSize, 10) || 20, 1), 200);
|
|
197
|
+
const promptKey = String(req.query.promptKey || "").trim();
|
|
198
|
+
const providerKey = String(req.query.providerKey || "").trim();
|
|
199
|
+
const model = String(req.query.model || "").trim();
|
|
200
|
+
|
|
201
|
+
const query = { action: "llm.completion", outcome: "success" };
|
|
202
|
+
if (promptKey) query["meta.promptKey"] = promptKey;
|
|
203
|
+
if (providerKey) query["meta.providerKey"] = providerKey;
|
|
204
|
+
if (model) query["meta.model"] = model;
|
|
205
|
+
|
|
206
|
+
const providers = await getJsonSetting(PROVIDERS_KEY, {});
|
|
207
|
+
|
|
208
|
+
const total = await AuditEvent.countDocuments(query);
|
|
209
|
+
const items = await AuditEvent.find(query)
|
|
210
|
+
.sort({ createdAt: -1 })
|
|
211
|
+
.skip((page - 1) * pageSize)
|
|
212
|
+
.limit(pageSize)
|
|
213
|
+
.lean();
|
|
214
|
+
|
|
215
|
+
const normalizedItems = (items || []).map((ev) => {
|
|
216
|
+
const meta = ev.meta || {};
|
|
217
|
+
const usage = meta.usage && typeof meta.usage === "object" ? { ...meta.usage } : null;
|
|
218
|
+
|
|
219
|
+
const promptTokens = usage ? usage.prompt_tokens : null;
|
|
220
|
+
const completionTokens = usage ? usage.completion_tokens : null;
|
|
221
|
+
const totalTokens = usage ? usage.total_tokens : null;
|
|
222
|
+
const existingCost = usage ? usage.cost : undefined;
|
|
223
|
+
|
|
224
|
+
let cost = existingCost;
|
|
225
|
+
let costSource = usage ? usage.cost_source : undefined;
|
|
226
|
+
|
|
227
|
+
if ((cost === undefined || cost === null) && usage) {
|
|
228
|
+
const p = providers && typeof providers === "object" ? providers[meta.providerKey] : null;
|
|
229
|
+
const modelPricing = p && typeof p === "object" && p.modelPricing && typeof p.modelPricing === "object"
|
|
230
|
+
? p.modelPricing[meta.model]
|
|
231
|
+
: null;
|
|
232
|
+
const computed = modelPricing
|
|
233
|
+
? computeCostFromPricing(usage, modelPricing)
|
|
234
|
+
: null;
|
|
235
|
+
if (computed !== null) {
|
|
236
|
+
cost = computed;
|
|
237
|
+
costSource = costSource || "computed_current";
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
id: ev._id,
|
|
243
|
+
createdAt: ev.createdAt,
|
|
244
|
+
promptKey: meta.promptKey,
|
|
245
|
+
providerKey: meta.providerKey,
|
|
246
|
+
model: meta.model,
|
|
247
|
+
prompt_tokens: promptTokens,
|
|
248
|
+
completion_tokens: completionTokens,
|
|
249
|
+
total_tokens: totalTokens,
|
|
250
|
+
cost,
|
|
251
|
+
cost_source: costSource,
|
|
252
|
+
};
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
res.json({
|
|
256
|
+
page,
|
|
257
|
+
pageSize,
|
|
258
|
+
total,
|
|
259
|
+
items: normalizedItems,
|
|
260
|
+
});
|
|
261
|
+
} catch (error) {
|
|
262
|
+
console.error("[adminLlm] listCosts error", error);
|
|
263
|
+
res.status(500).json({ error: "Failed to load LLM cost entries" });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
module.exports = {
|
|
268
|
+
getConfig,
|
|
269
|
+
saveConfig,
|
|
270
|
+
testPrompt,
|
|
271
|
+
listAudit,
|
|
272
|
+
listCosts,
|
|
273
|
+
};
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
const migrationService = require('../services/migration.service');
|
|
2
|
+
|
|
3
|
+
function getModelRegistry() {
|
|
4
|
+
return globalThis?.saasbackend?.models || null;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function getModelByName(modelName) {
|
|
8
|
+
const registry = getModelRegistry();
|
|
9
|
+
if (!registry) return null;
|
|
10
|
+
return registry[String(modelName || '')] || null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function describeSchema(model) {
|
|
14
|
+
const schemaPaths = model?.schema?.paths || {};
|
|
15
|
+
const fields = [];
|
|
16
|
+
|
|
17
|
+
for (const [key, pathDef] of Object.entries(schemaPaths)) {
|
|
18
|
+
if (key === '__v') continue;
|
|
19
|
+
const instance = pathDef?.instance || null;
|
|
20
|
+
const enumValues = Array.isArray(pathDef?.enumValues) && pathDef.enumValues.length > 0
|
|
21
|
+
? pathDef.enumValues
|
|
22
|
+
: (Array.isArray(pathDef?.options?.enum) && pathDef.options.enum.length > 0 ? pathDef.options.enum : null);
|
|
23
|
+
const isRequired = Array.isArray(pathDef?.validators)
|
|
24
|
+
? pathDef.validators.some((v) => v && v.type === 'required')
|
|
25
|
+
: false;
|
|
26
|
+
|
|
27
|
+
fields.push({
|
|
28
|
+
key,
|
|
29
|
+
type: instance,
|
|
30
|
+
required: isRequired,
|
|
31
|
+
enumValues,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
fields.sort((a, b) => a.key.localeCompare(b.key));
|
|
36
|
+
return {
|
|
37
|
+
modelName: model?.modelName,
|
|
38
|
+
fields,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function safeJsonParse(value, fallback) {
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(value);
|
|
45
|
+
} catch (_) {
|
|
46
|
+
return fallback;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
exports.listEnvironments = async (req, res) => {
|
|
51
|
+
try {
|
|
52
|
+
const { envKey, include } = req.query || {};
|
|
53
|
+
if (envKey && include === 'full') {
|
|
54
|
+
const env = await migrationService.getEnvironmentConfig(envKey);
|
|
55
|
+
if (!env) return res.status(404).json({ error: 'Environment not found' });
|
|
56
|
+
return res.json({ environment: env, environments: [env] });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const environments = await migrationService.listEnvironments();
|
|
60
|
+
res.json({ environments });
|
|
61
|
+
} catch (e) {
|
|
62
|
+
res.status(500).json({ error: e?.message ? String(e.message) : 'Failed to list environments' });
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
exports.listModels = async (req, res) => {
|
|
67
|
+
try {
|
|
68
|
+
const registry = getModelRegistry();
|
|
69
|
+
if (!registry) {
|
|
70
|
+
return res.status(500).json({ error: 'saasbackend models registry is not available' });
|
|
71
|
+
}
|
|
72
|
+
const models = Object.keys(registry).sort();
|
|
73
|
+
res.json({ models });
|
|
74
|
+
} catch (e) {
|
|
75
|
+
res.status(500).json({ error: e?.message ? String(e.message) : 'Failed to list models' });
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
exports.getModelSchema = async (req, res) => {
|
|
80
|
+
try {
|
|
81
|
+
const modelName = String(req.params.modelName || '').trim();
|
|
82
|
+
if (!modelName) return res.status(400).json({ error: 'modelName is required' });
|
|
83
|
+
const model = getModelByName(modelName);
|
|
84
|
+
if (!model) return res.status(404).json({ error: `Unknown model '${modelName}'` });
|
|
85
|
+
res.json({ schema: describeSchema(model) });
|
|
86
|
+
} catch (e) {
|
|
87
|
+
res.status(500).json({ error: e?.message ? String(e.message) : 'Failed to load schema' });
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
exports.preview = async (req, res) => {
|
|
92
|
+
try {
|
|
93
|
+
const { modelName, query, page, limit, sort, search } = req.body || {};
|
|
94
|
+
const safeModelName = String(modelName || '').trim();
|
|
95
|
+
if (!safeModelName) return res.status(400).json({ error: 'modelName is required' });
|
|
96
|
+
|
|
97
|
+
const model = getModelByName(safeModelName);
|
|
98
|
+
if (!model) {
|
|
99
|
+
const registry = getModelRegistry();
|
|
100
|
+
return res.status(400).json({
|
|
101
|
+
error: `Unknown model '${safeModelName}'`,
|
|
102
|
+
availableModels: registry ? Object.keys(registry).sort() : [],
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const safePage = Math.max(1, parseInt(page, 10) || 1);
|
|
107
|
+
const safeLimit = Math.min(100, Math.max(1, parseInt(limit, 10) || 20));
|
|
108
|
+
const skip = (safePage - 1) * safeLimit;
|
|
109
|
+
|
|
110
|
+
const filter = query && typeof query === 'object' ? query : {};
|
|
111
|
+
const trimmedSearch = String(search || '').trim();
|
|
112
|
+
|
|
113
|
+
if (trimmedSearch) {
|
|
114
|
+
const schemaInfo = describeSchema(model);
|
|
115
|
+
const stringFields = schemaInfo.fields
|
|
116
|
+
.filter((f) => f && (String(f.type || '').toLowerCase() === 'string'))
|
|
117
|
+
.map((f) => f.key)
|
|
118
|
+
.slice(0, 6);
|
|
119
|
+
if (stringFields.length) {
|
|
120
|
+
filter.$or = stringFields.map((k) => ({ [k]: { $regex: trimmedSearch, $options: 'i' } }));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const sortObj = (() => {
|
|
125
|
+
if (!sort) return { createdAt: -1 };
|
|
126
|
+
if (typeof sort === 'object') return sort;
|
|
127
|
+
const parsed = safeJsonParse(String(sort), null);
|
|
128
|
+
return parsed && typeof parsed === 'object' ? parsed : { createdAt: -1 };
|
|
129
|
+
})();
|
|
130
|
+
|
|
131
|
+
const [items, total] = await Promise.all([
|
|
132
|
+
model.find(filter).sort(sortObj).skip(skip).limit(safeLimit).lean(),
|
|
133
|
+
model.countDocuments(filter),
|
|
134
|
+
]);
|
|
135
|
+
|
|
136
|
+
res.json({
|
|
137
|
+
ok: true,
|
|
138
|
+
modelName: safeModelName,
|
|
139
|
+
page: safePage,
|
|
140
|
+
limit: safeLimit,
|
|
141
|
+
total,
|
|
142
|
+
pages: Math.ceil(total / safeLimit) || 1,
|
|
143
|
+
items,
|
|
144
|
+
});
|
|
145
|
+
} catch (e) {
|
|
146
|
+
res.status(500).json({ error: e?.message ? String(e.message) : 'Preview failed' });
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
exports.upsertEnvironment = async (req, res) => {
|
|
151
|
+
try {
|
|
152
|
+
const { envKey, name, connectionString, description, assetsTarget } = req.body || {};
|
|
153
|
+
const saved = await migrationService.upsertEnvironment(envKey, {
|
|
154
|
+
name,
|
|
155
|
+
connectionString,
|
|
156
|
+
description,
|
|
157
|
+
assetsTarget,
|
|
158
|
+
});
|
|
159
|
+
res.json({ environment: saved });
|
|
160
|
+
} catch (e) {
|
|
161
|
+
res.status(e.status || 500).json({ error: e?.message ? String(e.message) : 'Failed to save environment' });
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
exports.getEnvironment = async (req, res) => {
|
|
166
|
+
try {
|
|
167
|
+
const { envKey } = req.params || {};
|
|
168
|
+
if (!envKey) return res.status(400).json({ error: 'envKey is required' });
|
|
169
|
+
const env = await migrationService.getEnvironmentConfig(envKey);
|
|
170
|
+
if (!env) return res.status(404).json({ error: 'Environment not found' });
|
|
171
|
+
res.json({ environment: env });
|
|
172
|
+
} catch (e) {
|
|
173
|
+
res.status(e.status || 500).json({ error: e?.message ? String(e.message) : 'Failed to load environment' });
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
exports.testAssetsTarget = async (req, res) => {
|
|
178
|
+
try {
|
|
179
|
+
const { envKey } = req.body || {};
|
|
180
|
+
if (!envKey) return res.status(400).json({ error: 'envKey is required' });
|
|
181
|
+
const result = await migrationService.testAssetsTarget({ targetEnvKey: envKey });
|
|
182
|
+
res.json(result);
|
|
183
|
+
} catch (e) {
|
|
184
|
+
res.status(e.status || 400).json({ error: e?.message ? String(e.message) : 'Assets test failed' });
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
exports.testAssetsCopyKey = async (req, res) => {
|
|
189
|
+
try {
|
|
190
|
+
const { envKey, key, dryRun } = req.body || {};
|
|
191
|
+
if (!envKey) return res.status(400).json({ error: 'envKey is required' });
|
|
192
|
+
if (!key) return res.status(400).json({ error: 'key is required' });
|
|
193
|
+
const result = await migrationService.testAssetsCopyKey({
|
|
194
|
+
targetEnvKey: envKey,
|
|
195
|
+
key,
|
|
196
|
+
dryRun: !!dryRun,
|
|
197
|
+
});
|
|
198
|
+
res.json(result);
|
|
199
|
+
} catch (e) {
|
|
200
|
+
res.status(e.status || 400).json({ error: e?.message ? String(e.message) : 'Assets copy test failed' });
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
exports.deleteEnvironment = async (req, res) => {
|
|
205
|
+
try {
|
|
206
|
+
const { envKey } = req.params;
|
|
207
|
+
const result = await migrationService.deleteEnvironment(envKey);
|
|
208
|
+
res.json(result);
|
|
209
|
+
} catch (e) {
|
|
210
|
+
res.status(e.status || 500).json({ error: e?.message ? String(e.message) : 'Failed to delete environment' });
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
exports.testConnection = async (req, res) => {
|
|
215
|
+
try {
|
|
216
|
+
const { envKey } = req.body || {};
|
|
217
|
+
if (!envKey) return res.status(400).json({ error: 'envKey is required' });
|
|
218
|
+
const result = await migrationService.testConnection(envKey);
|
|
219
|
+
res.json(result);
|
|
220
|
+
} catch (e) {
|
|
221
|
+
res.status(400).json({ error: e?.message ? String(e.message) : 'Connection test failed' });
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
exports.runMigration = async (req, res) => {
|
|
226
|
+
try {
|
|
227
|
+
const { envKey, modelName, query, dryRun } = req.body || {};
|
|
228
|
+
if (!envKey) return res.status(400).json({ error: 'envKey is required' });
|
|
229
|
+
if (!modelName) return res.status(400).json({ error: 'modelName is required' });
|
|
230
|
+
|
|
231
|
+
const registry = getModelRegistry();
|
|
232
|
+
if (!registry) {
|
|
233
|
+
return res.status(500).json({ error: 'saasbackend models registry is not available' });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const sourceModel = registry[modelName] || null;
|
|
237
|
+
|
|
238
|
+
if (!sourceModel) {
|
|
239
|
+
return res.status(400).json({
|
|
240
|
+
error: `Unknown model '${modelName}'`,
|
|
241
|
+
availableModels: Object.keys(registry).sort(),
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const result = await migrationService.migrateModel({
|
|
246
|
+
sourceModel,
|
|
247
|
+
targetEnvKey: envKey,
|
|
248
|
+
modelName,
|
|
249
|
+
query,
|
|
250
|
+
dryRun: !!dryRun,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
res.json(result);
|
|
254
|
+
} catch (e) {
|
|
255
|
+
res.status(e.status || 500).json({ error: e?.message ? String(e.message) : 'Migration failed' });
|
|
256
|
+
}
|
|
257
|
+
};
|