Prace nad celym modelem: cesty, ukazka tela a smycka nad seznamem

Odesilatel posila cely model. Objednavka ze Shoptetu ma zanoreni, ceny
v podobjektech a seznam polozek - a dosud sel napojit jen plochy seznam
skalarnich poli, takze items[] neslo pouzit vubec.

- Odkaz v sablone muze byt cesta: {{data.order.billingAddress.city}},
  {{data.order.items[0].name}}, {{st_faktura.invoiceId}}. Overuje se prvni
  cast odkazu, takze ploche odkazy funguji dal presne jako driv.
- Cele telo je v kontextu i v puvodnim tvaru. Deklarovane parametry maji
  pri shode jmen prednost.
- Spoustec si pamatuje ukazku skutecneho tela. Server z ni odvodi model,
  tedy seznam cest i s typy, a ten se v krocich klika misto opisovani.
  Tlacitko doplni z hodnot v ukazce parametry pro podminky.
- Novy krok Pro kazdou polozku: projde seznam a za kazdou polozku vykona
  vnoreny podstrom. Uvnitr je item a index, po skonceni krok.results se
  seznamem vysledku. Kazdy vysledek nese i puvodni polozku - radek
  objednavky potrebuje jak ID z CRM, tak mnozstvi z puvodnich dat.
  Strop 200 polozek, mimo seznam krok selze s tim, co tam misto nej je.
- Prevod Za kazdou polozku seznamu v klikacim editoru mapovani. Engine ho
  umel, sel ale napsat jen rucnim JSONem.
- Typograficke uvozovky a sipka z kodu pryc.

Overeno nad skutecnym modelem objednavky: 23 cest vcetne
data.order.items[].unitPrice.withoutVat, sablony s cestou i s indexem,
smycka nad dvema polozkami s posbiranymi vysledky.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
JiriUhlir
2026-08-20 06:53:10 +02:00
co-authored by Claude Opus 5
parent fc0c2ff6c7
commit 9b18531d3e
25 changed files with 1299 additions and 124 deletions
+34 -5
View File
@@ -798,6 +798,13 @@ const stepSchema: z.ZodType<FlowStep> = z.lazy(() =>
// Klic je ID pole z katalogu, hodnota sablona. Overuje se nize.
inputs: z.record(z.string()).optional(),
}),
z.object({
id: z.string().min(1),
kind: z.literal('foreach'),
/** Cesta k seznamu v datech, napr. `data.order.items`. */
path: z.string().trim().min(1, 'Smyčka musí mít cestu k seznamu.').max(200),
steps: z.array(stepSchema),
}),
z.object({
id: z.string().min(1),
kind: z.literal('condition'),
@@ -849,6 +856,19 @@ const flowSchema = z.object({
serviceId: z.string().min(1),
operationId: z.string().min(1),
fields: z.array(fieldSchema),
/**
* Ukazka skutecneho tela. Odvozuje se z ni model, viz data/model.ts.
*
* Strop je kvuli tomu, ze se uklada s automatizaci a cte pri kazdem
* nacteni - cele katalogy sem nepatri.
*/
sample: z
.unknown()
.optional()
.refine(
(value) => value === undefined || JSON.stringify(value).length <= 100_000,
'Ukázka těla je moc velká, vejde se 100 kB.',
),
/** Jak casto se ma sluzba obvolavat. Plati jen u spoustecu, co se ptaji. */
intervalSec: z.number().int().min(10).max(86_400).optional(),
// Token generuje server. Cokoliv od klienta se ignoruje.
@@ -910,7 +930,7 @@ async function validateFlowReferences(
const names = new Set<string>();
for (const field of flow.trigger.fields) {
if (names.has(field.name)) {
problems.push(`Parametr „${field.name}" je uvedený dvakrát.`);
problems.push(`Parametr "${field.name}" je uvedený dvakrát.`);
}
names.add(field.name);
}
@@ -927,13 +947,22 @@ async function validateFlowReferences(
problems.push(`Podmínka odkazuje na neexistující parametr (${step.fieldId}).`);
} else if (!operatorAllowedForType(step.operator, field.type)) {
problems.push(
`Operátor "${step.operator}" nelze použít na parametr „${field.name}" typu ${field.type}.`,
`Operátor "${step.operator}" nelze použít na parametr "${field.name}" typu ${field.type}.`,
);
}
await walk(step.yes);
await walk(step.no);
continue;
}
if (step.kind === 'foreach') {
if (step.path.trim().length === 0) {
problems.push('Smyčka musí mít cestu k seznamu, například data.order.items.');
}
await walk(step.steps);
continue;
}
const action = findOperation(step.serviceId, step.operationId, 'action');
if (!action) {
problems.push(`Akce ${step.serviceId}/${step.operationId} neexistuje v katalogu.`);
@@ -946,7 +975,7 @@ async function validateFlowReferences(
for (const key of Object.keys(step.inputs ?? {})) {
if (!allowed.has(key)) {
problems.push(
`Akce ${step.serviceId}/${step.operationId} nemá nastavitelné pole „${key}".`,
`Akce ${step.serviceId}/${step.operationId} nemá nastavitelné pole "${key}".`,
);
}
}
@@ -965,12 +994,12 @@ async function validateFlowReferences(
parsed = JSON.parse(raw);
} catch (err) {
const detail = err instanceof Error ? err.message : 'neplatný JSON';
issues.push(`Pole „${input.label}" není platný JSON: ${detail}`);
issues.push(`Pole "${input.label}" není platný JSON: ${detail}`);
continue;
}
if (input.kind === 'mapping') {
issues.push(...validateRules(parsed, `pole „${input.label}"`));
issues.push(...validateRules(parsed, `pole "${input.label}"`));
}
}
+9 -3
View File
@@ -207,7 +207,13 @@ webhookRouter.post('/:token', (req, res) => {
* pod `_body`. Diky tomu se v sablonach pise `{{orderId}}` bez ohledu na
* to, jak hluboko to odesilatel schoval, a zaroven se nic neztrati.
*/
payload: { ...values, _body: body },
/*
* Krome pojmenovanych hodnot i **cele telo v puvodnim tvaru**, takze
* `{{data.order.billingAddress.city}}` funguje, aniz by to nekdo predem
* vypsal jako parametr. Deklarovane hodnoty maji prednost: kdyz si nekdo
* pojmenuje parametr stejne jako klic v tele, plati jeho.
*/
payload: { ...body, ...values, _body: body },
})
.then((item) => {
console.info(
@@ -283,13 +289,13 @@ function readPayload(
if (value === undefined || value === null) {
if (field.required) {
const where = field.path && field.path !== field.name ? ` (cesta ${field.path})` : '';
problems.push(`Chybí povinný parametr „${field.name}"${where}.`);
problems.push(`Chybí povinný parametr "${field.name}"${where}.`);
}
continue;
}
if (!matchesType(value, field.type)) {
problems.push(`Parametr „${field.name}" má mít typ ${field.type}.`);
problems.push(`Parametr "${field.name}" má mít typ ${field.type}.`);
continue;
}