Transformace dat, oprava ukladani udaju konektoru
Transformace dat ve dvou rezimech plus oprava chyby, kvuli ktere se neukladaly
pristupove udaje konektoru. Popis v documentation/13-transformace-dat.md.
Kroky si predavaji i cele struktury:
- FieldType ma object a list. Do sablony se nedosazuji, predavaji se jako celek
dalsimu kroku - proto je u nich v builderu vyber a ne textove pole. Z podminek
nad nimi ma smysl jen "prisla / neprisla"
- strop na velikost struktury (SCRIPT_MAX_VALUE_BYTES, vychozi 256 kB). Radek
s vystupem kroku je nejrychleji rostouci tabulka v systemu
Dva rezimy transformace, oba nad enginem v src/scripts/mapping.ts:
- transform.map-fields: pole na pole s prevody, klikatelne
- transform.to-json: sablona cileveho objektu s ${cesta}
Marker ${...} je zamerne jiny nez {{...}}. Sablony kroku se dosazuji driv, nez
krok bezi, takze {{total}} by strom stihl vyhodnotit, nenasel by parametr toho
jmena a dosadil by prazdno. Cely retezec navic zachova typ, takze
"unitPrice": "${total}" vyrobi cislo - jinak by cizi sluzba dostala castku jako
text a odmitla ji.
Prevod map pro seznamy je to, bez ceho by priklad nesel dokoncit. Bez nej jde
prevest hlavicku dokladu, ale ne polozky objednavky, a doklad by byl na nulu.
Dal pridano:
- idoklad.create-invoice-from-object: druha polovina prikladu, bere hotove telo
dokladu z transformace a doplni povinna pole ze vzoru iDokladu
- spoustec e-shopu predava celou objednavku jako objekt a polozky jako seznam
- klikaci editor pravidel vcetne rezimu JSON pro vnorena pravidla u map
- kontrola JSONu a tvaru pravidel uz pri ulozeni stromu. Preklep je nedodelek,
ne chyba ukladani - rozdelana prace se nezahazuje
Opraveno: konektor neukladal pristupove udaje. Server byl v poradku, overeno
volanim POST i PATCH. Chyba byla v prohlizeci: u pole type="password" prohlizec
ignoruje autocomplete="off" a dosazuje ulozene prihlaseni. Uzivatel pak videl
jednu hodnotu, React drzel jinou, a ulozilo se to, co drzel React, tedy nic.
Resi to autocomplete="new-password", jmena poli, ktera nepripominaji heslo,
a prepinac zobrazeni, aby slo overit, co je opravdu zapsane.
Zakladani a uprava konektoru se presunuly do dialogu, na strance jsou jen male
karty. Formulare rozlozene po strance byly u vic konektoru neprehledne.
Overeno: npm run typecheck prochazi na serveru i webu, node --check na skriptech.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8ad91a6c28
commit
ad56c7f513
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* iDoklad: vystaveni faktury z hotoveho objektu.
|
||||
*
|
||||
* Sluzba: https://services.csbot.cz/apps/idoklad
|
||||
* Endpointy: GET /issued-invoices/default, POST /issued-invoices
|
||||
*
|
||||
* Druha polovina prikladu z transformace: prijde objednavka z e-shopu,
|
||||
* `transform.map-fields` z ni udela telo dokladu a tenhle krok ho odesle.
|
||||
*
|
||||
* Rozdil proti `idoklad.create-issued-invoice`: ta akce ma pole na kliknuti
|
||||
* a umi jednu polozku. Tahle bere cely objekt, takze zvladne vic polozek
|
||||
* a cokoliv dalsiho, co iDoklad prijima. Cenou je, ze tvar tela musi znat ten,
|
||||
* kdo stavi transformaci.
|
||||
*
|
||||
* Vzor z `/default` se poradi i tady: nese povinna pole, ktera nikdo vyplnovat
|
||||
* nechce (`documentSerialNumber`, `isEet`, `isIncomeTax`). Objekt z transformace
|
||||
* ho jen prepise tam, kde neco rika.
|
||||
*/
|
||||
|
||||
export const manifest = {
|
||||
id: 'idoklad.create-invoice-from-object',
|
||||
name: 'Vystavit fakturu z objektu',
|
||||
description:
|
||||
'Odešle do iDokladu připravené tělo dokladu. Chybějící povinná pole se ' +
|
||||
'doplní z předvyplněného vzoru iDokladu.',
|
||||
timeoutMs: 25000,
|
||||
|
||||
inputs: [
|
||||
{
|
||||
id: 'payload',
|
||||
label: 'Tělo dokladu',
|
||||
type: 'object',
|
||||
required: true,
|
||||
hint: 'Objekt z transformace dat. Klíče podle iDokladu, například partnerId, items.',
|
||||
},
|
||||
{
|
||||
id: 'requireItems',
|
||||
label: 'Vyžadovat aspoň jednu položku',
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
default: true,
|
||||
hint: 'Doklad bez položek iDoklad přijme, ale bude na nulu.',
|
||||
},
|
||||
],
|
||||
|
||||
outputs: [
|
||||
{ id: 'invoiceId', label: 'ID faktury', type: 'number', required: true },
|
||||
{ id: 'documentNumber', label: 'Číslo dokladu', type: 'string', required: true },
|
||||
{ id: 'totalWithVat', label: 'Celkem s DPH', type: 'number', required: false },
|
||||
{ id: 'itemCount', label: 'Počet položek', type: 'number', required: true },
|
||||
{ id: 'invoice', label: 'Celý doklad', type: 'object', required: true },
|
||||
],
|
||||
};
|
||||
|
||||
export async function run(inputs, ctx) {
|
||||
const { unwrap, pick, text, num, need } = ctx.util;
|
||||
const payload = inputs.payload;
|
||||
|
||||
const items = Array.isArray(payload.items) ? payload.items : [];
|
||||
if ((inputs.requireItems ?? true) && items.length === 0) {
|
||||
ctx.fail('Tělo dokladu neobsahuje žádné položky. Zkontrolujte pravidla transformace.');
|
||||
}
|
||||
if (num(pick(payload, 'partnerId')) === null) {
|
||||
ctx.fail('Tělo dokladu neobsahuje partnerId, iDoklad by doklad nepřijal.');
|
||||
}
|
||||
|
||||
// Vzor nese povinna pole, ktera z objednavky nikdy neprijdou.
|
||||
const defaults = unwrap((await ctx.http.get('/issued-invoices/default')).body);
|
||||
if (!defaults || typeof defaults !== 'object') {
|
||||
ctx.retry('iDoklad nevrátil předvyplněný vzor faktury.');
|
||||
}
|
||||
|
||||
const body = { ...defaults, ...payload };
|
||||
|
||||
const response = await ctx.http.post('/issued-invoices', body);
|
||||
const invoice = unwrap(response.body);
|
||||
|
||||
ctx.log(`Doklad vystaven z objektu, položek: ${items.length}.`);
|
||||
|
||||
return {
|
||||
invoiceId: need(num(pick(invoice, 'id')), 'ID vystavené faktury'),
|
||||
documentNumber: need(text(pick(invoice, 'documentNumber', 'number')), 'číslo dokladu'),
|
||||
totalWithVat: num(pick(invoice, 'totalWithVat', 'totalWithVatHc', 'total')),
|
||||
itemCount: items.length,
|
||||
// Cely doklad dal, aby na nej sel navazat dalsi krok bez druheho volani.
|
||||
invoice: invoice && typeof invoice === 'object' ? invoice : {},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Transformace dat, rezim 1: pole na pole.
|
||||
*
|
||||
* Prijde objekt z jedne sluzby a vypadne objekt pro druhou. Kazde pravidlo rika
|
||||
* "vezmi tuhle cestu ve zdroji, projed temito prevody a uloz to sem".
|
||||
*
|
||||
* Typicky pripad: objednavka ze Shoptetu a doklad pro iDoklad maji uplne jina
|
||||
* jmena poli. Mezi nimi je tenhle krok.
|
||||
*
|
||||
* Rezim 2 je `transform.to-json` - tam se napise cely cilovy objekt a dosadi se
|
||||
* do nej hodnoty. Volba mezi rezimy je o tom, cehoz je vic: kdyz se prevadi
|
||||
* hodnoty, jsou lepsi pravidla. Kdyz se sklada tvar, je lepsi sablona.
|
||||
*
|
||||
* Sluzba je obecna, takze skript nic nevola a nepotrebuje konektor.
|
||||
*/
|
||||
|
||||
export const manifest = {
|
||||
id: 'transform.map-fields',
|
||||
name: 'Přemapovat pole',
|
||||
description:
|
||||
'Převede objekt na jiný objekt podle pravidel. U každého cílového pole se ' +
|
||||
'určí cesta ve zdroji a případné převody.',
|
||||
|
||||
inputs: [
|
||||
{
|
||||
id: 'source',
|
||||
label: 'Zdrojová data',
|
||||
type: 'object',
|
||||
required: true,
|
||||
hint: 'Objekt z předchozího kroku, například objednávka.',
|
||||
},
|
||||
{
|
||||
id: 'rules',
|
||||
label: 'Pravidla',
|
||||
type: 'list',
|
||||
required: true,
|
||||
control: 'mapping',
|
||||
hint: 'Seznam pravidel. V builderu se klikají, pod tím je vidět JSON.',
|
||||
},
|
||||
],
|
||||
|
||||
outputs: [
|
||||
{ id: 'result', label: 'Výsledek', type: 'object', required: true },
|
||||
{ id: 'fieldCount', label: 'Počet vyplněných polí', type: 'number', required: true },
|
||||
],
|
||||
};
|
||||
|
||||
export async function run(inputs, ctx) {
|
||||
const rules = inputs.rules;
|
||||
|
||||
if (!Array.isArray(rules) || rules.length === 0) {
|
||||
ctx.fail('Nejsou zadaná žádná pravidla, transformace by nic nevytvořila.');
|
||||
}
|
||||
|
||||
// Mapovaci engine je na kontextu, ne v tomhle souboru. Pouziva ho i rezim 2
|
||||
// a mel by byt na jednom miste, ne ve dvou skriptech.
|
||||
const result = ctx.util.applyRules(inputs.source, rules);
|
||||
|
||||
const fieldCount = Object.keys(result).length;
|
||||
ctx.log(`Přemapováno ${fieldCount} polí podle ${rules.length} pravidel.`);
|
||||
|
||||
return { result, fieldCount };
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Transformace dat, rezim 2: sablona JSON.
|
||||
*
|
||||
* Napise se cely cilovy objekt tak, jak ma vypadat, a na mistech hodnot se
|
||||
* odkaze na zdroj pres `${cesta}`. Hodi se, kdyz je hlavni prace ve **tvaru**
|
||||
* cilove struktury - zanoreni, seznamy, pevna pole.
|
||||
*
|
||||
* Rezim 1 je `transform.map-fields`. Ten je lepsi, kdyz je hlavni prace
|
||||
* v prevodech hodnot.
|
||||
*
|
||||
* Proc `${cesta}` a ne `{{cesta}}`: sablony kroku se dosazuji driv, nez se krok
|
||||
* spusti. `{{order.total}}` by tedy stihl vyhodnotit strom, zadny parametr toho
|
||||
* jmena by nenasel a dosadil by prazdno. Jiny marker to nemuze splest.
|
||||
*
|
||||
* Kdyz je hodnota **cely** retezec, zachova se puvodni typ:
|
||||
* `"total": "${order.total}"` vyrobi cislo, ne text. Bez toho by cizi sluzba
|
||||
* dostala castku jako retezec a vetsina jich to odmitne.
|
||||
*/
|
||||
|
||||
export const manifest = {
|
||||
id: 'transform.to-json',
|
||||
name: 'Poskládat objekt ze šablony',
|
||||
description:
|
||||
'Vezme šablonu JSON a dosadí do ní hodnoty ze zdroje přes ${cesta}. ' +
|
||||
'Hodí se, když je potřeba postavit strukturu se zanořením a seznamy.',
|
||||
|
||||
inputs: [
|
||||
{
|
||||
id: 'source',
|
||||
label: 'Zdrojová data',
|
||||
type: 'object',
|
||||
required: true,
|
||||
hint: 'Objekt z předchozího kroku.',
|
||||
},
|
||||
{
|
||||
id: 'template',
|
||||
label: 'Šablona JSON',
|
||||
type: 'string',
|
||||
required: true,
|
||||
multiline: true,
|
||||
control: 'json',
|
||||
hint: 'Cílový objekt. Hodnoty ze zdroje se vkládají jako ${cesta.k.poli}.',
|
||||
},
|
||||
],
|
||||
|
||||
outputs: [
|
||||
{ id: 'result', label: 'Výsledek', type: 'object', required: true },
|
||||
],
|
||||
};
|
||||
|
||||
export async function run(inputs, ctx) {
|
||||
let template;
|
||||
try {
|
||||
template = JSON.parse(String(inputs.template));
|
||||
} catch (err) {
|
||||
// Rozbita sablona je chyba nastaveni, opakovat ji nema smysl.
|
||||
ctx.fail(`Šablona není platný JSON: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
|
||||
if (template === null || typeof template !== 'object' || Array.isArray(template)) {
|
||||
ctx.fail('Šablona musí být objekt, tedy začínat složenou závorkou.');
|
||||
}
|
||||
|
||||
const result = ctx.util.fillJson(template, inputs.source);
|
||||
|
||||
ctx.log(`Poskládán objekt s ${Object.keys(result).length} polemi.`);
|
||||
|
||||
return { result };
|
||||
}
|
||||
Reference in New Issue
Block a user