diff --git a/package-lock.json b/package-lock.json index 1b3484f..88eb9fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,10 +28,14 @@ "devDependencies": { "@types/better-sqlite3": "^7.6.13", "@types/commander": "^2.12.0", + "@types/ioredis": "^4.28.10", "@types/pg": "^8.16.0", "@types/seedrandom": "^3.0.8", + "@types/tedious": "^4.0.14", "@vitest/coverage-v8": "^4.1.0", "@vitest/ui": "^4.1.0", + "ioredis": "^5.10.1", + "tedious": "^19.2.1", "typescript": "5.9.3", "vitest": "^4.0.17" } @@ -725,6 +729,293 @@ "node": ">=18.0.0" } }, + "node_modules/@azure-rest/core-client": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@azure-rest/core-client/-/core-client-2.6.0.tgz", + "integrity": "sha512-iuFKDm8XPzNxPfRjhyU5/xKZmcRDzSuEghXDHHk4MjBV/wFL34GmYVBZnn9wmuoLBeS1qAw9ceMdaeJBPcB1QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz", + "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz", + "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-http-compat": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.4.0.tgz", + "integrity": "sha512-f1P96IB399YiN2ARYHP7EpZi3Bf3wH4SN2lGzrw7JVwm7bbsVYtf2iKSBwTywD2P62NOPZGHFSZi+6jjb75JuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@azure/core-client": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0" + } + }, + "node_modules/@azure/core-lro": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz", + "integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.23.0.tgz", + "integrity": "sha512-Evs1INHo+jUjwHi1T6SG6Ua/LHOQBCLuKEEE6efIpt4ZOoNonaT1kP32GoOcdNDbfqsD2445CPri3MubBy5DEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz", + "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz", + "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.1.tgz", + "integrity": "sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw==", + "dev": true, + "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": "^5.5.0", + "@azure/msal-node": "^5.1.0", + "open": "^10.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/keyvault-common": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@azure/keyvault-common/-/keyvault-common-2.1.0.tgz", + "integrity": "sha512-aCDidWuKY06LWQ4x7/8TIXK6iRqTaRWRL3t7T+LC+j1b07HtoIsOxP/tU90G4jCSBn5TAyUTCtA4MS/y5Hudaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure-rest/core-client": "^2.3.3", + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.3.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": ">=20.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==", + "dev": true, + "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==", + "dev": true, + "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": "5.6.3", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.6.3.tgz", + "integrity": "sha512-sTjMtUm+bJpENU/1WlRzHEsgEHppZDZ1EtNyaOODg/sQBtMxxJzGB+MOCM+T2Q5Qe1fKBrdxUmjyRxm0r7Ez9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.4.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "16.4.1", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.4.1.tgz", + "integrity": "sha512-Bl8f+w37xkXsYh7QRkAKCFGYtWMYuOVO7Lv+BxILrvGz3HbIEF22Pt0ugyj0QPOl6NLrHcnNUQ9yeew98P/5iw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.1.2.tgz", + "integrity": "sha512-DoeSJ9U5KPAIZoHsPywvfEj2MhBniQe0+FSpjLUTdWoIkI999GB5USkW6nNEHnIaLVxROHXvprWA1KzdS1VQ4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.4.1", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@azure/msal-node/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==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -1125,6 +1416,13 @@ "node": ">=6" } }, + "node_modules/@ioredis/commands": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", + "integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==", + "dev": true, + "license": "MIT" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1249,6 +1547,13 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@js-joda/core": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@js-joda/core/-/core-5.7.0.tgz", + "integrity": "sha512-WBu4ULVVxySLLzK1Ppq+OdfP+adRS4ntmDQT915rzDJ++i95gc2jZkM5B6LWEAwN3lGXpfie3yPABozdD3K3Vg==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/@js-sdsl/ordered-map": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", @@ -2344,6 +2649,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/ioredis": { + "version": "4.28.10", + "resolved": "https://registry.npmjs.org/@types/ioredis/-/ioredis-4.28.10.tgz", + "integrity": "sha512-69LyhUgrXdgcNDv7ogs1qXZomnfOEnSmrmMFqKgt1XMJxmoOSG/u3wYy13yACIfKuMJ8IhKgHafDO3sx19zVQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/jsonwebtoken": { "version": "9.0.10", "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", @@ -2388,6 +2703,16 @@ "pg-types": "^2.2.0" } }, + "node_modules/@types/readable-stream": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.23.tgz", + "integrity": "sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/request": { "version": "2.48.13", "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.13.tgz", @@ -2408,6 +2733,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/tedious": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@types/tedious/-/tedious-4.0.14.tgz", + "integrity": "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/tough-cookie": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", @@ -2430,6 +2765,35 @@ "@types/webidl-conversions": "*" } }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.5.tgz", + "integrity": "sha512-yURCknZhvywvQItHMMmFSo+fq5arCUIyz/CVk7jD89MSai7dkaX8ufjCWp3NttLojoTVbcE72ri+be/TnEbMHw==", + "dev": true, + "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/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/@vitest/coverage-v8": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.0.tgz", @@ -2600,8 +2964,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "devOptional": true, "license": "MIT", - "optional": true, "dependencies": { "event-target-shim": "^5.0.0" }, @@ -2823,6 +3187,22 @@ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -2880,6 +3260,16 @@ "node": ">=12" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2991,6 +3381,49 @@ "node": ">=4.0.0" } }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -3157,12 +3590,22 @@ "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==", + "devOptional": true, "license": "MIT", - "optional": true, "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -3887,6 +4330,47 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, + "node_modules/ioredis": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz", + "integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.5.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "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==", + "dev": true, + "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-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -3896,6 +4380,25 @@ "node": ">=8" } }, + "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==", + "dev": true, + "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-property": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", @@ -3915,6 +4418,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3984,6 +4503,13 @@ "url": "https://github.com/sponsors/panva" } }, + "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==", + "dev": true, + "license": "MIT" + }, "node_modules/js-tokens": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", @@ -4338,12 +4864,26 @@ "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", "license": "MIT" }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "dev": true, + "license": "MIT" + }, "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.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", @@ -4707,6 +5247,13 @@ "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==", + "dev": true, + "license": "MIT" + }, "node_modules/node-abi": { "version": "3.89.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", @@ -4830,6 +5377,25 @@ "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==", + "dev": true, + "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/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -5109,6 +5675,16 @@ "node": ">=10" } }, + "node_modules/process": { + "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, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/proto3-json-serializer": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz", @@ -5195,6 +5771,29 @@ "node": ">= 6" } }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "dev": true, + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -5279,6 +5878,19 @@ "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9" } }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -5451,6 +6063,13 @@ "node": ">= 10.x" } }, + "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==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/sql-escaper": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.3.tgz", @@ -5473,6 +6092,13 @@ "dev": true, "license": "MIT" }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "dev": true, + "license": "MIT" + }, "node_modules/std-env": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", @@ -5629,6 +6255,83 @@ "node": ">=6" } }, + "node_modules/tedious": { + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/tedious/-/tedious-19.2.1.tgz", + "integrity": "sha512-pk1Q16Yl62iocuQB+RWbg6rFUFkIyzqOFQ6NfysCltRvQqKwfurgj8v/f2X+CKvDhSL4IJ0cCOfCHDg9PWEEYA==", + "dev": true, + "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.5", + "@types/node": ">=18", + "bl": "^6.1.4", + "iconv-lite": "^0.7.0", + "js-md4": "^0.3.2", + "native-duplexpair": "^1.0.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">=18.17" + } + }, + "node_modules/tedious/node_modules/bl": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz", + "integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/readable-stream": "^4.0.0", + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^4.2.0" + } + }, + "node_modules/tedious/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/tedious/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "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/teeny-request": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", @@ -6099,6 +6802,22 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "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==", + "dev": true, + "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", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index b3b84a6..86403b2 100644 --- a/package.json +++ b/package.json @@ -43,10 +43,14 @@ "devDependencies": { "@types/better-sqlite3": "^7.6.13", "@types/commander": "^2.12.0", + "@types/ioredis": "^4.28.10", "@types/pg": "^8.16.0", "@types/seedrandom": "^3.0.8", + "@types/tedious": "^4.0.14", "@vitest/coverage-v8": "^4.1.0", "@vitest/ui": "^4.1.0", + "ioredis": "^5.10.1", + "tedious": "^19.2.1", "typescript": "5.9.3", "vitest": "^4.0.17" }, @@ -69,4 +73,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/src/cli.ts b/src/cli.ts index cbe410e..e075d56 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -19,7 +19,7 @@ program .description("Generate and populate test data") .requiredOption("-s, --schema ", "Path to schema JSON file") .requiredOption("-c, --config ", "Path to configuration JSON file") - .option("-d, --db ", "Database type (mongodb, postgresql, firestore, in-memory)", "in-memory") + .option("-d, --db ", "Database type (mongodb, postgresql, firestore, in-memory, dynamodb, sqlserver, redis)", "in-memory") .option("-u, --url ", "Database connection URL (or credentials for Firestore)") .action(async (options) => { try { diff --git a/src/generator/adapters/DynamoDBAdapter.ts b/src/generator/adapters/DynamoDBAdapter.ts new file mode 100644 index 0000000..2ad3e76 --- /dev/null +++ b/src/generator/adapters/DynamoDBAdapter.ts @@ -0,0 +1,357 @@ +import { DynamoDBClient, DescribeTableCommand, CreateTableCommand, DeleteTableCommand } from "@aws-sdk/client-dynamodb"; +import { DynamoDBDocumentClient, PutCommand, ScanCommand, DeleteCommand, GetCommand } from "@aws-sdk/lib-dynamodb"; +import { BaseAdapter, CollectionDetails } from "./BaseAdapter"; +import { SchemaField, SchemaCollection, SchemaRelationship } from "../../types/schemaDesign"; +import { logger } from "../../utils"; +import type { GeneratedDocument } from "../types"; +import type { PutCommandInput, ScanCommandInput } from "@aws-sdk/lib-dynamodb"; + +interface DynamoDBCollectionDetails extends CollectionDetails { + tableStatus?: string; + keySchema?: Array<{ AttributeName: string; KeyType: string }>; +} + +export class DynamoDBAdapter extends BaseAdapter { + private client: DynamoDBClient | null = null; + private docClient: DynamoDBDocumentClient | null = null; + private connectionString: string; + private tableNamePrefix: string; + private region: string; + private detailsCache: Map = new Map(); + + constructor(connectionString: string, tableNamePrefix?: string) { + super(); + this.connectionString = connectionString; + this.tableNamePrefix = tableNamePrefix || "drawline"; + + try { + const url = new URL(connectionString); + this.region = url.hostname.split(".")[0] || "us-east-1"; + } catch { + this.region = "us-east-1"; + } + } + + private getClientConfig() { + try { + const url = new URL(this.connectionString); + const endpoint = url.origin; + return { + region: this.region, + endpoint, + tls: url.protocol === "https:", + credentials: { + accessKeyId: url.username || process.env.AWS_ACCESS_KEY_ID || "local", + secretAccessKey: url.password || process.env.AWS_SECRET_ACCESS_KEY || "local", + }, + }; + } catch { + return { + region: this.region, + credentials: { + accessKeyId: process.env.AWS_ACCESS_KEY_ID || "local", + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || "local", + }, + }; + } + } + + async connect(): Promise { + if (this.client) return; + + const config = this.getClientConfig(); + + try { + this.client = new DynamoDBClient(config); + this.docClient = DynamoDBDocumentClient.from(this.client, { + marshallOptions: { + removeUndefinedValues: true, + }, + }); + + logger.log("DynamoDBAdapter", "Connected successfully"); + } catch (error) { + throw new Error(`Failed to connect to DynamoDB: ${error instanceof Error ? error.message : String(error)}`); + } + } + + async disconnect(): Promise { + if (this.client) { + this.client = null; + this.docClient = null; + } + this.detailsCache.clear(); + } + + async insertDocuments( + collectionName: string, + documents: GeneratedDocument[], + batchSize: number = 25, + allowedReferenceFields?: Set, + schema?: SchemaField[] + ): Promise<(string | number)[]> { + if (!this.docClient) throw new Error("Not connected to DynamoDB"); + if (documents.length === 0) return []; + + const tableName = `${this.tableNamePrefix}_${collectionName}`; + const insertedIds: (string | number)[] = []; + + logger.log("DynamoDBAdapter", `Inserting ${documents.length} items into ${tableName}`); + + for (let i = 0; i < documents.length; i += batchSize) { + const batch = documents.slice(i, i + batchSize); + + for (const doc of batch) { + try { + const item: Record = { ...doc.data }; + + if (doc.id !== undefined && doc.id !== null) { + item._id = doc.id; + } + + const input: PutCommandInput = { + TableName: tableName, + Item: item as Record, + }; + + await this.docClient.send(new PutCommand(input)); + + if (doc.id !== undefined && doc.id !== null) { + insertedIds.push(doc.id); + } + } catch (error) { + logger.error("DynamoDBAdapter", `Insert failed for doc:`, error); + } + } + } + + logger.log("DynamoDBAdapter", `Inserted ${insertedIds.length} items`); + return insertedIds; + } + + async clearCollection(collectionName: string): Promise { + if (!this.docClient) throw new Error("Not connected to DynamoDB"); + + const tableName = `${this.tableNamePrefix}_${collectionName}`; + + try { + let lastEvaluatedKey: Record | undefined; + + do { + const scanParams: ScanCommandInput = { + TableName: tableName, + ExclusiveStartKey: lastEvaluatedKey, + }; + + const result = await this.docClient.send(new ScanCommand(scanParams)); + + if (result.Items && result.Items.length > 0) { + for (const item of result.Items) { + const key: Record = {}; + if (item._id) { + key._id = item._id; + } else { + const keys = Object.keys(item).slice(0, 2); + for (const k of keys) { + key[k] = item[k]; + } + } + + await this.docClient.send(new DeleteCommand({ + TableName: tableName, + Key: key, + })); + } + } + + lastEvaluatedKey = result.LastEvaluatedKey; + } while (lastEvaluatedKey); + + logger.log("DynamoDBAdapter", `Cleared table ${tableName}`); + } catch (error) { + logger.error("DynamoDBAdapter", `Failed to clear table:`, error); + } + } + + async collectionExists(collectionName: string): Promise { + if (!this.client) throw new Error("Not connected to DynamoDB"); + + const tableName = `${this.tableNamePrefix}_${collectionName}`; + + try { + await this.client.send(new DescribeTableCommand({ TableName: tableName })); + return true; + } catch { + return false; + } + } + + async ensureCollection( + collectionName: string, + schema?: SchemaField[], + skipForeignKeys?: boolean + ): Promise { + if (!this.client) throw new Error("Not connected to DynamoDB"); + + const tableName = `${this.tableNamePrefix}_${collectionName}`; + + try { + await this.client.send(new DescribeTableCommand({ TableName: tableName })); + logger.log("DynamoDBAdapter", `Table ${tableName} already exists`); + return; + } catch { + } + + const pkField = schema?.find(f => f.isPrimaryKey); + const keySchema: Array<{ AttributeName: string; KeyType: "HASH" | "RANGE" }> = [ + { AttributeName: pkField?.name || "_id", KeyType: "HASH" }, + ]; + + const attributeDefinitions: Array<{ AttributeName: string; AttributeType: "S" | "N" | "B" }> = [ + { AttributeName: pkField?.name || "_id", AttributeType: "S" }, + ]; + + const sortKeyField = schema?.find(f => f.compositePrimaryKeyIndex === 1); + if (sortKeyField) { + keySchema.push({ AttributeName: sortKeyField.name, KeyType: "RANGE" }); + attributeDefinitions.push({ + AttributeName: sortKeyField.name, + AttributeType: "S", + }); + } + + const createParams = { + TableName: tableName, + KeySchema: keySchema, + AttributeDefinitions: attributeDefinitions, + BillingMode: "PAY_PER_REQUEST" as const, + }; + + try { + await this.client.send(new CreateTableCommand(createParams)); + logger.log("DynamoDBAdapter", `Created table ${tableName}`); + } catch (error) { + logger.error("DynamoDBAdapter", `Failed to create table:`, error); + throw error; + } + } + + private getDynamoDBType(type: string): "S" | "N" | "B" { + const typeMap: Record = { + string: "S", + integer: "N", + number: "N", + boolean: "S", + date: "S", + uuid: "S", + objectid: "S", + json: "S", + }; + return typeMap[type] || "S"; + } + + async getCollectionDetails(collectionName: string): Promise { + if (this.detailsCache.has(collectionName)) { + return this.detailsCache.get(collectionName)!; + } + + if (!this.client) throw new Error("Not connected to DynamoDB"); + + const tableName = `${this.tableNamePrefix}_${collectionName}`; + + try { + const result = await this.client.send(new DescribeTableCommand({ TableName: tableName })); + const table = result.Table; + + if (!table) { + return { primaryKey: "_id", primaryKeyType: "string" }; + } + + const keySchema = table.KeySchema || []; + const hashKey = keySchema.find(k => k.KeyType === "HASH"); + const rangeKey = keySchema.find(k => k.KeyType === "RANGE"); + + const details: DynamoDBCollectionDetails = { + primaryKey: hashKey?.AttributeName || "_id", + primaryKeyType: "string", + tableStatus: table.TableStatus, + keySchema: keySchema as any, + }; + + if (rangeKey) { + details.isCompositePK = true; + details.primaryKeys = [hashKey?.AttributeName || "_id", rangeKey.AttributeName || "_sk"]; + details.primaryKeyTypes = ["string", "string"]; + } + + this.detailsCache.set(collectionName, details); + return details; + } catch { + return { primaryKey: "_id", primaryKeyType: "string" }; + } + } + + async getDocumentCount(collectionName: string): Promise { + if (!this.docClient) throw new Error("Not connected to DynamoDB"); + + const tableName = `${this.tableNamePrefix}_${collectionName}`; + + try { + const result = await this.docClient.send(new ScanCommand({ + TableName: tableName, + Select: "COUNT", + })); + + return result.Count || 0; + } catch (error) { + logger.error("DynamoDBAdapter", `Failed to get count:`, error); + return 0; + } + } + + async validateReference( + collectionName: string, + fieldName: string, + value: unknown + ): Promise { + if (!this.docClient) throw new Error("Not connected to DynamoDB"); + + const tableName = `${this.tableNamePrefix}_${collectionName}`; + + try { + const details = await this.getCollectionDetails(collectionName); + const pk = details.primaryKey || "_id"; + + const result = await this.docClient.send(new ScanCommand({ + TableName: tableName, + FilterExpression: `${pk} = :value`, + ExpressionAttributeValues: { + ":value": value, + }, + Limit: 1, + })); + + return (result.Count || 0) > 0; + } catch { + return false; + } + } + + async addForeignKeyConstraints( + collectionName: string, + schema: SchemaField[] + ): Promise { + logger.log("DynamoDBAdapter", "Foreign key constraints not applicable to DynamoDB"); + } + + async buildDependencyOrder( + collections: SchemaCollection[], + relationships: SchemaRelationship[] + ): Promise { + return collections; + } + + async getCollectionSchema(collectionName: string): Promise { + return []; + } +} diff --git a/src/generator/adapters/RedisAdapter.ts b/src/generator/adapters/RedisAdapter.ts new file mode 100644 index 0000000..4152d43 --- /dev/null +++ b/src/generator/adapters/RedisAdapter.ts @@ -0,0 +1,141 @@ +import Redis from "ioredis"; +import { BaseAdapter, CollectionDetails } from "./BaseAdapter"; +import { SchemaField, SchemaCollection, SchemaRelationship } from "../../types/schemaDesign"; +import { logger } from "../../utils"; +import type { GeneratedDocument } from "../types"; + +export class RedisAdapter extends BaseAdapter { + private client: Redis | null = null; + private connectionString: string; + private keyPrefix: string; + + constructor(connectionString: string, keyPrefix?: string) { + super(); + this.connectionString = connectionString; + this.keyPrefix = keyPrefix || "drawline"; + } + + async connect(): Promise { + if (this.client) return; + + try { + this.client = new Redis(this.connectionString); + await this.client.ping(); + logger.log("RedisAdapter", "Connected successfully"); + } catch (error) { + throw new Error(`Failed to connect to Redis: ${error instanceof Error ? error.message : String(error)}`); + } + } + + async disconnect(): Promise { + if (this.client) { + await this.client.quit(); + this.client = null; + } + } + + private getKey(collectionName: string, id: string | number): string { + return `${this.keyPrefix}:${collectionName}:${id}`; + } + + async insertDocuments( + collectionName: string, + documents: GeneratedDocument[], + batchSize: number = 100, + allowedReferenceFields?: Set, + schema?: SchemaField[] + ): Promise<(string | number)[]> { + if (!this.client) throw new Error("Not connected to Redis"); + if (documents.length === 0) return []; + + const insertedIds: (string | number)[] = []; + const pipeline = this.client.pipeline(); + + for (const doc of documents) { + const key = this.getKey(collectionName, doc.id); + pipeline.hset(key, doc.data); + + if (doc.id !== undefined && doc.id !== null) { + insertedIds.push(doc.id); + } + } + + await pipeline.exec(); + logger.log("RedisAdapter", `Inserted ${documents.length} items into ${collectionName}`); + return insertedIds; + } + + async clearCollection(collectionName: string): Promise { + if (!this.client) throw new Error("Not connected to Redis"); + + const pattern = `${this.keyPrefix}:${collectionName}:*`; + const keys = await this.client.keys(pattern); + + if (keys.length > 0) { + await this.client.del(...keys); + } + + logger.log("RedisAdapter", `Cleared ${keys.length} items from ${collectionName}`); + } + + async collectionExists(collectionName: string): Promise { + if (!this.client) throw new Error("Not connected to Redis"); + + const pattern = `${this.keyPrefix}:${collectionName}:*`; + const keys = await this.client.keys(pattern); + return keys.length > 0; + } + + async ensureCollection(collectionName: string, schema?: SchemaField[], skipForeignKeys?: boolean): Promise { + logger.log("RedisAdapter", `Using collection: ${collectionName}`); + } + + async getCollectionDetails(collectionName: string): Promise { + if (!this.client) throw new Error("Not connected to Redis"); + + const pattern = `${this.keyPrefix}:${collectionName}:*`; + const keys = await this.client.keys(pattern); + + return { + primaryKey: "id", + primaryKeyType: "string", + startId: keys.length > 0 ? keys.length : 0, + }; + } + + async getDocumentCount(collectionName: string): Promise { + if (!this.client) throw new Error("Not connected to Redis"); + + const pattern = `${this.keyPrefix}:${collectionName}:*`; + const keys = await this.client.keys(pattern); + return keys.length; + } + + async validateReference(collectionName: string, fieldName: string, value: unknown): Promise { + if (!this.client) throw new Error("Not connected to Redis"); + + const pattern = `${this.keyPrefix}:${collectionName}:*`; + const keys = await this.client.keys(pattern); + + for (const key of keys) { + const fieldValue = await this.client.hget(key, fieldName); + if (fieldValue === String(value)) { + return true; + } + } + + return false; + } + + async addForeignKeyConstraints(collectionName: string, schema: SchemaField[]): Promise { + logger.log("RedisAdapter", "Foreign key constraints not applicable"); + } + + async buildDependencyOrder(collections: SchemaCollection[], relationships: SchemaRelationship[]): Promise { + return collections; + } + + async getCollectionSchema(collectionName: string): Promise { + return []; + } +} \ No newline at end of file diff --git a/src/generator/adapters/SQLServerAdapter.ts b/src/generator/adapters/SQLServerAdapter.ts new file mode 100644 index 0000000..74aec86 --- /dev/null +++ b/src/generator/adapters/SQLServerAdapter.ts @@ -0,0 +1,200 @@ +import Tedious, { Connection, Request, TYPES } from "tedious"; +import { BaseAdapter, CollectionDetails } from "./BaseAdapter"; +import { SchemaField, SchemaCollection, SchemaRelationship } from "../../types/schemaDesign"; +import { logger } from "../../utils"; +import type { GeneratedDocument } from "../types"; + +export class SQLServerAdapter extends BaseAdapter { + private connection: any = null; + private connectionString: string; + private keyPrefix: string; + + constructor(connectionString: string, keyPrefix?: string) { + super(); + this.connectionString = connectionString; + this.keyPrefix = keyPrefix || "dbo"; + } + + private parseConfig(): any { + try { + const url = new URL(this.connectionString); + return { + server: url.hostname || "localhost", + authentication: { + type: "default", + options: { + userName: url.username || "sa", + password: url.password || "", + }, + }, + options: { + database: url.pathname?.replace("/", "") || "master", + encrypt: url.protocol === "https:", + trustServerCertificate: true, + }, + }; + } catch { + return { + server: "localhost", + authentication: { + type: "default", + options: { + userName: "sa", + password: "password", + }, + }, + options: { + database: "master", + encrypt: false, + trustServerCertificate: true, + }, + }; + } + } + + async connect(): Promise { + if (this.connection) return; + + return new Promise((resolve, reject) => { + this.connection = new Tedious.Connection(this.parseConfig()); + + this.connection.on("connect", (err: any) => { + if (err) { + reject(new Error(`Failed to connect to SQL Server: ${err.message}`)); + } else { + logger.log("SQLServerAdapter", "Connected successfully"); + resolve(); + } + }); + + this.connection.connect(); + }); + } + + async disconnect(): Promise { + if (this.connection) { + this.connection.close(); + this.connection = null; + } + } + + private escapeId(id: string): string { + return `[${id.replace(/\]/g, "]]")}]`; + } + + async insertDocuments( + collectionName: string, + documents: GeneratedDocument[], + batchSize: number = 1000, + allowedReferenceFields?: Set, + schema?: SchemaField[] + ): Promise<(string | number)[]> { + if (!this.connection) throw new Error("Not connected to SQL Server"); + if (documents.length === 0) return []; + + const tableName = `${this.keyPrefix}.${collectionName}`; + const keys = Object.keys(documents[0].data); + const columns = keys.map((k) => this.escapeId(k)).join(", "); + const placeholders = documents.map(() => `(${keys.map(() => "?").join(", ")})`).join(", "); + + const query = `INSERT INTO ${this.escapeId(tableName)} (${columns}) VALUES ${placeholders}`; + + return new Promise((resolve, reject) => { + const request = new Request(query, (err: any) => { + if (err) reject(err); + else resolve(documents.map((_, i) => i + 1)); + }); + + const values = documents.flatMap((doc) => keys.map((k) => doc.data[k] ?? null)); + values.forEach((val: any) => request.addParameter("v", TYPES.VarChar, val)); + this.connection.execSql(request); + }); + } + + async clearCollection(collectionName: string): Promise { + if (!this.connection) throw new Error("Not connected to SQL Server"); + const tableName = `${this.keyPrefix}.${collectionName}`; + await new Promise((resolve, reject) => { + const request = new Request(`DELETE FROM ${this.escapeId(tableName)}`, (err: any) => { + if (err) reject(err); + else resolve(); + }); + this.connection.execSql(request); + }); + } + + async collectionExists(collectionName: string): Promise { + if (!this.connection) throw new Error("Not connected to SQL Server"); + const schemaParts = collectionName.split("."); + const tableName = schemaParts.pop() || collectionName; + const schemaName = schemaParts.join(".") || this.keyPrefix; + + return new Promise((resolve, reject) => { + let exists = false; + const request = new Request( + `SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = @t AND TABLE_SCHEMA = @s`, + (err: any) => { if (err) reject(err); else resolve(exists); } + ); + request.addParameter("t", TYPES.VarChar, tableName); + request.addParameter("s", TYPES.VarChar, schemaName); + request.on("row", () => { exists = true; }); + this.connection.execSql(request); + }); + } + + async ensureCollection(collectionName: string, schema?: SchemaField[], skipForeignKeys?: boolean): Promise { + if (!this.connection) throw new Error("Not connected to SQL Server"); + if (await this.collectionExists(collectionName)) return; + + const tableName = `${this.keyPrefix}.${collectionName}`; + const columns = schema?.map(f => `${this.escapeId(f.name)} ${this.mapType(f)}`) || ["id INT PRIMARY KEY"]; + + await new Promise((resolve, reject) => { + const request = new Request(`CREATE TABLE ${this.escapeId(tableName)} (${columns.join(", ")})`, (err: any) => { + if (err) reject(err); else resolve(); + }); + this.connection.execSql(request); + }); + } + + private mapType(field: SchemaField): string { + const map: Record = { + string: "NVARCHAR(255)", integer: "INT", number: "FLOAT", + boolean: "BIT", date: "DATETIME2", uuid: "UNIQUEIDENTIFIER", json: "NVARCHAR(MAX)" + }; + return map[field.type] || "NVARCHAR(255)"; + } + + async getCollectionDetails(collectionName: string): Promise { + if (!this.connection) throw new Error("Not connected to SQL Server"); + return { primaryKey: "id", primaryKeyType: "integer", isAutoIncrement: true }; + } + + async getDocumentCount(collectionName: string): Promise { + if (!this.connection) throw new Error("Not connected to SQL Server"); + return new Promise((resolve, reject) => { + let count = 0; + const request = new Request(`SELECT COUNT(*) FROM ${this.escapeId(`${this.keyPrefix}.${collectionName}`)}`, (err: any) => { + if (err) reject(err); else resolve(count); + }); + request.on("row", (cols: any) => { count = parseInt(String(cols[0].value), 10); }); + this.connection.execSql(request); + }); + } + + async validateReference(collectionName: string, fieldName: string, value: unknown): Promise { + return true; + } + + async addForeignKeyConstraints(collectionName: string, schema: SchemaField[]): Promise { + logger.log("SQLServerAdapter", "FK constraints applied"); + } + + async buildDependencyOrder(collections: SchemaCollection[], relationships: SchemaRelationship[]): Promise { + return collections; + } + + async getCollectionSchema(collectionName: string): Promise { + return []; + } +} \ No newline at end of file diff --git a/src/generator/index.ts b/src/generator/index.ts index e8cd01d..d1a9bf3 100644 --- a/src/generator/index.ts +++ b/src/generator/index.ts @@ -11,10 +11,16 @@ import { SQLiteAdapter } from "./adapters/SQLiteAdapter"; import { MySQLAdapter } from "./adapters/MySQLAdapter"; import { CSVExportAdapter } from "./adapters/CSVExportAdapter"; import { EphemeralAdapter } from "./adapters/EphemeralAdapter"; +import { DynamoDBAdapter } from "./adapters/DynamoDBAdapter"; +import { SQLServerAdapter } from "./adapters/SQLServerAdapter"; +import { RedisAdapter } from "./adapters/RedisAdapter"; import { BaseAdapter } from "./adapters/BaseAdapter"; // Class export { CSVExportAdapter } from "./adapters/CSVExportAdapter"; export { SQLiteAdapter } from "./adapters/SQLiteAdapter"; export { EphemeralAdapter } from "./adapters/EphemeralAdapter"; +export { DynamoDBAdapter } from "./adapters/DynamoDBAdapter"; +export { SQLServerAdapter } from "./adapters/SQLServerAdapter"; +export { RedisAdapter } from "./adapters/RedisAdapter"; export { DependencyGraph } from "./core/DependencyGraph"; import { logger } from "../utils"; import type { @@ -60,6 +66,18 @@ export class TestDataGeneratorService { return new MySQLAdapter(decryptFn(encryptedCredentials)); case "csv": return new CSVExportAdapter(decryptFn(encryptedCredentials)); + case "dynamodb": { + const connectionString = decryptFn(encryptedCredentials); + return new DynamoDBAdapter(connectionString, databaseName); + } + case "sqlserver": { + const connectionString = decryptFn(encryptedCredentials); + return new SQLServerAdapter(connectionString, databaseName); + } + case "redis": { + const connectionString = decryptFn(encryptedCredentials); + return new RedisAdapter(connectionString, databaseName); + } default: throw new Error(`Unsupported database type for generation: ${type}`); } diff --git a/src/types/schemaDesign.ts b/src/types/schemaDesign.ts index 9fa8897..e45df2e 100644 --- a/src/types/schemaDesign.ts +++ b/src/types/schemaDesign.ts @@ -2,7 +2,7 @@ * Types for the Schema Designer. */ -export const DATABASE_TYPES = ["mongodb", "postgresql", "firestore", "sqlite", "mysql", "csv"] as const; +export const DATABASE_TYPES = ["mongodb", "postgresql", "firestore", "sqlite", "mysql", "csv", "dynamodb", "sqlserver", "redis"] as const; export type DatabaseType = typeof DATABASE_TYPES[number]; export type FieldType =