diff --git a/README.md b/README.md index 49fda15..a9f8adb 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,39 @@ cd /opt/equipment-management 默认只保留最近 `3` 份备份,备份目录在 `/opt/backups/equipment-management`。详细说明见 `BACKUP.md`。 +## 人员与组织同步 + +后端已预留钉钉中台同步能力,支持同步: + +- `hr_groups` -> 部门 +- `hr_employee` + `hr_employee_register` -> 使用者 + +同步接口: + +```bash +GET /api/sync/hr/preview +POST /api/sync/hr/run +``` + +这两个接口都只允许管理员访问。源库必须保持只读,不允许对 SQL Server 做写操作。 + +建议配置的环境变量: + +```bash +SQLSERVER_HOST=218.93.191.16 +SQLSERVER_PORT=1433 +SQLSERVER_DATABASE=JSDataMiddlePlatform +SQLSERVER_USER=nanting +SQLSERVER_PASSWORD=*** +HR_SYNC_ENABLED=false +HR_SYNC_SCHEDULE=03:00 +HR_SYNC_INACTIVE_STATUS_VALUES=5 +``` + +- `HR_SYNC_ENABLED=true` 时,后端会启动每日定时同步 +- `HR_SYNC_SCHEDULE` 建议使用 `03:00` +- `HR_SYNC_INACTIVE_STATUS_VALUES` 用来标记源系统里的非在职状态码 + ## 当前线上资源 - 前端:`http://1.13.22.133` diff --git a/backend/package-lock.json b/backend/package-lock.json index c146dc4..73ac899 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -17,6 +17,7 @@ "express": "^4.18.0", "express-session": "^1.17.3", "keycloak-connect": "^23.0.7", + "mssql": "^11.0.1", "multer": "^1.4.5-lts.1", "mysql2": "^3.0.0" }, @@ -33,6 +34,268 @@ "typescript": "^5.0.0" } }, + "node_modules/@azure-rest/core-client": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@azure-rest/core-client/-/core-client-2.5.1.tgz", + "integrity": "sha512-EHaOXW0RYDKS5CFffnixdyRPak5ytiCtU7uXDcP/uiY+A6jFRwNGzzJBiznkCzvi5EYpY+YWinieqHb0oY916A==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.10.1", + "resolved": "https://registry.npmmirror.com/@azure/core-auth/-/core-auth-1.10.1.tgz", + "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.10.1", + "resolved": "https://registry.npmmirror.com/@azure/core-client/-/core-client-1.10.1.tgz", + "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==", + "license": "MIT", + "peer": true, + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-http-compat": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/@azure/core-http-compat/-/core-http-compat-2.3.2.tgz", + "integrity": "sha512-Tf6ltdKzOJEgxZeWLCjMxrxbodB/ZeCbzzA1A2qHbhzAjzjHoBVSUeSl/baT/oHAxhc4qdqVaDKnc2+iE932gw==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@azure/core-client": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0" + } + }, + "node_modules/@azure/core-lro": { + "version": "2.7.2", + "resolved": "https://registry.npmmirror.com/@azure/core-lro/-/core-lro-2.7.2.tgz", + "integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.2.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-paging": { + "version": "1.6.2", + "resolved": "https://registry.npmmirror.com/@azure/core-paging/-/core-paging-1.6.2.tgz", + "integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.23.0", + "resolved": "https://registry.npmmirror.com/@azure/core-rest-pipeline/-/core-rest-pipeline-1.23.0.tgz", + "integrity": "sha512-Evs1INHo+jUjwHi1T6SG6Ua/LHOQBCLuKEEE6efIpt4ZOoNonaT1kP32GoOcdNDbfqsD2445CPri3MubBy5DEQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/@azure/core-tracing/-/core-tracing-1.3.1.tgz", + "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.13.1", + "resolved": "https://registry.npmmirror.com/@azure/core-util/-/core-util-1.13.1.tgz", + "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.13.0", + "resolved": "https://registry.npmmirror.com/@azure/identity/-/identity-4.13.0.tgz", + "integrity": "sha512-uWC0fssc+hs1TGGVkkghiaFkkS7NkTxfnCH+Hdg+yTehTpMcehpok4PgUKKdyCH+9ldu6FhiHRv84Ntqj1vVcw==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^4.2.0", + "@azure/msal-node": "^3.5.0", + "open": "^10.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/keyvault-common": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/@azure/keyvault-common/-/keyvault-common-2.0.0.tgz", + "integrity": "sha512-wRLVaroQtOqfg60cxkzUkGKrKMsCP6uYXAOomOIysSMyt1/YM0eUn9LqieAWM8DLcU4+07Fio2YGpPeqUbpP9w==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.5.0", + "@azure/core-rest-pipeline": "^1.8.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.10.0", + "@azure/logger": "^1.1.4", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/keyvault-keys": { + "version": "4.10.0", + "resolved": "https://registry.npmmirror.com/@azure/keyvault-keys/-/keyvault-keys-4.10.0.tgz", + "integrity": "sha512-eDT7iXoBTRZ2n3fLiftuGJFD+yjkiB1GNqzU2KbY1TLYeXeSPVTVgn2eJ5vmRTZ11978jy2Kg2wI7xa9Tyr8ag==", + "license": "MIT", + "dependencies": { + "@azure-rest/core-client": "^2.3.3", + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-http-compat": "^2.2.0", + "@azure/core-lro": "^2.7.2", + "@azure/core-paging": "^1.6.2", + "@azure/core-rest-pipeline": "^1.19.0", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/keyvault-common": "^2.0.0", + "@azure/logger": "^1.1.4", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/@azure/logger/-/logger-1.3.0.tgz", + "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", + "license": "MIT", + "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "4.29.0", + "resolved": "https://registry.npmmirror.com/@azure/msal-browser/-/msal-browser-4.29.0.tgz", + "integrity": "sha512-/f3eHkSNUTl6DLQHm+bKecjBKcRQxbd/XLx8lvSYp8Nl/HRyPuIPOijt9Dt0sH50/SxOwQ62RnFCmFlGK+bR/w==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "15.15.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "15.15.0", + "resolved": "https://registry.npmmirror.com/@azure/msal-common/-/msal-common-15.15.0.tgz", + "integrity": "sha512-/n+bN0AKlVa+AOcETkJSKj38+bvFs78BaP4rNtv3MJCmPH0YrHiskMRe74OhyZ5DZjGISlFyxqvf9/4QVEi2tw==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "3.8.8", + "resolved": "https://registry.npmmirror.com/@azure/msal-node/-/msal-node-3.8.8.tgz", + "integrity": "sha512-+f1VrJH1iI517t4zgmuhqORja0bL6LDQXfBqkjuMmfTYXTQQnh1EvwwxO3UbKLT05N0obF72SRHFrC1RBDv5Gg==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "15.15.0", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -115,6 +378,12 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@js-joda/core": { + "version": "5.7.0", + "resolved": "https://registry.npmmirror.com/@js-joda/core/-/core-5.7.0.tgz", + "integrity": "sha512-WBu4ULVVxySLLzK1Ppq+OdfP+adRS4ntmDQT915rzDJ++i95gc2jZkM5B6LWEAwN3lGXpfie3yPABozdD3K3Vg==", + "license": "BSD-3-Clause" + }, "node_modules/@prisma/client": { "version": "5.22.0", "resolved": "https://registry.npmmirror.com/@prisma/client/-/client-5.22.0.tgz", @@ -183,6 +452,12 @@ "@prisma/debug": "5.22.0" } }, + "node_modules/@tediousjs/connection-string": { + "version": "0.5.0", + "resolved": "https://registry.npmmirror.com/@tediousjs/connection-string/-/connection-string-0.5.0.tgz", + "integrity": "sha512-7qSgZbincDDDFyRweCIEvZULFAw5iz/DeunhvuxpL31nfntX3P4Yd4HkHBRg9H8CdqY1e5WFN1PZIz/REL9MVQ==", + "license": "MIT" + }, "node_modules/@testim/chrome-version": { "version": "1.1.4", "resolved": "https://registry.npmmirror.com/@testim/chrome-version/-/chrome-version-1.1.4.tgz", @@ -327,7 +602,6 @@ "version": "20.19.25", "resolved": "https://registry.npmmirror.com/@types/node/-/node-20.19.25.tgz", "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", - "devOptional": true, "license": "MIT", "peer": true, "dependencies": { @@ -348,6 +622,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/readable-stream": { + "version": "4.0.23", + "resolved": "https://registry.npmmirror.com/@types/readable-stream/-/readable-stream-4.0.23.tgz", + "integrity": "sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/send": { "version": "1.2.1", "resolved": "https://registry.npmmirror.com/@types/send/-/send-1.2.1.tgz", @@ -391,6 +674,32 @@ "@types/node": "*" } }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.4", + "resolved": "https://registry.npmmirror.com/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.4.tgz", + "integrity": "sha512-CI0NhTrz4EBaa0U+HaaUZrJhPoso8sG7ZFya8uQoBA57fjzrjRSv87ekCjLZOFExN+gXE/z0xuN2QfH4H2HrLQ==", + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz", @@ -435,7 +744,6 @@ "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz", "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "license": "MIT", - "optional": true, "engines": { "node": ">= 14" } @@ -800,6 +1108,12 @@ "node": "*" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -823,6 +1137,21 @@ "node": ">=0.2.0" } }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -945,6 +1274,15 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/compare-versions": { "version": "6.1.1", "resolved": "https://registry.npmmirror.com/compare-versions/-/compare-versions-6.1.1.tgz", @@ -1123,7 +1461,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "devOptional": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1144,6 +1481,46 @@ "license": "MIT", "optional": true }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmmirror.com/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/degenerator": { "version": "5.0.1", "resolved": "https://registry.npmmirror.com/degenerator/-/degenerator-5.0.1.tgz", @@ -1242,6 +1619,15 @@ "readable-stream": "^2.0.2" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmmirror.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", @@ -1398,6 +1784,24 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/exceljs": { "version": "4.4.0", "resolved": "https://registry.npmmirror.com/exceljs/-/exceljs-4.4.0.tgz", @@ -1961,7 +2365,6 @@ "resolved": "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "license": "MIT", - "optional": true, "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" @@ -1975,7 +2378,6 @@ "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "license": "MIT", - "optional": true, "dependencies": { "agent-base": "^7.1.2", "debug": "4" @@ -2088,6 +2490,21 @@ "node": ">=8" } }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2111,6 +2528,24 @@ "node": ">=0.10.0" } }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -2134,6 +2569,21 @@ "license": "MIT", "optional": true }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is2": { "version": "2.0.9", "resolved": "https://registry.npmmirror.com/is2/-/is2-2.0.9.tgz", @@ -2155,6 +2605,34 @@ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "license": "MIT" }, + "node_modules/js-md4": { + "version": "0.3.2", + "resolved": "https://registry.npmmirror.com/js-md4/-/js-md4-0.3.2.tgz", + "integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==", + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmmirror.com/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, "node_modules/jszip": { "version": "3.10.1", "resolved": "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz", @@ -2167,6 +2645,17 @@ "setimmediate": "^1.0.5" } }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, "node_modules/jwk-to-pem": { "version": "2.0.7", "resolved": "https://registry.npmmirror.com/jwk-to-pem/-/jwk-to-pem-2.0.7.tgz", @@ -2178,6 +2667,16 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keycloak-connect": { "version": "23.0.7", "resolved": "https://registry.npmmirror.com/keycloak-connect/-/keycloak-connect-23.0.7.tgz", @@ -2250,6 +2749,12 @@ "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==", "license": "MIT" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmmirror.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", @@ -2269,24 +2774,48 @@ "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==", "license": "MIT" }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, "node_modules/lodash.isnil": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/lodash.isnil/-/lodash.isnil-4.0.0.tgz", "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==", "license": "MIT" }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmmirror.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", "license": "MIT" }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, "node_modules/lodash.isundefined": { "version": "3.0.1", "resolved": "https://registry.npmmirror.com/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==", "license": "MIT" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/lodash.union": { "version": "4.6.0", "resolved": "https://registry.npmmirror.com/lodash.union/-/lodash.union-4.6.0.tgz", @@ -2456,6 +2985,26 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/mssql": { + "version": "11.0.1", + "resolved": "https://registry.npmmirror.com/mssql/-/mssql-11.0.1.tgz", + "integrity": "sha512-KlGNsugoT90enKlR8/G36H0kTxPthDhmtNUCwEHvgRza5Cjpjoj+P2X6eMpFUDN7pFrJZsKadL4x990G8RBE1w==", + "license": "MIT", + "dependencies": { + "@tediousjs/connection-string": "^0.5.0", + "commander": "^11.0.0", + "debug": "^4.3.3", + "rfdc": "^1.3.0", + "tarn": "^3.0.2", + "tedious": "^18.2.1" + }, + "bin": { + "mssql": "bin/mssql" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/multer": { "version": "1.4.5-lts.2", "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", @@ -2523,6 +3072,12 @@ "node": ">=12.0.0" } }, + "node_modules/native-duplexpair": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/native-duplexpair/-/native-duplexpair-1.0.0.tgz", + "integrity": "sha512-E7QQoM+3jvNtlmyfqRZ0/U75VFgCls+fSkbml2MpgWkWyz3ox8Y58gNhfuziuQYGNNQAbFZJQck55LHCnCK6CA==", + "license": "MIT" + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz", @@ -2631,6 +3186,24 @@ "wrappy": "1" } }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmmirror.com/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/pac-proxy-agent": { "version": "7.2.0", "resolved": "https://registry.npmmirror.com/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", @@ -2736,6 +3309,15 @@ "fsevents": "2.3.3" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmmirror.com/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -2912,6 +3494,12 @@ "node": ">=8.10.0" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, "node_modules/rimraf": { "version": "2.7.1", "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-2.7.1.tgz", @@ -2925,6 +3513,18 @@ "rimraf": "bin.js" } }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmmirror.com/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -2967,7 +3567,6 @@ "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3192,6 +3791,12 @@ "node": ">=0.10.0" } }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, "node_modules/sqlstring": { "version": "2.3.3", "resolved": "https://registry.npmmirror.com/sqlstring/-/sqlstring-2.3.3.tgz", @@ -3276,6 +3881,15 @@ "node": ">= 6" } }, + "node_modules/tarn": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/tarn/-/tarn-3.0.2.tgz", + "integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/tcp-port-used": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/tcp-port-used/-/tcp-port-used-1.0.2.tgz", @@ -3312,6 +3926,100 @@ "license": "MIT", "optional": true }, + "node_modules/tedious": { + "version": "18.6.2", + "resolved": "https://registry.npmmirror.com/tedious/-/tedious-18.6.2.tgz", + "integrity": "sha512-g7jC56o3MzLkE3lHkaFe2ZdOVFBahq5bsB60/M4NYUbocw/MCrS89IOEQUFr+ba6pb8ZHczZ/VqCyYeYq0xBAg==", + "license": "MIT", + "dependencies": { + "@azure/core-auth": "^1.7.2", + "@azure/identity": "^4.2.1", + "@azure/keyvault-keys": "^4.4.0", + "@js-joda/core": "^5.6.1", + "@types/node": ">=18", + "bl": "^6.0.11", + "iconv-lite": "^0.6.3", + "js-md4": "^0.3.2", + "native-duplexpair": "^1.0.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tedious/node_modules/bl": { + "version": "6.1.6", + "resolved": "https://registry.npmmirror.com/bl/-/bl-6.1.6.tgz", + "integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==", + "license": "MIT", + "dependencies": { + "@types/readable-stream": "^4.0.0", + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^4.2.0" + } + }, + "node_modules/tedious/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/tedious/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tedious/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/tedious/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/tmp": { "version": "0.2.5", "resolved": "https://registry.npmmirror.com/tmp/-/tmp-0.2.5.tgz", @@ -3410,8 +4118,7 @@ "version": "2.8.1", "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "optional": true + "license": "0BSD" }, "node_modules/type-is": { "version": "1.6.18", @@ -3470,7 +4177,6 @@ "version": "6.21.0", "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "devOptional": true, "license": "MIT" }, "node_modules/unpipe": { @@ -3546,6 +4252,21 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmmirror.com/xmlchars/-/xmlchars-2.2.0.tgz", diff --git a/backend/package.json b/backend/package.json index cd3396c..af06813 100644 --- a/backend/package.json +++ b/backend/package.json @@ -23,6 +23,7 @@ "express": "^4.18.0", "express-session": "^1.17.3", "keycloak-connect": "^23.0.7", + "mssql": "^11.0.1", "multer": "^1.4.5-lts.1", "mysql2": "^3.0.0" }, diff --git a/backend/prisma/migrations/20260310_add_sync_fields/migration.sql b/backend/prisma/migrations/20260310_add_sync_fields/migration.sql new file mode 100644 index 0000000..c2301bd --- /dev/null +++ b/backend/prisma/migrations/20260310_add_sync_fields/migration.sql @@ -0,0 +1,33 @@ +ALTER TABLE `department` + DROP INDEX `department_name_key`, + ADD COLUMN `source_group_id` INT NULL, + ADD COLUMN `source_parent_group_id` INT NULL, + ADD COLUMN `source_system` VARCHAR(30) NULL, + ADD COLUMN `last_synced_at` DATETIME(3) NULL; + +CREATE UNIQUE INDEX `department_source_group_id_key` + ON `department`(`source_group_id`); + +CREATE INDEX `department_source_system_idx` + ON `department`(`source_system`); + +CREATE INDEX `department_last_synced_at_idx` + ON `department`(`last_synced_at`); + +ALTER TABLE `person` + ADD COLUMN `source_userid` VARCHAR(100) NULL, + ADD COLUMN `source_system` VARCHAR(30) NULL, + ADD COLUMN `source_main_department_id` INT NULL, + ADD COLUMN `source_dept_ids` TEXT NULL, + ADD COLUMN `manager_source_userid` VARCHAR(100) NULL, + ADD COLUMN `source_employee_status` VARCHAR(30) NULL, + ADD COLUMN `last_synced_at` DATETIME(3) NULL; + +CREATE UNIQUE INDEX `person_source_userid_key` + ON `person`(`source_userid`); + +CREATE INDEX `person_source_system_idx` + ON `person`(`source_system`); + +CREATE INDEX `person_last_synced_at_idx` + ON `person`(`last_synced_at`); diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 5d1c34b..bd0a7cb 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -28,12 +28,16 @@ model AssetType { } model Department { - id Int @id @default(autoincrement()) - name String @unique @db.VarChar(100) - status String @default("active") @db.VarChar(20) - parent_id Int? // 父部门ID,null表示根部门 - created_at DateTime @default(now()) - updated_at DateTime @updatedAt + id Int @id @default(autoincrement()) + name String @db.VarChar(100) + status String @default("active") @db.VarChar(20) + parent_id Int? // 父部门ID,null表示根部门 + source_group_id Int? @unique + source_parent_group_id Int? + source_system String? @db.VarChar(30) + last_synced_at DateTime? + created_at DateTime @default(now()) + updated_at DateTime @updatedAt // 自关联 - 部门层级 parent Department? @relation("DepartmentHierarchy", fields: [parent_id], references: [id]) @@ -43,6 +47,8 @@ model Department { @@index([status]) @@index([parent_id]) + @@index([source_system]) + @@index([last_synced_at]) @@map("department") } @@ -172,24 +178,33 @@ model SiteConfig { } model Person { - id Int @id @default(autoincrement()) - nickname String @db.VarChar(50) - real_name String @db.VarChar(50) - id_number String? @unique @db.VarChar(50) - department String? @db.VarChar(100) - position String? @db.VarChar(100) - phone String? @db.VarChar(20) - email String? @db.VarChar(100) - status String @default("active") @db.VarChar(20) - remarks String? @db.Text - created_at DateTime @default(now()) - updated_at DateTime @updatedAt + id Int @id @default(autoincrement()) + nickname String @db.VarChar(50) + real_name String @db.VarChar(50) + id_number String? @unique @db.VarChar(50) + department String? @db.VarChar(100) + position String? @db.VarChar(100) + phone String? @db.VarChar(20) + email String? @db.VarChar(100) + status String @default("active") @db.VarChar(20) + remarks String? @db.Text + source_userid String? @unique @db.VarChar(100) + source_system String? @db.VarChar(30) + source_main_department_id Int? + source_dept_ids String? @db.Text + manager_source_userid String? @db.VarChar(100) + source_employee_status String? @db.VarChar(30) + last_synced_at DateTime? + created_at DateTime @default(now()) + updated_at DateTime @updatedAt borrow_records BorrowRecord[] @@index([nickname]) @@index([real_name]) @@index([status]) + @@index([source_system]) + @@index([last_synced_at]) @@map("person") } diff --git a/backend/src/controllers/departmentController.ts b/backend/src/controllers/departmentController.ts index 4227b63..efe7a9c 100644 --- a/backend/src/controllers/departmentController.ts +++ b/backend/src/controllers/departmentController.ts @@ -7,10 +7,64 @@ const baseSelect = { name: true, status: true, parent_id: true, + source_group_id: true, + source_parent_group_id: true, + source_system: true, + last_synced_at: true, created_at: true, updated_at: true } +const buildDepartmentLevelMap = (departments: Array<{ id: number; parent_id: number | null }>) => { + const byId = new Map(departments.map((dept) => [dept.id, dept])) + const cache = new Map() + + const resolveLevel = (id: number): number => { + if (cache.has(id)) { + return cache.get(id)! + } + const current = byId.get(id) + if (!current || !current.parent_id) { + cache.set(id, 0) + return 0 + } + const level = resolveLevel(current.parent_id) + 1 + cache.set(id, level) + return level + } + + departments.forEach((dept) => resolveLevel(dept.id)) + return cache +} + +const buildDepartmentTree = ( + departments: Array<{ + id: number + name: string + status: string + parent_id: number | null + created_at: Date + updated_at: Date + _count: { equipments: number; children: number } + }> +) => { + const childrenMap = new Map() + for (const department of departments) { + const siblings = childrenMap.get(department.parent_id) || [] + siblings.push(department) + childrenMap.set(department.parent_id, siblings) + } + + const buildNode = (department: typeof departments[number]): any => ({ + ...department, + equipment_count: department._count.equipments, + children_count: department._count.children, + children: (childrenMap.get(department.id) || []).map(buildNode) + }) + + return (childrenMap.get(null) || []).map(buildNode) +} + // 获取启用的部门列表(下拉选项,含层级信息) export const getDepartmentOptions = async (_req: Request, res: Response) => { try { @@ -25,11 +79,11 @@ export const getDepartmentOptions = async (_req: Request, res: Response) => { } }) - // 添加层级信息 + const levelMap = buildDepartmentLevelMap(departments) const options = departments.map(dept => ({ ...dept, parent_name: dept.parent?.name || null, - level: dept.parent_id ? 1 : 0 + level: levelMap.get(dept.id) || 0 })) successResponse(res, options, '获取成功') @@ -51,23 +105,7 @@ export const getDepartmentTree = async (_req: Request, res: Response) => { } }) - // 构建树形结构 - const rootDepts = departments.filter(d => !d.parent_id) - const childDepts = departments.filter(d => d.parent_id) - - const tree = rootDepts.map(root => ({ - ...root, - equipment_count: root._count.equipments, - children_count: root._count.children, - children: childDepts - .filter(child => child.parent_id === root.id) - .map(child => ({ - ...child, - equipment_count: child._count.equipments, - children_count: child._count.children, - children: [] - })) - })) + const tree = buildDepartmentTree(departments) successResponse(res, tree, '获取成功') } catch (error: any) { @@ -109,10 +147,11 @@ export const getDepartmentList = async (req: Request, res: Response) => { prisma.department.count({ where }) ]) + const levelMap = buildDepartmentLevelMap(list) const formattedList = list.map(dept => ({ ...dept, parent_name: dept.parent?.name || null, - level: dept.parent_id ? 1 : 0, + level: levelMap.get(dept.id) || 0, equipment_count: dept._count.equipments, children_count: dept._count.children })) @@ -149,16 +188,25 @@ const validateParentDepartment = async (parentId: number | null | undefined, cur return '父部门已禁用,无法设置' } - // 检查父部门是否已经是子部门(限制两级) - if (parent.parent_id) { - return '不能将子部门设为父部门(最多支持两级层级)' - } - // 检查是否设置自己为父部门 if (currentId && parentId === currentId) { return '不能将部门设为自己的子部门' } + if (currentId) { + let cursor = parent + while (cursor.parent_id) { + if (cursor.parent_id === currentId) { + return '不能将部门设为自己的子部门' + } + cursor = await prisma.department.findUnique({ + where: { id: cursor.parent_id }, + select: { id: true, parent_id: true, status: true } + }) as any + if (!cursor) break + } + } + return null } @@ -226,11 +274,6 @@ export const updateDepartment = async (req: Request, res: Response) => { // 如果要设置父部门,验证有效性 if (parent_id !== undefined) { - // 如果该部门有子部门,不能将其变为子部门 - if (parent_id && existing._count.children > 0) { - return errorResponse(res, '该部门下存在子部门,不能将其设为子部门', 400) - } - const parentError = await validateParentDepartment(parent_id, Number(id)) if (parentError) { return errorResponse(res, parentError, 400) diff --git a/backend/src/controllers/syncController.ts b/backend/src/controllers/syncController.ts new file mode 100644 index 0000000..69853d8 --- /dev/null +++ b/backend/src/controllers/syncController.ts @@ -0,0 +1,22 @@ +import type { Request, Response } from 'express' +import { errorResponse, successResponse } from '../utils/response' +import { previewHrSync, runHrSync } from '../services/hrSyncService' + +export const previewHrSyncController = async (_req: Request, res: Response) => { + try { + const result = await previewHrSync() + successResponse(res, result, '预览成功') + } catch (error: any) { + errorResponse(res, error.message || '预览同步数据失败', 500) + } +} + +export const runHrSyncController = async (_req: Request, res: Response) => { + try { + const operator = res.locals.user?.username || res.locals.user?.name || 'system-sync' + const result = await runHrSync(operator) + successResponse(res, result, '同步成功') + } catch (error: any) { + errorResponse(res, error.message || '执行同步失败', 500) + } +} diff --git a/backend/src/index.ts b/backend/src/index.ts index 14b73b7..fe53bff 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -23,8 +23,10 @@ import maintenanceRoutes from './routes/maintenance' import batchRoutes from './routes/batch' import exportRoutes from './routes/export' import searchRoutes from './routes/search' +import syncRoutes from './routes/sync' import { keycloak, memoryStore } from './utils/keycloak' import { protect, requireAdmin } from './middleware/auth' +import { startHrSyncScheduler } from './services/hrSyncService' dotenv.config() @@ -76,6 +78,7 @@ app.use('/api/maintenance', protect(), maintenanceRoutes) app.use('/api/batch', protect(), batchRoutes) app.use('/api/export', protect(), exportRoutes) app.use('/api/search', protect(), searchRoutes) +app.use('/api/sync', protect(), requireAdmin(), syncRoutes) // Health check app.get('/api/health', (req, res) => { @@ -95,3 +98,5 @@ app.use((err: any, req: express.Request, res: express.Response, next: express.Ne app.listen(PORT, () => { console.log(`Server is running on http://localhost:${PORT}`) }) + +startHrSyncScheduler() diff --git a/backend/src/routes/sync.ts b/backend/src/routes/sync.ts new file mode 100644 index 0000000..3c0c906 --- /dev/null +++ b/backend/src/routes/sync.ts @@ -0,0 +1,9 @@ +import { Router } from 'express' +import { previewHrSyncController, runHrSyncController } from '../controllers/syncController' + +const router = Router() + +router.get('/hr/preview', previewHrSyncController) +router.post('/hr/run', runHrSyncController) + +export default router diff --git a/backend/src/services/hrSyncService.ts b/backend/src/services/hrSyncService.ts new file mode 100644 index 0000000..c91a3bc --- /dev/null +++ b/backend/src/services/hrSyncService.ts @@ -0,0 +1,493 @@ +import prisma from '../utils/prisma' +import sql from 'mssql' + +type SourceDepartment = { + sourceGroupId: number + sourceParentGroupId: number | null + name: string +} + +type SourcePerson = { + sourceUserId: string + nickname: string + realName: string + idNumber: string | null + departmentName: string | null + position: string | null + phone: string | null + email: string | null + sourceMainDepartmentId: number | null + sourceDeptIds: string | null + managerSourceUserId: string | null + sourceEmployeeStatus: string | null + status: 'active' | 'inactive' +} + +type HrSyncResult = { + departments: { + total: number + created: number + updated: number + deactivated: number + } + persons: { + total: number + created: number + updated: number + deactivated: number + } +} + +const SOURCE_SYSTEM = 'ding_hr' +let schedulerStarted = false +let schedulerRunning = false +let lastSchedulerRunKey = '' + +const getSqlServerConfig = (): any => { + const server = process.env.SQLSERVER_HOST + const port = Number(process.env.SQLSERVER_PORT || '1433') + const database = process.env.SQLSERVER_DATABASE + const user = process.env.SQLSERVER_USER + const password = process.env.SQLSERVER_PASSWORD + + if (!server || !database || !user || !password) { + throw new Error('SQL Server 同步配置不完整,请设置 SQLSERVER_HOST / DATABASE / USER / PASSWORD') + } + + return { + server, + port, + database, + user, + password, + options: { + encrypt: true, + trustServerCertificate: true + }, + pool: { + max: 5, + min: 0, + idleTimeoutMillis: 30000 + } + } +} + +const safeTrim = (value: unknown) => { + if (value === null || value === undefined) return null + const text = String(value).trim() + return text ? text : null +} + +const normalizePhone = (value: unknown) => { + const text = safeTrim(value) + if (!text) return null + return text.replace(/^\+86-?/, '') +} + +const parseSourceInt = (value: unknown) => { + const text = safeTrim(value) + if (!text) return null + const num = Number(text) + return Number.isNaN(num) ? null : num +} + +const parseExtensionFlowerName = (extension: unknown) => { + const text = safeTrim(extension) + if (!text) return null + try { + const parsed = JSON.parse(text) + const value = parsed?.花名 || parsed?.flowerName + return safeTrim(value) + } catch { + return null + } +} + +const resolvePersonStatus = (active: unknown, employeeStatus: unknown): 'active' | 'inactive' => { + const inactiveStatusValues = new Set( + (process.env.HR_SYNC_INACTIVE_STATUS_VALUES || '5') + .split(',') + .map((item) => item.trim()) + .filter(Boolean) + ) + + const sourceStatus = safeTrim(employeeStatus) + const activeFlag = + active === true || + active === 1 || + String(active).toLowerCase() === 'true' + + if (!activeFlag) { + return 'inactive' + } + + if (sourceStatus && inactiveStatusValues.has(sourceStatus)) { + return 'inactive' + } + + return 'active' +} + +const getSourceDepartments = async (pool: any): Promise => { + const result = await pool + .request() + .query(` + SELECT + Id AS sourceGroupId, + parent_id AS sourceParentGroupId, + name + FROM dbo.hr_groups + ORDER BY Id + `) + + return result.recordset.map((row: any) => ({ + sourceGroupId: Number(row.sourceGroupId), + sourceParentGroupId: row.sourceParentGroupId === null ? null : Number(row.sourceParentGroupId), + name: String(row.name).trim() + })) +} + +const getSourcePersons = async (pool: any): Promise => { + const result = await pool + .request() + .query(` + SELECT + e.userid AS sourceUserId, + e.name AS employeeName, + e.mobile, + e.job_number, + e.title, + e.email, + e.dept_id_list, + e.manager_userid, + e.active, + e.extension, + r.[sys00-name] AS registerName, + r.[sys00-flowerName] AS flowerName, + r.[sys02-realName] AS realName, + r.[sys02-certNo] AS certNo, + r.[sys00-mobile] AS registerMobile, + r.[sys00-position] AS registerPosition, + r.[sys00-dept] AS registerDept, + r.[sys00-mainDeptId] AS mainDeptId, + r.[sys00-deptIds] AS registerDeptIds, + r.[sys01-employeeStatus] AS employeeStatus + FROM dbo.hr_employee e + LEFT JOIN dbo.hr_employee_register r + ON r.userid = e.userid + `) + + return result.recordset + .map((row: any) => { + const fallbackName = safeTrim(row.employeeName) || safeTrim(row.registerName) || safeTrim(row.sourceUserId) + const nickname = + safeTrim(row.flowerName) || + parseExtensionFlowerName(row.extension) || + fallbackName || + '未命名' + + const realName = safeTrim(row.realName) || fallbackName || nickname + + return { + sourceUserId: String(row.sourceUserId).trim(), + nickname, + realName, + idNumber: safeTrim(row.certNo), + departmentName: safeTrim(row.registerDept), + position: safeTrim(row.registerPosition) || safeTrim(row.title), + phone: normalizePhone(row.registerMobile) || normalizePhone(row.mobile), + email: safeTrim(row.email), + sourceMainDepartmentId: parseSourceInt(row.mainDeptId), + sourceDeptIds: safeTrim(row.registerDeptIds) || safeTrim(row.dept_id_list), + managerSourceUserId: safeTrim(row.manager_userid), + sourceEmployeeStatus: safeTrim(row.employeeStatus), + status: resolvePersonStatus(row.active, row.employeeStatus) + } satisfies SourcePerson + }) + .filter((item: SourcePerson) => item.sourceUserId) +} + +const syncDepartments = async (sourceDepartments: SourceDepartment[]) => { + const existing = await prisma.department.findMany({ + where: { source_system: SOURCE_SYSTEM }, + select: { + id: true, + source_group_id: true + } + }) + + const existingIds = new Set(existing.map((item) => item.source_group_id).filter(Boolean) as number[]) + let created = 0 + let updated = 0 + + for (const department of sourceDepartments) { + const exists = existingIds.has(department.sourceGroupId) + + await prisma.department.upsert({ + where: { source_group_id: department.sourceGroupId }, + update: { + name: department.name, + status: 'active', + source_parent_group_id: department.sourceParentGroupId, + source_system: SOURCE_SYSTEM, + last_synced_at: new Date() + }, + create: { + name: department.name, + status: 'active', + source_group_id: department.sourceGroupId, + source_parent_group_id: department.sourceParentGroupId, + source_system: SOURCE_SYSTEM, + last_synced_at: new Date() + } + }) + + if (exists) { + updated++ + } else { + created++ + } + } + + const syncedDepartments = await prisma.department.findMany({ + where: { source_system: SOURCE_SYSTEM }, + select: { + id: true, + source_group_id: true, + source_parent_group_id: true + } + }) + + const groupIdToDepartmentId = new Map( + syncedDepartments + .filter((item) => item.source_group_id !== null) + .map((item) => [item.source_group_id as number, item.id]) + ) + + for (const department of syncedDepartments) { + const targetParentId = department.source_parent_group_id + ? groupIdToDepartmentId.get(department.source_parent_group_id) || null + : null + + await prisma.department.update({ + where: { id: department.id }, + data: { parent_id: targetParentId } + }) + } + + const sourceGroupIds = sourceDepartments.map((item) => item.sourceGroupId) + const deactivatedResult = await prisma.department.updateMany({ + where: { + source_system: SOURCE_SYSTEM, + source_group_id: { + notIn: sourceGroupIds + } + }, + data: { + status: 'inactive', + last_synced_at: new Date() + } + }) + + return { + total: sourceDepartments.length, + created, + updated, + deactivated: deactivatedResult.count + } +} + +const resolveUniqueIdNumber = async (sourceUserId: string, idNumber: string | null) => { + if (!idNumber) return null + const conflict = await prisma.person.findFirst({ + where: { + id_number: idNumber, + NOT: { + source_userid: sourceUserId + } + }, + select: { id: true } + }) + + return conflict ? null : idNumber +} + +const syncPersons = async (sourcePersons: SourcePerson[]) => { + const existing = await prisma.person.findMany({ + where: { source_system: SOURCE_SYSTEM }, + select: { + id: true, + source_userid: true + } + }) + + const existingIds = new Set(existing.map((item) => item.source_userid).filter(Boolean) as string[]) + let created = 0 + let updated = 0 + + for (const person of sourcePersons) { + const normalizedIdNumber = await resolveUniqueIdNumber(person.sourceUserId, person.idNumber) + const exists = existingIds.has(person.sourceUserId) + + await prisma.person.upsert({ + where: { source_userid: person.sourceUserId }, + update: { + nickname: person.nickname, + real_name: person.realName, + id_number: normalizedIdNumber, + department: person.departmentName, + position: person.position, + phone: person.phone, + email: person.email, + status: person.status, + source_system: SOURCE_SYSTEM, + source_main_department_id: person.sourceMainDepartmentId, + source_dept_ids: person.sourceDeptIds, + manager_source_userid: person.managerSourceUserId, + source_employee_status: person.sourceEmployeeStatus, + last_synced_at: new Date() + }, + create: { + nickname: person.nickname, + real_name: person.realName, + id_number: normalizedIdNumber, + department: person.departmentName, + position: person.position, + phone: person.phone, + email: person.email, + status: person.status, + source_userid: person.sourceUserId, + source_system: SOURCE_SYSTEM, + source_main_department_id: person.sourceMainDepartmentId, + source_dept_ids: person.sourceDeptIds, + manager_source_userid: person.managerSourceUserId, + source_employee_status: person.sourceEmployeeStatus, + last_synced_at: new Date() + } + }) + + if (exists) { + updated++ + } else { + created++ + } + } + + const sourceUserIds = sourcePersons.map((item) => item.sourceUserId) + const deactivatedResult = await prisma.person.updateMany({ + where: { + source_system: SOURCE_SYSTEM, + source_userid: { + notIn: sourceUserIds + } + }, + data: { + status: 'inactive', + last_synced_at: new Date() + } + }) + + return { + total: sourcePersons.length, + created, + updated, + deactivated: deactivatedResult.count + } +} + +const writeSyncLog = async (result: HrSyncResult, operator = 'system-sync') => { + await prisma.operationLog.create({ + data: { + user_id: null, + username: operator, + operation_type: '同步钉钉组织与人员', + operation_details: JSON.stringify(result), + ip_address: null + } + }) +} + +export const previewHrSync = async () => { + const pool = await sql.connect(getSqlServerConfig()) + try { + const [departments, persons] = await Promise.all([ + getSourceDepartments(pool), + getSourcePersons(pool) + ]) + + return { + departments: { + total: departments.length, + sample: departments.slice(0, 10) + }, + persons: { + total: persons.length, + sample: persons.slice(0, 10) + } + } + } finally { + await pool.close() + } +} + +export const runHrSync = async (operator = 'system-sync') => { + const pool = await sql.connect(getSqlServerConfig()) + try { + const [departments, persons] = await Promise.all([ + getSourceDepartments(pool), + getSourcePersons(pool) + ]) + + const departmentResult = await syncDepartments(departments) + const personResult = await syncPersons(persons) + const result = { + departments: departmentResult, + persons: personResult + } + + await writeSyncLog(result, operator) + return result + } finally { + await pool.close() + } +} + +export const startHrSyncScheduler = () => { + if (schedulerStarted) { + return + } + schedulerStarted = true + + const enabled = process.env.HR_SYNC_ENABLED === 'true' + if (!enabled) { + console.log('[hr-sync] scheduler disabled') + return + } + + const scheduleTime = process.env.HR_SYNC_SCHEDULE || '03:00' + + setInterval(async () => { + const now = new Date() + const hh = String(now.getHours()).padStart(2, '0') + const mm = String(now.getMinutes()).padStart(2, '0') + const currentTime = `${hh}:${mm}` + const runKey = `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}` + + if (currentTime !== scheduleTime || schedulerRunning || lastSchedulerRunKey === runKey) { + return + } + + schedulerRunning = true + try { + await runHrSync('system-scheduler') + lastSchedulerRunKey = runKey + console.log(`[hr-sync] completed at ${now.toISOString()}`) + } catch (error) { + console.error('[hr-sync] failed:', error) + } finally { + schedulerRunning = false + } + }, 60 * 1000) + + console.log(`[hr-sync] scheduler started, schedule=${scheduleTime}`) +} diff --git a/backend/src/types/mssql.d.ts b/backend/src/types/mssql.d.ts new file mode 100644 index 0000000..b0492ae --- /dev/null +++ b/backend/src/types/mssql.d.ts @@ -0,0 +1 @@ +declare module 'mssql';