copy of the @agn repository from as of April 23 2026
This commit is contained in:
parent
d6e6e8164f
commit
3362073a35
1
.svn/entries
Normal file
1
.svn/entries
Normal file
@ -0,0 +1 @@
|
|||||||
|
12
|
||||||
1
.svn/format
Normal file
1
.svn/format
Normal file
@ -0,0 +1 @@
|
|||||||
|
12
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
module.exports.RAPParser = require('./rap-parser');
|
||||||
|
module.exports.AgNavParser = require('./agn-parser');
|
||||||
@ -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=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
};
|
||||||
@ -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
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"name": "bit-util",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 1
|
||||||
|
}
|
||||||
@ -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;
|
||||||
@ -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
|
||||||
|
}
|
||||||
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
module.exports = {
|
||||||
|
default: require('./mailer.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 => <Cmd><Len>
|
||||||
|
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 <Cmd> from read <Cmd><Len>
|
||||||
|
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;
|
||||||
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
exports.TCPDevice = require('./tcp-device');
|
||||||
|
exports.AgNavTCPDevice = require('./agnav-device');
|
||||||
|
exports.RAPTCPDevice = require('./rap-device');
|
||||||
@ -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"
|
||||||
|
}
|
||||||
@ -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" <agm_admin@agnav.com>',
|
||||||
|
};
|
||||||
|
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
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
};
|
||||||
@ -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><Len><Data>, 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 <Cmd><Len> 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;
|
||||||
@ -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." <noreply@agnav.com>`
|
||||||
|
};
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
@ -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><Data>, 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;
|
||||||
@ -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 <trungh@agnav.com>",
|
||||||
|
"license": "Proprietary",
|
||||||
|
"dependencies": {
|
||||||
|
"number-util": "file:../number-util"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 }
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"name": "number-util",
|
||||||
|
"version": "1.1.0",
|
||||||
|
"lockfileVersion": 1
|
||||||
|
}
|
||||||
@ -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"
|
||||||
|
}
|
||||||
BIN
.svn/wc.db
Normal file
BIN
.svn/wc.db
Normal file
Binary file not shown.
0
.svn/wc.db-journal
Normal file
0
.svn/wc.db-journal
Normal file
74
binary-package-parser/agn-parser.js
Normal file
74
binary-package-parser/agn-parser.js
Normal file
@ -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><Data>, 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;
|
||||||
2
binary-package-parser/index.js
Normal file
2
binary-package-parser/index.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
module.exports.RAPParser = require('./rap-parser');
|
||||||
|
module.exports.AgNavParser = require('./agn-parser');
|
||||||
25
binary-package-parser/package-lock.json
generated
Normal file
25
binary-package-parser/package-lock.json
generated
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
binary-package-parser/package.json
Normal file
18
binary-package-parser/package.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
93
binary-package-parser/rap-parser.js
Normal file
93
binary-package-parser/rap-parser.js
Normal file
@ -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><Len><Data>, 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 <Cmd><Len> 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;
|
||||||
32
bit-util/index.js
Normal file
32
bit-util/index.js
Normal file
@ -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 }
|
||||||
5
bit-util/package-lock.json
generated
Normal file
5
bit-util/package-lock.json
generated
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"name": "bit-util",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 1
|
||||||
|
}
|
||||||
12
bit-util/package.json
Normal file
12
bit-util/package.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
138
device-tcp-socket/agnav-device.js
Normal file
138
device-tcp-socket/agnav-device.js
Normal file
@ -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;
|
||||||
3
device-tcp-socket/index.js
Normal file
3
device-tcp-socket/index.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
exports.TCPDevice = require('./tcp-device');
|
||||||
|
exports.AgNavTCPDevice = require('./agnav-device');
|
||||||
|
exports.RAPTCPDevice = require('./rap-device');
|
||||||
14
device-tcp-socket/package.json
Normal file
14
device-tcp-socket/package.json
Normal file
@ -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 <trungh@agnav.com>",
|
||||||
|
"license": "Proprietary",
|
||||||
|
"dependencies": {
|
||||||
|
"number-util": "file:../number-util"
|
||||||
|
}
|
||||||
|
}
|
||||||
158
device-tcp-socket/rap-device.js
Normal file
158
device-tcp-socket/rap-device.js
Normal file
@ -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 => <Cmd><Len>
|
||||||
|
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 <Cmd> from read <Cmd><Len>
|
||||||
|
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;
|
||||||
88
device-tcp-socket/tcp-device.js
Normal file
88
device-tcp-socket/tcp-device.js
Normal file
@ -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;
|
||||||
46
error-handler/api_err_handler.js
Normal file
46
error-handler/api_err_handler.js
Normal file
@ -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
|
||||||
|
}
|
||||||
87
error-handler/error_handler.js
Normal file
87
error-handler/error_handler.js
Normal file
@ -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" <agm_admin@agnav.com>',
|
||||||
|
};
|
||||||
|
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
|
||||||
|
}
|
||||||
9
error-handler/index.js
Normal file
9
error-handler/index.js
Normal file
@ -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
|
||||||
|
};
|
||||||
548
error-handler/package-lock.json
generated
Normal file
548
error-handler/package-lock.json
generated
Normal file
@ -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=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
error-handler/package.json
Normal file
20
error-handler/package.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
error-handler/utils/common.js
Normal file
10
error-handler/utils/common.js
Normal file
@ -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
|
||||||
|
};
|
||||||
4
mailer/index.js
Normal file
4
mailer/index.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
module.exports = {
|
||||||
|
default: require('./mailer.js')
|
||||||
|
}
|
||||||
84
mailer/mailer.js
Normal file
84
mailer/mailer.js
Normal file
@ -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." <noreply@agnav.com>`
|
||||||
|
};
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
66
mailer/package-lock.json
generated
Normal file
66
mailer/package-lock.json
generated
Normal file
@ -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=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
mailer/package.json
Normal file
19
mailer/package.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
25
number-util/index.js
Normal file
25
number-util/index.js
Normal file
@ -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
|
||||||
|
}
|
||||||
5
number-util/package-lock.json
generated
Normal file
5
number-util/package-lock.json
generated
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"name": "number-util",
|
||||||
|
"version": "1.1.0",
|
||||||
|
"lockfileVersion": 1
|
||||||
|
}
|
||||||
12
number-util/package.json
Normal file
12
number-util/package.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user