diff --git a/package.json b/package.json index 8418777..cb857d2 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "canonical-json": "^0.2.0", "cors": "^2.8.5", "dotenv": "^17.2.3", + "exceljs": "^4.4.0", "exifr": "^7.1.3", "express": "^5.1.0", "express-session": "^1.18.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3dfc8e5..7768e19 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: dotenv: specifier: ^17.2.3 version: 17.2.3 + exceljs: + specifier: ^4.4.0 + version: 4.4.0 exifr: specifier: ^7.1.3 version: 7.1.3 @@ -998,6 +1001,12 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@fast-csv/format@4.3.5': + resolution: {integrity: sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==} + + '@fast-csv/parse@4.3.6': + resolution: {integrity: sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==} + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -1493,6 +1502,9 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@14.18.63': + resolution: {integrity: sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==} + '@types/node@25.2.0': resolution: {integrity: sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==} @@ -1671,6 +1683,18 @@ packages: append-field@1.0.0: resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + archiver-utils@2.1.0: + resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==} + engines: {node: '>= 6'} + + archiver-utils@3.0.4: + resolution: {integrity: sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==} + engines: {node: '>= 10'} + + archiver@5.3.2: + resolution: {integrity: sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==} + engines: {node: '>= 10'} + argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -1727,6 +1751,9 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -1838,6 +1865,9 @@ packages: bare-url@2.3.2: resolution: {integrity: sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==} + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.9.19: resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} hasBin: true @@ -1851,10 +1881,23 @@ packages: resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==} engines: {node: '>= 18'} + big-integer@1.6.52: + resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} + engines: {node: '>=0.6'} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + binary@0.3.0: + resolution: {integrity: sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + bluebird@3.4.7: + resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==} + bluebird@3.7.2: resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} @@ -1902,6 +1945,17 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer-indexof-polyfill@1.0.2: + resolution: {integrity: sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==} + engines: {node: '>=0.10'} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + buffers@0.1.1: + resolution: {integrity: sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==} + engines: {node: '>=0.2.0'} + builtins@5.1.0: resolution: {integrity: sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==} @@ -1944,6 +1998,9 @@ packages: resolution: {integrity: sha512-xeH/NgtNA7kIuKSxopJVdXqCKWyDB79aqxQRQ9FV02fvmqW7DSnjoFyzAUrBpfbwjU6lwTYOLc+HM6KupbsfVQ==} hasBin: true + chainsaw@0.1.0: + resolution: {integrity: sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -2027,6 +2084,10 @@ packages: component-emitter@1.3.1: resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + compress-commons@4.1.2: + resolution: {integrity: sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==} + engines: {node: '>= 10'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -2073,6 +2134,9 @@ packages: core-js@3.48.0: resolution: {integrity: sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==} + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cors@2.8.6: resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} engines: {node: '>= 0.10'} @@ -2086,6 +2150,15 @@ packages: typescript: optional: true + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + crc32-stream@4.0.3: + resolution: {integrity: sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==} + engines: {node: '>= 10'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -2110,6 +2183,9 @@ packages: resolution: {integrity: sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==} engines: {node: '>=4.0'} + dayjs@1.11.19: + resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} + debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -2208,6 +2284,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + duplexer2@0.1.4: + resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -2486,6 +2565,10 @@ packages: events-universal@1.0.1: resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + exceljs@4.4.0: + resolution: {integrity: sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==} + engines: {node: '>=8.3.0'} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -2514,6 +2597,10 @@ packages: engines: {node: '>= 10.17.0'} hasBin: true + fast-csv@4.3.6: + resolution: {integrity: sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==} + engines: {node: '>=10.0.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2621,6 +2708,9 @@ packages: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fs-extra@8.1.0: resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} engines: {node: '>=6 <7 || >=8'} @@ -2640,6 +2730,11 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + fstream@1.0.12: + resolution: {integrity: sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==} + engines: {node: '>=0.6'} + deprecated: This package is no longer supported. + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -2800,6 +2895,9 @@ packages: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore-by-default@1.0.1: resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} @@ -2807,6 +2905,9 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -2987,6 +3088,9 @@ packages: resolution: {integrity: sha512-rZkHeBn9Zzq52sd9IUIV3a5mfwBY+o2HePMh0wkGBM4z4qjvy2GwVxQ6nNXSfw6MmVP6gf1QIlWjiOavhM3x5g==} engines: {node: '>=v0.10.0'} + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -3219,6 +3323,9 @@ packages: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + jwa@2.0.1: resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} @@ -3243,6 +3350,10 @@ packages: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} + lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} @@ -3251,9 +3362,15 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + listenercount@1.0.1: + resolution: {integrity: sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==} + load-json-file@5.3.0: resolution: {integrity: sha512-cJGP40Jc/VXUsp8/OrnyKyTZ1y6v/dphm3bioS+RrKXjK2BB6wHUd6JptZEFDGgGahMT+InnZO5i1Ei9mpC8Bw==} engines: {node: '>=6'} @@ -3273,15 +3390,40 @@ packages: lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.difference@4.5.0: + resolution: {integrity: sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==} + + lodash.escaperegexp@4.1.2: + resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==} + + lodash.flatten@4.4.0: + resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==} + + lodash.groupby@4.6.0: + resolution: {integrity: sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==} + lodash.includes@4.3.0: resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} lodash.isboolean@3.0.3: resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. + + lodash.isfunction@3.0.9: + resolution: {integrity: sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==} + lodash.isinteger@4.0.4: resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + lodash.isnil@4.0.0: + resolution: {integrity: sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==} + lodash.isnumber@3.0.3: resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} @@ -3291,12 +3433,21 @@ packages: lodash.isstring@4.0.1: resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + lodash.isundefined@3.0.1: + resolution: {integrity: sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash.union@4.6.0: + resolution: {integrity: sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==} + + lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + lodash@4.17.23: resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} @@ -3393,6 +3544,10 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + minimatch@9.0.1: resolution: {integrity: sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==} engines: {node: '>=16 || 14 >=14.17'} @@ -3670,6 +3825,9 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -3821,6 +3979,9 @@ packages: resolution: {integrity: sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + progress@2.0.3: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} @@ -3892,10 +4053,16 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} + readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -3972,6 +4139,11 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rimraf@2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} deprecated: Rimraf versions prior to v4 are no longer supported @@ -3991,6 +4163,9 @@ packages: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -4005,6 +4180,10 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + saxes@5.0.1: + resolution: {integrity: sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==} + engines: {node: '>=10'} + semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true @@ -4085,6 +4264,9 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -4237,6 +4419,9 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} @@ -4298,6 +4483,10 @@ packages: tar-fs@3.1.1: resolution: {integrity: sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==} + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + tar-stream@3.1.8: resolution: {integrity: sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==} @@ -4317,6 +4506,10 @@ packages: text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + tmp@0.2.5: + resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} + engines: {node: '>=14.14'} + tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -4339,6 +4532,9 @@ packages: resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} engines: {node: '>=18'} + traverse@0.3.9: + resolution: {integrity: sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -4451,6 +4647,9 @@ packages: unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + unzipper@0.10.14: + resolution: {integrity: sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -4563,6 +4762,9 @@ packages: xml@1.0.1: resolution: {integrity: sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==} + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -4597,6 +4799,10 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zip-stream@4.1.1: + resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==} + engines: {node: '>= 10'} + zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -5972,6 +6178,25 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 + '@fast-csv/format@4.3.5': + dependencies: + '@types/node': 14.18.63 + lodash.escaperegexp: 4.1.2 + lodash.isboolean: 3.0.3 + lodash.isequal: 4.5.0 + lodash.isfunction: 3.0.9 + lodash.isnil: 4.0.0 + + '@fast-csv/parse@4.3.6': + dependencies: + '@types/node': 14.18.63 + lodash.escaperegexp: 4.1.2 + lodash.groupby: 4.6.0 + lodash.isfunction: 3.0.9 + lodash.isnil: 4.0.0 + lodash.isundefined: 3.0.1 + lodash.uniq: 4.5.0 + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -6704,6 +6929,8 @@ snapshots: '@types/ms@2.1.0': {} + '@types/node@14.18.63': {} + '@types/node@25.2.0': dependencies: undici-types: 7.16.0 @@ -6835,6 +7062,42 @@ snapshots: append-field@1.0.0: {} + archiver-utils@2.1.0: + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash.defaults: 4.2.0 + lodash.difference: 4.5.0 + lodash.flatten: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.union: 4.6.0 + normalize-path: 3.0.0 + readable-stream: 2.3.8 + + archiver-utils@3.0.4: + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash.defaults: 4.2.0 + lodash.difference: 4.5.0 + lodash.flatten: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.union: 4.6.0 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + + archiver@5.3.2: + dependencies: + archiver-utils: 2.1.0 + async: 3.2.6 + buffer-crc32: 0.2.13 + readable-stream: 3.6.2 + readdir-glob: 1.1.3 + tar-stream: 2.2.0 + zip-stream: 4.1.1 + argparse@1.0.10: dependencies: sprintf-js: 1.0.3 @@ -6934,6 +7197,8 @@ snapshots: async-function@1.0.0: {} + async@3.2.6: {} + asynckit@0.4.0: {} at-least-node@1.0.0: {} @@ -7078,6 +7343,8 @@ snapshots: dependencies: bare-path: 3.0.0 + base64-js@1.5.1: {} + baseline-browser-mapping@2.9.19: {} basic-ftp@5.1.0: {} @@ -7087,8 +7354,23 @@ snapshots: node-addon-api: 8.5.0 node-gyp-build: 4.8.4 + big-integer@1.6.52: {} + binary-extensions@2.3.0: {} + binary@0.3.0: + dependencies: + buffers: 0.1.1 + chainsaw: 0.1.0 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + bluebird@3.4.7: {} + bluebird@3.7.2: {} bn.js@4.12.2: {} @@ -7144,6 +7426,15 @@ snapshots: buffer-from@1.1.2: {} + buffer-indexof-polyfill@1.0.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + buffers@0.1.1: {} + builtins@5.1.0: dependencies: semver: 7.7.3 @@ -7181,6 +7472,10 @@ snapshots: canonical-json@0.2.0: {} + chainsaw@0.1.0: + dependencies: + traverse: 0.3.9 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -7271,6 +7566,13 @@ snapshots: component-emitter@1.3.1: {} + compress-commons@4.1.2: + dependencies: + buffer-crc32: 0.2.13 + crc32-stream: 4.0.3 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + concat-map@0.0.1: {} concat-stream@2.0.0: @@ -7314,6 +7616,8 @@ snapshots: core-js@3.48.0: {} + core-util-is@1.0.3: {} + cors@2.8.6: dependencies: object-assign: 4.1.1 @@ -7326,6 +7630,13 @@ snapshots: js-yaml: 4.1.1 parse-json: 5.2.0 + crc-32@1.2.2: {} + + crc32-stream@4.0.3: + dependencies: + crc-32: 1.2.2 + readable-stream: 3.6.2 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -7354,6 +7665,8 @@ snapshots: date-format@4.0.14: {} + dayjs@1.11.19: {} + debug@2.6.9: dependencies: ms: 2.0.0 @@ -7428,6 +7741,10 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + duplexer2@0.1.4: + dependencies: + readable-stream: 2.3.8 + eastasianwidth@0.2.0: {} ecdsa-sig-formatter@1.0.11: @@ -7601,7 +7918,7 @@ snapshots: eslint: 8.57.1 eslint-plugin-react: 7.37.5(eslint@8.57.1) - eslint-config-standard@17.1.0(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1): + eslint-config-standard@17.1.0(eslint-plugin-import@2.32.0(eslint@9.39.2))(eslint-plugin-n@15.7.0(eslint@9.39.2))(eslint-plugin-promise@6.6.0(eslint@9.39.2))(eslint@8.57.1): dependencies: eslint: 8.57.1 eslint-plugin-import: 2.32.0(eslint@8.57.1) @@ -7848,6 +8165,18 @@ snapshots: transitivePeerDependencies: - bare-abort-controller + exceljs@4.4.0: + dependencies: + archiver: 5.3.2 + dayjs: 1.11.19 + fast-csv: 4.3.6 + jszip: 3.10.1 + readable-stream: 3.6.2 + saxes: 5.0.1 + tmp: 0.2.5 + unzipper: 0.10.14 + uuid: 8.3.2 + execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -7929,6 +8258,11 @@ snapshots: transitivePeerDependencies: - supports-color + fast-csv@4.3.6: + dependencies: + '@fast-csv/format': 4.3.5 + '@fast-csv/parse': 4.3.6 + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -8042,6 +8376,8 @@ snapshots: fresh@2.0.0: {} + fs-constants@1.0.0: {} + fs-extra@8.1.0: dependencies: graceful-fs: 4.2.11 @@ -8062,6 +8398,13 @@ snapshots: fsevents@2.3.3: optional: true + fstream@1.0.12: + dependencies: + graceful-fs: 4.2.11 + inherits: 2.0.4 + mkdirp: 0.5.6 + rimraf: 2.7.1 + function-bind@1.1.2: {} function.prototype.name@1.1.8: @@ -8237,10 +8580,14 @@ snapshots: dependencies: safer-buffer: 2.1.2 + ieee754@1.2.1: {} + ignore-by-default@1.0.1: {} ignore@5.3.2: {} + immediate@3.0.6: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -8415,6 +8762,8 @@ snapshots: is-url: 1.2.4 optional: true + isarray@1.0.0: {} + isarray@2.0.5: {} isexe@2.0.0: {} @@ -8854,6 +9203,13 @@ snapshots: object.assign: 4.1.7 object.values: 1.2.1 + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + jwa@2.0.1: dependencies: buffer-equal-constant-time: 1.0.1 @@ -8888,6 +9244,10 @@ snapshots: kind-of@6.0.3: {} + lazystream@1.0.1: + dependencies: + readable-stream: 2.3.8 + leven@3.1.0: {} levn@0.4.1: @@ -8895,8 +9255,14 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lie@3.3.0: + dependencies: + immediate: 3.0.6 + lines-and-columns@1.2.4: {} + listenercount@1.0.1: {} + load-json-file@5.3.0: dependencies: graceful-fs: 4.2.11 @@ -8920,22 +9286,44 @@ snapshots: lodash.debounce@4.0.8: {} + lodash.defaults@4.2.0: {} + + lodash.difference@4.5.0: {} + + lodash.escaperegexp@4.1.2: {} + + lodash.flatten@4.4.0: {} + + lodash.groupby@4.6.0: {} + lodash.includes@4.3.0: {} lodash.isboolean@3.0.3: {} + lodash.isequal@4.5.0: {} + + lodash.isfunction@3.0.9: {} + lodash.isinteger@4.0.4: {} + lodash.isnil@4.0.0: {} + lodash.isnumber@3.0.3: {} lodash.isplainobject@4.0.6: {} lodash.isstring@4.0.1: {} + lodash.isundefined@3.0.1: {} + lodash.merge@4.6.2: {} lodash.once@4.1.1: {} + lodash.union@4.6.0: {} + + lodash.uniq@4.5.0: {} + lodash@4.17.23: {} log4js@6.9.1: @@ -9016,6 +9404,10 @@ snapshots: dependencies: brace-expansion: 1.1.12 + minimatch@5.1.9: + dependencies: + brace-expansion: 2.0.2 + minimatch@9.0.1: dependencies: brace-expansion: 2.0.2 @@ -9284,6 +9676,8 @@ snapshots: package-json-from-dist@1.0.1: {} + pako@1.0.11: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -9407,6 +9801,8 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 + process-nextick-args@2.0.1: {} + progress@2.0.3: {} prop-types@15.8.1: @@ -9506,12 +9902,26 @@ snapshots: react-is@18.3.1: {} + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + readable-stream@3.6.2: dependencies: inherits: 2.0.4 string_decoder: 1.3.0 util-deprecate: 1.0.2 + readdir-glob@1.1.3: + dependencies: + minimatch: 5.1.9 + readdirp@3.6.0: dependencies: picomatch: 2.3.1 @@ -9597,6 +10007,10 @@ snapshots: rfdc@1.4.1: {} + rimraf@2.7.1: + dependencies: + glob: 7.2.3 + rimraf@3.0.2: dependencies: glob: 7.2.3 @@ -9627,6 +10041,8 @@ snapshots: has-symbols: 1.1.0 isarray: 2.0.5 + safe-buffer@5.1.2: {} + safe-buffer@5.2.1: {} safe-push-apply@1.0.0: @@ -9642,6 +10058,10 @@ snapshots: safer-buffer@2.1.2: {} + saxes@5.0.1: + dependencies: + xmlchars: 2.2.0 + semver@5.7.2: {} semver@6.3.1: {} @@ -9732,6 +10152,8 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.1 + setimmediate@1.0.5: {} + setprototypeof@1.2.0: {} shallow-clone@3.0.1: @@ -9837,7 +10259,7 @@ snapshots: standard@17.1.2: dependencies: eslint: 8.57.1 - eslint-config-standard: 17.1.0(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1) + eslint-config-standard: 17.1.0(eslint-plugin-import@2.32.0(eslint@9.39.2))(eslint-plugin-n@15.7.0(eslint@9.39.2))(eslint-plugin-promise@6.6.0(eslint@9.39.2))(eslint@8.57.1) eslint-config-standard-jsx: 11.0.0(eslint-plugin-react@7.37.5(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-import: 2.32.0(eslint@8.57.1) eslint-plugin-n: 15.7.0(eslint@8.57.1) @@ -9938,6 +10360,10 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 @@ -10012,6 +10438,14 @@ snapshots: - bare-buffer - react-native-b4a + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + tar-stream@3.1.8: dependencies: b4a: 1.8.0 @@ -10052,6 +10486,8 @@ snapshots: text-table@0.2.0: {} + tmp@0.2.5: {} + tmpl@1.0.5: {} to-regex-range@5.0.1: @@ -10068,6 +10504,8 @@ snapshots: dependencies: punycode: 2.3.1 + traverse@0.3.9: {} + tree-kill@1.2.2: {} tsconfig-paths@3.15.0: @@ -10201,6 +10639,19 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + unzipper@0.10.14: + dependencies: + big-integer: 1.6.52 + binary: 0.3.0 + bluebird: 3.4.7 + buffer-indexof-polyfill: 1.0.2 + duplexer2: 0.1.4 + fstream: 1.0.12 + graceful-fs: 4.2.11 + listenercount: 1.0.1 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + update-browserslist-db@1.2.3(browserslist@4.28.1): dependencies: browserslist: 4.28.1 @@ -10320,6 +10771,8 @@ snapshots: xml@1.0.1: {} + xmlchars@2.2.0: {} + xtend@4.0.2: {} y18n@5.0.8: {} @@ -10357,4 +10810,10 @@ snapshots: yocto-queue@0.1.0: {} + zip-stream@4.1.1: + dependencies: + archiver-utils: 3.0.4 + compress-commons: 4.1.2 + readable-stream: 3.6.2 + zod@3.25.76: {} diff --git a/src/database/excel.js b/src/database/excel.js new file mode 100644 index 0000000..e3a8e9e --- /dev/null +++ b/src/database/excel.js @@ -0,0 +1,160 @@ +import { customAlphabet } from 'nanoid'; +import log4js from 'log4js'; +import ExcelJS from 'exceljs'; +import config from '../config.js'; +import { redisServer } from './redis.js'; + +const logger = log4js.getLogger('Excel'); +const EXCEL_TEMP_KEY_PREFIX = 'excel:temp:'; +const EXCEL_TEMP_TTL_SECONDS = 15; // 15 seconds + +const excelNanoid = customAlphabet( + 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', + 12 +); + +/** + * Create a temp token and store export params in Redis. + * @param {Object} params - { objectType, filter, sort, order } + * @returns {Promise<{ token: string, url: string }>} + */ +export async function createExcelTempToken(params) { + const baseUrl = config.app?.urlApi?.replace(/\/$/, '') || ''; + if (!baseUrl) { + throw new Error('config.app.urlApi is not set; required for Excel temp URLs'); + } + const objectType = params.objectType || 'Export'; + const now = new Date(); + const datetime = + now.getFullYear() + + String(now.getMonth() + 1).padStart(2, '0') + + String(now.getDate()).padStart(2, '0') + + String(now.getHours()).padStart(2, '0') + + String(now.getMinutes()).padStart(2, '0'); + const token = `${objectType}s-${datetime}-${excelNanoid()}`; + const key = EXCEL_TEMP_KEY_PREFIX + token; + const stored = { ...params, requestCount: 0 }; + await redisServer.setKey(key, stored, EXCEL_TEMP_TTL_SECONDS); + logger.debug('Stored excel temp token in Redis:', key); + const url = `${baseUrl}/excel/temp/${token}.xlsx`; + return { token, url }; +} + +/** + * Get export params for a temp token (supports up to 2 requests; requestCount stored in Redis). + * @param {string} token + * @returns {Promise} { objectType, filter, sort, order } or null + */ +export async function getExcelTempParams(token) { + if (!token) return null; + const key = EXCEL_TEMP_KEY_PREFIX + token; + const params = await redisServer.getKey(key); + if (!params) { + logger.debug('Excel temp token not found in Redis:', key); + return null; + } + return params; +} + +/** + * Convert a value to an Excel cell-friendly format. + * Primitives pass through; objects/arrays are stringified; dates are preserved. + */ +function toExcelValue(val) { + if (val === null || val === undefined) return null; + if (val instanceof Date) return val; + if (typeof val === 'number' || typeof val === 'boolean') return val; + if (typeof val === 'string') return val; + if (typeof val === 'object') { + if (Array.isArray(val)) return val.map(toExcelValue).join(', '); + return JSON.stringify(val); + } + return String(val); +} + +/** + * Generate an Excel workbook from tabular data. + * @param {Array} data - Array of row objects (keys = column headers) + * @param {Object} options - Options + * @param {string} [options.sheetName='Export'] - Worksheet name + * @param {string[]} [options.columnOrder] - Optional column order (uses Object.keys of first row if not provided) + * @returns {Promise} Excel file as buffer + */ +export async function generateExcelTable(data, options = {}) { + const { sheetName = 'Export', columnOrder } = options; + + const workbook = new ExcelJS.Workbook(); + const worksheet = workbook.addWorksheet(sheetName, { + views: [{ state: 'frozen', ySplit: 1 }], + }); + + if (!data || data.length === 0) { + const buffer = await workbook.xlsx.writeBuffer(); + return Buffer.from(buffer); + } + + const keys = columnOrder || Object.keys(data[0]).filter((k) => !k.startsWith('@')); + const colCount = keys.length; + const rowCount = data.length + 1; + + const toColLetter = (n) => { + let s = ''; + while (n >= 0) { + s = String.fromCharCode((n % 26) + 65) + s; + n = Math.floor(n / 26) - 1; + } + return s; + }; + const endCol = toColLetter(colCount - 1); + + const tableRows = data.map((row) => keys.map((key) => toExcelValue(row[key]))); + worksheet.addTable({ + name: 'DataTable', + ref: `A1:${endCol}${rowCount}`, + headerRow: true, + style: { + theme: 'TableStyleLight1', + showRowStripes: true, + }, + columns: keys.map((key) => ({ name: key, filterButton: true })), + rows: tableRows, + }); + + // Auto-fit columns (approximate) + worksheet.columns.forEach((col, i) => { + let maxLen = keys[i]?.length || 10; + worksheet.eachRow({ includeEmpty: false }, (row) => { + const cell = row.getCell(i + 1); + const val = cell.value; + const len = val != null ? String(val).length : 0; + maxLen = Math.min(Math.max(maxLen, len), 50); + }); + col.width = maxLen + 2; + }); + + const buffer = await workbook.xlsx.writeBuffer(); + return Buffer.from(buffer); +} + +/** + * Increment request count for a temp token. Returns new count or null if token not found. + * @param {string} token + * @returns {Promise} New requestCount or null + */ +export async function incrementExcelTempRequestCount(token) { + if (!token) return null; + const key = EXCEL_TEMP_KEY_PREFIX + token; + const data = await redisServer.getKey(key); + if (!data) return null; + const requestCount = (data.requestCount ?? 0) + 1; + const updated = { ...data, requestCount }; + await redisServer.setKey(key, updated, EXCEL_TEMP_TTL_SECONDS); + logger.debug('Incremented excel temp request count:', key, requestCount); + return requestCount; +} + +export async function deleteExcelTempToken(token) { + const key = EXCEL_TEMP_KEY_PREFIX + token; + await redisServer.deleteKey(key); + logger.debug('Deleted excel temp token from Redis:', key); +} diff --git a/src/index.js b/src/index.js index 603c9d5..edcf35a 100644 --- a/src/index.js +++ b/src/index.js @@ -3,6 +3,7 @@ import bodyParser from 'body-parser'; import cors from 'cors'; import config from './config.js'; import { dbConnect } from './database/mongo.js'; +import { redisServer } from './database/redis.js'; import { authRoutes, userRoutes, @@ -44,6 +45,7 @@ import { userNotifierRoutes, notificationRoutes, odataRoutes, + excelRoutes, } from './routes/index.js'; import path from 'path'; import * as fs from 'fs'; @@ -83,6 +85,9 @@ async function initializeApp() { // Connect to database await dbConnect(); + // Connect to Redis (required for excel temp tokens, sessions, cache) + await redisServer.connect(); + // Connect to NATS await natsServer.connect(); @@ -153,6 +158,7 @@ app.use('/notes', noteRoutes); app.use('/usernotifiers', userNotifierRoutes); app.use('/notifications', notificationRoutes); app.use('/odata', odataRoutes); +app.use('/excel', excelRoutes); // Start the application if (process.env.NODE_ENV !== 'test') { diff --git a/src/routes/index.js b/src/routes/index.js index 41ba973..113afa4 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -38,6 +38,7 @@ import noteRoutes from './misc/notes.js'; import userNotifierRoutes from './misc/usernotifiers.js'; import notificationRoutes from './misc/notifications.js'; import odataRoutes from './misc/odata.js'; +import excelRoutes from './misc/excel.js'; export { userRoutes, @@ -80,4 +81,5 @@ export { userNotifierRoutes, notificationRoutes, odataRoutes, + excelRoutes, }; diff --git a/src/routes/misc/excel.js b/src/routes/misc/excel.js new file mode 100644 index 0000000..f1f2db2 --- /dev/null +++ b/src/routes/misc/excel.js @@ -0,0 +1,16 @@ +import express from 'express'; +import { isAuthenticated } from '../../keycloak.js'; +import { + excelExportRouteHandler, + excelOpenRouteHandler, + excelTempRouteHandler, +} from '../../services/misc/excel.js'; + +const router = express.Router(); + +// Temp route must come before /:objectType so "temp" is not matched as objectType +router.get('/temp/:token', excelTempRouteHandler); +router.post('/:objectType/open', isAuthenticated, excelOpenRouteHandler); +router.get('/:objectType', isAuthenticated, excelExportRouteHandler); + +export default router; diff --git a/src/services/misc/excel.js b/src/services/misc/excel.js new file mode 100644 index 0000000..9f0eff3 --- /dev/null +++ b/src/services/misc/excel.js @@ -0,0 +1,289 @@ +import config from '../../config.js'; +import log4js from 'log4js'; +import { getModelByName } from './model.js'; +import { listObjectsOData } from '../../database/odata.js'; +import { getFilter } from '../../utils.js'; +import { + generateExcelTable, + createExcelTempToken, + getExcelTempParams, + incrementExcelTempRequestCount, + deleteExcelTempToken, +} from '../../database/excel.js'; + +const logger = log4js.getLogger('Excel'); +logger.level = config.server.logLevel; + +/** + * Flatten nested objects for Excel display. + * Objects become "key.subkey: value" or JSON string; arrays become comma-separated. + */ +function flattenForExcel(obj, prefix = '') { + if (obj === null || obj === undefined) return {}; + if (typeof obj !== 'object') return { [prefix]: obj }; + if (obj instanceof Date) return { [prefix]: obj }; + if (Array.isArray(obj)) { + const str = obj + .map((v) => (v && typeof v === 'object' && !(v instanceof Date) ? JSON.stringify(v) : v)) + .join(', '); + return { [prefix]: str }; + } + const result = {}; + for (const [k, v] of Object.entries(obj)) { + const key = prefix ? `${prefix}.${k}` : k; + if (v !== null && typeof v === 'object' && !(v instanceof Date) && !Array.isArray(v)) { + Object.assign(result, flattenForExcel(v, key)); + } else { + result[key] = v; + } + } + return result; +} + +/** + * Convert a row to flat key-value for Excel. Nested objects are flattened. + */ +function rowToFlat(row) { + const flat = {}; + for (const [key, val] of Object.entries(row)) { + if (key.startsWith('@')) continue; + if (val !== null && typeof val === 'object' && !(val instanceof Date) && !Array.isArray(val)) { + Object.assign(flat, flattenForExcel(val, key)); + } else { + flat[key] = val; + } + } + return flat; +} + +/** + * Get allowed filter fields for Excel export (reuse OData logic). + */ +function getModelFilterFields(objectType) { + const base = ['_id']; + const byType = { + note: ['parent._id', 'noteType', 'user'], + notification: ['user'], + userNotifier: ['user', 'object', 'objectType'], + printer: ['host'], + job: ['printer', 'gcodeFile'], + subJob: ['job'], + filamentStock: ['filament'], + partStock: ['part'], + purchaseOrder: ['vendor'], + orderItem: ['order._id', 'orderType', 'item._id', 'itemType', 'shipment._id'], + shipment: ['order._id', 'orderType', 'courierService._id'], + stockEvent: ['parent._id', 'parentType', 'owner._id', 'ownerType'], + stockAudit: ['filamentStock._id', 'partStock._id'], + documentJob: ['documentTemplate', 'documentPrinter', 'object._id', 'objectType'], + documentTemplate: ['parent._id', 'documentSize._id'], + salesOrder: ['client'], + invoice: ['to._id', 'from._id', 'order._id', 'orderType'], + auditLog: ['parent._id', 'parentType', 'owner._id', 'ownerType'], + appPassword: ['name', 'user', 'active'], + }; + const extra = byType[objectType] || []; + return [...base, ...extra]; +} + +function parseOrderBy(orderby) { + if (!orderby || typeof orderby !== 'string') { + return { sort: 'createdAt', order: 'ascend' }; + } + const trimmed = orderby.trim(); + const parts = trimmed.split(/\s+/); + const sort = parts[0] || 'createdAt'; + const dir = (parts[1] || 'asc').toLowerCase(); + const order = dir === 'desc' ? 'descend' : 'ascend'; + return { sort, order }; +} + +/** + * Generate Excel file for the given object type. + * @param {Object} options + * @param {string} options.objectType - Model type (e.g. 'appPassword', 'user') + * @param {Object} [options.filter] - Filter object + * @param {string} [options.sort] - Sort field + * @param {string} [options.order] - 'ascend' | 'descend' + * @param {number} [options.limit=10000] - Max rows to export + * @returns {Promise<{ buffer: Buffer, error?: Object }>} + */ +export async function exportToExcel({ objectType, filter = {}, sort, order, limit = 10000 }) { + logger.info('[Excel Export] Starting', { objectType, filter, sort, order }); + const entry = getModelByName(objectType); + if (!entry?.model) { + logger.warn('[Excel Export] Unknown object type:', objectType); + return { error: { message: `Unknown object type: ${objectType}` }, code: 404 }; + } + + const orderbyStr = sort ? `${sort} ${order === 'descend' ? 'desc' : 'asc'}` : undefined; + const { sort: sortField, order: orderDir } = parseOrderBy(orderbyStr); + + const result = await listObjectsOData({ + model: entry.model, + populate: [], + page: 1, + limit, + filter, + sort: sortField, + order: orderDir, + pagination: true, + project: undefined, + count: false, + }); + + if (result?.error) { + logger.error('[Excel Export] listObjectsOData error:', result.error); + return { error: result.error, code: result.code || 500 }; + } + + const rows = result?.value || []; + logger.info('[Excel Export] Rows fetched', { rowCount: rows.length }); + const flatRows = rows.map(rowToFlat); + + const allKeys = new Set(); + flatRows.forEach((r) => Object.keys(r).forEach((k) => allKeys.add(k))); + const columnOrder = Array.from(allKeys).sort(); + + let buffer; + try { + buffer = await generateExcelTable(flatRows, { + sheetName: entry.model.modelName || objectType, + columnOrder, + }); + logger.info('[Excel Export] Buffer generated', { bufferLength: buffer?.length }); + } catch (err) { + logger.error('[Excel Export] generateExcelTable threw:', err?.message, err?.stack); + return { error: { message: err.message || 'Failed to generate Excel' }, code: 500 }; + } + + return { buffer }; +} + +/** + * Route handler for GET /odata/:objectType/excel + */ +export const excelExportRouteHandler = async (req, res) => { + const objectType = req.params.objectType; + const allowedFilters = getModelFilterFields(objectType); + const filter = getFilter(req.query, allowedFilters); + const { sort, order } = parseOrderBy(req.query.$orderby); + + const result = await exportToExcel({ + objectType, + filter, + sort, + order, + }); + + if (result.error) { + return res.status(result.code || 500).json(result.error); + } + + const filename = `${objectType}-export-${new Date().toISOString().slice(0, 10)}.xlsx`; + res.set('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); + res.set('Content-Disposition', `attachment; filename="${filename}"`); + res.send(result.buffer); +}; + +/** + * Route handler for POST /excel/:objectType/open - creates a temporary URL for opening in Excel. + * Stores export params in Redis; spreadsheet is generated when Excel fetches the temp URL. + * Returns { url } - an https URL Excel can fetch (no auth required, one-time use, 5 min expiry). + */ +export const excelOpenRouteHandler = async (req, res) => { + try { + const objectType = req.params.objectType; + logger.info('[Excel Open] POST received', { objectType, query: req.query }); + const allowedFilters = getModelFilterFields(objectType); + const filter = getFilter(req.query, allowedFilters); + const { sort, order } = parseOrderBy(req.query.$orderby); + + const entry = getModelByName(objectType); + if (!entry?.model) { + logger.warn('[Excel Open] Unknown object type:', objectType); + return res.status(404).json({ message: `Unknown object type: ${objectType}` }); + } + + const params = { objectType, filter, sort, order }; + logger.info('[Excel Open] Creating temp token with params:', JSON.stringify(params)); + const { token, url } = await createExcelTempToken(params); + if (!url) { + logger.error('[Excel Open] createExcelTempToken returned no url'); + return res.status(500).json({ message: 'Failed to create temporary URL' }); + } + logger.info('[Excel Open] Temp URL created', { objectType, token, url }); + res.json({ url }); + } catch (err) { + logger.error('[Excel Open] Route error:', err?.message, err?.stack); + res.status(500).json({ message: err.message || 'Failed to create Excel URL' }); + } +}; + +/** + * Route handler for GET /excel/temp/:token - generates and serves temporary Excel file (no auth). + * Retrieves export params from Redis, generates spreadsheet on demand. + * Token allows 2 requests before deletion; expires after 5 minutes. + */ +export const excelTempRouteHandler = async (req, res) => { + const tokenParam = req.params.token || ''; + const token = tokenParam.endsWith('.xlsx') ? tokenParam.slice(0, -5) : tokenParam; + logger.info('[Excel Temp] GET received', { + tokenParam, + token, + userAgent: req.get('User-Agent'), + referer: req.get('Referer'), + }); + + const params = await getExcelTempParams(token); + if (!params) { + logger.error('[Excel Temp] Token not found or expired', { token, tokenParam }); + return res.status(404).json({ message: 'Link expired or invalid' }); + } + + logger.info('[Excel Temp] Params retrieved from Redis', { objectType: params.objectType }); + + const result = await exportToExcel({ + objectType: params.objectType, + filter: params.filter || {}, + sort: params.sort, + order: params.order, + }); + + if (result.error) { + logger.error('[Excel Temp] Export failed', { error: result.error, code: result.code }); + return res.status(result.code || 500).json(result.error); + } + + const buffer = Buffer.isBuffer(result.buffer) ? result.buffer : Buffer.from(result.buffer); + const filename = `${token}.xlsx`; + logger.info('[Excel Temp] Sending buffer', { bufferLength: buffer.length }); + + // Strong ETag based on buffer length + timestamp + const strongETag = `"${buffer.length.toString(16)}-${Date.now()}"`; + + res.setHeader('Content-Length', buffer.length); + res.setHeader( + 'Content-Type', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + ); + res.setHeader('Content-Disposition', `attachment; filename="${filename}"`); + res.setHeader('Access-Control-Expose-Headers', 'Content-Disposition'); + res.setHeader('Cache-Control', 'private, max-age=300'); + res.setHeader('X-Content-Type-Options', 'nosniff'); + res.setHeader('ETag', strongETag); + + // Remove headers that may break Excel + res.removeHeader('Vary'); + res.removeHeader('Access-Control-Allow-Credentials'); + res.removeHeader('X-Powered-By'); + + res.end(buffer); + + if (req.method === 'GET') { + const requestCount = await incrementExcelTempRequestCount(token); + if (requestCount !== null && requestCount >= 2) { + await deleteExcelTempToken(token); + } + } +};