INIT Backend
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
# dependencies (bun install)
|
||||
node_modules
|
||||
|
||||
# output
|
||||
out
|
||||
dist
|
||||
*.tgz
|
||||
|
||||
# code coverage
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# logs
|
||||
logs
|
||||
_.log
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# caches
|
||||
.eslintcache
|
||||
.cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
@@ -0,0 +1,15 @@
|
||||
# job-info-sync
|
||||
|
||||
To install dependencies:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
To run:
|
||||
|
||||
```bash
|
||||
bun run index.ts
|
||||
```
|
||||
|
||||
This project was created using `bun init` in bun v1.3.2. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
|
||||
@@ -0,0 +1,341 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "job-info-sync",
|
||||
"dependencies": {
|
||||
"axios": "^1.13.2",
|
||||
"dotenv": "^17.2.3",
|
||||
"exceljs": "^4.4.0",
|
||||
"hono": "^4.10.6",
|
||||
"joi": "^18.0.1",
|
||||
"pino": "^10.1.0",
|
||||
"pocketbase": "^0.26.3",
|
||||
"qs": "^6.14.0",
|
||||
"uuid": "^13.0.0",
|
||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"@types/node": "^24.10.1",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@fast-csv/format": ["@fast-csv/format@4.3.5", "", { "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" } }, "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A=="],
|
||||
|
||||
"@fast-csv/parse": ["@fast-csv/parse@4.3.6", "", { "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" } }, "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA=="],
|
||||
|
||||
"@hapi/address": ["@hapi/address@5.1.1", "", { "dependencies": { "@hapi/hoek": "^11.0.2" } }, "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA=="],
|
||||
|
||||
"@hapi/formula": ["@hapi/formula@3.0.2", "", {}, "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw=="],
|
||||
|
||||
"@hapi/hoek": ["@hapi/hoek@11.0.7", "", {}, "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ=="],
|
||||
|
||||
"@hapi/pinpoint": ["@hapi/pinpoint@2.0.1", "", {}, "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q=="],
|
||||
|
||||
"@hapi/tlds": ["@hapi/tlds@1.1.4", "", {}, "sha512-Fq+20dxsxLaUn5jSSWrdtSRcIUba2JquuorF9UW1wIJS5cSUwxIsO2GIhaWynPRflvxSzFN+gxKte2HEW1OuoA=="],
|
||||
|
||||
"@hapi/topo": ["@hapi/topo@6.0.2", "", { "dependencies": { "@hapi/hoek": "^11.0.2" } }, "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg=="],
|
||||
|
||||
"@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.2", "", { "dependencies": { "bun-types": "1.3.2" } }, "sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg=="],
|
||||
|
||||
"@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.5", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-keKxkZMqnDicuvFoJbzrhbtdLSPhj/rZThDlKWCDbgXmUg0rEUFtRssDXKYmtXluZlIqiC5VqkCgRwzuyLHKHw=="],
|
||||
|
||||
"archiver": ["archiver@5.3.2", "", { "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" } }, "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw=="],
|
||||
|
||||
"archiver-utils": ["archiver-utils@2.1.0", "", { "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" } }, "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw=="],
|
||||
|
||||
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
|
||||
|
||||
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||
|
||||
"atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="],
|
||||
|
||||
"axios": ["axios@1.13.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA=="],
|
||||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||
|
||||
"big-integer": ["big-integer@1.6.52", "", {}, "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg=="],
|
||||
|
||||
"binary": ["binary@0.3.0", "", { "dependencies": { "buffers": "~0.1.1", "chainsaw": "~0.1.0" } }, "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg=="],
|
||||
|
||||
"bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
|
||||
|
||||
"bluebird": ["bluebird@3.4.7", "", {}, "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
|
||||
|
||||
"buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
|
||||
|
||||
"buffer-indexof-polyfill": ["buffer-indexof-polyfill@1.0.2", "", {}, "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A=="],
|
||||
|
||||
"buffers": ["buffers@0.1.1", "", {}, "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.2", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
|
||||
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||
|
||||
"chainsaw": ["chainsaw@0.1.0", "", { "dependencies": { "traverse": ">=0.3.0 <0.4" } }, "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ=="],
|
||||
|
||||
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
||||
|
||||
"compress-commons": ["compress-commons@4.1.2", "", { "dependencies": { "buffer-crc32": "^0.2.13", "crc32-stream": "^4.0.2", "normalize-path": "^3.0.0", "readable-stream": "^3.6.0" } }, "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg=="],
|
||||
|
||||
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||
|
||||
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
|
||||
|
||||
"crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="],
|
||||
|
||||
"crc32-stream": ["crc32-stream@4.0.3", "", { "dependencies": { "crc-32": "^1.2.0", "readable-stream": "^3.4.0" } }, "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw=="],
|
||||
|
||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||
|
||||
"dayjs": ["dayjs@1.11.19", "", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="],
|
||||
|
||||
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||
|
||||
"dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"duplexer2": ["duplexer2@0.1.4", "", { "dependencies": { "readable-stream": "^2.0.2" } }, "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA=="],
|
||||
|
||||
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||
|
||||
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
||||
|
||||
"exceljs": ["exceljs@4.4.0", "", { "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" } }, "sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg=="],
|
||||
|
||||
"fast-csv": ["fast-csv@4.3.6", "", { "dependencies": { "@fast-csv/format": "4.3.5", "@fast-csv/parse": "4.3.6" } }, "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw=="],
|
||||
|
||||
"follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
|
||||
|
||||
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
|
||||
|
||||
"fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="],
|
||||
|
||||
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
|
||||
|
||||
"fstream": ["fstream@1.0.12", "", { "dependencies": { "graceful-fs": "^4.1.2", "inherits": "~2.0.0", "mkdirp": ">=0.5 0", "rimraf": "2" } }, "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
|
||||
"glob": ["glob@7.2.3", "", { "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" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||
|
||||
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"hono": ["hono@4.10.6", "", {}, "sha512-BIdolzGpDO9MQ4nu3AUuDwHZZ+KViNm+EZ75Ae55eMXMqLVhDFqEMXxtUe9Qh8hjL+pIna/frs2j6Y2yD5Ua/g=="],
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="],
|
||||
|
||||
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
|
||||
|
||||
"joi": ["joi@18.0.1", "", { "dependencies": { "@hapi/address": "^5.1.1", "@hapi/formula": "^3.0.2", "@hapi/hoek": "^11.0.7", "@hapi/pinpoint": "^2.0.1", "@hapi/tlds": "^1.1.1", "@hapi/topo": "^6.0.2", "@standard-schema/spec": "^1.0.0" } }, "sha512-IiQpRyypSnLisQf3PwuN2eIHAsAIGZIrLZkd4zdvIar2bDyhM91ubRjy8a3eYablXsh9BeI/c7dmPYHca5qtoA=="],
|
||||
|
||||
"jszip": ["jszip@3.10.1", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="],
|
||||
|
||||
"lazystream": ["lazystream@1.0.1", "", { "dependencies": { "readable-stream": "^2.0.5" } }, "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw=="],
|
||||
|
||||
"lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="],
|
||||
|
||||
"listenercount": ["listenercount@1.0.1", "", {}, "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ=="],
|
||||
|
||||
"lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="],
|
||||
|
||||
"lodash.difference": ["lodash.difference@4.5.0", "", {}, "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA=="],
|
||||
|
||||
"lodash.escaperegexp": ["lodash.escaperegexp@4.1.2", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="],
|
||||
|
||||
"lodash.flatten": ["lodash.flatten@4.4.0", "", {}, "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g=="],
|
||||
|
||||
"lodash.groupby": ["lodash.groupby@4.6.0", "", {}, "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw=="],
|
||||
|
||||
"lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="],
|
||||
|
||||
"lodash.isequal": ["lodash.isequal@4.5.0", "", {}, "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="],
|
||||
|
||||
"lodash.isfunction": ["lodash.isfunction@3.0.9", "", {}, "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw=="],
|
||||
|
||||
"lodash.isnil": ["lodash.isnil@4.0.0", "", {}, "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng=="],
|
||||
|
||||
"lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="],
|
||||
|
||||
"lodash.isundefined": ["lodash.isundefined@3.0.1", "", {}, "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA=="],
|
||||
|
||||
"lodash.union": ["lodash.union@4.6.0", "", {}, "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw=="],
|
||||
|
||||
"lodash.uniq": ["lodash.uniq@4.5.0", "", {}, "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="],
|
||||
|
||||
"mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="],
|
||||
|
||||
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
|
||||
"on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="],
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
|
||||
|
||||
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
|
||||
|
||||
"pino": ["pino@10.1.0", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w=="],
|
||||
|
||||
"pino-abstract-transport": ["pino-abstract-transport@2.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw=="],
|
||||
|
||||
"pino-std-serializers": ["pino-std-serializers@7.0.0", "", {}, "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA=="],
|
||||
|
||||
"pocketbase": ["pocketbase@0.26.3", "", {}, "sha512-5deUKRoEczpxxuHzwr6/DHVmgbggxylEVig8CKN+MjvtYxPUqX/C6puU0yaR2yhTi8zrh7J9s7Ty+qBGwVzWOQ=="],
|
||||
|
||||
"process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
|
||||
|
||||
"process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="],
|
||||
|
||||
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
||||
|
||||
"qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
|
||||
|
||||
"quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="],
|
||||
|
||||
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||
|
||||
"readdir-glob": ["readdir-glob@1.1.3", "", { "dependencies": { "minimatch": "^5.1.0" } }, "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA=="],
|
||||
|
||||
"real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="],
|
||||
|
||||
"rimraf": ["rimraf@2.7.1", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
|
||||
|
||||
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
|
||||
|
||||
"saxes": ["saxes@5.0.1", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw=="],
|
||||
|
||||
"setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="],
|
||||
|
||||
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
||||
|
||||
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
|
||||
|
||||
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
||||
|
||||
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||
|
||||
"sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="],
|
||||
|
||||
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
|
||||
|
||||
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||
|
||||
"tar-stream": ["tar-stream@2.2.0", "", { "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" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
|
||||
|
||||
"thread-stream": ["thread-stream@3.1.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="],
|
||||
|
||||
"tmp": ["tmp@0.2.5", "", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="],
|
||||
|
||||
"traverse": ["traverse@0.3.9", "", {}, "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
"unzipper": ["unzipper@0.10.14", "", { "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" } }, "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
"uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="],
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"xlsx": ["xlsx@https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", { "bin": { "xlsx": "./bin/xlsx.njs" } }],
|
||||
|
||||
"xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="],
|
||||
|
||||
"zip-stream": ["zip-stream@4.1.1", "", { "dependencies": { "archiver-utils": "^3.0.4", "compress-commons": "^4.1.2", "readable-stream": "^3.6.0" } }, "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ=="],
|
||||
|
||||
"@fast-csv/format/@types/node": ["@types/node@14.18.63", "", {}, "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ=="],
|
||||
|
||||
"@fast-csv/parse/@types/node": ["@types/node@14.18.63", "", {}, "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ=="],
|
||||
|
||||
"archiver-utils/readable-stream": ["readable-stream@2.3.8", "", { "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" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
||||
|
||||
"duplexer2/readable-stream": ["readable-stream@2.3.8", "", { "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" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
||||
|
||||
"exceljs/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
|
||||
|
||||
"glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||
|
||||
"jszip/readable-stream": ["readable-stream@2.3.8", "", { "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" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
||||
|
||||
"lazystream/readable-stream": ["readable-stream@2.3.8", "", { "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" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
||||
|
||||
"string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"unzipper/readable-stream": ["readable-stream@2.3.8", "", { "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" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
||||
|
||||
"zip-stream/archiver-utils": ["archiver-utils@3.0.4", "", { "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" } }, "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw=="],
|
||||
|
||||
"archiver-utils/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||
|
||||
"duplexer2/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||
|
||||
"glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"jszip/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||
|
||||
"lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||
|
||||
"unzipper/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"tenantId": "3fd97ea7-b124-41f1-855f-52d8ac3b16c7",
|
||||
"clientId": "3c846e71-9609-40e1-b458-0eb805e21b9f",
|
||||
"sharepointFileUrl": "https://czflex.sharepoint.com/:x:/r/sites/Team/Shared%20Documents/General/JOBS%201000-9999.xls",
|
||||
"pbUrl": "http://10.10.1.109:8080",
|
||||
"pbCollection": "Job_Info_TestEnv",
|
||||
"pbEmail": "pbserviceupdate@cardoza.construction"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
console.log("Hello via Bun!");
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "job-info-sync",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "bun --watch src/server.ts",
|
||||
"sharelink": "bun run utilities/sharepointShareLinktoGraph.html",
|
||||
"sync": "bun run /src/excelsync.js",
|
||||
"server": "bun run src/server.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"@types/node": "^24.10.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.13.2",
|
||||
"dotenv": "^17.2.3",
|
||||
"exceljs": "^4.4.0",
|
||||
"hono": "^4.10.6",
|
||||
"joi": "^18.0.1",
|
||||
"pino": "^10.1.0",
|
||||
"pocketbase": "^0.26.3",
|
||||
"qs": "^6.14.0",
|
||||
"uuid": "^13.0.0",
|
||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,513 @@
|
||||
// excelsync.js
|
||||
import { FIELD_CONFIG } from "./fieldConfig.js";
|
||||
import fs from "fs";
|
||||
import axios from "axios";
|
||||
import qs from "qs";
|
||||
import * as XLSX from "xlsx";
|
||||
import dotenv from "dotenv";
|
||||
import { serve } from "bun";
|
||||
import PocketBase from "pocketbase"
|
||||
|
||||
dotenv.config();
|
||||
|
||||
// -----------------------------
|
||||
// EXCEL PARSER
|
||||
// -----------------------------
|
||||
const HEADER_ROW_INDEX = 2; // Row 3
|
||||
const DATA_START_ROW_INDEX = 3; // Row 4
|
||||
|
||||
// -----------------------------
|
||||
// CONFIG
|
||||
// -----------------------------
|
||||
const CONFIG = JSON.parse(fs.readFileSync("config.json", "utf8"));
|
||||
const tenantId = CONFIG.tenantId;
|
||||
const clientId = CONFIG.clientId;
|
||||
const sharepointFileUrl = CONFIG.sharepointFileUrl; // production Excel file URL
|
||||
const pbUrl = CONFIG.pbUrl;
|
||||
const pbCollection = CONFIG.pbCollection || "Job_Info_TestEnv";
|
||||
const pbEmail = CONFIG.pbEmail;
|
||||
|
||||
const clientSecret = process.env.CLIENT_SECRET;
|
||||
const pbPassword = process.env.PB_PASSWORD;
|
||||
|
||||
if (!tenantId || !clientId || !clientSecret || !sharepointFileUrl || !pbUrl || !pbEmail || !pbPassword) {
|
||||
console.error("❌ Missing required config/env values");
|
||||
process.exit(1);
|
||||
}
|
||||
console.log({
|
||||
tenantId,
|
||||
clientId,
|
||||
clientSecret: clientSecret ? clientSecret.slice(0,4) + "..." : null,
|
||||
sharepointFileUrl,
|
||||
pbUrl,
|
||||
pbEmail,
|
||||
pbPassword: pbPassword ? "***" : null
|
||||
});
|
||||
// -----------------------------
|
||||
// LOGGING
|
||||
// -----------------------------
|
||||
function log(message) {
|
||||
console.log(message);
|
||||
fs.appendFileSync("excelsync.log", message + "\n");
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// HEADER + ROW DETECTION HELPERS
|
||||
// -----------------------------
|
||||
//Gets the last column letter in the table converting from 0 index to excel actual column in letter form.
|
||||
function columnLetter(index) {
|
||||
let letter = "";
|
||||
while (index >= 0) {
|
||||
letter = String.fromCharCode((index % 26) + 65) + letter;
|
||||
index = Math.floor(index / 26) - 1;
|
||||
}
|
||||
return letter;
|
||||
}
|
||||
|
||||
const headerRowIndex = HEADER_ROW_INDEX + 1;
|
||||
|
||||
function getColumnLetterForHeader(headerName, sheet, headerRowIndex) {
|
||||
const headerRow = XLSX.utils.sheet_to_json(sheet, { header: 1, range: `${headerRowIndex}:${headerRowIndex}` })[0]
|
||||
.map(h => (h || "").toString().trim().toLowerCase());
|
||||
const idx = headerRow.findIndex(h => h === headerName.toLowerCase());
|
||||
if (idx === -1) {
|
||||
log(`❌ Header "${headerName}" not found. Row contents: ${JSON.stringify(headerRow)}`);
|
||||
throw new Error(`Header "${headerName}" not found on row ${headerRowIndex}`);
|
||||
}
|
||||
log(`✅ Header "${headerName}" found at column index ${idx}`);
|
||||
return columnLetter(idx);
|
||||
}
|
||||
function getNotesColumnLetterFromParsedHeaders(headerMap) {
|
||||
const headers = Object.keys(headerMap);
|
||||
const notesIndex = headers.findIndex(h => h.trim().toLowerCase() === "notes");
|
||||
if (notesIndex === -1) {
|
||||
log(`❌ 'Notes' header not found in HEADER_MAP keys: ${headers.join(", ")}`);
|
||||
throw new Error("Notes column not found");
|
||||
}
|
||||
return columnLetter(notesIndex);
|
||||
}
|
||||
// -----------------------------
|
||||
// GRAPH TOKEN
|
||||
// -----------------------------
|
||||
async function getGraphToken() {
|
||||
const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
|
||||
const body = qs.stringify({
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
scope: "https://graph.microsoft.com/.default",
|
||||
grant_type: "client_credentials",
|
||||
});
|
||||
const res = await axios.post(tokenUrl, body, {
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
});
|
||||
log("✅ Graph token acquired");
|
||||
return res.data.access_token;
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// GRAPH DOWNLOADER HELPER
|
||||
// -----------------------------
|
||||
async function downloadExcelFromGraph(graphToken, driveId, itemId) {
|
||||
const url = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/content`;
|
||||
const res = await axios.get(url, {
|
||||
headers: { Authorization: `Bearer ${graphToken}` },
|
||||
responseType: "arraybuffer", // important: get raw binary
|
||||
});
|
||||
return Buffer.from(res.data); // convert to Buffer for XLSX
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// GRAPH UPLOADER HELPER
|
||||
// -----------------------------
|
||||
async function uploadExcelToGraph(graphToken, driveId, itemId, buffer) {
|
||||
const url = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/content`;
|
||||
const res = await axios.put(url, buffer, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${graphToken}`,
|
||||
"Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
}
|
||||
});
|
||||
log(`✅ Excel file updated in SharePoint: ${res.status} ${res.statusText}`);
|
||||
}
|
||||
|
||||
|
||||
// -----------------------------
|
||||
// POCKETBASE LOGIN
|
||||
// -----------------------------
|
||||
async function getPocketBaseClient() {
|
||||
const pb = new PocketBase(pbUrl);
|
||||
await pb.collection("users").authWithPassword(pbEmail, pbPassword);
|
||||
log("✅ PocketBase token acquired");
|
||||
return pb;
|
||||
}
|
||||
|
||||
function parseExcelFile(input, headerMap) {
|
||||
let data;
|
||||
// In case of data from graph
|
||||
if (Buffer.isBuffer(input)) {
|
||||
data = input;
|
||||
log(`Reading Excel file from Graph buffer`);
|
||||
// In case of file path (Not used at this time)
|
||||
} else {
|
||||
data = fs.readFileSync(input);
|
||||
log(`Reading Excel file: ${input}`);
|
||||
}
|
||||
|
||||
const workbook = XLSX.read(data, { type: "buffer", cellDates: true });
|
||||
const sheet = workbook.Sheets["Job Sheet"]; // explicitly use Job Sheet
|
||||
const range = XLSX.utils.decode_range(sheet["!ref"]);
|
||||
const merges = sheet["!merges"] || [];
|
||||
|
||||
const getValue = (r, c) => {
|
||||
const addr = XLSX.utils.encode_cell({ r, c });
|
||||
const cell = sheet[addr];
|
||||
if (cell) return cell.v ?? cell.w ?? null;
|
||||
for (const m of merges) {
|
||||
if (m.s.r <= r && r <= m.e.r && m.s.c <= c && c <= m.e.c) {
|
||||
const master = sheet[XLSX.utils.encode_cell({ r: m.s.r, c: m.s.c })];
|
||||
return master ? (master.v ?? master.w ?? null) : null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Read headers
|
||||
const rawHeaders = [];
|
||||
for (let c = range.s.c; c <= range.e.c; c++) {
|
||||
let h = getValue(HEADER_ROW_INDEX, c);
|
||||
rawHeaders.push(h ? String(h).trim() : "");
|
||||
}
|
||||
log(`Found ${rawHeaders.length} headers in Row 3`);
|
||||
log(`Headers: ${rawHeaders.join(" | ")}`);
|
||||
// Compare Excel headers against HEADER_MAP
|
||||
const mappedHeaders = [];
|
||||
const unmappedHeaders = [];
|
||||
for (const h of rawHeaders) {
|
||||
if (headerMap[h]) {
|
||||
mappedHeaders.push([h, headerMap[h]]);
|
||||
} else {
|
||||
unmappedHeaders.push([h, ""]);
|
||||
}
|
||||
}
|
||||
|
||||
// Output as CSV-style rows
|
||||
console.log("\n📑 Header mapping preview (CSV style):");
|
||||
console.log("ExcelHeader,PocketBaseField");
|
||||
for (const [excel, pb] of mappedHeaders) {
|
||||
console.log(`${excel},${pb}`);
|
||||
}
|
||||
for (const [excel, pb] of unmappedHeaders) {
|
||||
console.log(`${excel},${pb}`);
|
||||
}
|
||||
|
||||
// Parse data rows
|
||||
const rows = [];
|
||||
for (let r = DATA_START_ROW_INDEX; r <= range.e.r; r++) {
|
||||
const row = {};
|
||||
let hasData = false;
|
||||
for (let c = range.s.c; c <= range.e.c; c++) {
|
||||
const excelHeader = rawHeaders[c - range.s.c];
|
||||
const pbField = headerMap[excelHeader];
|
||||
if (!pbField) continue;
|
||||
let val = getValue(r, c);
|
||||
if (val !== null && val !== undefined) {
|
||||
const s = String(val).trim();
|
||||
if (s === "" || /^n\/?a$/i.test(s) || s === "#N/A") val = null;
|
||||
}
|
||||
row[pbField] = val;
|
||||
if (val !== null && val !== "") hasData = true;
|
||||
}
|
||||
if (hasData) rows.push(row);
|
||||
}
|
||||
|
||||
log(`Loaded ${rows.length} real jobs`);
|
||||
if (rows[0]) {
|
||||
log(`First job preview: Job_Number=${rows[0].Job_Number}, pb_id=${rows[0].id}`);
|
||||
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// HEADER MAP
|
||||
// -----------------------------
|
||||
const HEADER_MAP = {
|
||||
"Job_Number": "Job_Number",
|
||||
"Job_Full_Name": "Job_Full_Name",
|
||||
"Job_Name": "Job_Name",
|
||||
"Job Address": "Job_Address",
|
||||
"Job Type": "Job_Type",
|
||||
"Flag": "Flag",
|
||||
"Tax_Exempt": "Tax_Exempt",
|
||||
"Job Status": "Job_Status",
|
||||
"Job Division": "Job_Division",
|
||||
"Estimator": "Estimator",
|
||||
"Office Rep": "Office_Rep",
|
||||
"Manager": "Project_Manager",
|
||||
"Company/Client": "Company_Client",
|
||||
"Contact Person": "Contact_Person",
|
||||
"Phone Number": "Phone_Number",
|
||||
"Email": "Email",
|
||||
"Due Date": "Due_Date",
|
||||
"Due Date Source": "Due_Date_Source",
|
||||
"Due Date Counter": "Due_Date_Counter",
|
||||
"Due Time": "Due_Time",
|
||||
"Tax_Exempt_Docs_Uploaded": "Tax_Exempt_Docs_Uploaded",
|
||||
"Docs Uploaded": "Docs_Uploaded",
|
||||
"QB Created": "QB_Created",
|
||||
"Added to Calendar": "Added_To_Calendar",
|
||||
"PSwift Uploaded": "PSwift_Uploaded",
|
||||
"Voxer Created": "Voxer_Created",
|
||||
"Estimate Reviewed": "Estimate_Reviewed",
|
||||
"Estimate Draft": "Estimate_Draft",
|
||||
"Estimate Approved": "Estimate_Approved",
|
||||
"Start Date": "Start_Date",
|
||||
"Schedule_Confidence": "Schedule_Confidence",
|
||||
"Notes": "Notes",
|
||||
"Job QB Link": "Job_QB_Link",
|
||||
"Voxer Link": "Voxer_Link",
|
||||
"Job_Codes": "Job_Codes",
|
||||
"Submission Date": "Submission_Date",
|
||||
"Active": "Active",
|
||||
"Job Folder Link": "Job_Folder_Link",
|
||||
"pb_id": "id"
|
||||
};
|
||||
|
||||
// -----------------------------
|
||||
// MAIN
|
||||
// -----------------------------
|
||||
let parsedRows = [];
|
||||
(async () => {
|
||||
try {
|
||||
log("🔵 Starting Excel Sync…");
|
||||
|
||||
const graphToken = await getGraphToken();
|
||||
const pb = await getPocketBaseClient();
|
||||
// These can be update if the Excel File changes
|
||||
const driveId = "b!9YOqqr2xM0G2DFBwMEJYY4UzQgocFddEtpLYWuS9_AtkCKl2q8yjQJteQ7Ti4QSx";
|
||||
const itemId = "01SPNXLDSQTMG4F6BDANCLJU2FZKEDQLVU";
|
||||
const excelBuffer = await downloadExcelFromGraph(graphToken, driveId, itemId);
|
||||
parsedRows = parseExcelFile(excelBuffer, HEADER_MAP);
|
||||
|
||||
// Run enhanced test (safe, no writes)
|
||||
runEnhancedTest(parsedRows, graphToken, pb);
|
||||
// At this stage you have:
|
||||
// - graphToken (for SharePoint updates later)
|
||||
// - pb client (for PB updates)
|
||||
// - rows[] mapped to PB fields
|
||||
|
||||
const updatedBuffer = exportExcelforDL(parsedRows);
|
||||
await updateNotesInSharePoint(graphToken, driveId, itemId, "Job Sheet", parsedRows, pb, excelBuffer);
|
||||
// Sync to PocketBase
|
||||
//await syncRowsToPocketBase(parsedRows, pb);
|
||||
|
||||
log("✅ First part complete: tokens + Excel parsing ready");
|
||||
} catch (err) {
|
||||
log("❌ Sync failed: " + (err.message || err));
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
// -----------------------------
|
||||
// ENHANCED TEST HARNESS
|
||||
// -----------------------------
|
||||
function runEnhancedTest(rows, graphToken, pb) {
|
||||
log("🔎 Running enhanced test (no writes)…");
|
||||
|
||||
// 1. Verify tokens
|
||||
log(`Graph token length: ${graphToken.length}`);
|
||||
log(`PocketBase token length: ${pb.authStore.token.length}`);
|
||||
|
||||
// 2. Verify Excel parsing
|
||||
log(`Parsed ${rows.length} rows from Excel`);
|
||||
if (rows.length > 0) {
|
||||
const sample = rows[0];
|
||||
log("Sample row preview:");
|
||||
log(` Job_Number: ${sample.Job_Number}`);
|
||||
log(` Job_Full_Name: ${sample.Job_Full_Name}`);
|
||||
log(` Active: ${sample.Active}`);
|
||||
log(` pb_id: ${sample.id}`);
|
||||
log(` Notes: ${sample.Notes}`);
|
||||
}
|
||||
|
||||
// 3. Verify header mapping
|
||||
const mappedFields = Object.values(HEADER_MAP);
|
||||
const sampleKeys = Object.keys(rows[0] || {});
|
||||
const missingFields = mappedFields.filter(f => !sampleKeys.includes(f));
|
||||
if (missingFields.length > 0) {
|
||||
log(`⚠️ Missing mapped fields in sample row: ${missingFields.join(", ")}`);
|
||||
} else {
|
||||
log("✅ All mapped fields present in sample row");
|
||||
}
|
||||
|
||||
log("✅ Enhanced test complete — no writes performed");
|
||||
}
|
||||
|
||||
|
||||
|
||||
// -------------------------
|
||||
// Export to xlsx - Excel
|
||||
// ---------------------------
|
||||
|
||||
// Toggle this flag to enable/disable the export server
|
||||
const ENABLE_EXPORT = false; // set to false to bypass download
|
||||
|
||||
function exportExcelforDL(parsedRows) {
|
||||
if (!parsedRows || parsedRows.length === 0) {
|
||||
throw new Error("⚠️ No rows to export");
|
||||
}
|
||||
|
||||
const worksheet = XLSX.utils.json_to_sheet(parsedRows);
|
||||
|
||||
// Ensure Job_Number is the first column
|
||||
const cols = Object.keys(parsedRows[0] || {});
|
||||
if (cols.includes("Job_Number")) {
|
||||
const ordered = ["Job_Number", ...cols.filter(c => c !== "Job_Number")];
|
||||
const remapped = parsedRows.map(r =>
|
||||
ordered.reduce((acc, key) => { acc[key] = r[key]; return acc; }, {})
|
||||
);
|
||||
const ws2 = XLSX.utils.json_to_sheet(remapped);
|
||||
Object.assign(worksheet, ws2);
|
||||
}
|
||||
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, "ParsedData");
|
||||
|
||||
return XLSX.write(workbook, { type: "buffer", bookType: "xlsx" });
|
||||
}
|
||||
|
||||
if (ENABLE_EXPORT) {
|
||||
const PORT = 3000;
|
||||
serve({
|
||||
port: PORT,
|
||||
fetch(req) {
|
||||
const url = new URL(req.url);
|
||||
|
||||
if (url.pathname === "/") {
|
||||
const html = `
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head><meta charset="utf-8"><title>ExcelSync Export</title></head>
|
||||
<body>
|
||||
<h1>ExcelSync Export</h1>
|
||||
<p>Click below to download the parsed Excel file:</p>
|
||||
<button onclick="window.location.href='/download'">Download ParsedOutput.xlsx</button>
|
||||
</body>
|
||||
</html>`;
|
||||
return new Response(html, { headers: { "Content-Type": "text/html" } });
|
||||
}
|
||||
|
||||
if (url.pathname === "/download") {
|
||||
try {
|
||||
const buffer = exportExcelforDL(parsedRows);
|
||||
return new Response(buffer, {
|
||||
headers: {
|
||||
"Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"Content-Disposition": "attachment; filename=ParsedOutput.xlsx"
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
return new Response("❌ Failed to generate Excel file: " + err.message, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
return new Response("Visit / to open the export page.");
|
||||
}
|
||||
});
|
||||
|
||||
log(`🚀 ExcelSync export server running at http://localhost:${PORT}/`);
|
||||
log(`👉 Open this URL in your browser to verify and download the parsed Excel file.`);
|
||||
} else {
|
||||
log("⏭️ Export server bypassed (ENABLE_EXPORT=false)");
|
||||
}
|
||||
|
||||
|
||||
// -----------------------------
|
||||
// UPDATE NOTES CELLS IN SHAREPOINT (REAL-TIME)
|
||||
// -----------------------------
|
||||
function getNotesColumnLetterFromParsedHeaders(headerMap) {
|
||||
const headers = Object.keys(headerMap);
|
||||
const notesIndex = headers.findIndex(h => h.trim().toLowerCase() === "notes");
|
||||
if (notesIndex === -1) {
|
||||
log(`❌ 'Notes' header not found in HEADER_MAP keys: ${headers.join(", ")}`);
|
||||
throw new Error("Notes column not found");
|
||||
}
|
||||
return columnLetter(notesIndex);
|
||||
}
|
||||
|
||||
// Update only changed Notes cells using workbook session + PATCH.
|
||||
// This version relies solely on DATA_START_ROW_INDEX for row mapping.
|
||||
async function updateNotesInSharePoint(graphToken, driveId, itemId, worksheetName, parsedRows, pb, workbookBuffer) {
|
||||
const wb = XLSX.read(workbookBuffer, { type: "buffer", cellDates: true });
|
||||
const sheet = wb.Sheets[worksheetName];
|
||||
if (!sheet) throw new Error(`Worksheet "${worksheetName}" not found`);
|
||||
|
||||
// Column detection aligned to HEADER_MAP (same mapping used by parseExcelFile)
|
||||
const notesColumnLetter = getNotesColumnLetterFromParsedHeaders(HEADER_MAP);
|
||||
log(`✅ Notes column resolved to ${notesColumnLetter} via HEADER_MAP`);
|
||||
|
||||
// Create a workbook session that persists changes
|
||||
const sessionRes = await axios.post(
|
||||
`https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/workbook/createSession`,
|
||||
{ persistChanges: true },
|
||||
{ headers: { Authorization: `Bearer ${graphToken}` } }
|
||||
);
|
||||
const sessionId = sessionRes.data.id;
|
||||
|
||||
let changes = 0;
|
||||
|
||||
// Loop parsed rows; map i to Excel row using DATA_START_ROW_INDEX (zero-based)
|
||||
for (let i = 0; i < parsedRows.length; i++) {
|
||||
const row = parsedRows[i];
|
||||
const jobNumber = row.Job_Number;
|
||||
if (jobNumber == null || jobNumber === "") continue;
|
||||
|
||||
try {
|
||||
const records = await pb.collection(pbCollection).getFullList({
|
||||
filter: Number.isFinite(jobNumber) ? `Job_Number=${jobNumber}` : `Job_Number="${jobNumber}"`,
|
||||
batchSize: 1
|
||||
});
|
||||
if (records.length === 0) continue;
|
||||
|
||||
// Normalize notes for fair comparison
|
||||
const normalize = v => (v ?? "").toString().replace(/\s+/g, " ").trim();
|
||||
const pbNotes = normalize(records[0].Notes);
|
||||
const excelNotes = normalize(row.Notes);
|
||||
|
||||
if (pbNotes !== excelNotes) {
|
||||
// Excel uses 1-based row numbers; DATA_START_ROW_INDEX is zero-based
|
||||
const excelRowNumber = DATA_START_ROW_INDEX + i + 1;
|
||||
const cellAddress = `${notesColumnLetter}${excelRowNumber}`;
|
||||
|
||||
log(`✏️ Notes change for Job_Number=${jobNumber}`);
|
||||
log(` Before: "${excelNotes}"`);
|
||||
log(` After : "${pbNotes}"`);
|
||||
log(`➡️ Writing to ${worksheetName}!${cellAddress}`);
|
||||
|
||||
await axios.patch(
|
||||
`https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/workbook/worksheets('${worksheetName}')/range(address='${cellAddress}')`,
|
||||
{ values: [[pbNotes]] },
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${graphToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"workbook-session-id": sessionId
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
row.Notes = pbNotes;
|
||||
changes++;
|
||||
}
|
||||
} catch (err) {
|
||||
log(`❌ Failed for Job_Number=${jobNumber}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
await axios.post(
|
||||
`https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/workbook/closeSession`,
|
||||
{},
|
||||
{ headers: { Authorization: `Bearer ${graphToken}`, "workbook-session-id": sessionId } }
|
||||
);
|
||||
|
||||
log(`✅ Notes-only sync complete. Changes applied: ${changes}`);
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
// excelsync.js
|
||||
import fs from "fs";
|
||||
import axios from "axios";
|
||||
import qs from "qs";
|
||||
import * as XLSX from "xlsx";
|
||||
import dotenv from "dotenv";
|
||||
import { serve } from "bun";
|
||||
import PocketBase from "pocketbase"
|
||||
|
||||
dotenv.config();
|
||||
|
||||
// -----------------------------
|
||||
// CONFIG
|
||||
// -----------------------------
|
||||
const CONFIG = JSON.parse(fs.readFileSync("config.json", "utf8"));
|
||||
const tenantId = CONFIG.tenantId;
|
||||
const clientId = CONFIG.clientId;
|
||||
const sharepointFileUrl = CONFIG.sharepointFileUrl; // production Excel file URL
|
||||
const pbUrl = CONFIG.pbUrl;
|
||||
const pbCollection = CONFIG.pbCollection || "Job_Info_TestEnv";
|
||||
const pbEmail = CONFIG.pbEmail;
|
||||
|
||||
const clientSecret = process.env.CLIENT_SECRET;
|
||||
const pbPassword = process.env.PB_PASSWORD;
|
||||
|
||||
if (!tenantId || !clientId || !clientSecret || !sharepointFileUrl || !pbUrl || !pbEmail || !pbPassword) {
|
||||
console.error("❌ Missing required config/env values");
|
||||
process.exit(1);
|
||||
}
|
||||
console.log({
|
||||
tenantId,
|
||||
clientId,
|
||||
clientSecret: clientSecret ? clientSecret.slice(0,4) + "..." : null,
|
||||
sharepointFileUrl,
|
||||
pbUrl,
|
||||
pbEmail,
|
||||
pbPassword: pbPassword ? "***" : null
|
||||
});
|
||||
// -----------------------------
|
||||
// LOGGING
|
||||
// -----------------------------
|
||||
function log(message) {
|
||||
console.log(message);
|
||||
fs.appendFileSync("src/excelsync.log", message + "\n");
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// GRAPH TOKEN
|
||||
// -----------------------------
|
||||
async function getGraphToken() {
|
||||
const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
|
||||
const body = qs.stringify({
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
scope: "https://graph.microsoft.com/.default",
|
||||
grant_type: "client_credentials",
|
||||
});
|
||||
const res = await axios.post(tokenUrl, body, {
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
});
|
||||
log("✅ Graph token acquired");
|
||||
return res.data.access_token;
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// GRAPH DOWNLOADER HELPER
|
||||
// -----------------------------
|
||||
async function downloadExcelFromGraph(graphToken, driveId, itemId) {
|
||||
const url = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/content`;
|
||||
const res = await axios.get(url, {
|
||||
headers: { Authorization: `Bearer ${graphToken}` },
|
||||
responseType: "arraybuffer", // important: get raw binary
|
||||
});
|
||||
return Buffer.from(res.data); // convert to Buffer for XLSX
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// POCKETBASE LOGIN
|
||||
// -----------------------------
|
||||
async function getPocketBaseClient() {
|
||||
const pb = new PocketBase(pbUrl);
|
||||
await pb.collection("users").authWithPassword(pbEmail, pbPassword);
|
||||
log("✅ PocketBase token acquired");
|
||||
return pb;
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// EXCEL PARSER
|
||||
// -----------------------------
|
||||
const HEADER_ROW_INDEX = 2; // Row 3
|
||||
const DATA_START_ROW_INDEX = 3; // Row 4
|
||||
|
||||
function parseExcelFile(input, headerMap) {
|
||||
let data;
|
||||
// In case of data from graph
|
||||
if (Buffer.isBuffer(input)) {
|
||||
data = input;
|
||||
log(`Reading Excel file from Graph buffer`);
|
||||
// In case of file path (Not used at this time)
|
||||
} else {
|
||||
data = fs.readFileSync(input);
|
||||
log(`Reading Excel file: ${input}`);
|
||||
}
|
||||
|
||||
const workbook = XLSX.read(data, { type: "buffer", cellDates: true });
|
||||
const sheet = workbook.Sheets["Job Sheet"]; // explicitly use Job Sheet
|
||||
const range = XLSX.utils.decode_range(sheet["!ref"]);
|
||||
const merges = sheet["!merges"] || [];
|
||||
|
||||
const getValue = (r, c) => {
|
||||
const addr = XLSX.utils.encode_cell({ r, c });
|
||||
const cell = sheet[addr];
|
||||
if (cell) return cell.v ?? cell.w ?? null;
|
||||
for (const m of merges) {
|
||||
if (m.s.r <= r && r <= m.e.r && m.s.c <= c && c <= m.e.c) {
|
||||
const master = sheet[XLSX.utils.encode_cell({ r: m.s.r, c: m.s.c })];
|
||||
return master ? (master.v ?? master.w ?? null) : null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Read headers
|
||||
const rawHeaders = [];
|
||||
for (let c = range.s.c; c <= range.e.c; c++) {
|
||||
let h = getValue(HEADER_ROW_INDEX, c);
|
||||
rawHeaders.push(h ? String(h).trim() : "");
|
||||
}
|
||||
log(`Found ${rawHeaders.length} headers in Row 3`);
|
||||
log(`Headers: ${rawHeaders.join(" | ")}`);
|
||||
// Compare Excel headers against HEADER_MAP
|
||||
const mappedHeaders = [];
|
||||
const unmappedHeaders = [];
|
||||
for (const h of rawHeaders) {
|
||||
if (headerMap[h]) {
|
||||
mappedHeaders.push([h, headerMap[h]]);
|
||||
} else {
|
||||
unmappedHeaders.push([h, ""]);
|
||||
}
|
||||
}
|
||||
|
||||
// Output as CSV-style rows
|
||||
console.log("\n📑 Header mapping preview (CSV style):");
|
||||
console.log("ExcelHeader,PocketBaseField");
|
||||
for (const [excel, pb] of mappedHeaders) {
|
||||
console.log(`${excel},${pb}`);
|
||||
}
|
||||
for (const [excel, pb] of unmappedHeaders) {
|
||||
console.log(`${excel},${pb}`);
|
||||
}
|
||||
|
||||
// Parse data rows
|
||||
const rows = [];
|
||||
for (let r = DATA_START_ROW_INDEX; r <= range.e.r; r++) {
|
||||
const row = {};
|
||||
let hasData = false;
|
||||
for (let c = range.s.c; c <= range.e.c; c++) {
|
||||
const excelHeader = rawHeaders[c - range.s.c];
|
||||
const pbField = headerMap[excelHeader];
|
||||
if (!pbField) continue;
|
||||
let val = getValue(r, c);
|
||||
if (val !== null && val !== undefined) {
|
||||
const s = String(val).trim();
|
||||
if (s === "" || /^n\/?a$/i.test(s) || s === "#N/A") val = null;
|
||||
}
|
||||
row[pbField] = val;
|
||||
if (val !== null && val !== "") hasData = true;
|
||||
}
|
||||
if (hasData) rows.push(row);
|
||||
}
|
||||
|
||||
log(`Loaded ${rows.length} real jobs`);
|
||||
if (rows[0]) {
|
||||
log(`First job preview: Job_Number=${rows[0].Job_Number}, pb_id=${rows[0].id}`);
|
||||
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// HEADER MAP
|
||||
// -----------------------------
|
||||
const HEADER_MAP = {
|
||||
"Job_Number": "Job_Number",
|
||||
"Job_Full_Name": "Job_Full_Name",
|
||||
"Job_Name": "Job_Name",
|
||||
"Job Address": "Job_Address",
|
||||
"Job Type": "Job_Type",
|
||||
"Flag": "Flag",
|
||||
"Tax_Exempt": "Tax_Exempt",
|
||||
"Job Status": "Job_Status",
|
||||
"Job Division": "Division",
|
||||
"Estimator": "Estimator",
|
||||
"Office Rep": "Office_Rep",
|
||||
"Manager": "Project_Manager",
|
||||
"Company/Client": "Company_Client",
|
||||
"Contact Person": "Contact_Person",
|
||||
"Phone Number": "Phone_Number",
|
||||
"Email": "Email",
|
||||
"Due Date": "Due_Date",
|
||||
"Due Date Source": "Due_Date_Source",
|
||||
"Due Date Counter": "Due_Date_Counter",
|
||||
"Due Time": "Due_Time",
|
||||
"Tax_Exempt_Docs_Uploaded": "Tax_Exempt_Docs_Uploaded",
|
||||
"Docs Uploaded": "Docs_Uploaded",
|
||||
"QB Created": "QB_Created",
|
||||
"Added to Calendar": "Calendar_Added",
|
||||
"PSwift Uploaded": "PSwift_Uploaded",
|
||||
"Voxer Created": "Voxer_Created",
|
||||
"Estimate Reviewed": "Estimate_Reviewed",
|
||||
"Estimate Draft": "Estimate_Draft",
|
||||
"Estimate Approved": "Estimate_Approved",
|
||||
"Start Date": "Start_Date",
|
||||
"Schedule_Confidence": "Schedule_Confidence",
|
||||
"Notes": "Notes",
|
||||
"Job QB Link": "Job_QB_Link",
|
||||
"Voxer Link": "Voxer_Link",
|
||||
"Job_Codes": "Job_Codes",
|
||||
"Submission Date": "Submission_Date",
|
||||
"Active": "Active",
|
||||
"Job Folder Link": "Job_Folder_Link",
|
||||
"pb_id": "id"
|
||||
};
|
||||
|
||||
// -----------------------------
|
||||
// MAIN
|
||||
// -----------------------------
|
||||
let parsedRows = [];
|
||||
(async () => {
|
||||
try {
|
||||
log("🔵 Starting Excel Sync…");
|
||||
|
||||
const graphToken = await getGraphToken();
|
||||
const pb = await getPocketBaseClient();
|
||||
// These can be update if the Excel File changes
|
||||
const driveId = "b!9YOqqr2xM0G2DFBwMEJYY4UzQgocFddEtpLYWuS9_AtkCKl2q8yjQJteQ7Ti4QSx";
|
||||
const itemId = "01SPNXLDSQTMG4F6BDANCLJU2FZKEDQLVU";
|
||||
const excelBuffer = await downloadExcelFromGraph(graphToken, driveId, itemId);
|
||||
parsedRows = parseExcelFile(excelBuffer, HEADER_MAP);
|
||||
|
||||
// Run enhanced test (safe, no writes)
|
||||
runEnhancedTest(parsedRows, graphToken, pb);
|
||||
// At this stage you have:
|
||||
// - graphToken (for SharePoint updates later)
|
||||
// - pb client (for PB updates)
|
||||
// - rows[] mapped to PB fields
|
||||
|
||||
// Sync to PocketBase
|
||||
// await syncRowsToPocketBase(parsedRows, pb);
|
||||
|
||||
log("✅ First part complete: tokens + Excel parsing ready");
|
||||
} catch (err) {
|
||||
log("❌ Sync failed: " + (err.message || err));
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
// -----------------------------
|
||||
// ENHANCED TEST HARNESS
|
||||
// -----------------------------
|
||||
function runEnhancedTest(rows, graphToken, pb) {
|
||||
log("🔎 Running enhanced test (no writes)…");
|
||||
|
||||
// 1. Verify tokens
|
||||
log(`Graph token length: ${graphToken.length}`);
|
||||
log(`PocketBase token length: ${pb.authStore.token.length}`);
|
||||
|
||||
// 2. Verify Excel parsing
|
||||
log(`Parsed ${rows.length} rows from Excel`);
|
||||
if (rows.length > 0) {
|
||||
const sample = rows[0];
|
||||
log("Sample row preview:");
|
||||
log(` Job_Number: ${sample.Job_Number}`);
|
||||
log(` Job_Full_Name: ${sample.Job_Full_Name}`);
|
||||
log(` Active: ${sample.Active}`);
|
||||
log(` pb_id: ${sample.id}`);
|
||||
log(` Notes: ${sample.Notes}`);
|
||||
}
|
||||
|
||||
// 3. Verify header mapping
|
||||
const mappedFields = Object.values(HEADER_MAP);
|
||||
const sampleKeys = Object.keys(rows[0] || {});
|
||||
const missingFields = mappedFields.filter(f => !sampleKeys.includes(f));
|
||||
if (missingFields.length > 0) {
|
||||
log(`⚠️ Missing mapped fields in sample row: ${missingFields.join(", ")}`);
|
||||
} else {
|
||||
log("✅ All mapped fields present in sample row");
|
||||
}
|
||||
|
||||
log("✅ Enhanced test complete — no writes performed");
|
||||
}
|
||||
|
||||
|
||||
|
||||
// -------------------------
|
||||
// Export to xlsx - Excel
|
||||
// ---------------------------
|
||||
|
||||
function exportExcelforDL(parsedRows) {
|
||||
if (!parsedRows || parsedRows.length === 0) {
|
||||
throw new Error("⚠️ No rows to export");
|
||||
}
|
||||
|
||||
const worksheet = XLSX.utils.json_to_sheet(parsedRows);
|
||||
|
||||
// Ensure Job_Number is the first column
|
||||
const cols = Object.keys(parsedRows[0] || {});
|
||||
if (cols.includes("Job_Number")) {
|
||||
const ordered = ["Job_Number", ...cols.filter(c => c !== "Job_Number")];
|
||||
const remapped = parsedRows.map(r =>
|
||||
ordered.reduce((acc, key) => { acc[key] = r[key]; return acc; }, {})
|
||||
);
|
||||
const ws2 = XLSX.utils.json_to_sheet(remapped);
|
||||
Object.assign(worksheet, ws2);
|
||||
}
|
||||
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, "ParsedData");
|
||||
|
||||
return XLSX.write(workbook, { type: "buffer", bookType: "xlsx" });
|
||||
}
|
||||
|
||||
function isParsedReady() {
|
||||
return Array.isArray(parsedRows) && parsedRows.length > 0;
|
||||
}
|
||||
|
||||
const PORT = 3000;
|
||||
|
||||
serve({
|
||||
port: PORT,
|
||||
fetch(req) {
|
||||
const url = new URL(req.url);
|
||||
|
||||
if (url.pathname === "/") {
|
||||
const html = `
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head><meta charset="utf-8"><title>ExcelSync Export</title></head>
|
||||
<body>
|
||||
<h1>ExcelSync Export</h1>
|
||||
<p>Click below to download the parsed Excel file:</p>
|
||||
<button onclick="window.location.href='/download'">Download ParsedOutput.xlsx</button>
|
||||
</body>
|
||||
</html>`;
|
||||
return new Response(html, { headers: { "Content-Type": "text/html" } });
|
||||
}
|
||||
|
||||
if (url.pathname === "/download") {
|
||||
try {
|
||||
if (!isParsedReady()) {
|
||||
return new Response("⚠️ Parsed rows are not ready yet. Try again in a moment.", { status: 409 });
|
||||
}
|
||||
const buffer = exportExcelforDL(parsedRows);
|
||||
return new Response(buffer, {
|
||||
headers: {
|
||||
"Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"Content-Disposition": "attachment; filename=ParsedOutput.xlsx"
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
return new Response("❌ Failed to generate Excel file: " + err.message, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
return new Response("Visit / to open the export page.");
|
||||
}
|
||||
});
|
||||
|
||||
// Prompt on startup
|
||||
log(`🚀 ExcelSync export server running at http://localhost:${PORT}/`);
|
||||
log(`👉 Open this URL in your browser to verify and download the parsed Excel file.`);
|
||||
@@ -0,0 +1,375 @@
|
||||
//notes - works to get excel downloaded bypass downloader with "false"
|
||||
// excelsync.js
|
||||
import fs from "fs";
|
||||
import axios from "axios";
|
||||
import qs from "qs";
|
||||
import * as XLSX from "xlsx";
|
||||
import dotenv from "dotenv";
|
||||
import { serve } from "bun";
|
||||
import PocketBase from "pocketbase"
|
||||
|
||||
dotenv.config();
|
||||
|
||||
// -----------------------------
|
||||
// CONFIG
|
||||
// -----------------------------
|
||||
const CONFIG = JSON.parse(fs.readFileSync("config.json", "utf8"));
|
||||
const tenantId = CONFIG.tenantId;
|
||||
const clientId = CONFIG.clientId;
|
||||
const sharepointFileUrl = CONFIG.sharepointFileUrl; // production Excel file URL
|
||||
const pbUrl = CONFIG.pbUrl;
|
||||
const pbCollection = CONFIG.pbCollection || "Job_Info_TestEnv";
|
||||
const pbEmail = CONFIG.pbEmail;
|
||||
|
||||
const clientSecret = process.env.CLIENT_SECRET;
|
||||
const pbPassword = process.env.PB_PASSWORD;
|
||||
|
||||
if (!tenantId || !clientId || !clientSecret || !sharepointFileUrl || !pbUrl || !pbEmail || !pbPassword) {
|
||||
console.error("❌ Missing required config/env values");
|
||||
process.exit(1);
|
||||
}
|
||||
console.log({
|
||||
tenantId,
|
||||
clientId,
|
||||
clientSecret: clientSecret ? clientSecret.slice(0,4) + "..." : null,
|
||||
sharepointFileUrl,
|
||||
pbUrl,
|
||||
pbEmail,
|
||||
pbPassword: pbPassword ? "***" : null
|
||||
});
|
||||
// -----------------------------
|
||||
// LOGGING
|
||||
// -----------------------------
|
||||
function log(message) {
|
||||
console.log(message);
|
||||
fs.appendFileSync("src/excelsync.log", message + "\n");
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// GRAPH TOKEN
|
||||
// -----------------------------
|
||||
async function getGraphToken() {
|
||||
const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
|
||||
const body = qs.stringify({
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
scope: "https://graph.microsoft.com/.default",
|
||||
grant_type: "client_credentials",
|
||||
});
|
||||
const res = await axios.post(tokenUrl, body, {
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
});
|
||||
log("✅ Graph token acquired");
|
||||
return res.data.access_token;
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// GRAPH DOWNLOADER HELPER
|
||||
// -----------------------------
|
||||
async function downloadExcelFromGraph(graphToken, driveId, itemId) {
|
||||
const url = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/content`;
|
||||
const res = await axios.get(url, {
|
||||
headers: { Authorization: `Bearer ${graphToken}` },
|
||||
responseType: "arraybuffer", // important: get raw binary
|
||||
});
|
||||
return Buffer.from(res.data); // convert to Buffer for XLSX
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// POCKETBASE LOGIN
|
||||
// -----------------------------
|
||||
async function getPocketBaseClient() {
|
||||
const pb = new PocketBase(pbUrl);
|
||||
await pb.collection("users").authWithPassword(pbEmail, pbPassword);
|
||||
log("✅ PocketBase token acquired");
|
||||
return pb;
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// EXCEL PARSER
|
||||
// -----------------------------
|
||||
const HEADER_ROW_INDEX = 2; // Row 3
|
||||
const DATA_START_ROW_INDEX = 3; // Row 4
|
||||
|
||||
function parseExcelFile(input, headerMap) {
|
||||
let data;
|
||||
// In case of data from graph
|
||||
if (Buffer.isBuffer(input)) {
|
||||
data = input;
|
||||
log(`Reading Excel file from Graph buffer`);
|
||||
// In case of file path (Not used at this time)
|
||||
} else {
|
||||
data = fs.readFileSync(input);
|
||||
log(`Reading Excel file: ${input}`);
|
||||
}
|
||||
|
||||
const workbook = XLSX.read(data, { type: "buffer", cellDates: true });
|
||||
const sheet = workbook.Sheets["Job Sheet"]; // explicitly use Job Sheet
|
||||
const range = XLSX.utils.decode_range(sheet["!ref"]);
|
||||
const merges = sheet["!merges"] || [];
|
||||
|
||||
const getValue = (r, c) => {
|
||||
const addr = XLSX.utils.encode_cell({ r, c });
|
||||
const cell = sheet[addr];
|
||||
if (cell) return cell.v ?? cell.w ?? null;
|
||||
for (const m of merges) {
|
||||
if (m.s.r <= r && r <= m.e.r && m.s.c <= c && c <= m.e.c) {
|
||||
const master = sheet[XLSX.utils.encode_cell({ r: m.s.r, c: m.s.c })];
|
||||
return master ? (master.v ?? master.w ?? null) : null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Read headers
|
||||
const rawHeaders = [];
|
||||
for (let c = range.s.c; c <= range.e.c; c++) {
|
||||
let h = getValue(HEADER_ROW_INDEX, c);
|
||||
rawHeaders.push(h ? String(h).trim() : "");
|
||||
}
|
||||
log(`Found ${rawHeaders.length} headers in Row 3`);
|
||||
log(`Headers: ${rawHeaders.join(" | ")}`);
|
||||
// Compare Excel headers against HEADER_MAP
|
||||
const mappedHeaders = [];
|
||||
const unmappedHeaders = [];
|
||||
for (const h of rawHeaders) {
|
||||
if (headerMap[h]) {
|
||||
mappedHeaders.push([h, headerMap[h]]);
|
||||
} else {
|
||||
unmappedHeaders.push([h, ""]);
|
||||
}
|
||||
}
|
||||
|
||||
// Output as CSV-style rows
|
||||
console.log("\n📑 Header mapping preview (CSV style):");
|
||||
console.log("ExcelHeader,PocketBaseField");
|
||||
for (const [excel, pb] of mappedHeaders) {
|
||||
console.log(`${excel},${pb}`);
|
||||
}
|
||||
for (const [excel, pb] of unmappedHeaders) {
|
||||
console.log(`${excel},${pb}`);
|
||||
}
|
||||
|
||||
// Parse data rows
|
||||
const rows = [];
|
||||
for (let r = DATA_START_ROW_INDEX; r <= range.e.r; r++) {
|
||||
const row = {};
|
||||
let hasData = false;
|
||||
for (let c = range.s.c; c <= range.e.c; c++) {
|
||||
const excelHeader = rawHeaders[c - range.s.c];
|
||||
const pbField = headerMap[excelHeader];
|
||||
if (!pbField) continue;
|
||||
let val = getValue(r, c);
|
||||
if (val !== null && val !== undefined) {
|
||||
const s = String(val).trim();
|
||||
if (s === "" || /^n\/?a$/i.test(s) || s === "#N/A") val = null;
|
||||
}
|
||||
row[pbField] = val;
|
||||
if (val !== null && val !== "") hasData = true;
|
||||
}
|
||||
if (hasData) rows.push(row);
|
||||
}
|
||||
|
||||
log(`Loaded ${rows.length} real jobs`);
|
||||
if (rows[0]) {
|
||||
log(`First job preview: Job_Number=${rows[0].Job_Number}, pb_id=${rows[0].id}`);
|
||||
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// HEADER MAP
|
||||
// -----------------------------
|
||||
const HEADER_MAP = {
|
||||
"Job_Number": "Job_Number",
|
||||
"Job_Full_Name": "Job_Full_Name",
|
||||
"Job_Name": "Job_Name",
|
||||
"Job Address": "Job_Address",
|
||||
"Job Type": "Job_Type",
|
||||
"Flag": "Flag",
|
||||
"Tax_Exempt": "Tax_Exempt",
|
||||
"Job Status": "Job_Status",
|
||||
"Job Division": "Job_Division",
|
||||
"Estimator": "Estimator",
|
||||
"Office Rep": "Office_Rep",
|
||||
"Manager": "Project_Manager",
|
||||
"Company/Client": "Company_Client",
|
||||
"Contact Person": "Contact_Person",
|
||||
"Phone Number": "Phone_Number",
|
||||
"Email": "Email",
|
||||
"Due Date": "Due_Date",
|
||||
"Due Date Source": "Due_Date_Source",
|
||||
"Due Date Counter": "Due_Date_Counter",
|
||||
"Due Time": "Due_Time",
|
||||
"Tax_Exempt_Docs_Uploaded": "Tax_Exempt_Docs_Uploaded",
|
||||
"Docs Uploaded": "Docs_Uploaded",
|
||||
"QB Created": "QB_Created",
|
||||
"Added to Calendar": "Added_To_Calendar",
|
||||
"PSwift Uploaded": "PSwift_Uploaded",
|
||||
"Voxer Created": "Voxer_Created",
|
||||
"Estimate Reviewed": "Estimate_Reviewed",
|
||||
"Estimate Draft": "Estimate_Draft",
|
||||
"Estimate Approved": "Estimate_Approved",
|
||||
"Start Date": "Start_Date",
|
||||
"Schedule_Confidence": "Schedule_Confidence",
|
||||
"Notes": "Notes",
|
||||
"Job QB Link": "Job_QB_Link",
|
||||
"Voxer Link": "Voxer_Link",
|
||||
"Job_Codes": "Job_Codes",
|
||||
"Submission Date": "Submission_Date",
|
||||
"Active": "Active",
|
||||
"Job Folder Link": "Job_Folder_Link",
|
||||
"pb_id": "id"
|
||||
};
|
||||
|
||||
// -----------------------------
|
||||
// MAIN
|
||||
// -----------------------------
|
||||
let parsedRows = [];
|
||||
(async () => {
|
||||
try {
|
||||
log("🔵 Starting Excel Sync…");
|
||||
|
||||
const graphToken = await getGraphToken();
|
||||
const pb = await getPocketBaseClient();
|
||||
// These can be update if the Excel File changes
|
||||
const driveId = "b!9YOqqr2xM0G2DFBwMEJYY4UzQgocFddEtpLYWuS9_AtkCKl2q8yjQJteQ7Ti4QSx";
|
||||
const itemId = "01SPNXLDSQTMG4F6BDANCLJU2FZKEDQLVU";
|
||||
const excelBuffer = await downloadExcelFromGraph(graphToken, driveId, itemId);
|
||||
parsedRows = parseExcelFile(excelBuffer, HEADER_MAP);
|
||||
|
||||
// Run enhanced test (safe, no writes)
|
||||
runEnhancedTest(parsedRows, graphToken, pb);
|
||||
// At this stage you have:
|
||||
// - graphToken (for SharePoint updates later)
|
||||
// - pb client (for PB updates)
|
||||
// - rows[] mapped to PB fields
|
||||
|
||||
// Sync to PocketBase
|
||||
//await syncRowsToPocketBase(parsedRows, pb);
|
||||
|
||||
log("✅ First part complete: tokens + Excel parsing ready");
|
||||
} catch (err) {
|
||||
log("❌ Sync failed: " + (err.message || err));
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
// -----------------------------
|
||||
// ENHANCED TEST HARNESS
|
||||
// -----------------------------
|
||||
function runEnhancedTest(rows, graphToken, pb) {
|
||||
log("🔎 Running enhanced test (no writes)…");
|
||||
|
||||
// 1. Verify tokens
|
||||
log(`Graph token length: ${graphToken.length}`);
|
||||
log(`PocketBase token length: ${pb.authStore.token.length}`);
|
||||
|
||||
// 2. Verify Excel parsing
|
||||
log(`Parsed ${rows.length} rows from Excel`);
|
||||
if (rows.length > 0) {
|
||||
const sample = rows[0];
|
||||
log("Sample row preview:");
|
||||
log(` Job_Number: ${sample.Job_Number}`);
|
||||
log(` Job_Full_Name: ${sample.Job_Full_Name}`);
|
||||
log(` Active: ${sample.Active}`);
|
||||
log(` pb_id: ${sample.id}`);
|
||||
log(` Notes: ${sample.Notes}`);
|
||||
}
|
||||
|
||||
// 3. Verify header mapping
|
||||
const mappedFields = Object.values(HEADER_MAP);
|
||||
const sampleKeys = Object.keys(rows[0] || {});
|
||||
const missingFields = mappedFields.filter(f => !sampleKeys.includes(f));
|
||||
if (missingFields.length > 0) {
|
||||
log(`⚠️ Missing mapped fields in sample row: ${missingFields.join(", ")}`);
|
||||
} else {
|
||||
log("✅ All mapped fields present in sample row");
|
||||
}
|
||||
|
||||
log("✅ Enhanced test complete — no writes performed");
|
||||
}
|
||||
|
||||
|
||||
|
||||
// -------------------------
|
||||
// Export to xlsx - Excel
|
||||
// ---------------------------
|
||||
|
||||
// Toggle this flag to enable/disable the export server
|
||||
const ENABLE_EXPORT = true; // set to false to bypass download
|
||||
|
||||
function exportExcelforDL(parsedRows) {
|
||||
if (!parsedRows || parsedRows.length === 0) {
|
||||
throw new Error("⚠️ No rows to export");
|
||||
}
|
||||
|
||||
const worksheet = XLSX.utils.json_to_sheet(parsedRows);
|
||||
|
||||
// Ensure Job_Number is the first column
|
||||
const cols = Object.keys(parsedRows[0] || {});
|
||||
if (cols.includes("Job_Number")) {
|
||||
const ordered = ["Job_Number", ...cols.filter(c => c !== "Job_Number")];
|
||||
const remapped = parsedRows.map(r =>
|
||||
ordered.reduce((acc, key) => { acc[key] = r[key]; return acc; }, {})
|
||||
);
|
||||
const ws2 = XLSX.utils.json_to_sheet(remapped);
|
||||
Object.assign(worksheet, ws2);
|
||||
}
|
||||
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, "ParsedData");
|
||||
|
||||
return XLSX.write(workbook, { type: "buffer", bookType: "xlsx" });
|
||||
}
|
||||
|
||||
if (ENABLE_EXPORT) {
|
||||
const PORT = 3000;
|
||||
serve({
|
||||
port: PORT,
|
||||
fetch(req) {
|
||||
const url = new URL(req.url);
|
||||
|
||||
if (url.pathname === "/") {
|
||||
const html = `
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head><meta charset="utf-8"><title>ExcelSync Export</title></head>
|
||||
<body>
|
||||
<h1>ExcelSync Export</h1>
|
||||
<p>Click below to download the parsed Excel file:</p>
|
||||
<button onclick="window.location.href='/download'">Download ParsedOutput.xlsx</button>
|
||||
</body>
|
||||
</html>`;
|
||||
return new Response(html, { headers: { "Content-Type": "text/html" } });
|
||||
}
|
||||
|
||||
if (url.pathname === "/download") {
|
||||
try {
|
||||
const buffer = exportExcelforDL(parsedRows);
|
||||
return new Response(buffer, {
|
||||
headers: {
|
||||
"Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"Content-Disposition": "attachment; filename=ParsedOutput.xlsx"
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
return new Response("❌ Failed to generate Excel file: " + err.message, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
return new Response("Visit / to open the export page.");
|
||||
}
|
||||
});
|
||||
|
||||
log(`🚀 ExcelSync export server running at http://localhost:${PORT}/`);
|
||||
log(`👉 Open this URL in your browser to verify and download the parsed Excel file.`);
|
||||
} else {
|
||||
log("⏭️ Export server bypassed (ENABLE_EXPORT=false)");
|
||||
}
|
||||
|
||||
|
||||
// -----------------------------
|
||||
// FINAL SYNC FUNCTION
|
||||
// -----------------------------
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
// fieldConfig.js
|
||||
export const FIELD_CONFIG = {
|
||||
id: {
|
||||
excelHeader: "pb_id",
|
||||
pbField: "id",
|
||||
type: "string",
|
||||
required: true
|
||||
},
|
||||
Job_Number: {
|
||||
excelHeader: "Job_Number",
|
||||
pbField: "Job_Number",
|
||||
type: "string"
|
||||
},
|
||||
Job_Full_Name: {
|
||||
excelHeader: "Job_Full_Name",
|
||||
pbField: "Job_Full_Name",
|
||||
type: "string"
|
||||
},
|
||||
Job_Name: {
|
||||
excelHeader: "Job_Name",
|
||||
pbField: "Job_Name",
|
||||
type: "string"
|
||||
},
|
||||
Job_Address: {
|
||||
excelHeader: "Job Address",
|
||||
pbField: "Job_Address",
|
||||
type: "string"
|
||||
},
|
||||
Job_Type: {
|
||||
excelHeader: "Job Type",
|
||||
pbField: "Job_Type",
|
||||
type: "string"
|
||||
},
|
||||
Job_Status: {
|
||||
excelHeader: "Job Status",
|
||||
pbField: "Job_Status",
|
||||
type: "string"
|
||||
},
|
||||
Job_Division: {
|
||||
excelHeader: "Job Division",
|
||||
pbField: "Job_Division",
|
||||
type: "string"
|
||||
},
|
||||
Estimator: {
|
||||
excelHeader: "Estimator",
|
||||
pbField: "Estimator",
|
||||
type: "string"
|
||||
},
|
||||
Office_Rep: {
|
||||
excelHeader: "Office Rep",
|
||||
pbField: "Office_Rep",
|
||||
type: "string"
|
||||
},
|
||||
Project_Manager: {
|
||||
excelHeader: "Manager",
|
||||
pbField: "Project_Manager",
|
||||
type: "string"
|
||||
},
|
||||
Company_Client: {
|
||||
excelHeader: "Company/Client",
|
||||
pbField: "Company_Client",
|
||||
type: "string"
|
||||
},
|
||||
Contact_Person: {
|
||||
excelHeader: "Contact Person",
|
||||
pbField: "Contact_Person",
|
||||
type: "string"
|
||||
},
|
||||
Phone_Number: {
|
||||
excelHeader: "Phone Number",
|
||||
pbField: "Phone_Number",
|
||||
type: "string"
|
||||
},
|
||||
Email: {
|
||||
excelHeader: "Email",
|
||||
pbField: "Email",
|
||||
type: "string"
|
||||
},
|
||||
Due_Date: {
|
||||
excelHeader: "Due Date",
|
||||
pbField: "Due_Date",
|
||||
type: "date"
|
||||
},
|
||||
Due_Date_Source: {
|
||||
excelHeader: "Due Date Source",
|
||||
pbField: "Due_Date_Source",
|
||||
type: "string"
|
||||
},
|
||||
Due_Date_Counter: {
|
||||
excelHeader: "Due Date Counter",
|
||||
pbField: "Due_Date_Counter",
|
||||
type: "string"
|
||||
},
|
||||
Due_Time: {
|
||||
excelHeader: "Due Time",
|
||||
pbField: "Due_Time",
|
||||
type: "string"
|
||||
},
|
||||
Docs_Uploaded: {
|
||||
excelHeader: "Docs Uploaded",
|
||||
pbField: "Docs_Uploaded",
|
||||
type: "bool"
|
||||
},
|
||||
QB_Created: {
|
||||
excelHeader: "QB Created",
|
||||
pbField: "QB_Created",
|
||||
type: "bool"
|
||||
},
|
||||
Added_To_Calendar: {
|
||||
excelHeader: "Added to Calendar",
|
||||
pbField: "Added_To_Calendar",
|
||||
type: "bool"
|
||||
},
|
||||
PSwift_Uploaded: {
|
||||
excelHeader: "PSwift Uploaded",
|
||||
pbField: "PSwift_Uploaded",
|
||||
type: "bool"
|
||||
},
|
||||
Voxer_Created: {
|
||||
excelHeader: "Voxer Created",
|
||||
pbField: "Voxer_Created",
|
||||
type: "bool"
|
||||
},
|
||||
Estimate_Reviewed: {
|
||||
excelHeader: "Estimate Reviewed",
|
||||
pbField: "Estimate_Reviewed",
|
||||
type: "bool"
|
||||
},
|
||||
Estimate_Draft: {
|
||||
excelHeader: "Estimate Draft",
|
||||
pbField: "Estimate_Draft",
|
||||
type: "bool"
|
||||
},
|
||||
Estimate_Approved: {
|
||||
excelHeader: "Estimate Approved",
|
||||
pbField: "Estimate_Approved",
|
||||
type: "bool"
|
||||
},
|
||||
Start_Date: {
|
||||
excelHeader: "Start Date",
|
||||
pbField: "Start_Date",
|
||||
type: "date"
|
||||
},
|
||||
Submission_Date: {
|
||||
excelHeader: "Submission Date",
|
||||
pbField: "Submission_Date",
|
||||
type: "date"
|
||||
},
|
||||
Active: {
|
||||
excelHeader: "Active",
|
||||
pbField: "Active",
|
||||
type: "bool"
|
||||
},
|
||||
Tax_Exempt: {
|
||||
excelHeader: "Tax_Exempt",
|
||||
pbField: "Tax_Exempt",
|
||||
type: "bool"
|
||||
},
|
||||
Tax_Exempt_Docs_Uploaded: {
|
||||
excelHeader: "Tax_Exempt_Docs_Uploaded",
|
||||
pbField: "Tax_Exempt_Docs_Uploaded",
|
||||
type: "bool"
|
||||
},
|
||||
Flag: {
|
||||
excelHeader: "Flag",
|
||||
pbField: "Flag",
|
||||
type: "bool"
|
||||
},
|
||||
Notes: {
|
||||
excelHeader: "Notes",
|
||||
pbField: "Notes",
|
||||
type: "string"
|
||||
},
|
||||
Job_QB_Link: {
|
||||
excelHeader: "Job QB Link",
|
||||
pbField: "Job_QB_Link",
|
||||
type: "string"
|
||||
},
|
||||
Voxer_Link: {
|
||||
excelHeader: "Voxer Link",
|
||||
pbField: "Voxer_Link",
|
||||
type: "string"
|
||||
},
|
||||
Job_Codes: {
|
||||
excelHeader: "Job_Codes",
|
||||
pbField: "Job_Codes",
|
||||
type: "string"
|
||||
},
|
||||
Job_Folder_Link: {
|
||||
excelHeader: "Job Folder Link",
|
||||
pbField: "Job_Folder_Link",
|
||||
type: "string"
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import dotenv from "dotenv";
|
||||
import PocketBase from "pocketbase";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const pb = new PocketBase(process.env.PB_URL);
|
||||
|
||||
const email = process.env.PB_EMAIL;
|
||||
const password = process.env.PB_PASSWORD;
|
||||
|
||||
if (!email || !password) {
|
||||
throw new Error("Missing PB_EMAIL or PB_PASSWORD in .env");
|
||||
}
|
||||
try {
|
||||
await pb.collection("users").authWithPassword(email, password);
|
||||
} catch (err: any) {
|
||||
console.error("Error response:", err.response);
|
||||
}
|
||||
|
||||
const authData = await pb.collection("users").authWithPassword(email, password);
|
||||
|
||||
console.log("Valid:", pb.authStore.isValid);
|
||||
console.log("Token:", pb.authStore.token);
|
||||
console.log("User ID:", pb.authStore.record?.id);
|
||||
|
||||
pb.authStore.clear();
|
||||
@@ -0,0 +1,77 @@
|
||||
// sharepointDownload.js
|
||||
import axios from "axios";
|
||||
import qs from "qs";
|
||||
import dotenv from "dotenv";
|
||||
import fs from "fs";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
// -----------------------------
|
||||
// CONFIG
|
||||
// -----------------------------
|
||||
const CONFIG = JSON.parse(fs.readFileSync("config.json", "utf8"));
|
||||
const tenantId = CONFIG.tenantId;
|
||||
const clientId = CONFIG.clientId;
|
||||
|
||||
// Secret stays in .env
|
||||
const clientSecret = process.env.CLIENT_SECRET;
|
||||
|
||||
// -----------------------------
|
||||
// GRAPH TOKEN
|
||||
// -----------------------------
|
||||
async function getGraphToken() {
|
||||
const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
|
||||
const body = qs.stringify({
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
scope: "https://graph.microsoft.com/.default",
|
||||
grant_type: "client_credentials",
|
||||
});
|
||||
const res = await axios.post(tokenUrl, body, {
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
});
|
||||
console.log("✅ Graph token acquired");
|
||||
return res.data.access_token;
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// DOWNLOAD BY ITEM ID
|
||||
// -----------------------------
|
||||
// Example link: https://czflex.sharepoint.com/sites/Team/Shared%20Documents/General/JOBS%201000-9999.xlsx?d=wc20d9b5023f84403b4d345ca88382eb4
|
||||
// The ?d=... part is the itemId
|
||||
async function downloadFileById(graphToken, sitePath, itemId) {
|
||||
const url = `https://graph.microsoft.com/v1.0/sites/${sitePath}/drive/items/${itemId}/content`;
|
||||
const res = await axios.get(url, {
|
||||
headers: { Authorization: `Bearer ${graphToken}` },
|
||||
responseType: "arraybuffer",
|
||||
});
|
||||
console.log("✅ File downloaded, bytes:", res.data.byteLength);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// MAIN
|
||||
// -----------------------------
|
||||
(async () => {
|
||||
try {
|
||||
const graphToken = await getGraphToken();
|
||||
|
||||
// sitePath matches your site
|
||||
const sitePath = "czflex.sharepoint.com:/sites/Team";
|
||||
|
||||
// itemId from your link (?d=...)
|
||||
const itemId = "wc20d9b5023f84403b4d345ca88382eb4";
|
||||
|
||||
const buffer = await downloadFileById(graphToken, sitePath, itemId);
|
||||
|
||||
// At this point you can feed buffer into XLSX.read(buffer, { type: "buffer", cellDates: true })
|
||||
// Example:
|
||||
// const workbook = XLSX.read(buffer, { type: "buffer", cellDates: true });
|
||||
// const sheetNames = workbook.SheetNames;
|
||||
// console.log("Sheets:", sheetNames);
|
||||
|
||||
} catch (err) {
|
||||
console.error("❌ Error:", err.response?.data || err.message || err);
|
||||
}
|
||||
})();
|
||||
"id": "czflex.sharepoint.com,aaaa83f5-b1bd-4133-b60c-507030425863,0a423385-151c-44d7-b692-d85ae4bdfc0b"
|
||||
@@ -0,0 +1,260 @@
|
||||
// excelsync.js
|
||||
import { FIELD_CONFIG } from "./fieldConfig.js";
|
||||
import fs from "fs";
|
||||
import axios from "axios";
|
||||
import qs from "qs";
|
||||
import dotenv from "dotenv";
|
||||
import PocketBase from "pocketbase";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
// -----------------------------
|
||||
// EXCEL PARSER
|
||||
// -----------------------------
|
||||
const HEADER_ROW_INDEX = 2; // 0-index → Row 3 actual
|
||||
const DATA_START_ROW_INDEX = 3; // 0-index → Row 4 actual
|
||||
|
||||
// -----------------------------
|
||||
// CONFIG
|
||||
// -----------------------------
|
||||
const CONFIG = JSON.parse(fs.readFileSync("config.json", "utf8"));
|
||||
const tenantId = CONFIG.tenantId;
|
||||
const clientId = CONFIG.clientId;
|
||||
const pbUrl = CONFIG.pbUrl;
|
||||
const pbCollection = CONFIG.pbCollection || "Job_Info_TestEnv";
|
||||
const pbEmail = CONFIG.pbEmail;
|
||||
const clientSecret = process.env.CLIENT_SECRET;
|
||||
const pbPassword = process.env.PB_PASSWORD;
|
||||
|
||||
// -----------------------------
|
||||
// LOGGING
|
||||
// -----------------------------
|
||||
function log(message) {
|
||||
console.log(message);
|
||||
fs.appendFileSync("excelsync.log", message + "\n");
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// GRAPH TOKEN
|
||||
// -----------------------------
|
||||
async function getGraphToken() {
|
||||
log("🔵 Requesting Graph token...");
|
||||
const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
|
||||
const body = qs.stringify({
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
scope: "https://graph.microsoft.com/.default",
|
||||
grant_type: "client_credentials",
|
||||
});
|
||||
const res = await axios.post(tokenUrl, body, {
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
});
|
||||
log("✅ Graph token acquired");
|
||||
return res.data.access_token;
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// POCKETBASE LOGIN
|
||||
// -----------------------------
|
||||
async function getPocketBaseClient() {
|
||||
log("🔵 Logging into PocketBase...");
|
||||
const pb = new PocketBase(pbUrl);
|
||||
// ⚠️ CHANGE "users" if your auth collection is different
|
||||
await pb.collection("users").authWithPassword(pbEmail, pbPassword);
|
||||
log("✅ PocketBase token acquired");
|
||||
return pb;
|
||||
}
|
||||
// -----------------------------
|
||||
// CLEAN + COERCE
|
||||
// -----------------------------
|
||||
function cleanString(value) {
|
||||
if (value == null) return "";
|
||||
return String(value).replace(/\u00A0/g, " ").trim();
|
||||
}
|
||||
|
||||
function coerceValue(fieldConfig, rawValue) {
|
||||
switch (fieldConfig.type) {
|
||||
case "string": return cleanString(rawValue);
|
||||
case "bool": return ["true","1","yes","y"].includes(cleanString(rawValue).toLowerCase());
|
||||
case "date":
|
||||
if (typeof rawValue === "number") {
|
||||
const origin = new Date(Date.UTC(1899, 11, 30));
|
||||
return new Date(origin.getTime() + rawValue * 86400000).toISOString();
|
||||
}
|
||||
const d = new Date(cleanString(rawValue));
|
||||
return isNaN(d.getTime()) ? null : d.toISOString();
|
||||
case "number": return Number(cleanString(rawValue));
|
||||
case "json": try { return JSON.parse(cleanString(rawValue)); } catch { return null; }
|
||||
case "relation": return cleanString(rawValue);
|
||||
default: return cleanString(rawValue);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// PAYLOAD BUILDER
|
||||
// -----------------------------
|
||||
export function buildPayload(headers, row) {
|
||||
const payload = {};
|
||||
headers.forEach((excelHeader, i) => {
|
||||
const fieldConfig = Object.values(FIELD_CONFIG).find(f => f.excelHeader === excelHeader);
|
||||
if (!fieldConfig) return;
|
||||
payload[fieldConfig.pbField] = coerceValue(fieldConfig, row[i]);
|
||||
});
|
||||
return payload;
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// DIFFERENCE LOGGER + UPDATE CHECK
|
||||
// -----------------------------
|
||||
function logDifferences(pbRecord, headers, row) {
|
||||
const diffs = [];
|
||||
headers.forEach((excelHeader, i) => {
|
||||
const fieldConfig = Object.values(FIELD_CONFIG).find(f => f.excelHeader === excelHeader);
|
||||
if (!fieldConfig) return;
|
||||
const newValue = coerceValue(fieldConfig, row[i]);
|
||||
const currentValue = coerceValue(fieldConfig, pbRecord[fieldConfig.pbField]);
|
||||
if (newValue !== currentValue) {
|
||||
diffs.push({ field: fieldConfig.pbField, excelHeader, current: currentValue, incoming: newValue });
|
||||
}
|
||||
});
|
||||
if (diffs.length > 0) {
|
||||
log(`Differences detected for record: ${pbRecord.id}`);
|
||||
diffs.forEach(d => {
|
||||
log(`Field: ${d.field} (Excel: ${d.excelHeader})\n Current: ${JSON.stringify(d.current)}\n Incoming: ${JSON.stringify(d.incoming)}\n`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function needsUpdate(pbRecord, headers, row) {
|
||||
let hasDiff = false;
|
||||
headers.forEach((excelHeader, i) => {
|
||||
const fieldConfig = Object.values(FIELD_CONFIG).find(f => f.excelHeader === excelHeader);
|
||||
if (!fieldConfig) return;
|
||||
const newValue = coerceValue(fieldConfig, row[i]);
|
||||
const currentValue = coerceValue(fieldConfig, pbRecord[fieldConfig.pbField]);
|
||||
if (newValue !== currentValue) hasDiff = true;
|
||||
});
|
||||
if (hasDiff) logDifferences(pbRecord, headers, row);
|
||||
return hasDiff;
|
||||
}
|
||||
// -----------------------------
|
||||
// GRAPH HELPERS
|
||||
// -----------------------------
|
||||
async function getUsedRangeInfo(graphToken, driveId, itemId, worksheetName) {
|
||||
log(`🔵 Requesting usedRange for ${worksheetName}`);
|
||||
const url = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/workbook/worksheets('${worksheetName}')/usedRange`;
|
||||
const res = await axios.get(url, { headers: { Authorization: `Bearer ${graphToken}` } });
|
||||
log(`✅ Used range address: ${res.data.address}`);
|
||||
return res.data.address;
|
||||
}
|
||||
|
||||
function extractLastColumn(address) {
|
||||
const match = address.match(/:([A-Z]+)\d+$/);
|
||||
if (!match) {
|
||||
log("⚠️ Falling back to column Z, check sheet width");
|
||||
return "Z";
|
||||
}
|
||||
return match[1];
|
||||
}
|
||||
|
||||
async function fetchExcelRange(graphToken, driveId, itemId, worksheetName, range) {
|
||||
log(`🔵 Fetching range ${range} from worksheet ${worksheetName}`);
|
||||
const url = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/workbook/worksheets('${worksheetName}')/range(address='${range}')`;
|
||||
const res = await axios.get(url, { headers: { Authorization: `Bearer ${graphToken}` } });
|
||||
log(`✅ Range ${range} returned ${res.data.values?.length || 0} rows`);
|
||||
return res.data.values || [];
|
||||
}
|
||||
|
||||
async function fetchAllRows(graphToken, driveId, itemId, worksheetName, blockSize = 500) {
|
||||
const address = await getUsedRangeInfo(graphToken, driveId, itemId, worksheetName);
|
||||
const lastCol = extractLastColumn(address);
|
||||
|
||||
let allRows = [];
|
||||
let startRow = 1;
|
||||
|
||||
while (true) {
|
||||
const endRow = startRow + blockSize - 1;
|
||||
const range = `A${startRow}:${lastCol}${endRow}`;
|
||||
const rows = await fetchExcelRange(graphToken, driveId, itemId, worksheetName, range);
|
||||
|
||||
if (!rows.length) break;
|
||||
allRows = allRows.concat(rows);
|
||||
|
||||
if (rows.length < blockSize) break;
|
||||
startRow = endRow + 1;
|
||||
}
|
||||
|
||||
return allRows;
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// MAIN
|
||||
// -----------------------------
|
||||
(async () => {
|
||||
try {
|
||||
log("🔵 Checking token acquisition and excel readiness");
|
||||
|
||||
const graphToken = await getGraphToken();
|
||||
const pb = await getPocketBaseClient();
|
||||
|
||||
const driveId = "b!9YOqqr2xM0G2DFBwMEJYY4UzQgocFddEtpLYWuS9_AtkCKl2q8yjQJteQ7Ti4QSx";
|
||||
const itemId = "01SPNXLDSQTMG4F6BDANCLJU2FZKEDQLVU";
|
||||
const worksheetName = "Job Sheet";
|
||||
|
||||
const rows = await fetchAllRows(graphToken, driveId, itemId, worksheetName);
|
||||
log(`✅ Total rows fetched: ${rows.length}`);
|
||||
|
||||
if (!rows.length) {
|
||||
log("⚠️ No rows returned from Graph. Check driveId, itemId, and worksheetName.");
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = rows[HEADER_ROW_INDEX];
|
||||
const dataRows = rows.slice(DATA_START_ROW_INDEX);
|
||||
|
||||
if (!headers || !Array.isArray(headers)) {
|
||||
log("❌ Header row not found at expected index. Verify HEADER_ROW_INDEX alignment.");
|
||||
return;
|
||||
}
|
||||
|
||||
log(`✅ Headers detected: ${JSON.stringify(headers)}`);
|
||||
log(`✅ Data rows to process: ${dataRows.length}`);
|
||||
|
||||
for (const row of dataRows) {
|
||||
if (!row || row.length === 0) continue; // skip empty rows
|
||||
|
||||
const payload = buildPayload(headers, row);
|
||||
|
||||
// Ensure primary key exists before querying PocketBase
|
||||
const jobNumber = payload.Job_Number;
|
||||
if (!jobNumber || String(jobNumber).trim() === "") {
|
||||
log("⚠️ Skipping row with missing Job_Number.");
|
||||
continue;
|
||||
}
|
||||
|
||||
let existing = null;
|
||||
try {
|
||||
existing = await pb.collection(pbCollection).getFirstListItem(`Job_Number="${jobNumber}"`);
|
||||
} catch {
|
||||
existing = null;
|
||||
}
|
||||
|
||||
if (!existing) {
|
||||
log(`➕ Creating new record: ${jobNumber}`);
|
||||
await pb.collection(pbCollection).create(payload);
|
||||
} else {
|
||||
if (needsUpdate(existing, headers, row)) {
|
||||
log(`✏️ Updating record: ${existing.id} (${jobNumber})`);
|
||||
await pb.collection(pbCollection).update(existing.id, payload);
|
||||
} else {
|
||||
log(`✅ No changes for record: ${existing.id} (${jobNumber})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log("✅ Sync complete, PocketBase updated!");
|
||||
} catch (err) {
|
||||
log("❌ Error, not ready for updating. " + (err?.message || err));
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,136 @@
|
||||
// excelsync.js
|
||||
import fs from "fs";
|
||||
import axios from "axios";
|
||||
import qs from "qs";
|
||||
// may need for reading excel
|
||||
//import * as XLSX from "xlsx";
|
||||
import dotenv from "dotenv";
|
||||
// may need to serve html to port
|
||||
//import { serve } from "bun";
|
||||
import PocketBase from "pocketbase"
|
||||
|
||||
dotenv.config(); // connects to .env for sensitive info
|
||||
|
||||
// -----------------------------
|
||||
// EXCEL PARSER
|
||||
// -----------------------------
|
||||
const HEADER_ROW_INDEX = 2; // O index --> Row 3 Actual
|
||||
const DATA_START_ROW_INDEX = 3; // 0 index --> Row 4 Actual
|
||||
const RANGE_HEADER_ROW = 3 // Excel index headers
|
||||
const RANGE_DATA_ROW = 4 // Excel index data
|
||||
|
||||
// -----------------------------
|
||||
// CONFIG
|
||||
// -----------------------------
|
||||
const CONFIG = JSON.parse(fs.readFileSync("config.json", "utf8"));
|
||||
const tenantId = CONFIG.tenantId;
|
||||
const clientId = CONFIG.clientId;
|
||||
//const sharepointFileUrl = CONFIG.sharepointFileUrl; // production Excel file URL
|
||||
const pbUrl = CONFIG.pbUrl;
|
||||
const pbCollection = CONFIG.pbCollection || "Job_Info_TestEnv";
|
||||
const pbEmail = CONFIG.pbEmail;
|
||||
const clientSecret = process.env.CLIENT_SECRET;
|
||||
const pbPassword = process.env.PB_PASSWORD;
|
||||
|
||||
// -----------------------------
|
||||
// LOGGING
|
||||
// -----------------------------
|
||||
function log(message) {
|
||||
console.log(message);
|
||||
fs.appendFileSync("excelsync.log", message + "\n");
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// GRAPH TOKEN
|
||||
// -----------------------------
|
||||
async function getGraphToken() {
|
||||
const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
|
||||
const body = qs.stringify({
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
scope: "https://graph.microsoft.com/.default",
|
||||
grant_type: "client_credentials",
|
||||
});
|
||||
const res = await axios.post(tokenUrl, body, {
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
});
|
||||
log("✅ Graph token acquired");
|
||||
return res.data.access_token;
|
||||
}
|
||||
// -----------------------------
|
||||
// POCKETBASE LOGIN AND TOKEN
|
||||
// -----------------------------
|
||||
async function getPocketBaseClient() {
|
||||
const pb = new PocketBase(pbUrl);
|
||||
await pb.collection("users").authWithPassword(pbEmail, pbPassword);
|
||||
log("✅ PocketBase token acquired");
|
||||
return pb;
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// HEADER MAP
|
||||
// -----------------------------
|
||||
const HEADER_MAP = {
|
||||
"Job_Number": "Job_Number",
|
||||
"Job_Full_Name": "Job_Full_Name",
|
||||
"Job_Name": "Job_Name",
|
||||
"Job Address": "Job_Address",
|
||||
"Job Type": "Job_Type",
|
||||
"Flag": "Flag",
|
||||
"Tax_Exempt": "Tax_Exempt",
|
||||
"Job Status": "Job_Status",
|
||||
"Job Division": "Job_Division",
|
||||
"Estimator": "Estimator",
|
||||
"Office Rep": "Office_Rep",
|
||||
"Manager": "Project_Manager",
|
||||
"Company/Client": "Company_Client",
|
||||
"Contact Person": "Contact_Person",
|
||||
"Phone Number": "Phone_Number",
|
||||
"Email": "Email",
|
||||
"Due Date": "Due_Date",
|
||||
"Due Date Source": "Due_Date_Source",
|
||||
"Due Date Counter": "Due_Date_Counter",
|
||||
"Due Time": "Due_Time",
|
||||
"Tax_Exempt_Docs_Uploaded": "Tax_Exempt_Docs_Uploaded",
|
||||
"Docs Uploaded": "Docs_Uploaded",
|
||||
"QB Created": "QB_Created",
|
||||
"Added to Calendar": "Added_To_Calendar",
|
||||
"PSwift Uploaded": "PSwift_Uploaded",
|
||||
"Voxer Created": "Voxer_Created",
|
||||
"Estimate Reviewed": "Estimate_Reviewed",
|
||||
"Estimate Draft": "Estimate_Draft",
|
||||
"Estimate Approved": "Estimate_Approved",
|
||||
"Start Date": "Start_Date",
|
||||
"Schedule_Confidence": "Schedule_Confidence",
|
||||
"Notes": "Notes",
|
||||
"Job QB Link": "Job_QB_Link",
|
||||
"Voxer Link": "Voxer_Link",
|
||||
"Job_Codes": "Job_Codes",
|
||||
"Submission Date": "Submission_Date",
|
||||
"Active": "Active",
|
||||
"Job Folder Link": "Job_Folder_Link",
|
||||
"pb_id": "id"
|
||||
};
|
||||
|
||||
// -----------------------------
|
||||
// MAIN
|
||||
// -----------------------------
|
||||
(async () => {
|
||||
try {
|
||||
log("🔵 Checking token acquisition and excel readiness");
|
||||
|
||||
const graphToken = await getGraphToken();
|
||||
const pb = await getPocketBaseClient();
|
||||
// These can be update if the Excel File changes
|
||||
//const driveId = "b!9YOqqr2xM0G2DFBwMEJYY4UzQgocFddEtpLYWuS9_AtkCKl2q8yjQJteQ7Ti4QSx";
|
||||
//const itemId = "01SPNXLDSQTMG4F6BDANCLJU2FZKEDQLVU"
|
||||
|
||||
await exceltopb(graphToken, driveId, itemId, "Job Sheet", pb);
|
||||
await pbtoupdate(pbCollection);
|
||||
|
||||
log("✅ Ready for updating!");
|
||||
} catch (err) {
|
||||
log("❌ Error, not ready for updating." + (err.message || err));
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,105 @@
|
||||
import axios from "axios";
|
||||
import qs from "qs";
|
||||
import fs from "fs";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
// -----------------------------
|
||||
// CONFIG
|
||||
// -----------------------------
|
||||
dotenv.config();
|
||||
// Load non‑sensitive values from config.json
|
||||
const CONFIG = JSON.parse(fs.readFileSync("config.json", "utf8"));
|
||||
const tenantId = CONFIG.tenantId;
|
||||
const clientId = CONFIG.clientId;
|
||||
const testSharePointFileUrl = CONFIG.testSharePointFileUrl;
|
||||
|
||||
// Load secret only from .env
|
||||
const clientSecret = process.env.CLIENT_SECRET;
|
||||
|
||||
// -----------------------------
|
||||
// Validation
|
||||
// -----------------------------
|
||||
const missing = [];
|
||||
if (!tenantId) missing.push("tenantId");
|
||||
if (!clientId) missing.push("clientId");
|
||||
if (!clientSecret) missing.push("clientSecret");
|
||||
if (!testSharePointFileUrl) missing.push("testSharePointFileUrl");
|
||||
|
||||
if (missing.length > 0) {
|
||||
console.error("❌ Missing values:", missing.join(", "));
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log("✅ All required values present");
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// 1. Get Graph Token (Client Credentials)
|
||||
// -----------------------------
|
||||
async function getGraphToken() {
|
||||
const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
|
||||
|
||||
const body = qs.stringify({
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
scope: "https://graph.microsoft.com/.default",
|
||||
grant_type: "client_credentials",
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await axios.post(tokenUrl, body, {
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
});
|
||||
|
||||
console.log("✅ Token acquired");
|
||||
// can use to test to see the full token as needed, just remove this comment// console.log("🔑 Access Token:", res.data.access_token);
|
||||
return res.data.access_token;
|
||||
|
||||
} catch (err) {
|
||||
console.error("❌ Failed to acquire token");
|
||||
console.error(err.response?.data || err.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// 2. Convert SharePoint File URL → Drive Item ID
|
||||
// -----------------------------
|
||||
function normalizeSharePointUrl(fileUrl) {
|
||||
return fileUrl
|
||||
.replace("guestaccess.aspx", "")
|
||||
.replace(/\?.*$/, ""); // remove ?query
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// 3. Test GET file metadata
|
||||
// -----------------------------
|
||||
async function testFileAccess(token) {
|
||||
const cleanUrl = normalizeSharePointUrl(testSharePointFileUrl);
|
||||
|
||||
console.log("🔎 Normalized URL:", cleanUrl);
|
||||
|
||||
const graphUrl =
|
||||
`https://graph.microsoft.com/v1.0/shares/u!${Buffer.from(cleanUrl).toString("base64")}/driveItem`;
|
||||
|
||||
try {
|
||||
const res = await axios.get(graphUrl, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
console.log("✅ SharePoint file access test succeeded!");
|
||||
console.log("File name:", res.data.name);
|
||||
console.log("Drive Item ID:", res.data.id);
|
||||
} catch (err) {
|
||||
console.error("❌ SharePoint file access failed");
|
||||
console.error(err.response?.data || err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// MAIN
|
||||
// -----------------------------
|
||||
(async () => {
|
||||
console.log("🔵 Testing SharePoint Access…");
|
||||
const token = await getGraphToken();
|
||||
await testFileAccess(token);
|
||||
})();
|
||||
@@ -0,0 +1,204 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
const XLSX = require('xlsx');
|
||||
const axios = require('axios');
|
||||
|
||||
// ================================================
|
||||
// CONFIG — ONLY EDIT THESE VALUES
|
||||
// ================================================
|
||||
const EXCEL_FILE_URL = "https://czflex.sharepoint.com/:x:/r/sites/Team/Shared%20Documents/General/JOBS%201000-9999.xlsx?d=wc20d9b5023f84403b4d345ca88382eb4&csf=1&web=1&e=aNwcPv";
|
||||
const EXCEL_SHEET_NAME = "Job Sheet";
|
||||
|
||||
const AZURE_TENANT_ID = "3fd97ea7-b124-41f1-855f-52d8ac3b16c7";
|
||||
const AZURE_CLIENT_ID = "3c846e71-9609-40e1-b458-0eb805e21b9f";
|
||||
const AZURE_CLIENT_SECRET = "lG~8Q~kKp5nyc-SK8THNE4ZLTdp81zv_gVm2jdm~"; // Store in Settings → Secret keys (see below)
|
||||
|
||||
// Run every 5 minutes
|
||||
routerAdd("GET", "/_cron/sync-excel", (c) => {
|
||||
const correlationId = $security.randomString(8);
|
||||
console.log(`[${new Date().toISOString()}] Sync started (${correlationId})`);
|
||||
|
||||
// Use secret storage (secure!)
|
||||
const clientSecret = $app.settings().meta.appSecret || AZURE_CLIENT_SECRET;
|
||||
|
||||
// ================================================
|
||||
// 1. Get OAuth 2.0 token (client credentials flow)
|
||||
// ================================================
|
||||
$os.exec("curl", "-X", "POST",
|
||||
`https://login.microsoftonline.com/${AZURE_TENANT_ID}/oauth2/v2.0/token`,
|
||||
"-d", `client_id=${AZURE_CLIENT_ID}`,
|
||||
"-d", `client_secret=${clientSecret}`,
|
||||
"-d", "grant_type=client_credentials",
|
||||
"-d", "scope=https://graph.microsoft.com/.default"
|
||||
).then(async (tokenRes) => {
|
||||
const tokenData = JSON.parse(tokenRes.stdout);
|
||||
const accessToken = tokenData.access_token;
|
||||
|
||||
// ================================================
|
||||
// 2. Download Excel file
|
||||
// ================================================
|
||||
const fileRes = await $http.send({
|
||||
url: EXCEL_FILE_URL + "?:@microsoft.graph.downloadUrl",
|
||||
method: "GET",
|
||||
headers: { Authorization: `Bearer ${accessToken}` }
|
||||
});
|
||||
|
||||
if (fileRes.statusCode !== 200) {
|
||||
console.error("Failed to download Excel:", fileRes.raw);
|
||||
return c.json(500, { error: "Download failed" });
|
||||
}
|
||||
|
||||
const workbook = XLSX.read(fileRes.raw, { type: "buffer", cellDates: true });
|
||||
const sheet = workbook.Sheets[EXCEL_SHEET_NAME];
|
||||
if (!sheet) return console.error("Sheet not found");
|
||||
|
||||
const range = XLSX.utils.decode_range(sheet["!ref"]);
|
||||
const merges = sheet["!merges"] || [];
|
||||
|
||||
const getValue = (r, c) => {
|
||||
const addr = XLSX.utils.encode_cell({ r, c });
|
||||
const cell = sheet[addr];
|
||||
if (cell) return cell.v ?? cell.w ?? null;
|
||||
for (const m of merges) {
|
||||
if (m.s.r <= r && r <= m.e.r && m.s.c <= c && c <= m.e.c) {
|
||||
const master = sheet[XLSX.utils.encode_cell({ r: m.s.r, c: m.s.c })];
|
||||
return master ? (master.v ?? master.w ?? null) : null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// ================================================
|
||||
// 3. Parse headers & data (same as browser tool)
|
||||
// ================================================
|
||||
const HEADER_ROW = 2;
|
||||
const DATA_START = 3;
|
||||
|
||||
const rawHeaders = [];
|
||||
for (let c = range.s.c; c <= range.e.c; c++) {
|
||||
let h = getValue(HEADER_ROW, c);
|
||||
rawHeaders.push(h ? String(h).trim() : '');
|
||||
}
|
||||
|
||||
const HEADER_MAP = {
|
||||
"Job_Number": "Job_Number",
|
||||
"Job_Full_Name": "Job_Full_Name",
|
||||
"Job_Name": "Job_Name",
|
||||
"Job Address": "Job_Address",
|
||||
"Job Type": "Job_Type",
|
||||
"Flag": "Flag",
|
||||
"Job Status": "Job_Status",
|
||||
"Job Division": "Division",
|
||||
"Estimator": "Estimator",
|
||||
"Office Rep": "Office_Rep",
|
||||
"Manager": "Project_Manager",
|
||||
"Company/Client": "Company_Client",
|
||||
"Contact Person": "Contact_Person",
|
||||
"Phone Number": "Phone",
|
||||
"Email": "Email",
|
||||
"Due Date": "Due_Date",
|
||||
"Due Date Source": "Due_Date_Source",
|
||||
"Due Date Counter": "Due_Date_Counter",
|
||||
"Due Time": "Due_Time",
|
||||
"Docs Uploaded": "Docs_Uploaded",
|
||||
"QB Created": "QB_Created",
|
||||
"Added to Calendar": "Calendar_Added",
|
||||
"PSwift Uploaded": "PSwift_Uploaded",
|
||||
"Voxer Created": "Voxer_Created",
|
||||
"Estimate Reviewed": "Estimate_Reviewed",
|
||||
"Estimate Draft": "Estimate_Draft",
|
||||
"Estimate Approved": "Estimate_Approved",
|
||||
"Start Date": "Start_Date",
|
||||
"Schedule\nConfidence": "Schedule_Confidence",
|
||||
"Notes": "Notes",
|
||||
"Job QB Link": "Job_QB_Link",
|
||||
"Voxer Link": "Voxer_Link",
|
||||
"Job_Codes": "Job_Codes",
|
||||
"Submission Date": "Submission_Date",
|
||||
"Active": "Active",
|
||||
"Job Folder Link": "Job_Folder_Link",
|
||||
"Tax Exempt": "Tax_Exempt",
|
||||
"pb_id": "id"
|
||||
};
|
||||
|
||||
const rows = [];
|
||||
for (let r = DATA_START; r <= range.e.r; r++) {
|
||||
const row = { id: null, Notes: null, Job_Number: null };
|
||||
let hasData = false;
|
||||
|
||||
for (let c = range.s.c; c <= range.e.c; c++) {
|
||||
const excelHeader = rawHeaders[c - range.s.c];
|
||||
const pbField = HEADER_MAP[excelHeader];
|
||||
if (!pbField) continue;
|
||||
|
||||
let val = getValue(r, c);
|
||||
if (val !== null && val !== undefined) {
|
||||
const s = String(val).trim();
|
||||
if (s === '' || /^n\/?a$/i.test(s) || s === '#N/A') val = null;
|
||||
}
|
||||
|
||||
if (pbField === "id" && excelHeader === "pb_id") row.id = val;
|
||||
else if (pbField === "Notes") row.Notes = val;
|
||||
else if (pbField === "Job_Number") row.Job_Number = val;
|
||||
|
||||
row[pbField] = val;
|
||||
if (val !== null && val !== '') hasData = true;
|
||||
}
|
||||
|
||||
if (hasData) rows.push(row);
|
||||
}
|
||||
|
||||
// ================================================
|
||||
// 4. Full Sync Excel → PocketBase
|
||||
// ================================================
|
||||
let created = 0, updated = 0;
|
||||
for (const row of rows) {
|
||||
try {
|
||||
if (row.id) {
|
||||
// Update existing
|
||||
const cleanRow = { ...row };
|
||||
delete cleanRow.id;
|
||||
$app.dao().runInTransaction((txDao) => {
|
||||
txDao.saveRecord(txDao.findRecordById("Job_Info_TestEnv", row.id, cleanRow));
|
||||
});
|
||||
updated++;
|
||||
} else {
|
||||
// Create new
|
||||
const record = $app.dao().newRecord("Job_Info_TestEnv");
|
||||
Object.assign(record, row);
|
||||
$app.dao().saveRecord(record);
|
||||
created++;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Row error (Job_Number ${row.Job_Number}):`, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================
|
||||
// 5. Bidirectional Notes Sync (PB → Excel if PB is newer)
|
||||
// ================================================
|
||||
const pbRecords = arrayOf(new Record({
|
||||
collection: $app.dao().findCollectionByNameOrId("Job_Info_TestEnv")
|
||||
})).fill($app.dao().findRecordsByExpr("Job_Info_TestEnv", DB().and()));
|
||||
|
||||
for (const pbRec of pbRecords) {
|
||||
const excelMatch = rows.find(r => r.Job_Number === pbRec.getString("Job_Number"));
|
||||
if (!excelMatch) continue;
|
||||
|
||||
const pbNotes = pbRec.getString("Notes") || "";
|
||||
const excelNotes = excelMatch.Notes || "";
|
||||
|
||||
if (pbNotes !== excelNotes && pbRec.getDate("updated") > new Date()) {
|
||||
// Push PB → Excel
|
||||
console.log(`Pushing Notes to Excel for ${pbRec.getString("Job_Number")}`);
|
||||
// Future: Use Graph workbook API to update cell
|
||||
// For now: just log (add Graph PATCH later)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Sync complete: ${created} created, ${updated} updated`);
|
||||
return c.json(200, { status: "success", created, updated });
|
||||
}).catch(err => {
|
||||
console.error("Sync failed:", err);
|
||||
return c.json(500, { error: err.message });
|
||||
});
|
||||
}, null, { cron: "*/5 * * * *" }); // Every 5 minutes
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>SharePoint Link Encoder</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; margin: 2rem; }
|
||||
input[type=text] { width: 100%; padding: 0.5rem; height: 400px; width: 800px;}
|
||||
button { margin-top: 1rem; padding: 0.5rem 1rem; }
|
||||
pre { background: #f4f4f4; padding: 1rem; white-space: pre-wrap; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>SharePoint Link Encoder</h1>
|
||||
<p>Paste your SharePoint/OneDrive link below and click Encode.</p>
|
||||
<form id="encoderForm">
|
||||
<input type="text" id="urlInput" placeholder="https://..." />
|
||||
<button type="submit">Encode</button>
|
||||
</form>
|
||||
|
||||
<div id="results"></div>
|
||||
|
||||
<script>
|
||||
function encodeShareLink(url) {
|
||||
const step0 = url
|
||||
const step1 = btoa(unescape(encodeURIComponent(step0))) // UTF-8 → Base64
|
||||
const step2 = step1.replace(/\//g, '_')
|
||||
const step3 = step2.replace(/\+/g, '-')
|
||||
const step4 = step3.replace(/=+$/g, '')
|
||||
const step5 = 'u!' + step4
|
||||
const endpoint = 'https://graph.microsoft.com/v1.0/shares/' + step5 + '/driveItem'
|
||||
|
||||
return { step0, step1, step2, step3, step4, step5, endpoint }
|
||||
}
|
||||
|
||||
document.getElementById('encoderForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault()
|
||||
const url = document.getElementById('urlInput').value.trim()
|
||||
if (!url) return
|
||||
|
||||
const result = encodeShareLink(url)
|
||||
|
||||
const output = `
|
||||
Step 0 — Original URL:
|
||||
${result.step0}
|
||||
|
||||
Step 1 — Base64:
|
||||
${result.step1}
|
||||
|
||||
Step 2 — "/" → "_":
|
||||
${result.step2}
|
||||
|
||||
Step 3 — "+" → "-":
|
||||
${result.step3}
|
||||
|
||||
Step 4 — Trim "=":
|
||||
${result.step4}
|
||||
|
||||
Step 5 — Add "u!":
|
||||
${result.step5}
|
||||
|
||||
Step 6 — Graph endpoint:
|
||||
${result.endpoint}
|
||||
`
|
||||
document.getElementById('results').innerHTML = '<h2>Encoding Steps</h2><pre>' + output + '</pre>'
|
||||
})
|
||||
</script>
|
||||
<br>
|
||||
<br>
|
||||
From Graph return you will get JSON with "id" and "driveId" under parentReference.
|
||||
id and driveId are the correct values.
|
||||
<br>
|
||||
Join them as /drives/{driveId}/items/{id} for use in app graph document manipulation
|
||||
<br>
|
||||
<form id="freeForm">
|
||||
<br>
|
||||
<input type="text" id="joinText" placeholder="Type or Paste stuff here." />
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user