diff --git a/.gitignore b/.gitignore
index e37e371..68d5051 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,5 @@
-js/
node_modules/
-coverage/
+./coverage/
.DS_Store
.baseDir.ts
npm-debug.log
diff --git a/README.md b/README.md
index 67ced68..7559c6b 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
## sdkzer ##
-[](https://htmlpreview.github.io/?https://github.com/howerest/sdkzer/blob/master/dist/tests_report.html) 
+[
](https://htmlpreview.github.io/?https://github.com/howerest/sdkzer/blob/master/dist/tests_report.html) [
](https://htmlpreview.github.io/?https://github.com/howerest/sdkzer/blob/master/dist/tests_report.html)
[http://howerest.com/labs/sdkzer](http://howerest.com/labs/sdkzer)
diff --git a/dist/coverage/badges.svg b/dist/coverage/badges.svg
new file mode 100644
index 0000000..4354a36
--- /dev/null
+++ b/dist/coverage/badges.svg
@@ -0,0 +1,20 @@
+
\ No newline at end of file
diff --git a/dist/coverage/coverage-summary.json b/dist/coverage/coverage-summary.json
new file mode 100644
index 0000000..4c603d7
--- /dev/null
+++ b/dist/coverage/coverage-summary.json
@@ -0,0 +1,4 @@
+{"total": {"lines":{"total":171,"covered":171,"skipped":0,"pct":100},"statements":{"total":179,"covered":179,"skipped":0,"pct":100},"functions":{"total":41,"covered":34,"skipped":0,"pct":82.92},"branches":{"total":84,"covered":70,"skipped":0,"pct":83.33},"branchesTrue":{"total":0,"covered":0,"skipped":0,"pct":"Unknown"}}
+,"/home/me/shared_MAIN/sdkzer/src/howerest.sdkzer.ts": {"lines":{"total":157,"covered":157,"skipped":0,"pct":100},"functions":{"total":37,"covered":30,"skipped":0,"pct":81.08},"statements":{"total":164,"covered":164,"skipped":0,"pct":100},"branches":{"total":83,"covered":69,"skipped":0,"pct":83.13}}
+,"/home/me/shared_MAIN/sdkzer/src/validation_rule.ts": {"lines":{"total":14,"covered":14,"skipped":0,"pct":100},"functions":{"total":4,"covered":4,"skipped":0,"pct":100},"statements":{"total":15,"covered":15,"skipped":0,"pct":100},"branches":{"total":1,"covered":1,"skipped":0,"pct":100}}
+}
diff --git a/dist/coverage/lcov-report/base.css b/dist/coverage/lcov-report/base.css
new file mode 100644
index 0000000..f418035
--- /dev/null
+++ b/dist/coverage/lcov-report/base.css
@@ -0,0 +1,224 @@
+body, html {
+ margin:0; padding: 0;
+ height: 100%;
+}
+body {
+ font-family: Helvetica Neue, Helvetica, Arial;
+ font-size: 14px;
+ color:#333;
+}
+.small { font-size: 12px; }
+*, *:after, *:before {
+ -webkit-box-sizing:border-box;
+ -moz-box-sizing:border-box;
+ box-sizing:border-box;
+ }
+h1 { font-size: 20px; margin: 0;}
+h2 { font-size: 14px; }
+pre {
+ font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace;
+ margin: 0;
+ padding: 0;
+ -moz-tab-size: 2;
+ -o-tab-size: 2;
+ tab-size: 2;
+}
+a { color:#0074D9; text-decoration:none; }
+a:hover { text-decoration:underline; }
+.strong { font-weight: bold; }
+.space-top1 { padding: 10px 0 0 0; }
+.pad2y { padding: 20px 0; }
+.pad1y { padding: 10px 0; }
+.pad2x { padding: 0 20px; }
+.pad2 { padding: 20px; }
+.pad1 { padding: 10px; }
+.space-left2 { padding-left:55px; }
+.space-right2 { padding-right:20px; }
+.center { text-align:center; }
+.clearfix { display:block; }
+.clearfix:after {
+ content:'';
+ display:block;
+ height:0;
+ clear:both;
+ visibility:hidden;
+ }
+.fl { float: left; }
+@media only screen and (max-width:640px) {
+ .col3 { width:100%; max-width:100%; }
+ .hide-mobile { display:none!important; }
+}
+
+.quiet {
+ color: #7f7f7f;
+ color: rgba(0,0,0,0.5);
+}
+.quiet a { opacity: 0.7; }
+
+.fraction {
+ font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
+ font-size: 10px;
+ color: #555;
+ background: #E8E8E8;
+ padding: 4px 5px;
+ border-radius: 3px;
+ vertical-align: middle;
+}
+
+div.path a:link, div.path a:visited { color: #333; }
+table.coverage {
+ border-collapse: collapse;
+ margin: 10px 0 0 0;
+ padding: 0;
+}
+
+table.coverage td {
+ margin: 0;
+ padding: 0;
+ vertical-align: top;
+}
+table.coverage td.line-count {
+ text-align: right;
+ padding: 0 5px 0 20px;
+}
+table.coverage td.line-coverage {
+ text-align: right;
+ padding-right: 10px;
+ min-width:20px;
+}
+
+table.coverage td span.cline-any {
+ display: inline-block;
+ padding: 0 5px;
+ width: 100%;
+}
+.missing-if-branch {
+ display: inline-block;
+ margin-right: 5px;
+ border-radius: 3px;
+ position: relative;
+ padding: 0 4px;
+ background: #333;
+ color: yellow;
+}
+
+.skip-if-branch {
+ display: none;
+ margin-right: 10px;
+ position: relative;
+ padding: 0 4px;
+ background: #ccc;
+ color: white;
+}
+.missing-if-branch .typ, .skip-if-branch .typ {
+ color: inherit !important;
+}
+.coverage-summary {
+ border-collapse: collapse;
+ width: 100%;
+}
+.coverage-summary tr { border-bottom: 1px solid #bbb; }
+.keyline-all { border: 1px solid #ddd; }
+.coverage-summary td, .coverage-summary th { padding: 10px; }
+.coverage-summary tbody { border: 1px solid #bbb; }
+.coverage-summary td { border-right: 1px solid #bbb; }
+.coverage-summary td:last-child { border-right: none; }
+.coverage-summary th {
+ text-align: left;
+ font-weight: normal;
+ white-space: nowrap;
+}
+.coverage-summary th.file { border-right: none !important; }
+.coverage-summary th.pct { }
+.coverage-summary th.pic,
+.coverage-summary th.abs,
+.coverage-summary td.pct,
+.coverage-summary td.abs { text-align: right; }
+.coverage-summary td.file { white-space: nowrap; }
+.coverage-summary td.pic { min-width: 120px !important; }
+.coverage-summary tfoot td { }
+
+.coverage-summary .sorter {
+ height: 10px;
+ width: 7px;
+ display: inline-block;
+ margin-left: 0.5em;
+ background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent;
+}
+.coverage-summary .sorted .sorter {
+ background-position: 0 -20px;
+}
+.coverage-summary .sorted-desc .sorter {
+ background-position: 0 -10px;
+}
+.status-line { height: 10px; }
+/* yellow */
+.cbranch-no { background: yellow !important; color: #111; }
+/* dark red */
+.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 }
+.low .chart { border:1px solid #C21F39 }
+.highlighted,
+.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{
+ background: #C21F39 !important;
+}
+/* medium red */
+.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE }
+/* light red */
+.low, .cline-no { background:#FCE1E5 }
+/* light green */
+.high, .cline-yes { background:rgb(230,245,208) }
+/* medium green */
+.cstat-yes { background:rgb(161,215,106) }
+/* dark green */
+.status-line.high, .high .cover-fill { background:rgb(77,146,33) }
+.high .chart { border:1px solid rgb(77,146,33) }
+/* dark yellow (gold) */
+.status-line.medium, .medium .cover-fill { background: #f9cd0b; }
+.medium .chart { border:1px solid #f9cd0b; }
+/* light yellow */
+.medium { background: #fff4c2; }
+
+.cstat-skip { background: #ddd; color: #111; }
+.fstat-skip { background: #ddd; color: #111 !important; }
+.cbranch-skip { background: #ddd !important; color: #111; }
+
+span.cline-neutral { background: #eaeaea; }
+
+.coverage-summary td.empty {
+ opacity: .5;
+ padding-top: 4px;
+ padding-bottom: 4px;
+ line-height: 1;
+ color: #888;
+}
+
+.cover-fill, .cover-empty {
+ display:inline-block;
+ height: 12px;
+}
+.chart {
+ line-height: 0;
+}
+.cover-empty {
+ background: white;
+}
+.cover-full {
+ border-right: none !important;
+}
+pre.prettyprint {
+ border: none !important;
+ padding: 0 !important;
+ margin: 0 !important;
+}
+.com { color: #999 !important; }
+.ignore-none { color: #999; font-weight: normal; }
+
+.wrapper {
+ min-height: 100%;
+ height: auto !important;
+ height: 100%;
+ margin: 0 auto -48px;
+}
+.footer, .push {
+ height: 48px;
+}
diff --git a/dist/coverage/lcov-report/block-navigation.js b/dist/coverage/lcov-report/block-navigation.js
new file mode 100644
index 0000000..cc12130
--- /dev/null
+++ b/dist/coverage/lcov-report/block-navigation.js
@@ -0,0 +1,87 @@
+/* eslint-disable */
+var jumpToCode = (function init() {
+ // Classes of code we would like to highlight in the file view
+ var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no'];
+
+ // Elements to highlight in the file listing view
+ var fileListingElements = ['td.pct.low'];
+
+ // We don't want to select elements that are direct descendants of another match
+ var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > `
+
+ // Selecter that finds elements on the page to which we can jump
+ var selector =
+ fileListingElements.join(', ') +
+ ', ' +
+ notSelector +
+ missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b`
+
+ // The NodeList of matching elements
+ var missingCoverageElements = document.querySelectorAll(selector);
+
+ var currentIndex;
+
+ function toggleClass(index) {
+ missingCoverageElements
+ .item(currentIndex)
+ .classList.remove('highlighted');
+ missingCoverageElements.item(index).classList.add('highlighted');
+ }
+
+ function makeCurrent(index) {
+ toggleClass(index);
+ currentIndex = index;
+ missingCoverageElements.item(index).scrollIntoView({
+ behavior: 'smooth',
+ block: 'center',
+ inline: 'center'
+ });
+ }
+
+ function goToPrevious() {
+ var nextIndex = 0;
+ if (typeof currentIndex !== 'number' || currentIndex === 0) {
+ nextIndex = missingCoverageElements.length - 1;
+ } else if (missingCoverageElements.length > 1) {
+ nextIndex = currentIndex - 1;
+ }
+
+ makeCurrent(nextIndex);
+ }
+
+ function goToNext() {
+ var nextIndex = 0;
+
+ if (
+ typeof currentIndex === 'number' &&
+ currentIndex < missingCoverageElements.length - 1
+ ) {
+ nextIndex = currentIndex + 1;
+ }
+
+ makeCurrent(nextIndex);
+ }
+
+ return function jump(event) {
+ if (
+ document.getElementById('fileSearch') === document.activeElement &&
+ document.activeElement != null
+ ) {
+ // if we're currently focused on the search input, we don't want to navigate
+ return;
+ }
+
+ switch (event.which) {
+ case 78: // n
+ case 74: // j
+ goToNext();
+ break;
+ case 66: // b
+ case 75: // k
+ case 80: // p
+ goToPrevious();
+ break;
+ }
+ };
+})();
+window.addEventListener('keydown', jumpToCode);
diff --git a/dist/coverage/lcov-report/favicon.png b/dist/coverage/lcov-report/favicon.png
new file mode 100644
index 0000000..c1525b8
Binary files /dev/null and b/dist/coverage/lcov-report/favicon.png differ
diff --git a/dist/coverage/lcov-report/howerest.sdkzer.ts.html b/dist/coverage/lcov-report/howerest.sdkzer.ts.html
new file mode 100644
index 0000000..275170f
--- /dev/null
+++ b/dist/coverage/lcov-report/howerest.sdkzer.ts.html
@@ -0,0 +1,1858 @@
+
+
+
+
+
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 | + + + + + + + + + + + + + + + + + + +2x + + + + +67x +67x +67x + + +2x +2x + + + + + + + + +67x +67x + +67x + +67x + +53x +53x + + + + + + + + + + +1x + + + + + + + +68x +68x +68x +94x + + + + + + + + + + +51x + + + + + + + + +2x +2x +3x +1x + + +1x + + + + + + + + +4x + +4x + + +4x +6x + +4x +1x +1x + + +1x + +3x + + + + + + + + + + + + + + + + + + + + + + + +10x + +3x +3x +3x +7x + +6x +6x +6x +2x + +6x + + + +1x + + + + + + + + + + + + + + +1x + + + + + + + + + +1x + + + + + + + + + +2x + + + + + + + +2x + + + + + + + +2x + +2x +2x +1x + + + +1x + + + + + + + +6x +6x +6x + + +6x +16x +39x +4x +4x + + + + +6x + + + + + + + +3x +3x +7x +6x + + + +3x + + + + + + + +1x + + + + + + + +9x + + +9x +8x + +8x + + + + + + + +8x +1x + + + + + +8x +8x + + + + + +6x + +6x +6x + + + +6x + +6x +6x + + +2x +2x + + + + + + + + + + +19x + + + + + + + + +3x + + + + + + + +1x + + + + + + + +2x + + + + + + + + + + +3x + +2x +2x + +1x +1x + + +3x + + + + + + + +4x + + +4x + + +4x +2x + + + + + + + + + +2x + + + + + + + + +4x +4x + + + + +3x + +2x + +3x +3x + +1x + + + + + + + + + + +2x + + + + + + + +2x +2x + + + + + +1x + + + + + + + + + + + + +4x + + +4x +4x + + + + + + +4x +1x + + + + + +4x +4x + + + +3x +3x +9x +9x +9x + +3x + +1x + + + +4x + + + + + + + + + +4x + + + + +4x +4x + + + + + + +4x +1x + + + + + +4x +4x + + + +3x +3x +3x + +1x + + +4x + + + + +9x + +9x +1x +1x +1x +1x +2x +2x +2x + + +9x + + + + + + + + + + + + + + + + + + + + + + +2x +2x +2x +2x + + +2x +2x + + +2x +2x + + +2x +2x +2x + + + | /* ========================================================================= + + howerest 2023 - <hola@davidvalin.com> | www.howerest.com + ___________________ + Apache 2.0 Licensed + + Implements a standarized & friendly API to deal with RESTful http + resources that implement endpoints to perform the CRUD operations. + + 1. Define a resource by extending Sdkzer class + 2. Define a "baseEndpoint()" function for your class + 3. Start consuming your resource + +=========================================================================== */ + +export interface SdkzerParams { + id: any +} + +export class Sdkzer<T extends SdkzerParams> { + + public attrs:T; + public pAttrs:T; + protected validationRules:object; + public invalidMessages:object = {}; + public syncing:boolean = false; + public lastResponse:Response|null = null; + + // Configuration + private static DEFAULT_HTTP_HEADERS:THttpHeaders = {}; + private static PARENTS_FETCH_STRATEGY:string = 'none'; + + /** + * Creates an instance of a model entity with an API to communicate with + * a resource (http RESTful resource) + * @param {object} attrs The initial attributes for the resource. + * Those attributes are in force to defaults() + */ + public constructor(attrs:T = {} as T) { + this.attrs = { id: null } as T; + this.pAttrs = { id: null } as T; + + this.setDefaults(); + + for (let attrKey in attrs) { + // Object initialization parameters are in force to default parameters + this.attrs[attrKey] = attrs[attrKey]; + this.pAttrs[attrKey] = attrs[attrKey]; + } + } + + + /** + * Configures Sdkzer constants that determine the behaviour of Sdkzer in all + * classes that extend from Sdkzer in the current scope. + * @param {ISdkzerConfigOptions} options The configuration options + */ + public static configure(options:ISdkzerConfigOptions) : void { + Sdkzer.DEFAULT_HTTP_HEADERS = options.defaultHttpHeaders || {}; + } + + + /** + * Sets the defaults() values in the instance attributes + */ + public setDefaults() : void { + if (this.defaults()) { + let defaults = this.defaults(); + for (let attrKey in defaults) { + this.attrs[attrKey] = defaults[attrKey]; + } + } + } + + + /** + * Retrieves the defaults for the entity. Override it using your default + * attributes if you need any + */ + public defaults() : object { + return {}; + } + + + /** + * Checks wether an entity is a valid entity. + * It doesn't perform validation (check validate()) + */ + public isValid() : boolean { + const attrs = Object.keys(this.invalidMessages); + for(const attrName of attrs) { + if (this.invalidMessages[attrName] && this.invalidMessages[attrName].length > 0) { + return false; + } + } + return true; + } + + + /** + * Checks wether an entity is a valid entity + */ + public validate() : void { + // Reset previous invalid messages from previous validations + this.invalidMessages = {}; + let toValidateAttr, validationRule; + const toValidateAttrs = Object.keys(this.validationRules || {}); + + // Validate attribute's ValidationRules + for(toValidateAttr of toValidateAttrs) { + for(validationRule of this.validationRules[toValidateAttr]) { + // When the ValidationRule is invalid... + if (!validationRule.isValid(this.pAttrs[toValidateAttr], this.attrs[toValidateAttr])) { + if (!this.invalidMessages[toValidateAttr]) { + this.invalidMessages[toValidateAttr] = []; + } + // Collect the invalid message from the ValidationRules for that field + this.invalidMessages[toValidateAttr].push(validationRule.invalidMessage); + } else { + this.invalidMessages[toValidateAttr] = []; + } + } + } + } + + + /** + * This method can do 3 different things: + * + * - 1) Reads all attributes. When called as instance.attr() + * - 2) Read one attribute. When called as instance.attr('name') + * - 3) Set one attribute. When called as instance.attr('name', 'Bruce Lee') + * + * It's recommended to use this method instead of accessing to attr attribute + * directly. This allows you to execute logic before and after setting or + * reading attributes. Also, instead of creating 100 setters and getters, + * we use a single attr() method + * + * @param attrName The attribute name that we want to read or set + * @param value The attribute value that we want to set for "attrName" + */ + public attr(attrName?: string, value?: any) : string | number | boolean | object { + // Setting an attribute? + if (attrName !== undefined && value !== undefined) { + // TODO: Add before&after-callback + let attrKeys = attrName.split('.'); + let attrKeyName = ''; + eval("this.attrs['"+attrKeys.join("']['")+"'] = " + (typeof(value) === 'string' ? "'"+value+"'" : value)); + } else if (attrName !== undefined && value === undefined) { + // Reading an attribute? + let attrKeys = attrName.split('.'); + let attrValue = this.attrs[attrName.split('.')[0]]; + for (let i = 1; i < attrKeys.length; i++) { + attrValue = attrValue[attrKeys[i]]; + } + return attrValue; + } else { + // Reading all attributes? + // TODO: Add before&after-callbacks + return this.attrs || {}; + } + } + + + /** + * Retrieves the base resource url. Override it using your base endpoint + * for your resource. + * + * NOTE: You need to define a baseEndpoint method in your entities + * in order to be able to sync with a backend endpoint + * A base endpoint for a RESTful endpoint look like: + * return "https://www.an-api.com/v1/users" + */ + public baseEndpoint() : string { + return null; + } + + + /** + * Retrieves the resource url + * NOTE: This method will become the interface to connect using different + * http patterns + */ + public resourceEndpoint() : string { + return ''; + } + + + /** + * Checks if the record is not saved in the origin. An record will be + * consiered new when it has an "id" attribute set to null and it lacks of + * a "lastResponse" attribute value + */ + public isNew() : boolean { + return ((this.attrs.id !== null) ? false : true); + } + + + /** + * Checks if the record has changed since the last save + */ + public hasChanged() : boolean { + return (this.changedAttrs().length > 0 ? true : false); + } + + + /** + * Checks if an attribute has changed from the origin + */ + public hasAttrChanged(attrName:string) : boolean { + let i, changedAttrs = this.changedAttrs(); + + for (i = 0; i < changedAttrs.length; i++) { + if (changedAttrs[i] === attrName) { + return true; + } + } + + return false; + } + + + /** + * Retrieves the name of the changed attributes since the last save + */ + public changedAttrs() : Array<string> { + let changedAttrs = [], + currAttrs = Object.keys(this.attrs), + prevAttrs = Object.keys(this.pAttrs), + i, i2; + + for (i=0; i <= currAttrs.length; i++) { + for (i2=0; i2 <= prevAttrs.length; i2++) { + if (currAttrs[i] !== null && currAttrs[i] === prevAttrs[i2] && this.attrs[currAttrs[i]] !== this.pAttrs[prevAttrs[i2]]) { + changedAttrs.push(currAttrs[i]); + break; + } + } + } + + return changedAttrs; + } + + + /** + * Retrieves the previous attributes + */ + public prevAttrs() : T { + let previousAttrs = {} as T; + for (let attrKey in this.attrs) { + if (this.pAttrs[attrKey] !== this.attrs[attrKey]) { + previousAttrs[attrKey] = (this.pAttrs[attrKey] ? this.pAttrs[attrKey] : null); + } + } + + return previousAttrs; + } + + + /** + * Retrieves the previous value prior to last save for a specific attribute + */ + public prevValue(attrName:string) : any { + return this.prevAttrs()[attrName]; + } + + + /** + * Fetches the newest attributes from the origin. + */ + public async fetch(httpQuery?:IQuery, camelize: boolean = true) : Promise<Response> { + let _this = this, + promise; + + if (this.attrs.id) { + this.syncing = true; + + let query:IQuery = { + url: `${this.baseEndpoint()}/${this.attrs.id}`, + method: 'GET', + headers: Sdkzer.DEFAULT_HTTP_HEADERS || {}, + qsParams: {}, + data: {} + } + + if (typeof(httpQuery) !== 'undefined') { + query = { + ...query, + ...httpQuery + }; + } + + try { + let response = await fetch(`${query.url}${query.qsParams ? qsToString(query.qsParams): ''}`, { + method: query.method, + headers: query.headers, + body: query.data.toString() + }); + // Success + _this.syncing = false; + // TODO: Keep lastResponse + let parsedData = _this.parseRecord(JSON.parse(await response.json())); + if (camelize) { + // parsedData = util.Camel.camelize(parsedData); + } + // Keep track of previous attributes + _this.pAttrs = parsedData; + // Assign the parsed attributes + _this.attrs = parsedData; + return response; + } catch(e) { + // Fail + _this.syncing = false; + return Promise.reject(false); + } + } + } + + + /** + * Parses a single resource record from an incoming HttpResponse data + * NOTE: The idea is to return the parsed record data only + */ + public parseRecord(data:object, prefix?:string) : T { + return prefix ? data[prefix] : data; + } + + + /** + * Parses a collection of resource records from an incoming HttpResponse data + * NOTE: The idea is to return the parsed collection of records data only + */ + public static parseCollection(data:Array<object>, prefix?:string) : Array<object> { + return prefix ? data[prefix] : data; + } + + + /** + * Transforms the local attributes to be processed by the origin in JSON format + */ + public toOriginJSON() : object { + return this.attrs; + } + + + /** + * Transforms the local attributes to be processed by the origin in XML format + */ + public toOriginXML() : string { + return ''; + } + + + /** + * Transforms the local attributes to be processed by the origin in a specific format + * @param format The format to transform into + */ + public toOrigin(format:string = 'json') : object|string { + let snapshot; + + switch(format) { + case 'json': + snapshot = this.toOriginJSON(); + break; + case 'xml': + snapshot = this.toOriginXML(); + break; + } + + return snapshot; + } + + + /** + * Persists the local state into the origin + */ + public async save(httpHeaders:THttpHeaders = {}) : Promise<Response> { + let _this = this, + query:IQuery, + request, + httpMethod:THttpMethod = (this.attr('id') == null ? 'POST' : 'PUT'); + + // New record in the origin? + if (httpMethod === 'POST') { + query = { + method: httpMethod, + url: this.baseEndpoint(), + headers: Sdkzer.DEFAULT_HTTP_HEADERS || {}, + qsParams: {}, + data: this.toOriginJSON() + }; + + // Existing record in the origin? + } else { + query = { + method: httpMethod, + url: `${this.baseEndpoint()}/${this.attrs.id}${query && query.qsParams ? qsToString(query.qsParams): ''}`, + headers: Sdkzer.DEFAULT_HTTP_HEADERS || {}, + qsParams: {}, + data: this.toOriginJSON() + }; + } + + try { + const response = await fetch(query.url, { + method: query.method, + headers: query.headers, + body: query.data.toString() + }); + if (httpMethod === 'POST') { + // Append id to attributes + _this.attrs.id = (await response.json())['id']; + } + _this.lastResponse = response; + return response; + } catch(e) { + return Promise.reject(false); + } + } + + + /** + * Destroys the current record in the origin + */ + public async destroy() : Promise<Response> { + let query:IQuery; + + query = { + method: 'DELETE', + url: `${this.baseEndpoint()}/${this.attrs.id}`, + headers: Sdkzer.DEFAULT_HTTP_HEADERS || {}, + qsParams: {}, + data: {} + }; + + try { + return await fetch(query.url, { + method: query.method, + headers: query.headers, + body: query.data.toString() + }) + } catch(e) { + return Promise.reject(false); + } + } + + + /** + * Retrieves a collection of records from the origin + * @param httpQuery An optional query to be merged with the default one + */ + public static async fetchIndex(httpQuery?:IQuery) : Promise<Array<any>> { + let query:IQuery, + request, + instancesPromise, + instances = [], + instance; + + instancesPromise = new Promise(async (resolve, reject) => { + query = { + method: 'GET', + url: `${new this().baseEndpoint()}${httpQuery && httpQuery.qsParams ? qsToString(httpQuery.qsParams): ''}`, + headers: Sdkzer.DEFAULT_HTTP_HEADERS || {}, + qsParams: {} + }; + + if (typeof(httpQuery) !== 'undefined') { + query = { + ...query, + ...httpQuery + }; + } + + try { + const response = await fetch(query.url, { + method: query.method, + headers: query.headers + }); + const collectionList = this.parseCollection(JSON.parse(await response.json())); + for (let i in collectionList) { + instance = new this(); + instance.attrs = instance.pAttrs = instance.parseRecord(collectionList[i]); + instances.push(instance); + } + resolve(instances); + } catch(e) { + reject(e); + } + }); + + return instancesPromise; + } + + + /** + * Retrieves a single record from the origin + * @param id The record id that we want to fetch by + * @param httpQuery Use a HttpQuery instance to override the default query + */ + public static fetchOne(id: number|string, httpQuery?:IQuery) : Promise<any> { + let model = new this(), + query:IQuery, + instancePromise, + instance; + + instancePromise = new Promise(async (resolve, reject) => { + query = { + method: 'GET', + url: `${model.baseEndpoint()}/${id}${httpQuery && httpQuery.qsParams ? qsToString(httpQuery.qsParams) : ''}`, + headers: Sdkzer.DEFAULT_HTTP_HEADERS || {}, + qsParams: {} + }; + + if (typeof(httpQuery) !== 'undefined') { + query = { + ...query, + ...httpQuery + } + } + + try { + const response = await fetch(query.url, { + method: query.method, + headers: query.headers + }) + instance = new this(); + instance.attrs = instance.pAttrs = instance.parseRecord(JSON.parse(await response.json())); + resolve(instance); + } catch(e) { + reject(e); + } + }); + return instancePromise; + } +} + +function qsToString(qs:IQueryString) { + let qsPart = ''; + // Add query string to url + if (Object.keys(qs).length > 0) { + qsPart += '?'; + let i=0; + let keys = Object.keys(qs); + for(let key of keys) { + if (i > 0) { qsPart += '&'; } + qsPart += `${key}=${qs[key]}`; + i++; + } + } + return qsPart; +} + +export type THttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' +export type THttpHeaders = { + [key:string] : string +} +export interface IQueryString { + [key:string] : string | number +} + +export interface IQuery { + url?: string, + method?: THttpMethod, + headers?: THttpHeaders, + qsParams?: IQueryString, + data?: {} +} + +export interface ISdkzerConfigOptions { + defaultHttpHeaders: THttpHeaders +} + +export { ValidationRule, IValidationRule } from "./validation_rule"; +export { RequiredValidator } from "./validation_rules/required_validator" +export { + RegExpValidator, + IParams as IRegExpValidatorParams +} from "./validation_rules/reg_exp_validator" +export { + NumberValidator, + IParams as INumberValidatorParams +} from "./validation_rules/number_validator" +export { + LengthValidator, + IParams as ILengthValidatorParams +} from "./validation_rules/length_validator" +export { EmailValidator } from "./validation_rules/email_validator" +export { + AllowedValueSwitchValidator, + IParams as IAllowedValueSwitchValidatorParams +} from "./validation_rules/allowed_value_switch_validator" + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
---|---|---|---|---|---|---|---|---|---|
howerest.sdkzer.ts | +
+
+ |
+ 100% | +164/164 | +83.13% | +69/83 | +81.08% | +30/37 | +100% | +157/157 | +
validation_rule.ts | +
+
+ |
+ 100% | +15/15 | +100% | +1/1 | +100% | +4/4 | +100% | +14/14 | +