Jednoduchý pattern pre tvorbu Total.js aplikácií
Total.js framework vyvíjam už niekoľko rokov a veľmi dlhý čas z tohto vývoja som hľadal vhodný spôsob, ako tvoriť aplikácie. Vytvoril som veľmi veľa aplikácií a stále to nebolo ono, preto som sa trošku inšpiroval Node.js modulom Mongoose.js a jeho schémami.
Total.js schémy
Považujem ich za základnú stavebnú jednotku všetkých Total.js aplikácií, pretože do schém ukladám celú business logiku, validácie a všetko okolo toho. Pomocou schém sa mi podarilo urobiť poriadok a prehľadnosť v zdrojových kódoch všetkých aplikácií. Takže na otázku čo sú vlastne Total.js schémy? Odpoviem jednoducho, že schéma je len nejaký objekt s preddefinovanou štruktúrou vlastností a metód, nič viac.
Hlavné výhody
- dodáva prehľadnosť do kódu
- podporuje veľa dátových typov (
String
, Boolean
, Number
, Date
, Email
, Phone
, atď.)
- podporuje validácie
- podporuje klasické CRUD operácie
save
, get
, delete
a query
- podporuje
workflows
- podporuje
transformations
- podporuje
hooks
- podporuje
operations
- podporuje lokalizácie
- podporuje predvolené hodnoty
- podporuje asynchrónne operácie (veľmi dôležitá funkcia)
- vývojár sa môže vždy spoľahnúť na zadefinovaný dátový typ
- ako dátový typ môže byť použitá iná schéma
- má vstavený error handling (veľmi dôležitá funkcia)
- na schému je možné napojiť routing
- vlastnosti, ktoré nie sú definované v schéme, sú automaticky vymazané
- a mnoho ďalšieho
Ukážka schémy
Ak začnete používať Total.js tak narazíte na množstvo divných názvov, ale postupne si určite zvyknete. Funkcia NEWSCHEMA()
vytvorí novú schému a .make()
vytvorí len privátny scope na deklaráciu schémy.
models/newsletter.js:
NEWSCHEMA('Newsletter').make(function(schema) {
// Alebo:
// var schema = NEWSCHEMA('Newsletter');
// schema.define('property name', 'Data Type', [required], [filter]);
schema.define('email', 'Email', true);
// Overí email v databáze
schema.addWorkflow('check', function(error, model, options, callback) {
// SQL Agent automaticky podporuje error handling zo schém
var sql = DB(error);
sql.exists('newsletter', 'tbl_newsletter').make(function(builder) {
builder.where('email', model.email);
builder.where('isremoved', false);
});
// Nižšie uvedená funkcia skontroluje, či newsletter obsahuje nejaký záznam.
// V prípade, že obsahuje chybu tak podľa aktuálnej lokalizácie sa vyberie podľa
// kľúča "error-email-exists" správa z resources.
sql.validate('newsletter', 'error-email-exists', true);
// Exec vyvolá všetky vyššie uvedené DB operácie v poradí, v akom sú zadané.
sql.exec(n => callback(SUCCESS(true)));
});
// Uloží model do DB
schema.setSave(function(error, model, controller, callback) {
var sql = DB(error);
sql.insert('tbl_newsletter').make(function(builder) {
builder.set('email', model.email);
builder.set('created', F.datetime);
builder.set('ip', controller.ip);
builder.set('isremoved', false);
});
sql.exec(n => callback(SUCCESS(true)));
});
// Notifikuje užívateľa o tom, že sa zaregistroval
schema.addWorkflow('email', function(error, model, options, callback) {
// Nastavenie SMTP sa nachádza v configu
var mail = F.logmail(model.email, 'Boli ste pridaný na odber noviniek', 'Ďakujeme, že ste sa rozhodli odoberať novinky z nášho internetového servera.');
mail.reply('petersirka@gmail.com', true);
callback(SUCCESS(true));
});
});
Takže vytvoril som schému Newsletter
a túto schému teraz môžeme prepojiť s formulárom pre odoberanie noviniek emailom. V prípade, že do schémy pridú iné data, tak framework ich jednoducho odstráni, pretože sa nenachádzajú v schéme.
controllers/api.js:
exports.install = function() {
// Zadafinuje sa trasa a pomocou flags priradíme odkaz na schému "Newsletter"
// Trasa načúva na POST metódu
F.route('/api/newsletter/', json_newsletter_save, ['*Newsletter', 'post']);
}
// Akcia na uloženie
function json_newsletter_save() {
var self = this;
// self === controller
// self.body obsahuje validné data podľa schémy a táto property je obalená
// internými funkciami schémy.
// self.body.$async() metóda je zo schém a urobí to, že začne za sebou volať
// všetky vymenované operácie v poradí, v akom sú zadané.
// Ak nejaká operácia skončí chybou, tak framework ukončí ďalšie volania
// a nebude pokračovať ďalej + vyvolá ihneď callback() s chybami.
// self.body.$async(callback, [return_index])
// v prípade, že nie je uvedený return_index, tak ako výsledok v callbacku
// bude pole výsledkov z každej operácie v poradí, v akom boli volané. Čiže
// nižšie uvedený kód vráti výsledok z metódy $save(), pretože return_index
// je "1", keby bol "0" tak vráti výsledok z workflowu "check".
self.body.$async(self.callback(), 1).$workflow('check').$save(self).$workflow('email');
}
Ďalšie špecialitky
Error Handling ErrorBuilder
:
Považujem ho za veľmi dôležitú časť frameworku a podporuje skutočne zaujímavé funkcie. Môžete do neho vkladať rôzne chyby (aj viacej naraz), ďalej je prepojený s resources (podpora lokalizácie) a výstup môžete transformovať na čo len chcete (stačí zaregistrovať transformáciu a dokonca nami vytvorenú transformáciu môžete nastaviť ako predvolenú default
). Chyby sa ukladajú do Array
, pričom každá chyba musí mať svoj identifikátor. Zároveň pri automatickom validovaný v schémach ErrorBuilder
obsahuje aj cestu k property
, poprípade Array index
, takže krásne viete zistiť pôvod chyby aj v rôznych vnorených schémách alebo v poli.
error.push('fieldname1', 'error message or error object');
error.push('fieldname2', 'error message or error object');
error.push('fieldname3', 'error message or error object');
error.push('fieldname4', 'error message or error object');
// Napr. rozšírené použitie ErrorBuildera v schéme:
if (model.age < 18)
error.push('age', 'Nemáte ešte 18 rokov');
if (!model.terms)
error.push('terms', 'Nesúhlasili ste s podmienkami internetového servera.');
if (error.hasError())
return callback();
V predvolenom režime je výstup ErrorBuilder
veľmi jednoduchý a framework vždy vracia pole:
[{
"name": "email",
"error": "Your <b>e-mail address</b> @ is not valid.",
"path": "Support.email"
}, {
"name": "firstname",
"error": "The field <b>First name</b> is required.",
"path": "Support.firstname"
}, {
"name": "lastname",
"error": "The field <b>Last name</b> is required.",
"path": "Support.lastname"
}, {
"name": "body",
"error": "The field <b>Issue</b> is required.",
"path": "Support.body"
}]
Na client-side sa dá veľmi jednoducho overiť či data obsahujú chybu:
// jComponent AJAX call:
AJAX('GET /api/support/', function(response) {
if (response instanceof Array) {
console.log('Server vrátil chybu!');
return;
}
});
Následne podľa vlastností v poli name
a path
viete krásne označiť vo formulári, ktorých prvkov sa chyba týka. Moja filozofia prepojenia JSONovo server-side
a client-side
je v 2 podmienkach:
- chyby zo servera sú vždy vracané ako
Array
(viď vyššie uvedený kód)
- opak pola, čiže
Object
, String
, Number
, Boolean
je považovaný za požadovanú response
Rozšírenie schémy:
Rozšíriť schému (narážam na modulárnosť) je veľmi jednoduché a možete to urobiť na hociktorom mieste v aplikácii:
GETSCHEMA('Newsletter').make(function(schema) {
schema.addWorkflow('whatever', function(error, model, options, callback) {
callback();
});
schema.addHook('whatever', function(error, model, options, callback) {
callback();
});
});
Auto trim stringov:
Toto bol jeden skvelý nápad pridať do schém. V prípade, že Vám hodnody v string
vlastnostiach obsahujú na začiatku a na konci biele znaky \n
, \r
, \s
, \t
- tak ich framework automaticky odstráni. Môžem Vám z úprimného srdca povedať, že od vtedy ako je toto implementované, mám čisté databázy (hodnotovo). Hodnoty v schéme sú automaticky trimované (by default) a toto správanie sa dá vypnúť cez property schema.trim = false
.
Vnorené schémy:
Ďalšou skvelou funkciou sú vnorené schémy, pomocou ktorých môžete mať prepojené rôzne schémy/modely medzi sebou. Ako príklad uvediem objednávku a jej adresy:
NEWSCHEMA('Address').make(function(schema) {
schema.define('street', 'String(50)', true);
schema.define('city', 'Capitalize(30)', true);
schema.define('zip', 'Zip', true);
// ...
});
NEWSCHEMA('Order').make(function(schema) {
schema.define('billingaddress', 'Address');
schema.define('postaladdress', 'Address');
// ...
});
Polia - Array:
Aj Array
môžete zaevidovať ako dátový typ:
// Pole string.toLowerCase() s maximálnou dĺžkou 25 znakov
schema.define('tags', '[Lower(25)]');
schema.define('emails', '[Email]');
// Pole čísiel a je povinné, takže pole musí mať viac prvkov ako 0
schema.define('numbers', '[Number]', true);
// Pole ďalších Total.js schém
schema.define('addresses', '[Address]');
Asynchrónne operácie:
Podporujú toho oveľa viacej ako som popísal. Dynamicky viete upraviť volania, návratové hodnoty a dokonca každá inštancia schémy obsahuje skrytý objekt na zapisovanie ďalších potrebných (temporary
) údajov. Tieto údaje sú jednotné skrz všetky asynchrónne operácie.
model.$repository('my-custom-value', 34030);
console.log(model.$repository('my-custom-value));
Validácie:
Total.js sa snaží ušetriť vývojárom kopec roboty a preto aj väčšina validácií je implementovaná v jadre. V prípade, že chcete vytvoriť validáciu na nejakú property
v schéme tak budete postupovať takto:
schema.setValidate(function(name, value, path, schema, model) {
switch (name) {
case 'cardnumber':
return value.length === 17;
case 'cardtype':
return value.match(/VISA|MASTERCARD|MAESTRO/) ? true : false;
}
});
Predvolené hodnoty:
Schémy podporujú aj metódu na vygenerovanie predvolených hodnôt. Takže v prípade, že sa generuje nová schéma tak je volaný nižšie uvedený delegát.
schema.setDefault(function(name) {
switch (name) {
case 'terms':
case 'newsletter':
return true;
case 'email':
return '@';
}
});
Meníme hodnoty ešte pred validáciou:
V prípade, že pred validáciou potrebujete upraviť ešte nejaké prijaté hodnoty v schéme, tak je možné využiť metódu podľa nižšie uvedeného kódu:
schema.setPrepare(function(name, value) {
switch (name) {
case 'cardtype':
// e.g. visa => VISA
return value.toUpperCase();
}
});
Schéma bez vlastností/fieldov? No problem
Sú prípady, kedy nepotrebujete definovať žiadne dátové typy / vlastnosti, proste potrebujete nejaký objekt, ktorý má nejaké metódy a v tomto prípade je k dispozícii workflows
, transformations
, operations
, atď., príklad:
NEWSCHEMA('Empty').make(function(schema) {
schema.addWorkflow('send', function(error, model, options, callback) {
// model bude vždy prázdny objekt typu SchemaInstance {}
callback();
});
});
Vytvorenie objektu podľa schémy:
Nižšie uvedený kód vytvorý objekt podľa schémy s predvolenými hodnotami a zabalenými internými funkciami. Tento kód je možné volať v celom frameworku - v *.js
súboroch.
var Newsletter = GETSCHEMA('Newsletter');
var obj = Newsletter.create();
obj.email = 'petersirka@gmail.com';
obj.$save();
Ako skúsiť schémy?
Nemusíte vytvárať projekt na to, aby ste skúsili schémy. Stačí vytvoriť jednoduchý .js
script a odkázať sa na require('total.js')
.
require('total.js');
var Newsletter = NEWSCHEMA('Newsletter');
Newsletter.make(function(schema) {
// definícia
schema.define('email', 'Email');
schema.setSave(function(error, model, options, callback) {
console.log('model (ako SchemaInstance)', model);
console.log('čistý objekt napr. na serializáciu', model.$clean());
console.log('options', options);
console.log('saved');
callback(SUCCESS(true));
});
});
var obj = Newsletter.create();
obj.email = 'petersirka@gmail.com';
obj.$save({ custom: 'options' }, function(err, response) {
console.log('---> RESPONSE', err, response);
});
Je toho ešte veľmi veľa čo obsahujú schémy, ale pre rýchly začiatok a predstavu to bude hádam postačovať. Pre vytvorenie schém je možné použiť aplikáciu AppDesigner.
Schémy v projektoch
Nižšie uvedené projekty sú postavené na schémach, takže ak chcete vidieť reálne projekty - tak doporučujem naštudovať tieto zdrojové kódy na GitHube: