diff --git a/README.md b/README.md index 205b261..6005bbf 100644 --- a/README.md +++ b/README.md @@ -16,20 +16,27 @@ Plugins Server is used by some built-in plugins on Typing Mind (e.g., Web Page R Plugins Server is open-sourced and is intended to be self-hosted by individual users for private use only. +**Note**: The Plugins Server only provides an endpoint for retrieving server-side processing results. To make the plugin work, you must also install a TypingMind's plugin configured to send requests to this server endpoint. + ## 🔌 How to use (for Typing Mind users) Two simple steps: -1. Deploy this repo on any hosting provider that supports NodeJS (e.g., Render.com, AWS, etc.). (We also provide a Dockerfile for easy deployment on Docker-supported hosting providers) -2. Use the server endpoint URL in your Settings page of Typing Mind's plugins. +1. Deploy this repo on any hosting provider that supports NodeJS (e.g., Render.com, AWS, etc.). (We also provide a Dockerfile for easy deployment on Docker-supported hosting providers). + +2. Install your desired TypingMind's plugin. Update the server endpoint URL in your Settings page. Follow this guide for detailed instructions: [How to Deploy Plugins Server on Render.com](https://docs.typingmind.com/plugins/plugins-server/how-to-deploy-plugins-server-on-render) +Follow this guide for setting up a TypingMind's plugin: [Build a TypingMind Plugin](https://docs.typingmind.com/plugins/build-a-typingmind-plugin) + ## List of available endpoints After deploying, visit your Plugins Server URL to see the list of available endpoints (served in Swagger UI). -Here are the latest endpoints from our public servers: https://plugins.typingmind.com/ (**Note**: this public server only hosts the API documentation. You cannot use this Public Server as your proxy. You must deploy your own Plugins Server to use all the available endpoints). +In your local development environment, visit http://localhost:3000 to access the page. + +**Note**: this public server only hosts the API documentation. You cannot use this Public Server as your proxy. You must deploy your own Plugins Server to use all the available endpoints. ## 🛠️ Development (for Typing Mind plugins developers) diff --git a/package-lock.json b/package-lock.json index dfe3c30..8bb680a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,8 +16,10 @@ "body-parser": "^1.20.2", "cheerio": "^1.0.0-rc.12", "cors": "^2.8.5", + "docx": "^9.1.0", "dotenv": "^16.4.5", "envalid": "^8.0.0", + "exceljs": "^4.4.0", "express": "^4.19.2", "express-rate-limit": "^7.2.0", "fs": "^0.0.1-security", @@ -25,8 +27,10 @@ "helmet": "^7.1.0", "http-status-codes": "^2.3.0", "jsdom": "^24.0.0", + "node-cron": "^3.0.3", "path": "^0.12.7", "pino-http": "^9.0.0", + "pptxgenjs": "^3.12.0", "swagger-ui-express": "^5.0.0", "uuidv4": "^6.2.13", "youtube-transcript": "^1.1.0", @@ -38,6 +42,7 @@ "@release-it/conventional-changelog": "^8.0.1", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", + "@types/node-cron": "^3.0.11", "@types/supertest": "^6.0.2", "@types/swagger-ui-express": "^4.1.6", "@typescript-eslint/eslint-plugin": "^7.13.1", @@ -878,6 +883,43 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@fast-csv/format": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", + "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==", + "dependencies": { + "@types/node": "^14.0.1", + "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" + } + }, + "node_modules/@fast-csv/format/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==" + }, + "node_modules/@fast-csv/parse": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-4.3.6.tgz", + "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==", + "dependencies": { + "@types/node": "^14.0.1", + "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" + } + }, + "node_modules/@fast-csv/parse/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -1820,6 +1862,12 @@ "undici-types": "~6.20.0" } }, + "node_modules/@types/node-cron": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.11.tgz", + "integrity": "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==", + "dev": true + }, "node_modules/@types/normalize-package-data": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", @@ -2389,6 +2437,110 @@ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", "dev": true }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "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.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/archiver-utils/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2442,6 +2594,11 @@ "node": ">=4" } }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" + }, "node_modules/async-retry": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", @@ -2477,8 +2634,7 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/base64-js": { "version": "1.5.1", @@ -2514,17 +2670,41 @@ "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", "dev": true }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==" + }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -2604,7 +2784,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -2625,7 +2804,6 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, "funding": [ { "type": "github", @@ -2645,12 +2823,36 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "engines": { + "node": "*" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "engines": { + "node": ">=0.2.0" + } + }, "node_modules/bundle-name": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", @@ -2791,6 +2993,17 @@ "node": ">=4" } }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/chalk": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", @@ -3102,11 +3315,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "node_modules/concat-stream": { "version": "2.0.0", @@ -3478,6 +3704,11 @@ "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", "dev": true }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -3533,6 +3764,30 @@ "typescript": ">=4" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3626,6 +3881,11 @@ "node": "*" } }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -3863,6 +4123,39 @@ "node": ">=6.0.0" } }, + "node_modules/docx": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/docx/-/docx-9.1.0.tgz", + "integrity": "sha512-XOtseSTRrkKN/sV5jNBqyLazyhNpWfaUhpuKc22cs+5DavNjRQvchnohb0g0S+x/96/D06U/i0/U/Gc4E5kwuQ==", + "dependencies": { + "@types/node": "^22.7.5", + "hash.js": "^1.1.7", + "jszip": "^3.10.1", + "nanoid": "^5.0.4", + "xml": "^1.0.1", + "xml-js": "^1.6.8" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/docx/node_modules/nanoid": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz", + "integrity": "sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -3950,6 +4243,41 @@ "node": ">= 0.4" } }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -4002,7 +4330,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, "dependencies": { "once": "^1.4.0" } @@ -4576,6 +4903,44 @@ "node": ">=0.8.x" } }, + "node_modules/exceljs": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.4.0.tgz", + "integrity": "sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==", + "dependencies": { + "archiver": "^5.0.0", + "dayjs": "^1.8.34", + "fast-csv": "^4.3.1", + "jszip": "^3.10.1", + "readable-stream": "^3.6.0", + "saxes": "^5.0.1", + "tmp": "^0.2.0", + "unzipper": "^0.10.11", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/exceljs/node_modules/saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/exceljs/node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "engines": { + "node": ">=14.14" + } + }, "node_modules/execa": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", @@ -4715,6 +5080,18 @@ "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==", "dev": true }, + "node_modules/fast-csv": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz", + "integrity": "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==", + "dependencies": { + "@fast-csv/format": "4.3.5", + "@fast-csv/parse": "4.3.6" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5020,11 +5397,15 @@ "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", "integrity": "sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==" }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { "version": "2.3.3", @@ -5040,6 +5421,73 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "deprecated": "This package is no longer supported.", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/fstream/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/fstream/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fstream/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/fstream/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -5393,8 +5841,7 @@ "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, "node_modules/graphemer": { "version": "1.4.0", @@ -5454,6 +5901,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -5584,6 +6040,11 @@ "node": ">=10.19.0" } }, + "node_modules/https": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz", + "integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==" + }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -5659,6 +6120,25 @@ "node": ">= 4" } }, + "node_modules/image-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.1.1.tgz", + "integrity": "sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ==", + "dependencies": { + "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -5708,7 +6188,6 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -6242,6 +6721,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -6477,6 +6961,44 @@ "node": "*" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -6512,6 +7034,44 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -6525,6 +7085,14 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -6587,6 +7155,11 @@ } } }, + "node_modules/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==" + }, "node_modules/listr2": { "version": "8.2.5", "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.5.tgz", @@ -6662,17 +7235,55 @@ "integrity": "sha512-kZzYOKspf8XVX5AvmQF94gQW0lejFVgb80G85bU4ZWzoJ6C03PQg3coYAUpSTpQWelrZELd3XWgHzw4Ck5kaIw==", "dev": true }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==" + }, "node_modules/lodash.escaperegexp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", - "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", - "dev": true + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==" + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==" + }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + }, + "node_modules/lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==" + }, + "node_modules/lodash.isnil": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", + "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==" }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" }, "node_modules/lodash.isstring": { "version": "4.0.1", @@ -6680,6 +7291,11 @@ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", "dev": true }, + "node_modules/lodash.isundefined": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", + "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==" + }, "node_modules/lodash.kebabcase": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", @@ -6716,11 +7332,15 @@ "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", "dev": true }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==" + }, "node_modules/lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", - "dev": true + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" }, "node_modules/lodash.uniqby": { "version": "4.7.0", @@ -7041,6 +7661,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, "node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -7060,7 +7685,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -7074,6 +7698,17 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/mlly": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.3.tgz", @@ -7185,6 +7820,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/node-cron": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz", + "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==", + "dependencies": { + "uuid": "8.3.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/normalize-package-data": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", @@ -7199,6 +7845,14 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/normalize-url": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz", @@ -7295,7 +7949,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } @@ -7527,6 +8180,11 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -7639,7 +8297,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -7982,6 +8639,30 @@ } } }, + "node_modules/pptxgenjs": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/pptxgenjs/-/pptxgenjs-3.12.0.tgz", + "integrity": "sha512-ZozkYKWb1MoPR4ucw3/aFYlHkVIJxo9czikEclcUVnS4Iw/M+r+TEwdlB3fyAWO9JY1USxJDt0Y0/r15IR/RUA==", + "dependencies": { + "@types/node": "^18.7.3", + "https": "^1.0.0", + "image-size": "^1.0.0", + "jszip": "^3.7.1" + } + }, + "node_modules/pptxgenjs/node_modules/@types/node": { + "version": "18.19.68", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.68.tgz", + "integrity": "sha512-QGtpFH1vB99ZmTa63K4/FU8twThj4fuVSBkGddTp7uIL/cuoLWIUSL2RcOaigBhfR+hg5pgGkBnkoOxrTVBMKw==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/pptxgenjs/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -8052,6 +8733,11 @@ "node": ">= 0.6.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, "node_modules/process-warning": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", @@ -8178,6 +8864,14 @@ "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" }, + "node_modules/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "dependencies": { + "inherits": "~2.0.3" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -8400,7 +9094,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -8410,6 +9103,25 @@ "node": ">= 6" } }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/readdirp": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", @@ -8885,6 +9597,11 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==" + }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -8988,6 +9705,11 @@ "node": ">= 0.4" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -9599,6 +10321,21 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -9835,6 +10572,14 @@ "node": ">=18" } }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "engines": { + "node": "*" + } + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -10548,6 +11293,50 @@ "node": ">= 0.8" } }, + "node_modules/unzipper": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, + "node_modules/unzipper/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/unzipper/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/unzipper/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/update-notifier": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-7.3.1.tgz", @@ -10610,8 +11399,7 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/util/node_modules/inherits": { "version": "2.0.3", @@ -11607,8 +12395,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { "version": "8.18.0", @@ -11642,6 +12429,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==" + }, + "node_modules/xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "dependencies": { + "sax": "^1.2.4" + }, + "bin": { + "xml-js": "bin/cli.js" + } + }, "node_modules/xml-name-validator": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", @@ -11763,6 +12566,79 @@ "node": ">=18.0.0" } }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "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.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/zip-stream/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/zip-stream/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/zod": { "version": "3.23.8", "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", diff --git a/package.json b/package.json index a92c681..304f9e0 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,10 @@ "body-parser": "^1.20.2", "cheerio": "^1.0.0-rc.12", "cors": "^2.8.5", + "docx": "^9.1.0", "dotenv": "^16.4.5", "envalid": "^8.0.0", + "exceljs": "^4.4.0", "express": "^4.19.2", "express-rate-limit": "^7.2.0", "fs": "^0.0.1-security", @@ -34,8 +36,10 @@ "helmet": "^7.1.0", "http-status-codes": "^2.3.0", "jsdom": "^24.0.0", + "node-cron": "^3.0.3", "path": "^0.12.7", "pino-http": "^9.0.0", + "pptxgenjs": "^3.12.0", "swagger-ui-express": "^5.0.0", "uuidv4": "^6.2.13", "youtube-transcript": "^1.1.0", @@ -47,6 +51,7 @@ "@release-it/conventional-changelog": "^8.0.1", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", + "@types/node-cron": "^3.0.11", "@types/supertest": "^6.0.2", "@types/swagger-ui-express": "^4.1.6", "@typescript-eslint/eslint-plugin": "^7.13.1", diff --git a/src/api-docs/openAPIDocumentGenerator.ts b/src/api-docs/openAPIDocumentGenerator.ts index b08b956..40a6788 100644 --- a/src/api-docs/openAPIDocumentGenerator.ts +++ b/src/api-docs/openAPIDocumentGenerator.ts @@ -1,11 +1,21 @@ import { OpenApiGeneratorV3, OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; -import { articleReaderRegistry } from '@/routes/articleReader/articleReaderRouter'; +import { excelGeneratorRegistry } from '@/routes/excelGenerator/excelGeneratorRouter'; import { healthCheckRegistry } from '@/routes/healthCheck/healthCheckRouter'; -import { transcriptRegistry } from '@/routes/youtubeTranscript/transcriptRouter'; +import { powerpointGeneratorRegistry } from '@/routes/powerpointGenerator/powerpointGeneratorRouter'; +import { articleReaderRegistry } from '@/routes/webPageReader/webPageReaderRouter'; +import { wordGeneratorRegistry } from '@/routes/wordGenerator/wordGeneratorRouter'; +import { youtubeTranscriptRegistry } from '@/routes/youtubeTranscript/youtubeTranscriptRouter'; export function generateOpenAPIDocument() { - const registry = new OpenAPIRegistry([healthCheckRegistry, transcriptRegistry, articleReaderRegistry]); + const registry = new OpenAPIRegistry([ + healthCheckRegistry, + youtubeTranscriptRegistry, + articleReaderRegistry, + powerpointGeneratorRegistry, + wordGeneratorRegistry, + excelGeneratorRegistry, + ]); const generator = new OpenApiGeneratorV3(registry.definitions); return generator.generateDocument({ diff --git a/src/api-docs/openAPIRequestBuilders.ts b/src/api-docs/openAPIRequestBuilders.ts new file mode 100644 index 0000000..33fb430 --- /dev/null +++ b/src/api-docs/openAPIRequestBuilders.ts @@ -0,0 +1,18 @@ +import { z } from 'zod'; + +export function createApiRequestBody( + schema: z.ZodTypeAny, + mediaType: string = 'application/json', + description: string = '', + required: boolean = true +) { + return { + content: { + [mediaType]: { + schema: schema, + }, + }, + description, + required, + }; +} diff --git a/src/routes/articleReader/articleReaderModel.ts b/src/routes/articleReader/articleReaderModel.ts deleted file mode 100644 index bfc40ef..0000000 --- a/src/routes/articleReader/articleReaderModel.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; -import { z } from 'zod'; - -extendZodWithOpenApi(z); - -export type Transcript = z.infer; -export const ArticleReaderSchema = z.object({ - title: z.string(), - content: z.string(), -}); diff --git a/src/routes/excelGenerator/excelGeneratorModel.ts b/src/routes/excelGenerator/excelGeneratorModel.ts new file mode 100644 index 0000000..642a1e9 --- /dev/null +++ b/src/routes/excelGenerator/excelGeneratorModel.ts @@ -0,0 +1,17 @@ +import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; +import { z } from 'zod'; + +extendZodWithOpenApi(z); + +// Define Word Generator Response Schema +export type ExcelGeneratorResponse = z.infer; +export const ExcelGeneratorResponseSchema = z.object({ + downloadUrl: z.string().openapi({ + description: 'The file path where the generated Word document is saved.', + }), +}); + +// Request Body Schema +export const ExcelGeneratorRequestBodySchema = z.object({}); + +export type ExcelGeneratorRequestBody = z.infer; diff --git a/src/routes/excelGenerator/excelGeneratorRouter.ts b/src/routes/excelGenerator/excelGeneratorRouter.ts new file mode 100644 index 0000000..8695c7d --- /dev/null +++ b/src/routes/excelGenerator/excelGeneratorRouter.ts @@ -0,0 +1,393 @@ +import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; +import * as ExcelJS from 'exceljs'; +import express, { Request, Response, Router } from 'express'; +import fs from 'fs'; +import { StatusCodes } from 'http-status-codes'; +import cron from 'node-cron'; +import path from 'path'; + +import { createApiRequestBody } from '@/api-docs/openAPIRequestBuilders'; +import { createApiResponse } from '@/api-docs/openAPIResponseBuilders'; +import { ResponseStatus, ServiceResponse } from '@/common/models/serviceResponse'; +import { handleServiceResponse } from '@/common/utils/httpHandlers'; + +import { ExcelGeneratorRequestBodySchema, ExcelGeneratorResponseSchema } from './excelGeneratorModel'; +export const COMPRESS = true; +export const excelGeneratorRegistry = new OpenAPIRegistry(); +excelGeneratorRegistry.register('ExcelGenerator', ExcelGeneratorResponseSchema); +excelGeneratorRegistry.registerPath({ + method: 'post', + path: '/excel-generator/generate', + tags: ['Excel Generator'], + request: { + body: createApiRequestBody(ExcelGeneratorRequestBodySchema, 'application/json'), + }, + responses: createApiResponse(ExcelGeneratorResponseSchema, 'Success'), +}); + +// Create folder to contains generated files +const exportsDir = path.join(__dirname, '../../..', 'excel-exports'); + +// Ensure the exports directory exists +if (!fs.existsSync(exportsDir)) { + fs.mkdirSync(exportsDir, { recursive: true }); +} + +// Cron job to delete files older than 1 hour +cron.schedule('0 * * * *', () => { + const now = Date.now(); + const oneHour = 60 * 60 * 1000; + // Read the files in the exports directory + fs.readdir(exportsDir, (err, files) => { + if (err) { + console.error(`Error reading directory ${exportsDir}:`, err); + return; + } + + files.forEach((file) => { + const filePath = path.join(exportsDir, file); + fs.stat(filePath, (err, stats) => { + if (err) { + console.error(`Error getting stats for file ${filePath}:`, err); + return; + } + + // Check if the file is older than 1 hour + if (now - stats.mtime.getTime() > oneHour) { + fs.unlink(filePath, (err) => { + if (err) { + console.error(`Error deleting file ${filePath}:`, err); + } else { + console.log(`Deleted file: ${filePath}`); + } + }); + } + }); + }); + }); +}); + +const serverUrl = process.env.RENDER_EXTERNAL_URL || 'http://localhost:3000'; + +interface SheetData { + sheetName: string; + tables: { + title: string; + startCell: string; + rows: { + type: string; // static_value or formula, + value: string; + }[][]; + columns: { name: string; type: string; format: string }[]; // types that have format, number, percent, currency + skipHeader: boolean; + }[]; +} + +interface ExcelConfig { + fontFamily: string; + tableTitleFontSize: number; + headerFontSize: number; + fontSize: number; + autoFitColumnWidth: boolean; + autoFilter: boolean; + borderStyle: ExcelJS.BorderStyle | null; // thin, double, dashed, thick + wrapText: boolean; +} + +const DEFAULT_EXCEL_CONFIGS: ExcelConfig = { + fontFamily: 'Calibri', + tableTitleFontSize: 13, + headerFontSize: 11, + fontSize: 11, + autoFitColumnWidth: true, + autoFilter: false, + wrapText: false, + borderStyle: null, +}; + +// Helper function to convert column letter (e.g., 'A') to column index (e.g., 1) +function columnLetterToNumber(letter: string): number { + let column = 0; + for (let i = 0; i < letter.length; i++) { + column = column * 26 + letter.charCodeAt(i) - 'A'.charCodeAt(0) + 1; + } + return column; +} + +// Helper function to auto-fit column widths based on content +function autoFitColumns( + worksheet: ExcelJS.Worksheet, + startRow: number, + rows: any[], + numColumns: number, + startCol: number +): void { + for (let colIdx = 0; colIdx < numColumns; colIdx++) { + let maxLength = 0; + + // Check the max length of the content in the column + rows.forEach((row) => { + const cellValue = row[colIdx]; + if (cellValue != null) { + const cellLength = String(cellValue).length; + maxLength = Math.max(maxLength, cellLength); + } + }); + + // Account for the header row + const headerCell = worksheet.getCell(startRow, startCol + colIdx).value; + if (headerCell != null) { + const headerLength = String(headerCell).length; + maxLength = Math.max(maxLength, headerLength); + } + + // Set the column width + worksheet.getColumn(startCol + colIdx).width = maxLength + 2; // Adding some padding + } +} + +export function execGenExcelFuncs(sheetsData: SheetData[], excelConfigs: ExcelConfig): string { + const workbook = new ExcelJS.Workbook(); + const borderConfigs = excelConfigs.borderStyle + ? { + top: { style: excelConfigs.borderStyle }, + left: { style: excelConfigs.borderStyle }, + bottom: { style: excelConfigs.borderStyle }, + right: { style: excelConfigs.borderStyle }, + } + : {}; + const titleAlignmentConfigs: any = { + horizontal: 'center', + vertical: 'middle', + wrapText: excelConfigs.wrapText, + }; + const titleFontConfigs: any = { + name: excelConfigs.fontFamily, + bold: true, + size: excelConfigs.tableTitleFontSize, + }; + const headerAligmentConfigs: any = { + wrapText: excelConfigs.wrapText, + horizontal: 'center', + vertical: 'middle', + }; + const headerFontConfigs: any = { + name: excelConfigs.fontFamily, + bold: true, + size: excelConfigs.headerFontSize, + }; + const cellAlignmentConfigs: any = { + wrapText: excelConfigs.wrapText, + }; + const cellFontConfigs: any = { + name: excelConfigs.fontFamily, + size: excelConfigs.fontSize, + }; + + sheetsData.forEach(({ sheetName, tables }) => { + const worksheet = workbook.addWorksheet(sheetName); + tables.forEach(({ startCell, title, rows = [], columns = [], skipHeader }) => { + const startCol = columnLetterToNumber(startCell[0]); // Convert column letter to index (e.g., 'A' -> 1) + const startRow = parseInt(startCell.slice(1)); // Extract the row number (e.g., 'A1' -> 1) + let rowIndex = startRow; // Set the initial row index to startRow for each table + + // Add table name row + if (title) { + const startCell = worksheet.getCell(rowIndex, startCol); + startCell.value = title; + worksheet.mergeCells(rowIndex, startCol, rowIndex, startCol + columns.length - 1); + startCell.alignment = titleAlignmentConfigs; + startCell.font = titleFontConfigs; + startCell.border = borderConfigs; + rowIndex++; // Move to the next row + } + + // Add column headers if not skipped + if (!skipHeader && columns) { + columns.forEach((col, colIdx) => { + const cell = worksheet.getCell(rowIndex, startCol + colIdx); + cell.value = col.name; + cell.alignment = headerAligmentConfigs; + cell.font = headerFontConfigs; + cell.border = borderConfigs; + }); + rowIndex++; // Increment row index after adding headers + } + + // Map headers to types + const columnTypes = columns.map((col: any) => col.type) || []; + const columnFormats = + columns?.map((col: any) => { + let format = undefined; + switch (col.type) { + case 'number': + format = col.format || undefined; + break; + case 'percent': + format = col.format || '0.00%'; // Default to percentage format + break; + case 'currency': + format = col.format || '$#,##0'; // Default to currency format + break; + case 'date': + format = col.format || undefined; + break; + } + return format; + }) || []; + + // Add rows with data types + rows.forEach((rowData) => { + rowData.forEach((cellData, colIdx) => { + const { type = 'static_value', value } = cellData; + const valueType = columnTypes[colIdx]; + const format = columnFormats[colIdx]; + let cellValue: any = value != null ? value : ''; // Handle empty/null values + const cell = worksheet.getCell(rowIndex, startCol + colIdx); + // Check if the value is a formula + if (type == 'formula') { + const formulaCell: any = { formula: cellValue }; // Handle formula + if (valueType === 'percent' || valueType === 'currency' || valueType === 'number' || valueType === 'date') { + cell.numFmt = format; // Apply number format + } + cell.value = formulaCell; + } else { + // Assign cell type based on the header definition + switch (valueType) { + case 'number': { + cellValue = !isNaN(Number(cellValue)) ? Math.round(Number(cellValue)) : cellValue; + cell.value = cellValue; + cell.numFmt = format || '0'; + break; + } + case 'boolean': { + cellValue = Boolean(cellValue); + cell.value = cellValue; + break; + } + case 'date': { + const parsedDate = new Date(cellValue); + cellValue = !isNaN(parsedDate.getTime()) ? parsedDate : cellValue; + cell.value = cellValue; + cell.numFmt = format || 'yyyy-mm-dd'; + break; + } + case 'percent': { + cellValue = !isNaN(Number(cellValue)) ? Number(cellValue) : cellValue; + cell.value = cellValue; + cell.numFmt = format || '0.00%'; + break; + } + case 'currency': { + cellValue = !isNaN(Number(cellValue)) ? Number(cellValue) : cellValue; + cell.value = cellValue; + cell.numFmt = format || '$#,##0'; + break; + } + case 'string': + default: { + cellValue = String(cellValue); + cell.value = cellValue; + break; + } + } + } + + // Apply styles to the cell + cell.font = cellFontConfigs; + cell.border = borderConfigs; + cell.alignment = cellAlignmentConfigs; + }); + rowIndex++; // Move to the next row + }); + + // Apply auto-filter + if (excelConfigs.autoFilter) { + const lastCol = startCol + columns.length - 1; // Calculate the last column + worksheet.autoFilter = { + from: { row: startRow + 1, column: startCol }, // Start from header row + to: { row: rowIndex - 1, column: lastCol }, // End at the last row of data + }; + } + + // Auto-fit column widths + if (excelConfigs.autoFitColumnWidth) { + autoFitColumns(worksheet, startRow, rows, columns.length, startCol); + } + }); + }); + + // Write the workbook to a file + const fileName = `excel-file-${new Date().toISOString().replace(/\D/gi, '')}.xlsx`; + const filePath = path.join(exportsDir, fileName); + + workbook.xlsx + .writeFile(filePath) + .then(() => { + console.log('File has been written to', filePath); + }) + .catch((err) => { + console.error('Error writing Excel file', err); + }); + + return fileName; +} + +export const excelGeneratorRouter: Router = (() => { + const router = express.Router(); + // Static route for downloading files + router.use('/downloads', express.static(exportsDir)); + + router.post('/generate', async (_req: Request, res: Response) => { + const { sheetsData, excelConfigs } = _req.body; // TODO: extract excel config object from request body + if (!sheetsData.length) { + const validateServiceResponse = new ServiceResponse( + ResponseStatus.Failed, + '[Validation Error] Sheets data is required!', + 'Please make sure you have sent the excel sheets content generated from TypingMind.', + StatusCodes.BAD_REQUEST + ); + return handleServiceResponse(validateServiceResponse, res); + } + + try { + const fileName = execGenExcelFuncs(sheetsData, { + fontFamily: excelConfigs.fontFamily ?? DEFAULT_EXCEL_CONFIGS.fontFamily, + tableTitleFontSize: excelConfigs.titleFontSize ?? DEFAULT_EXCEL_CONFIGS.tableTitleFontSize, + headerFontSize: excelConfigs.headerFontSize ?? DEFAULT_EXCEL_CONFIGS.headerFontSize, + fontSize: excelConfigs.fontSize ?? DEFAULT_EXCEL_CONFIGS.fontSize, + autoFilter: excelConfigs.autoFilter ?? DEFAULT_EXCEL_CONFIGS.autoFilter, + borderStyle: + excelConfigs.borderStyle || excelConfigs.borderStyle !== 'none' + ? excelConfigs.borderStyle + : DEFAULT_EXCEL_CONFIGS.borderStyle, + wrapText: excelConfigs.wrapText ?? DEFAULT_EXCEL_CONFIGS.wrapText, + autoFitColumnWidth: excelConfigs.autoFitColumnWidth ?? DEFAULT_EXCEL_CONFIGS.autoFitColumnWidth, + }); + + const serviceResponse = new ServiceResponse( + ResponseStatus.Success, + 'File generated successfully', + { + downloadUrl: `${serverUrl}/excel-generator/downloads/${fileName}`, + }, + StatusCodes.OK + ); + return handleServiceResponse(serviceResponse, res); + } catch (error) { + const errorMessage = (error as Error).message; + let responseObject = ''; + if (errorMessage.includes('')) { + responseObject = `Sorry, we couldn't generate excel file.`; + } + const errorServiceResponse = new ServiceResponse( + ResponseStatus.Failed, + `Error ${errorMessage}`, + responseObject, + StatusCodes.INTERNAL_SERVER_ERROR + ); + return handleServiceResponse(errorServiceResponse, res); + } + }); + return router; +})(); diff --git a/src/routes/powerpointGenerator/powerpointGeneratorModel.ts b/src/routes/powerpointGenerator/powerpointGeneratorModel.ts new file mode 100644 index 0000000..c7af1f5 --- /dev/null +++ b/src/routes/powerpointGenerator/powerpointGeneratorModel.ts @@ -0,0 +1,116 @@ +import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; +import { z } from 'zod'; + +extendZodWithOpenApi(z); + +export type PowerPointGeneratorResponse = z.infer; +export const PowerpointGeneratorResponseSchema = z.object({ + filepath: z.string(), +}); + +export type PowerPointGeneratorRequestBody = z.infer; +export const PowerpointGeneratorRequestBodySchema = z.object({ + slides: z + .array( + z.object({ + type: z + .enum(['title_slide', 'content_slide', 'table_slide', 'chart_slide']) + .describe( + "The type of slide, either 'title_slide' (title-only), 'content_slide' (title and content), 'table_slide' (title with tabular content), or 'chart_slide' (title with chart content)." + ), + title: z.string().describe('The title of the slide.'), + content: z + .array( + z + .array(z.string()) + .describe("It could be a row in the table for 'table_slide'. Each row is an array of strings.") + ) + .optional() + .describe( + "The content of the slide. For 'title_slide', this is not required. For 'content_slide', it can be a string (paragraph) or an array of strings (list). For 'table_slide', it is an array of rows, where each row is an array of strings." + ), + subtitle: z + .string() + .optional() + .describe( + 'The subtitle of the slide, which provides additional information to support the title. Only title_slide has the subtitle.' + ), + chartContent: z + .object({ + type: z + .enum(['pie', 'bar', 'line', 'doughnut']) + .describe("The type of chart, either 'pie', 'doughnut', 'bar', or 'line'."), + data: z + .array( + z.object({ + name: z.string().describe('The name of the chart series.'), + labels: z.array(z.string()).describe('The labels for the chart (e.g., categories, time periods).'), + values: z.array(z.number()).describe('The values for each label in the chart.'), + }) + ) + .describe('The chart data. Depending on the type, this can be an array of labels and values.'), + }) + .optional() + .describe( + "The chart data for 'chart_slide'. This includes various types of chart data such as pie charts, doughnut charts, bar charts, and line charts." + ), + }) + ) + .describe('A list of slides, where each slide includes its type, title, and content.'), + slideConfig: z + .object({ + layout: z + .enum(['LAYOUT_WIDE', 'LAYOUT_16x9', 'LAYOUT_16x10', 'LAYOUT_4x3']) + .describe( + 'Defines the slide layout. Options include LAYOUT_WIDE (default), LAYOUT_16x9, LAYOUT_16x10, and LAYOUT_4x3.' + ), + titleFontSize: z.number().optional().describe('Font size for the title slide. Default is 52 pt.'), + headerFontSize: z.number().optional().describe('Font size for headers in content slides. Default is 32 pt.'), + bodyFontSize: z.number().optional().describe('Font size for the main content text. Default is 24 pt.'), + fontFamily: z + .enum([ + 'Calibri', + 'Arial', + 'Courier New', + 'Georgia', + 'Helvetica', + 'Impact', + 'Lucida Console', + 'Tahoma', + 'Times New Roman', + 'Trebuchet MS', + 'Verdana', + 'Comic Sans MS', + 'Franklin Gothic Medium', + 'Century Gothic', + 'Gill Sans', + 'Garamond', + 'Palatino Linotype', + 'Segoe UI', + 'Book Antiqua', + 'Symbol', + 'Monospace', + 'Webdings', + 'Wingdings', + ]) + .describe("Font family for slide text. Default is 'Calibri'."), + backgroundColor: z.string().describe('Hex color for slide background. Default is #FFFFFF.'), + textColor: z.string().describe('Hex color for slide text. Default is #000000.'), + showFooter: z.boolean().describe('Boolean to display footer. Default is false.'), + showSlideNumber: z.boolean().describe('Boolean to display slide numbers. Default is false.'), + footerBackgroundColor: z.string().describe('Hex color for footer background. Default is #003B75.'), + footerText: z.string().optional().describe("Text content for the footer. Default is 'footer text'."), + footerTextColor: z.string().describe('Hex color for footer text. Default is #FFFFFF.'), + footerFontSize: z.number().optional().describe('Font size for footer text. Default is 10 pt.'), + showTableBorder: z.boolean().describe('Boolean to display table borders. Default is true.'), + tableHeaderBackgroundColor: z.string().describe('Hex color for table header background. Default is #003B75.'), + tableHeaderTextColor: z.string().describe('Hex color for table header text. Default is #FFFFFF.'), + tableBorderThickness: z.number().optional().describe('Thickness of table borders in points. Default is 1 pt.'), + tableBorderColor: z.string().describe('Hex color for table borders. Default is #000000.'), + tableFontSize: z.number().optional().describe('Font size for text inside tables. Default is 14 pt.'), + tableTextColor: z.string().describe('Hex color for table text. Default is #000000.'), + }) + .describe( + 'Configuration settings for customizing slide properties, including layout, font sizes, font family, colors, footer settings, and table appearance.' + ), +}); diff --git a/src/routes/powerpointGenerator/powerpointGeneratorRouter.ts b/src/routes/powerpointGenerator/powerpointGeneratorRouter.ts new file mode 100644 index 0000000..8d0f543 --- /dev/null +++ b/src/routes/powerpointGenerator/powerpointGeneratorRouter.ts @@ -0,0 +1,542 @@ +import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; +import express, { Request, Response, Router } from 'express'; +import fs from 'fs'; +import { StatusCodes } from 'http-status-codes'; +import cron from 'node-cron'; +import path from 'path'; +import pptxgen from 'pptxgenjs'; + +import { createApiRequestBody } from '@/api-docs/openAPIRequestBuilders'; +import { createApiResponse } from '@/api-docs/openAPIResponseBuilders'; +import { ResponseStatus, ServiceResponse } from '@/common/models/serviceResponse'; +import { handleServiceResponse } from '@/common/utils/httpHandlers'; + +import { PowerpointGeneratorRequestBodySchema, PowerpointGeneratorResponseSchema } from './powerpointGeneratorModel'; +export const COMPRESS = true; + +// API Doc definition +export const powerpointGeneratorRegistry = new OpenAPIRegistry(); +powerpointGeneratorRegistry.register('PowerpointGenerator', PowerpointGeneratorResponseSchema); +powerpointGeneratorRegistry.registerPath({ + method: 'post', + path: '/powerpoint-generator/generate', + tags: ['Powerpoint Generator'], + request: { + body: createApiRequestBody(PowerpointGeneratorRequestBodySchema, 'application/json'), + }, + responses: createApiResponse(PowerpointGeneratorResponseSchema, 'Success'), +}); + +// Create folder to contains generated files +const exportsDir = path.join(__dirname, '../../..', 'powerpoint-exports'); +// Ensure the exports directory exists +if (!fs.existsSync(exportsDir)) { + fs.mkdirSync(exportsDir, { recursive: true }); +} + +// Cron job to delete files older than 1 hour +cron.schedule('0 * * * *', () => { + const now = Date.now(); + const oneHour = 60 * 60 * 1000; + // Read the files in the exports directory + fs.readdir(exportsDir, (err, files) => { + if (err) { + console.error(`Error reading directory ${exportsDir}:`, err); + return; + } + + files.forEach((file) => { + const filePath = path.join(exportsDir, file); + fs.stat(filePath, (err, stats) => { + if (err) { + console.error(`Error getting stats for file ${filePath}:`, err); + return; + } + + // Check if the file is older than 1 hour + if (now - stats.mtime.getTime() > oneHour) { + fs.unlink(filePath, (err) => { + if (err) { + console.error(`Error deleting file ${filePath}:`, err); + } else { + console.log(`Deleted file: ${filePath}`); + } + }); + } + }); + }); + }); +}); + +const serverUrl = process.env.RENDER_EXTERNAL_URL || 'http://localhost:3000'; + +// Define configurable options for layout, font size, and font family +const defaultSlideConfig = { + layout: 'LAYOUT_WIDE', // Default: LAYOUT_WIDE, enum: LAYOUT_16x9 10 x 5.625 inches, LAYOUT_16x10 10 x 6.25 inches, LAYOUT_16x10 10 x 6.25 inches, LAYOUT_4x3 10 x 7.5 inches + titleFontSize: 44, // Emphasize the main topic in Title Slide + headerFontSize: 32, // The slide headers in the Content Slide + bodyFontSize: 22, // The main text font size + fontFamily: 'Calibri', // Default font family for the slide, Calibri, Arial + backgroundColor: '#FFFFFF', // Default background color + textColor: '#000000', // Text color + showFooter: false, // Display footer or not + showSlideNumber: false, // Display slide number or not + footerBackgroundColor: '#003B75', // Default background color + footerText: 'footer text', // Footer text content. + footerTextColor: '#FFFFFF', // Default footer color + footerFontSize: 10, // Default footer font size + showTableBorder: true, // Show table border or not + tableHeaderBackgroundColor: '#003B75', // Background of table header, // Dark blue background for headers + tableHeaderTextColor: '#FFFFFF', // Table header color + tableBorderThickness: 1, // pt: 1, // Border thickness + tableBorderColor: '#000000', // Black border + tableFontSize: 14, // Font size inside the table + tableTextColor: '#000000', // Text color inside the table +}; + +// Helper function to detect number, percent, or currency +function detectType(value: string) { + // Regular expression patterns + const numberPattern = /^[+-]?\d+(\.\d+)?$/; // Matches general numbers + const percentPattern = /^[+-]?\d+(\.\d+)?%$/; // Matches percentages + const currencyPattern = /^[€$]\d+(\.\d+)?$/; // Matches currency values (e.g., $, €) + + if (currencyPattern.test(value)) { + return 'currency'; + } else if (percentPattern.test(value)) { + return 'percent'; + } else if (numberPattern.test(value)) { + return 'number'; + } else { + return 'text'; // Default to text if no match + } +} + +// Helper function to get alignment based on detected type +function getAlignment(type: string) { + switch (type) { + case 'number': + case 'percent': + case 'currency': + return 'center'; + default: + return 'left'; + } +} + +function defineMasterSlides(pptx: any, config: any) { + const slideNumberConfig = + config.showFooter && config.showSlideNumber + ? { + x: 0.0, + y: 6.9, + h: 0.6, + align: 'center', // Center text horizontally + valign: 'middle', // Center text vertically + fontSize: config.footerFontSize, + fontFace: config.fontFamily, + color: config.footerTextColor, + bold: true, + } + : undefined; + + // Define the TITLE_SLIDE MasterSlide with vertically aligned header and subheader + pptx.defineSlideMaster({ + title: 'TITLE_SLIDE', + slideNumber: slideNumberConfig, + objects: [ + { + // Header (Section Title) + placeholder: { + options: { + name: 'header', + type: 'title', + x: '10%', // 10% from the left side of the slide for responsiveness + y: '20%', // Positioned 20% from the top of the slide + w: '80%', // Width adjusted to 80% of the slide width + h: 0.75, // Fixed height for the header + align: 'center', // Center-align the text horizontally + valign: 'middle', // Vertically align the text to the middle + margin: 0, + fontSize: config.titleFontSize, + fontFace: config.fontFamily, // Set font face + color: config.textColor, + }, + text: '(title placeholer)', // Placeholder text for the title + }, + }, + { + // Subheader (Subsection Title) + placeholder: { + options: { + name: 'subheader', + type: 'body', + x: '10%', // 10% from the left side of the slide for responsiveness + y: '35%', // Positioned 30% from the top, below the header + w: '80%', // Width adjusted to 80% of the slide width + h: 1.25, // Fixed height for the subheader + align: 'center', // Center-align the text horizontally + valign: 'middle', // Vertically align the text to the middle + margin: 0, + fontSize: config.headerFontSize, + fontFace: config.fontFamily, // Set font face + color: config.textColor, + }, + text: '(subtitle placeholder)', // Placeholder text for the subheader + }, + }, + // Footer background + config.showFooter + ? { rect: { x: 0.0, y: 6.9, w: '100%', h: 0.6, fill: { color: config.footerBackgroundColor } } } + : {}, + // Footer Text + config.showFooter + ? { + placeholder: { + options: { + name: 'footer', + type: 'body', + x: 0.0, + y: 6.9, + w: '100%', // Extend across the full width of the slide + h: 0.6, // Match the height of the footer background + align: 'center', // Center text horizontally + valign: 'middle', // Center text vertically + color: config.footerTextColor, // White text for contrast + fontSize: config.footerFontSize, // Suitable size for footer text + fontFace: config.fontFamily, // Set font face + }, + text: config.footerText, // Default footer text + }, + } + : {}, + ], + }); + + pptx.defineSlideMaster({ + title: 'MASTER_SLIDE', + background: { + color: config.backgroundColor, + }, + margin: [0.5, 0.25, 1.0, 0.25], // top, left, bottom, right + slideNumber: slideNumberConfig, + objects: [ + // Header (Title) + { + placeholder: { + options: { + name: 'header', + type: 'title', + x: '10%', + y: '5%', + w: '80%', + h: 1.0, + margin: 0.2, + align: 'center', + valign: 'middle', + color: config.textColor, + fontSize: config.headerFontSize, // Dynamically chosen for visibility + fontFace: config.fontFamily, // Set font face + }, + text: '(slide title placeholder)', // Default placeholder for title + }, + }, + // Content (Body) + { + placeholder: { + options: { + name: 'body', + type: 'body', + x: '10%', + y: '20%', + w: '80%', + h: config.showFooter ? '60%' : '70%', // Responsive height + color: config.textColor, + fontSize: config.bodyFontSize, // Suitable for body text + fontFace: config.fontFamily, // Set font face + }, + text: '(supports custom placeholder text!)', + }, + }, + // Footer background + config.showFooter + ? { rect: { x: 0.0, y: 6.9, w: '100%', h: 0.6, fill: { color: config.footerBackgroundColor } } } + : {}, + // Footer Text + config.showFooter + ? { + placeholder: { + options: { + name: 'footer', + type: 'body', + x: 0.0, + y: 6.9, + w: '100%', // Extend across the full width of the slide + h: 0.6, // Match the height of the footer background + align: 'center', // Center text horizontally + valign: 'middle', // Center text vertically + color: config.footerTextColor, // White text for contrast + fontSize: config.footerFontSize, // Suitable size for footer text + fontFace: config.fontFamily, // Set font face + }, + text: config.footerText, // Default footer text + }, + } + : {}, + ], + }); +} + +async function execGenSlidesFuncs(slides: any[], config: any) { + // STEP 1: Instantiate new PptxGenJS object + const pptx = new pptxgen(); + + // STEP 2: Set layout + pptx.layout = config.layout; + + // STEP 3: Create Master Slides (from the old `pptxgen.masters.js` file - `gObjPptxMasters` items) + defineMasterSlides(pptx, config); + + // STEP 4: Run requested test + slides.forEach((slideData, index) => { + const { type, title, subtitle, chartContent, content = [] } = slideData; + if (!type || !title) { + throw new Error(`Slide ${index + 1} is missing required properties: type or title.`); + } + + if (type === 'title_slide') { + // Add a title slide + const slide = pptx.addSlide({ masterName: 'TITLE_SLIDE' }); + slide.addText(title, { placeholder: 'header' }); + if (subtitle) { + slide.addText(subtitle, { placeholder: 'subheader' }); + } + } else if (type === 'content_slide') { + // Add a content slide + const slide = pptx.addSlide({ masterName: 'MASTER_SLIDE' }); + slide.addText(title, { placeholder: 'header' }); + + // Add content based on contentType + if (content.length === 1) { + slide.addText(content[0], { placeholder: 'body' }); + } else if (content.length > 1) { + const bullets = content.map((item: any) => ({ + text: item, + options: { bullet: true, valign: 'top' }, + })); + + slide.addText(bullets, { placeholder: 'body', valign: 'top' }); + } else { + throw new Error(`Invalid content length on slide ${index + 1}`); + } + } else if (type === 'table_slide') { + // Table Slide + const slide = pptx.addSlide({ masterName: 'MASTER_SLIDE' }); + slide.addText(title, { placeholder: 'header' }); + + // Map content to tableData with alternating row colors and alignment + const tableHeaders = content[0]; + const tableData = [ + tableHeaders.map((header: any) => ({ + text: header, + options: { + bold: true, + color: config.tableHeaderTextColor, + fill: config.tableHeaderBackgroundColor, + align: 'center', + valign: 'middle', + }, + })), + ...content.slice(1).map((row: any, rowIndex: number) => + row.map((cell: any) => { + const cellType = detectType(cell); + const align = getAlignment(cellType); + + return { + text: cell, + options: { + fill: rowIndex % 2 === 0 ? 'E8F1FA' : 'DDEBF7', // Alternating row colors + align, + valign: 'middle', + color: config.tableTextColor, // Black text + }, + }; + }) + ), + ]; + + const tableBorderConfigs = config.showTableBorder + ? { + pt: config.tableBorderThickness, // Border thickness + color: config.tableBorderColor, // Black border + } + : undefined; + + slide.addTable(tableData, { + x: '10%', // Position aligned with placeholder + y: '20%', + w: '80%', // Table width + h: '60%', // Table height + border: tableBorderConfigs, + fontSize: 14, // Font size for table text + placeholder: 'body', + } as any); + } else if (type === 'chart_slide' && chartContent) { + // Add a slide with the custom master slide + const slide = pptx.addSlide({ masterName: 'MASTER_SLIDE' }); + + // Set the slide title + slide.addText(title, { + placeholder: 'header', + }); + + // Handle chart content based on chart type + const { data: chartData, type: chartType } = chartContent; + // Default to line chart if no type is provided + if (chartType === 'pie') { + slide.addChart(pptx.ChartType.pie, chartData, { + x: '10%', // Position aligned with placeholder + y: '20%', + w: '80%', // Table width + h: '60%', // Table height + showLegend: true, + showCategoryAxis: true, + showValueAxis: true, + showPercent: true, + dataLabelPosition: 'outside', + placeholder: 'body', + } as any); + } else if (chartType === 'line') { + slide.addChart(pptx.ChartType.line, chartData, { + x: '10%', // Position aligned with placeholder + y: '20%', + w: '80%', // Table width + h: '60%', // Table height + showLegend: true, + showCategoryAxis: true, + showValueAxis: true, + dataLabelPosition: 'outside', + placeholder: 'body', + } as any); + } else if (chartType === 'bar') { + slide.addChart(pptx.ChartType.bar, chartData, { + x: '10%', // Position aligned with placeholder + y: '20%', + w: '80%', // Table width + h: '60%', // Table height + showLegend: true, + showCategoryAxis: true, + showValueAxis: true, + placeholder: 'body', + } as any); + } else if (chartType === 'doughnut') { + slide.addChart(pptx.ChartType.doughnut, chartData, { + x: '10%', // Position aligned with placeholder + y: '20%', + w: '80%', // Table width + h: '60%', // Table height + showPercent: true, + showLegend: true, + placeholder: 'body', + } as any); + } else { + throw new Error(`Invalid chart type: ${chartType}`); + } + } + }); + + const fileName = `your-presentation-${new Date().toISOString().replace(/\D/gi, '')}`; + const filePath = path.join(exportsDir, fileName); + + await pptx.writeFile({ + fileName: filePath, + compression: COMPRESS, + }); + + return fileName + '.pptx'; +} + +export const powerpointGeneratorRouter: Router = (() => { + const router = express.Router(); + // Static route for downloading files + router.use('/downloads', express.static(exportsDir)); + + router.post('/generate', async (_req: Request, res: Response) => { + const { slides = [], slideConfig = {} } = _req.body; + if (!slides.length) { + const validateServiceResponse = new ServiceResponse( + ResponseStatus.Failed, + '[Validation Error] Presentation slides is required!', + 'Please make sure you have sent the slide content generated from TypingMind.', + StatusCodes.BAD_REQUEST + ); + return handleServiceResponse(validateServiceResponse, res); + } + + try { + const fileName = await execGenSlidesFuncs(slides, { + layout: slideConfig.layout === '' ? defaultSlideConfig.layout : slideConfig.layout, // Default: LAYOUT_WIDE, enum: LAYOUT_16x9 10 x 5.625 inches, LAYOUT_16x10 10 x 6.25 inches, LAYOUT_4x3 10 x 7.5 inches + titleFontSize: slideConfig.titleFontSize === 0 ? defaultSlideConfig.titleFontSize : slideConfig.titleFontSize, // Default: 52, Emphasize the main topic in Title Slide + headerFontSize: + slideConfig.headerFontSize === 0 ? defaultSlideConfig.headerFontSize : slideConfig.headerFontSize, // Default: 32, The slide headers in the Content Slide + bodyFontSize: slideConfig.bodyFontSize === 0 ? defaultSlideConfig.bodyFontSize : slideConfig.bodyFontSize, // Default: 24, The main text font size + fontFamily: slideConfig.fontFamily === '' ? defaultSlideConfig.fontFamily : slideConfig.fontFamily, // Default: 'Calibri', Default font family for the slide, Calibri, Arial + backgroundColor: + slideConfig.backgroundColor === '' ? defaultSlideConfig.backgroundColor : slideConfig.backgroundColor, // Default: '#FFFFFF', Default background color + textColor: slideConfig.textColor === '' ? defaultSlideConfig.textColor : slideConfig.textColor, // Default: '#000000', Text color + showFooter: slideConfig.showFooter ?? defaultSlideConfig.showFooter, // Default: false, Display footer or not + showSlideNumber: slideConfig.showSlideNumber ?? defaultSlideConfig.showSlideNumber, // Default: false, Display slide number or not + footerBackgroundColor: + slideConfig.footerBackgroundColor === '' + ? defaultSlideConfig.footerBackgroundColor + : slideConfig.footerBackgroundColor, // Default: '#003B75', Default footer background color + footerText: slideConfig.footerText === '' ? defaultSlideConfig.footerText : slideConfig.footerText, // Default: 'footer text', Footer text content. + footerTextColor: + slideConfig.footerTextColor === '' ? defaultSlideConfig.footerTextColor : slideConfig.footerTextColor, // Default: '#FFFFFF', Default footer text color + footerFontSize: + slideConfig.footerFontSize === 0 ? defaultSlideConfig.footerFontSize : slideConfig.footerFontSize, // Default: 10, Default footer font size + showTableBorder: slideConfig.showTableBorder ?? defaultSlideConfig.showTableBorder, // Default: true, Show table border or not + tableHeaderBackgroundColor: + slideConfig.tableHeaderBackgroundColor === '' + ? defaultSlideConfig.tableHeaderBackgroundColor + : slideConfig.tableHeaderBackgroundColor, // Default: '#003B75', Dark blue background for headers + tableHeaderTextColor: + slideConfig.tableHeaderTextColor === '' + ? defaultSlideConfig.tableHeaderTextColor + : slideConfig.tableHeaderTextColor, // Default: '#FFFFFF', Table header text color + tableBorderThickness: + slideConfig.tableBorderThickness === 0 + ? defaultSlideConfig.tableBorderThickness + : slideConfig.tableBorderThickness, // Default: 1 pt, Border thickness + tableBorderColor: + slideConfig.tableBorderColor === '' ? defaultSlideConfig.tableBorderColor : slideConfig.tableBorderColor, // Default: '#000000', Black border + tableFontSize: slideConfig.tableFontSize === 0 ? defaultSlideConfig.tableFontSize : slideConfig.tableFontSize, // Default: 14, Font size inside the table + tableTextColor: + slideConfig.tableTextColor === '' ? defaultSlideConfig.tableTextColor : slideConfig.tableTextColor, // Default: '#000000', Text color inside the table + }); + const serviceResponse = new ServiceResponse( + ResponseStatus.Success, + 'File generated successfully', + { + downloadUrl: `${serverUrl}/powerpoint-generator/downloads/${fileName}`, + }, + StatusCodes.OK + ); + return handleServiceResponse(serviceResponse, res); + } catch (error) { + const errorMessage = (error as Error).message; + let responseObject = ''; + if (errorMessage.includes('')) { + responseObject = `Sorry, we couldn't generate powerpoint file.`; + } + const errorServiceResponse = new ServiceResponse( + ResponseStatus.Failed, + `Error ${errorMessage}`, + responseObject, + StatusCodes.INTERNAL_SERVER_ERROR + ); + return handleServiceResponse(errorServiceResponse, res); + } + }); + return router; +})(); diff --git a/src/routes/webPageReader/webPageReaderModel.ts b/src/routes/webPageReader/webPageReaderModel.ts new file mode 100644 index 0000000..8af4684 --- /dev/null +++ b/src/routes/webPageReader/webPageReaderModel.ts @@ -0,0 +1,15 @@ +import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; +import { z } from 'zod'; + +extendZodWithOpenApi(z); + +export type WebPageReaderResponse = z.infer; +export const WebPageReaderResponseSchema = z.object({ + title: z.string(), + content: z.string(), +}); + +export type WebPageReaderRequestParam = z.infer; +export const WebPageReaderRequestParamSchema = z.object({ + url: z.string().describe('The URL of the web page to retrieve content from'), +}); diff --git a/src/routes/articleReader/articleReaderRouter.ts b/src/routes/webPageReader/webPageReaderRouter.ts similarity index 80% rename from src/routes/articleReader/articleReaderRouter.ts rename to src/routes/webPageReader/webPageReaderRouter.ts index 4e76679..5905454 100644 --- a/src/routes/articleReader/articleReaderRouter.ts +++ b/src/routes/webPageReader/webPageReaderRouter.ts @@ -10,10 +10,10 @@ import { createApiResponse } from '@/api-docs/openAPIResponseBuilders'; import { ResponseStatus, ServiceResponse } from '@/common/models/serviceResponse'; import { handleServiceResponse } from '@/common/utils/httpHandlers'; -import { ArticleReaderSchema } from './articleReaderModel'; +import { WebPageReaderRequestParamSchema, WebPageReaderResponseSchema } from './webPageReaderModel'; export const articleReaderRegistry = new OpenAPIRegistry(); -articleReaderRegistry.register('ArticleReader', ArticleReaderSchema); +articleReaderRegistry.register('Web Page Reader', WebPageReaderResponseSchema); const removeUnwantedElements = (_cheerio: any) => { const elementsToRemove = [ @@ -59,17 +59,20 @@ const fetchAndCleanContent = async (url: string) => { return { title, content: article ? article.textContent : '' }; }; -export const articleReaderRouter: Router = (() => { +export const webPageReaderRouter: Router = (() => { const router = express.Router(); articleReaderRegistry.registerPath({ method: 'get', - path: '/content', - tags: ['Article Reader'], - responses: createApiResponse(ArticleReaderSchema, 'Success'), + path: '/web-page-reader/get-content', + tags: ['Web Page Reader'], + request: { + query: WebPageReaderRequestParamSchema, + }, + responses: createApiResponse(WebPageReaderResponseSchema, 'Success'), }); - router.get('/', async (_req: Request, res: Response) => { + router.get('/get-content', async (_req: Request, res: Response) => { const { url } = _req.query; if (typeof url !== 'string') { @@ -80,7 +83,7 @@ export const articleReaderRouter: Router = (() => { const content = await fetchAndCleanContent(url); const serviceResponse = new ServiceResponse( ResponseStatus.Success, - 'Service is healthy', + 'Content fetched successfully', content, StatusCodes.OK ); diff --git a/src/routes/wordGenerator/wordGeneratorModel.ts b/src/routes/wordGenerator/wordGeneratorModel.ts new file mode 100644 index 0000000..db0f2c5 --- /dev/null +++ b/src/routes/wordGenerator/wordGeneratorModel.ts @@ -0,0 +1,138 @@ +import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; +import { z } from 'zod'; + +extendZodWithOpenApi(z); + +// Define Word Generator Response Schema +export type WordGeneratorResponse = z.infer; +export const WordGeneratorResponseSchema = z.object({ + filepath: z.string().openapi({ + description: 'The file path where the generated Word document is saved.', + }), +}); + +// Define Cell Schema +const CellSchema = z.object({ + text: z.string().optional().openapi({ + description: 'Text content within a cell.', + }), +}); + +// Define Row Schema +const RowSchema = z.object({ + cells: z.array(CellSchema).optional().openapi({ + description: 'Array of cells within a row.', + }), +}); + +// Define Content Schema +const ContentSchema = z.object({ + type: z.enum(['paragraph', 'listing', 'table', 'pageBreak', 'emptyLine']).openapi({ + description: 'Type of the content item.', + }), + text: z.string().optional().openapi({ + description: 'Text content for paragraphs or listings.', + }), + items: z.array(z.string()).optional().openapi({ + description: 'Items in a list for listing type content.', + }), + headers: z.array(z.string()).optional().openapi({ + description: 'Headers for table content.', + }), + rows: z.array(RowSchema).optional().openapi({ + description: 'Rows for table content.', + }), +}); + +// Define the base schema for a section +const SectionSchema = z.object({ + sectionId: z.string().openapi({ + description: 'A unique identifier for the section.', + }), + heading: z.string().optional().openapi({ + description: 'Heading of the section.', + }), + headingLevel: z.number().int().min(1).optional().openapi({ + description: 'Level of the heading (e.g., 1 for main heading, 2 for subheading).', + }), + parentSectionId: z.string().optional().openapi({ + description: + 'The unique identifier of the parent section, if this section is a child of another. Leave empty if this section has no parent.', + }), + content: z.array(ContentSchema).optional().openapi({ + description: 'Content contained within the section, including paragraphs, tables, etc.', + }), +}); + +// Request Body Schema +export const WordGeneratorRequestBodySchema = z.object({ + title: z.string().openapi({ + description: 'Title of the document.', + }), + header: z.object({ + text: z.string().openapi({ + description: 'Text content for the header.', + }), + alignment: z.enum(['left', 'center', 'right']).default('left').openapi({ + description: 'Alignment of the header text.', + }), + }), + footer: z.object({ + text: z.string().openapi({ + description: 'Text content for the footer.', + }), + alignment: z.enum(['left', 'center', 'right']).default('left').openapi({ + description: 'Alignment of the footer text.', + }), + }), + sections: z.array(SectionSchema).openapi({ + description: 'Sections of the document, which may include sub-sections.', + }), + wordConfig: z + .object({ + fontSize: z.number().default(12).openapi({ + description: 'Font size for the slides, default is 12 pt.', + }), + lineHeight: z.enum(['1', '1.15', '1.25', '1.5', '2']).default('1').openapi({ + description: 'Line height for text content.', + }), + fontFamily: z + .enum(['Arial', 'Calibri', 'Times New Roman', 'Courier New', 'Verdana', 'Tahoma', 'Georgia', 'Comic Sans MS']) + .default('Arial') + .openapi({ + description: 'Font family for the slides, default is Arial.', + }), + showPageNumber: z.boolean().default(false).openapi({ + description: 'Option to display page numbers in the document.', + }), + showTableOfContent: z.boolean().default(false).openapi({ + description: 'Option to display a table of contents.', + }), + showNumberingInHeader: z.boolean().default(false).openapi({ + description: 'Option to display numbering in the header.', + }), + numberingReference: z + .enum([ + '1.1.1.1 (Decimal)', + 'I.1.a.i (Roman -> Decimal > Lower Letter -> Lower Roman)', + 'I.A.1.a (Roman -> Upper Letter -> Decimal -> Lower Letter)', + '1)a)i)(i) (Decimal -> Lower Letter -> Lower Roman -> Lower Roman with Parentheses)', + 'A.1.a.i (Upper Letter -> Decimal -> Lower Letter -> Lower Roman)', + ]) + .default('1.1.1.1 (Decimal)') + .openapi({ + description: 'Set numbering hierarchy format for the document.', + }), + pageOrientation: z.enum(['portrait', 'landscape']).default('portrait').openapi({ + description: 'Set the page orientation for the document.', + }), + margins: z.enum(['normal', 'narrow', 'moderate', 'wide', 'mirrored']).default('normal').openapi({ + description: 'Set page margins for the document.', + }), + }) + .openapi({ + description: 'Word configuration settings for generating the document.', + }), +}); + +export type WordGeneratorRequestBody = z.infer; diff --git a/src/routes/wordGenerator/wordGeneratorRouter.ts b/src/routes/wordGenerator/wordGeneratorRouter.ts new file mode 100644 index 0000000..8823b0a --- /dev/null +++ b/src/routes/wordGenerator/wordGeneratorRouter.ts @@ -0,0 +1,652 @@ +import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; +import { + AlignmentType, + Document, + Footer, + FootnoteReferenceRun, + Header, + HeadingLevel, + LeaderType, + LevelFormat, + Packer, + PageNumber, + PageOrientation, + Paragraph, + Table, + TableCell, + TableOfContents, + TableRow, + TextRun, + WidthType, +} from 'docx'; +import express, { Request, Response, Router } from 'express'; +import fs from 'fs'; +import { StatusCodes } from 'http-status-codes'; +import cron from 'node-cron'; +import path from 'path'; + +import { createApiRequestBody } from '@/api-docs/openAPIRequestBuilders'; +import { createApiResponse } from '@/api-docs/openAPIResponseBuilders'; +import { ResponseStatus, ServiceResponse } from '@/common/models/serviceResponse'; +import { handleServiceResponse } from '@/common/utils/httpHandlers'; + +import { WordGeneratorRequestBodySchema, WordGeneratorResponseSchema } from './wordGeneratorModel'; +export const COMPRESS = true; +export const wordGeneratorRegistry = new OpenAPIRegistry(); +wordGeneratorRegistry.register('WordGenerator', WordGeneratorResponseSchema); +wordGeneratorRegistry.registerPath({ + method: 'post', + path: '/word-generator/generate', + tags: ['Word Generator'], + request: { + body: createApiRequestBody(WordGeneratorRequestBodySchema, 'application/json'), + }, + responses: createApiResponse(WordGeneratorResponseSchema, 'Success'), +}); + +// Create folder to contains generated files +const exportsDir = path.join(__dirname, '../../..', 'word-exports'); +// Ensure the exports directory exists +if (!fs.existsSync(exportsDir)) { + fs.mkdirSync(exportsDir, { recursive: true }); +} + +// Cron job to delete files older than 1 hour +cron.schedule('0 * * * *', () => { + const now = Date.now(); + const oneHour = 60 * 60 * 1000; + // Read the files in the exports directory + fs.readdir(exportsDir, (err, files) => { + if (err) { + console.error(`Error reading directory ${exportsDir}:`, err); + return; + } + + files.forEach((file) => { + const filePath = path.join(exportsDir, file); + fs.stat(filePath, (err, stats) => { + if (err) { + console.error(`Error getting stats for file ${filePath}:`, err); + return; + } + + // Check if the file is older than 1 hour + if (now - stats.mtime.getTime() > oneHour) { + fs.unlink(filePath, (err) => { + if (err) { + console.error(`Error deleting file ${filePath}:`, err); + } else { + console.log(`Deleted file: ${filePath}`); + } + }); + } + }); + }); + }); +}); + +const serverUrl = process.env.RENDER_EXTERNAL_URL || 'http://localhost:3000'; + +const FONT_CONFIG = { + size: 12, // Font size in point + titleSize: 32, // Title font size in point + tableOfContentSize: 16, // Table of content font size in point + family: 'Arial', // Font family +}; + +const SPACING_CONFIG = { + // unit of inches + title: { + after: 12, + }, + tableOfContent: { + after: 6, + }, + heading: { + before: 4, + after: 4, + }, +}; + +const LINE_HEIGHT_CONFIG: any = { + 1: 240, // Single line + 1.15: 276, // 1.15 line spacing + 1.25: 300, // 1.25 line spacing + 1.5: 360, // 1.5 line spacing + 2: 480, // Double line +}; + +// Predefined Margins in Twips +const PAGE_MARGINS: any = { + normal: { + top: 1440, // 2.54 cm = 1440 twips + bottom: 1440, + left: 1440, + right: 1440, + }, + narrow: { + top: 720, // 1.27 cm = 720 twips + bottom: 720, + left: 720, + right: 720, + }, + moderate: { + top: 1440, // 2.54 cm = 1440 twips + bottom: 1440, + left: 1080, // 1.91 cm = 1080 twips + right: 1080, + }, + wide: { + top: 1440, // 2.54 cm = 1440 twips + bottom: 1440, + left: 2880, // 5.08 cm = 2880 twips + right: 2880, + }, + mirrored: { + top: 1440, // 2.54 cm = 1440 twips + bottom: 1440, + left: 1800, // 3.18 cm = 1800 twips + right: 1440, + }, +}; + +const NUMBERING_OPTIONS: any = { + '1.1.1.1 (Decimal)': { + reference: 'decimal-numbering', + levels: [ + { level: 0, format: LevelFormat.DECIMAL, text: '%1', alignment: AlignmentType.START }, + { level: 1, format: LevelFormat.DECIMAL, text: '%1.%2', alignment: AlignmentType.START }, + { level: 2, format: LevelFormat.DECIMAL, text: '%1.%2.%3', alignment: AlignmentType.START }, + { level: 3, format: LevelFormat.DECIMAL, text: '%1.%2.%3.%4', alignment: AlignmentType.START }, + ], + }, + 'I.1.a.i (Roman -> Decimal > Lower Letter -> Lower Roman)': { + reference: 'roman-decimal-lower-letter-lower-roman', + levels: [ + { level: 0, format: LevelFormat.UPPER_ROMAN, text: '%1.', alignment: AlignmentType.START }, // Roman + { level: 1, format: LevelFormat.DECIMAL, text: '%2.', alignment: AlignmentType.START }, // Decimal + { level: 2, format: LevelFormat.LOWER_LETTER, text: '%3.', alignment: AlignmentType.START }, // Lower Letter + { level: 3, format: LevelFormat.LOWER_ROMAN, text: '%4.', alignment: AlignmentType.START }, // Lower Roman + ], + }, + 'I.A.1.a (Roman -> Upper Letter -> Decimal -> Lower Letter)': { + reference: 'roman-upper-decimal-lower', + levels: [ + { level: 0, format: LevelFormat.UPPER_ROMAN, text: '%1', alignment: AlignmentType.START }, + { level: 1, format: LevelFormat.UPPER_LETTER, text: '%2', alignment: AlignmentType.START }, + { level: 2, format: LevelFormat.DECIMAL, text: '%3', alignment: AlignmentType.START }, + { level: 3, format: LevelFormat.LOWER_LETTER, text: '%4', alignment: AlignmentType.START }, + ], + }, + '1)a)i)(i) (Decimal -> Lower Letter -> Lower Roman -> Lower Roman with Parentheses)': { + reference: 'decimal-lower-letter-lower-roman-parentheses', + levels: [ + { level: 0, format: LevelFormat.DECIMAL, text: '%1)', alignment: AlignmentType.START }, + { level: 1, format: LevelFormat.LOWER_LETTER, text: '%2)', alignment: AlignmentType.START }, + { level: 2, format: LevelFormat.LOWER_ROMAN, text: '%3)', alignment: AlignmentType.START }, + { level: 3, format: LevelFormat.LOWER_ROMAN, text: '(%4)', alignment: AlignmentType.START }, + ], + }, + 'A.1.a.i (Upper Letter -> Decimal -> Lower Letter -> Lower Roman)': { + reference: 'upper-letter-decimal-lower-letter-lower-roman', + levels: [ + { level: 0, format: LevelFormat.UPPER_LETTER, text: '%1', alignment: AlignmentType.START }, + { level: 1, format: LevelFormat.DECIMAL, text: '%1.%2', alignment: AlignmentType.START }, + { level: 2, format: LevelFormat.LOWER_LETTER, text: '%1.%2.%3', alignment: AlignmentType.START }, + { level: 3, format: LevelFormat.LOWER_ROMAN, text: '%1.%2.%3.%4', alignment: AlignmentType.START }, + ], + }, +}; + +const BULLET_CONFIG = { + reference: 'my-listing-with-bullet-points', + levels: [ + { + level: 0, + format: LevelFormat.NUMBER_IN_DASH, + alignment: AlignmentType.START, + }, + ], +}; + +// Function to map heading levels +const getHeadingLevel = (level: any) => { + switch (level) { + case 1: + return HeadingLevel.HEADING_1; + case 2: + return HeadingLevel.HEADING_2; + case 3: + return HeadingLevel.HEADING_3; + case 4: + return HeadingLevel.HEADING_4; + default: + throw Error(`Unsupported heading with input level: ${level}`); + } +}; + +// Helper function to process footnotes +const generateFootnotes = (sections: any[]) => { + const footnotes: any = {}; + let currentFootnoteId = 1; + + sections.forEach((section) => { + section.content.forEach((content: any) => { + if (content.footnote) { + footnotes[currentFootnoteId] = { + children: [new Paragraph(content.footnote.note)], + }; + content.footnote.id = currentFootnoteId; // Add the ID for later use + currentFootnoteId++; + } + }); + }); + + return footnotes; +}; + +// Generate a table with optional headers +const generateTable = (tableData: any) => { + const rows = []; + + // Add header row if headers exist + if (tableData.headers) { + const headerRow = new TableRow({ + children: tableData.headers.map( + (header: any) => + new TableCell({ + children: [ + new Paragraph({ + children: [new TextRun({ text: header, bold: true })], + alignment: AlignmentType.CENTER, + }), + ], + }) + ), + tableHeader: true, + }); + rows.push(headerRow); + } + + // Add table rows + tableData.rows.forEach((row: any) => { + const tableRow = new TableRow({ + children: row.cells.map( + (cell: any) => + new TableCell({ + children: [ + new Paragraph({ + children: [new TextRun(cell.text)], + }), + ], + }) + ), + }); + rows.push(tableRow); + }); + + // Return the Table object + return new Table({ + rows, + width: { + size: 100, // Table width set in DXA (adjust as needed) + type: WidthType.PERCENTAGE, + }, + }); +}; + +// Recursive function to handle sections and sub-sections +const generateSectionContent = (section: any, config: any) => { + // Section Content + const sectionContents = section.content.flatMap((child: any) => { + const results = []; + // Handle paragraph content + if (child.type === 'paragraph') { + const paragraphChildren = []; + if (child.text.includes('\n')) { + // Split the text by newline characters + const lines = child.text.split('\n'); + // Log each line + lines.forEach((line: string) => { + paragraphChildren.push(new TextRun({ text: line, break: 1 })); + }); + paragraphChildren.push(...lines); + } else { + paragraphChildren.push(new TextRun(child.text)); + } + + if (child.footnote) { + paragraphChildren.push(new FootnoteReferenceRun(child.footnote.id)); + } + results.push(new Paragraph({ children: paragraphChildren })); + } else if (child.type === 'listing' && child.items) { + // Handle list content with bullets (level 0) + // Create a new paragraph for each list item and apply the bullet style (level 0) + results.push( + ...child.items.flatMap( + (item: any) => + new Paragraph({ + children: [new TextRun(item)], + bullet: { + level: 0, + reference: BULLET_CONFIG.reference, + } as any, + }) + ) + ); + } else if (child.type === 'table') { + results.push(generateTable(child)); + } else if (child.type === 'pageBreak') { + results.push( + new Paragraph({ + text: '', + pageBreakBefore: true, + }) + ); + } else if (child.type === 'emptyLine') { + results.push( + new Paragraph({ + text: '', + }) + ); + } else { + results.push( + new Paragraph({ + children: [new TextRun('Unsupported content type.')], + }) + ); + } + return results; + }); + + let numberingConfig; + if (config.numberingReference) { + numberingConfig = { + reference: config.numberingReference, + level: section.headingLevel - 1, + }; + } + + let headingContent; + if (section.heading) { + headingContent = new Paragraph({ + children: [new TextRun(section.heading)], + heading: getHeadingLevel(section.headingLevel), + numbering: numberingConfig, + spacing: { + before: SPACING_CONFIG.heading.before * 20, + after: SPACING_CONFIG.heading.after * 20, + }, + }); + } + + const sectionContent = [ + // Section Heading with index + headingContent, + ...sectionContents, + // Process sub-sections if they exist + ...(section.subSections + ? section.subSections.flatMap((subSection: any) => generateSectionContent(subSection, config)) + : []), + ]; + + return sectionContent; +}; + +// Function to build a hierarchical structure from a flat list of sections +const buildSectionsHierarchy = (sections: any[]) => { + const sectionMap = new Map(); + + // Create a map of sections by ID + sections.forEach((section) => { + sectionMap.set(section.sectionId, { ...section, subSections: [] }); + }); + + const rootSections: any[] = []; + + // Organize sections into a hierarchy + sections.forEach((section) => { + if (section.parentSectionId) { + // If the section has a parent, add it as a subSection + const parent = sectionMap.get(section.parentSectionId); + if (parent) { + parent.subSections.push(sectionMap.get(section.sectionId)); + } else { + console.warn(`Parent section with ID ${section.parentSectionId} not found.`); + } + } else { + // If no parent, it's a root section + rootSections.push(sectionMap.get(section.sectionId)); + } + }); + + return rootSections; +}; + +async function execGenWordFuncs( + data: { + title: string; + header?: any; + footer?: any; + sections: any[]; + }, + config: { + numberingReference: string; + showPageNumber: boolean; + pageOrientation: string; + fontFamily: string; + fontSize: number; + lineHeight: number; + margins: string; + showTableOfContent: boolean; + } +) { + let headerConfigs = {}; + if (data.header && data.header.text) { + headerConfigs = { + default: new Header({ + children: [ + new Paragraph({ + text: data.header.text, + alignment: String(data.header?.alignment ?? 'left').toLowerCase(), + } as any), + ], + }), + }; + } + + let footerConfigs = {}; + const footerChildren = []; + if (config.showPageNumber || (data.footer && data.footer.text)) { + if (data.footer && data.footer.text) { + footerChildren.push( + new Paragraph({ + text: data.footer.text, + alignment: String(data.footer?.alignment ?? 'left').toLowerCase(), + } as any) + ); + } + + if (config.showPageNumber) { + footerChildren.push( + new Paragraph({ + children: [ + new TextRun({ + children: ['Page ', PageNumber.CURRENT, ' of ', PageNumber.TOTAL_PAGES], + }), + ], + }) + ); + } + + footerConfigs = { + default: new Footer({ + children: footerChildren, + }), + }; + } + + // Generate the footnotes + const footnoteConfig = generateFootnotes(data.sections); + const numberingConfig: any[] = [BULLET_CONFIG]; + const selectedNumberingOption = NUMBERING_OPTIONS[config.numberingReference]; + if (selectedNumberingOption) { + numberingConfig.push(selectedNumberingOption); + } + + const tableOfContentConfigs = []; + if (config.showTableOfContent) { + tableOfContentConfigs.push( + new Paragraph({ + children: [ + new TextRun({ + text: 'Table of Contents', + bold: true, + size: FONT_CONFIG.tableOfContentSize * 2, + }), + ], + spacing: { after: SPACING_CONFIG.tableOfContent.after * 20 }, + }) + ); + tableOfContentConfigs.push( + new TableOfContents({ + stylesWithLevels: [ + { style: 'Heading1', level: 1 }, + { style: 'Heading2', level: 2 }, + { style: 'Heading3', level: 3 }, + { style: 'Heading4', level: 4 }, + ], + leader: LeaderType.DOT, // Dot leader + } as any) + ); + } + + // Build sections hierarchy + const sectionsHierarchy = buildSectionsHierarchy(data.sections); + + // Create the document based on JSON data + const doc = new Document({ + styles: { + default: { + document: { + run: { + font: config.fontFamily, + size: config.fontSize * 2, // Font size in half-points + }, + paragraph: { + spacing: { line: config.lineHeight }, // Line height + }, + }, + }, + }, + numbering: { + config: numberingConfig, + }, + sections: [ + { + properties: { + page: { + margin: config.margins, + orientation: config.pageOrientation, + } as any, + }, + headers: headerConfigs, + footers: footerConfigs, + children: [ + // Title of the proposal with larger font size + new Paragraph({ + children: [ + new TextRun({ + text: data.title, + size: FONT_CONFIG.titleSize * 2, + }), + ], + heading: HeadingLevel.TITLE, + spacing: { after: SPACING_CONFIG.title.after * 20 }, // 12 inches * 20 = 240 twips + }), + ...tableOfContentConfigs, + // Generate all sections and sub-sections + ...sectionsHierarchy.flatMap((section) => + generateSectionContent(section, { ...config, numberingReference: selectedNumberingOption?.reference }) + ), + ], + }, + ], + footnotes: footnoteConfig, // TODO: Enhance footnote + }); + + const fileName = `word-file-${new Date().toISOString().replace(/\D/gi, '')}.docx`; + const filePath = path.join(exportsDir, fileName); + + // Create and save the document + Packer.toBuffer(doc).then((buffer) => { + fs.writeFileSync(filePath, buffer); + }); + + return fileName; +} + +export const wordGeneratorRouter: Router = (() => { + const router = express.Router(); + // Static route for downloading files + router.use('/downloads', express.static(exportsDir)); + + router.post('/generate', async (_req: Request, res: Response) => { + const { title, sections = [], header, footer, wordConfig = {} } = _req.body; + if (!sections.length) { + const validateServiceResponse = new ServiceResponse( + ResponseStatus.Failed, + '[Validation Error] Sections is required!', + 'Please make sure you have sent the sections content generated from TypingMind.', + StatusCodes.BAD_REQUEST + ); + return handleServiceResponse(validateServiceResponse, res); + } + + try { + const wordConfigs = { + numberingReference: wordConfig.showNumberingInHeader ? wordConfig.numberingReference : '', + showPageNumber: wordConfig.showPageNumber ?? false, + pageOrientation: wordConfig.pageOrientation ? wordConfig.pageOrientation : PageOrientation.PORTRAIT, + fontFamily: wordConfig.fontFamily ? wordConfig.fontFamily : FONT_CONFIG.family, + fontSize: wordConfig.fontSize ? wordConfig.fontSize : FONT_CONFIG.size, + lineHeight: wordConfig.lineHeight ? LINE_HEIGHT_CONFIG[wordConfig.lineHeight] : LINE_HEIGHT_CONFIG['1.15'], + margins: wordConfig.margins ? PAGE_MARGINS[wordConfig.margins] : PAGE_MARGINS.NORMAL, + showTableOfContent: wordConfig.showTableOfContent ?? false, + }; + + const fileName = await execGenWordFuncs( + { + title, + sections, + header, + footer, + }, + wordConfigs + ); + const serviceResponse = new ServiceResponse( + ResponseStatus.Success, + 'File generated successfully', + { + downloadUrl: `${serverUrl}/word-generator/downloads/${fileName}`, + }, + StatusCodes.OK + ); + return handleServiceResponse(serviceResponse, res); + } catch (error) { + const errorMessage = (error as Error).message; + let responseObject = ''; + if (errorMessage.includes('')) { + responseObject = `Sorry, we couldn't generate word file.`; + } + const errorServiceResponse = new ServiceResponse( + ResponseStatus.Failed, + `Error ${errorMessage}`, + responseObject, + StatusCodes.INTERNAL_SERVER_ERROR + ); + return handleServiceResponse(errorServiceResponse, res); + } + }); + return router; +})(); diff --git a/src/routes/youtubeTranscript/transcriptModel.ts b/src/routes/youtubeTranscript/transcriptModel.ts deleted file mode 100644 index 137d434..0000000 --- a/src/routes/youtubeTranscript/transcriptModel.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; -import { z } from 'zod'; - -extendZodWithOpenApi(z); - -export type Transcript = z.infer; -export const TranscriptSchema = z.object({ - textOnly: z.string(), -}); diff --git a/src/routes/youtubeTranscript/youtubeTranscriptModel.ts b/src/routes/youtubeTranscript/youtubeTranscriptModel.ts new file mode 100644 index 0000000..84df9b4 --- /dev/null +++ b/src/routes/youtubeTranscript/youtubeTranscriptModel.ts @@ -0,0 +1,14 @@ +import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; +import { z } from 'zod'; + +extendZodWithOpenApi(z); + +export type YoutubeTranscriptResponse = z.infer; +export const YoutubeTranscriptResponseSchema = z.object({ + textOnly: z.string(), +}); + +export type YoutubeTranscriptRequestParam = z.infer; +export const YoutubeTranscriptRequestParamSchema = z.object({ + videoId: z.string().describe('The id of the Youtube video to retrieve the transcript'), +}); diff --git a/src/routes/youtubeTranscript/transcriptRouter.ts b/src/routes/youtubeTranscript/youtubeTranscriptRouter.ts similarity index 67% rename from src/routes/youtubeTranscript/transcriptRouter.ts rename to src/routes/youtubeTranscript/youtubeTranscriptRouter.ts index 8597bc7..1d71a00 100644 --- a/src/routes/youtubeTranscript/transcriptRouter.ts +++ b/src/routes/youtubeTranscript/youtubeTranscriptRouter.ts @@ -7,22 +7,25 @@ import { createApiResponse } from '@/api-docs/openAPIResponseBuilders'; import { ResponseStatus, ServiceResponse } from '@/common/models/serviceResponse'; import { handleServiceResponse } from '@/common/utils/httpHandlers'; -import { TranscriptSchema } from './transcriptModel'; +import { YoutubeTranscriptRequestParamSchema, YoutubeTranscriptResponseSchema } from './youtubeTranscriptModel'; -export const transcriptRegistry = new OpenAPIRegistry(); -transcriptRegistry.register('Transcript', TranscriptSchema); +export const youtubeTranscriptRegistry = new OpenAPIRegistry(); +youtubeTranscriptRegistry.register('YoutubeTranscript', YoutubeTranscriptResponseSchema); -export const transcriptRouter: Router = (() => { +export const youtubeTranscriptRouter: Router = (() => { const router = express.Router(); - transcriptRegistry.registerPath({ + youtubeTranscriptRegistry.registerPath({ method: 'get', - path: '/transcript', + path: '/youtube-transcript/get-transcript', tags: ['Youtube Transcript'], - responses: createApiResponse(TranscriptSchema, 'Success'), + request: { + query: YoutubeTranscriptRequestParamSchema, + }, + responses: createApiResponse(YoutubeTranscriptResponseSchema, 'Success'), }); - router.get('/', async (_req: Request, res: Response) => { + router.get('/get-transcript', async (_req: Request, res: Response) => { const { videoId } = _req.query; if (!videoId) { @@ -39,7 +42,7 @@ export const transcriptRouter: Router = (() => { const textOnly = transcript.map((entry) => entry.text).join(' '); const serviceResponse = new ServiceResponse( ResponseStatus.Success, - 'Service is healthy', + 'Transcript fetched successfully', { textOnly }, StatusCodes.OK ); diff --git a/src/server.ts b/src/server.ts index 806cef3..0da49de 100644 --- a/src/server.ts +++ b/src/server.ts @@ -10,8 +10,11 @@ import rateLimiter from '@/common/middleware/rateLimiter'; import requestLogger from '@/common/middleware/requestLogger'; import { healthCheckRouter } from '@/routes/healthCheck/healthCheckRouter'; -import { articleReaderRouter } from './routes/articleReader/articleReaderRouter'; -import { transcriptRouter } from './routes/youtubeTranscript/transcriptRouter'; +import { excelGeneratorRouter } from './routes/excelGenerator/excelGeneratorRouter'; +import { powerpointGeneratorRouter } from './routes/powerpointGenerator/powerpointGeneratorRouter'; +import { webPageReaderRouter } from './routes/webPageReader/webPageReaderRouter'; +import { wordGeneratorRouter } from './routes/wordGenerator/wordGeneratorRouter'; +import { youtubeTranscriptRouter } from './routes/youtubeTranscript/youtubeTranscriptRouter'; const logger = pino({ name: 'server start' }); const app: Express = express(); @@ -34,8 +37,11 @@ app.use(requestLogger()); // Routes app.use('/health-check', healthCheckRouter); app.use('/images', express.static('public/images')); -app.use('/transcript', transcriptRouter); -app.use('/get-content', articleReaderRouter); +app.use('/youtube-transcript', youtubeTranscriptRouter); +app.use('/web-page-reader', webPageReaderRouter); +app.use('/powerpoint-generator', powerpointGeneratorRouter); +app.use('/word-generator', wordGeneratorRouter); +app.use('/excel-generator', excelGeneratorRouter); // Swagger UI app.use(openAPIRouter);