Skripty konektoru: vykonna cast s manifestem a kontrolou parametru
Konektory dostaly vykonnou cast. Jeden skript je jeden soubor, ktery nese manifest (vstupni a vystupni parametry) i kod. Diky manifestu s nim umi pracovat strom automatizace, aniz by o kodu cokoliv vedel. Soubory jsou zamerne obycejny JavaScript, ne TypeScript. TypeScript by se musel prelozit a to je presne to otaceni, ktere tady nema byt. Registr sleduje cas zmeny souboru, takze uprava v portalu, rucni uprava souboru i novy soubor ve slozce funguji stejne a bez restartu. Pridano: - scripts/ se skripty konektoru, nazev souboru je zaroven ID operace - kontrola vstupu i vystupu proti manifestu, jedna funkce pro obe strany. Chybejici povinny vystup je chyba skriptu, ne uzivatele - jinak by strom veril parametru, ktery nikdy nedosel - ctx predavany skriptu: http nad adresou napojeni, util, log, config, idempotencyKey, fail a retry. Skript nedostane pristupove udaje - rozliseni opakovatelne a koncove chyby. Runner nikdy nevyhodi vyjimku, vzdy vraci vysledek vcetne retryable - redakce tajnych hodnot pred zapisem do logu. Cizi API rado vraci prijaty token v chybove zprave a log ticketu vidi klient - napojeni z environment variables vcetne iDokladu - sest ukazkovych skriptu pro iDoklad proti skutecnemu API sluzby services.csbot.cz/apps/idoklad, kazdy na jiny vzor - stranka /dashboard/skripty: seznam, manifest, editor, zkusebni spusteni. Formular testu se sklada z manifestu, nepise se pro kazdy skript - endpointy /api/dashboard/scripts vcetne Swaggeru Zmeneno: - katalog konektoru uz neni jen staticky seznam. Akce ze skriptu se domeruji prekryvem v src/data/connectors.ts, takze se naraz objevi ve validaci stromu, ve vypoctu scope i v sablonach. Pri stejnem ID vyhrava skript - ConnectorOperation ma implementation a scriptId - ApiError na klientovi nese cele telo odpovedi a umi z nej vytahnout issues - Dockerfile kopiruje scripts/ do vysledneho image Ukladani nemuze rozbit fungujici skript: kod se nejdriv zapise do docasneho souboru, ten se nacte a overi, a az pak prepise puvodni. K tomu tri dokumenty navrhu dalsich kroku: 09 datove modely a prava, 10 runtime a rozpocet na 150 klientu, 11 popis skriptu konektoru. Overeno: npm run typecheck prochazi na serveru i webu. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
bbc2236c0d
commit
6f6b287d7e
+11
-2
@@ -18,8 +18,8 @@ import {
|
||||
} from '../data/automationStore.js';
|
||||
import { operatorAllowedForType, operatorsByType } from '../data/conditions.js';
|
||||
import {
|
||||
connectorCatalog,
|
||||
connectorCategories,
|
||||
connectors,
|
||||
findOperation,
|
||||
providedFieldsFor,
|
||||
} from '../data/connectors.js';
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
type TicketStatus,
|
||||
} from '../data/ticketStore.js';
|
||||
import { requireAuth } from '../middleware/auth.js';
|
||||
import { scriptsRouter } from './scripts.js';
|
||||
import { streamRouter } from './stream.js';
|
||||
|
||||
export const dashboardRouter = Router();
|
||||
@@ -341,12 +342,20 @@ dashboardRouter.post('/tickets/:id/comment', (req, res) => {
|
||||
// Zivy stream zmen. Musi byt pred obecnymi cestami, aby ho nic neprebilo.
|
||||
dashboardRouter.use('/stream', streamRouter);
|
||||
|
||||
// Skripty konektoru. Taky pred obecnymi cestami.
|
||||
dashboardRouter.use('/scripts', scriptsRouter);
|
||||
|
||||
// ---------------------------------------------------------------- konektory
|
||||
|
||||
/**
|
||||
* Katalog uz neni jen staticky seznam. Operace, ktere obsluhuje skript, se
|
||||
* domeruji z jeho manifestu, takze builder vidi skutecne vstupy a vystupy.
|
||||
* Podrobnosti v `src/scripts/catalog.ts`.
|
||||
*/
|
||||
dashboardRouter.get('/connectors', (_req, res) => {
|
||||
res.json({
|
||||
categories: connectorCategories,
|
||||
items: connectors,
|
||||
items: connectorCatalog(),
|
||||
// Frontend potrebuje vedet, jake operatory nabidnout ke kteremu typu,
|
||||
// a jakou zakladni adresu ukazat u webhooku.
|
||||
operatorsByType,
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Sprava skriptu konektoru z portalu.
|
||||
*
|
||||
* Cteni smi kazdy prihlaseny - builder potrebuje vedet, co skript umi.
|
||||
* Uprava a spusteni smi jen spravce platformy. Uprava skriptu meni chovani
|
||||
* vseho, co ho pouziva, takze to neni pravo, ktere se dava vedle prava
|
||||
* zakladat tickety (viz documentation/09-navrh-rozsireni.md, bod 9).
|
||||
*/
|
||||
|
||||
import { Router } from 'express';
|
||||
import { z } from 'zod';
|
||||
import { requirePlatformAdmin } from '../middleware/auth.js';
|
||||
import { connectionStatus, connectorsWithAuth } from '../scripts/connections.js';
|
||||
import { connectorIdOf, operationIdOf } from '../scripts/manifest.js';
|
||||
import {
|
||||
ensureLoaded,
|
||||
getScript,
|
||||
isValidScriptId,
|
||||
listManifests,
|
||||
readSource,
|
||||
saveSource,
|
||||
scriptProblems,
|
||||
scriptsDir,
|
||||
} from '../scripts/registry.js';
|
||||
import { runScript } from '../scripts/runner.js';
|
||||
|
||||
export const scriptsRouter = Router();
|
||||
|
||||
/** Manifest plus to, co si klient nema dopocitavat sam. */
|
||||
async function scriptSummaries() {
|
||||
const manifests = await listManifests();
|
||||
return manifests.map((manifest) => ({
|
||||
...manifest,
|
||||
connectorId: connectorIdOf(manifest.id),
|
||||
operationId: operationIdOf(manifest.id),
|
||||
}));
|
||||
}
|
||||
|
||||
scriptsRouter.get('/', async (_req, res) => {
|
||||
const [items, problems] = await Promise.all([scriptSummaries(), scriptProblems()]);
|
||||
|
||||
res.json({
|
||||
items,
|
||||
problems,
|
||||
connections: connectorsWithAuth().map(connectionStatus),
|
||||
/** Kam se soubory ukladaji. Kdo ma na server pristup, upravi je i rucne. */
|
||||
directory: scriptsDir(),
|
||||
});
|
||||
});
|
||||
|
||||
scriptsRouter.post('/reload', requirePlatformAdmin, async (_req, res) => {
|
||||
await ensureLoaded(true);
|
||||
const [items, problems] = await Promise.all([scriptSummaries(), scriptProblems()]);
|
||||
res.json({ items, problems });
|
||||
});
|
||||
|
||||
scriptsRouter.get('/:id', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
if (!isValidScriptId(id)) {
|
||||
return res.status(400).json({ error: 'validation_error', message: 'Neplatné ID skriptu.' });
|
||||
}
|
||||
|
||||
const [script, source] = await Promise.all([getScript(id), readSource(id)]);
|
||||
if (source === null) {
|
||||
return res.status(404).json({ error: 'not_found', message: 'Skript neexistuje.' });
|
||||
}
|
||||
|
||||
// Manifest muze chybet, kdyz je soubor rozbity. Kod se vrati vzdy, aby slo opravit.
|
||||
const problems = await scriptProblems();
|
||||
return res.json({
|
||||
id,
|
||||
connectorId: connectorIdOf(id),
|
||||
operationId: operationIdOf(id),
|
||||
manifest: script?.manifest ?? null,
|
||||
code: source,
|
||||
problem: problems.find((problem) => problem.scriptId === id) ?? null,
|
||||
connection: connectionStatus(connectorIdOf(id)),
|
||||
});
|
||||
});
|
||||
|
||||
const saveSchema = z.object({
|
||||
code: z.string().min(1, 'Kód skriptu nesmí být prázdný.'),
|
||||
});
|
||||
|
||||
scriptsRouter.put('/:id', requirePlatformAdmin, async (req, res) => {
|
||||
const { id } = req.params;
|
||||
if (!isValidScriptId(id)) {
|
||||
return res.status(400).json({
|
||||
error: 'validation_error',
|
||||
message: 'Neplatné ID skriptu. Povolený tvar je konektor.operace.',
|
||||
});
|
||||
}
|
||||
|
||||
const parsed = saveSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
return res.status(400).json({
|
||||
error: 'validation_error',
|
||||
message: parsed.error.issues[0]?.message ?? 'Neplatný vstup.',
|
||||
});
|
||||
}
|
||||
|
||||
const result = await saveSource(id, parsed.data.code);
|
||||
if (!result.ok) {
|
||||
// Rozbita uprava se neulozi a puvodni skript dal funguje.
|
||||
return res.status(400).json({
|
||||
error: 'validation_error',
|
||||
message: result.message,
|
||||
issues: result.issues ?? [],
|
||||
});
|
||||
}
|
||||
|
||||
return res.json({ id, manifest: result.manifest });
|
||||
});
|
||||
|
||||
const testSchema = z.object({
|
||||
inputs: z.record(z.unknown()).default({}),
|
||||
});
|
||||
|
||||
/**
|
||||
* Zkusebni spusteni.
|
||||
*
|
||||
* Vola opravdovou sluzbu, tedy vystavena faktura opravdu vznikne. Zamerne:
|
||||
* test, ktery volani predstira, nerekne nic o tom, jestli skript funguje.
|
||||
* Portal na to upozorni pred stiskem.
|
||||
*/
|
||||
scriptsRouter.post('/:id/test', requirePlatformAdmin, async (req, res) => {
|
||||
const { id } = req.params;
|
||||
if (!isValidScriptId(id)) {
|
||||
return res.status(400).json({ error: 'validation_error', message: 'Neplatné ID skriptu.' });
|
||||
}
|
||||
|
||||
const parsed = testSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
return res.status(400).json({
|
||||
error: 'validation_error',
|
||||
message: 'Vstupy musí být objekt s hodnotami parametrů.',
|
||||
});
|
||||
}
|
||||
|
||||
const result = await runScript(id, parsed.data.inputs);
|
||||
console.info(`[scripts] test ${id} uzivatelem ${req.user!.email}: ${result.ok ? 'ok' : 'chyba'}`);
|
||||
|
||||
// Chyba skriptu neni chyba API. Vysledek se vraci vzdy s 200 vcetne popisu.
|
||||
return res.json(result);
|
||||
});
|
||||
Reference in New Issue
Block a user