Vlastni skripty firmy: prevod dat v JS, v logu vstup i vystup
Klikaci pravidla jsou u peti poli rychlejsi, ale u modelu objednavky je jich dvacet a v tom se necte. Vedle nich proto skript firmy: prevod z A do B napsany v JS, jeden na zakaznika. - Skripty se ukladaji do uloziste, ne na disk. Disk je uvnitr kontejneru a redeploy ho vymaze. - Krok Transformace dat - Vlastni skript. Vysledek jde dal jako krok.result. - V logu ticketu je u kroku vstup i vystup. Prave to byl duvod, proc skript nad pravidly vyhral. - Zkouska bez ulozeni: v portalu se vlepi skutecne telo a hned je videt, co z toho leze. - Skripty jsou v zalozce Akce, vedle definic akci. Obojí je popis toho, co aplikace ve firme umi, a spravuje to tentyz clovek. Skript je ciste prevod hodnot: dostane input, vrati objekt. Nema require, import, process, fetch ani console, bezi nejvys 2 s a vysledek se vejde do 256 kB. node:vm neni bezpecnostni hranice proti nekomu, kdo se chce dostat ven - je to izolace proti nehode a proti zacykleni. Pri zkousce se ukazalo, ze casovy limit nepokryval samotny beh: runInContext jen vyrobil funkci a zavolat ji zvenku znamenalo, ze while (true) uvnitr zablokovalo proces navzdy. Kod se ted vola uvnitr runInContext. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
99561c4682
commit
e4779caf82
@@ -17,6 +17,7 @@ import {
|
||||
type FlowStep,
|
||||
} from '../data/automationStore.js';
|
||||
import { operatorAllowedForType, operatorsByType } from '../data/conditions.js';
|
||||
import { listTenantScripts } from '../data/tenantScripts.js';
|
||||
import {
|
||||
defaultConnectorFor,
|
||||
getConnector,
|
||||
@@ -70,6 +71,7 @@ import { settingsRouter } from './settings.js';
|
||||
import { ticketActionsRouter } from './ticketActions.js';
|
||||
import { widgetDataRouter } from './widgetData.js';
|
||||
import { scriptsRouter } from './scripts.js';
|
||||
import { tenantScriptRouter } from './tenantScripts.js';
|
||||
import { streamRouter } from './stream.js';
|
||||
|
||||
export const dashboardRouter = Router();
|
||||
@@ -729,6 +731,8 @@ dashboardRouter.use('/stream', streamRouter);
|
||||
|
||||
// Skripty konektoru. Taky pred obecnymi cestami.
|
||||
dashboardRouter.use('/scripts', scriptsRouter);
|
||||
// Skripty firmy. Prevod dat v JS, na rozdil od skriptu sluzeb nevolaji ven.
|
||||
dashboardRouter.use('/tenant-scripts', tenantScriptRouter);
|
||||
|
||||
// Sluzby a konektory. `/connectors/services` je uvnitr toho routeru.
|
||||
dashboardRouter.use('/connectors', connectorsRouter);
|
||||
@@ -771,6 +775,10 @@ dashboardRouter.get('/services', (req, res) => {
|
||||
{
|
||||
people: listPeople(tenantIds).map((person) => ({ id: person.id, name: person.name })),
|
||||
groups: listGroups(tenantIds).map((group) => ({ id: group.id, name: group.name })),
|
||||
scripts: listTenantScripts(tenantIds).map((script) => ({
|
||||
id: script.id,
|
||||
name: script.name,
|
||||
})),
|
||||
},
|
||||
),
|
||||
// Frontend potrebuje vedet, jake operatory nabidnout ke kteremu typu,
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* Skripty firmy: prevod dat z A do B napsany v JS.
|
||||
*
|
||||
* Zamerne to **neni** `routes/scripts.ts`. Ten spravuje skripty sluzeb, ktere
|
||||
* volaji cizi API, patri celé platforme a smi je menit jen jeji spravce.
|
||||
* Tohle je prevod hodnot uvnitr jedne firmy: nic nevola ven, vidi jen data,
|
||||
* ktera mu krok preda, a spravuje si ho firma sama.
|
||||
*
|
||||
* Pravo je `action.manage`, tedy totez co u definic akci. Obojí je popis toho,
|
||||
* co aplikace ve firme umi - ne bezna prace s tickety.
|
||||
*/
|
||||
|
||||
import { Router } from 'express';
|
||||
import { z } from 'zod';
|
||||
import { recordAudit } from '../data/audit.js';
|
||||
import {
|
||||
findTenantScript,
|
||||
listTenantScripts,
|
||||
newTenantScript,
|
||||
refreshTenantScripts,
|
||||
tenantScriptStore,
|
||||
MAX_CODE_LENGTH,
|
||||
SCRIPT_TEMPLATE,
|
||||
} from '../data/tenantScripts.js';
|
||||
import { hasPermission } from '../data/permissions.js';
|
||||
import { requireAuth } from '../middleware/auth.js';
|
||||
import { runSandbox } from '../runtime/sandbox.js';
|
||||
import { currentTenant } from './crud.js';
|
||||
|
||||
export const tenantScriptRouter = Router();
|
||||
|
||||
tenantScriptRouter.use(requireAuth);
|
||||
|
||||
/** Kdo smi menit. Cteni staci prihlaseni - builder potrebuje nabidku skriptu. */
|
||||
const PERMISSION = 'action.manage';
|
||||
|
||||
/** Vrati false a odpovi, kdyz volajici na zmenu nema pravo. */
|
||||
function mayWrite(req: Parameters<typeof requireAuth>[0], res: Parameters<typeof requireAuth>[1]): boolean {
|
||||
if (hasPermission(req.user!, PERMISSION)) return true;
|
||||
console.warn(`[skripty] ${req.user!.email}: chybi pravo ${PERMISSION}`);
|
||||
res.status(403).json({ error: 'forbidden', message: 'K této změně nemáte oprávnění.' });
|
||||
return false;
|
||||
}
|
||||
|
||||
const bodySchema = z.object({
|
||||
name: z.string().trim().min(2, 'Skript musí mít název.').max(80),
|
||||
description: z.string().trim().max(300).optional(),
|
||||
code: z
|
||||
.string()
|
||||
.min(1, 'Skript nemůže být prázdný.')
|
||||
.max(MAX_CODE_LENGTH, `Skript se vejde do ${MAX_CODE_LENGTH} znaků.`),
|
||||
enabled: z.boolean().optional(),
|
||||
});
|
||||
|
||||
tenantScriptRouter.get('/', (req, res) => {
|
||||
const tenantId = currentTenant(req, res);
|
||||
if (!tenantId) return;
|
||||
|
||||
return res.json({
|
||||
items: listTenantScripts([tenantId]),
|
||||
/** Vzor pro novy skript. Radeji neco, co rovnou bezi, nez prazdne okno. */
|
||||
template: SCRIPT_TEMPLATE,
|
||||
maxLength: MAX_CODE_LENGTH,
|
||||
});
|
||||
});
|
||||
|
||||
tenantScriptRouter.post('/', (req, res) => {
|
||||
const tenantId = currentTenant(req, res);
|
||||
if (!tenantId) return;
|
||||
if (!mayWrite(req, res)) return;
|
||||
|
||||
const parsed = bodySchema.safeParse(req.body ?? {});
|
||||
if (!parsed.success) {
|
||||
return res.status(400).json({
|
||||
error: 'validation_error',
|
||||
message: parsed.error.issues[0]?.message ?? 'Neplatné údaje.',
|
||||
});
|
||||
}
|
||||
|
||||
const script = newTenantScript({ tenantId, ...parsed.data });
|
||||
|
||||
return void tenantScriptStore
|
||||
.create(script)
|
||||
.then(async () => {
|
||||
await refreshTenantScripts();
|
||||
recordAudit({
|
||||
userId: req.user!.id,
|
||||
userEmail: req.user!.email,
|
||||
tenantId,
|
||||
action: 'script.create',
|
||||
target: script.id,
|
||||
detail: { nazev: script.name },
|
||||
});
|
||||
return res.status(201).json(script);
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
console.error('[skripty] zalozeni selhalo:', err);
|
||||
return res
|
||||
.status(500)
|
||||
.json({ error: 'internal_error', message: 'Skript se nepodařilo uložit.' });
|
||||
});
|
||||
});
|
||||
|
||||
tenantScriptRouter.put('/:id', (req, res) => {
|
||||
const tenantId = currentTenant(req, res);
|
||||
if (!tenantId) return;
|
||||
if (!mayWrite(req, res)) return;
|
||||
|
||||
if (!findTenantScript(req.params.id, [tenantId])) {
|
||||
return res.status(404).json({ error: 'not_found', message: 'Skript neexistuje.' });
|
||||
}
|
||||
|
||||
const parsed = bodySchema.safeParse(req.body ?? {});
|
||||
if (!parsed.success) {
|
||||
return res.status(400).json({
|
||||
error: 'validation_error',
|
||||
message: parsed.error.issues[0]?.message ?? 'Neplatné údaje.',
|
||||
});
|
||||
}
|
||||
|
||||
return void tenantScriptStore
|
||||
.update(req.params.id, parsed.data, { tenantIds: [tenantId], includeGlobal: false })
|
||||
.then(async (updated) => {
|
||||
if (!updated) {
|
||||
return res.status(404).json({ error: 'not_found', message: 'Skript neexistuje.' });
|
||||
}
|
||||
await refreshTenantScripts();
|
||||
recordAudit({
|
||||
userId: req.user!.id,
|
||||
userEmail: req.user!.email,
|
||||
tenantId,
|
||||
action: 'script.update',
|
||||
target: req.params.id,
|
||||
detail: { nazev: parsed.data.name },
|
||||
});
|
||||
return res.json(updated);
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
console.error('[skripty] uprava selhala:', err);
|
||||
return res
|
||||
.status(500)
|
||||
.json({ error: 'internal_error', message: 'Skript se nepodařilo uložit.' });
|
||||
});
|
||||
});
|
||||
|
||||
tenantScriptRouter.delete('/:id', (req, res) => {
|
||||
const tenantId = currentTenant(req, res);
|
||||
if (!tenantId) return;
|
||||
if (!mayWrite(req, res)) return;
|
||||
|
||||
return void tenantScriptStore
|
||||
.remove(req.params.id, { tenantIds: [tenantId], includeGlobal: false })
|
||||
.then(async (removed) => {
|
||||
if (!removed) {
|
||||
return res.status(404).json({ error: 'not_found', message: 'Skript neexistuje.' });
|
||||
}
|
||||
await refreshTenantScripts();
|
||||
recordAudit({
|
||||
userId: req.user!.id,
|
||||
userEmail: req.user!.email,
|
||||
tenantId,
|
||||
action: 'script.delete',
|
||||
target: req.params.id,
|
||||
});
|
||||
return res.status(204).end();
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
console.error('[skripty] smazani selhalo:', err);
|
||||
return res
|
||||
.status(500)
|
||||
.json({ error: 'internal_error', message: 'Skript se nepodařilo smazat.' });
|
||||
});
|
||||
});
|
||||
|
||||
const testSchema = z.object({
|
||||
code: z.string().min(1).max(MAX_CODE_LENGTH),
|
||||
/** Vstup, na kterem se to zkusi. Typicky ukazka tela od spoustece. */
|
||||
input: z.unknown(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Zkuseni skriptu bez ulozeni.
|
||||
*
|
||||
* Tohle je duvod, proc se skript pise tady a ne do souboru: napisu prevod,
|
||||
* vlepim skutecne telo a hned vidim, co z toho leze. Bez toho se chyba pozna
|
||||
* az z padleho behu o hodinu pozdeji.
|
||||
*/
|
||||
tenantScriptRouter.post('/test', (req, res) => {
|
||||
const tenantId = currentTenant(req, res);
|
||||
if (!tenantId) return;
|
||||
if (!mayWrite(req, res)) return;
|
||||
|
||||
const parsed = testSchema.safeParse(req.body ?? {});
|
||||
if (!parsed.success) {
|
||||
return res.status(400).json({
|
||||
error: 'validation_error',
|
||||
message: parsed.error.issues[0]?.message ?? 'Neplatné údaje.',
|
||||
});
|
||||
}
|
||||
|
||||
const result = runSandbox(parsed.data.code, parsed.data.input ?? {});
|
||||
|
||||
// 200 i kdyz skript spadl. Chyba ve skriptu neni chyba API.
|
||||
return res.json({
|
||||
ok: result.ok,
|
||||
output: result.value,
|
||||
error: result.error,
|
||||
durationMs: result.durationMs,
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user