Compare commits
2 Commits
5a4770820a
...
f3391fb8f3
| Author | SHA1 | Date | |
|---|---|---|---|
| f3391fb8f3 | |||
| 0fe1714e1a |
@ -13,8 +13,10 @@
|
||||
"body-parser": "^2.2.0",
|
||||
"canonical-json": "^0.2.0",
|
||||
"cors": "^2.8.5",
|
||||
"dayjs": "^1.11.23",
|
||||
"diff": "^9.0.0",
|
||||
"dotenv": "^17.2.3",
|
||||
"ejs": "^3.1.10",
|
||||
"exceljs": "^4.4.0",
|
||||
"exifr": "^7.1.3",
|
||||
"express": "^5.1.0",
|
||||
@ -33,7 +35,9 @@
|
||||
"node-cron": "^4.2.1",
|
||||
"nodemailer": "*",
|
||||
"nodemon": "^3.1.11",
|
||||
"pdf-to-img": "^6.2.0",
|
||||
"pg": "^8.16.3",
|
||||
"posthtml": "^0.16.7",
|
||||
"puppeteer": "^24.37.5",
|
||||
"sequelize": "^6.37.7",
|
||||
"sharp": "^0.34.3",
|
||||
|
||||
321
pnpm-lock.yaml
generated
321
pnpm-lock.yaml
generated
@ -35,12 +35,18 @@ importers:
|
||||
cors:
|
||||
specifier: ^2.8.5
|
||||
version: 2.8.6
|
||||
dayjs:
|
||||
specifier: ^1.11.23
|
||||
version: 1.11.23
|
||||
diff:
|
||||
specifier: ^9.0.0
|
||||
version: 9.0.0
|
||||
dotenv:
|
||||
specifier: ^17.2.3
|
||||
version: 17.2.3
|
||||
ejs:
|
||||
specifier: ^3.1.10
|
||||
version: 3.1.10
|
||||
exceljs:
|
||||
specifier: ^4.4.0
|
||||
version: 4.4.0
|
||||
@ -95,9 +101,15 @@ importers:
|
||||
nodemon:
|
||||
specifier: ^3.1.11
|
||||
version: 3.1.11
|
||||
pdf-to-img:
|
||||
specifier: ^6.2.0
|
||||
version: 6.2.0
|
||||
pg:
|
||||
specifier: ^8.16.3
|
||||
version: 8.18.0
|
||||
posthtml:
|
||||
specifier: ^0.16.7
|
||||
version: 0.16.7
|
||||
puppeteer:
|
||||
specifier: ^24.37.5
|
||||
version: 24.37.5
|
||||
@ -1303,6 +1315,76 @@ packages:
|
||||
'@mongodb-js/saslprep@1.4.5':
|
||||
resolution: {integrity: sha512-k64Lbyb7ycCSXHSLzxVdb2xsKGPMvYZfCICXvDsI8Z65CeWQzTEKS4YmGbnqw+U9RBvLPTsB6UCmwkgsDTGWIw==}
|
||||
|
||||
'@napi-rs/canvas-android-arm64@0.1.100':
|
||||
resolution: {integrity: sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@napi-rs/canvas-darwin-arm64@0.1.100':
|
||||
resolution: {integrity: sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@napi-rs/canvas-darwin-x64@0.1.100':
|
||||
resolution: {integrity: sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.100':
|
||||
resolution: {integrity: sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-gnu@0.1.100':
|
||||
resolution: {integrity: sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-musl@0.1.100':
|
||||
resolution: {integrity: sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@napi-rs/canvas-linux-riscv64-gnu@0.1.100':
|
||||
resolution: {integrity: sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@napi-rs/canvas-linux-x64-gnu@0.1.100':
|
||||
resolution: {integrity: sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@napi-rs/canvas-linux-x64-musl@0.1.100':
|
||||
resolution: {integrity: sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@napi-rs/canvas-win32-arm64-msvc@0.1.100':
|
||||
resolution: {integrity: sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@napi-rs/canvas-win32-x64-msvc@0.1.100':
|
||||
resolution: {integrity: sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@napi-rs/canvas@0.1.100':
|
||||
resolution: {integrity: sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
'@napi-rs/wasm-runtime@0.2.12':
|
||||
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
|
||||
|
||||
@ -1781,9 +1863,9 @@ packages:
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
adm-zip@0.5.18:
|
||||
resolution: {integrity: sha512-ufJnssQGbxzLNS1Ho9bCtX4rQKCCvoVuDLHoJyc3F9dOGDB4BkWs2Ci0kv53lqocAEQ/Cbi+I2XCsNYGqVYqng==}
|
||||
engines: {node: '>=12.0'}
|
||||
adm-zip@0.6.0:
|
||||
resolution: {integrity: sha512-XleryMhbuksdKtofnWZ9Sk+4CUTbms4Mb/EU32SZwToAyZ5RgVos/ki8n+yr0LWHOGKuakbXTuuYNHLQjhddgg==}
|
||||
engines: {node: '>=14.0'}
|
||||
|
||||
agent-base@6.0.2:
|
||||
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
|
||||
@ -1916,8 +1998,8 @@ packages:
|
||||
axios@1.13.4:
|
||||
resolution: {integrity: sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==}
|
||||
|
||||
axios@1.18.1:
|
||||
resolution: {integrity: sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==}
|
||||
axios@1.19.0:
|
||||
resolution: {integrity: sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==}
|
||||
|
||||
b4a@1.8.0:
|
||||
resolution: {integrity: sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==}
|
||||
@ -2172,8 +2254,8 @@ packages:
|
||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||
engines: {node: '>= 8.10.0'}
|
||||
|
||||
chromedriver@151.0.2:
|
||||
resolution: {integrity: sha512-p2NI0qrBSpoksa9S4lxqpML28I1U1oMtNPyvbjiMlBB+JOn9qRQC/cuiAWPf36lAdsYgrqhQczb12UH2he04MQ==}
|
||||
chromedriver@152.0.0:
|
||||
resolution: {integrity: sha512-JBMT88W2bMGSytKcIy6A3F/0DrGJeKTO1ZSThSNICeexSY+xBAEuAvjzlsGkIMaebqNF0KkDUI203CJ+GTA12g==}
|
||||
engines: {node: '>=22'}
|
||||
hasBin: true
|
||||
|
||||
@ -2346,8 +2428,8 @@ packages:
|
||||
resolution: {integrity: sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==}
|
||||
engines: {node: '>=4.0'}
|
||||
|
||||
dayjs@1.11.19:
|
||||
resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==}
|
||||
dayjs@1.11.23:
|
||||
resolution: {integrity: sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==}
|
||||
|
||||
debug@2.6.9:
|
||||
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
||||
@ -2454,6 +2536,19 @@ packages:
|
||||
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
dom-serializer@1.4.1:
|
||||
resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==}
|
||||
|
||||
domelementtype@2.3.0:
|
||||
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
|
||||
|
||||
domhandler@4.3.1:
|
||||
resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
domutils@2.8.0:
|
||||
resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==}
|
||||
|
||||
dotenv@17.2.3:
|
||||
resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==}
|
||||
engines: {node: '>=12'}
|
||||
@ -2482,6 +2577,11 @@ packages:
|
||||
ee-first@1.1.1:
|
||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||
|
||||
ejs@3.1.10:
|
||||
resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
hasBin: true
|
||||
|
||||
electron-to-chromium@1.5.283:
|
||||
resolution: {integrity: sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w==}
|
||||
|
||||
@ -2505,6 +2605,13 @@ packages:
|
||||
end-of-stream@1.4.5:
|
||||
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
|
||||
|
||||
entities@2.2.0:
|
||||
resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==}
|
||||
|
||||
entities@3.0.1:
|
||||
resolution: {integrity: sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==}
|
||||
engines: {node: '>=0.12'}
|
||||
|
||||
env-paths@2.2.1:
|
||||
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
|
||||
engines: {node: '>=6'}
|
||||
@ -2821,6 +2928,9 @@ packages:
|
||||
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
|
||||
filelist@1.0.6:
|
||||
resolution: {integrity: sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==}
|
||||
|
||||
fill-range@7.1.1:
|
||||
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
||||
engines: {node: '>=8'}
|
||||
@ -2886,6 +2996,10 @@ packages:
|
||||
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
form-data@4.0.6:
|
||||
resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
formidable@3.5.4:
|
||||
resolution: {integrity: sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@ -3055,6 +3169,10 @@ packages:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
hasown@2.0.4:
|
||||
resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
hmac-drbg@1.0.1:
|
||||
resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==}
|
||||
|
||||
@ -3065,6 +3183,9 @@ packages:
|
||||
html-escaper@2.0.2:
|
||||
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
|
||||
|
||||
htmlparser2@7.2.0:
|
||||
resolution: {integrity: sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog==}
|
||||
|
||||
http-errors@2.0.1:
|
||||
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@ -3228,6 +3349,9 @@ packages:
|
||||
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-json@2.0.1:
|
||||
resolution: {integrity: sha512-6BEnpVn1rcf3ngfmViLM6vjUjGErbdrL4rwlv+u1NO1XO8kqT4YGL8+19Q+Z/bas8tY90BTWMk2+fW1g6hQjbA==}
|
||||
|
||||
is-map@2.0.3:
|
||||
resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -3342,6 +3466,11 @@ packages:
|
||||
jackspeak@3.4.3:
|
||||
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||
|
||||
jake@10.9.4:
|
||||
resolution: {integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
jest-changed-files@30.2.0:
|
||||
resolution: {integrity: sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==}
|
||||
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||
@ -3924,6 +4053,9 @@ packages:
|
||||
node-int64@0.4.0:
|
||||
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
|
||||
|
||||
node-readable-to-web-readable-stream@0.4.2:
|
||||
resolution: {integrity: sha512-/cMZNI34v//jUTrI+UIo4ieHAB5EZRY/+7OmXZgBxaWBMcW2tGdceIw06RFxWxrKZ5Jp3sI2i5TsRo+CBhtVLQ==}
|
||||
|
||||
node-releases@2.0.27:
|
||||
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
|
||||
|
||||
@ -4102,6 +4234,15 @@ packages:
|
||||
path-to-regexp@8.3.0:
|
||||
resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==}
|
||||
|
||||
pdf-to-img@6.2.0:
|
||||
resolution: {integrity: sha512-54CaUwpTXw0SkG6U3Twx7AJNM2ZuFNpDCJ7oHmmW113gT8RwLZSbzv4/GOr+H0PFpFisuHcUirZBKgXkJXA1zg==}
|
||||
engines: {node: '>=20.19.0 || >=22.13.0 || >=24'}
|
||||
hasBin: true
|
||||
|
||||
pdfjs-dist@5.6.205:
|
||||
resolution: {integrity: sha512-tlUj+2IDa7G1SbvBNN74UHRLJybZDWYom+k6p5KIZl7huBvsA4APi6mKL+zCxd3tLjN5hOOEE9Tv7VdzO88pfg==}
|
||||
engines: {node: '>=20.19.0 || >=22.13.0 || >=24'}
|
||||
|
||||
pend@1.2.0:
|
||||
resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
|
||||
|
||||
@ -4190,6 +4331,18 @@ packages:
|
||||
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
posthtml-parser@0.11.0:
|
||||
resolution: {integrity: sha512-QecJtfLekJbWVo/dMAA+OSwY79wpRmbqS5TeXvXSX+f0c6pW4/SE6inzZ2qkU7oAMCPqIDkZDvd/bQsSFUnKyw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
posthtml-render@3.0.0:
|
||||
resolution: {integrity: sha512-z+16RoxK3fUPgwaIgH9NGnK1HKY9XIDpydky5eQGgAFVXTCSezalv9U2jQuNV+Z9qV1fDWNzldcw4eK0SSbqKA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
posthtml@0.16.7:
|
||||
resolution: {integrity: sha512-7Hc+IvlQ7hlaIfQFZnxlRl0jnpWq2qwibORBhQYIb0QbNtuicc5ZxvKkVT71HJ4Py1wSZ/3VR1r8LfkCtoCzhw==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
prelude-ls@1.2.1:
|
||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@ -6810,6 +6963,54 @@ snapshots:
|
||||
dependencies:
|
||||
sparse-bitfield: 3.0.3
|
||||
|
||||
'@napi-rs/canvas-android-arm64@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-darwin-arm64@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-darwin-x64@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-gnu@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-musl@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-riscv64-gnu@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-x64-gnu@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-x64-musl@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-win32-arm64-msvc@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-win32-x64-msvc@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas@0.1.100':
|
||||
optionalDependencies:
|
||||
'@napi-rs/canvas-android-arm64': 0.1.100
|
||||
'@napi-rs/canvas-darwin-arm64': 0.1.100
|
||||
'@napi-rs/canvas-darwin-x64': 0.1.100
|
||||
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.100
|
||||
'@napi-rs/canvas-linux-arm64-gnu': 0.1.100
|
||||
'@napi-rs/canvas-linux-arm64-musl': 0.1.100
|
||||
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.100
|
||||
'@napi-rs/canvas-linux-x64-gnu': 0.1.100
|
||||
'@napi-rs/canvas-linux-x64-musl': 0.1.100
|
||||
'@napi-rs/canvas-win32-arm64-msvc': 0.1.100
|
||||
'@napi-rs/canvas-win32-x64-msvc': 0.1.100
|
||||
optional: true
|
||||
|
||||
'@napi-rs/wasm-runtime@0.2.12':
|
||||
dependencies:
|
||||
'@emnapi/core': 1.8.1
|
||||
@ -7387,7 +7588,7 @@ snapshots:
|
||||
|
||||
acorn@8.15.0: {}
|
||||
|
||||
adm-zip@0.5.18:
|
||||
adm-zip@0.6.0:
|
||||
optional: true
|
||||
|
||||
agent-base@6.0.2:
|
||||
@ -7585,10 +7786,10 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
axios@1.18.1:
|
||||
axios@1.19.0:
|
||||
dependencies:
|
||||
follow-redirects: 1.16.0
|
||||
form-data: 4.0.5
|
||||
form-data: 4.0.6
|
||||
https-proxy-agent: 5.0.1
|
||||
proxy-from-env: 2.1.0
|
||||
transitivePeerDependencies:
|
||||
@ -7874,11 +8075,11 @@ snapshots:
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
chromedriver@151.0.2:
|
||||
chromedriver@152.0.0:
|
||||
dependencies:
|
||||
'@testim/chrome-version': 1.1.4
|
||||
adm-zip: 0.5.18
|
||||
axios: 1.18.1
|
||||
adm-zip: 0.6.0
|
||||
axios: 1.19.0
|
||||
compare-versions: 6.1.1
|
||||
proxy-agent: 8.0.2
|
||||
proxy-from-env: 2.1.0
|
||||
@ -8048,7 +8249,7 @@ snapshots:
|
||||
|
||||
date-format@4.0.14: {}
|
||||
|
||||
dayjs@1.11.19: {}
|
||||
dayjs@1.11.23: {}
|
||||
|
||||
debug@2.6.9:
|
||||
dependencies:
|
||||
@ -8128,6 +8329,24 @@ snapshots:
|
||||
dependencies:
|
||||
esutils: 2.0.3
|
||||
|
||||
dom-serializer@1.4.1:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 4.3.1
|
||||
entities: 2.2.0
|
||||
|
||||
domelementtype@2.3.0: {}
|
||||
|
||||
domhandler@4.3.1:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
|
||||
domutils@2.8.0:
|
||||
dependencies:
|
||||
dom-serializer: 1.4.1
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 4.3.1
|
||||
|
||||
dotenv@17.2.3: {}
|
||||
|
||||
dottie@2.0.6: {}
|
||||
@ -8157,6 +8376,10 @@ snapshots:
|
||||
|
||||
ee-first@1.1.1: {}
|
||||
|
||||
ejs@3.1.10:
|
||||
dependencies:
|
||||
jake: 10.9.4
|
||||
|
||||
electron-to-chromium@1.5.283: {}
|
||||
|
||||
elliptic@6.6.1:
|
||||
@ -8181,6 +8404,10 @@ snapshots:
|
||||
dependencies:
|
||||
once: 1.4.0
|
||||
|
||||
entities@2.2.0: {}
|
||||
|
||||
entities@3.0.1: {}
|
||||
|
||||
env-paths@2.2.1: {}
|
||||
|
||||
error-ex@1.3.4:
|
||||
@ -8565,7 +8792,7 @@ snapshots:
|
||||
exceljs@4.4.0:
|
||||
dependencies:
|
||||
archiver: 5.3.2
|
||||
dayjs: 1.11.19
|
||||
dayjs: 1.11.23
|
||||
fast-csv: 4.3.6
|
||||
jszip: 3.10.1
|
||||
readable-stream: 3.6.2
|
||||
@ -8696,6 +8923,10 @@ snapshots:
|
||||
dependencies:
|
||||
flat-cache: 4.0.1
|
||||
|
||||
filelist@1.0.6:
|
||||
dependencies:
|
||||
minimatch: 5.1.9
|
||||
|
||||
fill-range@7.1.1:
|
||||
dependencies:
|
||||
to-regex-range: 5.0.1
|
||||
@ -8766,6 +8997,15 @@ snapshots:
|
||||
hasown: 2.0.2
|
||||
mime-types: 2.1.35
|
||||
|
||||
form-data@4.0.6:
|
||||
dependencies:
|
||||
asynckit: 0.4.0
|
||||
combined-stream: 1.0.8
|
||||
es-set-tostringtag: 2.1.0
|
||||
hasown: 2.0.4
|
||||
mime-types: 2.1.35
|
||||
optional: true
|
||||
|
||||
formidable@3.5.4:
|
||||
dependencies:
|
||||
'@paralleldrive/cuid2': 2.3.1
|
||||
@ -8947,6 +9187,11 @@ snapshots:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
hasown@2.0.4:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
optional: true
|
||||
|
||||
hmac-drbg@1.0.1:
|
||||
dependencies:
|
||||
hash.js: 1.1.7
|
||||
@ -8959,6 +9204,13 @@ snapshots:
|
||||
|
||||
html-escaper@2.0.2: {}
|
||||
|
||||
htmlparser2@7.2.0:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 4.3.1
|
||||
domutils: 2.8.0
|
||||
entities: 3.0.1
|
||||
|
||||
http-errors@2.0.1:
|
||||
dependencies:
|
||||
depd: 2.0.0
|
||||
@ -9152,6 +9404,8 @@ snapshots:
|
||||
dependencies:
|
||||
is-extglob: 2.1.1
|
||||
|
||||
is-json@2.0.1: {}
|
||||
|
||||
is-map@2.0.3: {}
|
||||
|
||||
is-negative-zero@2.0.3: {}
|
||||
@ -9276,6 +9530,12 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@pkgjs/parseargs': 0.11.0
|
||||
|
||||
jake@10.9.4:
|
||||
dependencies:
|
||||
async: 3.2.6
|
||||
filelist: 1.0.6
|
||||
picocolors: 1.1.1
|
||||
|
||||
jest-changed-files@30.2.0:
|
||||
dependencies:
|
||||
execa: 5.1.1
|
||||
@ -9693,7 +9953,7 @@ snapshots:
|
||||
dependencies:
|
||||
jwk-to-pem: 2.0.7
|
||||
optionalDependencies:
|
||||
chromedriver: 151.0.2
|
||||
chromedriver: 152.0.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
- kerberos
|
||||
@ -9987,6 +10247,9 @@ snapshots:
|
||||
|
||||
node-int64@0.4.0: {}
|
||||
|
||||
node-readable-to-web-readable-stream@0.4.2:
|
||||
optional: true
|
||||
|
||||
node-releases@2.0.27: {}
|
||||
|
||||
nodemailer@7.0.13: {}
|
||||
@ -10198,6 +10461,15 @@ snapshots:
|
||||
|
||||
path-to-regexp@8.3.0: {}
|
||||
|
||||
pdf-to-img@6.2.0:
|
||||
dependencies:
|
||||
pdfjs-dist: 5.6.205
|
||||
|
||||
pdfjs-dist@5.6.205:
|
||||
optionalDependencies:
|
||||
'@napi-rs/canvas': 0.1.100
|
||||
node-readable-to-web-readable-stream: 0.4.2
|
||||
|
||||
pend@1.2.0: {}
|
||||
|
||||
pg-cloudflare@1.3.0:
|
||||
@ -10270,6 +10542,19 @@ snapshots:
|
||||
dependencies:
|
||||
xtend: 4.0.2
|
||||
|
||||
posthtml-parser@0.11.0:
|
||||
dependencies:
|
||||
htmlparser2: 7.2.0
|
||||
|
||||
posthtml-render@3.0.0:
|
||||
dependencies:
|
||||
is-json: 2.0.1
|
||||
|
||||
posthtml@0.16.7:
|
||||
dependencies:
|
||||
posthtml-parser: 0.11.0
|
||||
posthtml-render: 3.0.0
|
||||
|
||||
prelude-ls@1.2.1: {}
|
||||
|
||||
prettier-linter-helpers@1.0.1:
|
||||
|
||||
@ -72,6 +72,7 @@ import { populateUserMiddleware } from './services/misc/auth.js';
|
||||
import { natsServer } from './database/nats.js';
|
||||
import { initializeBuckets } from './database/ceph.js';
|
||||
import { getEnvironment } from './config.js';
|
||||
import { startPDFBrowser } from './templates/pdffactory.js';
|
||||
|
||||
const PORT = config.server.port;
|
||||
const app = express();
|
||||
@ -125,6 +126,12 @@ async function initializeApp() {
|
||||
// Don't throw error - allow app to start without Ceph for development
|
||||
}
|
||||
|
||||
try {
|
||||
await startPDFBrowser();
|
||||
} catch (err) {
|
||||
logger.error('Failed to start persistent Puppeteer browser:', err);
|
||||
}
|
||||
|
||||
// Start server
|
||||
app.listen(PORT, () => logger.info(`Server listening to port ${PORT}`));
|
||||
logger.info(`Allowed origins: ${allowedOrigins.join(', ')}`);
|
||||
|
||||
@ -41,6 +41,8 @@ import {
|
||||
getDocumentTemplatePropertyValuesRouteHandler,
|
||||
searchDocumentTemplatesRouteHandler,
|
||||
getDocumentTemplateNeighborsRouteHandler,
|
||||
previewDocumentTemplateRouteHandler,
|
||||
downloadDocumentTemplateRouteHandler,
|
||||
} from '../../services/management/documenttemplates.js';
|
||||
|
||||
// list of document templates
|
||||
@ -90,6 +92,14 @@ router.get('/neighbors', isAuthenticated, async (req, res) => {
|
||||
getDocumentTemplateNeighborsRouteHandler(req, res, property, filter, search, getSort(sort, listAllowedSorters), order, id);
|
||||
});
|
||||
|
||||
router.post('/:id/preview', isAuthenticated, async (req, res) => {
|
||||
previewDocumentTemplateRouteHandler(req, res);
|
||||
});
|
||||
|
||||
router.post('/:id/download', isAuthenticated, async (req, res) => {
|
||||
downloadDocumentTemplateRouteHandler(req, res);
|
||||
});
|
||||
|
||||
router.get('/:id', isAuthenticated, async (req, res) => {
|
||||
getDocumentTemplateRouteHandler(req, res);
|
||||
});
|
||||
|
||||
@ -18,6 +18,10 @@ jest.unstable_mockModule('../../../database/schemas/management/documentjob.schem
|
||||
documentJobModel: { modelName: 'DocumentJob' },
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('../../../utils.js', () => ({
|
||||
createDocumentJobUserNotifier: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('log4js', () => ({
|
||||
default: {
|
||||
getLogger: () => ({
|
||||
@ -37,6 +41,7 @@ const {
|
||||
} = await import('../documentjobs.js');
|
||||
|
||||
const { listObjects, getObject, newObject } = await import('../../../database/database.js');
|
||||
const { createDocumentJobUserNotifier } = await import('../../../utils.js');
|
||||
const { documentJobModel } = await import(
|
||||
'../../../database/schemas/management/documentjob.schema.js'
|
||||
);
|
||||
@ -81,6 +86,7 @@ describe('Document Job Service Route Handlers', () => {
|
||||
await newDocumentJobRouteHandler(req, res);
|
||||
|
||||
expect(newObject).toHaveBeenCalled();
|
||||
expect(createDocumentJobUserNotifier).toHaveBeenCalledWith('456', req.user);
|
||||
expect(res.send).toHaveBeenCalledWith(mockJob);
|
||||
});
|
||||
});
|
||||
|
||||
@ -18,6 +18,13 @@ jest.unstable_mockModule('../../../database/schemas/management/documenttemplate.
|
||||
documentTemplateModel: { modelName: 'DocumentTemplate' },
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('../../../templates/templatemanager.js', () => ({
|
||||
templateManager: {
|
||||
renderTemplate: jest.fn(),
|
||||
renderDownload: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('log4js', () => ({
|
||||
default: {
|
||||
getLogger: () => ({
|
||||
@ -35,6 +42,8 @@ const {
|
||||
getDocumentTemplateRouteHandler,
|
||||
newDocumentTemplateRouteHandler,
|
||||
editDocumentTemplateRouteHandler,
|
||||
previewDocumentTemplateRouteHandler,
|
||||
downloadDocumentTemplateRouteHandler,
|
||||
} = await import('../documenttemplates.js');
|
||||
|
||||
const { listObjects, getObject, editObject, newObject } = await import(
|
||||
@ -43,6 +52,7 @@ const { listObjects, getObject, editObject, newObject } = await import(
|
||||
const { documentTemplateModel } = await import(
|
||||
'../../../database/schemas/management/documenttemplate.schema.js'
|
||||
);
|
||||
const { templateManager } = await import('../../../templates/templatemanager.js');
|
||||
|
||||
describe('Document Template Service Route Handlers', () => {
|
||||
let req, res;
|
||||
@ -57,6 +67,7 @@ describe('Document Template Service Route Handlers', () => {
|
||||
res = {
|
||||
send: jest.fn(),
|
||||
status: jest.fn().mockReturnThis(),
|
||||
set: jest.fn(),
|
||||
};
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
@ -87,5 +98,91 @@ describe('Document Template Service Route Handlers', () => {
|
||||
expect(res.send).toHaveBeenCalledWith(mockTemplate);
|
||||
});
|
||||
});
|
||||
|
||||
describe('previewDocumentTemplateRouteHandler', () => {
|
||||
it('should preview a document template', async () => {
|
||||
req.params.id = 'template-1';
|
||||
req.body = { content: '<Text>Hello</Text>', testObject: { name: 'Test' }, scale: 1 };
|
||||
const mockPreview = { html: '<html></html>', width: 100, height: 50 };
|
||||
templateManager.renderTemplate.mockResolvedValue(mockPreview);
|
||||
|
||||
await previewDocumentTemplateRouteHandler(req, res);
|
||||
|
||||
expect(templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||
'template-1',
|
||||
'<Text>Hello</Text>',
|
||||
{ name: 'Test' },
|
||||
1,
|
||||
{},
|
||||
true
|
||||
);
|
||||
expect(res.send).toHaveBeenCalledWith(mockPreview);
|
||||
});
|
||||
|
||||
it('should return an error when preview fails', async () => {
|
||||
req.params.id = 'template-1';
|
||||
templateManager.renderTemplate.mockResolvedValue({
|
||||
error: 'Document template not found.',
|
||||
code: 404,
|
||||
});
|
||||
|
||||
await previewDocumentTemplateRouteHandler(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(404);
|
||||
expect(res.send).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ error: 'Document template not found.' })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('downloadDocumentTemplateRouteHandler', () => {
|
||||
it('should download a single PDF', async () => {
|
||||
req.params.id = 'template-1';
|
||||
req.query = { type: 'pdf' };
|
||||
req.body = { content: '<Text>Hello</Text>', object: { name: 'Test' } };
|
||||
const pdf = Buffer.from('pdf-data');
|
||||
templateManager.renderDownload.mockResolvedValue({
|
||||
type: 'pdf',
|
||||
mime: 'application/pdf',
|
||||
extension: 'pdf',
|
||||
buffers: [pdf],
|
||||
});
|
||||
|
||||
await downloadDocumentTemplateRouteHandler(req, res);
|
||||
|
||||
expect(templateManager.renderDownload).toHaveBeenCalledWith(
|
||||
'template-1',
|
||||
'<Text>Hello</Text>',
|
||||
{ name: 'Test' },
|
||||
'pdf',
|
||||
{}
|
||||
);
|
||||
expect(res.set).toHaveBeenCalledWith('Content-Type', 'application/pdf');
|
||||
expect(res.send).toHaveBeenCalledWith(pdf);
|
||||
});
|
||||
|
||||
it('should return base64 images when multiple pages are rendered', async () => {
|
||||
req.params.id = 'template-1';
|
||||
req.query = { type: 'png' };
|
||||
req.body = { object: { name: 'Test' } };
|
||||
const pageOne = Buffer.from('page-1');
|
||||
const pageTwo = Buffer.from('page-2');
|
||||
templateManager.renderDownload.mockResolvedValue({
|
||||
type: 'png',
|
||||
mime: 'image/png',
|
||||
extension: 'png',
|
||||
buffers: [pageOne, pageTwo],
|
||||
});
|
||||
|
||||
await downloadDocumentTemplateRouteHandler(req, res);
|
||||
|
||||
expect(res.send).toHaveBeenCalledWith({
|
||||
type: 'png',
|
||||
mime: 'image/png',
|
||||
extension: 'png',
|
||||
images: [pageOne.toString('base64'), pageTwo.toString('base64')],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -15,6 +15,7 @@ import {
|
||||
getPropertyValues,
|
||||
getObjectNeighbors,
|
||||
} from '../../database/database.js';
|
||||
import { createDocumentJobUserNotifier } from '../../utils.js';
|
||||
const logger = log4js.getLogger('Document Jobs');
|
||||
logger.level = config.server.logLevel;
|
||||
|
||||
@ -155,6 +156,8 @@ export const newDocumentJobRouteHandler = async (req, res) => {
|
||||
return res.status(result.code).send(result);
|
||||
}
|
||||
|
||||
await createDocumentJobUserNotifier(result._id, req.user);
|
||||
|
||||
logger.debug(`New document job with ID: ${result._id}`);
|
||||
|
||||
res.send(result);
|
||||
|
||||
@ -15,6 +15,7 @@ import {
|
||||
getPropertyValues,
|
||||
getObjectNeighbors,
|
||||
} from '../../database/database.js';
|
||||
import { templateManager } from '../../templates/templatemanager.js';
|
||||
const logger = log4js.getLogger('Document Templates');
|
||||
logger.level = config.server.logLevel;
|
||||
|
||||
@ -262,3 +263,73 @@ export const getDocumentTemplateNeighborsRouteHandler = async (
|
||||
res.send(result);
|
||||
};
|
||||
|
||||
export const previewDocumentTemplateRouteHandler = async (req, res) => {
|
||||
const id = req.params.id;
|
||||
const content = req.body?.content;
|
||||
const testObject = req.body?.testObject || req.body?.object || {};
|
||||
const scale = req.body?.scale ?? 1;
|
||||
|
||||
logger.debug(`Previewing document template with ID: ${id}`);
|
||||
|
||||
const result = await templateManager.renderTemplate(
|
||||
id,
|
||||
content,
|
||||
testObject,
|
||||
scale,
|
||||
{},
|
||||
true
|
||||
);
|
||||
|
||||
if (result?.error) {
|
||||
logger.warn('Error previewing document template:', result.error);
|
||||
return res.status(result.code || 400).send(result);
|
||||
}
|
||||
|
||||
res.send(result);
|
||||
};
|
||||
|
||||
export const downloadDocumentTemplateRouteHandler = async (req, res) => {
|
||||
const id = req.params.id;
|
||||
const type = req.query.type || req.body?.type || 'pdf';
|
||||
const content = req.body?.content;
|
||||
const data = req.body?.object || req.body?.testObject || {};
|
||||
const width = req.body?.width || req.query.width;
|
||||
const filename = req.body?.filename || req.query.filename || 'document';
|
||||
const options = {};
|
||||
if (width) {
|
||||
options.width = Number(width);
|
||||
}
|
||||
|
||||
logger.debug(`Downloading document template ${id} as ${type}`);
|
||||
|
||||
const result = await templateManager.renderDownload(id, content, data, type, options);
|
||||
|
||||
if (result?.error) {
|
||||
logger.warn('Error downloading document template:', result.error);
|
||||
return res.status(result.code || 400).send(result);
|
||||
}
|
||||
|
||||
const buffers = result.buffers || [];
|
||||
if (buffers.length === 0) {
|
||||
return res.status(500).send({ error: 'Failed to render document template.' });
|
||||
}
|
||||
|
||||
if (buffers.length === 1) {
|
||||
const buffer = buffers[0];
|
||||
res.set('Content-Type', result.mime);
|
||||
res.set(
|
||||
'Content-Disposition',
|
||||
`attachment; filename="${filename}.${result.extension}"`
|
||||
);
|
||||
res.set('Content-Length', String(buffer.length));
|
||||
return res.send(buffer);
|
||||
}
|
||||
|
||||
res.send({
|
||||
type: result.type,
|
||||
mime: result.mime,
|
||||
extension: result.extension,
|
||||
images: buffers.map((buffer) => Buffer.from(buffer).toString('base64')),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
108
src/templates/__tests__/pdffactory.test.js
Normal file
108
src/templates/__tests__/pdffactory.test.js
Normal file
@ -0,0 +1,108 @@
|
||||
import { jest } from '@jest/globals';
|
||||
|
||||
const page = {
|
||||
setContent: jest.fn().mockResolvedValue(undefined),
|
||||
evaluate: jest.fn().mockResolvedValue(120),
|
||||
pdf: jest.fn().mockResolvedValue(Buffer.from('pdf-bytes')),
|
||||
close: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
const browserInstance = {
|
||||
connected: true,
|
||||
newPage: jest.fn().mockResolvedValue(page),
|
||||
close: jest.fn().mockResolvedValue(undefined),
|
||||
on: jest.fn(),
|
||||
};
|
||||
|
||||
const puppeteer = {
|
||||
launch: jest.fn().mockResolvedValue(browserInstance),
|
||||
};
|
||||
|
||||
jest.unstable_mockModule('puppeteer', () => ({
|
||||
default: puppeteer,
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('../../config.js', () => ({
|
||||
default: {
|
||||
server: {
|
||||
logLevel: 'info',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('log4js', () => ({
|
||||
default: {
|
||||
getLogger: () => ({
|
||||
level: 'info',
|
||||
debug: jest.fn(),
|
||||
error: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
info: jest.fn(),
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
const { generatePDF, startPDFBrowser, closePDFBrowser } = await import('../pdffactory.js');
|
||||
|
||||
describe('PDF Factory', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
browserInstance.connected = true;
|
||||
browserInstance.newPage.mockResolvedValue(page);
|
||||
puppeteer.launch.mockResolvedValue(browserInstance);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await closePDFBrowser();
|
||||
});
|
||||
|
||||
it('launches puppeteer once and reuses it across renders', async () => {
|
||||
await generatePDF('<html></html>', { width: 100, height: 50 });
|
||||
await generatePDF('<html></html>', { width: 100, height: 50 });
|
||||
|
||||
expect(puppeteer.launch).toHaveBeenCalledTimes(1);
|
||||
expect(browserInstance.newPage).toHaveBeenCalledTimes(2);
|
||||
expect(page.close).toHaveBeenCalledTimes(2);
|
||||
expect(browserInstance.close).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('shares a single in-flight launch across concurrent renders', async () => {
|
||||
let resolveLaunch;
|
||||
puppeteer.launch.mockImplementation(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
resolveLaunch = () => resolve(browserInstance);
|
||||
})
|
||||
);
|
||||
|
||||
const first = generatePDF('<html></html>', { width: 100, height: 50 });
|
||||
const second = generatePDF('<html></html>', { width: 100, height: 50 });
|
||||
|
||||
await Promise.resolve();
|
||||
resolveLaunch();
|
||||
|
||||
await Promise.all([first, second]);
|
||||
|
||||
expect(puppeteer.launch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('measures auto height from the content element', async () => {
|
||||
await generatePDF('<html></html>', { width: 80, height: 'auto' });
|
||||
|
||||
expect(page.evaluate).toHaveBeenCalled();
|
||||
expect(page.pdf).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
width: '80mm',
|
||||
height: '120px',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('can start the browser before the first render', async () => {
|
||||
await startPDFBrowser();
|
||||
expect(puppeteer.launch).toHaveBeenCalledTimes(1);
|
||||
|
||||
await generatePDF('<html></html>', { width: 100, height: 50 });
|
||||
expect(puppeteer.launch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
193
src/templates/__tests__/templatemanager.test.js
Normal file
193
src/templates/__tests__/templatemanager.test.js
Normal file
@ -0,0 +1,193 @@
|
||||
import { jest } from '@jest/globals';
|
||||
|
||||
jest.unstable_mockModule('fs', () => ({
|
||||
default: {
|
||||
readFileSync: jest.fn((filePath) => {
|
||||
if (filePath.endsWith('basetemplate.ejs')) return '<html><%- content %></html>';
|
||||
if (filePath.endsWith('styles.css')) return 'body { color: red; }';
|
||||
if (filePath.endsWith('previewtemplate.ejs'))
|
||||
return '<div class="preview"><%- content %></div>';
|
||||
if (filePath.endsWith('rendertemplate.ejs'))
|
||||
return '<div class="render"><%- content %></div>';
|
||||
if (filePath.endsWith('contentplaceholder.ejs'))
|
||||
return '<div class="placeholder"></div>';
|
||||
if (filePath.endsWith('previewpagination.js')) return 'console.log("pagination");';
|
||||
return '';
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('ejs', () => ({
|
||||
default: {
|
||||
render: jest.fn(async (content) => `rendered: ${content}`),
|
||||
compile: jest.fn(() => jest.fn()),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('posthtml', () => ({
|
||||
default: jest.fn(() => ({
|
||||
process: jest.fn(async (content) => ({ html: `transformed: ${content}` })),
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('../../database/database.js', () => ({
|
||||
getObject: jest.fn(),
|
||||
listObjects: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('../../database/schemas/management/documenttemplate.schema.js', () => ({
|
||||
documentTemplateModel: { modelName: 'DocumentTemplate' },
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('../../database/schemas/management/documentsize.schema.js', () => ({}));
|
||||
|
||||
jest.unstable_mockModule('../../services/misc/model.js', () => ({
|
||||
getModelByName: jest.fn(() => ({
|
||||
model: {
|
||||
schema: {
|
||||
obj: { name: {}, status: {} },
|
||||
},
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('../pdffactory.js', () => ({
|
||||
generatePDF: jest.fn().mockResolvedValue(Buffer.from('pdf-data')),
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('../pdfUtils.js', () => ({
|
||||
convertPDFToImage: jest.fn().mockResolvedValue([Buffer.from('image-data')]),
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('sharp', () => {
|
||||
const sharpInstance = {
|
||||
png: jest.fn().mockReturnThis(),
|
||||
jpeg: jest.fn().mockReturnThis(),
|
||||
toBuffer: jest.fn().mockResolvedValue(Buffer.from('converted-image')),
|
||||
metadata: jest.fn().mockResolvedValue({ width: 100, height: 50 }),
|
||||
};
|
||||
const sharp = jest.fn(() => sharpInstance);
|
||||
return { default: sharp };
|
||||
});
|
||||
|
||||
jest.unstable_mockModule('log4js', () => ({
|
||||
default: {
|
||||
getLogger: () => ({
|
||||
level: 'info',
|
||||
debug: jest.fn(),
|
||||
error: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
trace: jest.fn(),
|
||||
info: jest.fn(),
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('../../config.js', () => ({
|
||||
default: {
|
||||
server: {
|
||||
logLevel: 'info',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const { TemplateManager } = await import('../templatemanager.js');
|
||||
const { getObject } = await import('../../database/database.js');
|
||||
const ejs = (await import('ejs')).default;
|
||||
const { generatePDF } = await import('../pdffactory.js');
|
||||
|
||||
describe('TemplateManager', () => {
|
||||
let templateManager;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
templateManager = new TemplateManager();
|
||||
});
|
||||
|
||||
describe('renderTemplate', () => {
|
||||
it('should render a template successfully', async () => {
|
||||
const mockTemplate = {
|
||||
documentSize: { width: 100, height: 100, infiniteHeight: false },
|
||||
global: false,
|
||||
objectType: 'printer',
|
||||
};
|
||||
|
||||
getObject.mockResolvedValue(mockTemplate);
|
||||
|
||||
const result = await templateManager.renderTemplate('temp-id', 'some content', {
|
||||
name: 'Test',
|
||||
});
|
||||
|
||||
expect(getObject).toHaveBeenCalledWith(expect.objectContaining({ id: 'temp-id' }));
|
||||
expect(ejs.render).toHaveBeenCalled();
|
||||
expect(result).toHaveProperty('html');
|
||||
expect(result.width).toBe(100);
|
||||
expect(result.height).toBe(100);
|
||||
});
|
||||
|
||||
it('should return error if template not found', async () => {
|
||||
getObject.mockResolvedValue(null);
|
||||
|
||||
const result = await templateManager.renderTemplate('invalid', 'content');
|
||||
|
||||
expect(result).toEqual({ error: 'Document template not found.', code: 404 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateTemplate', () => {
|
||||
it('should return true for valid EJS', () => {
|
||||
expect(templateManager.validateTemplate('<div><%= name %></div>')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for invalid EJS', () => {
|
||||
ejs.compile.mockImplementationOnce(() => {
|
||||
throw new Error('syntax error');
|
||||
});
|
||||
expect(templateManager.validateTemplate('<% invalid %>')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderPDF', () => {
|
||||
it('should render a PDF successfully', async () => {
|
||||
const mockTemplate = {
|
||||
documentSize: { width: 100, height: 100, infiniteHeight: false },
|
||||
global: false,
|
||||
objectType: 'printer',
|
||||
};
|
||||
getObject.mockResolvedValue(mockTemplate);
|
||||
|
||||
const result = await templateManager.renderPDF('temp-id', 'content');
|
||||
|
||||
expect(generatePDF).toHaveBeenCalled();
|
||||
expect(result.pdf).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderDownload', () => {
|
||||
it('should reject unsupported types', async () => {
|
||||
const result = await templateManager.renderDownload('temp-id', 'content', {}, 'gif');
|
||||
expect(result.error).toMatch(/Unsupported download type/);
|
||||
expect(result.code).toBe(400);
|
||||
});
|
||||
|
||||
it('should render a PDF download', async () => {
|
||||
getObject.mockResolvedValue({
|
||||
documentSize: { width: 100, height: 100, infiniteHeight: false },
|
||||
global: false,
|
||||
objectType: 'printer',
|
||||
});
|
||||
|
||||
const result = await templateManager.renderDownload('temp-id', 'content', {}, 'pdf');
|
||||
expect(result.type).toBe('pdf');
|
||||
expect(result.buffers).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatDate', () => {
|
||||
it('should format date correctly', () => {
|
||||
const date = new Date('2023-01-01T12:00:00Z');
|
||||
const formatted = templateManager.formatDate(date, 'YYYY-MM-DD');
|
||||
expect(formatted).toBe('2023-01-01');
|
||||
});
|
||||
});
|
||||
});
|
||||
49
src/templates/assets/basetemplate.ejs
Normal file
49
src/templates/assets/basetemplate.ejs
Normal file
@ -0,0 +1,49 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="initial-scale=1.0" />
|
||||
<title>Document</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Figtree:ital,wght@0,300..900;1,300..900&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<style>
|
||||
<%- baseCSS %>
|
||||
</style>
|
||||
<style>
|
||||
body {
|
||||
}
|
||||
|
||||
.previewWrapper {
|
||||
width: <%= (scaledWidth) || '50mm' %>;
|
||||
height: <%= (scaledHeight) || '50mm' %>;
|
||||
}
|
||||
.previewDocument {
|
||||
width: <%= (width) || '50mm' %>;
|
||||
height: <%= (height) || '50mm' %>;
|
||||
transform: scale(<%= scale || '1' %>);
|
||||
transform-origin: top left;
|
||||
}
|
||||
.renderDocument {
|
||||
width: <%= (scaledWidth) || '50mm' %>;
|
||||
height: <%= (scaledHeight) || '50mm' %>;
|
||||
transform: scale(<%= scale || '1' %>);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<%- content %>
|
||||
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.0/dist/JsBarcode.all.min.js"></script>
|
||||
<script>
|
||||
JsBarcode('.documentBarcode').init();
|
||||
</script>
|
||||
<% if (typeof previewPaginationScript !== 'undefined' && previewPaginationScript) { %>
|
||||
<script>
|
||||
<%- previewPaginationScript %>
|
||||
</script>
|
||||
<% } %>
|
||||
</body>
|
||||
</html>
|
||||
3
src/templates/assets/contentplaceholder.ejs
Normal file
3
src/templates/assets/contentplaceholder.ejs
Normal file
@ -0,0 +1,3 @@
|
||||
<div class="contentPlaceholder">
|
||||
<p>Content</p>
|
||||
</div>
|
||||
0
src/templates/assets/previewpagination.js
Normal file
0
src/templates/assets/previewpagination.js
Normal file
5
src/templates/assets/previewtemplate.ejs
Normal file
5
src/templates/assets/previewtemplate.ejs
Normal file
@ -0,0 +1,5 @@
|
||||
<div class="previewContainer">
|
||||
<div class="previewWrapper">
|
||||
<div class="previewDocument" id="content"><%- content %></div>
|
||||
</div>
|
||||
</div>
|
||||
1
src/templates/assets/rendertemplate.ejs
Normal file
1
src/templates/assets/rendertemplate.ejs
Normal file
@ -0,0 +1 @@
|
||||
<div class="renderDocument" id="content"><%- content %></div>
|
||||
131
src/templates/assets/styles.css
Normal file
131
src/templates/assets/styles.css
Normal file
@ -0,0 +1,131 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Figtree', sans-serif;
|
||||
font-optical-sizing: auto;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.previewContainer {
|
||||
display: flex;
|
||||
justify-content: center; /* Horizontal center */
|
||||
align-items: center; /* Vertical center */
|
||||
padding: 60px;
|
||||
box-sizing: border-box;
|
||||
width: fit-content;
|
||||
}
|
||||
.previewWrapper {
|
||||
position: relative;
|
||||
}
|
||||
.previewDocument {
|
||||
background: #ffffff;
|
||||
border: 1px solid #000;
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
|
||||
transform-origin: top left;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.documentText {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
.documentTitle {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1.documentTitle {
|
||||
font-weight: 800;
|
||||
font-size: 34px;
|
||||
}
|
||||
|
||||
h2.documentTitle {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
h3.documentTitle {
|
||||
font-weight: 700;
|
||||
}
|
||||
h4.documentTitle {
|
||||
font-weight: 700;
|
||||
}
|
||||
.documentFlex {
|
||||
display: flex;
|
||||
}
|
||||
.documentDivider {
|
||||
background: black;
|
||||
height: 1px;
|
||||
margin: 4px 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.contentPlaceholder {
|
||||
border: 1px solid black;
|
||||
max-height: 250px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: repeating-linear-gradient(
|
||||
45deg,
|
||||
/* Angle of the stripes */ #ccc,
|
||||
/* Light grey */ #ccc 10px,
|
||||
/* End of first stripe */ #eee 10px,
|
||||
/* Start of next stripe (slightly lighter grey) */ #eee 20px
|
||||
/* End of second stripe */
|
||||
);
|
||||
}
|
||||
|
||||
.contentPlaceholder > p {
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.documentBarcode {
|
||||
width: auto !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.documentProgressBar {
|
||||
height: 8px;
|
||||
width: 260px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #000000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.documentProgressBarInner {
|
||||
height: 100%;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.documentTable {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border: 1px solid #000000;
|
||||
}
|
||||
|
||||
.documentTableRow {
|
||||
border: 1px solid #000000;
|
||||
}
|
||||
|
||||
.documentTableRow td,
|
||||
.documentTableRowHeader th,
|
||||
.documentTableRowFooter td {
|
||||
padding: 4px;
|
||||
border: 1px solid #000000;
|
||||
}
|
||||
|
||||
.documentTableRowHeader {
|
||||
background: #0000002e;
|
||||
text-align: left;
|
||||
border: 1px solid #000000;
|
||||
}
|
||||
|
||||
.documentTableRowFooter {
|
||||
background: #0000002e;
|
||||
border: 1px solid #000000;
|
||||
}
|
||||
78
src/templates/pdfUtils.js
Normal file
78
src/templates/pdfUtils.js
Normal file
@ -0,0 +1,78 @@
|
||||
/**
|
||||
* PDF conversion utilities loaded on demand so startup
|
||||
* does not require ESM-only pdf-to-img at module load time.
|
||||
*/
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
async function importPackage(name) {
|
||||
const packagePath = require.resolve(name);
|
||||
return import(pathToFileURL(packagePath).href);
|
||||
}
|
||||
|
||||
function resolveDefault(module) {
|
||||
return module?.default ?? module;
|
||||
}
|
||||
|
||||
export async function convertPDFToImage(pdfInput, options = {}) {
|
||||
const [pdfToImgModule, sharpModule] = await Promise.all([
|
||||
importPackage('pdf-to-img'),
|
||||
importPackage('sharp')
|
||||
]);
|
||||
const { pdf } = pdfToImgModule;
|
||||
const sharp = resolveDefault(sharpModule);
|
||||
|
||||
try {
|
||||
const { width, height, page_numbers, scale = 2, ...pdfOnlyOptions } =
|
||||
options;
|
||||
|
||||
const pdfOptions = {
|
||||
scale,
|
||||
...pdfOnlyOptions
|
||||
};
|
||||
|
||||
const document = await pdf(pdfInput, pdfOptions);
|
||||
const outputImages = [];
|
||||
|
||||
if (
|
||||
page_numbers &&
|
||||
Array.isArray(page_numbers) &&
|
||||
page_numbers.length > 0
|
||||
) {
|
||||
for (const pageNum of page_numbers) {
|
||||
let image = await document.getPage(pageNum);
|
||||
|
||||
if (width || height) {
|
||||
const resizeOptions = {};
|
||||
if (width) resizeOptions.width = width;
|
||||
if (height) resizeOptions.height = height;
|
||||
image = await sharp(image).resize(resizeOptions).toBuffer();
|
||||
}
|
||||
|
||||
outputImages.push(image);
|
||||
}
|
||||
} else {
|
||||
for await (const image of document) {
|
||||
let processedImage = image;
|
||||
|
||||
if (width || height) {
|
||||
const resizeOptions = {};
|
||||
if (width) resizeOptions.width = width;
|
||||
if (height) resizeOptions.height = height;
|
||||
processedImage = await sharp(image)
|
||||
.resize(resizeOptions)
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
outputImages.push(processedImage);
|
||||
}
|
||||
}
|
||||
|
||||
return outputImages;
|
||||
} catch (error) {
|
||||
console.error('Error converting PDF to image:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
149
src/templates/pdffactory.js
Normal file
149
src/templates/pdffactory.js
Normal file
@ -0,0 +1,149 @@
|
||||
import log4js from 'log4js';
|
||||
import puppeteer from 'puppeteer';
|
||||
import config from '../config.js';
|
||||
|
||||
const logger = log4js.getLogger('PDF Factory');
|
||||
logger.level = config.server.logLevel;
|
||||
|
||||
const LAUNCH_OPTIONS = {
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
};
|
||||
|
||||
let browser = null;
|
||||
let launchPromise = null;
|
||||
let shutdownRegistered = false;
|
||||
|
||||
function isBrowserConnected() {
|
||||
return browser != null && browser.connected;
|
||||
}
|
||||
|
||||
async function launchBrowser() {
|
||||
logger.info('Launching persistent Puppeteer browser...');
|
||||
const instance = await puppeteer.launch(LAUNCH_OPTIONS);
|
||||
instance.on('disconnected', () => {
|
||||
if (browser === instance) {
|
||||
logger.warn('Puppeteer browser disconnected');
|
||||
browser = null;
|
||||
launchPromise = null;
|
||||
}
|
||||
});
|
||||
logger.info('Persistent Puppeteer browser ready');
|
||||
return instance;
|
||||
}
|
||||
|
||||
function registerShutdownHandlers() {
|
||||
if (shutdownRegistered) {
|
||||
return;
|
||||
}
|
||||
shutdownRegistered = true;
|
||||
|
||||
const shutdown = async () => {
|
||||
await closePDFBrowser();
|
||||
};
|
||||
|
||||
process.once('SIGINT', shutdown);
|
||||
process.once('SIGTERM', shutdown);
|
||||
process.once('beforeExit', shutdown);
|
||||
}
|
||||
|
||||
export async function startPDFBrowser() {
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
registerShutdownHandlers();
|
||||
}
|
||||
|
||||
if (isBrowserConnected()) {
|
||||
return browser;
|
||||
}
|
||||
|
||||
if (launchPromise == null) {
|
||||
launchPromise = launchBrowser()
|
||||
.then((instance) => {
|
||||
browser = instance;
|
||||
return instance;
|
||||
})
|
||||
.catch((error) => {
|
||||
launchPromise = null;
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
return launchPromise;
|
||||
}
|
||||
|
||||
export async function closePDFBrowser() {
|
||||
const instance = browser;
|
||||
browser = null;
|
||||
launchPromise = null;
|
||||
if (instance != null) {
|
||||
try {
|
||||
await instance.close();
|
||||
} catch (error) {
|
||||
logger.warn('Error closing Puppeteer browser:', error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function withPage(run) {
|
||||
let page;
|
||||
try {
|
||||
const instance = await startPDFBrowser();
|
||||
page = await instance.newPage();
|
||||
return await run(page);
|
||||
} catch (error) {
|
||||
if (page == null && isBrowserConnected() === false) {
|
||||
const instance = await startPDFBrowser();
|
||||
page = await instance.newPage();
|
||||
return await run(page);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
if (page != null) {
|
||||
await page.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a PDF from HTML content using a persistent Puppeteer browser
|
||||
* @param {string} html - The HTML content to convert to PDF
|
||||
* @param {Object} options - PDF generation options
|
||||
* @param {number} options.width - Document width in mm
|
||||
* @param {number|string} options.height - Document height in mm, or 'auto'
|
||||
* @returns {Promise<Buffer>} The PDF buffer
|
||||
*/
|
||||
export async function generatePDF(html, options = {}) {
|
||||
try {
|
||||
return await withPage(async (page) => {
|
||||
await page.setContent(html, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
|
||||
var height = options?.height || '50';
|
||||
|
||||
if (options.height == 'auto') {
|
||||
const calculatedHeight = await page.evaluate(() => {
|
||||
return document.getElementById('content').scrollHeight;
|
||||
});
|
||||
|
||||
height = calculatedHeight;
|
||||
}
|
||||
|
||||
return page.pdf({
|
||||
printBackground: true,
|
||||
preferCSSPageSize: true,
|
||||
width: options.width ? `${options.width}mm` : undefined,
|
||||
height: height ? (options.height == 'auto' ? `${height + 1}px` : `${height}mm`) : undefined,
|
||||
margin: {
|
||||
top: '0mm',
|
||||
right: '0mm',
|
||||
bottom: '0mm',
|
||||
left: '0mm',
|
||||
},
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error generating PDF:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
683
src/templates/templatemanager.js
Normal file
683
src/templates/templatemanager.js
Normal file
@ -0,0 +1,683 @@
|
||||
import ejs from 'ejs';
|
||||
import log4js from 'log4js';
|
||||
import posthtml from 'posthtml';
|
||||
import fs from 'fs';
|
||||
import dayjs from 'dayjs';
|
||||
import utc from 'dayjs/plugin/utc.js';
|
||||
import timezone from 'dayjs/plugin/timezone.js';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
import sharp from 'sharp';
|
||||
|
||||
import { documentTemplateModel } from '../database/schemas/management/documenttemplate.schema.js';
|
||||
import '../database/schemas/management/documentsize.schema.js';
|
||||
import config from '../config.js';
|
||||
import { getObject, listObjects } from '../database/database.js';
|
||||
import { getModelByName } from '../services/misc/model.js';
|
||||
import { generatePDF } from './pdffactory.js';
|
||||
import { convertPDFToImage } from './pdfUtils.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
|
||||
const logger = log4js.getLogger('Template Manager');
|
||||
logger.level = config.server.logLevel;
|
||||
|
||||
const DOWNLOAD_TYPES = ['pdf', 'png', 'svg', 'jpeg'];
|
||||
|
||||
let baseTemplate;
|
||||
let baseCSS;
|
||||
let previewTemplate;
|
||||
let renderTemplateEjs;
|
||||
let contentPlaceholder;
|
||||
let previewPaginationScript;
|
||||
|
||||
async function loadTemplates() {
|
||||
baseTemplate = fs.readFileSync(join(__dirname, '/assets/basetemplate.ejs'), 'utf8');
|
||||
baseCSS = fs.readFileSync(join(__dirname, '/assets/styles.css'), 'utf8');
|
||||
previewTemplate = fs.readFileSync(join(__dirname, '/assets/previewtemplate.ejs'), 'utf8');
|
||||
renderTemplateEjs = fs.readFileSync(join(__dirname, '/assets/rendertemplate.ejs'), 'utf8');
|
||||
contentPlaceholder = fs.readFileSync(
|
||||
join(__dirname, '/assets/contentplaceholder.ejs'),
|
||||
'utf8'
|
||||
);
|
||||
previewPaginationScript = fs.readFileSync(
|
||||
join(__dirname, '/assets/previewpagination.js'),
|
||||
'utf8'
|
||||
);
|
||||
}
|
||||
|
||||
loadTemplates();
|
||||
|
||||
function getNodeStyles(attributes) {
|
||||
var styles = '';
|
||||
if (attributes?.padding) {
|
||||
styles += `padding: ${attributes.padding};`;
|
||||
}
|
||||
if (attributes?.width) {
|
||||
styles += `width: ${attributes.width};`;
|
||||
}
|
||||
if (attributes?.height) {
|
||||
styles += `height: ${attributes.height};`;
|
||||
}
|
||||
if (attributes?.maxWidth) {
|
||||
styles += `max-width: ${attributes.maxWidth};`;
|
||||
}
|
||||
if (attributes?.maxHeight) {
|
||||
styles += `max-height: ${attributes.maxHeight};`;
|
||||
}
|
||||
if (attributes?.gap && attributes?.vertical != 'true') {
|
||||
styles += `column-gap: ${attributes.gap};`;
|
||||
}
|
||||
if (attributes?.gap && attributes?.vertical == 'true') {
|
||||
styles += `row-gap: ${attributes.gap};`;
|
||||
}
|
||||
if (attributes?.justify) {
|
||||
styles += `justify-content: ${attributes.justify};`;
|
||||
}
|
||||
if (attributes?.align) {
|
||||
styles += `align-items: ${attributes.align};`;
|
||||
}
|
||||
if (attributes?.border) {
|
||||
styles += `border: ${attributes.border};`;
|
||||
}
|
||||
if (attributes?.borderRadius) {
|
||||
styles += `border-radius: ${attributes.borderRadius};`;
|
||||
}
|
||||
if (attributes?.vertical == 'true') {
|
||||
styles += `flex-direction: column;`;
|
||||
}
|
||||
if (attributes?.grow) {
|
||||
styles += `flex-grow: ${attributes.grow};`;
|
||||
}
|
||||
if (attributes?.shrink) {
|
||||
styles += `flex-shrink: ${attributes.shrink};`;
|
||||
}
|
||||
if (attributes?.color) {
|
||||
styles += `color: ${attributes.color};`;
|
||||
}
|
||||
if (attributes?.background) {
|
||||
styles += `background: ${attributes.background};`;
|
||||
}
|
||||
if (attributes?.scale) {
|
||||
styles += `transform: scale(${attributes.scale});`;
|
||||
}
|
||||
if (attributes?.textAlign) {
|
||||
styles += `text-align: ${attributes.textAlign};`;
|
||||
}
|
||||
if (attributes?.textSize) {
|
||||
styles += `font-size: ${attributes.textSize};`;
|
||||
}
|
||||
if (attributes?.wordWrap) {
|
||||
styles += `word-wrap: ${attributes.wordWrap};`;
|
||||
}
|
||||
return styles;
|
||||
}
|
||||
|
||||
async function transformCustomElements(content) {
|
||||
const result = await posthtml([
|
||||
(tree) =>
|
||||
tree.match({ tag: 'Title1' }, (node) => ({
|
||||
...node,
|
||||
tag: 'h1',
|
||||
attrs: { class: 'documentTitle' },
|
||||
})),
|
||||
(tree) =>
|
||||
tree.match({ tag: 'Title2' }, (node) => ({
|
||||
...node,
|
||||
tag: 'h2',
|
||||
attrs: { class: 'documentTitle' },
|
||||
})),
|
||||
(tree) =>
|
||||
tree.match({ tag: 'Title3' }, (node) => ({
|
||||
...node,
|
||||
tag: 'h3',
|
||||
attrs: { class: 'documentText' },
|
||||
})),
|
||||
(tree) =>
|
||||
tree.match({ tag: 'Title4' }, (node) => ({
|
||||
...node,
|
||||
tag: 'h4',
|
||||
attrs: { class: 'documentText' },
|
||||
})),
|
||||
(tree) =>
|
||||
tree.match({ tag: 'Text' }, (node) => ({
|
||||
...node,
|
||||
tag: 'p',
|
||||
attrs: { class: 'documentText', style: getNodeStyles(node.attrs) },
|
||||
})),
|
||||
(tree) =>
|
||||
tree.match({ tag: 'Bold' }, (node) => ({
|
||||
...node,
|
||||
tag: 'strong',
|
||||
attrs: {
|
||||
class: 'documentBoldText',
|
||||
style: getNodeStyles(node.attrs),
|
||||
},
|
||||
})),
|
||||
(tree) =>
|
||||
tree.match({ tag: 'Barcode' }, (node) => {
|
||||
return {
|
||||
tag: 'div',
|
||||
content: [
|
||||
{
|
||||
tag: 'svg',
|
||||
attrs: {
|
||||
class: 'documentBarcode',
|
||||
'jsbarcode-displayValue': 'false',
|
||||
'jsbarcode-value': node.content.at(0) || '',
|
||||
'jsbarcode-format': node.attrs.format || 'code128',
|
||||
'jsbarcode-width': node.attrs.barcodeWidth || 2,
|
||||
'jsbarcode-margin': 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
attrs: {
|
||||
class: 'documentBarcodeContainer',
|
||||
style: getNodeStyles(node.attrs),
|
||||
},
|
||||
};
|
||||
}),
|
||||
(tree) =>
|
||||
tree.match({ tag: 'Container' }, (node) => ({
|
||||
...node,
|
||||
tag: 'div',
|
||||
attrs: {
|
||||
class: 'documentContainer',
|
||||
style: getNodeStyles(node.attrs),
|
||||
},
|
||||
})),
|
||||
(tree) =>
|
||||
tree.match({ tag: 'Flex' }, (node) => {
|
||||
return {
|
||||
...node,
|
||||
tag: 'div',
|
||||
attrs: {
|
||||
class: 'documentFlex',
|
||||
style: getNodeStyles(node.attrs),
|
||||
},
|
||||
};
|
||||
}),
|
||||
(tree) =>
|
||||
tree.match({ tag: 'Divider' }, (node) => {
|
||||
return {
|
||||
...node,
|
||||
tag: 'hr',
|
||||
attrs: {
|
||||
class: 'documentDivider',
|
||||
style: getNodeStyles(node.attrs),
|
||||
},
|
||||
};
|
||||
}),
|
||||
(tree) =>
|
||||
tree.match({ tag: 'ProgressBar' }, (node) => {
|
||||
return {
|
||||
...node,
|
||||
tag: 'div',
|
||||
attrs: {
|
||||
class: 'documentProgressBar',
|
||||
style: getNodeStyles(node.attrs),
|
||||
},
|
||||
content: [
|
||||
{
|
||||
tag: 'div',
|
||||
attrs: {
|
||||
class: 'documentProgressBarInner',
|
||||
style: `width: ${Math.round((node.content[0] || 0) * 100)}%`,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}),
|
||||
(tree) =>
|
||||
tree.match({ tag: 'DateTime' }, (node) => {
|
||||
const dateTime = dayjs.utc(node.content[0]);
|
||||
return {
|
||||
content: [dateTime.format('YYYY-MM-DD hh:mm:ss')],
|
||||
tag: 'span',
|
||||
attrs: {
|
||||
class: 'documentDateTime',
|
||||
style: getNodeStyles(node.attrs),
|
||||
},
|
||||
};
|
||||
}),
|
||||
(tree) =>
|
||||
tree.match({ tag: 'Table' }, (node) => {
|
||||
return {
|
||||
...node,
|
||||
tag: 'table',
|
||||
attrs: {
|
||||
class: 'documentTable',
|
||||
style: getNodeStyles(node.attrs),
|
||||
},
|
||||
};
|
||||
}),
|
||||
(tree) =>
|
||||
tree.match({ tag: 'Row' }, (node) => {
|
||||
const rowType = node.attrs?.type?.toLowerCase() || '';
|
||||
|
||||
const transformCols = (content) => {
|
||||
if (!Array.isArray(content)) return content;
|
||||
return content.map((child) => {
|
||||
if (typeof child === 'string' || child == null) {
|
||||
return child;
|
||||
}
|
||||
if (child.tag !== 'Col') {
|
||||
return child;
|
||||
}
|
||||
|
||||
const baseAttrs = {
|
||||
...child.attrs,
|
||||
style: getNodeStyles(child.attrs),
|
||||
};
|
||||
|
||||
if (rowType === 'header') {
|
||||
return {
|
||||
...child,
|
||||
tag: 'th',
|
||||
attrs: baseAttrs,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...child,
|
||||
tag: 'td',
|
||||
attrs: baseAttrs,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const content = transformCols(node.content);
|
||||
|
||||
if (rowType === 'header') {
|
||||
return {
|
||||
...node,
|
||||
tag: 'tr',
|
||||
content,
|
||||
attrs: {
|
||||
class: 'documentTableRowHeader',
|
||||
style: getNodeStyles(node.attrs),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (rowType === 'footer') {
|
||||
return {
|
||||
...node,
|
||||
tag: 'tr',
|
||||
content,
|
||||
attrs: {
|
||||
class: 'documentTableRowFooter',
|
||||
style: getNodeStyles(node.attrs),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...node,
|
||||
tag: 'tr',
|
||||
content,
|
||||
attrs: {
|
||||
class: 'documentTableRow',
|
||||
style: getNodeStyles(node.attrs),
|
||||
},
|
||||
};
|
||||
}),
|
||||
]).process(content);
|
||||
|
||||
return result.html;
|
||||
}
|
||||
|
||||
function normalizeDownloadType(type) {
|
||||
const normalized = String(type || 'pdf').toLowerCase();
|
||||
if (normalized === 'jpg') {
|
||||
return 'jpeg';
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function imageToSvg(pngBuffer, widthPx, heightPx, documentWidthMm, documentHeightMm) {
|
||||
const b64 = pngBuffer.toString('base64');
|
||||
const heightAttr =
|
||||
documentHeightMm == 'auto' || documentHeightMm == null
|
||||
? ''
|
||||
: ` height="${documentHeightMm}mm"`;
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="${documentWidthMm}mm"${heightAttr} viewBox="0 0 ${widthPx} ${heightPx}">
|
||||
<image href="data:image/png;base64,${b64}" width="${widthPx}" height="${heightPx}"/>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
export class TemplateManager {
|
||||
constructor() {
|
||||
this.fc = {
|
||||
listObjects: this.listObjects.bind(this),
|
||||
getObject: this.getObject.bind(this),
|
||||
formatDate: this.formatDate.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
async renderTemplate(id, content, data = {}, scale = 1, options = {}, preview = true) {
|
||||
try {
|
||||
const defaultOptions = {
|
||||
async: true,
|
||||
...options,
|
||||
};
|
||||
logger.debug('Rendering template:', id);
|
||||
|
||||
const documentTemplate = await getObject({
|
||||
model: documentTemplateModel,
|
||||
id,
|
||||
populate: [
|
||||
{ path: 'documentSize' },
|
||||
{ path: 'parent', strictPopulate: false },
|
||||
],
|
||||
});
|
||||
|
||||
if (documentTemplate == null || documentTemplate.error) {
|
||||
return { error: documentTemplate?.error || 'Document template not found.', code: 404 };
|
||||
}
|
||||
|
||||
const documentSize = documentTemplate.documentSize;
|
||||
if (documentSize == null) {
|
||||
return { error: 'Document template size not found.', code: 400 };
|
||||
}
|
||||
|
||||
const templateContentSource =
|
||||
content != null && typeof content === 'string' ? content : documentTemplate.content;
|
||||
|
||||
if (templateContentSource == null || typeof templateContentSource !== 'string') {
|
||||
return { error: 'Template content is required and must be a string.', code: 400 };
|
||||
}
|
||||
|
||||
var templateData = {};
|
||||
if (documentTemplate.global == true) {
|
||||
templateData = { content: contentPlaceholder, fc: this.fc };
|
||||
} else {
|
||||
const objectType = documentTemplate?.objectType;
|
||||
const modelEntry = getModelByName(objectType);
|
||||
const model = modelEntry?.model || modelEntry;
|
||||
if (model == null || model.schema == null) {
|
||||
return { error: `Unknown object type: ${objectType}`, code: 400 };
|
||||
}
|
||||
const defaultKeys = Object.keys(model.schema.obj);
|
||||
const defaultValues = {};
|
||||
for (const key of defaultKeys) {
|
||||
defaultValues[key] = null;
|
||||
}
|
||||
templateData = { ...defaultValues, ...data, fc: this.fc };
|
||||
}
|
||||
|
||||
const templateContent = await ejs.render(templateContentSource, templateData, defaultOptions);
|
||||
|
||||
var templateWithParentContent;
|
||||
var parentTemplate = documentTemplate.parent;
|
||||
|
||||
if (documentTemplate.parent != undefined) {
|
||||
if (typeof parentTemplate === 'string') {
|
||||
parentTemplate = await getObject({
|
||||
model: documentTemplateModel,
|
||||
id: parentTemplate,
|
||||
populate: [
|
||||
{ path: 'documentSize' },
|
||||
{ path: 'parent', strictPopulate: false },
|
||||
],
|
||||
});
|
||||
}
|
||||
if (parentTemplate?.error || parentTemplate.content == null || typeof parentTemplate.content !== 'string') {
|
||||
logger.error(
|
||||
'Parent template content is required and must be a string.',
|
||||
parentTemplate?.content
|
||||
);
|
||||
return {
|
||||
error:
|
||||
'Parent template content is required and must be a string.' +
|
||||
(parentTemplate?.content || ''),
|
||||
code: 400,
|
||||
};
|
||||
}
|
||||
templateWithParentContent = await ejs.render(
|
||||
parentTemplate.content,
|
||||
{ content: templateContent, fc: this.fc },
|
||||
defaultOptions
|
||||
);
|
||||
} else {
|
||||
templateWithParentContent = templateContent;
|
||||
}
|
||||
|
||||
if (templateWithParentContent == null || typeof templateWithParentContent !== 'string') {
|
||||
return { error: 'Failed to render template content.', code: 500 };
|
||||
}
|
||||
|
||||
const templateHtml = await transformCustomElements(templateWithParentContent);
|
||||
|
||||
if (templateHtml == null || typeof templateHtml !== 'string') {
|
||||
return { error: 'Failed to transform template content.', code: 500 };
|
||||
}
|
||||
|
||||
var innerHtml = null;
|
||||
|
||||
if (preview == true) {
|
||||
innerHtml = await ejs.render(previewTemplate, { content: templateHtml }, defaultOptions);
|
||||
} else {
|
||||
innerHtml = await ejs.render(renderTemplateEjs, { content: templateHtml }, defaultOptions);
|
||||
}
|
||||
|
||||
if (innerHtml == null || typeof innerHtml !== 'string') {
|
||||
return { error: 'Failed to render inner template content.', code: 500 };
|
||||
}
|
||||
|
||||
const infiniteHeight = documentSize.infiniteHeight == true;
|
||||
|
||||
const baseHtml = await ejs.render(
|
||||
baseTemplate,
|
||||
{
|
||||
content: innerHtml,
|
||||
width: `${documentSize.width}mm`,
|
||||
height: infiniteHeight ? 'fit-content' : `${documentSize.height}mm`,
|
||||
scaledWidth: `${documentSize.width * scale}mm`,
|
||||
scaledHeight: infiniteHeight ? 'auto' : `${documentSize.height * scale}mm`,
|
||||
scale: `${scale}`,
|
||||
baseCSS: baseCSS,
|
||||
previewPaginationScript: preview ? previewPaginationScript : '',
|
||||
},
|
||||
defaultOptions
|
||||
);
|
||||
|
||||
return {
|
||||
html: baseHtml,
|
||||
width: documentSize.width,
|
||||
height: infiniteHeight ? 'auto' : documentSize.height,
|
||||
infiniteHeight: infiniteHeight,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
logger.warn('Error whilst previewing template:', error.message);
|
||||
return { error: error.message, code: 500 };
|
||||
}
|
||||
}
|
||||
|
||||
validateTemplate(templateString) {
|
||||
try {
|
||||
ejs.compile(templateString);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async renderPDF(id, content, data = {}, options = {}) {
|
||||
try {
|
||||
logger.debug('Rendering PDF for template:', id);
|
||||
|
||||
const renderedTemplate = await this.renderTemplate(id, content, data, 1, options, false);
|
||||
|
||||
if (renderedTemplate.error != undefined) {
|
||||
return { error: renderedTemplate.error, code: renderedTemplate.code };
|
||||
}
|
||||
const baseHtml = renderedTemplate.html;
|
||||
|
||||
const pdfBuffer = await generatePDF(baseHtml, {
|
||||
width: renderedTemplate.width,
|
||||
height: renderedTemplate.height,
|
||||
infiniteHeight: renderedTemplate.infiniteHeight,
|
||||
});
|
||||
|
||||
return {
|
||||
pdf: pdfBuffer,
|
||||
width: renderedTemplate.width,
|
||||
height: renderedTemplate.height,
|
||||
infiniteHeight: renderedTemplate.infiniteHeight,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.warn('Error whilst rendering PDF:', error.message);
|
||||
return { error: error.message, code: 500 };
|
||||
}
|
||||
}
|
||||
|
||||
async renderImages(id, content, data = {}, options = {}, format = 'jpeg') {
|
||||
try {
|
||||
logger.debug(`Rendering ${format} for template:`, id);
|
||||
|
||||
const pdfResult = await this.renderPDF(id, content, data, options);
|
||||
if (pdfResult.error != undefined) {
|
||||
return { error: pdfResult.error, code: pdfResult.code };
|
||||
}
|
||||
|
||||
const images = await convertPDFToImage(pdfResult.pdf, options);
|
||||
const converted = [];
|
||||
for (const image of images) {
|
||||
if (format === 'png') {
|
||||
converted.push(await sharp(image).png().toBuffer());
|
||||
} else {
|
||||
converted.push(await sharp(image).jpeg().toBuffer());
|
||||
}
|
||||
}
|
||||
return { images: converted };
|
||||
} catch (error) {
|
||||
logger.warn('Error whilst rendering image:', error.message);
|
||||
return { error: error.message, code: 500 };
|
||||
}
|
||||
}
|
||||
|
||||
async renderSVG(id, content, data = {}, options = {}) {
|
||||
try {
|
||||
logger.debug('Rendering SVG for template:', id);
|
||||
|
||||
const pdfResult = await this.renderPDF(id, content, data, options);
|
||||
if (pdfResult.error != undefined) {
|
||||
return { error: pdfResult.error, code: pdfResult.code };
|
||||
}
|
||||
|
||||
const images = await convertPDFToImage(pdfResult.pdf, options);
|
||||
const svgs = [];
|
||||
for (const image of images) {
|
||||
const png = await sharp(image).png().toBuffer();
|
||||
const meta = await sharp(png).metadata();
|
||||
svgs.push(
|
||||
imageToSvg(
|
||||
png,
|
||||
meta.width,
|
||||
meta.height,
|
||||
pdfResult.width,
|
||||
pdfResult.height
|
||||
)
|
||||
);
|
||||
}
|
||||
return { svgs };
|
||||
} catch (error) {
|
||||
logger.warn('Error whilst rendering SVG:', error.message);
|
||||
return { error: error.message, code: 500 };
|
||||
}
|
||||
}
|
||||
|
||||
async renderDownload(id, content, data = {}, type = 'pdf', options = {}) {
|
||||
const normalizedType = normalizeDownloadType(type);
|
||||
if (!DOWNLOAD_TYPES.includes(normalizedType)) {
|
||||
return {
|
||||
error: `Unsupported download type: ${type}. Use pdf, png, svg or jpeg.`,
|
||||
code: 400,
|
||||
};
|
||||
}
|
||||
|
||||
if (normalizedType === 'pdf') {
|
||||
const result = await this.renderPDF(id, content, data, options);
|
||||
if (result.error) {
|
||||
return result;
|
||||
}
|
||||
return {
|
||||
type: 'pdf',
|
||||
mime: 'application/pdf',
|
||||
extension: 'pdf',
|
||||
buffers: [result.pdf],
|
||||
};
|
||||
}
|
||||
|
||||
if (normalizedType === 'svg') {
|
||||
const result = await this.renderSVG(id, content, data, options);
|
||||
if (result.error) {
|
||||
return result;
|
||||
}
|
||||
return {
|
||||
type: 'svg',
|
||||
mime: 'image/svg+xml',
|
||||
extension: 'svg',
|
||||
buffers: result.svgs.map((svg) => Buffer.from(svg, 'utf8')),
|
||||
};
|
||||
}
|
||||
|
||||
const result = await this.renderImages(id, content, data, options, normalizedType);
|
||||
if (result.error) {
|
||||
return result;
|
||||
}
|
||||
return {
|
||||
type: normalizedType,
|
||||
mime: normalizedType === 'png' ? 'image/png' : 'image/jpeg',
|
||||
extension: normalizedType === 'png' ? 'png' : 'jpeg',
|
||||
buffers: result.images,
|
||||
};
|
||||
}
|
||||
|
||||
async listObjects(objectType, filter = {}, populate = []) {
|
||||
const modelEntry = getModelByName(objectType);
|
||||
const model = modelEntry?.model || modelEntry;
|
||||
if (model == undefined) {
|
||||
throw new Error('Farm Control: Object type not found.');
|
||||
}
|
||||
const objects = await listObjects({
|
||||
model,
|
||||
filter,
|
||||
populate,
|
||||
pagination: false,
|
||||
});
|
||||
return objects;
|
||||
}
|
||||
|
||||
formatDate(date, format) {
|
||||
return dayjs(date).format(format);
|
||||
}
|
||||
|
||||
async getObject(objectType = undefined, id = undefined, populate = []) {
|
||||
if (objectType == undefined || objectType == '') {
|
||||
logger.warn('Object type is required');
|
||||
return { error: 'Object type is required' };
|
||||
}
|
||||
if (id == undefined || id == '') {
|
||||
logger.warn('Object ID is required');
|
||||
return { error: 'Object ID is required' };
|
||||
}
|
||||
const modelEntry = getModelByName(objectType);
|
||||
const model = modelEntry?.model || modelEntry;
|
||||
if (model == undefined) {
|
||||
logger.warn('Object type not found:', objectType);
|
||||
}
|
||||
const object = await getObject({ model, id, populate });
|
||||
return object;
|
||||
}
|
||||
}
|
||||
|
||||
export const templateManager = new TemplateManager();
|
||||
export { DOWNLOAD_TYPES, normalizeDownloadType };
|
||||
23
src/utils.js
23
src/utils.js
@ -1390,6 +1390,28 @@ async function createNotification(user, title, message, type = 'info', metadata)
|
||||
return notification;
|
||||
}
|
||||
|
||||
async function createDocumentJobUserNotifier(documentJobId, user) {
|
||||
const userId = user?._id ?? user;
|
||||
if (!userId) return null;
|
||||
|
||||
const existing = await userNotifierModel.findOne({
|
||||
user: userId,
|
||||
object: documentJobId,
|
||||
objectType: 'documentJob',
|
||||
});
|
||||
if (existing) return existing;
|
||||
|
||||
const userNotifier = await userNotifierModel.create({
|
||||
user: userId,
|
||||
object: documentJobId,
|
||||
objectType: 'documentJob',
|
||||
email: false,
|
||||
});
|
||||
|
||||
await distributeNew(userNotifier, 'userNotifier');
|
||||
return userNotifier;
|
||||
}
|
||||
|
||||
let mailWorker = null;
|
||||
|
||||
function getMailWorker() {
|
||||
@ -1736,6 +1758,7 @@ export {
|
||||
distributeChildNew,
|
||||
notfiyObjectUserNotifiers,
|
||||
createNotification,
|
||||
createDocumentJobUserNotifier,
|
||||
sendEmailNotification,
|
||||
getFilter,
|
||||
getSort,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user