Decizie arhitecturală

În programul meu de facturare vreau să introduc posibilitatea de scriptare cu javacript (cu cea mai nouă jucărie din domeniu, QuickJS).

De exemplu, vreau să dau posiblitatea utilizatorilor pricepuţi (sau care mă vor plăti să le fac eu scriptul) să importe un set de date oarecare (json, xlsx, csv etc), pe baza cărora să se genereze automat un set de facturi. Pentru că nu vreau să rup compatibilitatea scripturilor în viitor trebuie să cântăresc cu foarte mare grijă opţiunile pe care le va oferi API-ul. De exemplu, acum dilema mea este cum să încapsulez şi să salvez datele în db.

Exista două variante, fie expun o clasă de tipul “Factura” care să încapsuleze inclusiv operaţiile de retrieve/salvare, fie am un soi de obiect global care expune o metodă statică care să primească ca argument un array de date şi să salveze acele date.

Varianta 1)

let factura = new Factura;

factura.benef_denumire = "Firma X SRL";
factura.benef_cif = "1234567";
factura.benef_j = "J13/1234/2019";

factura.save();

Varianta 2)

let factura = {
    benef_denumire: "Firma X SRL",
    benef_cif: "1234567",
    benef_j: "J13/1234/2019"    
};

Facturi.save(factura);

Care variantă vi se pare mai “apetisantă” şi de ce? Mie varianta a doua îmi convine mai mult pentru că metodei save() îi pot pasa şi un array de facturi, pentru ca insertul să se facă într-o singură tranzacţie.

Varianta 1) pare a fi un model, varianta 2) pare a fi un repository, abordarea corecta este undeva la mijloc, o sa ai nevoie de modele,modelele le vei pasa unui repository pentru persistenta, iar in repository vei avea un gateway care mediaza de fapt conexiunea cu baza de date.

Sper ca este doar un exemplu si nu faci vreodata ceva de genul new Factura si foloseste un factory sau o familie de factories.

Atenţie, programul e scris în C++, eu vreau să expun către un utilizator un API javascript. Trebuie să fie ceva simplu, nu vrem să-şi prindă userii urechile în factories :slight_smile:

Acel API va permite js-ului să deschidă un fişier de pe disc sau să-l aducă de pe internet, să-l parseze şi, într-un mod cât mai simplu, să construiască structura de date şi s-o paseze programului-host, care mai departe se va ocupa să-i aloce număr de ordine şi să-l stocheze în baza de date. Să zicem că ar fi ceva asemănător cu sistemul de pluginuri din browsere. Mă gândesc ca la un moment dat chiar să ofer acces la GUI, pentru ca userul să poată personaliza interfaţa (să adauge butoane cu funcţii noi, de exemplu).

Atunci fa ceva mega simplu, expune-le ceva de genul asta si iti validezi tu datele in c++:

Facturi.add({key: value});
Facturi.bulkAdd([{key:value}]); 

O sa trebuiasca sa ai un set de exceptii super clare.

Eu am avut deaface cu Chromium Embedded Framework in jocuri, daca vrei sa ai un engine bun de JS e cam singurul pe care il recomand, poti sa faci debugging direct cu inspector-ul de chrome la interfetele si obiectele expuse in namespace-ul de node.

Hai sa zicem ca vreau sa scriu un script care imi importa facturile din SmartBill, cu JS in aplicatia ta. Ai nevoie prima data de un playground si un debugger ca sa vad ce fac, dupa trebuie sa convertesti obiectul de factura in obiect JS si inapoi, ar merge pur si simplu marshalling si unmarshalling la JSON, eu l-as face tranzactional, deschizi o factura, setezi proprietatile, inchei tranzactia.
Hai sa zicem ca vreau sa import 200 de facturi, iti trebuie o metoda de batching fiindca ar fi super incet pe tranzactii, aici ne-ar trebui ceva ce proceseaza un array de facturi intr-o singura tranzactie. O problema pe care o poti avea e daca faci validare pe fiecare camp. Daca nu e 100% validat ce se introduce din JS in memorie in C++ iti da direct segfault.

