diff --git a/.gitignore b/.gitignore index 08b341a..beed46a 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ pnpm-lock.yaml yarn-lock.json .vscode/database.json yarn-error.log +.history diff --git a/.vscodeignore b/.vscodeignore index 3ca8f15..3f764b3 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -13,4 +13,5 @@ package-lock.json *.vsix dist/**/*.map tsconfig.json -webpack.config.json \ No newline at end of file +webpack.config.json +.history diff --git a/README.md b/README.md index b041c0d..f086af0 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ Get it from [Visual Studio Marketplace](https://marketplace.visualstudio.com/ite "patternName": { // Your source database connection section "source": { + "type": "postgres", // Or mssql "user": string, "password": string, "host": string, @@ -90,6 +91,7 @@ Get it from [Visual Studio Marketplace](https://marketplace.visualstudio.com/ite // Your target database connection section "target": { + "type": "postgres", // Or mssql "user": string, "password": string, "host": string, diff --git a/package-lock.json b/package-lock.json index 2f676cb..bd3a021 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "data-sync", - "version": "1.0.27", + "version": "1.0.31", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "data-sync", - "version": "1.0.27", + "version": "1.0.31", "license": "MIT", "dependencies": { "@types/chance": "^1.1.3", @@ -15,6 +15,8 @@ "diff": "^5.1.0", "fs-extra": "^11.1.1", "lodash.groupby": "^4.6.0", + "msnodesqlv8": "^5.1.1", + "mssql": "^12.0.0", "pg": "^8.11.1", "pg-iterator": "^0.3.0", "pg-promise": "^11.5.0" @@ -26,6 +28,7 @@ "@types/glob": "^8.1.0", "@types/lodash.groupby": "^4.6.7", "@types/mocha": "^10.0.1", + "@types/mssql": "^9.1.7", "@types/node": "^20.2.5", "@types/pg": "^8.10.2", "@types/pg-promise": "^5.4.3", @@ -65,6 +68,264 @@ "node": ">=0.10.0" } }, + "node_modules/@azure-rest/core-client": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@azure-rest/core-client/-/core-client-2.5.0.tgz", + "integrity": "sha512-KMVIPxG6ygcQ1M2hKHahF7eddKejYsWTjoLIfTWiqnaj42dBkYzj4+S8rK9xxmlOaEHKZHcMrRbm0NfN4kgwHw==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-rest-pipeline": "^1.5.0", + "@azure/core-tracing": "^1.0.1", + "@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.npmjs.org/@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.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.0.tgz", + "integrity": "sha512-88Djs5vBvGbHQHf5ZZcaoNHo6Y8BKZkt3cw2iuJIQzLEgH4Ox6Tm4hjFhbqOxyYsgIG/eJbFEHpxRIfEEWv5Ow==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.0.tgz", + "integrity": "sha512-O4aP3CLFNodg8eTHXECaH3B3CjicfzkxVtnrfLkOq0XNP7TIECGfHpK/C6vADZkWP75wzmdBnsIA8ksuJMk18g==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-rest-pipeline": "^1.20.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.6.1", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-http-compat": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.3.0.tgz", + "integrity": "sha512-qLQujmUypBBG0gxHd0j6/Jdmul6ttl24c8WGiLXIk7IHXdBlfoBqW27hyz3Xn6xbfdyVSarl1Ttbk0AwnZBYCw==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-client": "^1.3.0", + "@azure/core-rest-pipeline": "^1.20.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-lro": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@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.npmjs.org/@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.22.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.22.0.tgz", + "integrity": "sha512-OKHmb3/Kpm06HypvB3g6Q3zJuvyXcpxDpCS1PnU8OV6AJgSFaee/covXBcPbWc6XDDxtEPlbi3EMQ6nUiPaQtw==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.8.0", + "@azure/core-tracing": "^1.0.1", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.0.tgz", + "integrity": "sha512-+XvmZLLWPe67WXNZo9Oc9CrPj/Tm8QnHR92fFAFdnbzwNdCH1h+7UdpaQgRSBsMY+oW1kHXNUZQLdZ1gHX3ROw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.0.tgz", + "integrity": "sha512-o0psW8QWQ58fq3i24Q1K2XfS/jYTxr7O1HRcyUE9bV9NttLU+kYOH82Ixj8DGlMTOWgxm1Sss2QAfKK5UkSPxw==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.10.2.tgz", + "integrity": "sha512-Uth4vz0j+fkXCkbvutChUj03PDCokjbC6Wk9JT8hHEUtpy/EurNKAseb3+gO6Zi9VYBvwt61pgbzn1ovk942Qg==", + "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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.18.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.18.0.tgz", + "integrity": "sha512-esQwdtHHVkFJhcKWnysnCTchiKsy3dmNZGs8AckD9PO3t8Lp5VtY0xcrbCBC0JbttG/5w2/xukUQOsMpoUFKrg==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "15.9.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "15.9.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.9.0.tgz", + "integrity": "sha512-lbz/D+C9ixUG3hiZzBLjU79a0+5ZXCorjel3mwXluisKNH0/rOS/ajm8yi4yI9RP5Uc70CAcs9Ipd0051Oh/kA==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.6.4.tgz", + "integrity": "sha512-jMeut9UQugcmq7aPWWlJKhJIse4DQ594zc/JaP6BIxg55XaX3aM/jcPuIQ4ryHnI4QSf03wUspy/uqAvjWKbOg==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "15.9.0", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@babel/code-frame": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", @@ -1134,6 +1395,12 @@ "@jridgewell/sourcemap-codec": "1.4.14" } }, + "node_modules/@js-joda/core": { + "version": "5.6.5", + "resolved": "https://registry.npmjs.org/@js-joda/core/-/core-5.6.5.tgz", + "integrity": "sha512-3zwefSMwHpu8iVUW8YYz227sIv6UFqO31p1Bf1ZH/Vom7CmNyUsXjDBlnNzcuhmOL1XfxZ3nvND42kR23XlbcQ==", + "license": "BSD-3-Clause" + }, "node_modules/@manypkg/find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@manypkg/find-root/-/find-root-1.1.0.tgz", @@ -1281,6 +1548,12 @@ "node": ">=14" } }, + "node_modules/@tediousjs/connection-string": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@tediousjs/connection-string/-/connection-string-0.6.0.tgz", + "integrity": "sha512-GxlsW354Vi6QqbUgdPyQVcQjI7cZBdGV5vOYVYuCVDTylx2wl3WHR2HlhcxxHTrMigbelpXsdcZso+66uxPfow==", + "license": "MIT" + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -1404,11 +1677,26 @@ "integrity": "sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q==", "dev": true }, + "node_modules/@types/mssql": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/@types/mssql/-/mssql-9.1.7.tgz", + "integrity": "sha512-eIOEe78nuSW5KctDHImDhLZ9a+jV/z/Xs5RBhcG/jrk+YWqhdNmzBmHVWV7aWQ5fW+jbIGtX6Ph+bbVqfhzafg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "tarn": "^3.0.1", + "tedious": "*" + } + }, "node_modules/@types/node": { - "version": "20.4.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.2.tgz", - "integrity": "sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==", - "dev": true + "version": "20.19.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.9.tgz", + "integrity": "sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } }, "node_modules/@types/normalize-package-data": { "version": "2.4.1", @@ -1447,6 +1735,15 @@ "pg-query-stream": "*" } }, + "node_modules/@types/readable-stream": { + "version": "4.0.21", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.21.tgz", + "integrity": "sha512-19eKVv9tugr03IgfXlA9UVUVRbW6IuqRO5B92Dl4a6pT7K8uaGrNS0GkxiZD0BOk6PLuXl5FhWl//eX/pzYdTQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/semver": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", @@ -1657,6 +1954,55 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.0.tgz", + "integrity": "sha512-sOx1PKSuFwnIl7z4RN0Ls7N9AQawmR9r66eI5rFCzLDIs8HTIYrIpH9QjYWoX0lkgGrkLxXhi4QnK7MizPRrIg==", + "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/@typespec/ts-http-runtime/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/@typespec/ts-http-runtime/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@typespec/ts-http-runtime/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/@vscode/test-electron": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.3.3.tgz", @@ -1874,6 +2220,18 @@ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/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/acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", @@ -2122,7 +2480,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -2159,6 +2516,34 @@ "node": ">=8" } }, + "node_modules/bl": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.1.tgz", + "integrity": "sha512-yYc8UIHrd1ZTLgNBIE7JjMzUPZH+dec3q7nWkrSHEbtvkQ3h6WKC63W9K5jthcL5EXFyMuWYq+2pq5WMSIgFHw==", + "license": "MIT", + "dependencies": { + "@types/readable-stream": "^4.0.0", + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^4.2.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/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/bn.js": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", @@ -2308,7 +2693,6 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, "funding": [ { "type": "github", @@ -2328,6 +2712,12 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/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", @@ -2348,6 +2738,21 @@ "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", "dev": true }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/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/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -2480,6 +2885,12 @@ "node": ">= 6" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/chrome-trace-event": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", @@ -2784,7 +3195,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -2831,12 +3241,64 @@ "node": ">=0.10.0" } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "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.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/defaults": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", @@ -2849,6 +3311,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/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/define-properties": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", @@ -2884,6 +3358,15 @@ "node": ">=8" } }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/diff": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", @@ -2939,6 +3422,15 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/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/electron-to-chromium": { "version": "1.4.461", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.461.tgz", @@ -2972,6 +3464,15 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.15.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", @@ -3533,11 +4034,19 @@ "node": ">=0.10.0" } }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/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.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "engines": { "node": ">=0.8.x" } @@ -3552,6 +4061,15 @@ "safe-buffer": "^5.1.1" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/extendable-error": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/extendable-error/-/extendable-error-0.1.7.tgz", @@ -3736,6 +4254,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fs-extra": { "version": "11.1.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", @@ -3842,6 +4366,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/glob": { "version": "10.3.3", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.3.tgz", @@ -4174,7 +4704,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -4280,8 +4809,13 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" }, "node_modules/internal-slot": { "version": "1.0.5", @@ -4433,6 +4967,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/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", @@ -4478,6 +5027,24 @@ "node": ">=0.10.0" } }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/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-nan": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", @@ -4682,6 +5249,21 @@ "node": ">=0.10.0" } }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -4759,6 +5341,12 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/js-md4": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz", + "integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==", + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4807,6 +5395,28 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "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.npmjs.org/jszip/-/jszip-3.10.1.tgz", @@ -4855,6 +5465,27 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -4942,12 +5573,54 @@ "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/lodash.startcase": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", @@ -5177,6 +5850,18 @@ "node": ">= 0.6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -5210,6 +5895,15 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minimist-options": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", @@ -5242,6 +5936,12 @@ "node": ">= 8.0.0" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/mocha": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", @@ -5486,8 +6186,55 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/msnodesqlv8": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/msnodesqlv8/-/msnodesqlv8-5.1.1.tgz", + "integrity": "sha512-Q0uhQFOulfUrimhyIoPPSqyWtY4PaPu3Gg5wnpCTBY2ugTb5KOju0pPtNT+sikFBdyClvv6IL7hdj2Pn16Ssrw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "os": [ + "win32", + "linux", + "darwin" + ], + "dependencies": { + "node-abi": "^4.12.0", + "node-addon-api": "^8.4.0", + "prebuild-install": "^7.1.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/mssql": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/mssql/-/mssql-12.0.0.tgz", + "integrity": "sha512-FcDQ1Gwe4g3Mhw25R1Onr8N+jmqBTWE/pmtcgxYnAUSIf/vBQMvJfMnyMY8ruOICtBch5+Wgbcfd3REDQSlWpA==", + "license": "MIT", + "dependencies": { + "@tediousjs/connection-string": "^0.6.0", + "commander": "^11.0.0", + "debug": "^4.3.3", + "tarn": "^3.0.2", + "tedious": "^19.0.0" + }, + "bin": { + "mssql": "bin/mssql" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/mssql/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } }, "node_modules/nanoid": { "version": "3.3.3", @@ -5501,6 +6248,18 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/native-duplexpair": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/native-duplexpair/-/native-duplexpair-1.0.0.tgz", + "integrity": "sha512-E7QQoM+3jvNtlmyfqRZ0/U75VFgCls+fSkbml2MpgWkWyz3ox8Y58gNhfuziuQYGNNQAbFZJQck55LHCnCK6CA==", + "license": "MIT" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -5519,6 +6278,27 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "node_modules/node-abi": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.12.0.tgz", + "integrity": "sha512-bPSN9a/qIEiURzVVO/I7P/8oPeYTSl+vnvVZBXM/8XerKOgA3dMAIUjl+a+lz9VwTowwSKS3EMsgz/vWDXOkuQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.6.3" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, "node_modules/node-releases": { "version": "2.0.13", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", @@ -5617,11 +6397,28 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/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/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -6223,6 +7020,44 @@ "integrity": "sha512-VdlZoocy5lCP0c/t66xAfclglEapXPCIVhqqJRncYpvbCgImF0w67aPKfbqUMr72tO2k5q0TdTZwCLjPTI6C9g==", "dev": true }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prebuild-install/node_modules/node-abi": { + "version": "3.75.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz", + "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/preferred-pm": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/preferred-pm/-/preferred-pm-3.0.3.tgz", @@ -6327,7 +7162,6 @@ "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "dev": true, "engines": { "node": ">= 0.6.0" } @@ -6364,6 +7198,16 @@ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", @@ -6421,6 +7265,30 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -6490,7 +7358,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -6668,6 +7535,18 @@ "inherits": "^2.0.1" } }, + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -6713,7 +7592,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -6746,8 +7624,7 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/schema-utils": { "version": "3.3.0", @@ -6768,13 +7645,10 @@ } }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -6782,18 +7656,6 @@ "node": ">=10" } }, - "node_modules/semver/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/serialize-javascript": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", @@ -6887,6 +7749,51 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "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" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "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": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -7194,7 +8101,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "dependencies": { "safe-buffer": "~5.2.0" } @@ -7400,6 +8306,117 @@ "node": ">=6" } }, + "node_modules/tar-fs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", + "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/tar-stream/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "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.1.13" + } + }, + "node_modules/tarn": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz", + "integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/tedious": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/tedious/-/tedious-19.0.0.tgz", + "integrity": "sha512-nmxNBAT72mMVCIYp0Ts0Zzd5+LBQjoXlqigCrIjSo2OERSi04vr3EHq3qJxv/zgrSkg7si03SoIIfekTAadA7w==", + "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.17" + } + }, + "node_modules/tedious/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/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/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, "node_modules/term-size": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", @@ -7618,6 +8635,12 @@ "node": ">=8" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/tty-table": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/tty-table/-/tty-table-4.2.1.tgz", @@ -7787,6 +8810,18 @@ "node": ">=12" } }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -7877,10 +8912,11 @@ } }, "node_modules/typescript": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", - "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7904,6 +8940,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, "node_modules/universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", @@ -7967,8 +9009,16 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } }, "node_modules/validate-npm-package-license": { "version": "3.0.4", @@ -8337,8 +9387,22 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/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/xtend": { "version": "4.0.2", @@ -8357,12 +9421,6 @@ "node": ">=10" } }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", diff --git a/package.json b/package.json index 40899e0..618c7a4 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,8 @@ "database", "management", "sync", - "postgresql" + "postgresql", + "mssql" ], "contributes": { "viewsContainers": { @@ -307,7 +308,7 @@ "vscode:prepublish": "npm run vscode-desktop:publish && npm run vscode-web:publish", "vscode-desktop:publish": "npm run esbuild-base -- --minify", "vscode-web:publish": "npm run compile-web -- --mode production --devtool false", - "esbuild-base": "esbuild ./src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --format=cjs --platform=node", + "esbuild-base": "esbuild ./src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --format=cjs --platform=node --loader:.node=file", "esbuild": "npm run esbuild-base -- --sourcemap", "esbuild-watch": "npm run esbuild-base -- --sourcemap --watch", "compile": "tsc -p ./", @@ -338,6 +339,7 @@ "@types/glob": "^8.1.0", "@types/lodash.groupby": "^4.6.7", "@types/mocha": "^10.0.1", + "@types/mssql": "^9.1.7", "@types/node": "^20.2.5", "@types/pg": "^8.10.2", "@types/pg-promise": "^5.4.3", @@ -371,6 +373,8 @@ "diff": "^5.1.0", "fs-extra": "^11.1.1", "lodash.groupby": "^4.6.0", + "msnodesqlv8": "^5.1.1", + "mssql": "^12.0.0", "pg": "^8.11.1", "pg-iterator": "^0.3.0", "pg-promise": "^11.5.0" @@ -381,4 +385,4 @@ }, "publisher": "nguyenngoclong", "license": "MIT" -} \ No newline at end of file +} diff --git a/src/commands/actions/generatePlanAsync.ts b/src/commands/actions/generatePlanAsync.ts index 398fd19..ea5a77f 100644 --- a/src/commands/actions/generatePlanAsync.ts +++ b/src/commands/actions/generatePlanAsync.ts @@ -30,8 +30,9 @@ const writePlanOutput = async (filePath: string, lines: string[]): Promise => { - const { fileManager, tables } = options; + const { fileManager, tables, dbType } = options; // Read plan detail const sessionPath = fileManager.getSessionPath(); @@ -85,7 +86,7 @@ export const generatePlanAsync = async (options: { // Line start with '+' => insert if (line.startsWith('+')) { const values = JSON.parse(line.substring(1)); - const insertQuery = makeInsertQuery(table, planDetail, values); + const insertQuery = makeInsertQuery(table, planDetail, values, dbType); allPlanLines.push(insertQuery); insertPlanLines.push(insertQuery); planDetail.insert++; @@ -95,7 +96,7 @@ export const generatePlanAsync = async (options: { // Line start with '-' => remove if (line.startsWith('-')) { const values = JSON.parse(line.substring(1)); - const deleteQuery = makeDeleteQuery(table, planDetail, values); + const deleteQuery = makeDeleteQuery(table, planDetail, values, dbType); allPlanLines.push(deleteQuery); deletePlanLines.push(deleteQuery); planDetail.delete++; @@ -130,14 +131,14 @@ export const generatePlanAsync = async (options: { const currentRecord = JSON.parse(currentLine); switch (operation) { case '+': { - const insertQuery = makeInsertQuery(table, planDetail, currentRecord); + const insertQuery = makeInsertQuery(table, planDetail, currentRecord, dbType); allPlanLines.push(insertQuery); insertPlanLines.push(insertQuery); planDetail.insert++; break; } case '-': { - const deleteQuery = makeDeleteQuery(table, planDetail, currentRecord); + const deleteQuery = makeDeleteQuery(table, planDetail, currentRecord, dbType); allPlanLines.push(deleteQuery); deletePlanLines.push(deleteQuery); planDetail.delete++; @@ -160,13 +161,13 @@ export const generatePlanAsync = async (options: { const sameCurrentLine = groupItems[1]; if (rawCurrentLine.startsWith('+') && sameCurrentLine.startsWith('-')) { const record = JSON.parse(rawCurrentLine.substring(1)); - const updateQuery = makeUpdateQuery(table, planDetail, record); + const updateQuery = makeUpdateQuery(table, planDetail, record, dbType); allPlanLines.push(updateQuery); updatePlanLines.push(updateQuery); planDetail.update++; } else if (rawCurrentLine.startsWith('-') && sameCurrentLine.startsWith('+')) { const record = JSON.parse(sameCurrentLine.substring(1)); - const updateQuery = makeUpdateQuery(table, planDetail, record); + const updateQuery = makeUpdateQuery(table, planDetail, record, dbType); allPlanLines.push(updateQuery); updatePlanLines.push(updateQuery); planDetail.update++; @@ -192,17 +193,14 @@ export const generatePlanAsync = async (options: { await writePlanOutput(allPlanFilePath, allPlanLines); logger.info(`The ${allPlanFilePath} was successfully generated.`); - // Output all plan files const insertPlanFilePath = fileManager.getPlanOutputPath('insert'); await writePlanOutput(insertPlanFilePath, insertPlanLines); logger.info(`The ${insertPlanFilePath} was successfully generated.`); - // Output all plan files const updatePlanFilePath = fileManager.getPlanOutputPath('update'); await writePlanOutput(updatePlanFilePath, updatePlanLines); logger.info(`The ${updatePlanFilePath} was successfully generated.`); - // Output all plan files const deletePlanFilePath = fileManager.getPlanOutputPath('delete'); await writePlanOutput(deletePlanFilePath, deletePlanLines); logger.info(`The ${deletePlanFilePath} was successfully generated.`); diff --git a/src/commands/actions/generateSnapshotsAsync.ts b/src/commands/actions/generateSnapshotsAsync.ts index 191ec9d..6917019 100644 --- a/src/commands/actions/generateSnapshotsAsync.ts +++ b/src/commands/actions/generateSnapshotsAsync.ts @@ -1,48 +1,13 @@ import fs from 'fs-extra'; import { EOL } from 'node:os'; -import pg from 'pg'; -import { QueryIterablePool } from 'pg-iterator'; import { FileManager, SnapshotType } from '../../utils/fileManager'; import { logger } from '../../utils/logger'; -import { PatternConfig, PatternSession, TableConfig, getDatabaseInfo, getTabWidth } from '../../utils/utils'; - -const getPrimaryKeys = async (options: { pool: pg.Pool; table: TableConfig }): Promise => { - const { pool, table } = options; - const rawQuery = ` - SELECT c.column_name - FROM information_schema.table_constraints t - JOIN information_schema.constraint_column_usage c - ON c.constraint_name = t.constraint_name - WHERE t.constraint_type = 'PRIMARY KEY' AND c.table_name = '${table.name}'`; - const result = await pool.query(rawQuery); - const primaryKeys = []; - for (const row of result.rows) { - // eslint-disable-next-line @typescript-eslint/naming-convention - const { column_name } = row as { column_name: string }; - primaryKeys.push(column_name); - } - return primaryKeys; -}; - -const getColumnNames = async (options: { pool: pg.Pool; table: TableConfig }): Promise => { - const { pool, table } = options; - const rawQuery = ` - SELECT column_name - FROM information_schema.columns - WHERE table_name = '${table.name}' - ORDER BY ordinal_position`; - const result = await pool.query(rawQuery); - const tableColumns = []; - for await (const row of result.rows) { - // eslint-disable-next-line @typescript-eslint/naming-convention - const { column_name } = row as { column_name: string }; - tableColumns.push(column_name); - } - return tableColumns; -}; +import { PatternConfig, PatternSession, TableConfig, getTabWidth } from '../../utils/utils'; +import { createDatabaseProvider } from '../../utils/database/databaseProviderFactory'; +import { DatabaseProvider } from '../../utils/database/databaseProvider'; export type GenerateSnapshotOptions = { - pool: pg.Pool; + dbProvider: DatabaseProvider; fileManager: FileManager; snapshotType: SnapshotType; formatLine?: boolean; @@ -51,10 +16,12 @@ export type GenerateSnapshotOptions = { }; export const createSnapshotFiles = async (options: GenerateSnapshotOptions): Promise => { - const { pool, fileManager, tables, snapshotType, session, formatLine = false } = options; + const { dbProvider, fileManager, tables, snapshotType, session, formatLine = false } = options; const tab = getTabWidth(); - const qs = new QueryIterablePool(pool); + try { + await dbProvider.connect(); + for (let i = 0; i < tables.length; i++) { const table = tables[i]; @@ -70,14 +37,14 @@ export const createSnapshotFiles = async (options: GenerateSnapshotOptions): Pro // Get primary key let primaryKeys = table.primaryKeys; if (!primaryKeys || primaryKeys.length <= 0) { - primaryKeys = await getPrimaryKeys({ pool, table }); + primaryKeys = await dbProvider.getPrimaryKeys(table); } session.plan[table.name].primaryKeys = primaryKeys; // Get columns for table let tableColumns = table.columns; if (!tableColumns || tableColumns.length <= 0) { - tableColumns = await getColumnNames({ pool, table }); + tableColumns = await dbProvider.getColumnNames(table); } session.plan[table.name].columns = tableColumns; @@ -88,8 +55,14 @@ export const createSnapshotFiles = async (options: GenerateSnapshotOptions): Pro } // Generate select query - const selectColumns = tableColumns.length > 0 ? tableColumns.map((tc) => `"${tc}"`).join(', ') : '*'; - const selectQuery = `SELECT ${selectColumns} FROM ${table.name}`; + const selectColumns = + tableColumns.length > 0 ? tableColumns.map((tc) => dbProvider.escapeIdentifier(tc)).join(', ') : '*'; + + const tableIdentifier = table.schema + ? `${dbProvider.escapeIdentifier(table.schema)}.${dbProvider.escapeIdentifier(table.name)}` + : dbProvider.escapeIdentifier(table.name); + + const selectQuery = `SELECT ${selectColumns} FROM ${tableIdentifier}`; const where = table.where ? `WHERE ${table.where}` : ''; const orderBy = table.orderBy ? `ORDER BY ${table.orderBy}` : ''; const rawQuery = [selectQuery, where, orderBy].filter(Boolean).join(' '); @@ -99,19 +72,15 @@ export const createSnapshotFiles = async (options: GenerateSnapshotOptions): Pro await fs.remove(snapshotPath); await fs.ensureFile(snapshotPath); - // Get stream data from table + // Stream data from table to file logger.info(`Execute '${rawQuery}' at '${table.name}' table to create snapshot.`); - const rows = qs.query(rawQuery); - // Stream to snapshot file - logger.info(`Stream ${table.name} data to ${snapshotPath} ...`); - for await (const row of rows) { - // TODO: format json line to easy diff with JSONL - // const rowContent = formatLine ? JSON.stringify(row, null, tab) : JSON.stringify(row); + for await (const row of dbProvider.queryStream(rawQuery)) { const rowContent = JSON.stringify(row); fs.appendFileSync(snapshotPath, rowContent.concat(EOL), { encoding: 'utf-8' }); } - logger.info(`The ${table.name} data was successfully streamed .`); + + logger.info(`The ${table.name} data was successfully streamed.`); } // Save table detail @@ -119,19 +88,10 @@ export const createSnapshotFiles = async (options: GenerateSnapshotOptions): Pro await fs.outputJSON(sessionPath, session, { spaces: tab }); logger.info(`The ${sessionPath} was successfully created.`); } finally { - if (qs) { - qs.release(); - } + await dbProvider.disconnect(); } }; -/** - * Generate snapshot files - * - migrations//snapshots/original - * - migrations//snapshots/modified - * Create new migrate info file - * - migrations//session.json - */ export const generateSnapshotAsync = async (options: { fileManager: FileManager; selectedPattern: string; @@ -150,11 +110,11 @@ export const generateSnapshotAsync = async (options: { }; // Generate snapshot for original database (target apply) - const targetPool = new pg.Pool(target); - logger.info(`Generating target snapshot with db connection '${getDatabaseInfo(target)}'....`); + const targetProvider = createDatabaseProvider(target); + logger.info(`Generating target snapshot with db connection '${targetProvider.getDatabaseInfo()}'....`); await createSnapshotFiles({ fileManager, - pool: targetPool, + dbProvider: targetProvider, snapshotType: SnapshotType.original, formatLine, session, @@ -163,11 +123,11 @@ export const generateSnapshotAsync = async (options: { logger.info('The target snapshot files was successfully generated'); // Generate snapshot for modified database (source changed) - const sourcePool = new pg.Pool(source); - logger.info(`Generating source snapshot with db connection '${getDatabaseInfo(source)}'....`); + const sourceProvider = createDatabaseProvider(source); + logger.info(`Generating source snapshot with db connection '${sourceProvider.getDatabaseInfo()}'....`); await createSnapshotFiles({ fileManager, - pool: sourcePool, + dbProvider: sourceProvider, snapshotType: SnapshotType.modified, formatLine, session, diff --git a/src/commands/actions/tryConnectionAsync.ts b/src/commands/actions/tryConnectionAsync.ts index eb3bd20..efa036c 100644 --- a/src/commands/actions/tryConnectionAsync.ts +++ b/src/commands/actions/tryConnectionAsync.ts @@ -1,32 +1,17 @@ import { logger } from '../../utils/logger'; -import { getDatabaseInfo } from '../../utils/utils'; -import { Client, PoolConfig } from 'pg'; +import { DatabaseConfig } from '../../utils/database/databaseProvider'; +import { createDatabaseProvider } from '../../utils/database/databaseProviderFactory'; -/** - * Check database connection - */ -export const tryConnectionAsync = async (poolConfig: PoolConfig): Promise => { - let client: Client | undefined = undefined; +export const tryConnectionAsync = async (dbConfig: DatabaseConfig): Promise => { + const dbProvider = createDatabaseProvider(dbConfig); try { - client = new Client({ - host: poolConfig.host, - user: poolConfig.user, - password: poolConfig.password, - port: poolConfig.port, - database: poolConfig.database || 'postgres' - }); - await client.connect(); - - // Get current version - logger.info(`Connecting to the '${getDatabaseInfo(poolConfig)}' successful.`); + await dbProvider.connect(); + logger.info(`Connecting to the '${dbProvider.getDatabaseInfo()}' successful.`); return true; } catch (err) { - logger.info(`Could not connect to the '${getDatabaseInfo(poolConfig)}'.`, err); + logger.info(`Could not connect to the '${dbProvider.getDatabaseInfo()}'.`, err); return false; } finally { - if (client) { - await client.end(); - client = undefined; - } + await dbProvider.disconnect(); } }; diff --git a/src/commands/analyzeDataAsync.ts b/src/commands/analyzeDataAsync.ts index 19f9950..e937479 100644 --- a/src/commands/analyzeDataAsync.ts +++ b/src/commands/analyzeDataAsync.ts @@ -136,6 +136,11 @@ export const analyzeDataAsync = async (selectedPattern: string): Promise = showIncorrectConfigWarning(selectedPattern); return; } + // Determine dbType from pattern config (default to 'postgres') + const dbType: 'postgres' | 'mssql' = + (pattern?.source?.type as 'postgres' | 'mssql') || + (pattern?.target?.type as 'postgres' | 'mssql') || + 'postgres'; // Show output panel const config = workspace.getConfiguration(APP_ID) as ExtensionConfiguration; @@ -152,14 +157,14 @@ export const analyzeDataAsync = async (selectedPattern: string): Promise = }, async (progress) => { // Show password input if not defined - if (pattern.source.password === undefined) { + if (pattern.source.password === undefined && pattern.source.connectionString === undefined) { const inputPassword = await showInputPassword('source', pattern.source); if (typeof inputPassword === 'undefined') { return false; } pattern.source.password = inputPassword; } - if (pattern.target.password === undefined) { + if (pattern.target.password === undefined && pattern.target.connectionString === undefined) { const inputPassword = await showInputPassword('target', pattern.target); if (typeof inputPassword === 'undefined') { return false; @@ -180,7 +185,7 @@ export const analyzeDataAsync = async (selectedPattern: string): Promise = } // Store the password if connect successful - store.sourcePassword = pattern.source.password.toString(); + store.sourcePassword = pattern.source?.password?.toString(); // Check the target database connection config if (config.checkDatabaseConnection) { @@ -195,7 +200,7 @@ export const analyzeDataAsync = async (selectedPattern: string): Promise = } // Store the password if connect successful - store.targetPassword = pattern.target.password.toString(); + store.targetPassword = pattern.target?.password?.toString(); // Generate snapshot files (read database and output to file) showProgressReport(progress, 'Generating all snapshot files...'); @@ -221,7 +226,7 @@ export const analyzeDataAsync = async (selectedPattern: string): Promise = // Generate plan files (from diff files, generate to plan) showProgressReport(progress, 'Generating plan files...'); - const isGeneratePlanSuccess = await generatePlanAsync({ fileManager, tables }); + const isGeneratePlanSuccess = await generatePlanAsync({ fileManager, tables, dbType }); if (!isGeneratePlanSuccess) { showProgressWarn('Failed to generate plan files!'); return; diff --git a/src/commands/analyzeDataOnImportAsync.ts b/src/commands/analyzeDataOnImportAsync.ts index 618262f..66ec0bf 100644 --- a/src/commands/analyzeDataOnImportAsync.ts +++ b/src/commands/analyzeDataOnImportAsync.ts @@ -141,6 +141,11 @@ export const analyzeDataOnImportAsync = async (): Promise => { showIncorrectConfigWarning(selectedPattern); return; } + // Determine dbType from pattern config (default to 'postgres') + const dbType: 'postgres' | 'mssql' = + (pattern?.source?.type as 'postgres' | 'mssql') || + (pattern?.target?.type as 'postgres' | 'mssql') || + 'postgres'; // Show output panel const config = workspace.getConfiguration(APP_ID) as ExtensionConfiguration; @@ -167,7 +172,7 @@ export const analyzeDataOnImportAsync = async (): Promise => { // Generate plan files (from diff files, generate to plan) showProgressReport(progress, 'Generating plan files...'); - const isGeneratePlanSuccess = await generatePlanAsync({ fileManager, tables }); + const isGeneratePlanSuccess = await generatePlanAsync({ fileManager, tables, dbType }); if (!isGeneratePlanSuccess) { showProgressWarn('Failed to generate plan files!'); return; diff --git a/src/commands/analyzeDataOnRefreshAsync.ts b/src/commands/analyzeDataOnRefreshAsync.ts index e40b14a..6395d57 100644 --- a/src/commands/analyzeDataOnRefreshAsync.ts +++ b/src/commands/analyzeDataOnRefreshAsync.ts @@ -42,7 +42,14 @@ export const analyzeDataOnRefreshAsync = async (selectedPattern: string, migrate // Init configuration const { patterns, verbose } = configContent; + const pattern = patterns[selectedPattern]; + + const dbType: 'postgres' | 'mssql' = + (pattern?.source?.type as 'postgres' | 'mssql') || + (pattern?.target?.type as 'postgres' | 'mssql') || + 'postgres'; + const tables = pattern?.diff?.tables; if (!tables || tables.length <= 0) { showIncorrectConfigWarning(); @@ -81,7 +88,7 @@ export const analyzeDataOnRefreshAsync = async (selectedPattern: string, migrate // Generate plan files (from diff files, generate to plan) showProgressReport(progress, 'Generating plan files...'); - const isGeneratePlanSuccess = await generatePlanAsync({ fileManager, tables }); + const isGeneratePlanSuccess = await generatePlanAsync({ fileManager, tables, dbType }); if (!isGeneratePlanSuccess) { showProgressWarn('Failed to generate plan files!'); return; diff --git a/src/commands/migrateDataAsync.ts b/src/commands/migrateDataAsync.ts index 1b78526..9952f05 100644 --- a/src/commands/migrateDataAsync.ts +++ b/src/commands/migrateDataAsync.ts @@ -1,6 +1,9 @@ import fs from 'fs-extra'; import { EOL } from 'node:os'; import pg, { PoolConfig } from 'pg'; +import * as mssql from 'mssql'; +import * as mssqlV8 from 'mssql/msnodesqlv8'; + import { ProgressLocation, QuickPickItem, window, workspace } from 'vscode'; import { ExtensionConfiguration } from '../extension'; import { ConfigManager } from '../utils/configManager'; @@ -47,7 +50,540 @@ const handleWarningQueries = ( } }; -export const executeMigrate = async (options: { +// MSSQL helpers for IDENTITY_INSERT handling +const isMssqlIdentityInsertError = (err: unknown): boolean => { + const anyErr = err as any; + const number = anyErr?.number as number | undefined; + const message = (anyErr?.message as string | undefined)?.toLowerCase() || ''; + return number === 544 || message.includes('identity_insert is set to off'); +}; + +const extractInsertTargetTable = (sql: string): string | undefined => { + // Captures object name after INSERT INTO up to first space or '(' + // Works with [dbo].[Table], dbo.Table, [Table], Table + const m = /^\s*insert\s+into\s+([^\s(]+)\s*/i.exec(sql); + return m?.[1]; +}; + +// Split SQL script into executable statements, preserving newlines inside literals. +// - Uses ';' as a statement terminator (outside strings/comments) +// - For MSSQL also treats a standalone 'GO' line as a batch separator. +// - Understands: single-quoted strings ('' escape), "quoted identifiers", [bracket identifiers], +// line comments (-- ...), block comments (/* ... */), and Postgres dollar-quoted strings ($tag$ ... $tag$) +const splitSqlStatements = (input: string, dialect: 'mssql' | 'postgres'): string[] => { + const sql = input.replace(/\r\n/g, '\n'); // normalize + const out: string[] = []; + let buf: string[] = []; + + let inSingle = false; // 'string' + let inDoubleIdent = false; // "identifier" + let inBracketIdent = false; // [identifier] (MSSQL) + let inLineComment = false; // -- ... + let inBlockComment = false; // /* ... */ + let dollarTag: string | null = null; // $tag$ + + const n = sql.length; + let i = 0; + let atLineStart = true; + + const flush = () => { + const stmt = buf.join('').trim(); + if (stmt.length > 0) out.push(stmt); + buf = []; + }; + + while (i < n) { + const ch = sql[i]; + const next = i + 1 < n ? sql[i + 1] : ''; + + // Detect standalone GO (MSSQL) at start of a line when not inside any literal/comment + if ( + dialect === 'mssql' && + atLineStart && + !inSingle && + !inDoubleIdent && + !inBracketIdent && + !inLineComment && + !inBlockComment && + dollarTag === null + ) { + // Peek current line + let j = i; + while (j < n && sql[j] !== '\n') j++; + const line = sql.slice(i, j).trim(); + if (line.toUpperCase() === 'GO') { + // End of previous batch + flush(); + // Skip this GO line and its newline + i = j < n && sql[j] === '\n' ? j + 1 : j; + atLineStart = true; + continue; + } + } + + // Handle end of line for -- comments + if (inLineComment) { + buf.push(ch); + if (ch === '\n') { + inLineComment = false; + atLineStart = true; + } else if (!/\s/.test(ch)) { + atLineStart = false; + } + i++; + continue; + } + + // Handle block comment + if (inBlockComment) { + buf.push(ch); + if (ch === '*' && next === '/') { + buf.push(next); + i += 2; + inBlockComment = false; + continue; + } + if (ch === '\n') atLineStart = true; + else if (!/\s/.test(ch)) atLineStart = false; + i++; + continue; + } + + // Handle dollar-quoted string (Postgres) + if (dollarTag) { + // End tag matches exactly + if (sql.startsWith(dollarTag, i)) { + buf.push(dollarTag); + i += dollarTag.length; + dollarTag = null; + continue; + } + // Copy as-is + if (ch === '\n') atLineStart = true; + else if (!/\s/.test(ch)) atLineStart = false; + buf.push(ch); + i++; + continue; + } + + // Handle single-quoted string + if (inSingle) { + buf.push(ch); + if (ch === "'") { + // Escaped '' + if (next === "'") { + buf.push(next); + i += 2; + continue; + } + inSingle = false; + } + if (ch === '\n') atLineStart = true; + else if (!/\s/.test(ch)) atLineStart = false; + i++; + continue; + } + + // Handle quoted identifier " + if (inDoubleIdent) { + buf.push(ch); + if (ch === '"') inDoubleIdent = false; + if (ch === '\n') atLineStart = true; + else if (!/\s/.test(ch)) atLineStart = false; + i++; + continue; + } + + // Handle bracketed identifier [ + if (inBracketIdent) { + buf.push(ch); + if (ch === ']') inBracketIdent = false; + if (ch === '\n') atLineStart = true; + else if (!/\s/.test(ch)) atLineStart = false; + i++; + continue; + } + + // Outside any literal/comment: + // Start of comments + if (ch === '-' && next === '-') { + buf.push(ch, next); + i += 2; + inLineComment = true; + continue; + } + if (ch === '/' && next === '*') { + buf.push(ch, next); + i += 2; + inBlockComment = true; + continue; + } + + // Start of literals/identifiers + if (ch === "'") { + buf.push(ch); + inSingle = true; + i++; + continue; + } + if (ch === '"') { + buf.push(ch); + inDoubleIdent = true; + i++; + continue; + } + if (ch === '[') { + buf.push(ch); + inBracketIdent = true; + i++; + continue; + } + + // Postgres dollar-quote start: $tag$ + if (dialect === 'postgres' && ch === '$') { + let j = i + 1; + while (j < n && /[A-Za-z0-9_]/.test(sql[j])) j++; + if (j < n && sql[j] === '$') { + const tag = sql.slice(i, j + 1); // includes both '$' + buf.push(tag); + i = j + 1; + dollarTag = tag; + continue; + } + } + + // Statement terminator + if (ch === ';') { + // push ';' too, then flush (optional to include it) + buf.push(ch); + flush(); + i++; + atLineStart = true; + continue; + } + + // Track start-of-line + if (ch === '\n') { + atLineStart = true; + } else if (!/\s/.test(ch)) { + atLineStart = false; + } + + // Default: copy char + buf.push(ch); + i++; + } + + // Remaining buffer + flush(); + return out; +}; + +// Normalize unquoted boolean literals to BIT (1/0) for SQL Server. +// Skips single-quoted strings, bracketed identifiers, line and block comments. +export const normalizeBooleanLiteralsForMssql = (sql: string): string => { + let out = ''; + let i = 0; + const n = sql.length; + let inSingle = false; + let inBracketIdent = false; + let inLineComment = false; + let inBlockComment = false; + + const isWordChar = (c: string) => /[A-Za-z0-9_]/.test(c); + + while (i < n) { + const ch = sql[i]; + const next = i + 1 < n ? sql[i + 1] : ''; + + // End line comment + if (inLineComment) { + out += ch; + if (ch === '\n') inLineComment = false; + i++; + continue; + } + + // End block comment + if (inBlockComment) { + out += ch; + if (ch === '*' && next === '/') { + out += next; + i += 2; + inBlockComment = false; + } else { + i++; + } + continue; + } + + // Inside quoted string + if (inSingle) { + out += ch; + if (ch === "'") { + // Escaped '' stays in string + if (next === "'") { + out += next; + i += 2; + continue; + } + inSingle = false; + } + i++; + continue; + } + + // Inside bracketed identifier + if (inBracketIdent) { + out += ch; + if (ch === ']') inBracketIdent = false; + i++; + continue; + } + + // Detect starts of comments/strings/identifiers + if (ch === '-' && next === '-') { + out += ch + next; + i += 2; + inLineComment = true; + continue; + } + if (ch === '/' && next === '*') { + out += ch + next; + i += 2; + inBlockComment = true; + continue; + } + if (ch === "'") { + out += ch; + i++; + inSingle = true; + continue; + } + if (ch === '[') { + out += ch; + i++; + inBracketIdent = true; + continue; + } + + // Replace standalone true/false + if (/[A-Za-z_]/.test(ch)) { + let j = i; + while (j < n && isWordChar(sql[j])) j++; + const word = sql.slice(i, j); + const prev = i > 0 ? sql[i - 1] : ''; + const nextCh = j < n ? sql[j] : ''; + const isBoundaryLeft = !isWordChar(prev); + const isBoundaryRight = !isWordChar(nextCh); + if (isBoundaryLeft && isBoundaryRight) { + const lw = word.toLowerCase(); + if (lw === 'true') { + out += '1'; + i = j; + continue; + } + if (lw === 'false') { + out += '0'; + i = j; + continue; + } + } + // Not a boolean literal; copy as-is + out += word; + i = j; + continue; + } + + // Default: copy char + out += ch; + i++; + } + + return out; +}; + +// Add a new executeMigrateMssql function +const executeMigrateMssql = async (options: { + poolConfig: any; + migrateConfig?: MigrateConfig; + migrateUpLines: string[]; +}): Promise<{ insert: number; update: number; delete: number; error?: unknown }> => { + const { poolConfig, migrateConfig, migrateUpLines } = options; + + let driver: typeof mssql | typeof mssqlV8 = mssql; + let pool: mssql.ConnectionPool | mssqlV8.ConnectionPool | undefined = undefined; + let transaction: mssql.Transaction | mssqlV8.Transaction | undefined = undefined; + + let rowAffected = { + insert: 0, + update: 0, + delete: 0 + }; + + try { + if (poolConfig.connectionString) { + const parsed = mssql.ConnectionPool.parseConnectionString(poolConfig.connectionString); + const hasUser = !!parsed.user; + const hasPassword = !!parsed.password; + + if (!hasUser || !hasPassword) { + parsed.driver = 'msnodesqlv8'; + parsed.options = { + ...parsed.options, + trustServerCertificate: true, + trustedConnection: true + }; + pool = await new mssqlV8.ConnectionPool(parsed).connect(); + driver = mssqlV8; + } else { + pool = await new mssql.ConnectionPool(poolConfig.connectionString).connect(); + driver = mssql; + } + } else { + pool = await new mssql.ConnectionPool({ + server: poolConfig.host, + port: poolConfig.port, + user: poolConfig.user, + password: poolConfig.password, + database: poolConfig.database, + domain: poolConfig.domain, + options: { + trustServerCertificate: poolConfig.mssqlOptions?.trustServerCertificate ?? true, + encrypt: poolConfig.mssqlOptions?.encrypt ?? true + } + }).connect(); + driver = mssql; + } + + transaction = new driver.Transaction(pool); + await transaction.begin(); + + const request = new driver.Request(transaction); + + const noAffectedQueries: { rawQuery: string; affected: number }[] = []; + const multiAffectedQueries: { rawQuery: string; affected: number }[] = []; + + // Group inserts by table for IDENTITY_INSERT optimization + const tableInsertMap = new Map(); + migrateUpLines.forEach((line, idx) => { + const trimmed = line.trim(); + if (isInsertQuery(trimmed)) { + const table = extractInsertTargetTable(trimmed); + if (table) { + if (!tableInsertMap.has(table)) tableInsertMap.set(table, []); + tableInsertMap.get(table)!.push(idx); + } + } + }); + + // Track which tables have IDENTITY_INSERT ON + const identityOnTables = new Set(); + + for (let i = 0; i < migrateUpLines.length; i++) { + const rawQuery = migrateUpLines[i].trim(); + const isInsert = isInsertQuery(rawQuery); + const isUpdate = isUpdateQuery(rawQuery); + const isDelete = isDeleteQuery(rawQuery); + + const isBlankLine = rawQuery === ''; + const isCommentLine = rawQuery.startsWith('--'); + if (isBlankLine || isCommentLine) { + continue; + } + + const preparedQuery = normalizeBooleanLiteralsForMssql(rawQuery); + + let result: any; + const table: string | undefined = isInsert ? extractInsertTargetTable(preparedQuery) : undefined; + + try { + logger.info(`Execute '${rawQuery}' to migrate...`); + result = await request.query(preparedQuery); + } catch (err) { + if (isInsert && isMssqlIdentityInsertError(err)) { + if (!table) throw err; + + if (!identityOnTables.has(table)) { + logger.info(`Enabling IDENTITY_INSERT ON for ${table}...`); + try { + await request.query(`SET IDENTITY_INSERT ${table} ON;`); + } catch (err: any) { + const msg = err?.message?.toLowerCase() || ''; + if (!msg.includes('identity_insert is already on for table')) { + throw err; + } else { + logger.warn(`IDENTITY_INSERT was already ON for ${table}.`); + } + } + identityOnTables.add(table); + } + result = await request.query(preparedQuery); + } else { + throw err; + } + } + + const rowCount = Array.isArray(result.rowsAffected) + ? result.rowsAffected[0] + : (result.rowsAffected as number); + + if (isInsert) { + rowAffected.insert++; + } + if (isUpdate) { + rowAffected.update++; + } + if (isDelete) { + rowAffected.delete++; + } + + if ((rowCount ?? 0) <= 0) { + noAffectedQueries.push({ rawQuery, affected: rowCount ?? 0 }); + } + if ((rowCount ?? 0) >= 2) { + multiAffectedQueries.push({ rawQuery, affected: rowCount ?? 0 }); + } + + logger.info(`The '${rawQuery}' was successful migrated!`); + + // If this is the last insert for the table, turn IDENTITY_INSERT OFF + if (isInsert && table) { + const insertIndexes = tableInsertMap.get(table); + if (insertIndexes && insertIndexes[insertIndexes.length - 1] === i && identityOnTables.has(table)) { + logger.info(`Disabling IDENTITY_INSERT ON for ${table}...`); + await request.query(`SET IDENTITY_INSERT ${table} OFF;`); + identityOnTables.delete(table); + } + } + } + + if (noAffectedQueries.length > 0) { + const message = `The query was no affected to database:`; + handleWarningQueries(noAffectedQueries, migrateConfig?.noRowAffected || 'throw', message); + } + if (multiAffectedQueries.length > 0) { + const message = `The query was multiple affected to database:`; + handleWarningQueries(multiAffectedQueries, migrateConfig?.multipleRowAffected || 'throw', message); + } + + await transaction.commit(); + return rowAffected; + } catch (error) { + if (transaction) { + logger.error(`Failed to migrate data. Starting rollback data...`, error); + await transaction.rollback(); + logger.info(`The data was successful rollback!`); + } + return { ...rowAffected, error }; + } finally { + if (pool) { + await pool.close(); + } + } +}; + +export const executeMigratePostgres = async (options: { poolConfig: PoolConfig; migrateConfig?: MigrateConfig; migrateUpLines: string[]; @@ -240,11 +776,29 @@ export const migrateDataAsync = async (migrateFilePath: string, systemInfo?: Sys // Execute migrate showProgressReport(progress, `Starting migrate data...`); logger.info(`Migrate to target with db connection '${getDatabaseInfo(pattern.target)}'....`); - const rowAffected = await executeMigrate({ - migrateUpLines, - migrateConfig: pattern.migrate, - poolConfig: pattern.target - }); + const dbType = pattern.target.type; + // Parse statements per dialect + const migrateUpLines = splitSqlStatements(migrateUpContent, dbType === 'mssql' ? 'mssql' : 'postgres'); + if (migrateUpLines.length <= 0) { + window.showWarningMessage(`The migrate file ${migrateFilePath} does not contain any statements.`); + return false; + } + + let rowAffected; + if (dbType === 'mssql') { + rowAffected = await executeMigrateMssql({ + migrateUpLines, + migrateConfig: pattern.migrate, + poolConfig: pattern.target + }); + } else { + rowAffected = await executeMigratePostgres({ + migrateUpLines, + migrateConfig: pattern.migrate, + poolConfig: pattern.target + }); + } + if (rowAffected.error) { showErrorMessageWithDetail( `Failed to migrate data. The data will be rollback successful!`, @@ -252,6 +806,7 @@ export const migrateDataAsync = async (migrateFilePath: string, systemInfo?: Sys ); return false; } + showProgressReport( progress, `The data was successfully migrated with ${ diff --git a/src/explorer/info-provider.ts b/src/explorer/info-provider.ts index 6da65c5..056047b 100644 --- a/src/explorer/info-provider.ts +++ b/src/explorer/info-provider.ts @@ -92,19 +92,44 @@ export class InfoProvider implements TreeDataProvider { db: PoolConfig, tables: TableConfig[] ): InfoTreeItem => { + const parsedConnectionString = db?.connectionString + ? Object.fromEntries( + db.connectionString + .split(';') + .filter(Boolean) + .map((pair) => { + const [key, ...rest] = pair.split('='); + return [key.trim(), rest.join('=').trim()]; + }) + ) + : {}; + + const database = db.database || parsedConnectionString.Database; + const host = db.host || parsedConnectionString.Server; + const port = db.port || parsedConnectionString.Port; + const user = + db.user || + parsedConnectionString.User || + parsedConnectionString['User Id'] || + parsedConnectionString['UserID']; return { // Built-in field - id: `${parent.id}/${from}/${db.database}`, - label: `${db.database}`, + id: `${parent.id}/${from}/${database || 'unknown'}`, + label: database || 'Unknown Database', description: `from ${from}, ${tables.length} table(s) defined`, - tooltip: [`Database: ${db.database}`, `Host: ${db.host}`, `Port: ${db.port}`, `User: ${db.user}`].join(EOL), + tooltip: [ + `Database: ${database || ''}`, + `Host: ${host || ''}`, + `Port: ${port || ''}`, + `User: ${user || ''}` + ].join(EOL), contextValue: 'database-context', iconPath: new ThemeIcon('database'), collapsibleState: tables.length > 0 ? TreeItemCollapsibleState.Collapsed : undefined, // Addition type: 'database', - databaseName: db.database, + databaseName: database, parent }; }; diff --git a/src/templates/configuration.ts b/src/templates/configuration.ts index 9cdc2b6..9096993 100644 --- a/src/templates/configuration.ts +++ b/src/templates/configuration.ts @@ -13,6 +13,7 @@ export const configTemplate: DataSyncConfig = { patterns: { patternName: { source: { + type: 'postgres', database: '', host: '', port: 5432, @@ -20,6 +21,7 @@ export const configTemplate: DataSyncConfig = { password: '' }, target: { + type: 'postgres', database: '', host: '', port: 5432, diff --git a/src/utils/database/databaseProvider.ts b/src/utils/database/databaseProvider.ts new file mode 100644 index 0000000..33221c6 --- /dev/null +++ b/src/utils/database/databaseProvider.ts @@ -0,0 +1,32 @@ +import { TableConfig } from '../utils'; +import { QueryResultRow } from '../types'; +import { PoolConfig } from 'pg'; + +export interface DatabaseConfig extends PoolConfig { + type: 'postgres' | 'mssql'; + host: string; + port: number; + user: string; + password: string; + database: string; + connectionString?: string; + domain?: string; + mssqlOptions?: { + trustServerCertificate?: boolean; + encrypt?: boolean; + }; +} + +export interface DatabaseProvider { + connect(): Promise; + disconnect(): Promise; + beginTransaction(): Promise; + commitTransaction(): Promise; + rollbackTransaction(): Promise; + query(sql: string): Promise<{ rows: QueryResultRow[]; rowCount: number }>; + queryStream(sql: string): AsyncIterable; + getPrimaryKeys(table: TableConfig): Promise; + getColumnNames(table: TableConfig): Promise; + escapeIdentifier(identifier: string): string; + getDatabaseInfo(): string; +} diff --git a/src/utils/database/databaseProviderFactory.ts b/src/utils/database/databaseProviderFactory.ts new file mode 100644 index 0000000..d9a273e --- /dev/null +++ b/src/utils/database/databaseProviderFactory.ts @@ -0,0 +1,14 @@ +import { DatabaseProvider, DatabaseConfig } from './databaseProvider'; +import { PostgresProvider } from './postgresProvider'; +import { MssqlProvider } from './mssqlProvider'; + +export function createDatabaseProvider(config: DatabaseConfig): DatabaseProvider { + switch (config.type) { + case 'postgres': + return new PostgresProvider(config); + case 'mssql': + return new MssqlProvider(config); + default: + throw new Error(`Unsupported database type: ${config.type}`); + } +} diff --git a/src/utils/database/mssqlProvider.ts b/src/utils/database/mssqlProvider.ts new file mode 100644 index 0000000..ab8824f --- /dev/null +++ b/src/utils/database/mssqlProvider.ts @@ -0,0 +1,173 @@ +import * as mssqlV8 from 'mssql/msnodesqlv8'; +import * as mssql from 'mssql'; + +import { DatabaseProvider, DatabaseConfig } from './databaseProvider'; +import { TableConfig } from '../utils'; +import { QueryResultRow } from '../types'; +import { validateIdentifier } from './validateIdentifier'; + +export class MssqlProvider implements DatabaseProvider { + private pool: mssql.ConnectionPool | null = null; + private transaction: mssql.Transaction | null = null; + private config: DatabaseConfig; + private driver: typeof mssql | typeof mssqlV8 = mssql; + + constructor(config: DatabaseConfig) { + this.config = config; + } + + async connect(): Promise { + if (this.config.connectionString) { + const parsed = mssql.ConnectionPool.parseConnectionString(this.config.connectionString); + const hasUser = !!parsed.user; + const hasPassword = !!parsed.password; + // Windows Auth is only supported with 'msnodesqlv8' driver + if (!hasUser || !hasPassword) { + parsed.driver = 'msnodesqlv8'; + parsed.options = { + ...parsed.options, + trustServerCertificate: true, + trustedConnection: true + }; + this.pool = await new mssqlV8.ConnectionPool(parsed).connect(); + this.driver = mssqlV8; + } else { + this.pool = await new mssql.ConnectionPool(this.config.connectionString).connect(); + this.driver = mssql; + } + } else { + this.pool = await new mssql.ConnectionPool({ + server: this.config.host, + port: this.config.port, + user: this.config.user, + password: this.config.password, + database: this.config.database, + domain: this.config.domain, + options: { + trustServerCertificate: this.config.mssqlOptions?.trustServerCertificate ?? true, + encrypt: this.config.mssqlOptions?.encrypt ?? true + } + }).connect(); + this.driver = mssql; + } + } + + async disconnect(): Promise { + if (this.transaction) { + await this.transaction.rollback(); + this.transaction = null; + } + if (this.pool) { + await this.pool.close(); + this.pool = null; + } + } + + async beginTransaction(): Promise { + if (!this.pool) throw new Error('Not connected'); + this.transaction = new this.driver.Transaction(this.pool); // + await this.transaction.begin(); + } + + async commitTransaction(): Promise { + if (!this.transaction) throw new Error('No active transaction'); + await this.transaction.commit(); + this.transaction = null; + } + + async rollbackTransaction(): Promise { + if (!this.transaction) throw new Error('No active transaction'); + await this.transaction.rollback(); + this.transaction = null; + } + + async query(sql: string): Promise<{ rows: QueryResultRow[]; rowCount: number }> { + if (!this.pool) throw new Error('Not connected'); + const request = this.transaction + ? new this.driver.Request(this.transaction) + : new this.driver.Request(this.pool); + + const result = await request.query(sql); + return { + rows: result.recordset, + rowCount: result.rowsAffected.reduce((sum, count) => sum + count, 0) + }; + } + + async *queryStream(sql: string): AsyncIterable { + if (!this.pool) throw new Error('Not connected'); + const request = this.transaction + ? new this.driver.Request(this.transaction) + : new this.driver.Request(this.pool); // + + const result = await request.query(sql); + for (const row of result.recordset) { + yield row; + } + } + + async getPrimaryKeys(table: TableConfig): Promise { + if (!this.pool) { + throw new Error('Not connected'); + } + const tableSchema = table.schema || 'dbo'; // Default schema in SQL Server + + // Validate identifiers to prevent injection + validateIdentifier(table.name); + validateIdentifier(tableSchema); + + const sql = ` + SELECT column_name + FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE + WHERE OBJECTPROPERTY(OBJECT_ID(constraint_name), 'IsPrimaryKey') = 1 + AND table_name = @tableName + AND table_schema = @tableSchema`; + const request = this.transaction + ? new this.driver.Request(this.transaction) + : new this.driver.Request(this.pool); + + request.input('tableName', table.name); + request.input('tableSchema', tableSchema); + + const result = await request.query(sql); + return result.recordset.map((row) => row.column_name); + } + + async getColumnNames(table: TableConfig): Promise { + if (!this.pool) { + throw new Error('Not connected'); + } + const tableSchema = table.schema || 'dbo'; + validateIdentifier(table.name); + validateIdentifier(tableSchema); + + const sql = ` + SELECT column_name + FROM INFORMATION_SCHEMA.COLUMNS + WHERE table_name = @tableName + AND table_schema = @tableSchema + ORDER BY ordinal_position`; + + const request = this.transaction + ? new this.driver.Request(this.transaction) + : new this.driver.Request(this.pool); + + request.input('tableName', table.name); + request.input('tableSchema', tableSchema); + + const result = await request.query(sql); + return result.recordset.map((row) => row.column_name); + } + + escapeIdentifier(identifier: string): string { + return `[${identifier}]`; + } + + getDatabaseInfo(): string { + if (this.config.connectionString) { + return `mssql://${this.config.connectionString}`; + } + const { host, port, database, user } = this.config; + return `mssql://${user}@${host}:${port}/${database}`; + } +} diff --git a/src/utils/database/postgresProvider.ts b/src/utils/database/postgresProvider.ts new file mode 100644 index 0000000..b2d4452 --- /dev/null +++ b/src/utils/database/postgresProvider.ts @@ -0,0 +1,111 @@ +import pg from 'pg'; +import { QueryIterablePool } from 'pg-iterator'; +import { DatabaseProvider, DatabaseConfig } from './databaseProvider'; +import { TableConfig } from '../utils'; +import { QueryResultRow } from '../types'; +import { validateIdentifier } from './validateIdentifier'; + +export class PostgresProvider implements DatabaseProvider { + private pool: pg.Pool | null = null; + private client: pg.PoolClient | null = null; + private config: DatabaseConfig; + + constructor(config: DatabaseConfig) { + this.config = config; + } + + async connect(): Promise { + this.pool = new pg.Pool({ + host: this.config.host, + port: this.config.port, + user: this.config.user, + password: this.config.password, + database: this.config.database, + connectionString: this.config.connectionString + }); + this.client = await this.pool.connect(); + } + + async disconnect(): Promise { + if (this.client) { + this.client.release(); + this.client = null; + } + if (this.pool) { + await this.pool.end(); + this.pool = null; + } + } + + async beginTransaction(): Promise { + if (!this.client) throw new Error('Not connected'); + await this.client.query('BEGIN'); + } + + async commitTransaction(): Promise { + if (!this.client) throw new Error('Not connected'); + await this.client.query('COMMIT'); + } + + async rollbackTransaction(): Promise { + if (!this.client) throw new Error('Not connected'); + await this.client.query('ROLLBACK'); + } + + async query(sql: string): Promise<{ rows: QueryResultRow[]; rowCount: number }> { + if (!this.client) throw new Error('Not connected'); + const result = await this.client.query(sql); + return { + rows: result.rows, + rowCount: result.rowCount || 0 + }; + } + + async *queryStream(sql: string): AsyncIterable { + if (!this.pool) throw new Error('Not connected'); + const qs = new QueryIterablePool(this.pool); + try { + const rows = qs.query(sql); + for await (const row of rows) { + yield row as QueryResultRow; + } + } finally { + qs.release(); + } + } + + async getPrimaryKeys(table: TableConfig): Promise { + if (!this.pool) throw new Error('Not connected'); + validateIdentifier(table.name); + const sql = ` + SELECT c.column_name + FROM information_schema.table_constraints t + JOIN information_schema.constraint_column_usage c + ON c.constraint_name = t.constraint_name + WHERE t.constraint_type = 'PRIMARY KEY' + AND c.table_name = $1`; + const result = await this.pool.query(sql, [table.name]); + return result.rows.map((row) => row.column_name); + } + + async getColumnNames(table: TableConfig): Promise { + if (!this.pool) throw new Error('Not connected'); + validateIdentifier(table.name); + const sql = ` + SELECT column_name + FROM information_schema.columns + WHERE table_name = $1 + ORDER BY ordinal_position`; + const result = await this.pool.query(sql, [table.name]); + return result.rows.map((row) => row.column_name); + } + + escapeIdentifier(identifier: string): string { + return `"${identifier}"`; + } + + getDatabaseInfo(): string { + const { host, port, database, user } = this.config; + return `postgres://${user}@${host}:${port}/${database}`; + } +} diff --git a/src/utils/database/validateIdentifier.ts b/src/utils/database/validateIdentifier.ts new file mode 100644 index 0000000..7b5f7c7 --- /dev/null +++ b/src/utils/database/validateIdentifier.ts @@ -0,0 +1,5 @@ +export function validateIdentifier(identifier: string): void { + if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) { + throw new Error(`Invalid SQL identifier: ${identifier}`); + } +} diff --git a/src/utils/postgresProvider.ts b/src/utils/postgresProvider.ts new file mode 100644 index 0000000..a3f31c0 --- /dev/null +++ b/src/utils/postgresProvider.ts @@ -0,0 +1,107 @@ +import pg from 'pg'; +import { QueryIterablePool } from 'pg-iterator'; +import { DatabaseProvider, DatabaseConfig } from './database/databaseProvider'; +import { TableConfig } from './utils'; +import { QueryResultRow } from './types'; + +export class PostgresProvider implements DatabaseProvider { + private pool: pg.Pool | null = null; + private client: pg.PoolClient | null = null; + private config: DatabaseConfig; + + constructor(config: DatabaseConfig) { + this.config = config; + } + + async connect(): Promise { + this.pool = new pg.Pool({ + host: this.config.host, + port: this.config.port, + user: this.config.user, + password: this.config.password, + database: this.config.database, + connectionString: this.config.connectionString + }); + this.client = await this.pool.connect(); + } + + async disconnect(): Promise { + if (this.client) { + this.client.release(); + this.client = null; + } + if (this.pool) { + await this.pool.end(); + this.pool = null; + } + } + + async beginTransaction(): Promise { + if (!this.client) throw new Error('Not connected'); + await this.client.query('BEGIN'); + } + + async commitTransaction(): Promise { + if (!this.client) throw new Error('Not connected'); + await this.client.query('COMMIT'); + } + + async rollbackTransaction(): Promise { + if (!this.client) throw new Error('Not connected'); + await this.client.query('ROLLBACK'); + } + + async query(sql: string): Promise<{ rows: QueryResultRow[]; rowCount: number }> { + if (!this.client) throw new Error('Not connected'); + const result = await this.client.query(sql); + return { + rows: result.rows, + rowCount: result.rowCount || 0 + }; + } + + async *queryStream(sql: string): AsyncIterable { + if (!this.pool) throw new Error('Not connected'); + const qs = new QueryIterablePool(this.pool); + try { + const rows = qs.query(sql); + for await (const row of rows) { + yield row as QueryResultRow; + } + } finally { + qs.release(); + } + } + + async getPrimaryKeys(table: TableConfig): Promise { + if (!this.pool) throw new Error('Not connected'); + const rawQuery = ` + SELECT c.column_name + FROM information_schema.table_constraints t + JOIN information_schema.constraint_column_usage c + ON c.constraint_name = t.constraint_name + WHERE t.constraint_type = 'PRIMARY KEY' AND c.table_name = '${table.name}'`; + const result = await this.pool.query(rawQuery); + return result.rows.map((row) => row.column_name); + } + + async getColumnNames(table: TableConfig): Promise { + if (!this.pool) throw new Error('Not connected'); + const rawQuery = ` + SELECT column_name + FROM information_schema.columns + WHERE table_name = '${table.name}' + ORDER BY ordinal_position`; + const result = await this.pool.query(rawQuery); + return result.rows.map((row) => row.column_name); + } + + escapeIdentifier(identifier: string): string { + return `"${identifier}"`; + } + + getDatabaseInfo(): string { + const { host, port, database, user } = this.config; + return `postgres://${user}@${host}:${port}/${database}`; + } +} diff --git a/src/utils/query.ts b/src/utils/query.ts index bbf44d6..1b9aa88 100644 --- a/src/utils/query.ts +++ b/src/utils/query.ts @@ -11,27 +11,38 @@ export const isInsertQuery = (rawQuery: string | undefined): boolean => { export const makeInsertQuery = ( table: TableConfig, tableDetail: TableDetail, - columnValues: { [key: string]: any } + values: any, + dbType: 'postgres' | 'mssql' = 'postgres' ): string => { - const isExistedCreatedAt = columnValues['created_at']; - const hasCreatedAtColumn = tableDetail.columns?.includes('created_at'); - if (!isExistedCreatedAt && hasCreatedAtColumn) { - columnValues['created_at'] = 'NOW()'; - } - - const isExistedUpdatedAt = columnValues['updated_at']; - const hasUpdatedAtColumn = tableDetail.columns?.includes('updated_at'); - if (!isExistedUpdatedAt && hasUpdatedAtColumn) { - columnValues['updated_at'] = 'NOW()'; - } + const columns = tableDetail.columns ?? []; + const columnsStr = columns.map((c) => (dbType === 'postgres' ? `"${c}"` : `[${c}]`)).join(', '); - const columns = Object.keys(columnValues).join(', '); - const values = Object.values(columnValues) - .map((value) => { - return escape(value); + // Handle different value escaping for different databases + const valuesStr = columns + .map((c) => { + const value = values[c]; + if (value === null || value === undefined) { + return 'NULL'; + } else if (typeof value === 'string') { + return `'${value.replace(/'/g, "''")}'`; + } else if (typeof value === 'object' && value instanceof Date) { + return dbType === 'postgres' ? `'${value.toISOString()}'::timestamp` : `'${value.toISOString()}'`; + } else if (typeof value === 'object') { + return dbType === 'postgres' ? `'${JSON.stringify(value)}'::jsonb` : `'${JSON.stringify(value)}'`; + } + return value; }) .join(', '); - return `INSERT INTO ${table.name} (${columns}) VALUES (${values});`; + + const tableIdentifier = table.schema + ? dbType === 'postgres' + ? `"${table.schema}"."${table.name}"` + : `[${table.schema}].[${table.name}]` + : dbType === 'postgres' + ? `"${table.name}"` + : `[${table.name}]`; + + return `INSERT INTO ${tableIdentifier} (${columnsStr}) VALUES (${valuesStr});`; }; export const isUpdateQuery = (rawQuery: string | undefined): boolean => { @@ -44,20 +55,76 @@ export const isUpdateQuery = (rawQuery: string | undefined): boolean => { export const makeUpdateQuery = ( table: TableConfig, tableDetail: TableDetail, - columnValues: { [key: string]: any } + values: any, + dbType: 'postgres' | 'mssql' = 'postgres' ): string => { - const values = Object.entries(columnValues) - .map(([column, value]) => { - if (tableDetail.primaryKeys?.includes(column)) { - return null; // Skip update field if is primary key + const columns = tableDetail.columns; + const primaryKeys = tableDetail.primaryKeys; + + if (!Array.isArray(columns) || columns.length === 0) { + throw new Error(`The table '${table.name}' does not have columns defined.`); + } + + if (!primaryKeys || primaryKeys.length === 0) { + throw new Error(`The table '${table.name}' does not have primary keys.`); + } + + // Escape identifiers based on database type + const escapeIdentifier = (id: string) => (dbType === 'postgres' ? `"${id}"` : `[${id}]`); + + // Create the SET clause + const setClause = columns + .filter((c) => !primaryKeys.includes(c)) + .map((c) => { + const value = values[c]; + let valueStr = 'NULL'; + + if (value !== null && value !== undefined) { + if (typeof value === 'string') { + valueStr = `'${value.replace(/'/g, "''")}'`; + } else if (typeof value === 'object' && value instanceof Date) { + valueStr = + dbType === 'postgres' ? `'${value.toISOString()}'::timestamp` : `'${value.toISOString()}'`; + } else if (typeof value === 'object') { + valueStr = + dbType === 'postgres' ? `'${JSON.stringify(value)}'::jsonb` : `'${JSON.stringify(value)}'`; + } else { + valueStr = `${value}`; + } } - return `${column}=${escape(value)}`; + + return `${escapeIdentifier(c)} = ${valueStr}`; }) - .filter(Boolean) .join(', '); - return `UPDATE ${table.name} SET ${values} WHERE ${tableDetail.primaryKeys - ?.map((p) => `${p}='${columnValues[p]}'`) - .join(' AND ')};`; + + // Create the WHERE clause for primary keys + const whereClause = primaryKeys + .map((pk) => { + const value = values[pk]; + let valueStr = 'NULL'; + + if (value === null || value === undefined) { + return `${escapeIdentifier(pk)} IS NULL`; + } else if (typeof value === 'string') { + valueStr = `'${value.replace(/'/g, "''")}'`; + } else if (typeof value === 'object' && value instanceof Date) { + valueStr = dbType === 'postgres' ? `'${value.toISOString()}'::timestamp` : `'${value.toISOString()}'`; + } else if (typeof value === 'object') { + valueStr = dbType === 'postgres' ? `'${JSON.stringify(value)}'::jsonb` : `'${JSON.stringify(value)}'`; + } else { + valueStr = `${value}`; + } + + return `${escapeIdentifier(pk)} = ${valueStr}`; + }) + .join(' AND '); + + // Generate the table identifier with schema if provided + const tableIdentifier = table.schema + ? `${escapeIdentifier(table.schema)}.${escapeIdentifier(table.name)}` + : escapeIdentifier(table.name); + + return `UPDATE ${tableIdentifier} SET ${setClause} WHERE ${whereClause};`; }; export const isDeleteQuery = (rawQuery: string | undefined): boolean => { @@ -70,9 +137,45 @@ export const isDeleteQuery = (rawQuery: string | undefined): boolean => { export const makeDeleteQuery = ( table: TableConfig, tableDetail: TableDetail, - columnValues: { [key: string]: any } + values: any, + dbType: 'postgres' | 'mssql' = 'postgres' ): string => { - return `DELETE FROM ${table.name} WHERE ${tableDetail.primaryKeys - ?.map((p) => `${p}='${columnValues[p]}'`) - .join(' AND ')};`; + const primaryKeys = tableDetail.primaryKeys; + + if (!primaryKeys || primaryKeys.length === 0) { + throw new Error(`The table '${table.name}' does not have primary keys.`); + } + + // Escape identifiers based on database type + const escapeIdentifier = (id: string) => (dbType === 'postgres' ? `"${id}"` : `[${id}]`); + + // Create the WHERE clause for primary keys + const whereClause = primaryKeys + .map((pk) => { + const value = values[pk]; + + if (value === null || value === undefined) { + return `${escapeIdentifier(pk)} IS NULL`; + } else if (typeof value === 'string') { + return `${escapeIdentifier(pk)} = '${value.replace(/'/g, "''")}'`; + } else if (typeof value === 'object' && value instanceof Date) { + return dbType === 'postgres' + ? `${escapeIdentifier(pk)} = '${value.toISOString()}'::timestamp` + : `${escapeIdentifier(pk)} = '${value.toISOString()}'`; + } else if (typeof value === 'object') { + return dbType === 'postgres' + ? `${escapeIdentifier(pk)} = '${JSON.stringify(value)}'::jsonb` + : `${escapeIdentifier(pk)} = '${JSON.stringify(value)}'`; + } else { + return `${escapeIdentifier(pk)} = ${value}`; + } + }) + .join(' AND '); + + // Generate the table identifier with schema if provided + const tableIdentifier = table.schema + ? `${escapeIdentifier(table.schema)}.${escapeIdentifier(table.name)}` + : escapeIdentifier(table.name); + + return `DELETE FROM ${tableIdentifier} WHERE ${whereClause};`; }; diff --git a/src/utils/types.ts b/src/utils/types.ts new file mode 100644 index 0000000..67001df --- /dev/null +++ b/src/utils/types.ts @@ -0,0 +1,3 @@ +export interface QueryResultRow { + [key: string]: any; +} diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 799d817..5dcecb3 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -5,6 +5,7 @@ import { Uri, window, workspace } from 'vscode'; import { constants } from './constants'; import { logger } from './logger'; import { SystemInfo } from './systemInfo'; +import { DatabaseConfig } from './database/databaseProvider'; export type TableDetail = { primaryKeys: string[]; @@ -52,8 +53,8 @@ export type MigrateConfig = { }; export type PatternConfig = { - source: PoolConfig; - target: PoolConfig; + source: DatabaseConfig; + target: DatabaseConfig; diff: { format?: boolean; tables: TableConfig[]; @@ -132,10 +133,13 @@ export const upperToPascal = (input: string): string => { return capitalizedWords.join(''); }; -export const getDatabaseInfo = (poolConfig: PoolConfig): string => { - const { user, host, port, database } = poolConfig; +export const getDatabaseInfo = (poolConfig: DatabaseConfig): string => { + const { user, host, port, database, type, connectionString } = poolConfig; const maskPassword = '*'.repeat(6); - return `postgres://${user}:${maskPassword}@${host}:${port}/${database}`; + if (connectionString) { + return `${type}://${connectionString}`; + } + return `{${type}}://${user}:${maskPassword}@${host}:${port}/${database}`; }; export const getTablePrimaryKey = (table: TableConfig, tableDetail: TableDetail, record: any): string => { @@ -172,7 +176,7 @@ export const getMigrationDirs = async (migrationRoot: string): Promise<{ name: s export const showInputPassword = async ( from: 'source' | 'target', - poolConfig: PoolConfig + poolConfig: DatabaseConfig ): Promise => { const passwordInput = await window.showInputBox({ title: `Please enter a password to use with ${from} database.`, diff --git a/tsconfig.json b/tsconfig.json index aabc391..4c3f722 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,13 +1,15 @@ { "compilerOptions": { "module": "commonjs", - "target": "ES2020", + "target": "ES2022", "outDir": "dist", - "lib": ["es6", "WebWorker"], + "lib": ["es2022", "WebWorker"], "sourceMap": true, "rootDir": "src", "strict": true, - "esModuleInterop": true + "esModuleInterop": true, + "skipLibCheck": true }, + "exclude": ["node_modules", ".vscode-test", ".vscode-test-web"] } diff --git a/webpack.config.js b/webpack.config.js index 143ec6a..ca91af9 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -52,7 +52,7 @@ const webExtensionConfig = { fs: false, net: false, tls: false, - dns: false, + dns: false } }, module: { @@ -88,4 +88,29 @@ const webExtensionConfig = { devtool: 'nosources-source-map' // create a source map that points to the original source file }; -module.exports = [webExtensionConfig]; +const nodeExtensionConfig = { + ...webExtensionConfig, + target: 'node', + output: { + filename: '[name].js', + path: path.join(__dirname, './dist'), + libraryTarget: 'commonjs2' + }, + // Node build doesn't need browser fallbacks + resolve: { + mainFields: ['module', 'main'], + extensions: ['.ts', '.js'] + }, + // allow fs and other Node core modules in the node build + externalsPresets: { node: true }, + // keep externals for vscode + externals: { + vscode: 'commonjs vscode', + msnodesqlv8: 'commonjs msnodesqlv8', // do not bundle, require at runtime + mssql: 'commonjs mssql', // do not bundle, require at runtime + tedious: 'commonjs tedious' // do not bundle, require at runtime + }, + devtool: 'nosources-source-map' +}; + +module.exports = [nodeExtensionConfig];