From 3362073a35172e6d868c965f54952d988d2fd135 Mon Sep 17 00:00:00 2001 From: Devin Major Date: Thu, 23 Apr 2026 09:05:02 -0400 Subject: [PATCH] copy of the @agn repository from as of April 23 2026 --- .svn/entries | 1 + .svn/format | 1 + ...77a4684fc765135f30597a4883c14f458.svn-base | 2 + ...c2eee44cc222000eede04b59e26254963.svn-base | 547 +++++++++++++++++ ...a4f3b985d0c6ead49a69464d1dd72cb7e.svn-base | 9 + ...859b217d0c6c6c5c6940475f3aaea6af5.svn-base | 25 + ...c80105560e1f453c9750101385d778584.svn-base | 25 + ...b5fa21ba9627a2a5b23aac2028f55fe04.svn-base | 5 + ...8faf3dd104e8d78a0525f6aa79695e62f.svn-base | 138 +++++ ...8b8a1bafdb3d2a37e700ef36de0cdeac8.svn-base | 46 ++ ...a20dfcff5038841358904cf55f186be28.svn-base | 20 + ...2641ddf6cfbed1cd4bb04f04e4c667848.svn-base | 4 + ...6c0d1c983d655c4749115639b00ae8b94.svn-base | 158 +++++ ...f2185fb3e059e77851c50a91f67d37865.svn-base | 18 + ...3a408e347197ce33481baa9617ea77dcc.svn-base | 3 + ...a505e464dbe2b071fb9dbc747bcd8a831.svn-base | 12 + ...91ede54b89a09d1a77d9d352b23440bc1.svn-base | 87 +++ ...1896aad596884a89d462fa5c9d8735062.svn-base | 10 + ...cbace36bc66cf9d20b79bf8fdf64aeb9a.svn-base | 93 +++ ...f5f7e08a0bf8cc50d7d366109ed77976c.svn-base | 84 +++ ...e97c77ccec58cb50acbd60bd3a8888317.svn-base | 19 + ...809c8c4c2c5d7524faf6827144cfb06e9.svn-base | 88 +++ ...f9e2d816ff040c0c41839082d4caef692.svn-base | 74 +++ ...576134b8c59ea2e8dda57152ddeb13774.svn-base | 14 + ...a886adc2f890186ccf09c4b04fba63789.svn-base | 32 + ...6363a6f58ee06a5ca34c12c7f4d8dabf8.svn-base | 5 + ...18a78540d7c133b2c4ce836611ff74e88.svn-base | 12 + .svn/wc.db | Bin 0 -> 131072 bytes .svn/wc.db-journal | 0 binary-package-parser/agn-parser.js | 74 +++ binary-package-parser/index.js | 2 + binary-package-parser/package-lock.json | 25 + binary-package-parser/package.json | 18 + binary-package-parser/rap-parser.js | 93 +++ bit-util/index.js | 32 + bit-util/package-lock.json | 5 + bit-util/package.json | 12 + device-tcp-socket/agnav-device.js | 138 +++++ device-tcp-socket/index.js | 3 + device-tcp-socket/package.json | 14 + device-tcp-socket/rap-device.js | 158 +++++ device-tcp-socket/tcp-device.js | 88 +++ error-handler/api_err_handler.js | 46 ++ error-handler/error_handler.js | 87 +++ error-handler/index.js | 9 + error-handler/package-lock.json | 548 ++++++++++++++++++ error-handler/package.json | 20 + error-handler/utils/common.js | 10 + mailer/index.js | 4 + mailer/mailer.js | 84 +++ mailer/package-lock.json | 66 +++ mailer/package.json | 19 + number-util/index.js | 25 + number-util/package-lock.json | 5 + number-util/package.json | 12 + 55 files changed, 3129 insertions(+) create mode 100644 .svn/entries create mode 100644 .svn/format create mode 100644 .svn/pristine/02/02f467277a4684fc765135f30597a4883c14f458.svn-base create mode 100644 .svn/pristine/0d/0dd9b2ac2eee44cc222000eede04b59e26254963.svn-base create mode 100644 .svn/pristine/19/192434ba4f3b985d0c6ead49a69464d1dd72cb7e.svn-base create mode 100644 .svn/pristine/1c/1c8509c859b217d0c6c6c5c6940475f3aaea6af5.svn-base create mode 100644 .svn/pristine/1e/1ebb9cec80105560e1f453c9750101385d778584.svn-base create mode 100644 .svn/pristine/1f/1f37f37b5fa21ba9627a2a5b23aac2028f55fe04.svn-base create mode 100644 .svn/pristine/1f/1fa94838faf3dd104e8d78a0525f6aa79695e62f.svn-base create mode 100644 .svn/pristine/2d/2de35d48b8a1bafdb3d2a37e700ef36de0cdeac8.svn-base create mode 100644 .svn/pristine/2d/2df823ca20dfcff5038841358904cf55f186be28.svn-base create mode 100644 .svn/pristine/44/44e1fac2641ddf6cfbed1cd4bb04f04e4c667848.svn-base create mode 100644 .svn/pristine/58/58481746c0d1c983d655c4749115639b00ae8b94.svn-base create mode 100644 .svn/pristine/6a/6af69bbf2185fb3e059e77851c50a91f67d37865.svn-base create mode 100644 .svn/pristine/6b/6b44a153a408e347197ce33481baa9617ea77dcc.svn-base create mode 100644 .svn/pristine/70/70ac0b9a505e464dbe2b071fb9dbc747bcd8a831.svn-base create mode 100644 .svn/pristine/7b/7bef30291ede54b89a09d1a77d9d352b23440bc1.svn-base create mode 100644 .svn/pristine/8a/8a271fb1896aad596884a89d462fa5c9d8735062.svn-base create mode 100644 .svn/pristine/95/9504a2fcbace36bc66cf9d20b79bf8fdf64aeb9a.svn-base create mode 100644 .svn/pristine/a1/a160718f5f7e08a0bf8cc50d7d366109ed77976c.svn-base create mode 100644 .svn/pristine/b5/b56e38fe97c77ccec58cb50acbd60bd3a8888317.svn-base create mode 100644 .svn/pristine/bd/bd21ca0809c8c4c2c5d7524faf6827144cfb06e9.svn-base create mode 100644 .svn/pristine/cd/cde2c25f9e2d816ff040c0c41839082d4caef692.svn-base create mode 100644 .svn/pristine/d8/d8e06b6576134b8c59ea2e8dda57152ddeb13774.svn-base create mode 100644 .svn/pristine/dc/dcf362ca886adc2f890186ccf09c4b04fba63789.svn-base create mode 100644 .svn/pristine/f1/f10beb76363a6f58ee06a5ca34c12c7f4d8dabf8.svn-base create mode 100644 .svn/pristine/fb/fb5795118a78540d7c133b2c4ce836611ff74e88.svn-base create mode 100644 .svn/wc.db create mode 100644 .svn/wc.db-journal create mode 100644 binary-package-parser/agn-parser.js create mode 100644 binary-package-parser/index.js create mode 100644 binary-package-parser/package-lock.json create mode 100644 binary-package-parser/package.json create mode 100644 binary-package-parser/rap-parser.js create mode 100644 bit-util/index.js create mode 100644 bit-util/package-lock.json create mode 100644 bit-util/package.json create mode 100644 device-tcp-socket/agnav-device.js create mode 100644 device-tcp-socket/index.js create mode 100644 device-tcp-socket/package.json create mode 100644 device-tcp-socket/rap-device.js create mode 100644 device-tcp-socket/tcp-device.js create mode 100644 error-handler/api_err_handler.js create mode 100644 error-handler/error_handler.js create mode 100644 error-handler/index.js create mode 100644 error-handler/package-lock.json create mode 100644 error-handler/package.json create mode 100644 error-handler/utils/common.js create mode 100644 mailer/index.js create mode 100644 mailer/mailer.js create mode 100644 mailer/package-lock.json create mode 100644 mailer/package.json create mode 100644 number-util/index.js create mode 100644 number-util/package-lock.json create mode 100644 number-util/package.json diff --git a/.svn/entries b/.svn/entries new file mode 100644 index 0000000..48082f7 --- /dev/null +++ b/.svn/entries @@ -0,0 +1 @@ +12 diff --git a/.svn/format b/.svn/format new file mode 100644 index 0000000..48082f7 --- /dev/null +++ b/.svn/format @@ -0,0 +1 @@ +12 diff --git a/.svn/pristine/02/02f467277a4684fc765135f30597a4883c14f458.svn-base b/.svn/pristine/02/02f467277a4684fc765135f30597a4883c14f458.svn-base new file mode 100644 index 0000000..41d6788 --- /dev/null +++ b/.svn/pristine/02/02f467277a4684fc765135f30597a4883c14f458.svn-base @@ -0,0 +1,2 @@ +module.exports.RAPParser = require('./rap-parser'); +module.exports.AgNavParser = require('./agn-parser'); \ No newline at end of file diff --git a/.svn/pristine/0d/0dd9b2ac2eee44cc222000eede04b59e26254963.svn-base b/.svn/pristine/0d/0dd9b2ac2eee44cc222000eede04b59e26254963.svn-base new file mode 100644 index 0000000..66f35ec --- /dev/null +++ b/.svn/pristine/0d/0dd9b2ac2eee44cc222000eede04b59e26254963.svn-base @@ -0,0 +1,547 @@ +{ + "name": "error-handler", + "version": "2.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "error-handler", + "version": "2.0.0", + "license": "Proprietary", + "dependencies": { + "debug": "^4.4.0", + "key-file-storage": "^2.3.3", + "mailer": "file:../mailer" + } + }, + "../mailer": { + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "debug": "^4.4.0", + "nodemailer": "^6.10.0" + } + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/is-valid-path": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@types/is-valid-path/-/is-valid-path-0.1.2.tgz", + "integrity": "sha512-BsZtkfiPpnzDWFjSZanYllttVW7/46ayPZkcHBCSFBkBqIO9rWrflUvEmT2tF///hnPLwBJU3TJPzbBxpUEqCg==" + }, + "node_modules/@types/node": { + "version": "22.14.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", + "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha512-a1dBeB19NXsf/E0+FHqkagizel/LQw2DjSQpvQrj3zT+jYPpaUCryPnrQajXKFLCMuf4I6FhRpaGtw4lPrG6Eg==", + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-invalid-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-invalid-path/-/is-invalid-path-0.1.0.tgz", + "integrity": "sha512-aZMG0T3F34mTg4eTdszcGXx54oiZ4NtHSft3hWNJMGJXUUqdIj3cOZuHcU0nCWWcY3jd7yRe/3AEm3vSNTpBGQ==", + "dependencies": { + "is-glob": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-valid-path": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-valid-path/-/is-valid-path-0.1.1.tgz", + "integrity": "sha512-+kwPrVDu9Ms03L90Qaml+79+6DZHqHyRoANI6IsZJ/g8frhnfchDOBCa0RbQ6/kdHt5CS5OeIEyrYznNuVN+8A==", + "dependencies": { + "is-invalid-path": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/key-file-storage": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/key-file-storage/-/key-file-storage-2.3.3.tgz", + "integrity": "sha512-bqFrbE0ifIq5ahrquGewh5nMub8fdH/nXKMg9CJHdCdD5W/6KQea483Qyss0q53tCOC64CN43DQo9TSvohlbKA==", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "@types/is-valid-path": "^0.1.0", + "fs-extra": "^10.0.0", + "is-valid-path": "^0.1.1", + "recur-fs": "^2.2.4" + } + }, + "node_modules/mailer": { + "resolved": "../mailer", + "link": true + }, + "node_modules/minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-NyXjqu1IwcqH6nv5vmMtaG3iw7kdV3g6MwlUBZkc3Vn5b5AMIWYKfptvzipoyFfhlfOgBQ9zoTxQMravF1QTnw==", + "dependencies": { + "brace-expansion": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha512-miQKw5Hv4NS1Psg2517mV4e4dYNaO3++hjAvLOAzKqZ61rH8NS1SK+vbfBWZ5PY/Me/bEWhUwqMghEW5Fb9T7Q==" + }, + "node_modules/mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha512-SknJC52obPfGQPnjIkXbmA6+5H15E+fR+E4iR2oQ3zzCLbd7/ONua69R/Gw7AgkTLsRG+r5fzksYwWe1AgTyWA==", + "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", + "dependencies": { + "minimist": "0.0.8" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/recur-fs": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/recur-fs/-/recur-fs-2.2.4.tgz", + "integrity": "sha512-VofuavbR3PNwHAs2uxn2HNcyyXKIUBayVtdhdElJ8qqSmcr2TY71THQjAoF+PT1xEeUnEi57DWT2/+YSRCvr3w==", + "dependencies": { + "minimatch": "3.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.5.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.4.tgz", + "integrity": "sha512-Lw7SHMjssciQb/rRz7JyPIy9+bbUshEucPoLRvWqy09vC5zQixl8Uet+Zl+SROBB/JMWHJRdCk1qdxNWHNMvlQ==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dependencies": { + "glob": "^7.0.5" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + } + }, + "dependencies": { + "@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "requires": { + "@types/node": "*" + } + }, + "@types/is-valid-path": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@types/is-valid-path/-/is-valid-path-0.1.2.tgz", + "integrity": "sha512-BsZtkfiPpnzDWFjSZanYllttVW7/46ayPZkcHBCSFBkBqIO9rWrflUvEmT2tF///hnPLwBJU3TJPzbBxpUEqCg==" + }, + "@types/node": { + "version": "22.14.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", + "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", + "requires": { + "undici-types": "~6.21.0" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "requires": { + "ms": "^2.1.3" + } + }, + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "dependencies": { + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww==" + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha512-a1dBeB19NXsf/E0+FHqkagizel/LQw2DjSQpvQrj3zT+jYPpaUCryPnrQajXKFLCMuf4I6FhRpaGtw4lPrG6Eg==", + "requires": { + "is-extglob": "^1.0.0" + } + }, + "is-invalid-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-invalid-path/-/is-invalid-path-0.1.0.tgz", + "integrity": "sha512-aZMG0T3F34mTg4eTdszcGXx54oiZ4NtHSft3hWNJMGJXUUqdIj3cOZuHcU0nCWWcY3jd7yRe/3AEm3vSNTpBGQ==", + "requires": { + "is-glob": "^2.0.0" + } + }, + "is-valid-path": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-valid-path/-/is-valid-path-0.1.1.tgz", + "integrity": "sha512-+kwPrVDu9Ms03L90Qaml+79+6DZHqHyRoANI6IsZJ/g8frhnfchDOBCa0RbQ6/kdHt5CS5OeIEyrYznNuVN+8A==", + "requires": { + "is-invalid-path": "^0.1.0" + } + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "key-file-storage": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/key-file-storage/-/key-file-storage-2.3.3.tgz", + "integrity": "sha512-bqFrbE0ifIq5ahrquGewh5nMub8fdH/nXKMg9CJHdCdD5W/6KQea483Qyss0q53tCOC64CN43DQo9TSvohlbKA==", + "requires": { + "@types/fs-extra": "^9.0.11", + "@types/is-valid-path": "^0.1.0", + "fs-extra": "^10.0.0", + "is-valid-path": "^0.1.1", + "recur-fs": "^2.2.4" + } + }, + "mailer": { + "version": "file:../mailer", + "requires": { + "debug": "^4.4.0", + "nodemailer": "^6.10.0" + } + }, + "minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-NyXjqu1IwcqH6nv5vmMtaG3iw7kdV3g6MwlUBZkc3Vn5b5AMIWYKfptvzipoyFfhlfOgBQ9zoTxQMravF1QTnw==", + "requires": { + "brace-expansion": "^1.0.0" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha512-miQKw5Hv4NS1Psg2517mV4e4dYNaO3++hjAvLOAzKqZ61rH8NS1SK+vbfBWZ5PY/Me/bEWhUwqMghEW5Fb9T7Q==" + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha512-SknJC52obPfGQPnjIkXbmA6+5H15E+fR+E4iR2oQ3zzCLbd7/ONua69R/Gw7AgkTLsRG+r5fzksYwWe1AgTyWA==", + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" + }, + "recur-fs": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/recur-fs/-/recur-fs-2.2.4.tgz", + "integrity": "sha512-VofuavbR3PNwHAs2uxn2HNcyyXKIUBayVtdhdElJ8qqSmcr2TY71THQjAoF+PT1xEeUnEi57DWT2/+YSRCvr3w==", + "requires": { + "minimatch": "3.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.5.4" + } + }, + "rimraf": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.4.tgz", + "integrity": "sha512-Lw7SHMjssciQb/rRz7JyPIy9+bbUshEucPoLRvWqy09vC5zQixl8Uet+Zl+SROBB/JMWHJRdCk1qdxNWHNMvlQ==", + "requires": { + "glob": "^7.0.5" + } + }, + "undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" + }, + "universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + } + } +} diff --git a/.svn/pristine/19/192434ba4f3b985d0c6ead49a69464d1dd72cb7e.svn-base b/.svn/pristine/19/192434ba4f3b985d0c6ead49a69464d1dd72cb7e.svn-base new file mode 100644 index 0000000..63db4dd --- /dev/null +++ b/.svn/pristine/19/192434ba4f3b985d0c6ead49a69464d1dd72cb7e.svn-base @@ -0,0 +1,9 @@ +const common = require('./utils/common'); +const apiErrorHandler = require('./api_err_handler'); +const errorHandler = require('./error_handler'); + +module.exports = { + common: common, + apiErrorHandler: apiErrorHandler, + errorHandler: errorHandler +}; diff --git a/.svn/pristine/1c/1c8509c859b217d0c6c6c5c6940475f3aaea6af5.svn-base b/.svn/pristine/1c/1c8509c859b217d0c6c6c5c6940475f3aaea6af5.svn-base new file mode 100644 index 0000000..0af9105 --- /dev/null +++ b/.svn/pristine/1c/1c8509c859b217d0c6c6c5c6940475f3aaea6af5.svn-base @@ -0,0 +1,25 @@ +'use strict'; + +/** + * Trim the decimal to maximum numbers of digits + * @param {*} value the decimal value + * @param {*} digits maximum number of decimal digits + */ +function fixedTo(value, digits) { + if (!value) return value; + var re = new RegExp('(-?\\d+\\.\\d{' + digits + '})(\\d)'), + m = value.toString().match(re); + return m ? parseFloat(m[1]) : value; +}; + +function padZero(num, size) { + let s = num + ''; + while (s.length < size) { + s = '0' + s; + } + return s; +} + +module.exports = { + fixedTo, padZero +} diff --git a/.svn/pristine/1e/1ebb9cec80105560e1f453c9750101385d778584.svn-base b/.svn/pristine/1e/1ebb9cec80105560e1f453c9750101385d778584.svn-base new file mode 100644 index 0000000..487b0c1 --- /dev/null +++ b/.svn/pristine/1e/1ebb9cec80105560e1f453c9750101385d778584.svn-base @@ -0,0 +1,25 @@ +{ + "name": "binary-package-parser", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "binary-package-parser", + "version": "1.0.0", + "license": "Proprietary", + "dependencies": { + "bit-util": "file:../bit-util" + } + }, + "../bit-util": { + "name": "bit-util", + "version": "1.0.0", + "license": "Proprietary" + }, + "node_modules/bit-util": { + "resolved": "../bit-util", + "link": true + } + } +} diff --git a/.svn/pristine/1f/1f37f37b5fa21ba9627a2a5b23aac2028f55fe04.svn-base b/.svn/pristine/1f/1f37f37b5fa21ba9627a2a5b23aac2028f55fe04.svn-base new file mode 100644 index 0000000..06fb866 --- /dev/null +++ b/.svn/pristine/1f/1f37f37b5fa21ba9627a2a5b23aac2028f55fe04.svn-base @@ -0,0 +1,5 @@ +{ + "name": "bit-util", + "version": "1.0.0", + "lockfileVersion": 1 +} diff --git a/.svn/pristine/1f/1fa94838faf3dd104e8d78a0525f6aa79695e62f.svn-base b/.svn/pristine/1f/1fa94838faf3dd104e8d78a0525f6aa79695e62f.svn-base new file mode 100644 index 0000000..77373aa --- /dev/null +++ b/.svn/pristine/1f/1fa94838faf3dd104e8d78a0525f6aa79695e62f.svn-base @@ -0,0 +1,138 @@ +'use strict'; + +const TCPDevice = require('./tcp-device'); +const numberUtil = require('number-util'); + +const READ_0 = 0; +const READ_1 = 1; +const READ_BLOCKED = 99; + +const START_MARK = Buffer.from('FBFB', 'hex'); +const END_MARK = Buffer.from('0D0A', 'hex'); +const PKG_SIZE = 40; + +class AgNavTCPDevice extends TCPDevice { + + constructor(socket, ops) { + super(socket, ops); + + this.id = null; + this.msgReceived = 0; + + this._startPos = -1; + this._endPos = -1; + this._firstPart = null; + this._secondPart = null; + this._cmdBuf = null; + + this._readState = READ_0; + } + + /** + * _onData is triggered by the "readable" event on the underlying TCP socket. + * It is called each time there is new data * received. It is responsible for reading data from the socket and + * performing the appropriate action given the current read state. + */ + _onData() { + try { + // Loop while there was still data to process on the socket's buffer. + // This will stop when we don't have enough data or encountering a back pressure issue; + let readMore = true; + do { + switch (this._readState) { + case READ_0: + readMore = this._processRead_0(); + break; + case READ_1: + readMore = this._processRead_1(); + break; + case READ_BLOCKED: + readMore = false; + break; + default: + throw new Error('Unknown read state'); + } + } while (readMore); + } catch (err) { + console.log(err); + // Terminate on failures as we won't be able to recovery since data was corrupted and we won't + // be able to any more data without additional errors. + this.destroy(err); + } + } + + _processRead_0() { + this._firstPart = this._socket.read(PKG_SIZE / 2); + if (!this._firstPart) + return false; + + this._startPos = this._firstPart.indexOf(START_MARK); + if (this._startPos === -1) { + this._guardInvalidTries(); + + this._readState = READ_0; + } else { + this._readState = READ_1; + } + return true; + } + + _processRead_1() { + // Read payload data by the length + this._secondPart = this._socket.read(this._startPos + (PKG_SIZE / 2)); + if (!this._secondPart) { + return false; + } + + this._endPos = this._secondPart.indexOf(END_MARK); + + if (this._endPos === -1) { + this._guardInvalidTries(); + + // Could not find the match for end mark => skip the first part to find the next start mark + this.unshift(this._secondPart); + } else { + if ((this._firstPart.length - this._startPos) + this._endPos + END_MARK.length <= PKG_SIZE) { + + this._invalidTry = 0; + this.msgReceived++; + this._cmdBuf = Buffer.alloc(PKG_SIZE - START_MARK.length); + + this._firstPart.copy(this._cmdBuf, 0, this._startPos + START_MARK.length); + this._secondPart.copy(this._cmdBuf, this._firstPart.length - this._startPos - START_MARK.length, 0, this._endPos + END_MARK.length); + + if (!this.id) { + this.id = numberUtil.padZero(this._cmdBuf.readInt32LE(1), 10); + } + + // Check for the case of package size < PKG_SIZE + if (this._secondPart.length > this._endPos + END_MARK.length) { + // return the rest to the internal buffer for the next read + this.unshift(this._secondPart.slice(this._endPos + END_MARK.length)); + } + + // Push the message onto the read buffer for the consumer to read. We are mindful of slow reads by the consumer + // and will respect backpressure signals. + const pushOk = this.push(this._cmdBuf); + if (pushOk) { + this._readState = READ_0; + return true; + } else { + console.log('socket read is blocked'); + this._readState = READ_BLOCKED; + return false; + } + } else { + this._invalidTry++; + if (this._invalidTry > this.ops.maxInvalidPackages) + throw new Error('Reached maximum invalid read try'); + } + } + + this._readState = READ_0; + return true; + } + +} + +module.exports = AgNavTCPDevice; diff --git a/.svn/pristine/2d/2de35d48b8a1bafdb3d2a37e700ef36de0cdeac8.svn-base b/.svn/pristine/2d/2de35d48b8a1bafdb3d2a37e700ef36de0cdeac8.svn-base new file mode 100644 index 0000000..601c9f1 --- /dev/null +++ b/.svn/pristine/2d/2de35d48b8a1bafdb3d2a37e700ef36de0cdeac8.svn-base @@ -0,0 +1,46 @@ +'use strict'; + +/** + * Error handling functions for the REST server applications. + * Notes: This module provides reusable functions to create and handle API errors. + * It includes functions to create application-specific error objects, throw errors, and handle response errors. + * + * However, the error handling logic is just the simplest form of error management. + * @author: Trung Hoang + * @version: 1.0.1 + * @date: 2023-10-01 + * @license: proprietary + * @description: This module provides functions to create and handle API errors. + */ + +const { isAppError } = require('./utils/common'); + +function appError(tag) { + if (!tag) + return { error: { '.tag': 'unknown_error', text: 'Unknown Error' } } + else + return { error: { '.tag': tag } } + // return { error: { '.tag': tag, text: msg ? msg : '' } } +} + +function throwError(tag) { + throw new Error(tag); +} + +function handleResErr(res, err, code = 409) { + if (!res || typeof res !== "object") return; + + const _erCode = code; + const _err = isAppError(err); + if (_err) { + res.status(_erCode || 409).send(appError(_err)); + } else { + res.status(_erCode || 500).send(appError('unknown_error')); + if (err) console.log(err.stack); + } +} + + +module.exports = { + throwError, appError, handleResErr +} diff --git a/.svn/pristine/2d/2df823ca20dfcff5038841358904cf55f186be28.svn-base b/.svn/pristine/2d/2df823ca20dfcff5038841358904cf55f186be28.svn-base new file mode 100644 index 0000000..80f59c3 --- /dev/null +++ b/.svn/pristine/2d/2df823ca20dfcff5038841358904cf55f186be28.svn-base @@ -0,0 +1,20 @@ +{ + "name": "error-handler", + "version": "2.0.0", + "description": "A reusable error handling utility module for Node.js applications.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "Error Handling", + "Utility" + ], + "author": "Trung Hoang", + "license": "Proprietary", + "dependencies": { + "debug": "^4.4.0", + "key-file-storage": "^2.3.3", + "mailer": "file:../mailer" + } +} diff --git a/.svn/pristine/44/44e1fac2641ddf6cfbed1cd4bb04f04e4c667848.svn-base b/.svn/pristine/44/44e1fac2641ddf6cfbed1cd4bb04f04e4c667848.svn-base new file mode 100644 index 0000000..b10eb8a --- /dev/null +++ b/.svn/pristine/44/44e1fac2641ddf6cfbed1cd4bb04f04e4c667848.svn-base @@ -0,0 +1,4 @@ + +module.exports = { + default: require('./mailer.js') +} \ No newline at end of file diff --git a/.svn/pristine/58/58481746c0d1c983d655c4749115639b00ae8b94.svn-base b/.svn/pristine/58/58481746c0d1c983d655c4749115639b00ae8b94.svn-base new file mode 100644 index 0000000..49fb36e --- /dev/null +++ b/.svn/pristine/58/58481746c0d1c983d655c4749115639b00ae8b94.svn-base @@ -0,0 +1,158 @@ +'use strict'; + +const TCPDevice = require('./tcp-device'); + +const READ_0 = 0; +const READ_ID = 1; +const READ_LEN = 2; +const READ_BODY = 3; +const READ_BLOCKED = 99; + +const RESP_ID_HEADER = 0xE1; +const RESP_CMD_HEADER = 0xF1; + +class RAPTCPDevice extends TCPDevice { + + constructor(socket, ops) { + super(socket, ops); + + this.id = null; + this.msgReceived = 0; + + this._idPart = null; + this._lenPart = null; + this._dataLength = 0; + this._bodyPart = null; + this._cmdBuf = null; + + this._readState = READ_0; + } + + /** + * _onData is triggered by the "readable" event on the underlying TCP socket. + * It is called each time there is new data * received. It is responsible for reading data from the socket and + * performing the appropriate action given the current read state. + */ + _onData() { + try { + // Loop while there was still data to process on the socket's buffer. + // This will stop when we don't have enough data or encountering a back pressure issue; + let readMore = true; + do { + switch (this._readState) { + case READ_0: + // this._idPart = null; + // this._lenPart = null; + // this._dataLength = 0; + // this._bodyPart = null; + readMore = this._processRead_0(); + break; + case READ_ID: + readMore = this._processReadId(); + break; + case READ_LEN: + readMore = this._processReadLen(); + break; + case READ_BODY: + readMore = this._processReadBody(); + break; + case READ_BLOCKED: + readMore = false; + break; + default: + throw new Error('Unknown read state'); + } + } while (readMore); + } catch (err) { + // Terminate on failures as we won't be able to recovery since data was corrupted and we won't + // be able to any more data without additional errors. + this.destroy(err); + } + } + + _processRead_0() { + this._firstByte = this._socket.read(1); + if (!this._firstByte) + return false; + + if (this._firstByte[0] === RESP_ID_HEADER) { + this._readState = READ_ID; + } else if (this._firstByte[0] === RESP_CMD_HEADER) { + this._readState = READ_LEN; + } else { + this._readState = READ_0; + this._guardInvalidTries(); + } + + return true; + } + + _getDeviceId(buf) { + buf[7] = 0; // byte 8th is not used in the case ID is IMEI + // Convert this buf to 64 integer number + return buf.readBigUInt64LE(0).toString(); + } + + _processReadId() { + // Try to read the Report Data Length from the 3rd byte of the GPS Response message + // If we cannot read the 8 bytes, the attempt to process the message will abort. + // E1 => [8 byte ID] + this._idPart = this._socket.read(8); + if (!this._idPart) + return false; + + if (!this.id) { + // Parse the Device ID (8bytes) for the IMEI number + const id8byte = this._idPart.slice(0); + if (id8byte[7] === 0x01) + this.id = this._getDeviceId(id8byte); + } + + this._readState = READ_0; + return true; + } + + _processReadLen() { + // E1[8 byte ID]F1 => + this._lenPart = this._socket.read(2); + if (!this._lenPart) + return false; + + this._dataLength = this._lenPart[1]; + + this._readState = READ_BODY; + return true; + } + + _processReadBody() { + // Read payload data by the length + this._bodyPart = this._socket.read(this._dataLength); + if (!this._bodyPart) { + // this._socket.unshift(this._lenPart); // ??? needed + return false; + } + + this._invalidTry = 0; + this.msgReceived++; + this._cmdBuf = Buffer.alloc(this._lenPart.length + this._dataLength); + + // Copy the from read + this._lenPart.copy(this._cmdBuf); + this._bodyPart.copy(this._cmdBuf, this._lenPart.length); + // Push the message onto the read buffer for the consumer to read. We are mindful of slow reads by the consumer + // and will respect backpressure signals. + const pushOk = this.push(this._cmdBuf); + if (pushOk) { + this._readState = READ_0; + return true; + } else { + // debug("socket read is blocked"); + console.log('socket read is blocked'); + this._readState = READ_BLOCKED; + return false; + } + } + +} + +module.exports = RAPTCPDevice; diff --git a/.svn/pristine/6a/6af69bbf2185fb3e059e77851c50a91f67d37865.svn-base b/.svn/pristine/6a/6af69bbf2185fb3e059e77851c50a91f67d37865.svn-base new file mode 100644 index 0000000..7b8608d --- /dev/null +++ b/.svn/pristine/6a/6af69bbf2185fb3e059e77851c50a91f67d37865.svn-base @@ -0,0 +1,18 @@ +{ + "name": "binary-package-parser", + "version": "1.0.0", + "description": "Binary protocol package parsers for parsing binary packages into JSON objects", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "RAP", + "AgNav" + ], + "author": "Trung Hoang", + "license": "Proprietary", + "dependencies": { + "bit-util": "file:../bit-util" + } +} diff --git a/.svn/pristine/6b/6b44a153a408e347197ce33481baa9617ea77dcc.svn-base b/.svn/pristine/6b/6b44a153a408e347197ce33481baa9617ea77dcc.svn-base new file mode 100644 index 0000000..a15a119 --- /dev/null +++ b/.svn/pristine/6b/6b44a153a408e347197ce33481baa9617ea77dcc.svn-base @@ -0,0 +1,3 @@ +exports.TCPDevice = require('./tcp-device'); +exports.AgNavTCPDevice = require('./agnav-device'); +exports.RAPTCPDevice = require('./rap-device'); \ No newline at end of file diff --git a/.svn/pristine/70/70ac0b9a505e464dbe2b071fb9dbc747bcd8a831.svn-base b/.svn/pristine/70/70ac0b9a505e464dbe2b071fb9dbc747bcd8a831.svn-base new file mode 100644 index 0000000..c284b0d --- /dev/null +++ b/.svn/pristine/70/70ac0b9a505e464dbe2b071fb9dbc747bcd8a831.svn-base @@ -0,0 +1,12 @@ +{ + "name": "bit-util", + "version": "1.0.0", + "description": "Bit utilities with bit manipulation functions", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "Trung Hoang", + "license": "Proprietary" +} diff --git a/.svn/pristine/7b/7bef30291ede54b89a09d1a77d9d352b23440bc1.svn-base b/.svn/pristine/7b/7bef30291ede54b89a09d1a77d9d352b23440bc1.svn-base new file mode 100644 index 0000000..d7dfc72 --- /dev/null +++ b/.svn/pristine/7b/7bef30291ede54b89a09d1a77d9d352b23440bc1.svn-base @@ -0,0 +1,87 @@ +'use strict'; + +const util = require('util'), + path = require('path'), + mailer = require('mailer').default, + KeyFileStorage = require("key-file-storage").default, + debug = require('debug')('agm:errHelper'); + +let logFileName = path.basename(__filename); + + +/** + * Report important errors to Admin + * @param {*} error Error message in text or the Error object + */ +async function mailErrorToAdmin(error) { + const errorMsg = (error && error.message) ? error.message : error; + const contentOps = { + subject: '[Agm-Errors] Unexpected Errors', + body: error && error.stack ? errorMsg + '\n' + error.stack : errorMsg, + to: process.env.AGM_ADM_EMAIL || '"AgMission Admin" ', + }; + return await mailer.sendTextMail(contentOps); +} + +async function _handleCriticalError(error, message) { + const exitFunc = (err) => { + debug(err); + process.exit(1); + }; + try { + const baseFilename = path.basename(logFileName); + const kfs = KeyFileStorage(path.dirname(logFileName), false); + + let lastReport = await kfs[baseFilename]; + let curDateTime = new Date(); + + if (kfs[baseFilename] && (lastReport = kfs[baseFilename])) { + if (lastReport.when && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.*$/.test(lastReport.when)) { + if (curDateTime - new Date(lastReport.when) < 2 * 60 * 1e3 + && ((error.code && lastReport.code === error.code) || lastReport.message == error.message)) + return exitFunc(error); // Reported the same error two shortly, at least 2 minutes => ignore to avoid flooding with the same emails + } + } + const logObj = { + code: error.code, + message: error.message, + when: curDateTime + }; + await kfs(baseFilename, logObj); + } catch (err) { + debug(err); + } + + try { + await mailErrorToAdmin(message); + } + catch (err) { + debug(err); + } + finally { + return exitFunc(error); + } +} + +/** + * Register the process error handlers + * @param {*} process The process object + * @param {*} fileName The file name to log the error + */ +function registerUnCaughtProcessErrorsHandler(process, fileName) { + if (!process) return; + + if (fileName) logFileName = fileName; + + process + .on('uncaughtException', async (err) => { + await _handleCriticalError(err, util.format('uncaughtException:', err)); + }) + .on('unhandledRejection', async (reason, p) => { + await _handleCriticalError(reason, util.format(reason, 'Unhandled Rejection at Promise', p)); + }); +} + +module.exports = { + mailErrorToAdmin, registerUnCaughtProcessErrorsHandler +} diff --git a/.svn/pristine/8a/8a271fb1896aad596884a89d462fa5c9d8735062.svn-base b/.svn/pristine/8a/8a271fb1896aad596884a89d462fa5c9d8735062.svn-base new file mode 100644 index 0000000..8b665b7 --- /dev/null +++ b/.svn/pristine/8a/8a271fb1896aad596884a89d462fa5c9d8735062.svn-base @@ -0,0 +1,10 @@ +'use restrict'; + +function isAppError(err) { + const _err = err && err.message || err || ''; + return (err && (typeof _err === "string" && _err.includes('_'))) ? _err : ''; +} + +module.exports = { + isAppError +}; \ No newline at end of file diff --git a/.svn/pristine/95/9504a2fcbace36bc66cf9d20b79bf8fdf64aeb9a.svn-base b/.svn/pristine/95/9504a2fcbace36bc66cf9d20b79bf8fdf64aeb9a.svn-base new file mode 100644 index 0000000..8f93d77 --- /dev/null +++ b/.svn/pristine/95/9504a2fcbace36bc66cf9d20b79bf8fdf64aeb9a.svn-base @@ -0,0 +1,93 @@ +'use strict'; + +const assert = require('assert'), + bitUtil = require('bit-util'); + +const GPS_13 = 0x13; + +/** + * RAPParser is responsible for parsing RAP binary records + */ +class RAPParser { + + constructor() { + // Using these local vars to avoid GC for more memory efficency + this._hour = 0; + this._minute = 0; + this._second = 0; + this._year = 0; + this._month = 0; + this._day = 0; + this._byte = 0; + } + + /** + * Parse RAP binary response package + * @param {*} rapPkg The binary package data in Buffer. Structure: , Cmd = 0x13 + * @returns a GPS data object + */ + parse(rapPkg) { + let record; + if (rapPkg && rapPkg[0] >= GPS_13) { + assert.ok(Buffer.isBuffer(rapPkg), 'rapPkg argument must be an instance of Buffer'); // prettier-ignore + + // Byte 19th, if bit 4 is set -> has GPS Fix + if (rapPkg[18] && bitUtil.isBitOn(rapPkg[18], 4)) { + record = {}; + let offset = 2; // Add 2 to skip the data package header to read the data + + record.lat = rapPkg.readInt32LE(offset) * 1e-5; offset += 4; + record.lon = rapPkg.readInt32LE(offset) * 1e-5; offset += 4; + record.speed = rapPkg.readUInt8(offset); offset += 1; + // GPS Direction in 2 Degree Increasements + record.head = rapPkg.readUInt8(offset) << 1; offset += 1; + + this._hour = rapPkg.readUInt8(offset); offset += 1; + this._minute = rapPkg.readUInt8(offset); offset += 1; + this._second = rapPkg.readUInt8(offset); offset += 1; + this._year = 2000 + rapPkg.readUInt8(offset); offset += 1; + this._month = rapPkg.readUInt8(offset); offset += 1; + this._day = rapPkg.readUInt8(offset); offset += 1; + + // Month in JS 0 => 11 + record.gdt = new Date(Date.UTC(this._year, this._month - 1, this._day, this._hour, this._minute, this._second)); + + record.inputs = 0; + + // Cmd 0x13. Byte 19th + this._byte = rapPkg.readUInt8(offset); offset += 1; + + // Cmd 0x13. Byte 19th. Bits 0-3: numbers of GPS setellites + record.sats = bitUtil.extractBits(this._byte, 4, 0); + + // Cmd 0x13. Byte 20th + this._byte = rapPkg.readUInt8(offset); + + // Cmd 0x13. Byte 20th. Bit 3: Inputs+ + if (bitUtil.isBitOn(this._byte, 3)) { + + // Cmd 0x13. Byte 20th. Bit 0: 1 => extra inputs (21-24) + offset += bitUtil.isBitOn(this._byte, 0) ? 4 : 1; + + // Skip reading Odometer data + + // Cmd 0x13. Byte 21th/25th + this._byte = rapPkg.readUInt8(offset); offset += 1; + + if (bitUtil.isBitOn(this._byte, 0)) { + // Cmd 0x13. Byte 22th/25th: Whether Digital input state present + this._byte = rapPkg.readUInt8(offset); offset += 1; + + // Digital inputs (Up to 5, use only input 1 for Spray on/off, 0: spray on and 1 is off) + // Check Input 1 (bit 0) for spray on/off + record.inputs = bitUtil.isBitOn(this._byte, 0) ? 0 : 1; + } + } + + } + } + return record; + } +} + +module.exports = RAPParser; \ No newline at end of file diff --git a/.svn/pristine/a1/a160718f5f7e08a0bf8cc50d7d366109ed77976c.svn-base b/.svn/pristine/a1/a160718f5f7e08a0bf8cc50d7d366109ed77976c.svn-base new file mode 100644 index 0000000..379e55b --- /dev/null +++ b/.svn/pristine/a1/a160718f5f7e08a0bf8cc50d7d366109ed77976c.svn-base @@ -0,0 +1,84 @@ +'use strict'; + +const nodemailer = require('nodemailer'), + debug = require('debug')('app:mailer'); + +const createConfig = (env) => { + const isSecure = env?.SMTP_SECURE || process.env.SMTP_SECURE || false; + const port = env?.SMTP_PORT || process.env.SMTP_PORT || 587; + + const config = { + host: env?.SMTP_HOST || process.env.SMTP_HOST, + port: port, + secure: isSecure, + auth: { + user: env?.SMTP_USR || process.env.SMTP_USR, + pass: env?.SMTP_PWD || process.env.SMTP_PWD, + }, + sender: env?.SMTP_SENDER || process.env.SMTP_SENDER || `"AgNav Inc." ` + }; + + if (!config.host || !config.port || !config.auth.user || !config.auth.pass) { + throw new Error('SMTP configuration is missing required fields (host, port, user, pass).'); + } + return config; +}; + +const createMailOptions = (from, to, subject, body) => ({ + from: from, to: to, subject: subject, text: body +}); + +function createMailer(env) { + const config = createConfig(env); + + const transporter = nodemailer.createTransport(config); + + return { + sendMail: async (to, subject, body) => { + try { + if (!to || !subject || !body) { + throw new Error('Missing required fields: to, subject, or body'); + } + + const mailOptions = createMailOptions(config.sender, to, subject, body); + const info = await transporter.sendMail(mailOptions); + + return { success: true, message: 'Mail sent successfully', messageId: info.messageId }; + } catch (error) { + debug('Error sending mail:', error.message); + return { success: false, error: error.message }; + } + }, + }; +} + +/** + * Send a text email + * @param {Object} options Email options + * @param {string} options.from Sender of the email (Optional) + * @param {string} options.subject Subject of the email + * @param {string} options.to Recipient of the email + * @param {string} options.text Text content of the email + */ +async function sendTextMail(options, env) { + const { from, to, subject, body } = options; + if (!to || !subject || !body) { + throw new Error('Missing required fields: to, subject, or body'); + } + const config = createConfig(env); + + const transporter = nodemailer.createTransport(config); + const mailOptions = createMailOptions(from || config.sender, to, subject, body); + + try { + const info = await transporter.sendMail(mailOptions); + return { success: true, messageId: info.messageId }; + } catch (error) { + debug('Error sending email:', error.message); + return { success: false, error: error.message }; + } +} + +module.exports = { + createMailer, sendTextMail +}; \ No newline at end of file diff --git a/.svn/pristine/b5/b56e38fe97c77ccec58cb50acbd60bd3a8888317.svn-base b/.svn/pristine/b5/b56e38fe97c77ccec58cb50acbd60bd3a8888317.svn-base new file mode 100644 index 0000000..0446c69 --- /dev/null +++ b/.svn/pristine/b5/b56e38fe97c77ccec58cb50acbd60bd3a8888317.svn-base @@ -0,0 +1,19 @@ +{ + "name": "mailer", + "version": "1.0.0", + "description": "Simple reusable mailer utility.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "mailer", + "utility" + ], + "author": "Trung Hoang", + "license": "ISC", + "dependencies": { + "debug": "^4.4.0", + "nodemailer": "^6.10.0" + } +} \ No newline at end of file diff --git a/.svn/pristine/bd/bd21ca0809c8c4c2c5d7524faf6827144cfb06e9.svn-base b/.svn/pristine/bd/bd21ca0809c8c4c2c5d7524faf6827144cfb06e9.svn-base new file mode 100644 index 0000000..67b7e0e --- /dev/null +++ b/.svn/pristine/bd/bd21ca0809c8c4c2c5d7524faf6827144cfb06e9.svn-base @@ -0,0 +1,88 @@ +'use strict'; + +const stream = require('stream'), + Socket = require('net').Socket, + assert = require('assert'); + +/** + * Base TCPDevice class for handling TCP socket from tracking device + * + */ +class TCPDevice extends stream.Duplex { + + constructor(socket, ops) { + super(ops); + + this.ops = Object.assign({}, { + maxInvalidReadTry: 5 + }, ops); + + // Perform type assertions + assert.ok(socket instanceof Socket, 'socket argument must be an instance of Socket'); // prettier-ignore + + this.id = null; + this.msgReceived = 0; + this._invalidTry = 0; + + this._socket = socket; + this._socket.on('close', hadError => this.emit('close', hadError)); + // this._socket.on("connect", () => console.log("Client connected !")); + this._socket.on('drain', () => this.emit('drain')); + this._socket.on('end', () => this.emit('end')); + this._socket.on('error', err => this.emit('error', err)); + this._socket.on('lookup', (e, a, f, h) => this.emit('lookup', e, a, f, h)); + this._socket.on('readable', this._onData.bind(this)); + this._socket.on('timeout', () => this.emit('timeout')); + } + + /** + * Half-closes the socket. It is still possible that the opposite + * side is still sending data. + */ + end() { + this._socket.end(); + return this; + } + + /** + * Destroys the socket and ensures that no more I/O activity happens + * on the socket. When an `err` is included, an 'error' event will + * be emitted and all listeners will receive the error as an + * argument. + * @param err optional error to send + */ + destroy(err) { + this._socket.destroy(err); + return this; + } + + _guardInvalidTries() { + this._invalidTry++; + if (this._invalidTry > this.ops.maxInvalidReadTry) + throw new Error('Reached maximum invalid read try'); + } + + /** + * _onData is triggered by the "readable" event on the underlying TCP socket. + * It is called each time there is new data * received. It is responsible for reading data from the socket and * + * performing the appropriate action given the current read state. + * @virtual Extenders implementation must override this menthod acccording to the detail of the protocol + */ + _onData() { } + + _read() { + + // Trigger a read but wait until the end of the event loop. + // This is necessary when reading in paused mode where + // _read was triggered by stream.read() originating inside + // a "readable" event handler. Attempting to push more data + // synchronously will not trigger another "readable" event. + setImmediate(() => this._onData()); + } + + _write() { } + _final() { } + +} + +module.exports = TCPDevice; \ No newline at end of file diff --git a/.svn/pristine/cd/cde2c25f9e2d816ff040c0c41839082d4caef692.svn-base b/.svn/pristine/cd/cde2c25f9e2d816ff040c0c41839082d4caef692.svn-base new file mode 100644 index 0000000..6ee8fb1 --- /dev/null +++ b/.svn/pristine/cd/cde2c25f9e2d816ff040c0c41839082d4caef692.svn-base @@ -0,0 +1,74 @@ +'use strict'; + +const assert = require('assert'); + +/* Agnav Tracking binary protocol +trk_pos_type = Record {40 bytes} + {Header} + trk_Header1 : byte {0xFB} + trk_Header2 : byte {0xFB} + trk_DataID : byte {0xF7} + trk_ACID : LongInt; {4 bytes signed Integer} + trk_Time : LongInt // Number of seconds since 1970, 1, 1 to current UTC time + trk_Lat : float; {4 byte floating point} + trk_Lon : float; {4 byte floating point} + trk_Alt : integer; {GPS alt, m, 2 byte signed integer} + trk_SprayStat: byte; {0: off, 1: on, unsigned char} + trk_Speed : word; {dm/s, speed * 10 2 bytes signed integer} + trk_Heading : integer; { 0 - 359} + trk_Temp : byte; {C deg, offset by 100, unsigned char} + trk_Humid : byte; {0 - 100%, unsigned char} + trk_AppRate : integer {Application rate L/Ha * 10} + trk_Wspd : integer; {Windspeed * 10 dm/s} + trk_Whdg : integer; {Wind heading, 0-359} + trk_Res : byte; {reserved = 0, unsigned char} + trk_Chksum : word; {Sum of all bytes start from FB} + trk_End : word; {end of record, 0x0D 0x0A} + End; + +** NOTES: +byte : 1-byte unsigned integer +word : 2-byte unsigned int +integer : 2-byte signed int +LongInt : 4-byte signed int +float : 4-byte floating number + */ +class RAPParser { + + /** + * Parse AgNav Tracking binary response package + * @param {*} agnPkg The package data in Buffer. Structure: , Cmd = 0xF7 + * @returns a GPS data object + */ + parse(agnPkg) { + assert.ok(Buffer.isBuffer(agnPkg), 'agnPkg argument must be an instance of Buffer'); // prettier-ignore + assert.ok(agnPkg.length == 38, 'agnPkg must have 40 bytes'); // prettier-ignore + + let record, offset; + + record = {}; + offset = 5; // Skip the first Cmd byte (F7) + 4 bytes Aircraft Id + + record.gdt = new Date(agnPkg.readInt32LE(offset) * 1000); offset += 4; + record.lat = agnPkg.readFloatLE(offset); offset += 4; + record.lon = agnPkg.readFloatLE(offset); offset += 4; + record.alt = agnPkg.readInt16LE(offset); offset += 2; + record.inputs = agnPkg.readUInt8(offset); offset += 1; + + record.speed = agnPkg.readInt16LE(offset); offset += 2; + if (record.speed > 0) + record.speed = record.speed * 0.36; // dm/s to km/h + + record.head = agnPkg.readInt16LE(offset); offset += 2; + record.temp = agnPkg.readUInt8(offset) - 100; offset += 1; + record.humid = agnPkg.readUInt8(offset); offset += 1; + record.appRate = agnPkg.readInt16LE(offset) * 1e-1; offset += 2; + record.windSpd = agnPkg.readInt16LE(offset); offset += 2; + record.windHdg = agnPkg.readInt16LE(offset); offset += 2; + + agnPkg = null; + return record; + } +} + +module.exports = RAPParser; \ No newline at end of file diff --git a/.svn/pristine/d8/d8e06b6576134b8c59ea2e8dda57152ddeb13774.svn-base b/.svn/pristine/d8/d8e06b6576134b8c59ea2e8dda57152ddeb13774.svn-base new file mode 100644 index 0000000..1d81539 --- /dev/null +++ b/.svn/pristine/d8/d8e06b6576134b8c59ea2e8dda57152ddeb13774.svn-base @@ -0,0 +1,14 @@ +{ + "name": "device-tcp-socket", + "version": "1.1.0", + "description": "Device tcp socket implementation", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Trung Hoang ", + "license": "Proprietary", + "dependencies": { + "number-util": "file:../number-util" + } +} diff --git a/.svn/pristine/dc/dcf362ca886adc2f890186ccf09c4b04fba63789.svn-base b/.svn/pristine/dc/dcf362ca886adc2f890186ccf09c4b04fba63789.svn-base new file mode 100644 index 0000000..3e5bb09 --- /dev/null +++ b/.svn/pristine/dc/dcf362ca886adc2f890186ccf09c4b04fba63789.svn-base @@ -0,0 +1,32 @@ +'use strict'; + +/** + * Extract n bits from a position + * @param numVal the number + * @param bits number of bits to extract + * @param from the bit postion right to left to extract. Zero index. + */ +function extractBits(numVal, bits, from) { + let _from = Math.max(0, from); + let _bits = Math.abs(bits); + return (((1 << _bits) - 1) & (numVal >> _from)); +}; + +/** + * Check if k-th bit of a given number is set or not using right shift operator. + * @param {*} n the number value to check + * @param {*} at the bit position. Zero index. + */ +function isBitOn(n, bit) { + let _at = Math.max(0, bit); + return ((n >> _at) & 1); +}; + +function bytesToHex(bytes, delimeter = "") { + return bytes && Array.isArray(bytes) ? Array.from( + bytes, + byte => byte.toString(16).padStart(2, "0").toUpperCase() + ).join(delimeter) : ''; +} + +module.exports = { extractBits, isBitOn, bytesToHex } \ No newline at end of file diff --git a/.svn/pristine/f1/f10beb76363a6f58ee06a5ca34c12c7f4d8dabf8.svn-base b/.svn/pristine/f1/f10beb76363a6f58ee06a5ca34c12c7f4d8dabf8.svn-base new file mode 100644 index 0000000..d9b6003 --- /dev/null +++ b/.svn/pristine/f1/f10beb76363a6f58ee06a5ca34c12c7f4d8dabf8.svn-base @@ -0,0 +1,5 @@ +{ + "name": "number-util", + "version": "1.1.0", + "lockfileVersion": 1 +} diff --git a/.svn/pristine/fb/fb5795118a78540d7c133b2c4ce836611ff74e88.svn-base b/.svn/pristine/fb/fb5795118a78540d7c133b2c4ce836611ff74e88.svn-base new file mode 100644 index 0000000..2b73cb6 --- /dev/null +++ b/.svn/pristine/fb/fb5795118a78540d7c133b2c4ce836611ff74e88.svn-base @@ -0,0 +1,12 @@ +{ + "name": "number-util", + "version": "1.1.0", + "description": "Number utilities", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "Trung Hoang", + "license": "Proprietary" +} diff --git a/.svn/wc.db b/.svn/wc.db new file mode 100644 index 0000000000000000000000000000000000000000..5af37458ba5469b657840287642171795a87dbbe GIT binary patch literal 131072 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 JwAHly{{XI}29^K- literal 0 HcmV?d00001 diff --git a/.svn/wc.db-journal b/.svn/wc.db-journal new file mode 100644 index 0000000..e69de29 diff --git a/binary-package-parser/agn-parser.js b/binary-package-parser/agn-parser.js new file mode 100644 index 0000000..6ee8fb1 --- /dev/null +++ b/binary-package-parser/agn-parser.js @@ -0,0 +1,74 @@ +'use strict'; + +const assert = require('assert'); + +/* Agnav Tracking binary protocol +trk_pos_type = Record {40 bytes} + {Header} + trk_Header1 : byte {0xFB} + trk_Header2 : byte {0xFB} + trk_DataID : byte {0xF7} + trk_ACID : LongInt; {4 bytes signed Integer} + trk_Time : LongInt // Number of seconds since 1970, 1, 1 to current UTC time + trk_Lat : float; {4 byte floating point} + trk_Lon : float; {4 byte floating point} + trk_Alt : integer; {GPS alt, m, 2 byte signed integer} + trk_SprayStat: byte; {0: off, 1: on, unsigned char} + trk_Speed : word; {dm/s, speed * 10 2 bytes signed integer} + trk_Heading : integer; { 0 - 359} + trk_Temp : byte; {C deg, offset by 100, unsigned char} + trk_Humid : byte; {0 - 100%, unsigned char} + trk_AppRate : integer {Application rate L/Ha * 10} + trk_Wspd : integer; {Windspeed * 10 dm/s} + trk_Whdg : integer; {Wind heading, 0-359} + trk_Res : byte; {reserved = 0, unsigned char} + trk_Chksum : word; {Sum of all bytes start from FB} + trk_End : word; {end of record, 0x0D 0x0A} + End; + +** NOTES: +byte : 1-byte unsigned integer +word : 2-byte unsigned int +integer : 2-byte signed int +LongInt : 4-byte signed int +float : 4-byte floating number + */ +class RAPParser { + + /** + * Parse AgNav Tracking binary response package + * @param {*} agnPkg The package data in Buffer. Structure: , Cmd = 0xF7 + * @returns a GPS data object + */ + parse(agnPkg) { + assert.ok(Buffer.isBuffer(agnPkg), 'agnPkg argument must be an instance of Buffer'); // prettier-ignore + assert.ok(agnPkg.length == 38, 'agnPkg must have 40 bytes'); // prettier-ignore + + let record, offset; + + record = {}; + offset = 5; // Skip the first Cmd byte (F7) + 4 bytes Aircraft Id + + record.gdt = new Date(agnPkg.readInt32LE(offset) * 1000); offset += 4; + record.lat = agnPkg.readFloatLE(offset); offset += 4; + record.lon = agnPkg.readFloatLE(offset); offset += 4; + record.alt = agnPkg.readInt16LE(offset); offset += 2; + record.inputs = agnPkg.readUInt8(offset); offset += 1; + + record.speed = agnPkg.readInt16LE(offset); offset += 2; + if (record.speed > 0) + record.speed = record.speed * 0.36; // dm/s to km/h + + record.head = agnPkg.readInt16LE(offset); offset += 2; + record.temp = agnPkg.readUInt8(offset) - 100; offset += 1; + record.humid = agnPkg.readUInt8(offset); offset += 1; + record.appRate = agnPkg.readInt16LE(offset) * 1e-1; offset += 2; + record.windSpd = agnPkg.readInt16LE(offset); offset += 2; + record.windHdg = agnPkg.readInt16LE(offset); offset += 2; + + agnPkg = null; + return record; + } +} + +module.exports = RAPParser; \ No newline at end of file diff --git a/binary-package-parser/index.js b/binary-package-parser/index.js new file mode 100644 index 0000000..41d6788 --- /dev/null +++ b/binary-package-parser/index.js @@ -0,0 +1,2 @@ +module.exports.RAPParser = require('./rap-parser'); +module.exports.AgNavParser = require('./agn-parser'); \ No newline at end of file diff --git a/binary-package-parser/package-lock.json b/binary-package-parser/package-lock.json new file mode 100644 index 0000000..487b0c1 --- /dev/null +++ b/binary-package-parser/package-lock.json @@ -0,0 +1,25 @@ +{ + "name": "binary-package-parser", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "binary-package-parser", + "version": "1.0.0", + "license": "Proprietary", + "dependencies": { + "bit-util": "file:../bit-util" + } + }, + "../bit-util": { + "name": "bit-util", + "version": "1.0.0", + "license": "Proprietary" + }, + "node_modules/bit-util": { + "resolved": "../bit-util", + "link": true + } + } +} diff --git a/binary-package-parser/package.json b/binary-package-parser/package.json new file mode 100644 index 0000000..7b8608d --- /dev/null +++ b/binary-package-parser/package.json @@ -0,0 +1,18 @@ +{ + "name": "binary-package-parser", + "version": "1.0.0", + "description": "Binary protocol package parsers for parsing binary packages into JSON objects", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "RAP", + "AgNav" + ], + "author": "Trung Hoang", + "license": "Proprietary", + "dependencies": { + "bit-util": "file:../bit-util" + } +} diff --git a/binary-package-parser/rap-parser.js b/binary-package-parser/rap-parser.js new file mode 100644 index 0000000..8f93d77 --- /dev/null +++ b/binary-package-parser/rap-parser.js @@ -0,0 +1,93 @@ +'use strict'; + +const assert = require('assert'), + bitUtil = require('bit-util'); + +const GPS_13 = 0x13; + +/** + * RAPParser is responsible for parsing RAP binary records + */ +class RAPParser { + + constructor() { + // Using these local vars to avoid GC for more memory efficency + this._hour = 0; + this._minute = 0; + this._second = 0; + this._year = 0; + this._month = 0; + this._day = 0; + this._byte = 0; + } + + /** + * Parse RAP binary response package + * @param {*} rapPkg The binary package data in Buffer. Structure: , Cmd = 0x13 + * @returns a GPS data object + */ + parse(rapPkg) { + let record; + if (rapPkg && rapPkg[0] >= GPS_13) { + assert.ok(Buffer.isBuffer(rapPkg), 'rapPkg argument must be an instance of Buffer'); // prettier-ignore + + // Byte 19th, if bit 4 is set -> has GPS Fix + if (rapPkg[18] && bitUtil.isBitOn(rapPkg[18], 4)) { + record = {}; + let offset = 2; // Add 2 to skip the data package header to read the data + + record.lat = rapPkg.readInt32LE(offset) * 1e-5; offset += 4; + record.lon = rapPkg.readInt32LE(offset) * 1e-5; offset += 4; + record.speed = rapPkg.readUInt8(offset); offset += 1; + // GPS Direction in 2 Degree Increasements + record.head = rapPkg.readUInt8(offset) << 1; offset += 1; + + this._hour = rapPkg.readUInt8(offset); offset += 1; + this._minute = rapPkg.readUInt8(offset); offset += 1; + this._second = rapPkg.readUInt8(offset); offset += 1; + this._year = 2000 + rapPkg.readUInt8(offset); offset += 1; + this._month = rapPkg.readUInt8(offset); offset += 1; + this._day = rapPkg.readUInt8(offset); offset += 1; + + // Month in JS 0 => 11 + record.gdt = new Date(Date.UTC(this._year, this._month - 1, this._day, this._hour, this._minute, this._second)); + + record.inputs = 0; + + // Cmd 0x13. Byte 19th + this._byte = rapPkg.readUInt8(offset); offset += 1; + + // Cmd 0x13. Byte 19th. Bits 0-3: numbers of GPS setellites + record.sats = bitUtil.extractBits(this._byte, 4, 0); + + // Cmd 0x13. Byte 20th + this._byte = rapPkg.readUInt8(offset); + + // Cmd 0x13. Byte 20th. Bit 3: Inputs+ + if (bitUtil.isBitOn(this._byte, 3)) { + + // Cmd 0x13. Byte 20th. Bit 0: 1 => extra inputs (21-24) + offset += bitUtil.isBitOn(this._byte, 0) ? 4 : 1; + + // Skip reading Odometer data + + // Cmd 0x13. Byte 21th/25th + this._byte = rapPkg.readUInt8(offset); offset += 1; + + if (bitUtil.isBitOn(this._byte, 0)) { + // Cmd 0x13. Byte 22th/25th: Whether Digital input state present + this._byte = rapPkg.readUInt8(offset); offset += 1; + + // Digital inputs (Up to 5, use only input 1 for Spray on/off, 0: spray on and 1 is off) + // Check Input 1 (bit 0) for spray on/off + record.inputs = bitUtil.isBitOn(this._byte, 0) ? 0 : 1; + } + } + + } + } + return record; + } +} + +module.exports = RAPParser; \ No newline at end of file diff --git a/bit-util/index.js b/bit-util/index.js new file mode 100644 index 0000000..3e5bb09 --- /dev/null +++ b/bit-util/index.js @@ -0,0 +1,32 @@ +'use strict'; + +/** + * Extract n bits from a position + * @param numVal the number + * @param bits number of bits to extract + * @param from the bit postion right to left to extract. Zero index. + */ +function extractBits(numVal, bits, from) { + let _from = Math.max(0, from); + let _bits = Math.abs(bits); + return (((1 << _bits) - 1) & (numVal >> _from)); +}; + +/** + * Check if k-th bit of a given number is set or not using right shift operator. + * @param {*} n the number value to check + * @param {*} at the bit position. Zero index. + */ +function isBitOn(n, bit) { + let _at = Math.max(0, bit); + return ((n >> _at) & 1); +}; + +function bytesToHex(bytes, delimeter = "") { + return bytes && Array.isArray(bytes) ? Array.from( + bytes, + byte => byte.toString(16).padStart(2, "0").toUpperCase() + ).join(delimeter) : ''; +} + +module.exports = { extractBits, isBitOn, bytesToHex } \ No newline at end of file diff --git a/bit-util/package-lock.json b/bit-util/package-lock.json new file mode 100644 index 0000000..06fb866 --- /dev/null +++ b/bit-util/package-lock.json @@ -0,0 +1,5 @@ +{ + "name": "bit-util", + "version": "1.0.0", + "lockfileVersion": 1 +} diff --git a/bit-util/package.json b/bit-util/package.json new file mode 100644 index 0000000..c284b0d --- /dev/null +++ b/bit-util/package.json @@ -0,0 +1,12 @@ +{ + "name": "bit-util", + "version": "1.0.0", + "description": "Bit utilities with bit manipulation functions", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "Trung Hoang", + "license": "Proprietary" +} diff --git a/device-tcp-socket/agnav-device.js b/device-tcp-socket/agnav-device.js new file mode 100644 index 0000000..77373aa --- /dev/null +++ b/device-tcp-socket/agnav-device.js @@ -0,0 +1,138 @@ +'use strict'; + +const TCPDevice = require('./tcp-device'); +const numberUtil = require('number-util'); + +const READ_0 = 0; +const READ_1 = 1; +const READ_BLOCKED = 99; + +const START_MARK = Buffer.from('FBFB', 'hex'); +const END_MARK = Buffer.from('0D0A', 'hex'); +const PKG_SIZE = 40; + +class AgNavTCPDevice extends TCPDevice { + + constructor(socket, ops) { + super(socket, ops); + + this.id = null; + this.msgReceived = 0; + + this._startPos = -1; + this._endPos = -1; + this._firstPart = null; + this._secondPart = null; + this._cmdBuf = null; + + this._readState = READ_0; + } + + /** + * _onData is triggered by the "readable" event on the underlying TCP socket. + * It is called each time there is new data * received. It is responsible for reading data from the socket and + * performing the appropriate action given the current read state. + */ + _onData() { + try { + // Loop while there was still data to process on the socket's buffer. + // This will stop when we don't have enough data or encountering a back pressure issue; + let readMore = true; + do { + switch (this._readState) { + case READ_0: + readMore = this._processRead_0(); + break; + case READ_1: + readMore = this._processRead_1(); + break; + case READ_BLOCKED: + readMore = false; + break; + default: + throw new Error('Unknown read state'); + } + } while (readMore); + } catch (err) { + console.log(err); + // Terminate on failures as we won't be able to recovery since data was corrupted and we won't + // be able to any more data without additional errors. + this.destroy(err); + } + } + + _processRead_0() { + this._firstPart = this._socket.read(PKG_SIZE / 2); + if (!this._firstPart) + return false; + + this._startPos = this._firstPart.indexOf(START_MARK); + if (this._startPos === -1) { + this._guardInvalidTries(); + + this._readState = READ_0; + } else { + this._readState = READ_1; + } + return true; + } + + _processRead_1() { + // Read payload data by the length + this._secondPart = this._socket.read(this._startPos + (PKG_SIZE / 2)); + if (!this._secondPart) { + return false; + } + + this._endPos = this._secondPart.indexOf(END_MARK); + + if (this._endPos === -1) { + this._guardInvalidTries(); + + // Could not find the match for end mark => skip the first part to find the next start mark + this.unshift(this._secondPart); + } else { + if ((this._firstPart.length - this._startPos) + this._endPos + END_MARK.length <= PKG_SIZE) { + + this._invalidTry = 0; + this.msgReceived++; + this._cmdBuf = Buffer.alloc(PKG_SIZE - START_MARK.length); + + this._firstPart.copy(this._cmdBuf, 0, this._startPos + START_MARK.length); + this._secondPart.copy(this._cmdBuf, this._firstPart.length - this._startPos - START_MARK.length, 0, this._endPos + END_MARK.length); + + if (!this.id) { + this.id = numberUtil.padZero(this._cmdBuf.readInt32LE(1), 10); + } + + // Check for the case of package size < PKG_SIZE + if (this._secondPart.length > this._endPos + END_MARK.length) { + // return the rest to the internal buffer for the next read + this.unshift(this._secondPart.slice(this._endPos + END_MARK.length)); + } + + // Push the message onto the read buffer for the consumer to read. We are mindful of slow reads by the consumer + // and will respect backpressure signals. + const pushOk = this.push(this._cmdBuf); + if (pushOk) { + this._readState = READ_0; + return true; + } else { + console.log('socket read is blocked'); + this._readState = READ_BLOCKED; + return false; + } + } else { + this._invalidTry++; + if (this._invalidTry > this.ops.maxInvalidPackages) + throw new Error('Reached maximum invalid read try'); + } + } + + this._readState = READ_0; + return true; + } + +} + +module.exports = AgNavTCPDevice; diff --git a/device-tcp-socket/index.js b/device-tcp-socket/index.js new file mode 100644 index 0000000..a15a119 --- /dev/null +++ b/device-tcp-socket/index.js @@ -0,0 +1,3 @@ +exports.TCPDevice = require('./tcp-device'); +exports.AgNavTCPDevice = require('./agnav-device'); +exports.RAPTCPDevice = require('./rap-device'); \ No newline at end of file diff --git a/device-tcp-socket/package.json b/device-tcp-socket/package.json new file mode 100644 index 0000000..1d81539 --- /dev/null +++ b/device-tcp-socket/package.json @@ -0,0 +1,14 @@ +{ + "name": "device-tcp-socket", + "version": "1.1.0", + "description": "Device tcp socket implementation", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Trung Hoang ", + "license": "Proprietary", + "dependencies": { + "number-util": "file:../number-util" + } +} diff --git a/device-tcp-socket/rap-device.js b/device-tcp-socket/rap-device.js new file mode 100644 index 0000000..49fb36e --- /dev/null +++ b/device-tcp-socket/rap-device.js @@ -0,0 +1,158 @@ +'use strict'; + +const TCPDevice = require('./tcp-device'); + +const READ_0 = 0; +const READ_ID = 1; +const READ_LEN = 2; +const READ_BODY = 3; +const READ_BLOCKED = 99; + +const RESP_ID_HEADER = 0xE1; +const RESP_CMD_HEADER = 0xF1; + +class RAPTCPDevice extends TCPDevice { + + constructor(socket, ops) { + super(socket, ops); + + this.id = null; + this.msgReceived = 0; + + this._idPart = null; + this._lenPart = null; + this._dataLength = 0; + this._bodyPart = null; + this._cmdBuf = null; + + this._readState = READ_0; + } + + /** + * _onData is triggered by the "readable" event on the underlying TCP socket. + * It is called each time there is new data * received. It is responsible for reading data from the socket and + * performing the appropriate action given the current read state. + */ + _onData() { + try { + // Loop while there was still data to process on the socket's buffer. + // This will stop when we don't have enough data or encountering a back pressure issue; + let readMore = true; + do { + switch (this._readState) { + case READ_0: + // this._idPart = null; + // this._lenPart = null; + // this._dataLength = 0; + // this._bodyPart = null; + readMore = this._processRead_0(); + break; + case READ_ID: + readMore = this._processReadId(); + break; + case READ_LEN: + readMore = this._processReadLen(); + break; + case READ_BODY: + readMore = this._processReadBody(); + break; + case READ_BLOCKED: + readMore = false; + break; + default: + throw new Error('Unknown read state'); + } + } while (readMore); + } catch (err) { + // Terminate on failures as we won't be able to recovery since data was corrupted and we won't + // be able to any more data without additional errors. + this.destroy(err); + } + } + + _processRead_0() { + this._firstByte = this._socket.read(1); + if (!this._firstByte) + return false; + + if (this._firstByte[0] === RESP_ID_HEADER) { + this._readState = READ_ID; + } else if (this._firstByte[0] === RESP_CMD_HEADER) { + this._readState = READ_LEN; + } else { + this._readState = READ_0; + this._guardInvalidTries(); + } + + return true; + } + + _getDeviceId(buf) { + buf[7] = 0; // byte 8th is not used in the case ID is IMEI + // Convert this buf to 64 integer number + return buf.readBigUInt64LE(0).toString(); + } + + _processReadId() { + // Try to read the Report Data Length from the 3rd byte of the GPS Response message + // If we cannot read the 8 bytes, the attempt to process the message will abort. + // E1 => [8 byte ID] + this._idPart = this._socket.read(8); + if (!this._idPart) + return false; + + if (!this.id) { + // Parse the Device ID (8bytes) for the IMEI number + const id8byte = this._idPart.slice(0); + if (id8byte[7] === 0x01) + this.id = this._getDeviceId(id8byte); + } + + this._readState = READ_0; + return true; + } + + _processReadLen() { + // E1[8 byte ID]F1 => + this._lenPart = this._socket.read(2); + if (!this._lenPart) + return false; + + this._dataLength = this._lenPart[1]; + + this._readState = READ_BODY; + return true; + } + + _processReadBody() { + // Read payload data by the length + this._bodyPart = this._socket.read(this._dataLength); + if (!this._bodyPart) { + // this._socket.unshift(this._lenPart); // ??? needed + return false; + } + + this._invalidTry = 0; + this.msgReceived++; + this._cmdBuf = Buffer.alloc(this._lenPart.length + this._dataLength); + + // Copy the from read + this._lenPart.copy(this._cmdBuf); + this._bodyPart.copy(this._cmdBuf, this._lenPart.length); + // Push the message onto the read buffer for the consumer to read. We are mindful of slow reads by the consumer + // and will respect backpressure signals. + const pushOk = this.push(this._cmdBuf); + if (pushOk) { + this._readState = READ_0; + return true; + } else { + // debug("socket read is blocked"); + console.log('socket read is blocked'); + this._readState = READ_BLOCKED; + return false; + } + } + +} + +module.exports = RAPTCPDevice; diff --git a/device-tcp-socket/tcp-device.js b/device-tcp-socket/tcp-device.js new file mode 100644 index 0000000..67b7e0e --- /dev/null +++ b/device-tcp-socket/tcp-device.js @@ -0,0 +1,88 @@ +'use strict'; + +const stream = require('stream'), + Socket = require('net').Socket, + assert = require('assert'); + +/** + * Base TCPDevice class for handling TCP socket from tracking device + * + */ +class TCPDevice extends stream.Duplex { + + constructor(socket, ops) { + super(ops); + + this.ops = Object.assign({}, { + maxInvalidReadTry: 5 + }, ops); + + // Perform type assertions + assert.ok(socket instanceof Socket, 'socket argument must be an instance of Socket'); // prettier-ignore + + this.id = null; + this.msgReceived = 0; + this._invalidTry = 0; + + this._socket = socket; + this._socket.on('close', hadError => this.emit('close', hadError)); + // this._socket.on("connect", () => console.log("Client connected !")); + this._socket.on('drain', () => this.emit('drain')); + this._socket.on('end', () => this.emit('end')); + this._socket.on('error', err => this.emit('error', err)); + this._socket.on('lookup', (e, a, f, h) => this.emit('lookup', e, a, f, h)); + this._socket.on('readable', this._onData.bind(this)); + this._socket.on('timeout', () => this.emit('timeout')); + } + + /** + * Half-closes the socket. It is still possible that the opposite + * side is still sending data. + */ + end() { + this._socket.end(); + return this; + } + + /** + * Destroys the socket and ensures that no more I/O activity happens + * on the socket. When an `err` is included, an 'error' event will + * be emitted and all listeners will receive the error as an + * argument. + * @param err optional error to send + */ + destroy(err) { + this._socket.destroy(err); + return this; + } + + _guardInvalidTries() { + this._invalidTry++; + if (this._invalidTry > this.ops.maxInvalidReadTry) + throw new Error('Reached maximum invalid read try'); + } + + /** + * _onData is triggered by the "readable" event on the underlying TCP socket. + * It is called each time there is new data * received. It is responsible for reading data from the socket and * + * performing the appropriate action given the current read state. + * @virtual Extenders implementation must override this menthod acccording to the detail of the protocol + */ + _onData() { } + + _read() { + + // Trigger a read but wait until the end of the event loop. + // This is necessary when reading in paused mode where + // _read was triggered by stream.read() originating inside + // a "readable" event handler. Attempting to push more data + // synchronously will not trigger another "readable" event. + setImmediate(() => this._onData()); + } + + _write() { } + _final() { } + +} + +module.exports = TCPDevice; \ No newline at end of file diff --git a/error-handler/api_err_handler.js b/error-handler/api_err_handler.js new file mode 100644 index 0000000..601c9f1 --- /dev/null +++ b/error-handler/api_err_handler.js @@ -0,0 +1,46 @@ +'use strict'; + +/** + * Error handling functions for the REST server applications. + * Notes: This module provides reusable functions to create and handle API errors. + * It includes functions to create application-specific error objects, throw errors, and handle response errors. + * + * However, the error handling logic is just the simplest form of error management. + * @author: Trung Hoang + * @version: 1.0.1 + * @date: 2023-10-01 + * @license: proprietary + * @description: This module provides functions to create and handle API errors. + */ + +const { isAppError } = require('./utils/common'); + +function appError(tag) { + if (!tag) + return { error: { '.tag': 'unknown_error', text: 'Unknown Error' } } + else + return { error: { '.tag': tag } } + // return { error: { '.tag': tag, text: msg ? msg : '' } } +} + +function throwError(tag) { + throw new Error(tag); +} + +function handleResErr(res, err, code = 409) { + if (!res || typeof res !== "object") return; + + const _erCode = code; + const _err = isAppError(err); + if (_err) { + res.status(_erCode || 409).send(appError(_err)); + } else { + res.status(_erCode || 500).send(appError('unknown_error')); + if (err) console.log(err.stack); + } +} + + +module.exports = { + throwError, appError, handleResErr +} diff --git a/error-handler/error_handler.js b/error-handler/error_handler.js new file mode 100644 index 0000000..d7dfc72 --- /dev/null +++ b/error-handler/error_handler.js @@ -0,0 +1,87 @@ +'use strict'; + +const util = require('util'), + path = require('path'), + mailer = require('mailer').default, + KeyFileStorage = require("key-file-storage").default, + debug = require('debug')('agm:errHelper'); + +let logFileName = path.basename(__filename); + + +/** + * Report important errors to Admin + * @param {*} error Error message in text or the Error object + */ +async function mailErrorToAdmin(error) { + const errorMsg = (error && error.message) ? error.message : error; + const contentOps = { + subject: '[Agm-Errors] Unexpected Errors', + body: error && error.stack ? errorMsg + '\n' + error.stack : errorMsg, + to: process.env.AGM_ADM_EMAIL || '"AgMission Admin" ', + }; + return await mailer.sendTextMail(contentOps); +} + +async function _handleCriticalError(error, message) { + const exitFunc = (err) => { + debug(err); + process.exit(1); + }; + try { + const baseFilename = path.basename(logFileName); + const kfs = KeyFileStorage(path.dirname(logFileName), false); + + let lastReport = await kfs[baseFilename]; + let curDateTime = new Date(); + + if (kfs[baseFilename] && (lastReport = kfs[baseFilename])) { + if (lastReport.when && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.*$/.test(lastReport.when)) { + if (curDateTime - new Date(lastReport.when) < 2 * 60 * 1e3 + && ((error.code && lastReport.code === error.code) || lastReport.message == error.message)) + return exitFunc(error); // Reported the same error two shortly, at least 2 minutes => ignore to avoid flooding with the same emails + } + } + const logObj = { + code: error.code, + message: error.message, + when: curDateTime + }; + await kfs(baseFilename, logObj); + } catch (err) { + debug(err); + } + + try { + await mailErrorToAdmin(message); + } + catch (err) { + debug(err); + } + finally { + return exitFunc(error); + } +} + +/** + * Register the process error handlers + * @param {*} process The process object + * @param {*} fileName The file name to log the error + */ +function registerUnCaughtProcessErrorsHandler(process, fileName) { + if (!process) return; + + if (fileName) logFileName = fileName; + + process + .on('uncaughtException', async (err) => { + await _handleCriticalError(err, util.format('uncaughtException:', err)); + }) + .on('unhandledRejection', async (reason, p) => { + await _handleCriticalError(reason, util.format(reason, 'Unhandled Rejection at Promise', p)); + }); +} + +module.exports = { + mailErrorToAdmin, registerUnCaughtProcessErrorsHandler +} diff --git a/error-handler/index.js b/error-handler/index.js new file mode 100644 index 0000000..63db4dd --- /dev/null +++ b/error-handler/index.js @@ -0,0 +1,9 @@ +const common = require('./utils/common'); +const apiErrorHandler = require('./api_err_handler'); +const errorHandler = require('./error_handler'); + +module.exports = { + common: common, + apiErrorHandler: apiErrorHandler, + errorHandler: errorHandler +}; diff --git a/error-handler/package-lock.json b/error-handler/package-lock.json new file mode 100644 index 0000000..c66bc2b --- /dev/null +++ b/error-handler/package-lock.json @@ -0,0 +1,548 @@ +{ + "name": "error-handler", + "version": "2.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "error-handler", + "version": "2.0.0", + "license": "Proprietary", + "dependencies": { + "debug": "^4.4.0", + "key-file-storage": "^2.3.3", + "mailer": "file:../mailer" + } + }, + "../mailer": { + "name": "mailer", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "debug": "^4.4.0", + "nodemailer": "^6.10.0" + } + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/is-valid-path": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@types/is-valid-path/-/is-valid-path-0.1.2.tgz", + "integrity": "sha512-BsZtkfiPpnzDWFjSZanYllttVW7/46ayPZkcHBCSFBkBqIO9rWrflUvEmT2tF///hnPLwBJU3TJPzbBxpUEqCg==" + }, + "node_modules/@types/node": { + "version": "22.14.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", + "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha512-a1dBeB19NXsf/E0+FHqkagizel/LQw2DjSQpvQrj3zT+jYPpaUCryPnrQajXKFLCMuf4I6FhRpaGtw4lPrG6Eg==", + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-invalid-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-invalid-path/-/is-invalid-path-0.1.0.tgz", + "integrity": "sha512-aZMG0T3F34mTg4eTdszcGXx54oiZ4NtHSft3hWNJMGJXUUqdIj3cOZuHcU0nCWWcY3jd7yRe/3AEm3vSNTpBGQ==", + "dependencies": { + "is-glob": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-valid-path": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-valid-path/-/is-valid-path-0.1.1.tgz", + "integrity": "sha512-+kwPrVDu9Ms03L90Qaml+79+6DZHqHyRoANI6IsZJ/g8frhnfchDOBCa0RbQ6/kdHt5CS5OeIEyrYznNuVN+8A==", + "dependencies": { + "is-invalid-path": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/key-file-storage": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/key-file-storage/-/key-file-storage-2.3.3.tgz", + "integrity": "sha512-bqFrbE0ifIq5ahrquGewh5nMub8fdH/nXKMg9CJHdCdD5W/6KQea483Qyss0q53tCOC64CN43DQo9TSvohlbKA==", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "@types/is-valid-path": "^0.1.0", + "fs-extra": "^10.0.0", + "is-valid-path": "^0.1.1", + "recur-fs": "^2.2.4" + } + }, + "node_modules/mailer": { + "resolved": "../mailer", + "link": true + }, + "node_modules/minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-NyXjqu1IwcqH6nv5vmMtaG3iw7kdV3g6MwlUBZkc3Vn5b5AMIWYKfptvzipoyFfhlfOgBQ9zoTxQMravF1QTnw==", + "dependencies": { + "brace-expansion": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha512-miQKw5Hv4NS1Psg2517mV4e4dYNaO3++hjAvLOAzKqZ61rH8NS1SK+vbfBWZ5PY/Me/bEWhUwqMghEW5Fb9T7Q==" + }, + "node_modules/mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha512-SknJC52obPfGQPnjIkXbmA6+5H15E+fR+E4iR2oQ3zzCLbd7/ONua69R/Gw7AgkTLsRG+r5fzksYwWe1AgTyWA==", + "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", + "dependencies": { + "minimist": "0.0.8" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/recur-fs": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/recur-fs/-/recur-fs-2.2.4.tgz", + "integrity": "sha512-VofuavbR3PNwHAs2uxn2HNcyyXKIUBayVtdhdElJ8qqSmcr2TY71THQjAoF+PT1xEeUnEi57DWT2/+YSRCvr3w==", + "dependencies": { + "minimatch": "3.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.5.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.4.tgz", + "integrity": "sha512-Lw7SHMjssciQb/rRz7JyPIy9+bbUshEucPoLRvWqy09vC5zQixl8Uet+Zl+SROBB/JMWHJRdCk1qdxNWHNMvlQ==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dependencies": { + "glob": "^7.0.5" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + } + }, + "dependencies": { + "@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "requires": { + "@types/node": "*" + } + }, + "@types/is-valid-path": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@types/is-valid-path/-/is-valid-path-0.1.2.tgz", + "integrity": "sha512-BsZtkfiPpnzDWFjSZanYllttVW7/46ayPZkcHBCSFBkBqIO9rWrflUvEmT2tF///hnPLwBJU3TJPzbBxpUEqCg==" + }, + "@types/node": { + "version": "22.14.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", + "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", + "requires": { + "undici-types": "~6.21.0" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "requires": { + "ms": "^2.1.3" + } + }, + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "dependencies": { + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww==" + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha512-a1dBeB19NXsf/E0+FHqkagizel/LQw2DjSQpvQrj3zT+jYPpaUCryPnrQajXKFLCMuf4I6FhRpaGtw4lPrG6Eg==", + "requires": { + "is-extglob": "^1.0.0" + } + }, + "is-invalid-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-invalid-path/-/is-invalid-path-0.1.0.tgz", + "integrity": "sha512-aZMG0T3F34mTg4eTdszcGXx54oiZ4NtHSft3hWNJMGJXUUqdIj3cOZuHcU0nCWWcY3jd7yRe/3AEm3vSNTpBGQ==", + "requires": { + "is-glob": "^2.0.0" + } + }, + "is-valid-path": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-valid-path/-/is-valid-path-0.1.1.tgz", + "integrity": "sha512-+kwPrVDu9Ms03L90Qaml+79+6DZHqHyRoANI6IsZJ/g8frhnfchDOBCa0RbQ6/kdHt5CS5OeIEyrYznNuVN+8A==", + "requires": { + "is-invalid-path": "^0.1.0" + } + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "key-file-storage": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/key-file-storage/-/key-file-storage-2.3.3.tgz", + "integrity": "sha512-bqFrbE0ifIq5ahrquGewh5nMub8fdH/nXKMg9CJHdCdD5W/6KQea483Qyss0q53tCOC64CN43DQo9TSvohlbKA==", + "requires": { + "@types/fs-extra": "^9.0.11", + "@types/is-valid-path": "^0.1.0", + "fs-extra": "^10.0.0", + "is-valid-path": "^0.1.1", + "recur-fs": "^2.2.4" + } + }, + "mailer": { + "version": "file:../mailer", + "requires": { + "debug": "^4.4.0", + "nodemailer": "^6.10.0" + } + }, + "minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-NyXjqu1IwcqH6nv5vmMtaG3iw7kdV3g6MwlUBZkc3Vn5b5AMIWYKfptvzipoyFfhlfOgBQ9zoTxQMravF1QTnw==", + "requires": { + "brace-expansion": "^1.0.0" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha512-miQKw5Hv4NS1Psg2517mV4e4dYNaO3++hjAvLOAzKqZ61rH8NS1SK+vbfBWZ5PY/Me/bEWhUwqMghEW5Fb9T7Q==" + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha512-SknJC52obPfGQPnjIkXbmA6+5H15E+fR+E4iR2oQ3zzCLbd7/ONua69R/Gw7AgkTLsRG+r5fzksYwWe1AgTyWA==", + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" + }, + "recur-fs": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/recur-fs/-/recur-fs-2.2.4.tgz", + "integrity": "sha512-VofuavbR3PNwHAs2uxn2HNcyyXKIUBayVtdhdElJ8qqSmcr2TY71THQjAoF+PT1xEeUnEi57DWT2/+YSRCvr3w==", + "requires": { + "minimatch": "3.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.5.4" + } + }, + "rimraf": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.4.tgz", + "integrity": "sha512-Lw7SHMjssciQb/rRz7JyPIy9+bbUshEucPoLRvWqy09vC5zQixl8Uet+Zl+SROBB/JMWHJRdCk1qdxNWHNMvlQ==", + "requires": { + "glob": "^7.0.5" + } + }, + "undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" + }, + "universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + } + } +} diff --git a/error-handler/package.json b/error-handler/package.json new file mode 100644 index 0000000..80f59c3 --- /dev/null +++ b/error-handler/package.json @@ -0,0 +1,20 @@ +{ + "name": "error-handler", + "version": "2.0.0", + "description": "A reusable error handling utility module for Node.js applications.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "Error Handling", + "Utility" + ], + "author": "Trung Hoang", + "license": "Proprietary", + "dependencies": { + "debug": "^4.4.0", + "key-file-storage": "^2.3.3", + "mailer": "file:../mailer" + } +} diff --git a/error-handler/utils/common.js b/error-handler/utils/common.js new file mode 100644 index 0000000..8b665b7 --- /dev/null +++ b/error-handler/utils/common.js @@ -0,0 +1,10 @@ +'use restrict'; + +function isAppError(err) { + const _err = err && err.message || err || ''; + return (err && (typeof _err === "string" && _err.includes('_'))) ? _err : ''; +} + +module.exports = { + isAppError +}; \ No newline at end of file diff --git a/mailer/index.js b/mailer/index.js new file mode 100644 index 0000000..b10eb8a --- /dev/null +++ b/mailer/index.js @@ -0,0 +1,4 @@ + +module.exports = { + default: require('./mailer.js') +} \ No newline at end of file diff --git a/mailer/mailer.js b/mailer/mailer.js new file mode 100644 index 0000000..379e55b --- /dev/null +++ b/mailer/mailer.js @@ -0,0 +1,84 @@ +'use strict'; + +const nodemailer = require('nodemailer'), + debug = require('debug')('app:mailer'); + +const createConfig = (env) => { + const isSecure = env?.SMTP_SECURE || process.env.SMTP_SECURE || false; + const port = env?.SMTP_PORT || process.env.SMTP_PORT || 587; + + const config = { + host: env?.SMTP_HOST || process.env.SMTP_HOST, + port: port, + secure: isSecure, + auth: { + user: env?.SMTP_USR || process.env.SMTP_USR, + pass: env?.SMTP_PWD || process.env.SMTP_PWD, + }, + sender: env?.SMTP_SENDER || process.env.SMTP_SENDER || `"AgNav Inc." ` + }; + + if (!config.host || !config.port || !config.auth.user || !config.auth.pass) { + throw new Error('SMTP configuration is missing required fields (host, port, user, pass).'); + } + return config; +}; + +const createMailOptions = (from, to, subject, body) => ({ + from: from, to: to, subject: subject, text: body +}); + +function createMailer(env) { + const config = createConfig(env); + + const transporter = nodemailer.createTransport(config); + + return { + sendMail: async (to, subject, body) => { + try { + if (!to || !subject || !body) { + throw new Error('Missing required fields: to, subject, or body'); + } + + const mailOptions = createMailOptions(config.sender, to, subject, body); + const info = await transporter.sendMail(mailOptions); + + return { success: true, message: 'Mail sent successfully', messageId: info.messageId }; + } catch (error) { + debug('Error sending mail:', error.message); + return { success: false, error: error.message }; + } + }, + }; +} + +/** + * Send a text email + * @param {Object} options Email options + * @param {string} options.from Sender of the email (Optional) + * @param {string} options.subject Subject of the email + * @param {string} options.to Recipient of the email + * @param {string} options.text Text content of the email + */ +async function sendTextMail(options, env) { + const { from, to, subject, body } = options; + if (!to || !subject || !body) { + throw new Error('Missing required fields: to, subject, or body'); + } + const config = createConfig(env); + + const transporter = nodemailer.createTransport(config); + const mailOptions = createMailOptions(from || config.sender, to, subject, body); + + try { + const info = await transporter.sendMail(mailOptions); + return { success: true, messageId: info.messageId }; + } catch (error) { + debug('Error sending email:', error.message); + return { success: false, error: error.message }; + } +} + +module.exports = { + createMailer, sendTextMail +}; \ No newline at end of file diff --git a/mailer/package-lock.json b/mailer/package-lock.json new file mode 100644 index 0000000..d489b7a --- /dev/null +++ b/mailer/package-lock.json @@ -0,0 +1,66 @@ +{ + "name": "mailer", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "mailer", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "debug": "^4.4.0", + "nodemailer": "^6.10.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/nodemailer": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz", + "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==", + "engines": { + "node": ">=6.0.0" + } + } + }, + "dependencies": { + "debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "requires": { + "ms": "^2.1.3" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "nodemailer": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz", + "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==" + } + } +} diff --git a/mailer/package.json b/mailer/package.json new file mode 100644 index 0000000..0446c69 --- /dev/null +++ b/mailer/package.json @@ -0,0 +1,19 @@ +{ + "name": "mailer", + "version": "1.0.0", + "description": "Simple reusable mailer utility.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "mailer", + "utility" + ], + "author": "Trung Hoang", + "license": "ISC", + "dependencies": { + "debug": "^4.4.0", + "nodemailer": "^6.10.0" + } +} \ No newline at end of file diff --git a/number-util/index.js b/number-util/index.js new file mode 100644 index 0000000..0af9105 --- /dev/null +++ b/number-util/index.js @@ -0,0 +1,25 @@ +'use strict'; + +/** + * Trim the decimal to maximum numbers of digits + * @param {*} value the decimal value + * @param {*} digits maximum number of decimal digits + */ +function fixedTo(value, digits) { + if (!value) return value; + var re = new RegExp('(-?\\d+\\.\\d{' + digits + '})(\\d)'), + m = value.toString().match(re); + return m ? parseFloat(m[1]) : value; +}; + +function padZero(num, size) { + let s = num + ''; + while (s.length < size) { + s = '0' + s; + } + return s; +} + +module.exports = { + fixedTo, padZero +} diff --git a/number-util/package-lock.json b/number-util/package-lock.json new file mode 100644 index 0000000..d9b6003 --- /dev/null +++ b/number-util/package-lock.json @@ -0,0 +1,5 @@ +{ + "name": "number-util", + "version": "1.1.0", + "lockfileVersion": 1 +} diff --git a/number-util/package.json b/number-util/package.json new file mode 100644 index 0000000..2b73cb6 --- /dev/null +++ b/number-util/package.json @@ -0,0 +1,12 @@ +{ + "name": "number-util", + "version": "1.1.0", + "description": "Number utilities", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "Trung Hoang", + "license": "Proprietary" +}