Asta ar fi varianta a doua. Cred că este în spiritul plugin-urile abordarea asta şi cred că pe ea o să merg. În script pregăteşti structura de date şi o pasezi host-ului, să facă ce ştie el că trebuie să facă.

E mult prea mare şi complicat, nu prea merită efortul. Iniţial voiam să fac cu Lua, dar după aia am descoperit QuickJS şi am zis să o fac într-un limbaj mai răspândit. Pe QuickJS am reuşit chiar să rulez fără probleme SheetJS, care pare o chestie extrem de complexă.

Asta o problemă la care m-am tot gândit cum s-o fac. La un moment dat probabil chiar o să încorporez un editor de js, bazat pe Scintilla. În prima fază probabil o să bag o consolă js, să poţi rula comenzi interactiv, să te poţi juca interactiv cu API-ul.

Validările le fac şi acum foarte minimal, practic poţi să creezi o factură complet goală, fără date. A fost o decizie de design. Nu se pune problema de segfault, voi converti toate datele venite din js în string-uri. Dacă userul doreşte validare, are libertatea de a o face în js.

Mai este şi posibilitatea de a genera 1000 de facturi greşite, trebuie să mă gândesc la o posibilitate de undo, probabil le voi marca şi voi da userului posibilitatea de şterge complet ultimul import.

1 Like

Eu ma gandesc ca flow-ul pluginului ar fi urmatorul:

// Constructorul inregistreaza plugin-ul
const myPlugin = new Plugin('Import din soft X', alteoptiuni);

// onRun va fi butonul de run al plugin-ului din meniul aplicatiei
myPlugin.onRun = async function() { 
   const facturi = await descarcaFacturiDinAPI(user, pass,token...); 
   let facturiNoi = [];  
   facturi.forEach( factura => {
      const facturaNoua = new Factura('numeTemplate');
      facturaNoua.oldId = factura[0].id;
      facturaNoua.cui = factura.client && facturi.client.cui;
     ...
   // Populezi array-ul pentru batch import
   facturiNoi.push(facturaNoua);
  })
 try {
   await myPlugin.batchImport(facturiNoi);
 } catch (e) {
   myPlugin.errorModal(e);
   console.log('Eroare:', e);
 }
}

Acum daca vrei sa fie sincron o sa ai o mica bataie de cap la callback-uri din host fiindca JS nu e sincron, adica nu stiu cum implementezi callback-uri sincron cu QuickJS.

Am vazut ca ai compiler QuickJS cu TypeScript in loc de JS, eu recomand sa folosesti TypeScript direct.

Păi nu e nevoie de asincronism, se poate implementa totul sincron. QuickJS cred că nici măcar nu are AJAX implementat, se poate implementa pur şi simplu un wrapper peste CURL, care deja există în aplicaţia C++. Sau, dacă vreau neapărat să fac să nu-mi “îngheţe” GUI-ul în timpul importului, pot să lansez scriptul într-un thread separat, dar nu sunt foarte convins că merită efortul.

Ceva de genul ăsta:

let data = oMetodaOarecareDeFetch();

data.forEach(function(row)
{
    let serviciu = {
        denumire: "Abonament internet",
        um: "GB",
        cant: 10,
        pret_unitar: 25
    };
    
    let taxa = {
        denumire: "Taxa intretinere",
        um: "buc",
        cant: 1,
        pret_unitar: 10
    }
    
    let factura = {
        data: '2019-08-19',
        emite_chitanta: false,
        cota_tva: 19,
        beneficiar: Nomenclator.Beneficiari.getByCif(row.cif),
        delegat: Nomenclator.Delegati.getByCif(row.cif),
        articole: [ serviciu, taxa ]
    };

    Facturi.save(factura);
});

Daca cumva folosesti QT, poti direct sa expui qml-ul (care tot un fel de Javascript e).
In trecut, am creeat un joculet in care imi scriptam monstruletii si itemele din Qml si a mers super, plus a fost si super amuzant :slight_smile:

Nu, e FOX Toolkit.