diff --git a/backend-node/package-lock.json b/backend-node/package-lock.json index 495db410..a4bf97ba 100644 --- a/backend-node/package-lock.json +++ b/backend-node/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@prisma/client": "^5.7.1", + "@prisma/client": "^6.16.2", "@types/mssql": "^9.1.8", "axios": "^1.11.0", "bcryptjs": "^2.4.3", @@ -28,7 +28,6 @@ "nodemailer": "^6.9.7", "oracledb": "^6.9.0", "pg": "^8.16.3", - "prisma": "^5.7.1", "redis": "^4.6.10", "winston": "^3.11.0" }, @@ -55,6 +54,7 @@ "jest": "^29.7.0", "nodemon": "^3.1.10", "prettier": "^3.1.0", + "prisma": "^6.16.2", "supertest": "^6.3.3", "ts-jest": "^29.1.1", "ts-node": "^10.9.2", @@ -65,20 +65,6 @@ "npm": ">=10.0.0" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@aws-crypto/sha256-browser": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", @@ -215,51 +201,51 @@ } }, "node_modules/@aws-sdk/client-ses": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.864.0.tgz", - "integrity": "sha512-cmsOrJZsrNa892gD2cAsbVkweDulgmC8PE38cz//bM//1BW/R1MMFClapF+Q9gACtsRVTRBXNtsIsBq8Gm1Urw==", + "version": "3.896.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.896.0.tgz", + "integrity": "sha512-L5C1ZLdTnAAZJqngRxt6RB6boHnx1Jp1U/awmLsBcnW3tEax5iCLtaNkDRZ6XrccYktVcy2lIUXnFJ7G7WunoQ==", "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.864.0", - "@aws-sdk/credential-provider-node": "3.864.0", - "@aws-sdk/middleware-host-header": "3.862.0", - "@aws-sdk/middleware-logger": "3.862.0", - "@aws-sdk/middleware-recursion-detection": "3.862.0", - "@aws-sdk/middleware-user-agent": "3.864.0", - "@aws-sdk/region-config-resolver": "3.862.0", - "@aws-sdk/types": "3.862.0", - "@aws-sdk/util-endpoints": "3.862.0", - "@aws-sdk/util-user-agent-browser": "3.862.0", - "@aws-sdk/util-user-agent-node": "3.864.0", - "@smithy/config-resolver": "^4.1.5", - "@smithy/core": "^3.8.0", - "@smithy/fetch-http-handler": "^5.1.1", - "@smithy/hash-node": "^4.0.5", - "@smithy/invalid-dependency": "^4.0.5", - "@smithy/middleware-content-length": "^4.0.5", - "@smithy/middleware-endpoint": "^4.1.18", - "@smithy/middleware-retry": "^4.1.19", - "@smithy/middleware-serde": "^4.0.9", - "@smithy/middleware-stack": "^4.0.5", - "@smithy/node-config-provider": "^4.1.4", - "@smithy/node-http-handler": "^4.1.1", - "@smithy/protocol-http": "^5.1.3", - "@smithy/smithy-client": "^4.4.10", - "@smithy/types": "^4.3.2", - "@smithy/url-parser": "^4.0.5", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.26", - "@smithy/util-defaults-mode-node": "^4.0.26", - "@smithy/util-endpoints": "^3.0.7", - "@smithy/util-middleware": "^4.0.5", - "@smithy/util-retry": "^4.0.7", - "@smithy/util-utf8": "^4.0.0", - "@smithy/util-waiter": "^4.0.7", + "@aws-sdk/core": "3.896.0", + "@aws-sdk/credential-provider-node": "3.896.0", + "@aws-sdk/middleware-host-header": "3.893.0", + "@aws-sdk/middleware-logger": "3.893.0", + "@aws-sdk/middleware-recursion-detection": "3.893.0", + "@aws-sdk/middleware-user-agent": "3.896.0", + "@aws-sdk/region-config-resolver": "3.893.0", + "@aws-sdk/types": "3.893.0", + "@aws-sdk/util-endpoints": "3.895.0", + "@aws-sdk/util-user-agent-browser": "3.893.0", + "@aws-sdk/util-user-agent-node": "3.896.0", + "@smithy/config-resolver": "^4.2.2", + "@smithy/core": "^3.12.0", + "@smithy/fetch-http-handler": "^5.2.1", + "@smithy/hash-node": "^4.1.1", + "@smithy/invalid-dependency": "^4.1.1", + "@smithy/middleware-content-length": "^4.1.1", + "@smithy/middleware-endpoint": "^4.2.4", + "@smithy/middleware-retry": "^4.3.0", + "@smithy/middleware-serde": "^4.1.1", + "@smithy/middleware-stack": "^4.1.1", + "@smithy/node-config-provider": "^4.2.2", + "@smithy/node-http-handler": "^4.2.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/smithy-client": "^4.6.4", + "@smithy/types": "^4.5.0", + "@smithy/url-parser": "^4.1.1", + "@smithy/util-base64": "^4.1.0", + "@smithy/util-body-length-browser": "^4.1.0", + "@smithy/util-body-length-node": "^4.1.0", + "@smithy/util-defaults-mode-browser": "^4.1.4", + "@smithy/util-defaults-mode-node": "^4.1.4", + "@smithy/util-endpoints": "^3.1.2", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-retry": "^4.1.2", + "@smithy/util-utf8": "^4.1.0", + "@smithy/util-waiter": "^4.1.1", "tslib": "^2.6.2" }, "engines": { @@ -267,49 +253,49 @@ } }, "node_modules/@aws-sdk/client-sso": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.864.0.tgz", - "integrity": "sha512-THiOp0OpQROEKZ6IdDCDNNh3qnNn/kFFaTSOiugDpgcE5QdsOxh1/RXq7LmHpTJum3cmnFf8jG59PHcz9Tjnlw==", + "version": "3.896.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.896.0.tgz", + "integrity": "sha512-mpE3mrNili1dcvEvxaYjyoib8HlRXkb2bY5a3WeK++KObFY+HUujKtgQmiNSRX5YwQszm//fTrmGMmv9zpMcKg==", "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.864.0", - "@aws-sdk/middleware-host-header": "3.862.0", - "@aws-sdk/middleware-logger": "3.862.0", - "@aws-sdk/middleware-recursion-detection": "3.862.0", - "@aws-sdk/middleware-user-agent": "3.864.0", - "@aws-sdk/region-config-resolver": "3.862.0", - "@aws-sdk/types": "3.862.0", - "@aws-sdk/util-endpoints": "3.862.0", - "@aws-sdk/util-user-agent-browser": "3.862.0", - "@aws-sdk/util-user-agent-node": "3.864.0", - "@smithy/config-resolver": "^4.1.5", - "@smithy/core": "^3.8.0", - "@smithy/fetch-http-handler": "^5.1.1", - "@smithy/hash-node": "^4.0.5", - "@smithy/invalid-dependency": "^4.0.5", - "@smithy/middleware-content-length": "^4.0.5", - "@smithy/middleware-endpoint": "^4.1.18", - "@smithy/middleware-retry": "^4.1.19", - "@smithy/middleware-serde": "^4.0.9", - "@smithy/middleware-stack": "^4.0.5", - "@smithy/node-config-provider": "^4.1.4", - "@smithy/node-http-handler": "^4.1.1", - "@smithy/protocol-http": "^5.1.3", - "@smithy/smithy-client": "^4.4.10", - "@smithy/types": "^4.3.2", - "@smithy/url-parser": "^4.0.5", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.26", - "@smithy/util-defaults-mode-node": "^4.0.26", - "@smithy/util-endpoints": "^3.0.7", - "@smithy/util-middleware": "^4.0.5", - "@smithy/util-retry": "^4.0.7", - "@smithy/util-utf8": "^4.0.0", + "@aws-sdk/core": "3.896.0", + "@aws-sdk/middleware-host-header": "3.893.0", + "@aws-sdk/middleware-logger": "3.893.0", + "@aws-sdk/middleware-recursion-detection": "3.893.0", + "@aws-sdk/middleware-user-agent": "3.896.0", + "@aws-sdk/region-config-resolver": "3.893.0", + "@aws-sdk/types": "3.893.0", + "@aws-sdk/util-endpoints": "3.895.0", + "@aws-sdk/util-user-agent-browser": "3.893.0", + "@aws-sdk/util-user-agent-node": "3.896.0", + "@smithy/config-resolver": "^4.2.2", + "@smithy/core": "^3.12.0", + "@smithy/fetch-http-handler": "^5.2.1", + "@smithy/hash-node": "^4.1.1", + "@smithy/invalid-dependency": "^4.1.1", + "@smithy/middleware-content-length": "^4.1.1", + "@smithy/middleware-endpoint": "^4.2.4", + "@smithy/middleware-retry": "^4.3.0", + "@smithy/middleware-serde": "^4.1.1", + "@smithy/middleware-stack": "^4.1.1", + "@smithy/node-config-provider": "^4.2.2", + "@smithy/node-http-handler": "^4.2.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/smithy-client": "^4.6.4", + "@smithy/types": "^4.5.0", + "@smithy/url-parser": "^4.1.1", + "@smithy/util-base64": "^4.1.0", + "@smithy/util-body-length-browser": "^4.1.0", + "@smithy/util-body-length-node": "^4.1.0", + "@smithy/util-defaults-mode-browser": "^4.1.4", + "@smithy/util-defaults-mode-node": "^4.1.4", + "@smithy/util-endpoints": "^3.1.2", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-retry": "^4.1.2", + "@smithy/util-utf8": "^4.1.0", "tslib": "^2.6.2" }, "engines": { @@ -317,26 +303,24 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.864.0.tgz", - "integrity": "sha512-LFUREbobleHEln+Zf7IG83lAZwvHZG0stI7UU0CtwyuhQy5Yx0rKksHNOCmlM7MpTEbSCfntEhYi3jUaY5e5lg==", + "version": "3.896.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.896.0.tgz", + "integrity": "sha512-uJaoyWKeGNyCyeI+cIJrD7LEB4iF/W8/x2ij7zg32OFpAAJx96N34/e+XSKp/xkJpO5FKiBOskKLnHeUsJsAPA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.862.0", - "@aws-sdk/xml-builder": "3.862.0", - "@smithy/core": "^3.8.0", - "@smithy/node-config-provider": "^4.1.4", - "@smithy/property-provider": "^4.0.5", - "@smithy/protocol-http": "^5.1.3", - "@smithy/signature-v4": "^5.1.3", - "@smithy/smithy-client": "^4.4.10", - "@smithy/types": "^4.3.2", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-middleware": "^4.0.5", - "@smithy/util-utf8": "^4.0.0", - "fast-xml-parser": "5.2.5", + "@aws-sdk/types": "3.893.0", + "@aws-sdk/xml-builder": "3.894.0", + "@smithy/core": "^3.12.0", + "@smithy/node-config-provider": "^4.2.2", + "@smithy/property-provider": "^4.1.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/signature-v4": "^5.2.1", + "@smithy/smithy-client": "^4.6.4", + "@smithy/types": "^4.5.0", + "@smithy/util-base64": "^4.1.0", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-utf8": "^4.1.0", "tslib": "^2.6.2" }, "engines": { @@ -344,16 +328,16 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.864.0.tgz", - "integrity": "sha512-StJPOI2Rt8UE6lYjXUpg6tqSZaM72xg46ljPg8kIevtBAAfdtq9K20qT/kSliWGIBocMFAv0g2mC0hAa+ECyvg==", + "version": "3.896.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.896.0.tgz", + "integrity": "sha512-Cnqhupdkp825ICySrz4QTI64Nq3AmUAscPW8dueanni0avYBDp7RBppX4H0+6icqN569B983XNfQ0YSImQhfhg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.864.0", - "@aws-sdk/types": "3.862.0", - "@smithy/property-provider": "^4.0.5", - "@smithy/types": "^4.3.2", + "@aws-sdk/core": "3.896.0", + "@aws-sdk/types": "3.893.0", + "@smithy/property-provider": "^4.1.1", + "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, "engines": { @@ -361,21 +345,21 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.864.0.tgz", - "integrity": "sha512-E/RFVxGTuGnuD+9pFPH2j4l6HvrXzPhmpL8H8nOoJUosjx7d4v93GJMbbl1v/fkDLqW9qN4Jx2cI6PAjohA6OA==", + "version": "3.896.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.896.0.tgz", + "integrity": "sha512-CN0fTCKCUA1OTSx1c76o8XyJCy2WoI/av3J8r8mL6GmxTerhLRyzDy/MwxzPjTYPoL+GLEg6V4a9fRkWj1hBUA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.864.0", - "@aws-sdk/types": "3.862.0", - "@smithy/fetch-http-handler": "^5.1.1", - "@smithy/node-http-handler": "^4.1.1", - "@smithy/property-provider": "^4.0.5", - "@smithy/protocol-http": "^5.1.3", - "@smithy/smithy-client": "^4.4.10", - "@smithy/types": "^4.3.2", - "@smithy/util-stream": "^4.2.4", + "@aws-sdk/core": "3.896.0", + "@aws-sdk/types": "3.893.0", + "@smithy/fetch-http-handler": "^5.2.1", + "@smithy/node-http-handler": "^4.2.1", + "@smithy/property-provider": "^4.1.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/smithy-client": "^4.6.4", + "@smithy/types": "^4.5.0", + "@smithy/util-stream": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -383,24 +367,24 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.864.0.tgz", - "integrity": "sha512-PlxrijguR1gxyPd5EYam6OfWLarj2MJGf07DvCx9MAuQkw77HBnsu6+XbV8fQriFuoJVTBLn9ROhMr/ROAYfUg==", + "version": "3.896.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.896.0.tgz", + "integrity": "sha512-+rbYG98czzwZLTYHJasK+VBjnIeXk73mRpZXHvaa4kDNxBezdN2YsoGNpLlPSxPdbpq18LY3LRtkdFTaT6DIQA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.864.0", - "@aws-sdk/credential-provider-env": "3.864.0", - "@aws-sdk/credential-provider-http": "3.864.0", - "@aws-sdk/credential-provider-process": "3.864.0", - "@aws-sdk/credential-provider-sso": "3.864.0", - "@aws-sdk/credential-provider-web-identity": "3.864.0", - "@aws-sdk/nested-clients": "3.864.0", - "@aws-sdk/types": "3.862.0", - "@smithy/credential-provider-imds": "^4.0.7", - "@smithy/property-provider": "^4.0.5", - "@smithy/shared-ini-file-loader": "^4.0.5", - "@smithy/types": "^4.3.2", + "@aws-sdk/core": "3.896.0", + "@aws-sdk/credential-provider-env": "3.896.0", + "@aws-sdk/credential-provider-http": "3.896.0", + "@aws-sdk/credential-provider-process": "3.896.0", + "@aws-sdk/credential-provider-sso": "3.896.0", + "@aws-sdk/credential-provider-web-identity": "3.896.0", + "@aws-sdk/nested-clients": "3.896.0", + "@aws-sdk/types": "3.893.0", + "@smithy/credential-provider-imds": "^4.1.2", + "@smithy/property-provider": "^4.1.1", + "@smithy/shared-ini-file-loader": "^4.2.0", + "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, "engines": { @@ -408,23 +392,23 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.864.0.tgz", - "integrity": "sha512-2BEymFeXURS+4jE9tP3vahPwbYRl0/1MVaFZcijj6pq+nf5EPGvkFillbdBRdc98ZI2NedZgSKu3gfZXgYdUhQ==", + "version": "3.896.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.896.0.tgz", + "integrity": "sha512-J0Jm+56MNngk1PIyqoJFf5FC2fjA4CYXlqODqNRDtid7yk7HB9W3UTtvxofmii5KJOLcHGNPdGnHWKkUc+xYgw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "3.864.0", - "@aws-sdk/credential-provider-http": "3.864.0", - "@aws-sdk/credential-provider-ini": "3.864.0", - "@aws-sdk/credential-provider-process": "3.864.0", - "@aws-sdk/credential-provider-sso": "3.864.0", - "@aws-sdk/credential-provider-web-identity": "3.864.0", - "@aws-sdk/types": "3.862.0", - "@smithy/credential-provider-imds": "^4.0.7", - "@smithy/property-provider": "^4.0.5", - "@smithy/shared-ini-file-loader": "^4.0.5", - "@smithy/types": "^4.3.2", + "@aws-sdk/credential-provider-env": "3.896.0", + "@aws-sdk/credential-provider-http": "3.896.0", + "@aws-sdk/credential-provider-ini": "3.896.0", + "@aws-sdk/credential-provider-process": "3.896.0", + "@aws-sdk/credential-provider-sso": "3.896.0", + "@aws-sdk/credential-provider-web-identity": "3.896.0", + "@aws-sdk/types": "3.893.0", + "@smithy/credential-provider-imds": "^4.1.2", + "@smithy/property-provider": "^4.1.1", + "@smithy/shared-ini-file-loader": "^4.2.0", + "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, "engines": { @@ -432,17 +416,17 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.864.0.tgz", - "integrity": "sha512-Zxnn1hxhq7EOqXhVYgkF4rI9MnaO3+6bSg/tErnBQ3F8kDpA7CFU24G1YxwaJXp2X4aX3LwthefmSJHwcVP/2g==", + "version": "3.896.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.896.0.tgz", + "integrity": "sha512-UfWVMQPZy7dus40c4LWxh5vQ+I51z0q4vf09Eqas5848e9DrGRG46GYIuc/gy+4CqEypjbg/XNMjnZfGLHxVnQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.864.0", - "@aws-sdk/types": "3.862.0", - "@smithy/property-provider": "^4.0.5", - "@smithy/shared-ini-file-loader": "^4.0.5", - "@smithy/types": "^4.3.2", + "@aws-sdk/core": "3.896.0", + "@aws-sdk/types": "3.893.0", + "@smithy/property-provider": "^4.1.1", + "@smithy/shared-ini-file-loader": "^4.2.0", + "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, "engines": { @@ -450,19 +434,19 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.864.0.tgz", - "integrity": "sha512-UPyPNQbxDwHVGmgWdGg9/9yvzuedRQVF5jtMkmP565YX9pKZ8wYAcXhcYdNPWFvH0GYdB0crKOmvib+bmCuwkw==", + "version": "3.896.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.896.0.tgz", + "integrity": "sha512-77Te8WrVdLABKlv7QyetXP6aYEX1UORiahLA1PXQb/p66aFBw18Xc6JiN/6zJ4RqdyV1Xr9rwYBwGYua93ANIA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.864.0", - "@aws-sdk/core": "3.864.0", - "@aws-sdk/token-providers": "3.864.0", - "@aws-sdk/types": "3.862.0", - "@smithy/property-provider": "^4.0.5", - "@smithy/shared-ini-file-loader": "^4.0.5", - "@smithy/types": "^4.3.2", + "@aws-sdk/client-sso": "3.896.0", + "@aws-sdk/core": "3.896.0", + "@aws-sdk/token-providers": "3.896.0", + "@aws-sdk/types": "3.893.0", + "@smithy/property-provider": "^4.1.1", + "@smithy/shared-ini-file-loader": "^4.2.0", + "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, "engines": { @@ -470,17 +454,18 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.864.0.tgz", - "integrity": "sha512-nNcjPN4SYg8drLwqK0vgVeSvxeGQiD0FxOaT38mV2H8cu0C5NzpvA+14Xy+W6vT84dxgmJYKk71Cr5QL2Oz+rA==", + "version": "3.896.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.896.0.tgz", + "integrity": "sha512-gwMwZWumo+V0xJplO8j2HIb1TfPsF9fbcRGXS0CanEvjg4fF2Xs1pOQl2oCw3biPZpxHB0plNZjqSF2eneGg9g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.864.0", - "@aws-sdk/nested-clients": "3.864.0", - "@aws-sdk/types": "3.862.0", - "@smithy/property-provider": "^4.0.5", - "@smithy/types": "^4.3.2", + "@aws-sdk/core": "3.896.0", + "@aws-sdk/nested-clients": "3.896.0", + "@aws-sdk/types": "3.893.0", + "@smithy/property-provider": "^4.1.1", + "@smithy/shared-ini-file-loader": "^4.2.0", + "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, "engines": { @@ -488,15 +473,15 @@ } }, "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.862.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.862.0.tgz", - "integrity": "sha512-jDje8dCFeFHfuCAxMDXBs8hy8q9NCTlyK4ThyyfAj3U4Pixly2mmzY2u7b7AyGhWsjJNx8uhTjlYq5zkQPQCYw==", + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.893.0.tgz", + "integrity": "sha512-qL5xYRt80ahDfj9nDYLhpCNkDinEXvjLe/Qen/Y/u12+djrR2MB4DRa6mzBCkLkdXDtf0WAoW2EZsNCfGrmOEQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.862.0", - "@smithy/protocol-http": "^5.1.3", - "@smithy/types": "^4.3.2", + "@aws-sdk/types": "3.893.0", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, "engines": { @@ -504,14 +489,14 @@ } }, "node_modules/@aws-sdk/middleware-logger": { - "version": "3.862.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.862.0.tgz", - "integrity": "sha512-N/bXSJznNBR/i7Ofmf9+gM6dx/SPBK09ZWLKsW5iQjqKxAKn/2DozlnE54uiEs1saHZWoNDRg69Ww4XYYSlG1Q==", + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.893.0.tgz", + "integrity": "sha512-ZqzMecjju5zkBquSIfVfCORI/3Mge21nUY4nWaGQy+NUXehqCGG4W7AiVpiHGOcY2cGJa7xeEkYcr2E2U9U0AA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.862.0", - "@smithy/types": "^4.3.2", + "@aws-sdk/types": "3.893.0", + "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, "engines": { @@ -519,15 +504,16 @@ } }, "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.862.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.862.0.tgz", - "integrity": "sha512-KVoo3IOzEkTq97YKM4uxZcYFSNnMkhW/qj22csofLegZi5fk90ztUnnaeKfaEJHfHp/tm1Y3uSoOXH45s++kKQ==", + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.893.0.tgz", + "integrity": "sha512-H7Zotd9zUHQAr/wr3bcWHULYhEeoQrF54artgsoUGIf/9emv6LzY89QUccKIxYd6oHKNTrTyXm9F0ZZrzXNxlg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.862.0", - "@smithy/protocol-http": "^5.1.3", - "@smithy/types": "^4.3.2", + "@aws-sdk/types": "3.893.0", + "@aws/lambda-invoke-store": "^0.0.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, "engines": { @@ -535,18 +521,18 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.864.0.tgz", - "integrity": "sha512-wrddonw4EyLNSNBrApzEhpSrDwJiNfjxDm5E+bn8n32BbAojXASH8W8jNpxz/jMgNkkJNxCfyqybGKzBX0OhbQ==", + "version": "3.896.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.896.0.tgz", + "integrity": "sha512-so/3tZH34YIeqG/QJgn5ZinnmHRdXV1ehsj4wVUrezL/dVW86jfwIkQIwpw8roOC657UoUf91c9FDhCxs3J5aQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.864.0", - "@aws-sdk/types": "3.862.0", - "@aws-sdk/util-endpoints": "3.862.0", - "@smithy/core": "^3.8.0", - "@smithy/protocol-http": "^5.1.3", - "@smithy/types": "^4.3.2", + "@aws-sdk/core": "3.896.0", + "@aws-sdk/types": "3.893.0", + "@aws-sdk/util-endpoints": "3.895.0", + "@smithy/core": "^3.12.0", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, "engines": { @@ -554,49 +540,49 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.864.0.tgz", - "integrity": "sha512-H1C+NjSmz2y8Tbgh7Yy89J20yD/hVyk15hNoZDbCYkXg0M358KS7KVIEYs8E2aPOCr1sK3HBE819D/yvdMgokA==", + "version": "3.896.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.896.0.tgz", + "integrity": "sha512-KaHALB6DIXScJL/ExmonADr3jtTV6dpOHoEeTRSskJ/aW+rhZo7kH8SLmrwOT/qX8d5tza17YyR/oRkIKY6Eaw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.864.0", - "@aws-sdk/middleware-host-header": "3.862.0", - "@aws-sdk/middleware-logger": "3.862.0", - "@aws-sdk/middleware-recursion-detection": "3.862.0", - "@aws-sdk/middleware-user-agent": "3.864.0", - "@aws-sdk/region-config-resolver": "3.862.0", - "@aws-sdk/types": "3.862.0", - "@aws-sdk/util-endpoints": "3.862.0", - "@aws-sdk/util-user-agent-browser": "3.862.0", - "@aws-sdk/util-user-agent-node": "3.864.0", - "@smithy/config-resolver": "^4.1.5", - "@smithy/core": "^3.8.0", - "@smithy/fetch-http-handler": "^5.1.1", - "@smithy/hash-node": "^4.0.5", - "@smithy/invalid-dependency": "^4.0.5", - "@smithy/middleware-content-length": "^4.0.5", - "@smithy/middleware-endpoint": "^4.1.18", - "@smithy/middleware-retry": "^4.1.19", - "@smithy/middleware-serde": "^4.0.9", - "@smithy/middleware-stack": "^4.0.5", - "@smithy/node-config-provider": "^4.1.4", - "@smithy/node-http-handler": "^4.1.1", - "@smithy/protocol-http": "^5.1.3", - "@smithy/smithy-client": "^4.4.10", - "@smithy/types": "^4.3.2", - "@smithy/url-parser": "^4.0.5", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.26", - "@smithy/util-defaults-mode-node": "^4.0.26", - "@smithy/util-endpoints": "^3.0.7", - "@smithy/util-middleware": "^4.0.5", - "@smithy/util-retry": "^4.0.7", - "@smithy/util-utf8": "^4.0.0", + "@aws-sdk/core": "3.896.0", + "@aws-sdk/middleware-host-header": "3.893.0", + "@aws-sdk/middleware-logger": "3.893.0", + "@aws-sdk/middleware-recursion-detection": "3.893.0", + "@aws-sdk/middleware-user-agent": "3.896.0", + "@aws-sdk/region-config-resolver": "3.893.0", + "@aws-sdk/types": "3.893.0", + "@aws-sdk/util-endpoints": "3.895.0", + "@aws-sdk/util-user-agent-browser": "3.893.0", + "@aws-sdk/util-user-agent-node": "3.896.0", + "@smithy/config-resolver": "^4.2.2", + "@smithy/core": "^3.12.0", + "@smithy/fetch-http-handler": "^5.2.1", + "@smithy/hash-node": "^4.1.1", + "@smithy/invalid-dependency": "^4.1.1", + "@smithy/middleware-content-length": "^4.1.1", + "@smithy/middleware-endpoint": "^4.2.4", + "@smithy/middleware-retry": "^4.3.0", + "@smithy/middleware-serde": "^4.1.1", + "@smithy/middleware-stack": "^4.1.1", + "@smithy/node-config-provider": "^4.2.2", + "@smithy/node-http-handler": "^4.2.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/smithy-client": "^4.6.4", + "@smithy/types": "^4.5.0", + "@smithy/url-parser": "^4.1.1", + "@smithy/util-base64": "^4.1.0", + "@smithy/util-body-length-browser": "^4.1.0", + "@smithy/util-body-length-node": "^4.1.0", + "@smithy/util-defaults-mode-browser": "^4.1.4", + "@smithy/util-defaults-mode-node": "^4.1.4", + "@smithy/util-endpoints": "^3.1.2", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-retry": "^4.1.2", + "@smithy/util-utf8": "^4.1.0", "tslib": "^2.6.2" }, "engines": { @@ -604,17 +590,17 @@ } }, "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.862.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.862.0.tgz", - "integrity": "sha512-VisR+/HuVFICrBPY+q9novEiE4b3mvDofWqyvmxHcWM7HumTz9ZQSuEtnlB/92GVM3KDUrR9EmBHNRrfXYZkcQ==", + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.893.0.tgz", + "integrity": "sha512-/cJvh3Zsa+Of0Zbg7vl9wp/kZtdb40yk/2+XcroAMVPO9hPvmS9r/UOm6tO7FeX4TtkRFwWaQJiTZTgSdsPY+Q==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.862.0", - "@smithy/node-config-provider": "^4.1.4", - "@smithy/types": "^4.3.2", - "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.0.5", + "@aws-sdk/types": "3.893.0", + "@smithy/node-config-provider": "^4.2.2", + "@smithy/types": "^4.5.0", + "@smithy/util-config-provider": "^4.1.0", + "@smithy/util-middleware": "^4.1.1", "tslib": "^2.6.2" }, "engines": { @@ -622,18 +608,18 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.864.0.tgz", - "integrity": "sha512-gTc2QHOBo05SCwVA65dUtnJC6QERvFaPiuppGDSxoF7O5AQNK0UR/kMSenwLqN8b5E1oLYvQTv3C1idJLRX0cg==", + "version": "3.896.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.896.0.tgz", + "integrity": "sha512-WBoD+RY7tUfW9M+wGrZ2vdveR+ziZOjGHWFY3lcGnDvI8KE+fcSccEOTxgJBNBS5Z8B+WHKU2sZjb+Z7QqGwjw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.864.0", - "@aws-sdk/nested-clients": "3.864.0", - "@aws-sdk/types": "3.862.0", - "@smithy/property-provider": "^4.0.5", - "@smithy/shared-ini-file-loader": "^4.0.5", - "@smithy/types": "^4.3.2", + "@aws-sdk/core": "3.896.0", + "@aws-sdk/nested-clients": "3.896.0", + "@aws-sdk/types": "3.893.0", + "@smithy/property-provider": "^4.1.1", + "@smithy/shared-ini-file-loader": "^4.2.0", + "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, "engines": { @@ -641,13 +627,13 @@ } }, "node_modules/@aws-sdk/types": { - "version": "3.862.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.862.0.tgz", - "integrity": "sha512-Bei+RL0cDxxV+lW2UezLbCYYNeJm6Nzee0TpW0FfyTRBhH9C1XQh4+x+IClriXvgBnRquTMMYsmJfvx8iyLKrg==", + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.893.0.tgz", + "integrity": "sha512-Aht1nn5SnA0N+Tjv0dzhAY7CQbxVtmq1bBR6xI0MhG7p2XYVh1wXuKTzrldEvQWwA3odOYunAfT9aBiKZx9qIg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.2", + "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, "engines": { @@ -655,16 +641,16 @@ } }, "node_modules/@aws-sdk/util-endpoints": { - "version": "3.862.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.862.0.tgz", - "integrity": "sha512-eCZuScdE9MWWkHGM2BJxm726MCmWk/dlHjOKvkM0sN1zxBellBMw5JohNss1Z8/TUmnW2gb9XHTOiHuGjOdksA==", + "version": "3.895.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.895.0.tgz", + "integrity": "sha512-MhxBvWbwxmKknuggO2NeMwOVkHOYL98pZ+1ZRI5YwckoCL3AvISMnPJgfN60ww6AIXHGpkp+HhpFdKOe8RHSEg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.862.0", - "@smithy/types": "^4.3.2", - "@smithy/url-parser": "^4.0.5", - "@smithy/util-endpoints": "^3.0.7", + "@aws-sdk/types": "3.893.0", + "@smithy/types": "^4.5.0", + "@smithy/url-parser": "^4.1.1", + "@smithy/util-endpoints": "^3.1.2", "tslib": "^2.6.2" }, "engines": { @@ -672,9 +658,9 @@ } }, "node_modules/@aws-sdk/util-locate-window": { - "version": "3.804.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.804.0.tgz", - "integrity": "sha512-zVoRfpmBVPodYlnMjgVjfGoEZagyRF5IPn3Uo6ZvOZp24chnW/FRstH7ESDHDDRga4z3V+ElUQHKpFDXWyBW5A==", + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.893.0.tgz", + "integrity": "sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -685,29 +671,29 @@ } }, "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.862.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.862.0.tgz", - "integrity": "sha512-BmPTlm0r9/10MMr5ND9E92r8KMZbq5ltYXYpVcUbAsnB1RJ8ASJuRoLne5F7mB3YMx0FJoOTuSq7LdQM3LgW3Q==", + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.893.0.tgz", + "integrity": "sha512-PE9NtbDBW6Kgl1bG6A5fF3EPo168tnkj8TgMcT0sg4xYBWsBpq0bpJZRh+Jm5Bkwiw9IgTCLjEU7mR6xWaMB9w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.862.0", - "@smithy/types": "^4.3.2", + "@aws-sdk/types": "3.893.0", + "@smithy/types": "^4.5.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.864.0.tgz", - "integrity": "sha512-d+FjUm2eJEpP+FRpVR3z6KzMdx1qwxEYDz8jzNKwxYLBBquaBaP/wfoMtMQKAcbrR7aT9FZVZF7zDgzNxUvQlQ==", + "version": "3.896.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.896.0.tgz", + "integrity": "sha512-jegizucAwoxyBddKl0kRGNEgRHcfGuMeyhP1Nf+wIUmHz/9CxobIajqcVk/KRNLdZY5mSn7YG2VtP3z0BcBb0w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.864.0", - "@aws-sdk/types": "3.862.0", - "@smithy/node-config-provider": "^4.1.4", - "@smithy/types": "^4.3.2", + "@aws-sdk/middleware-user-agent": "3.896.0", + "@aws-sdk/types": "3.893.0", + "@smithy/node-config-provider": "^4.2.2", + "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, "engines": { @@ -723,19 +709,30 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.862.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.862.0.tgz", - "integrity": "sha512-6Ed0kmC1NMbuFTEgNmamAUU1h5gShgxL1hBVLbEzUa3trX5aJBz1vU4bXaBTvOYUAnOHtiy1Ml4AMStd6hJnFA==", + "version": "3.894.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.894.0.tgz", + "integrity": "sha512-E6EAMc9dT1a2DOdo4zyOf3fp5+NJ2wI+mcm7RaW1baFIWDwcb99PpvWoV7YEiK7oaBDshuOEGWKUSYXdW+JYgA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.2", + "@smithy/types": "^4.5.0", + "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.0.1.tgz", + "integrity": "sha512-ORHRQ2tmvnBXc8t/X9Z8IcSbBA4xTLKuN873FopzklHMeqBst7YG0d+AX97inkvDX+NChYtSr+qGfcqGFaI8Zw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@azure-rest/core-client": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@azure-rest/core-client/-/core-client-2.5.1.tgz", @@ -960,33 +957,33 @@ } }, "node_modules/@azure/msal-browser": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.23.0.tgz", - "integrity": "sha512-uHnfRwGAEHaYVXzpCtYsruy6PQxL2v76+MJ3+n/c/3PaTiTIa5ch7VofTUNoA39nHyjJbdiqTwFZK40OOTOkjw==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.24.0.tgz", + "integrity": "sha512-BNoiUEx4olj16U9ZiquvIhG1dZBnwWSzSXiSclq/9qiFQXYeLOKqEaEv98+xLXJ3oLw9APwHTR1eY2Qk0v6XBQ==", "license": "MIT", "dependencies": { - "@azure/msal-common": "15.12.0" + "@azure/msal-common": "15.13.0" }, "engines": { "node": ">=0.8.0" } }, "node_modules/@azure/msal-common": { - "version": "15.12.0", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.12.0.tgz", - "integrity": "sha512-4ucXbjVw8KJ5QBgnGJUeA07c8iznwlk5ioHIhI4ASXcXgcf2yRFhWzYOyWg/cI49LC9ekpFJeQtO3zjDTbl6TQ==", + "version": "15.13.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.13.0.tgz", + "integrity": "sha512-8oF6nj02qX7eE/6+wFT5NluXRHc05AgdCC3fJnkjiJooq8u7BcLmxaYYSwc2AfEkWRMRi6Eyvvbeqk4U4412Ag==", "license": "MIT", "engines": { "node": ">=0.8.0" } }, "node_modules/@azure/msal-node": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.7.4.tgz", - "integrity": "sha512-fjqvhrThwzzPvqhFOdkkGRJCHPQZTNijpceVy8QjcfQuH482tOVEjHyamZaioOhVtx+FK1u+eMpJA2Zz4U9LVg==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.8.0.tgz", + "integrity": "sha512-23BXm82Mp5XnRhrcd4mrHa0xuUNRp96ivu3nRatrfdAqjoeWAGyD0eEAafxAOHAEWWmdlyFK4ELFcdziXyw2sA==", "license": "MIT", "dependencies": { - "@azure/msal-common": "15.12.0", + "@azure/msal-common": "15.13.0", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" }, @@ -994,15 +991,6 @@ "node": ">=16" } }, - "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==", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -1019,9 +1007,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", - "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", "dev": true, "license": "MIT", "engines": { @@ -1029,22 +1017,22 @@ } }, "node_modules/@babel/core": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", - "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.3", - "@babel/parser": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.3", - "@babel/types": "^7.28.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -1196,27 +1184,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz", - "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2" + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", - "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.2" + "@babel/types": "^7.28.4" }, "bin": { "parser": "bin/babel-parser.js" @@ -1480,18 +1468,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz", - "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.3", + "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2", + "@babel/types": "^7.28.4", "debug": "^4.3.1" }, "engines": { @@ -1499,9 +1487,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", - "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1564,9 +1552,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, "license": "MIT", "dependencies": { @@ -1834,6 +1822,13 @@ "node": ">=8" } }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -2147,6 +2142,17 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -2165,9 +2171,9 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.30", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", - "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", "dependencies": { @@ -2243,66 +2249,88 @@ } }, "node_modules/@prisma/client": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", - "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", + "version": "6.16.2", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.16.2.tgz", + "integrity": "sha512-E00PxBcalMfYO/TWnXobBVUai6eW/g5OsifWQsQDzJYm7yaY+IRLo7ZLsaefi0QkTpxfuhFcQ/w180i6kX3iJw==", "hasInstallScript": true, "license": "Apache-2.0", "engines": { - "node": ">=16.13" + "node": ">=18.18" }, "peerDependencies": { - "prisma": "*" + "prisma": "*", + "typescript": ">=5.1.0" }, "peerDependenciesMeta": { "prisma": { "optional": true + }, + "typescript": { + "optional": true } } }, + "node_modules/@prisma/config": { + "version": "6.16.2", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.16.2.tgz", + "integrity": "sha512-mKXSUrcqXj0LXWPmJsK2s3p9PN+aoAbyMx7m5E1v1FufofR1ZpPoIArjjzOIm+bJRLLvYftoNYLx1tbHgF9/yg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.16.12", + "empathic": "2.0.0" + } + }, "node_modules/@prisma/debug": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", - "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "version": "6.16.2", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.16.2.tgz", + "integrity": "sha512-bo4/gA/HVV6u8YK2uY6glhNsJ7r+k/i5iQ9ny/3q5bt9ijCj7WMPUwfTKPvtEgLP+/r26Z686ly11hhcLiQ8zA==", + "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/engines": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", - "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "version": "6.16.2", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.16.2.tgz", + "integrity": "sha512-7yf3AjfPUgsg/l7JSu1iEhsmZZ/YE00yURPjTikqm2z4btM0bCl2coFtTGfeSOWbQMmq45Jab+53yGUIAT1sjA==", + "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "5.22.0", - "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", - "@prisma/fetch-engine": "5.22.0", - "@prisma/get-platform": "5.22.0" + "@prisma/debug": "6.16.2", + "@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", + "@prisma/fetch-engine": "6.16.2", + "@prisma/get-platform": "6.16.2" } }, "node_modules/@prisma/engines-version": { - "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", - "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43.tgz", + "integrity": "sha512-ThvlDaKIVrnrv97ujNFDYiQbeMQpLa0O86HFA2mNoip4mtFqM7U5GSz2ie1i2xByZtvPztJlNRgPsXGeM/kqAA==", + "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/fetch-engine": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", - "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "version": "6.16.2", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.16.2.tgz", + "integrity": "sha512-wPnZ8DMRqpgzye758ZvfAMiNJRuYpz+rhgEBZi60ZqDIgOU2694oJxiuu3GKFeYeR/hXxso4/2oBC243t/whxQ==", + "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "5.22.0", - "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", - "@prisma/get-platform": "5.22.0" + "@prisma/debug": "6.16.2", + "@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", + "@prisma/get-platform": "6.16.2" } }, "node_modules/@prisma/get-platform": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", - "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "version": "6.16.2", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.16.2.tgz", + "integrity": "sha512-U/P36Uke5wS7r1+omtAgJpEB94tlT4SdlgaeTc6HVTTT93pXj7zZ+B/cZnmnvjcNPfWddgoDx8RLjmQwqGDYyA==", + "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "5.22.0" + "@prisma/debug": "6.16.2" } }, "node_modules/@redis/bloom": { @@ -2419,13 +2447,13 @@ } }, "node_modules/@smithy/abort-controller": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.5.tgz", - "integrity": "sha512-jcrqdTQurIrBbUm4W2YdLVMQDoL0sA9DTxYd2s+R/y+2U9NLOP7Xf/YqfSg1FZhlZIYEnvk2mwbyvIfdLEPo8g==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.1.1.tgz", + "integrity": "sha512-vkzula+IwRvPR6oKQhMYioM3A/oX/lFCZiwuxkQbRhqJS2S4YRY2k7k/SyR2jMf3607HLtbEwlRxi0ndXHMjRg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.2", + "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, "engines": { @@ -2433,16 +2461,16 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.1.5.tgz", - "integrity": "sha512-viuHMxBAqydkB0AfWwHIdwf/PRH2z5KHGUzqyRtS/Wv+n3IHI993Sk76VCA7dD/+GzgGOmlJDITfPcJC1nIVIw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.2.2.tgz", + "integrity": "sha512-IT6MatgBWagLybZl1xQcURXRICvqz1z3APSCAI9IqdvfCkrA7RaQIEfgC6G/KvfxnDfQUDqFV+ZlixcuFznGBQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.1.4", - "@smithy/types": "^4.3.2", - "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.0.5", + "@smithy/node-config-provider": "^4.2.2", + "@smithy/types": "^4.5.0", + "@smithy/util-config-provider": "^4.1.0", + "@smithy/util-middleware": "^4.1.1", "tslib": "^2.6.2" }, "engines": { @@ -2450,39 +2478,38 @@ } }, "node_modules/@smithy/core": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.8.0.tgz", - "integrity": "sha512-EYqsIYJmkR1VhVE9pccnk353xhs+lB6btdutJEtsp7R055haMJp2yE16eSxw8fv+G0WUY6vqxyYOP8kOqawxYQ==", + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.12.0.tgz", + "integrity": "sha512-zJeAgogZfbwlPGL93y4Z/XNeIN37YCreRUd6YMIRvaq+6RnBK8PPYYIQ85Is/GglPh3kNImD5riDCXbVSDpCiQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/middleware-serde": "^4.0.9", - "@smithy/protocol-http": "^5.1.3", - "@smithy/types": "^4.3.2", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-middleware": "^4.0.5", - "@smithy/util-stream": "^4.2.4", - "@smithy/util-utf8": "^4.0.0", - "@types/uuid": "^9.0.1", - "tslib": "^2.6.2", - "uuid": "^9.0.1" + "@smithy/middleware-serde": "^4.1.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "@smithy/util-base64": "^4.1.0", + "@smithy/util-body-length-browser": "^4.1.0", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-stream": "^4.3.2", + "@smithy/util-utf8": "^4.1.0", + "@smithy/uuid": "^1.0.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.7.tgz", - "integrity": "sha512-dDzrMXA8d8riFNiPvytxn0mNwR4B3h8lgrQ5UjAGu6T9z/kRg/Xncf4tEQHE/+t25sY8IH3CowcmWi+1U5B1Gw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.1.2.tgz", + "integrity": "sha512-JlYNq8TShnqCLg0h+afqe2wLAwZpuoSgOyzhYvTgbiKBWRov+uUve+vrZEQO6lkdLOWPh7gK5dtb9dS+KGendg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.1.4", - "@smithy/property-provider": "^4.0.5", - "@smithy/types": "^4.3.2", - "@smithy/url-parser": "^4.0.5", + "@smithy/node-config-provider": "^4.2.2", + "@smithy/property-provider": "^4.1.1", + "@smithy/types": "^4.5.0", + "@smithy/url-parser": "^4.1.1", "tslib": "^2.6.2" }, "engines": { @@ -2490,16 +2517,16 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.1.1.tgz", - "integrity": "sha512-61WjM0PWmZJR+SnmzaKI7t7G0UkkNFboDpzIdzSoy7TByUzlxo18Qlh9s71qug4AY4hlH/CwXdubMtkcNEb/sQ==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.2.1.tgz", + "integrity": "sha512-5/3wxKNtV3wO/hk1is+CZUhL8a1yy/U+9u9LKQ9kZTkMsHaQjJhc3stFfiujtMnkITjzWfndGA2f7g9Uh9vKng==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.1.3", - "@smithy/querystring-builder": "^4.0.5", - "@smithy/types": "^4.3.2", - "@smithy/util-base64": "^4.0.0", + "@smithy/protocol-http": "^5.2.1", + "@smithy/querystring-builder": "^4.1.1", + "@smithy/types": "^4.5.0", + "@smithy/util-base64": "^4.1.0", "tslib": "^2.6.2" }, "engines": { @@ -2507,15 +2534,15 @@ } }, "node_modules/@smithy/hash-node": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.5.tgz", - "integrity": "sha512-cv1HHkKhpyRb6ahD8Vcfb2Hgz67vNIXEp2vnhzfxLFGRukLCNEA5QdsorbUEzXma1Rco0u3rx5VTqbM06GcZqQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.1.1.tgz", + "integrity": "sha512-H9DIU9WBLhYrvPs9v4sYvnZ1PiAI0oc8CgNQUJ1rpN3pP7QADbTOUjchI2FB764Ub0DstH5xbTqcMJu1pnVqxA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.2", - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@smithy/types": "^4.5.0", + "@smithy/util-buffer-from": "^4.1.0", + "@smithy/util-utf8": "^4.1.0", "tslib": "^2.6.2" }, "engines": { @@ -2523,13 +2550,13 @@ } }, "node_modules/@smithy/invalid-dependency": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.5.tgz", - "integrity": "sha512-IVnb78Qtf7EJpoEVo7qJ8BEXQwgC4n3igeJNNKEj/MLYtapnx8A67Zt/J3RXAj2xSO1910zk0LdFiygSemuLow==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.1.1.tgz", + "integrity": "sha512-1AqLyFlfrrDkyES8uhINRlJXmHA2FkG+3DY8X+rmLSqmFwk3DJnvhyGzyByPyewh2jbmV+TYQBEfngQax8IFGg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.2", + "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, "engines": { @@ -2537,9 +2564,9 @@ } }, "node_modules/@smithy/is-array-buffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", - "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.1.0.tgz", + "integrity": "sha512-ePTYUOV54wMogio+he4pBybe8fwg4sDvEVDBU8ZlHOZXbXK3/C0XfJgUCu6qAZcawv05ZhZzODGUerFBPsPUDQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2550,14 +2577,14 @@ } }, "node_modules/@smithy/middleware-content-length": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.5.tgz", - "integrity": "sha512-l1jlNZoYzoCC7p0zCtBDE5OBXZ95yMKlRlftooE5jPWQn4YBPLgsp+oeHp7iMHaTGoUdFqmHOPa8c9G3gBsRpQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.1.1.tgz", + "integrity": "sha512-9wlfBBgTsRvC2JxLJxv4xDGNBrZuio3AgSl0lSFX7fneW2cGskXTYpFxCdRYD2+5yzmsiTuaAJD1Wp7gWt9y9w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.1.3", - "@smithy/types": "^4.3.2", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, "engines": { @@ -2565,19 +2592,19 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.18.tgz", - "integrity": "sha512-ZhvqcVRPZxnZlokcPaTwb+r+h4yOIOCJmx0v2d1bpVlmP465g3qpVSf7wxcq5zZdu4jb0H4yIMxuPwDJSQc3MQ==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.2.4.tgz", + "integrity": "sha512-FZ4hzupOmthm8Q8ujYrd0I+/MHwVMuSTdkDtIQE0xVuvJt9pLT6Q+b0p4/t+slDyrpcf+Wj7SN+ZqT5OryaaZg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.8.0", - "@smithy/middleware-serde": "^4.0.9", - "@smithy/node-config-provider": "^4.1.4", - "@smithy/shared-ini-file-loader": "^4.0.5", - "@smithy/types": "^4.3.2", - "@smithy/url-parser": "^4.0.5", - "@smithy/util-middleware": "^4.0.5", + "@smithy/core": "^3.12.0", + "@smithy/middleware-serde": "^4.1.1", + "@smithy/node-config-provider": "^4.2.2", + "@smithy/shared-ini-file-loader": "^4.2.0", + "@smithy/types": "^4.5.0", + "@smithy/url-parser": "^4.1.1", + "@smithy/util-middleware": "^4.1.1", "tslib": "^2.6.2" }, "engines": { @@ -2585,36 +2612,35 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.1.19", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.19.tgz", - "integrity": "sha512-X58zx/NVECjeuUB6A8HBu4bhx72EoUz+T5jTMIyeNKx2lf+Gs9TmWPNNkH+5QF0COjpInP/xSpJGJ7xEnAklQQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.3.0.tgz", + "integrity": "sha512-qhEX9745fAxZvtLM4bQJAVC98elWjiMO2OiHl1s6p7hUzS4QfZO1gXUYNwEK8m0J6NoCD5W52ggWxbIDHI0XSg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.1.4", - "@smithy/protocol-http": "^5.1.3", - "@smithy/service-error-classification": "^4.0.7", - "@smithy/smithy-client": "^4.4.10", - "@smithy/types": "^4.3.2", - "@smithy/util-middleware": "^4.0.5", - "@smithy/util-retry": "^4.0.7", - "@types/uuid": "^9.0.1", - "tslib": "^2.6.2", - "uuid": "^9.0.1" + "@smithy/node-config-provider": "^4.2.2", + "@smithy/protocol-http": "^5.2.1", + "@smithy/service-error-classification": "^4.1.2", + "@smithy/smithy-client": "^4.6.4", + "@smithy/types": "^4.5.0", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-retry": "^4.1.2", + "@smithy/uuid": "^1.0.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/middleware-serde": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.9.tgz", - "integrity": "sha512-uAFFR4dpeoJPGz8x9mhxp+RPjo5wW0QEEIPPPbLXiRRWeCATf/Km3gKIVR5vaP8bN1kgsPhcEeh+IZvUlBv6Xg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.1.1.tgz", + "integrity": "sha512-lh48uQdbCoj619kRouev5XbWhCwRKLmphAif16c4J6JgJ4uXjub1PI6RL38d3BLliUvSso6klyB/LTNpWSNIyg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.1.3", - "@smithy/types": "^4.3.2", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, "engines": { @@ -2622,13 +2648,13 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.5.tgz", - "integrity": "sha512-/yoHDXZPh3ocRVyeWQFvC44u8seu3eYzZRveCMfgMOBcNKnAmOvjbL9+Cp5XKSIi9iYA9PECUuW2teDAk8T+OQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.1.1.tgz", + "integrity": "sha512-ygRnniqNcDhHzs6QAPIdia26M7e7z9gpkIMUe/pK0RsrQ7i5MblwxY8078/QCnGq6AmlUUWgljK2HlelsKIb/A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.2", + "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, "engines": { @@ -2636,15 +2662,15 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.1.4.tgz", - "integrity": "sha512-+UDQV/k42jLEPPHSn39l0Bmc4sB1xtdI9Gd47fzo/0PbXzJ7ylgaOByVjF5EeQIumkepnrJyfx86dPa9p47Y+w==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.2.2.tgz", + "integrity": "sha512-SYGTKyPvyCfEzIN5rD8q/bYaOPZprYUPD2f5g9M7OjaYupWOoQFYJ5ho+0wvxIRf471i2SR4GoiZ2r94Jq9h6A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.0.5", - "@smithy/shared-ini-file-loader": "^4.0.5", - "@smithy/types": "^4.3.2", + "@smithy/property-provider": "^4.1.1", + "@smithy/shared-ini-file-loader": "^4.2.0", + "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, "engines": { @@ -2652,16 +2678,16 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.1.1.tgz", - "integrity": "sha512-RHnlHqFpoVdjSPPiYy/t40Zovf3BBHc2oemgD7VsVTFFZrU5erFFe0n52OANZZ/5sbshgD93sOh5r6I35Xmpaw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.2.1.tgz", + "integrity": "sha512-REyybygHlxo3TJICPF89N2pMQSf+p+tBJqpVe1+77Cfi9HBPReNjTgtZ1Vg73exq24vkqJskKDpfF74reXjxfw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.0.5", - "@smithy/protocol-http": "^5.1.3", - "@smithy/querystring-builder": "^4.0.5", - "@smithy/types": "^4.3.2", + "@smithy/abort-controller": "^4.1.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/querystring-builder": "^4.1.1", + "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, "engines": { @@ -2669,13 +2695,13 @@ } }, "node_modules/@smithy/property-provider": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.5.tgz", - "integrity": "sha512-R/bswf59T/n9ZgfgUICAZoWYKBHcsVDurAGX88zsiUtOTA/xUAPyiT+qkNCPwFn43pZqN84M4MiUsbSGQmgFIQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.1.1.tgz", + "integrity": "sha512-gm3ZS7DHxUbzC2wr8MUCsAabyiXY0gaj3ROWnhSx/9sPMc6eYLMM4rX81w1zsMaObj2Lq3PZtNCC1J6lpEY7zg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.2", + "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, "engines": { @@ -2683,13 +2709,13 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.3.tgz", - "integrity": "sha512-fCJd2ZR7D22XhDY0l+92pUag/7je2BztPRQ01gU5bMChcyI0rlly7QFibnYHzcxDvccMjlpM/Q1ev8ceRIb48w==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.2.1.tgz", + "integrity": "sha512-T8SlkLYCwfT/6m33SIU/JOVGNwoelkrvGjFKDSDtVvAXj/9gOT78JVJEas5a+ETjOu4SVvpCstKgd0PxSu/aHw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.2", + "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, "engines": { @@ -2697,14 +2723,14 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.5.tgz", - "integrity": "sha512-NJeSCU57piZ56c+/wY+AbAw6rxCCAOZLCIniRE7wqvndqxcKKDOXzwWjrY7wGKEISfhL9gBbAaWWgHsUGedk+A==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.1.1.tgz", + "integrity": "sha512-J9b55bfimP4z/Jg1gNo+AT84hr90p716/nvxDkPGCD4W70MPms0h8KF50RDRgBGZeL83/u59DWNqJv6tEP/DHA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.2", - "@smithy/util-uri-escape": "^4.0.0", + "@smithy/types": "^4.5.0", + "@smithy/util-uri-escape": "^4.1.0", "tslib": "^2.6.2" }, "engines": { @@ -2712,13 +2738,13 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.5.tgz", - "integrity": "sha512-6SV7md2CzNG/WUeTjVe6Dj8noH32r4MnUeFKZrnVYsQxpGSIcphAanQMayi8jJLZAWm6pdM9ZXvKCpWOsIGg0w==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.1.1.tgz", + "integrity": "sha512-63TEp92YFz0oQ7Pj9IuI3IgnprP92LrZtRAkE3c6wLWJxfy/yOPRt39IOKerVr0JS770olzl0kGafXlAXZ1vng==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.2", + "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, "engines": { @@ -2726,26 +2752,26 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.7.tgz", - "integrity": "sha512-XvRHOipqpwNhEjDf2L5gJowZEm5nsxC16pAZOeEcsygdjv9A2jdOh3YoDQvOXBGTsaJk6mNWtzWalOB9976Wlg==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.1.2.tgz", + "integrity": "sha512-Kqd8wyfmBWHZNppZSMfrQFpc3M9Y/kjyN8n8P4DqJJtuwgK1H914R471HTw7+RL+T7+kI1f1gOnL7Vb5z9+NgQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.2" + "@smithy/types": "^4.5.0" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.5.tgz", - "integrity": "sha512-YVVwehRDuehgoXdEL4r1tAAzdaDgaC9EQvhK0lEbfnbrd0bd5+CTQumbdPryX3J2shT7ZqQE+jPW4lmNBAB8JQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.2.0.tgz", + "integrity": "sha512-OQTfmIEp2LLuWdxa8nEEPhZmiOREO6bcB6pjs0AySf4yiZhl6kMOfqmcwcY8BaBPX+0Tb+tG7/Ia/6mwpoZ7Pw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.2", + "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, "engines": { @@ -2753,19 +2779,19 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.1.3.tgz", - "integrity": "sha512-mARDSXSEgllNzMw6N+mC+r1AQlEBO3meEAkR/UlfAgnMzJUB3goRBWgip1EAMG99wh36MDqzo86SfIX5Y+VEaw==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.2.1.tgz", + "integrity": "sha512-M9rZhWQLjlQVCCR37cSjHfhriGRN+FQ8UfgrYNufv66TJgk+acaggShl3KS5U/ssxivvZLlnj7QH2CUOKlxPyA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.0.0", - "@smithy/protocol-http": "^5.1.3", - "@smithy/types": "^4.3.2", - "@smithy/util-hex-encoding": "^4.0.0", - "@smithy/util-middleware": "^4.0.5", - "@smithy/util-uri-escape": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@smithy/is-array-buffer": "^4.1.0", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "@smithy/util-hex-encoding": "^4.1.0", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-uri-escape": "^4.1.0", + "@smithy/util-utf8": "^4.1.0", "tslib": "^2.6.2" }, "engines": { @@ -2773,18 +2799,18 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.4.10", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.4.10.tgz", - "integrity": "sha512-iW6HjXqN0oPtRS0NK/zzZ4zZeGESIFcxj2FkWed3mcK8jdSdHzvnCKXSjvewESKAgGKAbJRA+OsaqKhkdYRbQQ==", + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.6.4.tgz", + "integrity": "sha512-qL7O3VDyfzCSN9r+sdbQXGhaHtrfSJL30En6Jboj0I3bobf2g1/T0eP2L4qxqrEW26gWhJ4THI4ElVVLjYyBHg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.8.0", - "@smithy/middleware-endpoint": "^4.1.18", - "@smithy/middleware-stack": "^4.0.5", - "@smithy/protocol-http": "^5.1.3", - "@smithy/types": "^4.3.2", - "@smithy/util-stream": "^4.2.4", + "@smithy/core": "^3.12.0", + "@smithy/middleware-endpoint": "^4.2.4", + "@smithy/middleware-stack": "^4.1.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "@smithy/util-stream": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -2792,9 +2818,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.2.tgz", - "integrity": "sha512-QO4zghLxiQ5W9UZmX2Lo0nta2PuE1sSrXUYDoaB6HMR762C0P7v/HEPHf6ZdglTVssJG1bsrSBxdc3quvDSihw==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.5.0.tgz", + "integrity": "sha512-RkUpIOsVlAwUIZXO1dsz8Zm+N72LClFfsNqf173catVlvRZiwPy0x2u0JLEA4byreOPKDZPGjmPDylMoP8ZJRg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2805,14 +2831,14 @@ } }, "node_modules/@smithy/url-parser": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.5.tgz", - "integrity": "sha512-j+733Um7f1/DXjYhCbvNXABV53NyCRRA54C7bNEIxNPs0YjfRxeMKjjgm2jvTYrciZyCjsicHwQ6Q0ylo+NAUw==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.1.1.tgz", + "integrity": "sha512-bx32FUpkhcaKlEoOMbScvc93isaSiRM75pQ5IgIBaMkT7qMlIibpPRONyx/0CvrXHzJLpOn/u6YiDX2hcvs7Dg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/querystring-parser": "^4.0.5", - "@smithy/types": "^4.3.2", + "@smithy/querystring-parser": "^4.1.1", + "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, "engines": { @@ -2820,14 +2846,14 @@ } }, "node_modules/@smithy/util-base64": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.0.0.tgz", - "integrity": "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.1.0.tgz", + "integrity": "sha512-RUGd4wNb8GeW7xk+AY5ghGnIwM96V0l2uzvs/uVHf+tIuVX2WSvynk5CxNoBCsM2rQRSZElAo9rt3G5mJ/gktQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@smithy/util-buffer-from": "^4.1.0", + "@smithy/util-utf8": "^4.1.0", "tslib": "^2.6.2" }, "engines": { @@ -2835,9 +2861,9 @@ } }, "node_modules/@smithy/util-body-length-browser": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.0.0.tgz", - "integrity": "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.1.0.tgz", + "integrity": "sha512-V2E2Iez+bo6bUMOTENPr6eEmepdY8Hbs+Uc1vkDKgKNA/brTJqOW/ai3JO1BGj9GbCeLqw90pbbH7HFQyFotGQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2848,9 +2874,9 @@ } }, "node_modules/@smithy/util-body-length-node": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz", - "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.1.0.tgz", + "integrity": "sha512-BOI5dYjheZdgR9XiEM3HJcEMCXSoqbzu7CzIgYrx0UtmvtC3tC2iDGpJLsSRFffUpy8ymsg2ARMP5fR8mtuUQQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2861,13 +2887,13 @@ } }, "node_modules/@smithy/util-buffer-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", - "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.1.0.tgz", + "integrity": "sha512-N6yXcjfe/E+xKEccWEKzK6M+crMrlwaCepKja0pNnlSkm6SjAeLKKA++er5Ba0I17gvKfN/ThV+ZOx/CntKTVw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.0.0", + "@smithy/is-array-buffer": "^4.1.0", "tslib": "^2.6.2" }, "engines": { @@ -2875,9 +2901,9 @@ } }, "node_modules/@smithy/util-config-provider": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.0.0.tgz", - "integrity": "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.1.0.tgz", + "integrity": "sha512-swXz2vMjrP1ZusZWVTB/ai5gK+J8U0BWvP10v9fpcFvg+Xi/87LHvHfst2IgCs1i0v4qFZfGwCmeD/KNCdJZbQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2888,15 +2914,15 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.0.26", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.26.tgz", - "integrity": "sha512-xgl75aHIS/3rrGp7iTxQAOELYeyiwBu+eEgAk4xfKwJJ0L8VUjhO2shsDpeil54BOFsqmk5xfdesiewbUY5tKQ==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.1.4.tgz", + "integrity": "sha512-mLDJ1s4eA3vwOGaQOEPlg5LB4LdZUUMpB5UMOMofeGhWqiS7WR7dTpLiNi9zVn+YziKUd3Af5NLfxDs7NJqmIw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.0.5", - "@smithy/smithy-client": "^4.4.10", - "@smithy/types": "^4.3.2", + "@smithy/property-provider": "^4.1.1", + "@smithy/smithy-client": "^4.6.4", + "@smithy/types": "^4.5.0", "bowser": "^2.11.0", "tslib": "^2.6.2" }, @@ -2905,18 +2931,18 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.0.26", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.26.tgz", - "integrity": "sha512-z81yyIkGiLLYVDetKTUeCZQ8x20EEzvQjrqJtb/mXnevLq2+w3XCEWTJ2pMp401b6BkEkHVfXb/cROBpVauLMQ==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.1.4.tgz", + "integrity": "sha512-pjX2iMTcOASaSanAd7bu6i3fcMMezr3NTr8Rh64etB0uHRZi+Aw86DoCxPESjY4UTIuA06hhqtTtw95o//imYA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/config-resolver": "^4.1.5", - "@smithy/credential-provider-imds": "^4.0.7", - "@smithy/node-config-provider": "^4.1.4", - "@smithy/property-provider": "^4.0.5", - "@smithy/smithy-client": "^4.4.10", - "@smithy/types": "^4.3.2", + "@smithy/config-resolver": "^4.2.2", + "@smithy/credential-provider-imds": "^4.1.2", + "@smithy/node-config-provider": "^4.2.2", + "@smithy/property-provider": "^4.1.1", + "@smithy/smithy-client": "^4.6.4", + "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, "engines": { @@ -2924,14 +2950,14 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.7.tgz", - "integrity": "sha512-klGBP+RpBp6V5JbrY2C/VKnHXn3d5V2YrifZbmMY8os7M6m8wdYFoO6w/fe5VkP+YVwrEktW3IWYaSQVNZJ8oQ==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.1.2.tgz", + "integrity": "sha512-+AJsaaEGb5ySvf1SKMRrPZdYHRYSzMkCoK16jWnIMpREAnflVspMIDeCVSZJuj+5muZfgGpNpijE3mUNtjv01Q==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.1.4", - "@smithy/types": "^4.3.2", + "@smithy/node-config-provider": "^4.2.2", + "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, "engines": { @@ -2939,9 +2965,9 @@ } }, "node_modules/@smithy/util-hex-encoding": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz", - "integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.1.0.tgz", + "integrity": "sha512-1LcueNN5GYC4tr8mo14yVYbh/Ur8jHhWOxniZXii+1+ePiIbsLZ5fEI0QQGtbRRP5mOhmooos+rLmVASGGoq5w==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2952,13 +2978,13 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.5.tgz", - "integrity": "sha512-N40PfqsZHRSsByGB81HhSo+uvMxEHT+9e255S53pfBw/wI6WKDI7Jw9oyu5tJTLwZzV5DsMha3ji8jk9dsHmQQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.1.1.tgz", + "integrity": "sha512-CGmZ72mL29VMfESz7S6dekqzCh8ZISj3B+w0g1hZFXaOjGTVaSqfAEFAq8EGp8fUL+Q2l8aqNmt8U1tglTikeg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.2", + "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, "engines": { @@ -2966,14 +2992,14 @@ } }, "node_modules/@smithy/util-retry": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.7.tgz", - "integrity": "sha512-TTO6rt0ppK70alZpkjwy+3nQlTiqNfoXja+qwuAchIEAIoSZW8Qyd76dvBv3I5bCpE38APafG23Y/u270NspiQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.1.2.tgz", + "integrity": "sha512-NCgr1d0/EdeP6U5PSZ9Uv5SMR5XRRYoVr1kRVtKZxWL3tixEL3UatrPIMFZSKwHlCcp2zPLDvMubVDULRqeunA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^4.0.7", - "@smithy/types": "^4.3.2", + "@smithy/service-error-classification": "^4.1.2", + "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, "engines": { @@ -2981,19 +3007,19 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.2.4.tgz", - "integrity": "sha512-vSKnvNZX2BXzl0U2RgCLOwWaAP9x/ddd/XobPK02pCbzRm5s55M53uwb1rl/Ts7RXZvdJZerPkA+en2FDghLuQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.3.2.tgz", + "integrity": "sha512-Ka+FA2UCC/Q1dEqUanCdpqwxOFdf5Dg2VXtPtB1qxLcSGh5C1HdzklIt18xL504Wiy9nNUKwDMRTVCbKGoK69g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^5.1.1", - "@smithy/node-http-handler": "^4.1.1", - "@smithy/types": "^4.3.2", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-hex-encoding": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@smithy/fetch-http-handler": "^5.2.1", + "@smithy/node-http-handler": "^4.2.1", + "@smithy/types": "^4.5.0", + "@smithy/util-base64": "^4.1.0", + "@smithy/util-buffer-from": "^4.1.0", + "@smithy/util-hex-encoding": "^4.1.0", + "@smithy/util-utf8": "^4.1.0", "tslib": "^2.6.2" }, "engines": { @@ -3001,9 +3027,9 @@ } }, "node_modules/@smithy/util-uri-escape": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", - "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.1.0.tgz", + "integrity": "sha512-b0EFQkq35K5NHUYxU72JuoheM6+pytEVUGlTwiFxWFpmddA+Bpz3LgsPRIpBk8lnPE47yT7AF2Egc3jVnKLuPg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -3014,13 +3040,13 @@ } }, "node_modules/@smithy/util-utf8": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", - "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.1.0.tgz", + "integrity": "sha512-mEu1/UIXAdNYuBcyEPbjScKi/+MQVXNIuY/7Cm5XLIWe319kDrT5SizBE95jqtmEXoDbGoZxKLCMttdZdqTZKQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-buffer-from": "^4.1.0", "tslib": "^2.6.2" }, "engines": { @@ -3028,20 +3054,40 @@ } }, "node_modules/@smithy/util-waiter": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.0.7.tgz", - "integrity": "sha512-mYqtQXPmrwvUljaHyGxYUIIRI3qjBTEb/f5QFi3A6VlxhpmZd5mWXn9W+qUkf2pVE1Hv3SqxefiZOPGdxmO64A==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.1.1.tgz", + "integrity": "sha512-PJBmyayrlfxM7nbqjomF4YcT1sApQwZio0NHSsT0EzhJqljRmvhzqZua43TyEs80nJk2Cn2FGPg/N8phH6KeCQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.0.5", - "@smithy/types": "^4.3.2", + "@smithy/abort-controller": "^4.1.1", + "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, + "node_modules/@smithy/uuid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.0.0.tgz", + "integrity": "sha512-OlA/yZHh0ekYFnbUkmYBDQPE6fGfdrvgz39ktp8Xf+FA6BfxLejPTMDOG0Nfk5/rDySAz1dRbFf24zaAFYVXlQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "devOptional": true, + "license": "MIT" + }, "node_modules/@tediousjs/connection-string": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/@tediousjs/connection-string/-/connection-string-0.5.0.tgz", @@ -3350,9 +3396,9 @@ } }, "node_modules/@types/node": { - "version": "20.19.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.11.tgz", - "integrity": "sha512-uug3FEEGv0r+jrecvUUpbY8lLisvIjg6AAic6a2bSP5OEOLeJsDSnvhCDov7ipFFMXS3orMpzlmi0ZcuGkBbow==", + "version": "20.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.17.tgz", + "integrity": "sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -3366,9 +3412,9 @@ "license": "MIT" }, "node_modules/@types/nodemailer": { - "version": "6.4.18", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.18.tgz", - "integrity": "sha512-K+OGGXYCxIGkZ59EzoEFkKDkxUT2yQ4f5zgLb+bOJ+pPTZd8M2i/DGMVYrRigUwFnL76URW5VMqMCkgHgjLX0w==", + "version": "6.4.19", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.19.tgz", + "integrity": "sha512-Fi8DwmuAduTk1/1MpkR9EwS0SsDvYXx5RxivAVII1InDCIxmhj/iQm3W8S3EVb/0arnblr6PK0FK4wYa7bwdLg==", "dev": true, "license": "MIT", "dependencies": { @@ -3432,9 +3478,9 @@ } }, "node_modules/@types/semver": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", - "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", "dev": true, "license": "MIT" }, @@ -3498,13 +3544,6 @@ "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", "license": "MIT" }, - "node_modules/@types/uuid": { - "version": "9.0.8", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", - "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -3971,9 +4010,9 @@ } }, "node_modules/axios": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", - "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -4134,6 +4173,16 @@ ], "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.7.tgz", + "integrity": "sha512-bxxN2M3a4d1CRoQC//IqsR5XrLh0IJ8TCv2x6Y9N0nckNz/rTjZB3//GGscZziZOxmjP55rzxg/ze7usFI9FqQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, "node_modules/bcryptjs": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", @@ -4230,9 +4279,9 @@ "license": "MIT" }, "node_modules/bowser": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.0.tgz", - "integrity": "sha512-HcOcTudTeEWgbHh0Y1Tyb6fdeR71m4b/QACf0D4KswGTsNeIJQmg38mRENZPAYPZvGFN3fk3604XbQEPdxXdKg==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.1.tgz", + "integrity": "sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==", "dev": true, "license": "MIT" }, @@ -4260,9 +4309,9 @@ } }, "node_modules/browserslist": { - "version": "4.25.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.3.tgz", - "integrity": "sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==", + "version": "4.26.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz", + "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==", "dev": true, "funding": [ { @@ -4280,9 +4329,10 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001735", - "electron-to-chromium": "^1.5.204", - "node-releases": "^2.0.19", + "baseline-browser-mapping": "^2.8.3", + "caniuse-lite": "^1.0.30001741", + "electron-to-chromium": "^1.5.218", + "node-releases": "^2.0.21", "update-browserslist-db": "^1.1.3" }, "bin": { @@ -4386,6 +4436,65 @@ "node": ">= 0.8" } }, + "node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/c12/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/c12/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "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", @@ -4436,9 +4545,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001735", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001735.tgz", - "integrity": "sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==", + "version": "1.0.30001745", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001745.tgz", + "integrity": "sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==", "dev": true, "funding": [ { @@ -4537,6 +4646,16 @@ "node": ">=8" } }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, "node_modules/cjs-module-lexer": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", @@ -4748,6 +4867,23 @@ "typedarray": "^0.0.6" } }, + "node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -4862,9 +4998,9 @@ } }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4879,9 +5015,9 @@ } }, "node_modules/dedent": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", - "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", + "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -4910,6 +5046,16 @@ "node": ">=0.10.0" } }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/default-browser": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", @@ -4950,6 +5096,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "devOptional": true, + "license": "MIT" + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -4977,6 +5130,13 @@ "node": ">= 0.8" } }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "devOptional": true, + "license": "MIT" + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -5154,10 +5314,21 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/effect": { + "version": "3.16.12", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.16.12.tgz", + "integrity": "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, "node_modules/electron-to-chromium": { - "version": "1.5.207", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.207.tgz", - "integrity": "sha512-mryFrrL/GXDTmAtIVMVf+eIXM09BBPlO5IQ7lUyKmK8d+A4VpRGG+M3ofoVef6qyF8s60rJei8ymlJxjUA8Faw==", + "version": "1.5.224", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.224.tgz", + "integrity": "sha512-kWAoUu/bwzvnhpdZSIc6KUyvkI1rbRXMT0Eq8pKReyOyaPZcctMli+EgvcN1PAvwVc7Tdo4Fxi2PsLNDU05mdg==", "dev": true, "license": "ISC" }, @@ -5181,6 +5352,16 @@ "dev": true, "license": "MIT" }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/enabled": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", @@ -5210,9 +5391,9 @@ } }, "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5635,6 +5816,36 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/exsolve": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", + "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5923,6 +6134,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -6040,6 +6252,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -7276,6 +7506,16 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jiti": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.0.tgz", + "integrity": "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ==", + "devOptional": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/joi": { "version": "17.13.3", "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", @@ -7824,9 +8064,9 @@ } }, "node_modules/mysql2": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.0.tgz", - "integrity": "sha512-tT6pomf5Z/I7Jzxu8sScgrYBMK9bUFWd7Kbo6Fs1L0M13OOIJ/ZobGKS3Z7tQ8Re4lj+LnLXIQVZZxa3fhYKzA==", + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.1.tgz", + "integrity": "sha512-WZMIRZstT2MFfouEaDz/AGFnGi1A2GwaDe7XvKTdRJEYiAHbOrh4S3d8KFmQeh11U85G+BFjIvS1Di5alusZsw==", "license": "MIT", "dependencies": { "aws-ssl-profiles": "^1.1.1", @@ -7918,6 +8158,13 @@ "node": ">=6.0.0" } }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "devOptional": true, + "license": "MIT" + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -7926,9 +8173,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "version": "2.0.21", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz", + "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", "dev": true, "license": "MIT" }, @@ -8040,6 +8287,26 @@ "node": ">=8" } }, + "node_modules/nypm": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz", + "integrity": "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.2", + "pathe": "^2.0.3", + "pkg-types": "^2.3.0", + "tinyexec": "^1.0.1" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": "^14.16.0 || >=16.10.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -8061,6 +8328,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "devOptional": true, + "license": "MIT" + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -8299,6 +8573,20 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "devOptional": true, + "license": "MIT" + }, "node_modules/pg": { "version": "8.16.3", "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", @@ -8487,6 +8775,18 @@ "node": ">=8" } }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, "node_modules/postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -8581,22 +8881,29 @@ } }, "node_modules/prisma": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", - "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "version": "6.16.2", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.16.2.tgz", + "integrity": "sha512-aRvldGE5UUJTtVmFiH3WfNFNiqFlAtePUxcI0UEGlnXCX7DqhiMT5TRYwncHFeA/Reca5W6ToXXyCMTeFPdSXA==", + "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/engines": "5.22.0" + "@prisma/config": "6.16.2", + "@prisma/engines": "6.16.2" }, "bin": { "prisma": "build/index.js" }, "engines": { - "node": ">=16.13" + "node": ">=18.18" }, - "optionalDependencies": { - "fsevents": "2.3.3" + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/process": { @@ -8668,7 +8975,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "dev": true, + "devOptional": true, "funding": [ { "type": "individual", @@ -8741,6 +9048,17 @@ "node": ">= 0.8" } }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -9167,18 +9485,18 @@ "license": "ISC" }, "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", "license": "MIT", "dependencies": { "is-arrayish": "^0.3.1" } }, "node_modules/simple-swizzle/node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", "license": "MIT" }, "node_modules/simple-update-notifier": { @@ -9242,10 +9560,9 @@ } }, "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, + "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/sqlstring": { @@ -9528,12 +9845,6 @@ "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/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -9586,6 +9897,13 @@ "dev": true, "license": "MIT" }, + "node_modules/tinyexec": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", + "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", + "devOptional": true, + "license": "MIT" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -9648,9 +9966,9 @@ } }, "node_modules/ts-jest": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.1.tgz", - "integrity": "sha512-SaeUtjfpg9Uqu8IbeDKtdaS0g8lS6FT6OzM3ezrDfErPJPHNDo/Ey+VFGP1bQIDfagYDLyRpd7O15XpG1Es2Uw==", + "version": "29.4.4", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.4.tgz", + "integrity": "sha512-ccVcRABct5ZELCT5U0+DZwkXMCcOCLi2doHRrKy1nK/s7J7bch6TzJMsrY09WxgUUIP/ITfmcDS8D2yl63rnXw==", "dev": true, "license": "MIT", "dependencies": { @@ -9822,7 +10140,7 @@ "version": "5.9.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -9925,14 +10243,9 @@ } }, "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "dev": true, - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], + "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" diff --git a/backend-node/package.json b/backend-node/package.json index 8cfa8cf7..2caf0d1c 100644 --- a/backend-node/package.json +++ b/backend-node/package.json @@ -27,7 +27,7 @@ "author": "", "license": "ISC", "dependencies": { - "@prisma/client": "^5.7.1", + "@prisma/client": "^6.16.2", "@types/mssql": "^9.1.8", "axios": "^1.11.0", "bcryptjs": "^2.4.3", @@ -46,7 +46,6 @@ "nodemailer": "^6.9.7", "oracledb": "^6.9.0", "pg": "^8.16.3", - "prisma": "^5.7.1", "redis": "^4.6.10", "winston": "^3.11.0" }, @@ -73,6 +72,7 @@ "jest": "^29.7.0", "nodemon": "^3.1.10", "prettier": "^3.1.0", + "prisma": "^6.16.2", "supertest": "^6.3.3", "ts-jest": "^29.1.1", "ts-node": "^10.9.2", diff --git a/backend-node/prisma/schema.prisma b/backend-node/prisma/schema.prisma index d9dfa8ad..3e8812ab 100644 --- a/backend-node/prisma/schema.prisma +++ b/backend-node/prisma/schema.prisma @@ -50,27 +50,27 @@ model db_type_categories { } model external_db_connections { - id Int @id @default(autoincrement()) - connection_name String @db.VarChar(100) + id Int @id @default(autoincrement()) + connection_name String @db.VarChar(100) description String? - db_type String @db.VarChar(20) - host String @db.VarChar(255) + db_type String @db.VarChar(20) + host String @db.VarChar(255) port Int - database_name String @db.VarChar(100) - username String @db.VarChar(100) + database_name String @db.VarChar(100) + username String @db.VarChar(100) password String - connection_timeout Int? @default(30) - query_timeout Int? @default(60) - max_connections Int? @default(10) - ssl_enabled String? @default("N") @db.Char(1) - ssl_cert_path String? @db.VarChar(500) + connection_timeout Int? @default(30) + query_timeout Int? @default(60) + max_connections Int? @default(10) + ssl_enabled String? @default("N") @db.Char(1) + ssl_cert_path String? @db.VarChar(500) connection_options Json? - company_code String? @default("*") @db.VarChar(20) - is_active String? @default("Y") @db.Char(1) - created_date DateTime? @default(now()) @db.Timestamp(6) - created_by String? @db.VarChar(50) - updated_date DateTime? @default(now()) @updatedAt @db.Timestamp(6) - updated_by String? @db.VarChar(50) + company_code String? @default("*") @db.VarChar(20) + is_active String? @default("Y") @db.Char(1) + created_date DateTime? @default(now()) @db.Timestamp(6) + created_by String? @db.VarChar(50) + updated_date DateTime? @default(now()) @updatedAt @db.Timestamp(6) + updated_by String? @db.VarChar(50) // 관계 db_type_category db_type_categories? @relation(fields: [db_type], references: [type_code]) @@ -80,6 +80,83 @@ model external_db_connections { @@index([db_type], map: "idx_external_db_connections_db_type") } +model batch_configs { + id Int @id @default(autoincrement()) + batch_name String @db.VarChar(100) + description String? + cron_schedule String @db.VarChar(50) + is_active String? @default("Y") @db.Char(1) + company_code String? @default("*") @db.VarChar(20) + created_date DateTime? @default(now()) @db.Timestamp(6) + created_by String? @db.VarChar(50) + updated_date DateTime? @default(now()) @updatedAt @db.Timestamp(6) + updated_by String? @db.VarChar(50) + + // 관계 설정 + batch_mappings batch_mappings[] + execution_logs batch_execution_logs[] + + @@index([batch_name], map: "idx_batch_configs_name") + @@index([is_active], map: "idx_batch_configs_active") +} + +model batch_mappings { + id Int @id @default(autoincrement()) + batch_config_id Int + from_connection_type String @db.VarChar(20) + from_connection_id Int? + from_table_name String @db.VarChar(100) + from_column_name String @db.VarChar(100) + from_column_type String? @db.VarChar(50) + from_api_url String? @db.VarChar(500) + from_api_key String? @db.VarChar(200) + from_api_method String? @db.VarChar(10) + to_connection_type String @db.VarChar(20) + to_connection_id Int? + to_table_name String @db.VarChar(100) + to_column_name String @db.VarChar(100) + to_column_type String? @db.VarChar(50) + to_api_url String? @db.VarChar(500) + to_api_key String? @db.VarChar(200) + to_api_method String? @db.VarChar(10) + to_api_body String? @db.Text + mapping_order Int? @default(1) + created_date DateTime? @default(now()) @db.Timestamp(6) + created_by String? @db.VarChar(50) + + // 관계 설정 + batch_config batch_configs @relation(fields: [batch_config_id], references: [id], onDelete: Cascade) + + @@index([batch_config_id], map: "idx_batch_mappings_config") + @@index([from_connection_type, from_connection_id], map: "idx_batch_mappings_from") + @@index([to_connection_type, to_connection_id], map: "idx_batch_mappings_to") + @@index([from_connection_type, from_api_url], map: "idx_batch_mappings_from_api") + @@index([to_connection_type, to_api_url], map: "idx_batch_mappings_to_api") +} + +model batch_execution_logs { + id Int @id @default(autoincrement()) + batch_config_id Int + execution_status String @db.VarChar(20) + start_time DateTime @default(now()) @db.Timestamp(6) + end_time DateTime? @db.Timestamp(6) + duration_ms Int? + total_records Int? @default(0) + success_records Int? @default(0) + failed_records Int? @default(0) + error_message String? + error_details String? + server_name String? @db.VarChar(100) + process_id String? @db.VarChar(50) + + // 관계 설정 + batch_config batch_configs @relation(fields: [batch_config_id], references: [id], onDelete: Cascade) + + @@index([batch_config_id], map: "idx_batch_execution_logs_config") + @@index([execution_status], map: "idx_batch_execution_logs_status") + @@index([start_time], map: "idx_batch_execution_logs_start_time") +} + model admin_supply_mng { objid Decimal @id @default(0) @db.Decimal supply_code String? @default("NULL::character varying") @db.VarChar(100) diff --git a/backend-node/src/app.ts b/backend-node/src/app.ts index f9e7cbb5..d3b53f33 100644 --- a/backend-node/src/app.ts +++ b/backend-node/src/app.ts @@ -33,12 +33,17 @@ import testButtonDataflowRoutes from "./routes/testButtonDataflowRoutes"; import externalDbConnectionRoutes from "./routes/externalDbConnectionRoutes"; import multiConnectionRoutes from "./routes/multiConnectionRoutes"; import screenFileRoutes from "./routes/screenFileRoutes"; -import dbTypeCategoryRoutes from "./routes/dbTypeCategoryRoutes"; +//import dbTypeCategoryRoutes from "./routes/dbTypeCategoryRoutes"; +import batchRoutes from "./routes/batchRoutes"; +import batchManagementRoutes from "./routes/batchManagementRoutes"; +import batchExecutionLogRoutes from "./routes/batchExecutionLogRoutes"; +// import dbTypeCategoryRoutes from "./routes/dbTypeCategoryRoutes"; // 파일이 존재하지 않음 import ddlRoutes from "./routes/ddlRoutes"; import entityReferenceRoutes from "./routes/entityReferenceRoutes"; import externalCallRoutes from "./routes/externalCallRoutes"; import externalCallConfigRoutes from "./routes/externalCallConfigRoutes"; import dataflowExecutionRoutes from "./routes/dataflowExecutionRoutes"; +import { BatchSchedulerService } from "./services/batchSchedulerService"; // import collectionRoutes from "./routes/collectionRoutes"; // 임시 주석 // import batchRoutes from "./routes/batchRoutes"; // 임시 주석 // import userRoutes from './routes/userRoutes'; @@ -144,7 +149,10 @@ app.use("/api/test-button-dataflow", testButtonDataflowRoutes); app.use("/api/external-db-connections", externalDbConnectionRoutes); app.use("/api/multi-connection", multiConnectionRoutes); app.use("/api/screen-files", screenFileRoutes); -app.use("/api/db-type-categories", dbTypeCategoryRoutes); +app.use("/api/batch-configs", batchRoutes); +app.use("/api/batch-management", batchManagementRoutes); +app.use("/api/batch-execution-logs", batchExecutionLogRoutes); +// app.use("/api/db-type-categories", dbTypeCategoryRoutes); // 파일이 존재하지 않음 app.use("/api/ddl", ddlRoutes); app.use("/api/entity-reference", entityReferenceRoutes); app.use("/api/external-calls", externalCallRoutes); @@ -171,11 +179,19 @@ app.use(errorHandler); const PORT = config.port; const HOST = config.host; -app.listen(PORT, HOST, () => { +app.listen(PORT, HOST, async () => { logger.info(`🚀 Server is running on ${HOST}:${PORT}`); logger.info(`📊 Environment: ${config.nodeEnv}`); logger.info(`🔗 Health check: http://${HOST}:${PORT}/health`); logger.info(`🌐 External access: http://39.117.244.52:${PORT}/health`); + + // 배치 스케줄러 초기화 + try { + await BatchSchedulerService.initialize(); + logger.info(`⏰ 배치 스케줄러가 시작되었습니다.`); + } catch (error) { + logger.error(`❌ 배치 스케줄러 초기화 실패:`, error); + } }); export default app; diff --git a/backend-node/src/controllers/batchController.ts b/backend-node/src/controllers/batchController.ts index 99b66364..ba270f41 100644 --- a/backend-node/src/controllers/batchController.ts +++ b/backend-node/src/controllers/batchController.ts @@ -1,294 +1,281 @@ -// 배치 관리 컨트롤러 -// 작성일: 2024-12-23 +// 배치관리 컨트롤러 +// 작성일: 2024-12-24 -import { Request, Response } from 'express'; -import { BatchService } from '../services/batchService'; -import { BatchJob, BatchJobFilter } from '../types/batchManagement'; -import { AuthenticatedRequest } from '../middleware/authMiddleware'; +import { Request, Response } from "express"; +import { BatchService } from "../services/batchService"; +import { BatchConfigFilter, CreateBatchConfigRequest, UpdateBatchConfigRequest } from "../types/batchTypes"; + +export interface AuthenticatedRequest extends Request { + user?: { + userId: string; + username: string; + companyCode: string; + }; +} export class BatchController { /** - * 배치 작업 목록 조회 + * 배치 설정 목록 조회 + * GET /api/batch-configs */ - static async getBatchJobs(req: AuthenticatedRequest, res: Response): Promise { + static async getBatchConfigs(req: AuthenticatedRequest, res: Response) { try { - const filter: BatchJobFilter = { - job_name: req.query.job_name as string, - job_type: req.query.job_type as string, - is_active: req.query.is_active as string, - company_code: req.user?.companyCode || '*', - search: req.query.search as string, + const { page = 1, limit = 10, search, isActive } = req.query; + + const filter: BatchConfigFilter = { + page: Number(page), + limit: Number(limit), + search: search as string, + is_active: isActive as string }; - const jobs = await BatchService.getBatchJobs(filter); - - res.status(200).json({ + const result = await BatchService.getBatchConfigs(filter); + + res.json({ success: true, - data: jobs, - message: '배치 작업 목록을 조회했습니다.', + data: result.data, + pagination: result.pagination }); } catch (error) { - console.error('배치 작업 목록 조회 오류:', error); + console.error("배치 설정 목록 조회 오류:", error); res.status(500).json({ success: false, - message: error instanceof Error ? error.message : '배치 작업 목록 조회에 실패했습니다.', + message: "배치 설정 목록 조회에 실패했습니다." }); } } /** - * 배치 작업 상세 조회 + * 사용 가능한 커넥션 목록 조회 + * GET /api/batch-configs/connections */ - static async getBatchJobById(req: AuthenticatedRequest, res: Response): Promise { + static async getAvailableConnections(req: AuthenticatedRequest, res: Response) { try { - const id = parseInt(req.params.id); - if (isNaN(id)) { - res.status(400).json({ + const result = await BatchService.getAvailableConnections(); + + if (result.success) { + res.json(result); + } else { + res.status(500).json(result); + } + } catch (error) { + console.error("커넥션 목록 조회 오류:", error); + res.status(500).json({ + success: false, + message: "커넥션 목록 조회에 실패했습니다." + }); + } + } + + /** + * 테이블 목록 조회 (내부/외부 DB) + * GET /api/batch-configs/connections/:type/tables + * GET /api/batch-configs/connections/:type/:id/tables + */ + static async getTablesFromConnection(req: AuthenticatedRequest, res: Response) { + try { + const { type, id } = req.params; + + if (!type || (type !== 'internal' && type !== 'external')) { + return res.status(400).json({ success: false, - message: '유효하지 않은 ID입니다.', + message: "올바른 연결 타입을 지정해주세요. (internal 또는 external)" }); - return; } - const job = await BatchService.getBatchJobById(id); - if (!job) { - res.status(404).json({ + const connectionId = type === 'external' ? Number(id) : undefined; + const result = await BatchService.getTablesFromConnection(type, connectionId); + + if (result.success) { + return res.json(result); + } else { + return res.status(500).json(result); + } + } catch (error) { + console.error("테이블 목록 조회 오류:", error); + return res.status(500).json({ + success: false, + message: "테이블 목록 조회에 실패했습니다." + }); + } + } + + /** + * 테이블 컬럼 정보 조회 (내부/외부 DB) + * GET /api/batch-configs/connections/:type/tables/:tableName/columns + * GET /api/batch-configs/connections/:type/:id/tables/:tableName/columns + */ + static async getTableColumns(req: AuthenticatedRequest, res: Response) { + try { + const { type, id, tableName } = req.params; + + if (!type || !tableName) { + return res.status(400).json({ success: false, - message: '배치 작업을 찾을 수 없습니다.', + message: "연결 타입과 테이블명을 모두 지정해주세요." }); - return; } - res.status(200).json({ - success: true, - data: job, - message: '배치 작업을 조회했습니다.', - }); - } catch (error) { - console.error('배치 작업 조회 오류:', error); - res.status(500).json({ - success: false, - message: error instanceof Error ? error.message : '배치 작업 조회에 실패했습니다.', - }); - } - } - - /** - * 배치 작업 생성 - */ - static async createBatchJob(req: AuthenticatedRequest, res: Response): Promise { - try { - const data: BatchJob = { - ...req.body, - company_code: req.user?.companyCode || '*', - created_by: req.user?.userId, - }; - - // 필수 필드 검증 - if (!data.job_name || !data.job_type) { - res.status(400).json({ + if (type !== 'internal' && type !== 'external') { + return res.status(400).json({ success: false, - message: '필수 필드가 누락되었습니다.', + message: "올바른 연결 타입을 지정해주세요. (internal 또는 external)" }); - return; } - const job = await BatchService.createBatchJob(data); - - res.status(201).json({ - success: true, - data: job, - message: '배치 작업을 생성했습니다.', - }); + const connectionId = type === 'external' ? Number(id) : undefined; + const result = await BatchService.getTableColumns(type, connectionId, tableName); + + if (result.success) { + return res.json(result); + } else { + return res.status(500).json(result); + } } catch (error) { - console.error('배치 작업 생성 오류:', error); - res.status(500).json({ + console.error("컬럼 정보 조회 오류:", error); + return res.status(500).json({ success: false, - message: error instanceof Error ? error.message : '배치 작업 생성에 실패했습니다.', + message: "컬럼 정보 조회에 실패했습니다." }); } } /** - * 배치 작업 수정 + * 특정 배치 설정 조회 + * GET /api/batch-configs/:id */ - static async updateBatchJob(req: AuthenticatedRequest, res: Response): Promise { + static async getBatchConfigById(req: AuthenticatedRequest, res: Response) { try { - const id = parseInt(req.params.id); - if (isNaN(id)) { - res.status(400).json({ + const { id } = req.params; + const batchConfig = await BatchService.getBatchConfigById(Number(id)); + + if (!batchConfig) { + return res.status(404).json({ success: false, - message: '유효하지 않은 ID입니다.', + message: "배치 설정을 찾을 수 없습니다." + }); + } + + return res.json({ + success: true, + data: batchConfig + }); + } catch (error) { + console.error("배치 설정 조회 오류:", error); + return res.status(500).json({ + success: false, + message: "배치 설정 조회에 실패했습니다." + }); + } + } + + /** + * 배치 설정 생성 + * POST /api/batch-configs + */ + static async createBatchConfig(req: AuthenticatedRequest, res: Response) { + try { + const { batchName, description, cronSchedule, mappings } = req.body; + + if (!batchName || !cronSchedule || !mappings || !Array.isArray(mappings)) { + return res.status(400).json({ + success: false, + message: "필수 필드가 누락되었습니다. (batchName, cronSchedule, mappings)" }); - return; } - const data: Partial = { - ...req.body, - updated_by: req.user?.userId, - }; - - const job = await BatchService.updateBatchJob(id, data); - - res.status(200).json({ + const batchConfig = await BatchService.createBatchConfig({ + batchName, + description, + cronSchedule, + mappings + } as CreateBatchConfigRequest); + + return res.status(201).json({ success: true, - data: job, - message: '배치 작업을 수정했습니다.', + data: batchConfig, + message: "배치 설정이 성공적으로 생성되었습니다." }); } catch (error) { - console.error('배치 작업 수정 오류:', error); - res.status(500).json({ + console.error("배치 설정 생성 오류:", error); + return res.status(500).json({ success: false, - message: error instanceof Error ? error.message : '배치 작업 수정에 실패했습니다.', + message: "배치 설정 생성에 실패했습니다." }); } } /** - * 배치 작업 삭제 + * 배치 설정 수정 + * PUT /api/batch-configs/:id */ - static async deleteBatchJob(req: AuthenticatedRequest, res: Response): Promise { + static async updateBatchConfig(req: AuthenticatedRequest, res: Response) { try { - const id = parseInt(req.params.id); - if (isNaN(id)) { - res.status(400).json({ + const { id } = req.params; + const { batchName, description, cronSchedule, mappings, isActive } = req.body; + + if (!batchName || !cronSchedule) { + return res.status(400).json({ success: false, - message: '유효하지 않은 ID입니다.', + message: "필수 필드가 누락되었습니다. (batchName, cronSchedule)" }); - return; } - await BatchService.deleteBatchJob(id); - - res.status(200).json({ - success: true, - message: '배치 작업을 삭제했습니다.', - }); - } catch (error) { - console.error('배치 작업 삭제 오류:', error); - res.status(500).json({ - success: false, - message: error instanceof Error ? error.message : '배치 작업 삭제에 실패했습니다.', - }); - } - } - - /** - * 배치 작업 수동 실행 - */ - static async executeBatchJob(req: AuthenticatedRequest, res: Response): Promise { - try { - const id = parseInt(req.params.id); - if (isNaN(id)) { - res.status(400).json({ + const batchConfig = await BatchService.updateBatchConfig(Number(id), { + batchName, + description, + cronSchedule, + mappings, + isActive + } as UpdateBatchConfigRequest); + + if (!batchConfig) { + return res.status(404).json({ success: false, - message: '유효하지 않은 ID입니다.', + message: "배치 설정을 찾을 수 없습니다." }); - return; } - - const execution = await BatchService.executeBatchJob(id); - - res.status(200).json({ + + return res.json({ success: true, - data: execution, - message: '배치 작업을 실행했습니다.', + data: batchConfig, + message: "배치 설정이 성공적으로 수정되었습니다." }); } catch (error) { - console.error('배치 작업 실행 오류:', error); - res.status(500).json({ + console.error("배치 설정 수정 오류:", error); + return res.status(500).json({ success: false, - message: error instanceof Error ? error.message : '배치 작업 실행에 실패했습니다.', + message: "배치 설정 수정에 실패했습니다." }); } } /** - * 배치 실행 목록 조회 + * 배치 설정 삭제 (논리 삭제) + * DELETE /api/batch-configs/:id */ - static async getBatchExecutions(req: AuthenticatedRequest, res: Response): Promise { + static async deleteBatchConfig(req: AuthenticatedRequest, res: Response) { try { - const jobId = req.query.job_id ? parseInt(req.query.job_id as string) : undefined; - const executions = await BatchService.getBatchExecutions(jobId); - - res.status(200).json({ + const { id } = req.params; + const result = await BatchService.deleteBatchConfig(Number(id)); + + if (!result) { + return res.status(404).json({ + success: false, + message: "배치 설정을 찾을 수 없습니다." + }); + } + + return res.json({ success: true, - data: executions, - message: '배치 실행 목록을 조회했습니다.', + message: "배치 설정이 성공적으로 삭제되었습니다." }); } catch (error) { - console.error('배치 실행 목록 조회 오류:', error); - res.status(500).json({ + console.error("배치 설정 삭제 오류:", error); + return res.status(500).json({ success: false, - message: error instanceof Error ? error.message : '배치 실행 목록 조회에 실패했습니다.', + message: "배치 설정 삭제에 실패했습니다." }); } } - - /** - * 배치 모니터링 정보 조회 - */ - static async getBatchMonitoring(req: AuthenticatedRequest, res: Response): Promise { - try { - const monitoring = await BatchService.getBatchMonitoring(); - - res.status(200).json({ - success: true, - data: monitoring, - message: '배치 모니터링 정보를 조회했습니다.', - }); - } catch (error) { - console.error('배치 모니터링 조회 오류:', error); - res.status(500).json({ - success: false, - message: error instanceof Error ? error.message : '배치 모니터링 조회에 실패했습니다.', - }); - } - } - - /** - * 지원되는 작업 타입 조회 - */ - static async getSupportedJobTypes(req: AuthenticatedRequest, res: Response): Promise { - try { - const { BATCH_JOB_TYPE_OPTIONS } = await import('../types/batchManagement'); - - res.status(200).json({ - success: true, - data: { - types: BATCH_JOB_TYPE_OPTIONS, - }, - message: '지원하는 작업 타입 목록을 조회했습니다.', - }); - } catch (error) { - console.error('작업 타입 조회 오류:', error); - res.status(500).json({ - success: false, - message: '작업 타입 조회에 실패했습니다.', - }); - } - } - - /** - * 스케줄 프리셋 조회 - */ - static async getSchedulePresets(req: AuthenticatedRequest, res: Response): Promise { - try { - const { SCHEDULE_PRESETS } = await import('../types/batchManagement'); - - res.status(200).json({ - success: true, - data: { - presets: SCHEDULE_PRESETS, - }, - message: '스케줄 프리셋 목록을 조회했습니다.', - }); - } catch (error) { - console.error('스케줄 프리셋 조회 오류:', error); - res.status(500).json({ - success: false, - message: '스케줄 프리셋 조회에 실패했습니다.', - }); - } - } -} +} \ No newline at end of file diff --git a/backend-node/src/controllers/batchExecutionLogController.ts b/backend-node/src/controllers/batchExecutionLogController.ts new file mode 100644 index 00000000..68d8d880 --- /dev/null +++ b/backend-node/src/controllers/batchExecutionLogController.ts @@ -0,0 +1,179 @@ +// 배치 실행 로그 컨트롤러 +// 작성일: 2024-12-24 + +import { Request, Response } from "express"; +import { AuthenticatedRequest } from "../types/auth"; +import { BatchExecutionLogService } from "../services/batchExecutionLogService"; +import { BatchExecutionLogFilter, CreateBatchExecutionLogRequest, UpdateBatchExecutionLogRequest } from "../types/batchExecutionLogTypes"; + +export class BatchExecutionLogController { + /** + * 배치 실행 로그 목록 조회 + */ + static async getExecutionLogs(req: AuthenticatedRequest, res: Response) { + try { + const { + batch_config_id, + execution_status, + start_date, + end_date, + page, + limit + } = req.query; + + const filter: BatchExecutionLogFilter = { + batch_config_id: batch_config_id ? Number(batch_config_id) : undefined, + execution_status: execution_status as string, + start_date: start_date ? new Date(start_date as string) : undefined, + end_date: end_date ? new Date(end_date as string) : undefined, + page: page ? Number(page) : undefined, + limit: limit ? Number(limit) : undefined + }; + + const result = await BatchExecutionLogService.getExecutionLogs(filter); + + if (result.success) { + res.json(result); + } else { + res.status(500).json(result); + } + } catch (error) { + console.error("배치 실행 로그 조회 오류:", error); + res.status(500).json({ + success: false, + message: "배치 실행 로그 조회 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류" + }); + } + } + + /** + * 배치 실행 로그 생성 + */ + static async createExecutionLog(req: AuthenticatedRequest, res: Response) { + try { + const data: CreateBatchExecutionLogRequest = req.body; + + const result = await BatchExecutionLogService.createExecutionLog(data); + + if (result.success) { + res.status(201).json(result); + } else { + res.status(500).json(result); + } + } catch (error) { + console.error("배치 실행 로그 생성 오류:", error); + res.status(500).json({ + success: false, + message: "배치 실행 로그 생성 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류" + }); + } + } + + /** + * 배치 실행 로그 업데이트 + */ + static async updateExecutionLog(req: AuthenticatedRequest, res: Response) { + try { + const { id } = req.params; + const data: UpdateBatchExecutionLogRequest = req.body; + + const result = await BatchExecutionLogService.updateExecutionLog(Number(id), data); + + if (result.success) { + res.json(result); + } else { + res.status(500).json(result); + } + } catch (error) { + console.error("배치 실행 로그 업데이트 오류:", error); + res.status(500).json({ + success: false, + message: "배치 실행 로그 업데이트 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류" + }); + } + } + + /** + * 배치 실행 로그 삭제 + */ + static async deleteExecutionLog(req: AuthenticatedRequest, res: Response) { + try { + const { id } = req.params; + + const result = await BatchExecutionLogService.deleteExecutionLog(Number(id)); + + if (result.success) { + res.json(result); + } else { + res.status(500).json(result); + } + } catch (error) { + console.error("배치 실행 로그 삭제 오류:", error); + res.status(500).json({ + success: false, + message: "배치 실행 로그 삭제 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류" + }); + } + } + + /** + * 특정 배치의 최신 실행 로그 조회 + */ + static async getLatestExecutionLog(req: AuthenticatedRequest, res: Response) { + try { + const { batchConfigId } = req.params; + + const result = await BatchExecutionLogService.getLatestExecutionLog(Number(batchConfigId)); + + if (result.success) { + res.json(result); + } else { + res.status(500).json(result); + } + } catch (error) { + console.error("최신 배치 실행 로그 조회 오류:", error); + res.status(500).json({ + success: false, + message: "최신 배치 실행 로그 조회 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류" + }); + } + } + + /** + * 배치 실행 통계 조회 + */ + static async getExecutionStats(req: AuthenticatedRequest, res: Response) { + try { + const { + batch_config_id, + start_date, + end_date + } = req.query; + + const result = await BatchExecutionLogService.getExecutionStats( + batch_config_id ? Number(batch_config_id) : undefined, + start_date ? new Date(start_date as string) : undefined, + end_date ? new Date(end_date as string) : undefined + ); + + if (result.success) { + res.json(result); + } else { + res.status(500).json(result); + } + } catch (error) { + console.error("배치 실행 통계 조회 오류:", error); + res.status(500).json({ + success: false, + message: "배치 실행 통계 조회 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류" + }); + } + } +} + diff --git a/backend-node/src/controllers/batchManagementController.ts b/backend-node/src/controllers/batchManagementController.ts new file mode 100644 index 00000000..4381a340 --- /dev/null +++ b/backend-node/src/controllers/batchManagementController.ts @@ -0,0 +1,619 @@ +// 배치관리 전용 컨트롤러 (기존 소스와 완전 분리) +// 작성일: 2024-12-24 + +import { Response } from "express"; +import { AuthenticatedRequest } from "../types/auth"; +import { BatchManagementService, BatchConnectionInfo, BatchTableInfo, BatchColumnInfo } from "../services/batchManagementService"; +import { BatchService } from "../services/batchService"; +import { BatchSchedulerService } from "../services/batchSchedulerService"; +import { BatchExternalDbService } from "../services/batchExternalDbService"; +import { CreateBatchConfigRequest, BatchConfig } from "../types/batchTypes"; + +export class BatchManagementController { + /** + * 사용 가능한 커넥션 목록 조회 + */ + static async getAvailableConnections(req: AuthenticatedRequest, res: Response) { + try { + const result = await BatchManagementService.getAvailableConnections(); + if (result.success) { + res.json(result); + } else { + res.status(500).json(result); + } + } catch (error) { + console.error("커넥션 목록 조회 오류:", error); + res.status(500).json({ + success: false, + message: "커넥션 목록 조회 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류" + }); + } + } + + /** + * 특정 커넥션의 테이블 목록 조회 + */ + static async getTablesFromConnection(req: AuthenticatedRequest, res: Response) { + try { + const { type, id } = req.params; + + if (type !== 'internal' && type !== 'external') { + return res.status(400).json({ + success: false, + message: "올바른 연결 타입을 지정해주세요. (internal 또는 external)" + }); + } + + const connectionId = type === 'external' ? Number(id) : undefined; + const result = await BatchManagementService.getTablesFromConnection(type, connectionId); + + if (result.success) { + return res.json(result); + } else { + return res.status(500).json(result); + } + } catch (error) { + console.error("테이블 목록 조회 오류:", error); + return res.status(500).json({ + success: false, + message: "테이블 목록 조회 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류" + }); + } + } + + /** + * 특정 테이블의 컬럼 정보 조회 + */ + static async getTableColumns(req: AuthenticatedRequest, res: Response) { + try { + const { type, id, tableName } = req.params; + + if (type !== 'internal' && type !== 'external') { + return res.status(400).json({ + success: false, + message: "올바른 연결 타입을 지정해주세요. (internal 또는 external)" + }); + } + + const connectionId = type === 'external' ? Number(id) : undefined; + const result = await BatchManagementService.getTableColumns(type, connectionId, tableName); + + if (result.success) { + return res.json(result); + } else { + return res.status(500).json(result); + } + } catch (error) { + console.error("컬럼 정보 조회 오류:", error); + return res.status(500).json({ + success: false, + message: "컬럼 정보 조회 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류" + }); + } + } + + /** + * 배치 설정 생성 + * POST /api/batch-management/batch-configs + */ + static async createBatchConfig(req: AuthenticatedRequest, res: Response) { + try { + const { batchName, description, cronSchedule, mappings, isActive } = req.body; + + if (!batchName || !cronSchedule || !mappings || !Array.isArray(mappings)) { + return res.status(400).json({ + success: false, + message: "필수 필드가 누락되었습니다. (batchName, cronSchedule, mappings)" + }); + } + + const batchConfig = await BatchService.createBatchConfig({ + batchName, + description, + cronSchedule, + mappings, + isActive: isActive !== undefined ? isActive : true + } as CreateBatchConfigRequest); + + return res.status(201).json({ + success: true, + data: batchConfig, + message: "배치 설정이 성공적으로 생성되었습니다." + }); + } catch (error) { + console.error("배치 설정 생성 오류:", error); + return res.status(500).json({ + success: false, + message: "배치 설정 생성에 실패했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류" + }); + } + } + + /** + * 특정 배치 설정 조회 + * GET /api/batch-management/batch-configs/:id + */ + static async getBatchConfigById(req: AuthenticatedRequest, res: Response) { + try { + const { id } = req.params; + console.log("🔍 배치 설정 조회 요청:", id); + + const result = await BatchService.getBatchConfigById(Number(id)); + + if (!result.success) { + return res.status(404).json({ + success: false, + message: result.message || "배치 설정을 찾을 수 없습니다." + }); + } + + console.log("📋 조회된 배치 설정:", result.data); + + return res.json({ + success: true, + data: result.data + }); + } catch (error) { + console.error("❌ 배치 설정 조회 오류:", error); + return res.status(500).json({ + success: false, + message: "배치 설정 조회에 실패했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류" + }); + } + } + + /** + * 배치 설정 목록 조회 + * GET /api/batch-management/batch-configs + */ + static async getBatchConfigs(req: AuthenticatedRequest, res: Response) { + try { + const { page = 1, limit = 10, search, isActive } = req.query; + + const filter = { + page: Number(page), + limit: Number(limit), + search: search as string, + is_active: isActive as string + }; + + const result = await BatchService.getBatchConfigs(filter); + + res.json({ + success: true, + data: result.data, + pagination: result.pagination + }); + } catch (error) { + console.error("배치 설정 목록 조회 오류:", error); + res.status(500).json({ + success: false, + message: "배치 설정 목록 조회에 실패했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류" + }); + } + } + + /** + * 배치 수동 실행 + * POST /api/batch-management/batch-configs/:id/execute + */ + static async executeBatchConfig(req: AuthenticatedRequest, res: Response) { + try { + const { id } = req.params; + + if (!id || isNaN(Number(id))) { + return res.status(400).json({ + success: false, + message: "올바른 배치 설정 ID를 제공해주세요." + }); + } + + // 배치 설정 조회 + const batchConfigResult = await BatchService.getBatchConfigById(Number(id)); + if (!batchConfigResult.success || !batchConfigResult.data) { + return res.status(404).json({ + success: false, + message: "배치 설정을 찾을 수 없습니다." + }); + } + + const batchConfig = batchConfigResult.data as BatchConfig; + + // 배치 실행 로직 (간단한 버전) + const startTime = new Date(); + let totalRecords = 0; + let successRecords = 0; + let failedRecords = 0; + + try { + console.log(`배치 실행 시작: ${batchConfig.batch_name} (ID: ${id})`); + + // 실행 로그 생성 + const executionLog = await BatchService.createExecutionLog({ + batch_config_id: Number(id), + execution_status: 'RUNNING', + start_time: startTime, + total_records: 0, + success_records: 0, + failed_records: 0 + }); + + // 실제 배치 실행 (매핑이 있는 경우) + if (batchConfig.batch_mappings && batchConfig.batch_mappings.length > 0) { + // 테이블별로 매핑을 그룹화 + const tableGroups = new Map(); + + for (const mapping of batchConfig.batch_mappings) { + const key = `${mapping.from_connection_type}:${mapping.from_connection_id || 'internal'}:${mapping.from_table_name}`; + if (!tableGroups.has(key)) { + tableGroups.set(key, []); + } + tableGroups.get(key)!.push(mapping); + } + + // 각 테이블 그룹별로 처리 + for (const [tableKey, mappings] of tableGroups) { + try { + const firstMapping = mappings[0]; + console.log(`테이블 처리 시작: ${tableKey} -> ${mappings.length}개 컬럼 매핑`); + + let fromData: any[] = []; + + // FROM 데이터 조회 (DB 또는 REST API) + if (firstMapping.from_connection_type === 'restapi') { + // REST API에서 데이터 조회 + console.log(`REST API에서 데이터 조회: ${firstMapping.from_api_url}${firstMapping.from_table_name}`); + console.log(`API 설정:`, { + url: firstMapping.from_api_url, + key: firstMapping.from_api_key ? '***' : 'null', + method: firstMapping.from_api_method, + endpoint: firstMapping.from_table_name + }); + + try { + const apiResult = await BatchExternalDbService.getDataFromRestApi( + firstMapping.from_api_url!, + firstMapping.from_api_key!, + firstMapping.from_table_name, + firstMapping.from_api_method as 'GET' | 'POST' | 'PUT' | 'DELETE' || 'GET', + mappings.map(m => m.from_column_name) + ); + + console.log(`API 조회 결과:`, { + success: apiResult.success, + dataCount: apiResult.data ? apiResult.data.length : 0, + message: apiResult.message + }); + + if (apiResult.success && apiResult.data) { + fromData = apiResult.data; + } else { + throw new Error(`REST API 데이터 조회 실패: ${apiResult.message}`); + } + } catch (error) { + console.error(`REST API 조회 오류:`, error); + throw error; + } + } else { + // DB에서 데이터 조회 + const fromColumns = mappings.map(m => m.from_column_name); + fromData = await BatchService.getDataFromTableWithColumns( + firstMapping.from_table_name, + fromColumns, + firstMapping.from_connection_type as 'internal' | 'external', + firstMapping.from_connection_id || undefined + ); + } + + totalRecords += fromData.length; + + // 컬럼 매핑 적용하여 TO 테이블 형식으로 변환 + const mappedData = fromData.map(row => { + const mappedRow: any = {}; + for (const mapping of mappings) { + // DB → REST API 배치인지 확인 + if (firstMapping.to_connection_type === 'restapi' && mapping.to_api_body) { + // DB → REST API: 원본 컬럼명을 키로 사용 (템플릿 처리용) + mappedRow[mapping.from_column_name] = row[mapping.from_column_name]; + } else { + // 기존 로직: to_column_name을 키로 사용 + mappedRow[mapping.to_column_name] = row[mapping.from_column_name]; + } + } + return mappedRow; + }); + + // TO 테이블에 데이터 삽입 (DB 또는 REST API) + let insertResult: { successCount: number; failedCount: number }; + + if (firstMapping.to_connection_type === 'restapi') { + // REST API로 데이터 전송 + console.log(`REST API로 데이터 전송: ${firstMapping.to_api_url}${firstMapping.to_table_name}`); + + // DB → REST API 배치인지 확인 (to_api_body가 있으면 템플릿 기반) + const hasTemplate = mappings.some(m => m.to_api_body); + + if (hasTemplate) { + // 템플릿 기반 REST API 전송 (DB → REST API 배치) + const templateBody = firstMapping.to_api_body || '{}'; + console.log(`템플릿 기반 REST API 전송, Request Body 템플릿: ${templateBody}`); + + // URL 경로 컬럼 찾기 (PUT/DELETE용) + const urlPathColumn = mappings.find(m => m.to_column_name === 'URL_PATH_PARAM')?.from_column_name; + + const apiResult = await BatchExternalDbService.sendDataToRestApiWithTemplate( + firstMapping.to_api_url!, + firstMapping.to_api_key!, + firstMapping.to_table_name, + firstMapping.to_api_method as 'POST' | 'PUT' | 'DELETE' || 'POST', + templateBody, + mappedData, + urlPathColumn + ); + + if (apiResult.success && apiResult.data) { + insertResult = apiResult.data; + } else { + throw new Error(`템플릿 기반 REST API 데이터 전송 실패: ${apiResult.message}`); + } + } else { + // 기존 REST API 전송 (REST API → DB 배치) + const apiResult = await BatchExternalDbService.sendDataToRestApi( + firstMapping.to_api_url!, + firstMapping.to_api_key!, + firstMapping.to_table_name, + firstMapping.to_api_method as 'POST' | 'PUT' || 'POST', + mappedData + ); + + if (apiResult.success && apiResult.data) { + insertResult = apiResult.data; + } else { + throw new Error(`REST API 데이터 전송 실패: ${apiResult.message}`); + } + } + } else { + // DB에 데이터 삽입 + insertResult = await BatchService.insertDataToTable( + firstMapping.to_table_name, + mappedData, + firstMapping.to_connection_type as 'internal' | 'external', + firstMapping.to_connection_id || undefined + ); + } + + successRecords += insertResult.successCount; + failedRecords += insertResult.failedCount; + + console.log(`테이블 처리 완료: ${insertResult.successCount}개 성공, ${insertResult.failedCount}개 실패`); + } catch (error) { + console.error(`테이블 처리 실패: ${tableKey}`, error); + failedRecords += 1; + } + } + } else { + console.log("매핑이 없어서 데이터 처리를 건너뜁니다."); + } + + // 실행 로그 업데이트 (성공) + await BatchService.updateExecutionLog(executionLog.id, { + execution_status: 'SUCCESS', + end_time: new Date(), + duration_ms: Date.now() - startTime.getTime(), + total_records: totalRecords, + success_records: successRecords, + failed_records: failedRecords + }); + + return res.json({ + success: true, + message: "배치가 성공적으로 실행되었습니다.", + data: { + batchId: id, + totalRecords, + successRecords, + failedRecords, + duration: Date.now() - startTime.getTime() + } + }); + } catch (error) { + console.error(`배치 실행 실패: ${batchConfig.batch_name}`, error); + + return res.status(500).json({ + success: false, + message: "배치 실행에 실패했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류" + }); + } + } catch (error) { + console.error("배치 실행 오류:", error); + return res.status(500).json({ + success: false, + message: "배치 실행 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류" + }); + } + } + + /** + * 배치 설정 업데이트 + * PUT /api/batch-management/batch-configs/:id + */ + static async updateBatchConfig(req: AuthenticatedRequest, res: Response) { + try { + const { id } = req.params; + const updateData = req.body; + + if (!id || isNaN(Number(id))) { + return res.status(400).json({ + success: false, + message: "올바른 배치 설정 ID를 제공해주세요." + }); + } + + const batchConfig = await BatchService.updateBatchConfig(Number(id), updateData); + + // 스케줄러에서 배치 스케줄 업데이트 + await BatchSchedulerService.updateBatchSchedule(Number(id)); + + return res.json({ + success: true, + data: batchConfig, + message: "배치 설정이 성공적으로 업데이트되었습니다." + }); + } catch (error) { + console.error("배치 설정 업데이트 오류:", error); + return res.status(500).json({ + success: false, + message: "배치 설정 업데이트에 실패했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류" + }); + } + } + + /** + * REST API 데이터 미리보기 + */ + static async previewRestApiData(req: AuthenticatedRequest, res: Response) { + try { + const { apiUrl, apiKey, endpoint, method = 'GET' } = req.body; + + if (!apiUrl || !apiKey || !endpoint) { + return res.status(400).json({ + success: false, + message: "API URL, API Key, 엔드포인트는 필수입니다." + }); + } + + // RestApiConnector 사용하여 데이터 조회 + const { RestApiConnector } = await import('../database/RestApiConnector'); + + const connector = new RestApiConnector({ + baseUrl: apiUrl, + apiKey: apiKey, + timeout: 30000 + }); + + // 연결 테스트 + await connector.connect(); + + // 데이터 조회 (최대 5개만) - GET 메서드만 지원 + const result = await connector.executeQuery(endpoint, method); + console.log(`[previewRestApiData] executeQuery 결과:`, { + rowCount: result.rowCount, + rowsLength: result.rows ? result.rows.length : 'undefined', + firstRow: result.rows && result.rows.length > 0 ? result.rows[0] : 'no data' + }); + + const data = result.rows.slice(0, 5); // 최대 5개 샘플만 + console.log(`[previewRestApiData] 슬라이스된 데이터:`, data); + + if (data.length > 0) { + // 첫 번째 객체에서 필드명 추출 + const fields = Object.keys(data[0]); + console.log(`[previewRestApiData] 추출된 필드:`, fields); + + return res.json({ + success: true, + data: { + fields: fields, + samples: data, + totalCount: result.rowCount || data.length + }, + message: `${fields.length}개 필드, ${result.rowCount || data.length}개 레코드를 조회했습니다.` + }); + } else { + return res.json({ + success: true, + data: { + fields: [], + samples: [], + totalCount: 0 + }, + message: "API에서 데이터를 가져올 수 없습니다." + }); + } + } catch (error) { + console.error("REST API 미리보기 오류:", error); + return res.status(500).json({ + success: false, + message: "REST API 데이터 미리보기 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류" + }); + } + } + + /** + * REST API 배치 설정 저장 + */ + static async saveRestApiBatch(req: AuthenticatedRequest, res: Response) { + try { + const { + batchName, + batchType, + cronSchedule, + description, + apiMappings + } = req.body; + + if (!batchName || !batchType || !cronSchedule || !apiMappings || apiMappings.length === 0) { + return res.status(400).json({ + success: false, + message: "필수 필드가 누락되었습니다." + }); + } + + console.log("REST API 배치 저장 요청:", { + batchName, + batchType, + cronSchedule, + description, + apiMappings + }); + + // BatchService를 사용하여 배치 설정 저장 + const batchConfig: CreateBatchConfigRequest = { + batchName: batchName, + description: description || '', + cronSchedule: cronSchedule, + mappings: apiMappings + }; + + const result = await BatchService.createBatchConfig(batchConfig); + + if (result.success && result.data) { + // 스케줄러에 자동 등록 ✅ + try { + await BatchSchedulerService.scheduleBatchConfig(result.data); + console.log(`✅ 새로운 배치가 스케줄러에 등록되었습니다: ${batchName} (ID: ${result.data.id})`); + } catch (schedulerError) { + console.error(`❌ 스케줄러 등록 실패: ${batchName}`, schedulerError); + // 스케줄러 등록 실패해도 배치 저장은 성공으로 처리 + } + + return res.json({ + success: true, + message: "REST API 배치가 성공적으로 저장되었습니다.", + data: result.data + }); + } else { + return res.status(500).json({ + success: false, + message: result.message || "배치 저장에 실패했습니다." + }); + } + } catch (error) { + console.error("REST API 배치 저장 오류:", error); + return res.status(500).json({ + success: false, + message: "배치 저장 중 오류가 발생했습니다." + }); + } + } +} diff --git a/backend-node/src/database/DatabaseConnectorFactory.ts b/backend-node/src/database/DatabaseConnectorFactory.ts index 8ece7bba..f8d277bf 100644 --- a/backend-node/src/database/DatabaseConnectorFactory.ts +++ b/backend-node/src/database/DatabaseConnectorFactory.ts @@ -3,6 +3,7 @@ import { PostgreSQLConnector } from './PostgreSQLConnector'; import { MariaDBConnector } from './MariaDBConnector'; import { MSSQLConnector } from './MSSQLConnector'; import { OracleConnector } from './OracleConnector'; +import { RestApiConnector, RestApiConfig } from './RestApiConnector'; export class DatabaseConnectorFactory { private static connectors = new Map(); @@ -33,6 +34,9 @@ export class DatabaseConnectorFactory { case 'oracle': connector = new OracleConnector(config); break; + case 'restapi': + connector = new RestApiConnector(config as RestApiConfig); + break; // Add other database types here default: throw new Error(`지원하지 않는 데이터베이스 타입: ${type}`); diff --git a/backend-node/src/database/MSSQLConnector.ts b/backend-node/src/database/MSSQLConnector.ts index b4555a7e..fc1c195c 100644 --- a/backend-node/src/database/MSSQLConnector.ts +++ b/backend-node/src/database/MSSQLConnector.ts @@ -1,5 +1,6 @@ import { DatabaseConnector, ConnectionConfig, QueryResult } from '../interfaces/DatabaseConnector'; import { ConnectionTestResult, TableInfo } from '../types/externalDbTypes'; +// @ts-ignore import * as mssql from 'mssql'; export class MSSQLConnector implements DatabaseConnector { diff --git a/backend-node/src/database/MariaDBConnector.ts b/backend-node/src/database/MariaDBConnector.ts index 2bfeda0a..f023bfc7 100644 --- a/backend-node/src/database/MariaDBConnector.ts +++ b/backend-node/src/database/MariaDBConnector.ts @@ -1,10 +1,7 @@ -import { - DatabaseConnector, - ConnectionConfig, - QueryResult, -} from "../interfaces/DatabaseConnector"; -import { ConnectionTestResult, TableInfo } from "../types/externalDbTypes"; -import * as mysql from "mysql2/promise"; +import { DatabaseConnector, ConnectionConfig, QueryResult } from '../interfaces/DatabaseConnector'; +import { ConnectionTestResult, TableInfo } from '../types/externalDbTypes'; +// @ts-ignore +import * as mysql from 'mysql2/promise'; export class MariaDBConnector implements DatabaseConnector { private connection: mysql.Connection | null = null; @@ -22,18 +19,8 @@ export class MariaDBConnector implements DatabaseConnector { user: this.config.user, password: this.config.password, database: this.config.database, - // 🔧 MySQL2에서 지원하는 타임아웃 설정 - connectTimeout: this.config.connectionTimeoutMillis || 30000, // 연결 타임아웃 30초 - ssl: typeof this.config.ssl === "boolean" ? undefined : this.config.ssl, - // 🔧 MySQL2에서 지원하는 추가 설정 - charset: "utf8mb4", - timezone: "Z", - supportBigNumbers: true, - bigNumberStrings: true, - // 🔧 연결 풀 설정 (단일 연결이지만 안정성을 위해) - dateStrings: true, - debug: false, - trace: false, + connectTimeout: this.config.connectionTimeoutMillis, + ssl: typeof this.config.ssl === 'boolean' ? undefined : this.config.ssl, }); } } @@ -49,9 +36,7 @@ export class MariaDBConnector implements DatabaseConnector { const startTime = Date.now(); try { await this.connect(); - const [rows] = await this.connection!.query( - "SELECT VERSION() as version" - ); + const [rows] = await this.connection!.query("SELECT VERSION() as version"); const version = (rows as any[])[0]?.version || "Unknown"; const responseTime = Date.now() - startTime; await this.disconnect(); @@ -79,18 +64,7 @@ export class MariaDBConnector implements DatabaseConnector { async executeQuery(query: string): Promise { try { await this.connect(); - - // 🔧 쿼리 타임아웃 수동 구현 (60초) - const queryTimeout = this.config.queryTimeoutMillis || 60000; - const queryPromise = this.connection!.query(query); - const timeoutPromise = new Promise((_, reject) => { - setTimeout(() => reject(new Error("쿼리 실행 타임아웃")), queryTimeout); - }); - - const [rows, fields] = (await Promise.race([ - queryPromise, - timeoutPromise, - ])) as any; + const [rows, fields] = await this.connection!.query(query); await this.disconnect(); return { rows: rows as any[], @@ -133,54 +107,28 @@ export class MariaDBConnector implements DatabaseConnector { async getColumns(tableName: string): Promise { try { - console.log(`🔍 MariaDB 컬럼 조회 시작: ${tableName}`); + console.log(`[MariaDBConnector] getColumns 호출: tableName=${tableName}`); await this.connect(); - - // 🔧 컬럼 조회 타임아웃 수동 구현 (30초) - const queryTimeout = this.config.queryTimeoutMillis || 30000; - // 스키마명을 명시적으로 확인 - const schemaQuery = `SELECT DATABASE() as schema_name`; - const [schemaResult] = await this.connection!.query(schemaQuery); - const schemaName = - (schemaResult as any[])[0]?.schema_name || this.config.database; - - console.log(`📋 사용할 스키마: ${schemaName}`); - - const query = ` + console.log(`[MariaDBConnector] 연결 완료, 쿼리 실행 시작`); + + const [rows] = await this.connection!.query(` SELECT COLUMN_NAME as column_name, DATA_TYPE as data_type, IS_NULLABLE as is_nullable, - COLUMN_DEFAULT as column_default, - COLUMN_COMMENT as column_comment + COLUMN_DEFAULT as column_default FROM information_schema.COLUMNS - WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION; - `; - - console.log( - `📋 실행할 쿼리: ${query.trim()}, 파라미터: [${schemaName}, ${tableName}]` - ); - - const queryPromise = this.connection!.query(query, [ - schemaName, - tableName, - ]); - const timeoutPromise = new Promise((_, reject) => { - setTimeout(() => reject(new Error("컬럼 조회 타임아웃")), queryTimeout); - }); - - const [rows] = (await Promise.race([ - queryPromise, - timeoutPromise, - ])) as any; - - console.log( - `✅ MariaDB 컬럼 조회 완료: ${tableName}, ${rows ? rows.length : 0}개 컬럼` - ); + `, [tableName]); + + console.log(`[MariaDBConnector] 쿼리 결과:`, rows); + console.log(`[MariaDBConnector] 결과 개수:`, Array.isArray(rows) ? rows.length : 'not array'); + await this.disconnect(); return rows as any[]; } catch (error: any) { + console.error(`[MariaDBConnector] getColumns 오류:`, error); await this.disconnect(); throw new Error(`컬럼 정보 조회 실패: ${error.message}`); } diff --git a/backend-node/src/database/OracleConnector.ts b/backend-node/src/database/OracleConnector.ts index a9fea5f6..b9360570 100644 --- a/backend-node/src/database/OracleConnector.ts +++ b/backend-node/src/database/OracleConnector.ts @@ -1,3 +1,4 @@ +// @ts-ignore import * as oracledb from 'oracledb'; import { DatabaseConnector, ConnectionConfig, QueryResult } from '../interfaces/DatabaseConnector'; import { ConnectionTestResult, TableInfo } from '../types/externalDbTypes'; @@ -100,7 +101,7 @@ export class OracleConnector implements DatabaseConnector { // Oracle XE 21c 쿼리 실행 옵션 const options: any = { - outFormat: oracledb.OUT_FORMAT_OBJECT, // OBJECT format + outFormat: (oracledb as any).OUT_FORMAT_OBJECT, // OBJECT format maxRows: 10000, // XE 제한 고려 fetchArraySize: 100 }; @@ -176,6 +177,8 @@ export class OracleConnector implements DatabaseConnector { async getColumns(tableName: string): Promise { try { + console.log(`[OracleConnector] getColumns 호출: tableName=${tableName}`); + const query = ` SELECT column_name, @@ -190,16 +193,23 @@ export class OracleConnector implements DatabaseConnector { ORDER BY column_id `; + console.log(`[OracleConnector] 쿼리 실행 시작: ${query}`); const result = await this.executeQuery(query, [tableName]); - return result.rows.map((row: any) => ({ + console.log(`[OracleConnector] 쿼리 결과:`, result.rows); + console.log(`[OracleConnector] 결과 개수:`, result.rows ? result.rows.length : 'null/undefined'); + + const mappedResult = result.rows.map((row: any) => ({ column_name: row.COLUMN_NAME, data_type: this.formatOracleDataType(row), is_nullable: row.NULLABLE === 'Y' ? 'YES' : 'NO', column_default: row.DATA_DEFAULT })); + + console.log(`[OracleConnector] 매핑된 결과:`, mappedResult); + return mappedResult; } catch (error: any) { - console.error('Oracle 테이블 컬럼 조회 실패:', error); + console.error('[OracleConnector] getColumns 오류:', error); throw new Error(`테이블 컬럼 조회 실패: ${error.message}`); } } diff --git a/backend-node/src/database/RestApiConnector.ts b/backend-node/src/database/RestApiConnector.ts new file mode 100644 index 00000000..98da0eb3 --- /dev/null +++ b/backend-node/src/database/RestApiConnector.ts @@ -0,0 +1,261 @@ +import axios, { AxiosInstance, AxiosResponse } from 'axios'; +import { DatabaseConnector, ConnectionConfig, QueryResult } from '../interfaces/DatabaseConnector'; +import { ConnectionTestResult, TableInfo } from '../types/externalDbTypes'; + +export interface RestApiConfig { + baseUrl: string; + apiKey: string; + timeout?: number; + // ConnectionConfig 호환성을 위한 더미 필드들 (사용하지 않음) + host?: string; + port?: number; + database?: string; + user?: string; + password?: string; +} + +export class RestApiConnector implements DatabaseConnector { + private httpClient: AxiosInstance; + private config: RestApiConfig; + + constructor(config: RestApiConfig) { + this.config = config; + + // Axios 인스턴스 생성 + this.httpClient = axios.create({ + baseURL: config.baseUrl, + timeout: config.timeout || 30000, + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': config.apiKey, + 'Accept': 'application/json' + } + }); + + // 요청/응답 인터셉터 설정 + this.setupInterceptors(); + } + + private setupInterceptors() { + // 요청 인터셉터 + this.httpClient.interceptors.request.use( + (config) => { + console.log(`[RestApiConnector] 요청: ${config.method?.toUpperCase()} ${config.url}`); + return config; + }, + (error) => { + console.error('[RestApiConnector] 요청 오류:', error); + return Promise.reject(error); + } + ); + + // 응답 인터셉터 + this.httpClient.interceptors.response.use( + (response) => { + console.log(`[RestApiConnector] 응답: ${response.status} ${response.statusText}`); + return response; + }, + (error) => { + console.error('[RestApiConnector] 응답 오류:', error.response?.status, error.response?.statusText); + return Promise.reject(error); + } + ); + } + + async connect(): Promise { + try { + // 연결 테스트 - 기본 엔드포인트 호출 + await this.httpClient.get('/health', { timeout: 5000 }); + console.log(`[RestApiConnector] 연결 성공: ${this.config.baseUrl}`); + } catch (error) { + // health 엔드포인트가 없을 수 있으므로 404는 정상으로 처리 + if (axios.isAxiosError(error) && error.response?.status === 404) { + console.log(`[RestApiConnector] 연결 성공 (health 엔드포인트 없음): ${this.config.baseUrl}`); + return; + } + console.error(`[RestApiConnector] 연결 실패: ${this.config.baseUrl}`, error); + throw new Error(`REST API 연결 실패: ${error instanceof Error ? error.message : '알 수 없는 오류'}`); + } + } + + async disconnect(): Promise { + // REST API는 연결 해제가 필요 없음 + console.log(`[RestApiConnector] 연결 해제: ${this.config.baseUrl}`); + } + + async testConnection(): Promise { + try { + await this.connect(); + return { + success: true, + message: 'REST API 연결이 성공했습니다.', + details: { + response_time: Date.now() + } + }; + } catch (error) { + return { + success: false, + message: error instanceof Error ? error.message : 'REST API 연결에 실패했습니다.', + details: { + response_time: Date.now() + } + }; + } + } + + async executeQuery(endpoint: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET', data?: any): Promise { + try { + const startTime = Date.now(); + let response: AxiosResponse; + + // HTTP 메서드에 따른 요청 실행 + switch (method.toUpperCase()) { + case 'GET': + response = await this.httpClient.get(endpoint); + break; + case 'POST': + response = await this.httpClient.post(endpoint, data); + break; + case 'PUT': + response = await this.httpClient.put(endpoint, data); + break; + case 'DELETE': + response = await this.httpClient.delete(endpoint); + break; + default: + throw new Error(`지원하지 않는 HTTP 메서드: ${method}`); + } + + const executionTime = Date.now() - startTime; + const responseData = response.data; + + console.log(`[RestApiConnector] 원본 응답 데이터:`, { + type: typeof responseData, + isArray: Array.isArray(responseData), + keys: typeof responseData === 'object' ? Object.keys(responseData) : 'not object', + responseData: responseData + }); + + // 응답 데이터 처리 + let rows: any[]; + if (Array.isArray(responseData)) { + rows = responseData; + } else if (responseData && responseData.data && Array.isArray(responseData.data)) { + // API 응답이 {success: true, data: [...]} 형태인 경우 + rows = responseData.data; + } else if (responseData && responseData.data && typeof responseData.data === 'object') { + // API 응답이 {success: true, data: {...}} 형태인 경우 (단일 객체) + rows = [responseData.data]; + } else if (responseData && typeof responseData === 'object' && !Array.isArray(responseData)) { + // 단일 객체 응답인 경우 + rows = [responseData]; + } else { + rows = []; + } + + console.log(`[RestApiConnector] 처리된 rows:`, { + rowsLength: rows.length, + firstRow: rows.length > 0 ? rows[0] : 'no data', + allRows: rows + }); + + console.log(`[RestApiConnector] API 호출 결과:`, { + endpoint, + method, + status: response.status, + rowCount: rows.length, + executionTime: `${executionTime}ms` + }); + + return { + rows: rows, + rowCount: rows.length, + fields: rows.length > 0 ? Object.keys(rows[0]).map(key => ({ name: key, type: 'string' })) : [] + }; + } catch (error) { + console.error(`[RestApiConnector] API 호출 오류 (${method} ${endpoint}):`, error); + + if (axios.isAxiosError(error)) { + throw new Error(`REST API 호출 실패: ${error.response?.status} ${error.response?.statusText}`); + } + + throw new Error(`REST API 호출 실패: ${error instanceof Error ? error.message : '알 수 없는 오류'}`); + } + } + + async getTables(): Promise { + // REST API의 경우 "테이블"은 사용 가능한 엔드포인트를 의미 + // 일반적인 REST API 엔드포인트들을 반환 + return [ + { + table_name: '/api/users', + columns: [], + description: '사용자 정보 API' + }, + { + table_name: '/api/data', + columns: [], + description: '기본 데이터 API' + }, + { + table_name: '/api/custom', + columns: [], + description: '사용자 정의 엔드포인트' + } + ]; + } + + async getTableList(): Promise { + return this.getTables(); + } + + async getColumns(endpoint: string): Promise { + try { + // GET 요청으로 샘플 데이터를 가져와서 필드 구조 파악 + const result = await this.executeQuery(endpoint, 'GET'); + + if (result.rows.length > 0) { + const sampleRow = result.rows[0]; + return Object.keys(sampleRow).map(key => ({ + column_name: key, + data_type: typeof sampleRow[key], + is_nullable: 'YES', + column_default: null, + description: `${key} 필드` + })); + } + + return []; + } catch (error) { + console.error(`[RestApiConnector] 컬럼 정보 조회 오류 (${endpoint}):`, error); + return []; + } + } + + async getTableColumns(endpoint: string): Promise { + return this.getColumns(endpoint); + } + + // REST API 전용 메서드들 + async getData(endpoint: string, params?: Record): Promise { + const queryString = params ? '?' + new URLSearchParams(params).toString() : ''; + const result = await this.executeQuery(endpoint + queryString, 'GET'); + return result.rows; + } + + async postData(endpoint: string, data: any): Promise { + const result = await this.executeQuery(endpoint, 'POST', data); + return result.rows[0]; + } + + async putData(endpoint: string, data: any): Promise { + const result = await this.executeQuery(endpoint, 'PUT', data); + return result.rows[0]; + } + + async deleteData(endpoint: string): Promise { + const result = await this.executeQuery(endpoint, 'DELETE'); + return result.rows[0]; + } +} diff --git a/backend-node/src/routes/batchExecutionLogRoutes.ts b/backend-node/src/routes/batchExecutionLogRoutes.ts new file mode 100644 index 00000000..8f2dd9ec --- /dev/null +++ b/backend-node/src/routes/batchExecutionLogRoutes.ts @@ -0,0 +1,47 @@ +// 배치 실행 로그 라우트 +// 작성일: 2024-12-24 + +import { Router } from "express"; +import { BatchExecutionLogController } from "../controllers/batchExecutionLogController"; +import { authenticateToken } from "../middleware/authMiddleware"; + +const router = Router(); + +/** + * GET /api/batch-execution-logs + * 배치 실행 로그 목록 조회 + */ +router.get("/", authenticateToken, BatchExecutionLogController.getExecutionLogs); + +/** + * POST /api/batch-execution-logs + * 배치 실행 로그 생성 + */ +router.post("/", authenticateToken, BatchExecutionLogController.createExecutionLog); + +/** + * PUT /api/batch-execution-logs/:id + * 배치 실행 로그 업데이트 + */ +router.put("/:id", authenticateToken, BatchExecutionLogController.updateExecutionLog); + +/** + * DELETE /api/batch-execution-logs/:id + * 배치 실행 로그 삭제 + */ +router.delete("/:id", authenticateToken, BatchExecutionLogController.deleteExecutionLog); + +/** + * GET /api/batch-execution-logs/latest/:batchConfigId + * 특정 배치의 최신 실행 로그 조회 + */ +router.get("/latest/:batchConfigId", authenticateToken, BatchExecutionLogController.getLatestExecutionLog); + +/** + * GET /api/batch-execution-logs/stats + * 배치 실행 통계 조회 + */ +router.get("/stats", authenticateToken, BatchExecutionLogController.getExecutionStats); + +export default router; + diff --git a/backend-node/src/routes/batchManagementRoutes.ts b/backend-node/src/routes/batchManagementRoutes.ts new file mode 100644 index 00000000..d6adf4c5 --- /dev/null +++ b/backend-node/src/routes/batchManagementRoutes.ts @@ -0,0 +1,82 @@ +// 배치관리 전용 라우트 (기존 소스와 완전 분리) +// 작성일: 2024-12-24 + +import { Router } from "express"; +import { BatchManagementController } from "../controllers/batchManagementController"; +import { authenticateToken } from "../middleware/authMiddleware"; + +const router = Router(); + +/** + * GET /api/batch-management/connections + * 사용 가능한 커넥션 목록 조회 + */ +router.get("/connections", authenticateToken, BatchManagementController.getAvailableConnections); + +/** + * GET /api/batch-management/connections/:type/tables + * 내부 DB 테이블 목록 조회 + */ +router.get("/connections/:type/tables", authenticateToken, BatchManagementController.getTablesFromConnection); + +/** + * GET /api/batch-management/connections/:type/:id/tables + * 외부 DB 테이블 목록 조회 + */ +router.get("/connections/:type/:id/tables", authenticateToken, BatchManagementController.getTablesFromConnection); + +/** + * GET /api/batch-management/connections/:type/tables/:tableName/columns + * 내부 DB 테이블 컬럼 정보 조회 + */ +router.get("/connections/:type/tables/:tableName/columns", authenticateToken, BatchManagementController.getTableColumns); + +/** + * GET /api/batch-management/connections/:type/:id/tables/:tableName/columns + * 외부 DB 테이블 컬럼 정보 조회 + */ +router.get("/connections/:type/:id/tables/:tableName/columns", authenticateToken, BatchManagementController.getTableColumns); + +/** + * POST /api/batch-management/batch-configs + * 배치 설정 생성 + */ +router.post("/batch-configs", authenticateToken, BatchManagementController.createBatchConfig); + +/** + * GET /api/batch-management/batch-configs + * 배치 설정 목록 조회 + */ +router.get("/batch-configs", authenticateToken, BatchManagementController.getBatchConfigs); + +/** + * GET /api/batch-management/batch-configs/:id + * 특정 배치 설정 조회 + */ +router.get("/batch-configs/:id", authenticateToken, BatchManagementController.getBatchConfigById); + +/** + * PUT /api/batch-management/batch-configs/:id + * 배치 설정 업데이트 + */ +router.put("/batch-configs/:id", authenticateToken, BatchManagementController.updateBatchConfig); + +/** + * POST /api/batch-management/batch-configs/:id/execute + * 배치 수동 실행 + */ +router.post("/batch-configs/:id/execute", authenticateToken, BatchManagementController.executeBatchConfig); + +/** + * POST /api/batch-management/rest-api/preview + * REST API 데이터 미리보기 + */ +router.post("/rest-api/preview", authenticateToken, BatchManagementController.previewRestApiData); + +/** + * POST /api/batch-management/rest-api/save + * REST API 배치 저장 + */ +router.post("/rest-api/save", authenticateToken, BatchManagementController.saveRestApiBatch); + +export default router; diff --git a/backend-node/src/routes/batchRoutes.ts b/backend-node/src/routes/batchRoutes.ts index 9be9d0ba..c34ee9e5 100644 --- a/backend-node/src/routes/batchRoutes.ts +++ b/backend-node/src/routes/batchRoutes.ts @@ -1,73 +1,70 @@ -// 배치 관리 라우트 -// 작성일: 2024-12-23 +// 배치관리 라우트 +// 작성일: 2024-12-24 -import { Router } from 'express'; -import { BatchController } from '../controllers/batchController'; -import { authenticateToken } from '../middleware/authMiddleware'; +import { Router } from "express"; +import { BatchController } from "../controllers/batchController"; +import { authenticateToken } from "../middleware/authMiddleware"; const router = Router(); -// 모든 라우트에 인증 미들웨어 적용 -router.use(authenticateToken); +/** + * GET /api/batch-configs + * 배치 설정 목록 조회 + */ +router.get("/", authenticateToken, BatchController.getBatchConfigs); /** - * GET /api/batch - * 배치 작업 목록 조회 + * GET /api/batch-configs/connections + * 사용 가능한 커넥션 목록 조회 */ -router.get('/', BatchController.getBatchJobs); +router.get("/connections", BatchController.getAvailableConnections); /** - * GET /api/batch/:id - * 배치 작업 상세 조회 + * GET /api/batch-configs/connections/:type/tables + * 내부 DB 테이블 목록 조회 */ -router.get('/:id', BatchController.getBatchJobById); +router.get("/connections/:type/tables", authenticateToken, BatchController.getTablesFromConnection); /** - * POST /api/batch - * 배치 작업 생성 + * GET /api/batch-configs/connections/:type/:id/tables + * 외부 DB 테이블 목록 조회 */ -router.post('/', BatchController.createBatchJob); +router.get("/connections/:type/:id/tables", authenticateToken, BatchController.getTablesFromConnection); /** - * PUT /api/batch/:id - * 배치 작업 수정 + * GET /api/batch-configs/connections/:type/tables/:tableName/columns + * 내부 DB 테이블 컬럼 정보 조회 */ -router.put('/:id', BatchController.updateBatchJob); +router.get("/connections/:type/tables/:tableName/columns", authenticateToken, BatchController.getTableColumns); /** - * DELETE /api/batch/:id - * 배치 작업 삭제 + * GET /api/batch-configs/connections/:type/:id/tables/:tableName/columns + * 외부 DB 테이블 컬럼 정보 조회 */ -router.delete('/:id', BatchController.deleteBatchJob); +router.get("/connections/:type/:id/tables/:tableName/columns", authenticateToken, BatchController.getTableColumns); /** - * POST /api/batch/:id/execute - * 배치 작업 수동 실행 + * GET /api/batch-configs/:id + * 특정 배치 설정 조회 */ -router.post('/:id/execute', BatchController.executeBatchJob); +router.get("/:id", authenticateToken, BatchController.getBatchConfigById); /** - * GET /api/batch/executions - * 배치 실행 목록 조회 + * POST /api/batch-configs + * 배치 설정 생성 */ -router.get('/executions/list', BatchController.getBatchExecutions); +router.post("/", authenticateToken, BatchController.createBatchConfig); /** - * GET /api/batch/monitoring - * 배치 모니터링 정보 조회 + * PUT /api/batch-configs/:id + * 배치 설정 수정 */ -router.get('/monitoring/status', BatchController.getBatchMonitoring); +router.put("/:id", authenticateToken, BatchController.updateBatchConfig); /** - * GET /api/batch/types/supported - * 지원되는 작업 타입 조회 + * DELETE /api/batch-configs/:id + * 배치 설정 삭제 (논리 삭제) */ -router.get('/types/supported', BatchController.getSupportedJobTypes); +router.delete("/:id", authenticateToken, BatchController.deleteBatchConfig); -/** - * GET /api/batch/schedules/presets - * 스케줄 프리셋 조회 - */ -router.get('/schedules/presets', BatchController.getSchedulePresets); - -export default router; +export default router; \ No newline at end of file diff --git a/backend-node/src/services/batchExecutionLogService.ts b/backend-node/src/services/batchExecutionLogService.ts new file mode 100644 index 00000000..2fee555a --- /dev/null +++ b/backend-node/src/services/batchExecutionLogService.ts @@ -0,0 +1,299 @@ +// 배치 실행 로그 서비스 +// 작성일: 2024-12-24 + +import prisma from "../config/database"; +import { + BatchExecutionLog, + CreateBatchExecutionLogRequest, + UpdateBatchExecutionLogRequest, + BatchExecutionLogFilter, + BatchExecutionLogWithConfig +} from "../types/batchExecutionLogTypes"; +import { ApiResponse } from "../types/batchTypes"; + +export class BatchExecutionLogService { + /** + * 배치 실행 로그 목록 조회 + */ + static async getExecutionLogs( + filter: BatchExecutionLogFilter = {} + ): Promise> { + try { + const { + batch_config_id, + execution_status, + start_date, + end_date, + page = 1, + limit = 50 + } = filter; + + const skip = (page - 1) * limit; + const take = limit; + + // WHERE 조건 구성 + const where: any = {}; + + if (batch_config_id) { + where.batch_config_id = batch_config_id; + } + + if (execution_status) { + where.execution_status = execution_status; + } + + if (start_date || end_date) { + where.start_time = {}; + if (start_date) { + where.start_time.gte = start_date; + } + if (end_date) { + where.start_time.lte = end_date; + } + } + + // 로그 조회 + const [logs, total] = await Promise.all([ + prisma.batch_execution_logs.findMany({ + where, + include: { + batch_config: { + select: { + id: true, + batch_name: true, + description: true, + cron_schedule: true, + is_active: true + } + } + }, + orderBy: { start_time: 'desc' }, + skip, + take + }), + prisma.batch_execution_logs.count({ where }) + ]); + + return { + success: true, + data: logs as BatchExecutionLogWithConfig[], + pagination: { + page, + limit, + total, + totalPages: Math.ceil(total / limit) + } + }; + } catch (error) { + console.error("배치 실행 로그 조회 실패:", error); + return { + success: false, + message: "배치 실행 로그 조회 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류" + }; + } + } + + /** + * 배치 실행 로그 생성 + */ + static async createExecutionLog( + data: CreateBatchExecutionLogRequest + ): Promise> { + try { + const log = await prisma.batch_execution_logs.create({ + data: { + batch_config_id: data.batch_config_id, + execution_status: data.execution_status, + start_time: data.start_time || new Date(), + end_time: data.end_time, + duration_ms: data.duration_ms, + total_records: data.total_records || 0, + success_records: data.success_records || 0, + failed_records: data.failed_records || 0, + error_message: data.error_message, + error_details: data.error_details, + server_name: data.server_name || process.env.HOSTNAME || 'unknown', + process_id: data.process_id || process.pid?.toString() + } + }); + + return { + success: true, + data: log as BatchExecutionLog, + message: "배치 실행 로그가 생성되었습니다." + }; + } catch (error) { + console.error("배치 실행 로그 생성 실패:", error); + return { + success: false, + message: "배치 실행 로그 생성 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류" + }; + } + } + + /** + * 배치 실행 로그 업데이트 + */ + static async updateExecutionLog( + id: number, + data: UpdateBatchExecutionLogRequest + ): Promise> { + try { + const log = await prisma.batch_execution_logs.update({ + where: { id }, + data: { + execution_status: data.execution_status, + end_time: data.end_time, + duration_ms: data.duration_ms, + total_records: data.total_records, + success_records: data.success_records, + failed_records: data.failed_records, + error_message: data.error_message, + error_details: data.error_details + } + }); + + return { + success: true, + data: log as BatchExecutionLog, + message: "배치 실행 로그가 업데이트되었습니다." + }; + } catch (error) { + console.error("배치 실행 로그 업데이트 실패:", error); + return { + success: false, + message: "배치 실행 로그 업데이트 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류" + }; + } + } + + /** + * 배치 실행 로그 삭제 + */ + static async deleteExecutionLog(id: number): Promise> { + try { + await prisma.batch_execution_logs.delete({ + where: { id } + }); + + return { + success: true, + message: "배치 실행 로그가 삭제되었습니다." + }; + } catch (error) { + console.error("배치 실행 로그 삭제 실패:", error); + return { + success: false, + message: "배치 실행 로그 삭제 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류" + }; + } + } + + /** + * 특정 배치의 최신 실행 로그 조회 + */ + static async getLatestExecutionLog( + batchConfigId: number + ): Promise> { + try { + const log = await prisma.batch_execution_logs.findFirst({ + where: { batch_config_id: batchConfigId }, + orderBy: { start_time: 'desc' } + }); + + return { + success: true, + data: log as BatchExecutionLog | null + }; + } catch (error) { + console.error("최신 배치 실행 로그 조회 실패:", error); + return { + success: false, + message: "최신 배치 실행 로그 조회 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류" + }; + } + } + + /** + * 배치 실행 통계 조회 + */ + static async getExecutionStats( + batchConfigId?: number, + startDate?: Date, + endDate?: Date + ): Promise> { + try { + const where: any = {}; + + if (batchConfigId) { + where.batch_config_id = batchConfigId; + } + + if (startDate || endDate) { + where.start_time = {}; + if (startDate) { + where.start_time.gte = startDate; + } + if (endDate) { + where.start_time.lte = endDate; + } + } + + const logs = await prisma.batch_execution_logs.findMany({ + where, + select: { + execution_status: true, + duration_ms: true, + total_records: true + } + }); + + const total_executions = logs.length; + const success_count = logs.filter((log: any) => log.execution_status === 'SUCCESS').length; + const failed_count = logs.filter((log: any) => log.execution_status === 'FAILED').length; + const success_rate = total_executions > 0 ? (success_count / total_executions) * 100 : 0; + + const validDurations = logs + .filter((log: any) => log.duration_ms !== null) + .map((log: any) => log.duration_ms!); + const average_duration_ms = validDurations.length > 0 + ? validDurations.reduce((sum: number, duration: number) => sum + duration, 0) / validDurations.length + : 0; + + const total_records_processed = logs + .filter((log: any) => log.total_records !== null) + .reduce((sum: number, log: any) => sum + (log.total_records || 0), 0); + + return { + success: true, + data: { + total_executions, + success_count, + failed_count, + success_rate, + average_duration_ms, + total_records_processed + } + }; + } catch (error) { + console.error("배치 실행 통계 조회 실패:", error); + return { + success: false, + message: "배치 실행 통계 조회 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류" + }; + } + } +} diff --git a/backend-node/src/services/batchExternalDbService.ts b/backend-node/src/services/batchExternalDbService.ts new file mode 100644 index 00000000..470c3b75 --- /dev/null +++ b/backend-node/src/services/batchExternalDbService.ts @@ -0,0 +1,912 @@ +// 배치관리 전용 외부 DB 서비스 +// 기존 ExternalDbConnectionService와 분리하여 배치관리 시스템에 특화된 기능 제공 +// 작성일: 2024-12-24 + +import prisma from "../config/database"; +import { PasswordEncryption } from "../utils/passwordEncryption"; +import { DatabaseConnectorFactory } from "../database/DatabaseConnectorFactory"; +import { RestApiConnector } from "../database/RestApiConnector"; +import { ApiResponse, ColumnInfo, TableInfo } from "../types/batchTypes"; + +export class BatchExternalDbService { + /** + * 배치관리용 외부 DB 연결 목록 조회 + */ + static async getAvailableConnections(): Promise>> { + try { + const connections: Array<{ + type: 'internal' | 'external'; + id?: number; + name: string; + db_type?: string; + }> = []; + + // 내부 DB 추가 + connections.push({ + type: 'internal', + name: '내부 데이터베이스 (PostgreSQL)', + db_type: 'postgresql' + }); + + // 활성화된 외부 DB 연결 조회 + const externalConnections = await prisma.external_db_connections.findMany({ + where: { is_active: 'Y' }, + select: { + id: true, + connection_name: true, + db_type: true, + description: true + }, + orderBy: { connection_name: 'asc' } + }); + + // 외부 DB 연결 추가 + externalConnections.forEach(conn => { + connections.push({ + type: 'external', + id: conn.id, + name: `${conn.connection_name} (${conn.db_type?.toUpperCase()})`, + db_type: conn.db_type || undefined + }); + }); + + return { + success: true, + data: connections, + message: `${connections.length}개의 연결을 조회했습니다.` + }; + } catch (error) { + console.error("배치관리 연결 목록 조회 실패:", error); + return { + success: false, + message: "연결 목록 조회 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류" + }; + } + } + + /** + * 배치관리용 테이블 목록 조회 + */ + static async getTablesFromConnection( + connectionType: 'internal' | 'external', + connectionId?: number + ): Promise> { + try { + let tables: TableInfo[] = []; + + if (connectionType === 'internal') { + // 내부 DB 테이블 조회 + const result = await prisma.$queryRaw>` + SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'public' + AND table_type = 'BASE TABLE' + ORDER BY table_name + `; + + tables = result.map(row => ({ + table_name: row.table_name, + columns: [] + })); + } else if (connectionType === 'external' && connectionId) { + // 외부 DB 테이블 조회 + const tablesResult = await this.getExternalTables(connectionId); + if (tablesResult.success && tablesResult.data) { + tables = tablesResult.data; + } + } + + return { + success: true, + data: tables, + message: `${tables.length}개의 테이블을 조회했습니다.` + }; + } catch (error) { + console.error("배치관리 테이블 목록 조회 실패:", error); + return { + success: false, + message: "테이블 목록 조회 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류" + }; + } + } + + /** + * 배치관리용 테이블 컬럼 정보 조회 + */ + static async getTableColumns( + connectionType: 'internal' | 'external', + connectionId: number | undefined, + tableName: string + ): Promise> { + try { + console.log(`[BatchExternalDbService] getTableColumns 호출:`, { + connectionType, + connectionId, + tableName + }); + + let columns: ColumnInfo[] = []; + + if (connectionType === 'internal') { + // 내부 DB 컬럼 조회 + console.log(`[BatchExternalDbService] 내부 DB 컬럼 조회 시작: ${tableName}`); + + const result = await prisma.$queryRaw>` + SELECT + column_name, + data_type, + is_nullable, + column_default + FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = ${tableName} + ORDER BY ordinal_position + `; + + console.log(`[BatchExternalDbService] 내부 DB 컬럼 조회 결과:`, result); + + columns = result.map(row => ({ + column_name: row.column_name, + data_type: row.data_type, + is_nullable: row.is_nullable, + column_default: row.column_default, + })); + } else if (connectionType === 'external' && connectionId) { + // 외부 DB 컬럼 조회 + console.log(`[BatchExternalDbService] 외부 DB 컬럼 조회 시작: connectionId=${connectionId}, tableName=${tableName}`); + + const columnsResult = await this.getExternalTableColumns(connectionId, tableName); + + console.log(`[BatchExternalDbService] 외부 DB 컬럼 조회 결과:`, columnsResult); + + if (columnsResult.success && columnsResult.data) { + columns = columnsResult.data; + } + } + + console.log(`[BatchExternalDbService] 최종 컬럼 목록:`, columns); + return { + success: true, + data: columns, + message: `${columns.length}개의 컬럼을 조회했습니다.` + }; + } catch (error) { + console.error("[BatchExternalDbService] 컬럼 정보 조회 오류:", error); + return { + success: false, + message: "컬럼 정보 조회 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류" + }; + } + } + + /** + * 외부 DB 테이블 목록 조회 (내부 구현) + */ + private static async getExternalTables(connectionId: number): Promise> { + try { + // 연결 정보 조회 + const connection = await prisma.external_db_connections.findUnique({ + where: { id: connectionId } + }); + + if (!connection) { + return { + success: false, + message: "연결 정보를 찾을 수 없습니다." + }; + } + + // 비밀번호 복호화 + const decryptedPassword = PasswordEncryption.decrypt(connection.password); + if (!decryptedPassword) { + return { + success: false, + message: "비밀번호 복호화에 실패했습니다." + }; + } + + // 연결 설정 준비 + const config = { + host: connection.host, + port: connection.port, + database: connection.database_name, + user: connection.username, + password: decryptedPassword, + connectionTimeoutMillis: connection.connection_timeout != null ? connection.connection_timeout * 1000 : undefined, + queryTimeoutMillis: connection.query_timeout != null ? connection.query_timeout * 1000 : undefined, + ssl: connection.ssl_enabled === "Y" ? { rejectUnauthorized: false } : false + }; + + // DatabaseConnectorFactory를 통한 테이블 목록 조회 + const connector = await DatabaseConnectorFactory.createConnector(connection.db_type, config, connectionId); + const tables = await connector.getTables(); + + return { + success: true, + message: "테이블 목록을 조회했습니다.", + data: tables + }; + } catch (error) { + console.error("외부 DB 테이블 목록 조회 오류:", error); + return { + success: false, + message: "테이블 목록 조회 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류" + }; + } + } + + /** + * 외부 DB 테이블 컬럼 정보 조회 (내부 구현) + */ + private static async getExternalTableColumns(connectionId: number, tableName: string): Promise> { + try { + console.log(`[BatchExternalDbService] getExternalTableColumns 호출: connectionId=${connectionId}, tableName=${tableName}`); + + // 연결 정보 조회 + const connection = await prisma.external_db_connections.findUnique({ + where: { id: connectionId } + }); + + if (!connection) { + console.log(`[BatchExternalDbService] 연결 정보를 찾을 수 없음: connectionId=${connectionId}`); + return { + success: false, + message: "연결 정보를 찾을 수 없습니다." + }; + } + + console.log(`[BatchExternalDbService] 연결 정보 조회 성공:`, { + id: connection.id, + connection_name: connection.connection_name, + db_type: connection.db_type, + host: connection.host, + port: connection.port, + database_name: connection.database_name + }); + + // 비밀번호 복호화 + const decryptedPassword = PasswordEncryption.decrypt(connection.password); + + // 연결 설정 준비 + const config = { + host: connection.host, + port: connection.port, + database: connection.database_name, + user: connection.username, + password: decryptedPassword, + connectionTimeoutMillis: connection.connection_timeout != null ? connection.connection_timeout * 1000 : undefined, + queryTimeoutMillis: connection.query_timeout != null ? connection.query_timeout * 1000 : undefined, + ssl: connection.ssl_enabled === "Y" ? { rejectUnauthorized: false } : false + }; + + console.log(`[BatchExternalDbService] 커넥터 생성 시작: db_type=${connection.db_type}`); + + // 데이터베이스 타입에 따른 커넥터 생성 + const connector = await DatabaseConnectorFactory.createConnector(connection.db_type, config, connectionId); + + console.log(`[BatchExternalDbService] 커넥터 생성 완료, 컬럼 조회 시작: tableName=${tableName}`); + + // 컬럼 정보 조회 + console.log(`[BatchExternalDbService] connector.getColumns 호출 전`); + const columns = await connector.getColumns(tableName); + + console.log(`[BatchExternalDbService] 원본 컬럼 조회 결과:`, columns); + console.log(`[BatchExternalDbService] 원본 컬럼 개수:`, columns ? columns.length : 'null/undefined'); + + // 각 데이터베이스 커넥터의 반환 구조가 다르므로 통일된 구조로 변환 + const standardizedColumns: ColumnInfo[] = columns.map((col: any) => { + console.log(`[BatchExternalDbService] 컬럼 변환 중:`, col); + + // MySQL/MariaDB 구조: {name, dataType, isNullable, defaultValue} (MySQLConnector만) + if (col.name && col.dataType !== undefined) { + const result = { + column_name: col.name, + data_type: col.dataType, + is_nullable: col.isNullable ? 'YES' : 'NO', + column_default: col.defaultValue || null, + }; + console.log(`[BatchExternalDbService] MySQL/MariaDB 구조로 변환:`, result); + return result; + } + // PostgreSQL/Oracle/MSSQL/MariaDB 구조: {column_name, data_type, is_nullable, column_default} + else { + const result = { + column_name: col.column_name || col.COLUMN_NAME, + data_type: col.data_type || col.DATA_TYPE, + is_nullable: col.is_nullable || col.IS_NULLABLE || (col.nullable === 'Y' ? 'YES' : 'NO'), + column_default: col.column_default || col.COLUMN_DEFAULT || null, + }; + console.log(`[BatchExternalDbService] 표준 구조로 변환:`, result); + return result; + } + }); + + console.log(`[BatchExternalDbService] 표준화된 컬럼 목록:`, standardizedColumns); + + // 빈 배열인 경우 경고 로그 + if (!standardizedColumns || standardizedColumns.length === 0) { + console.warn(`[BatchExternalDbService] 컬럼이 비어있음: connectionId=${connectionId}, tableName=${tableName}`); + console.warn(`[BatchExternalDbService] 연결 정보:`, { + db_type: connection.db_type, + host: connection.host, + port: connection.port, + database_name: connection.database_name, + username: connection.username + }); + + // 테이블 존재 여부 확인 + console.warn(`[BatchExternalDbService] 테이블 존재 여부 확인을 위해 테이블 목록 조회 시도`); + try { + const tables = await connector.getTables(); + console.warn(`[BatchExternalDbService] 사용 가능한 테이블 목록:`, tables.map(t => t.table_name)); + + // 테이블명이 정확한지 확인 + const tableExists = tables.some(t => t.table_name.toLowerCase() === tableName.toLowerCase()); + console.warn(`[BatchExternalDbService] 테이블 존재 여부: ${tableExists}`); + + // 정확한 테이블명 찾기 + const exactTable = tables.find(t => t.table_name.toLowerCase() === tableName.toLowerCase()); + if (exactTable) { + console.warn(`[BatchExternalDbService] 정확한 테이블명: ${exactTable.table_name}`); + } + + // 모든 테이블명 출력 + console.warn(`[BatchExternalDbService] 모든 테이블명:`, tables.map(t => `"${t.table_name}"`)); + + // 테이블명 비교 + console.warn(`[BatchExternalDbService] 요청된 테이블명: "${tableName}"`); + console.warn(`[BatchExternalDbService] 테이블명 비교 결과:`, tables.map(t => ({ + table_name: t.table_name, + matches: t.table_name.toLowerCase() === tableName.toLowerCase(), + exact_match: t.table_name === tableName + }))); + + // 정확한 테이블명으로 다시 시도 + if (exactTable && exactTable.table_name !== tableName) { + console.warn(`[BatchExternalDbService] 정확한 테이블명으로 다시 시도: ${exactTable.table_name}`); + try { + const correctColumns = await connector.getColumns(exactTable.table_name); + console.warn(`[BatchExternalDbService] 정확한 테이블명으로 조회한 컬럼:`, correctColumns); + } catch (correctError) { + console.error(`[BatchExternalDbService] 정확한 테이블명으로 조회 실패:`, correctError); + } + } + } catch (tableError) { + console.error(`[BatchExternalDbService] 테이블 목록 조회 실패:`, tableError); + } + } + + return { + success: true, + data: standardizedColumns, + message: "컬럼 정보를 조회했습니다." + }; + } catch (error) { + console.error("[BatchExternalDbService] 외부 DB 컬럼 정보 조회 오류:", error); + console.error("[BatchExternalDbService] 오류 스택:", error instanceof Error ? error.stack : 'No stack trace'); + return { + success: false, + message: "컬럼 정보 조회 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류" + }; + } + } + + /** + * 외부 DB 테이블에서 데이터 조회 + */ + static async getDataFromTable( + connectionId: number, + tableName: string, + limit: number = 100 + ): Promise> { + try { + console.log(`[BatchExternalDbService] 외부 DB 데이터 조회: connectionId=${connectionId}, tableName=${tableName}`); + + // 외부 DB 연결 정보 조회 + const connection = await prisma.external_db_connections.findUnique({ + where: { id: connectionId } + }); + + if (!connection) { + return { + success: false, + message: "외부 DB 연결을 찾을 수 없습니다." + }; + } + + // 패스워드 복호화 + const decryptedPassword = PasswordEncryption.decrypt(connection.password); + + // DB 연결 설정 + const config = { + host: connection.host, + port: connection.port, + user: connection.username, + password: decryptedPassword, + database: connection.database_name, + }; + + // DB 커넥터 생성 + const connector = await DatabaseConnectorFactory.createConnector( + connection.db_type || 'postgresql', + config, + connectionId + ); + + // 데이터 조회 (DB 타입에 따라 쿼리 구문 변경) + let query: string; + const dbType = connection.db_type?.toLowerCase() || 'postgresql'; + + if (dbType === 'oracle') { + query = `SELECT * FROM ${tableName} WHERE ROWNUM <= ${limit}`; + } else { + query = `SELECT * FROM ${tableName} LIMIT ${limit}`; + } + + console.log(`[BatchExternalDbService] 실행할 쿼리: ${query}`); + const result = await connector.executeQuery(query); + + console.log(`[BatchExternalDbService] 외부 DB 데이터 조회 완료: ${result.rows.length}개 레코드`); + + return { + success: true, + data: result.rows + }; + } catch (error) { + console.error(`외부 DB 데이터 조회 오류 (connectionId: ${connectionId}, table: ${tableName}):`, error); + return { + success: false, + message: "외부 DB 데이터 조회 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류" + }; + } + } + + /** + * 외부 DB 테이블에서 특정 컬럼들만 조회 + */ + static async getDataFromTableWithColumns( + connectionId: number, + tableName: string, + columns: string[], + limit: number = 100 + ): Promise> { + try { + console.log(`[BatchExternalDbService] 외부 DB 특정 컬럼 조회: connectionId=${connectionId}, tableName=${tableName}, columns=[${columns.join(', ')}]`); + + // 외부 DB 연결 정보 조회 + const connection = await prisma.external_db_connections.findUnique({ + where: { id: connectionId } + }); + + if (!connection) { + return { + success: false, + message: "외부 DB 연결을 찾을 수 없습니다." + }; + } + + // 패스워드 복호화 + const decryptedPassword = PasswordEncryption.decrypt(connection.password); + + // DB 연결 설정 + const config = { + host: connection.host, + port: connection.port, + user: connection.username, + password: decryptedPassword, + database: connection.database_name, + }; + + // DB 커넥터 생성 + const connector = await DatabaseConnectorFactory.createConnector( + connection.db_type || 'postgresql', + config, + connectionId + ); + + // 데이터 조회 (DB 타입에 따라 쿼리 구문 변경) + let query: string; + const dbType = connection.db_type?.toLowerCase() || 'postgresql'; + const columnList = columns.join(', '); + + if (dbType === 'oracle') { + query = `SELECT ${columnList} FROM ${tableName} WHERE ROWNUM <= ${limit}`; + } else { + query = `SELECT ${columnList} FROM ${tableName} LIMIT ${limit}`; + } + + console.log(`[BatchExternalDbService] 실행할 쿼리: ${query}`); + const result = await connector.executeQuery(query); + + console.log(`[BatchExternalDbService] 외부 DB 특정 컬럼 조회 완료: ${result.rows.length}개 레코드`); + + return { + success: true, + data: result.rows + }; + } catch (error) { + console.error(`외부 DB 특정 컬럼 조회 오류 (connectionId: ${connectionId}, table: ${tableName}):`, error); + return { + success: false, + message: "외부 DB 특정 컬럼 조회 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류" + }; + } + } + + /** + * 외부 DB 테이블에 데이터 삽입 + */ + static async insertDataToTable( + connectionId: number, + tableName: string, + data: any[] + ): Promise> { + try { + console.log(`[BatchExternalDbService] 외부 DB 데이터 삽입: connectionId=${connectionId}, tableName=${tableName}, ${data.length}개 레코드`); + + if (!data || data.length === 0) { + return { + success: true, + data: { successCount: 0, failedCount: 0 } + }; + } + + // 외부 DB 연결 정보 조회 + const connection = await prisma.external_db_connections.findUnique({ + where: { id: connectionId } + }); + + if (!connection) { + return { + success: false, + message: "외부 DB 연결을 찾을 수 없습니다." + }; + } + + // 패스워드 복호화 + const decryptedPassword = PasswordEncryption.decrypt(connection.password); + + // DB 연결 설정 + const config = { + host: connection.host, + port: connection.port, + user: connection.username, + password: decryptedPassword, + database: connection.database_name, + }; + + // DB 커넥터 생성 + const connector = await DatabaseConnectorFactory.createConnector( + connection.db_type || 'postgresql', + config, + connectionId + ); + + let successCount = 0; + let failedCount = 0; + + // 각 레코드를 개별적으로 삽입 (UPSERT 방식으로 중복 처리) + for (const record of data) { + try { + const columns = Object.keys(record); + const values = Object.values(record); + + // 값들을 SQL 문자열로 변환 (타입별 처리) + const formattedValues = values.map(value => { + if (value === null || value === undefined) { + return 'NULL'; + } else if (value instanceof Date) { + // Date 객체를 MySQL/MariaDB 형식으로 변환 + return `'${value.toISOString().slice(0, 19).replace('T', ' ')}'`; + } else if (typeof value === 'string') { + // 문자열이 날짜 형식인지 확인 + const dateRegex = /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{2}\s+\d{4}\s+\d{2}:\d{2}:\d{2}/; + if (dateRegex.test(value)) { + // JavaScript Date 문자열을 MySQL 형식으로 변환 + const date = new Date(value); + return `'${date.toISOString().slice(0, 19).replace('T', ' ')}'`; + } else { + return `'${value.replace(/'/g, "''")}'`; // SQL 인젝션 방지를 위한 간단한 이스케이프 + } + } else if (typeof value === 'number') { + return String(value); + } else if (typeof value === 'boolean') { + return value ? '1' : '0'; + } else { + // 기타 객체는 문자열로 변환 + return `'${String(value).replace(/'/g, "''")}'`; + } + }).join(', '); + + // Primary Key 컬럼 추정 + const primaryKeyColumn = columns.includes('id') ? 'id' : + columns.includes('user_id') ? 'user_id' : + columns[0]; + + // UPDATE SET 절 생성 (Primary Key 제외) + const updateColumns = columns.filter(col => col !== primaryKeyColumn); + + let query: string; + const dbType = connection.db_type?.toLowerCase() || 'mysql'; + + if (dbType === 'mysql' || dbType === 'mariadb') { + // MySQL/MariaDB: ON DUPLICATE KEY UPDATE 사용 + if (updateColumns.length > 0) { + const updateSet = updateColumns.map(col => `${col} = VALUES(${col})`).join(', '); + query = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${formattedValues}) + ON DUPLICATE KEY UPDATE ${updateSet}`; + } else { + // Primary Key만 있는 경우 IGNORE 사용 + query = `INSERT IGNORE INTO ${tableName} (${columns.join(', ')}) VALUES (${formattedValues})`; + } + } else { + // 다른 DB는 기본 INSERT 사용 + query = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${formattedValues})`; + } + + console.log(`[BatchExternalDbService] 실행할 쿼리: ${query}`); + console.log(`[BatchExternalDbService] 삽입할 데이터:`, record); + + await connector.executeQuery(query); + successCount++; + } catch (error) { + console.error(`외부 DB 레코드 UPSERT 실패:`, error); + failedCount++; + } + } + + console.log(`[BatchExternalDbService] 외부 DB 데이터 삽입 완료: 성공 ${successCount}개, 실패 ${failedCount}개`); + + return { + success: true, + data: { successCount, failedCount } + }; + } catch (error) { + console.error(`외부 DB 데이터 삽입 오류 (connectionId: ${connectionId}, table: ${tableName}):`, error); + return { + success: false, + message: "외부 DB 데이터 삽입 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류" + }; + } + } + + /** + * REST API에서 데이터 조회 + */ + static async getDataFromRestApi( + apiUrl: string, + apiKey: string, + endpoint: string, + method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET', + columns?: string[], + limit: number = 100 + ): Promise> { + try { + console.log(`[BatchExternalDbService] REST API 데이터 조회: ${apiUrl}${endpoint}`); + + // REST API 커넥터 생성 + const connector = new RestApiConnector({ + baseUrl: apiUrl, + apiKey: apiKey, + timeout: 30000 + }); + + // 연결 테스트 + await connector.connect(); + + // 데이터 조회 + const result = await connector.executeQuery(endpoint, method); + let data = result.rows; + + // 컬럼 필터링 (지정된 컬럼만 추출) + if (columns && columns.length > 0) { + data = data.map(row => { + const filteredRow: any = {}; + columns.forEach(col => { + if (row.hasOwnProperty(col)) { + filteredRow[col] = row[col]; + } + }); + return filteredRow; + }); + } + + // 제한 개수 적용 + if (limit > 0) { + data = data.slice(0, limit); + } + + console.log(`[BatchExternalDbService] REST API 데이터 조회 완료: ${data.length}개 레코드`); + + return { + success: true, + data: data + }; + } catch (error) { + console.error(`[BatchExternalDbService] REST API 데이터 조회 오류 (${apiUrl}${endpoint}):`, error); + return { + success: false, + message: "REST API 데이터 조회 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류" + }; + } + } + + /** + * 템플릿 기반 REST API로 데이터 전송 (DB → REST API 배치용) + */ + static async sendDataToRestApiWithTemplate( + apiUrl: string, + apiKey: string, + endpoint: string, + method: 'POST' | 'PUT' | 'DELETE' = 'POST', + templateBody: string, + data: any[], + urlPathColumn?: string // URL 경로에 사용할 컬럼명 (PUT/DELETE용) + ): Promise> { + try { + console.log(`[BatchExternalDbService] 템플릿 기반 REST API 데이터 전송: ${apiUrl}${endpoint}, ${data.length}개 레코드`); + console.log(`[BatchExternalDbService] Request Body 템플릿:`, templateBody); + + // REST API 커넥터 생성 + const connector = new RestApiConnector({ + baseUrl: apiUrl, + apiKey: apiKey, + timeout: 30000 + }); + + // 연결 테스트 + await connector.connect(); + + let successCount = 0; + let failedCount = 0; + + // 각 레코드를 개별적으로 전송 + for (const record of data) { + try { + // 템플릿 처리: {{컬럼명}} → 실제 값으로 치환 + let processedBody = templateBody; + for (const [key, value] of Object.entries(record)) { + const placeholder = `{{${key}}}`; + let stringValue = ''; + + if (value !== null && value !== undefined) { + // Date 객체인 경우 다양한 포맷으로 변환 + if (value instanceof Date) { + // ISO 형식: 2025-09-25T07:22:52.000Z + stringValue = value.toISOString(); + + // 다른 포맷이 필요한 경우 여기서 처리 + // 예: YYYY-MM-DD 형식 + // stringValue = value.toISOString().split('T')[0]; + + // 예: YYYY-MM-DD HH:mm:ss 형식 + // stringValue = value.toISOString().replace('T', ' ').replace(/\.\d{3}Z$/, ''); + } else { + stringValue = String(value); + } + } + + processedBody = processedBody.replace(new RegExp(placeholder.replace(/[{}]/g, '\\$&'), 'g'), stringValue); + } + + console.log(`[BatchExternalDbService] 원본 레코드:`, record); + console.log(`[BatchExternalDbService] 처리된 Request Body:`, processedBody); + + // JSON 파싱하여 객체로 변환 + let requestData; + try { + requestData = JSON.parse(processedBody); + } catch (parseError) { + console.error(`[BatchExternalDbService] JSON 파싱 오류:`, parseError); + throw new Error(`Request Body JSON 파싱 실패: ${parseError}`); + } + + // URL 경로 파라미터 처리 (PUT/DELETE용) + let finalEndpoint = endpoint; + if ((method === 'PUT' || method === 'DELETE') && urlPathColumn && record[urlPathColumn]) { + // /api/users → /api/users/user123 + finalEndpoint = `${endpoint}/${record[urlPathColumn]}`; + } + + console.log(`[BatchExternalDbService] 실행할 API 호출: ${method} ${finalEndpoint}`); + console.log(`[BatchExternalDbService] 전송할 데이터:`, requestData); + + await connector.executeQuery(finalEndpoint, method, requestData); + successCount++; + } catch (error) { + console.error(`REST API 레코드 전송 실패:`, error); + failedCount++; + } + } + + console.log(`[BatchExternalDbService] 템플릿 기반 REST API 데이터 전송 완료: 성공 ${successCount}개, 실패 ${failedCount}개`); + + return { + success: true, + data: { successCount, failedCount } + }; + } catch (error) { + console.error(`[BatchExternalDbService] 템플릿 기반 REST API 데이터 전송 오류:`, error); + return { + success: false, + message: `REST API 데이터 전송 실패: ${error}`, + data: { successCount: 0, failedCount: 0 } + }; + } + } + + /** + * REST API로 데이터 전송 (기존 메서드) + */ + static async sendDataToRestApi( + apiUrl: string, + apiKey: string, + endpoint: string, + method: 'POST' | 'PUT' = 'POST', + data: any[] + ): Promise> { + try { + console.log(`[BatchExternalDbService] REST API 데이터 전송: ${apiUrl}${endpoint}, ${data.length}개 레코드`); + + // REST API 커넥터 생성 + const connector = new RestApiConnector({ + baseUrl: apiUrl, + apiKey: apiKey, + timeout: 30000 + }); + + // 연결 테스트 + await connector.connect(); + + let successCount = 0; + let failedCount = 0; + + // 각 레코드를 개별적으로 전송 + for (const record of data) { + try { + console.log(`[BatchExternalDbService] 실행할 API 호출: ${method} ${endpoint}`); + console.log(`[BatchExternalDbService] 전송할 데이터:`, record); + + await connector.executeQuery(endpoint, method, record); + successCount++; + } catch (error) { + console.error(`REST API 레코드 전송 실패:`, error); + failedCount++; + } + } + + console.log(`[BatchExternalDbService] REST API 데이터 전송 완료: 성공 ${successCount}개, 실패 ${failedCount}개`); + + return { + success: true, + data: { successCount, failedCount } + }; + } catch (error) { + console.error(`[BatchExternalDbService] REST API 데이터 전송 오류 (${apiUrl}${endpoint}):`, error); + return { + success: false, + message: "REST API 데이터 전송 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류" + }; + } + } +} diff --git a/backend-node/src/services/batchManagementService.ts b/backend-node/src/services/batchManagementService.ts new file mode 100644 index 00000000..1b082209 --- /dev/null +++ b/backend-node/src/services/batchManagementService.ts @@ -0,0 +1,373 @@ +// 배치관리 전용 서비스 (기존 소스와 완전 분리) +// 작성일: 2024-12-24 + +import prisma from "../config/database"; +import { PasswordEncryption } from "../utils/passwordEncryption"; +import { DatabaseConnectorFactory } from "../database/DatabaseConnectorFactory"; + +// 배치관리 전용 타입 정의 +export interface BatchConnectionInfo { + type: 'internal' | 'external'; + id?: number; + name: string; + db_type?: string; +} + +export interface BatchTableInfo { + table_name: string; + columns: BatchColumnInfo[]; + description?: string | null; +} + +export interface BatchColumnInfo { + column_name: string; + data_type: string; + is_nullable?: string; + column_default?: string | null; +} + +export interface BatchApiResponse { + success: boolean; + data?: T; + message?: string; + error?: string; +} + +export class BatchManagementService { + /** + * 배치관리용 연결 목록 조회 + */ + static async getAvailableConnections(): Promise> { + try { + const connections: BatchConnectionInfo[] = []; + + // 내부 DB 추가 + connections.push({ + type: 'internal', + name: '내부 데이터베이스 (PostgreSQL)', + db_type: 'postgresql' + }); + + // 활성화된 외부 DB 연결 조회 + const externalConnections = await prisma.external_db_connections.findMany({ + where: { is_active: 'Y' }, + select: { + id: true, + connection_name: true, + db_type: true, + description: true + }, + orderBy: { connection_name: 'asc' } + }); + + // 외부 DB 연결 추가 + externalConnections.forEach(conn => { + connections.push({ + type: 'external', + id: conn.id, + name: `${conn.connection_name} (${conn.db_type?.toUpperCase()})`, + db_type: conn.db_type || undefined + }); + }); + + return { + success: true, + data: connections, + message: `${connections.length}개의 연결을 조회했습니다.` + }; + } catch (error) { + console.error("배치관리 연결 목록 조회 실패:", error); + return { + success: false, + message: "연결 목록 조회 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류" + }; + } + } + + /** + * 배치관리용 테이블 목록 조회 + */ + static async getTablesFromConnection( + connectionType: 'internal' | 'external', + connectionId?: number + ): Promise> { + try { + let tables: BatchTableInfo[] = []; + + if (connectionType === 'internal') { + // 내부 DB 테이블 조회 + const result = await prisma.$queryRaw>` + SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'public' + AND table_type = 'BASE TABLE' + ORDER BY table_name + `; + + tables = result.map(row => ({ + table_name: row.table_name, + columns: [] + })); + } else if (connectionType === 'external' && connectionId) { + // 외부 DB 테이블 조회 + const tablesResult = await this.getExternalTables(connectionId); + if (tablesResult.success && tablesResult.data) { + tables = tablesResult.data; + } + } + + return { + success: true, + data: tables, + message: `${tables.length}개의 테이블을 조회했습니다.` + }; + } catch (error) { + console.error("배치관리 테이블 목록 조회 실패:", error); + return { + success: false, + message: "테이블 목록 조회 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류" + }; + } + } + + /** + * 배치관리용 테이블 컬럼 정보 조회 + */ + static async getTableColumns( + connectionType: 'internal' | 'external', + connectionId: number | undefined, + tableName: string + ): Promise> { + try { + console.log(`[BatchManagementService] getTableColumns 호출:`, { + connectionType, + connectionId, + tableName + }); + + let columns: BatchColumnInfo[] = []; + + if (connectionType === 'internal') { + // 내부 DB 컬럼 조회 + console.log(`[BatchManagementService] 내부 DB 컬럼 조회 시작: ${tableName}`); + + const result = await prisma.$queryRaw>` + SELECT + column_name, + data_type, + is_nullable, + column_default + FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = ${tableName} + ORDER BY ordinal_position + `; + + console.log(`[BatchManagementService] 쿼리 결과:`, result); + + console.log(`[BatchManagementService] 내부 DB 컬럼 조회 결과:`, result); + + columns = result.map(row => ({ + column_name: row.column_name, + data_type: row.data_type, + is_nullable: row.is_nullable, + column_default: row.column_default, + })); + } else if (connectionType === 'external' && connectionId) { + // 외부 DB 컬럼 조회 + console.log(`[BatchManagementService] 외부 DB 컬럼 조회 시작: connectionId=${connectionId}, tableName=${tableName}`); + + const columnsResult = await this.getExternalTableColumns(connectionId, tableName); + + console.log(`[BatchManagementService] 외부 DB 컬럼 조회 결과:`, columnsResult); + + if (columnsResult.success && columnsResult.data) { + columns = columnsResult.data; + } + } + + console.log(`[BatchManagementService] 최종 컬럼 목록:`, columns); + return { + success: true, + data: columns, + message: `${columns.length}개의 컬럼을 조회했습니다.` + }; + } catch (error) { + console.error("[BatchManagementService] 컬럼 정보 조회 오류:", error); + return { + success: false, + message: "컬럼 정보 조회 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류" + }; + } + } + + /** + * 외부 DB 테이블 목록 조회 (내부 구현) + */ + private static async getExternalTables(connectionId: number): Promise> { + try { + // 연결 정보 조회 + const connection = await prisma.external_db_connections.findUnique({ + where: { id: connectionId } + }); + + if (!connection) { + return { + success: false, + message: "연결 정보를 찾을 수 없습니다." + }; + } + + // 비밀번호 복호화 + const decryptedPassword = PasswordEncryption.decrypt(connection.password); + if (!decryptedPassword) { + return { + success: false, + message: "비밀번호 복호화에 실패했습니다." + }; + } + + // 연결 설정 준비 + const config = { + host: connection.host, + port: connection.port, + database: connection.database_name, + user: connection.username, + password: decryptedPassword, + connectionTimeoutMillis: connection.connection_timeout != null ? connection.connection_timeout * 1000 : undefined, + queryTimeoutMillis: connection.query_timeout != null ? connection.query_timeout * 1000 : undefined, + ssl: connection.ssl_enabled === "Y" ? { rejectUnauthorized: false } : false + }; + + // DatabaseConnectorFactory를 통한 테이블 목록 조회 + const connector = await DatabaseConnectorFactory.createConnector(connection.db_type, config, connectionId); + const tables = await connector.getTables(); + + return { + success: true, + message: "테이블 목록을 조회했습니다.", + data: tables + }; + } catch (error) { + console.error("외부 DB 테이블 목록 조회 오류:", error); + return { + success: false, + message: "테이블 목록 조회 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류" + }; + } + } + + /** + * 외부 DB 테이블 컬럼 정보 조회 (내부 구현) + */ + private static async getExternalTableColumns(connectionId: number, tableName: string): Promise> { + try { + console.log(`[BatchManagementService] getExternalTableColumns 호출: connectionId=${connectionId}, tableName=${tableName}`); + + // 연결 정보 조회 + const connection = await prisma.external_db_connections.findUnique({ + where: { id: connectionId } + }); + + if (!connection) { + console.log(`[BatchManagementService] 연결 정보를 찾을 수 없음: connectionId=${connectionId}`); + return { + success: false, + message: "연결 정보를 찾을 수 없습니다." + }; + } + + console.log(`[BatchManagementService] 연결 정보 조회 성공:`, { + id: connection.id, + connection_name: connection.connection_name, + db_type: connection.db_type, + host: connection.host, + port: connection.port, + database_name: connection.database_name + }); + + // 비밀번호 복호화 + const decryptedPassword = PasswordEncryption.decrypt(connection.password); + + // 연결 설정 준비 + const config = { + host: connection.host, + port: connection.port, + database: connection.database_name, + user: connection.username, + password: decryptedPassword, + connectionTimeoutMillis: connection.connection_timeout != null ? connection.connection_timeout * 1000 : undefined, + queryTimeoutMillis: connection.query_timeout != null ? connection.query_timeout * 1000 : undefined, + ssl: connection.ssl_enabled === "Y" ? { rejectUnauthorized: false } : false + }; + + console.log(`[BatchManagementService] 커넥터 생성 시작: db_type=${connection.db_type}`); + + // 데이터베이스 타입에 따른 커넥터 생성 + const connector = await DatabaseConnectorFactory.createConnector(connection.db_type, config, connectionId); + + console.log(`[BatchManagementService] 커넥터 생성 완료, 컬럼 조회 시작: tableName=${tableName}`); + + // 컬럼 정보 조회 + console.log(`[BatchManagementService] connector.getColumns 호출 전`); + const columns = await connector.getColumns(tableName); + + console.log(`[BatchManagementService] 원본 컬럼 조회 결과:`, columns); + console.log(`[BatchManagementService] 원본 컬럼 개수:`, columns ? columns.length : 'null/undefined'); + + // 각 데이터베이스 커넥터의 반환 구조가 다르므로 통일된 구조로 변환 + const standardizedColumns: BatchColumnInfo[] = columns.map((col: any) => { + console.log(`[BatchManagementService] 컬럼 변환 중:`, col); + + // MySQL/MariaDB 구조: {name, dataType, isNullable, defaultValue} (MySQLConnector만) + if (col.name && col.dataType !== undefined) { + const result = { + column_name: col.name, + data_type: col.dataType, + is_nullable: col.isNullable ? 'YES' : 'NO', + column_default: col.defaultValue || null, + }; + console.log(`[BatchManagementService] MySQL/MariaDB 구조로 변환:`, result); + return result; + } + // PostgreSQL/Oracle/MSSQL/MariaDB 구조: {column_name, data_type, is_nullable, column_default} + else { + const result = { + column_name: col.column_name || col.COLUMN_NAME, + data_type: col.data_type || col.DATA_TYPE, + is_nullable: col.is_nullable || col.IS_NULLABLE || (col.nullable === 'Y' ? 'YES' : 'NO'), + column_default: col.column_default || col.COLUMN_DEFAULT || null, + }; + console.log(`[BatchManagementService] 표준 구조로 변환:`, result); + return result; + } + }); + + console.log(`[BatchManagementService] 표준화된 컬럼 목록:`, standardizedColumns); + + return { + success: true, + data: standardizedColumns, + message: "컬럼 정보를 조회했습니다." + }; + } catch (error) { + console.error("[BatchManagementService] 외부 DB 컬럼 정보 조회 오류:", error); + console.error("[BatchManagementService] 오류 스택:", error instanceof Error ? error.stack : 'No stack trace'); + return { + success: false, + message: "컬럼 정보 조회 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류" + }; + } + } +} + diff --git a/backend-node/src/services/batchSchedulerService.ts b/backend-node/src/services/batchSchedulerService.ts new file mode 100644 index 00000000..3d032291 --- /dev/null +++ b/backend-node/src/services/batchSchedulerService.ts @@ -0,0 +1,484 @@ +// 배치 스케줄러 서비스 +// 작성일: 2024-12-24 + +import * as cron from 'node-cron'; +import prisma from '../config/database'; +import { BatchService } from './batchService'; +import { BatchExecutionLogService } from './batchExecutionLogService'; +import { logger } from '../utils/logger'; + +export class BatchSchedulerService { + private static scheduledTasks: Map = new Map(); + private static isInitialized = false; + + /** + * 스케줄러 초기화 + */ + static async initialize() { + if (this.isInitialized) { + logger.info('배치 스케줄러가 이미 초기화되었습니다.'); + return; + } + + try { + logger.info('배치 스케줄러 초기화 시작...'); + + // 활성화된 배치 설정들을 로드하여 스케줄 등록 + await this.loadActiveBatchConfigs(); + + this.isInitialized = true; + logger.info('배치 스케줄러 초기화 완료'); + } catch (error) { + logger.error('배치 스케줄러 초기화 실패:', error); + throw error; + } + } + + /** + * 활성화된 배치 설정들을 로드하여 스케줄 등록 + */ + private static async loadActiveBatchConfigs() { + try { + const activeConfigs = await prisma.batch_configs.findMany({ + where: { + is_active: 'Y' + }, + include: { + batch_mappings: true + } + }); + + logger.info(`활성화된 배치 설정 ${activeConfigs.length}개 발견`); + + for (const config of activeConfigs) { + await this.scheduleBatchConfig(config); + } + } catch (error) { + logger.error('활성화된 배치 설정 로드 실패:', error); + throw error; + } + } + + /** + * 배치 설정을 스케줄에 등록 + */ + static async scheduleBatchConfig(config: any) { + try { + const { id, batch_name, cron_schedule } = config; + + // 기존 스케줄이 있다면 제거 + if (this.scheduledTasks.has(id)) { + this.scheduledTasks.get(id)?.stop(); + this.scheduledTasks.delete(id); + } + + // cron 스케줄 유효성 검사 + if (!cron.validate(cron_schedule)) { + logger.error(`잘못된 cron 스케줄: ${cron_schedule} (배치 ID: ${id})`); + return; + } + + // 새로운 스케줄 등록 + const task = cron.schedule(cron_schedule, async () => { + logger.info(`🔄 스케줄 배치 실행 시작: ${batch_name} (ID: ${id})`); + await this.executeBatchConfig(config); + }); + + // 스케줄 시작 (기본적으로 시작되지만 명시적으로 호출) + task.start(); + + this.scheduledTasks.set(id, task); + logger.info(`배치 스케줄 등록 완료: ${batch_name} (ID: ${id}, Schedule: ${cron_schedule}) - 스케줄 시작됨`); + } catch (error) { + logger.error(`배치 스케줄 등록 실패 (ID: ${config.id}):`, error); + } + } + + /** + * 배치 설정 스케줄 제거 + */ + static async unscheduleBatchConfig(batchConfigId: number) { + try { + if (this.scheduledTasks.has(batchConfigId)) { + this.scheduledTasks.get(batchConfigId)?.stop(); + this.scheduledTasks.delete(batchConfigId); + logger.info(`배치 스케줄 제거 완료 (ID: ${batchConfigId})`); + } + } catch (error) { + logger.error(`배치 스케줄 제거 실패 (ID: ${batchConfigId}):`, error); + } + } + + /** + * 배치 설정 업데이트 시 스케줄 재등록 + */ + static async updateBatchSchedule(configId: number) { + try { + // 기존 스케줄 제거 + await this.unscheduleBatchConfig(configId); + + // 업데이트된 배치 설정 조회 + const config = await prisma.batch_configs.findUnique({ + where: { id: configId }, + include: { batch_mappings: true } + }); + + if (!config) { + logger.warn(`배치 설정을 찾을 수 없습니다: ID ${configId}`); + return; + } + + // 활성화된 배치만 다시 스케줄 등록 + if (config.is_active === 'Y') { + await this.scheduleBatchConfig(config); + logger.info(`배치 스케줄 업데이트 완료: ${config.batch_name} (ID: ${configId})`); + } else { + logger.info(`비활성화된 배치 스케줄 제거: ${config.batch_name} (ID: ${configId})`); + } + } catch (error) { + logger.error(`배치 스케줄 업데이트 실패: ID ${configId}`, error); + } + } + + /** + * 배치 설정 실행 + */ + private static async executeBatchConfig(config: any) { + const startTime = new Date(); + let executionLog: any = null; + + try { + logger.info(`배치 실행 시작: ${config.batch_name} (ID: ${config.id})`); + + // 실행 로그 생성 + const executionLogResponse = await BatchExecutionLogService.createExecutionLog({ + batch_config_id: config.id, + execution_status: 'RUNNING', + start_time: startTime, + total_records: 0, + success_records: 0, + failed_records: 0 + }); + + if (!executionLogResponse.success || !executionLogResponse.data) { + logger.error(`배치 실행 로그 생성 실패: ${config.batch_name}`, executionLogResponse.message); + return; + } + + executionLog = executionLogResponse.data; + + // 실제 배치 실행 로직 (수동 실행과 동일한 로직 사용) + const result = await this.executeBatchMappings(config); + + // 실행 로그 업데이트 (성공) + await BatchExecutionLogService.updateExecutionLog(executionLog.id, { + execution_status: 'SUCCESS', + end_time: new Date(), + duration_ms: Date.now() - startTime.getTime(), + total_records: result.totalRecords, + success_records: result.successRecords, + failed_records: result.failedRecords + }); + + logger.info(`배치 실행 완료: ${config.batch_name} (처리된 레코드: ${result.totalRecords})`); + } catch (error) { + logger.error(`배치 실행 실패: ${config.batch_name}`, error); + + // 실행 로그 업데이트 (실패) + if (executionLog) { + await BatchExecutionLogService.updateExecutionLog(executionLog.id, { + execution_status: 'FAILED', + end_time: new Date(), + duration_ms: Date.now() - startTime.getTime(), + error_message: error instanceof Error ? error.message : '알 수 없는 오류', + error_details: error instanceof Error ? error.stack : String(error) + }); + } + } + } + + /** + * 배치 매핑 실행 (수동 실행과 동일한 로직) + */ + private static async executeBatchMappings(config: any) { + let totalRecords = 0; + let successRecords = 0; + let failedRecords = 0; + + if (!config.batch_mappings || config.batch_mappings.length === 0) { + logger.warn(`배치 매핑이 없습니다: ${config.batch_name}`); + return { totalRecords, successRecords, failedRecords }; + } + + // 테이블별로 매핑을 그룹화 + const tableGroups = new Map(); + + for (const mapping of config.batch_mappings) { + const key = `${mapping.from_connection_type}:${mapping.from_connection_id || 'internal'}:${mapping.from_table_name}`; + if (!tableGroups.has(key)) { + tableGroups.set(key, []); + } + tableGroups.get(key)!.push(mapping); + } + + // 각 테이블 그룹별로 처리 + for (const [tableKey, mappings] of tableGroups) { + try { + const firstMapping = mappings[0]; + logger.info(`테이블 처리 시작: ${tableKey} -> ${mappings.length}개 컬럼 매핑`); + + let fromData: any[] = []; + + // FROM 데이터 조회 (DB 또는 REST API) + if (firstMapping.from_connection_type === 'restapi') { + // REST API에서 데이터 조회 + logger.info(`REST API에서 데이터 조회: ${firstMapping.from_api_url}${firstMapping.from_table_name}`); + const { BatchExternalDbService } = await import('./batchExternalDbService'); + const apiResult = await BatchExternalDbService.getDataFromRestApi( + firstMapping.from_api_url!, + firstMapping.from_api_key!, + firstMapping.from_table_name, + firstMapping.from_api_method as 'GET' | 'POST' | 'PUT' | 'DELETE' || 'GET', + mappings.map((m: any) => m.from_column_name) + ); + + if (apiResult.success && apiResult.data) { + fromData = apiResult.data; + } else { + throw new Error(`REST API 데이터 조회 실패: ${apiResult.message}`); + } + } else { + // DB에서 데이터 조회 + const fromColumns = mappings.map((m: any) => m.from_column_name); + fromData = await BatchService.getDataFromTableWithColumns( + firstMapping.from_table_name, + fromColumns, + firstMapping.from_connection_type as 'internal' | 'external', + firstMapping.from_connection_id || undefined + ); + } + + totalRecords += fromData.length; + + // 컬럼 매핑 적용하여 TO 테이블 형식으로 변환 + const mappedData = fromData.map(row => { + const mappedRow: any = {}; + for (const mapping of mappings) { + // DB → REST API 배치인지 확인 + if (firstMapping.to_connection_type === 'restapi' && mapping.to_api_body) { + // DB → REST API: 원본 컬럼명을 키로 사용 (템플릿 처리용) + mappedRow[mapping.from_column_name] = row[mapping.from_column_name]; + } else { + // 기존 로직: to_column_name을 키로 사용 + mappedRow[mapping.to_column_name] = row[mapping.from_column_name]; + } + } + return mappedRow; + }); + + // TO 테이블에 데이터 삽입 (DB 또는 REST API) + let insertResult: { successCount: number; failedCount: number }; + + if (firstMapping.to_connection_type === 'restapi') { + // REST API로 데이터 전송 + logger.info(`REST API로 데이터 전송: ${firstMapping.to_api_url}${firstMapping.to_table_name}`); + const { BatchExternalDbService } = await import('./batchExternalDbService'); + + // DB → REST API 배치인지 확인 (to_api_body가 있으면 템플릿 기반) + const hasTemplate = mappings.some((m: any) => m.to_api_body); + + if (hasTemplate) { + // 템플릿 기반 REST API 전송 (DB → REST API 배치) + const templateBody = firstMapping.to_api_body || '{}'; + logger.info(`템플릿 기반 REST API 전송, Request Body 템플릿: ${templateBody}`); + + // URL 경로 컬럼 찾기 (PUT/DELETE용) + const urlPathColumn = mappings.find((m: any) => m.to_column_name === 'URL_PATH_PARAM')?.from_column_name; + + const apiResult = await BatchExternalDbService.sendDataToRestApiWithTemplate( + firstMapping.to_api_url!, + firstMapping.to_api_key!, + firstMapping.to_table_name, + firstMapping.to_api_method as 'POST' | 'PUT' | 'DELETE' || 'POST', + templateBody, + mappedData, + urlPathColumn + ); + + if (apiResult.success && apiResult.data) { + insertResult = apiResult.data; + } else { + throw new Error(`템플릿 기반 REST API 데이터 전송 실패: ${apiResult.message}`); + } + } else { + // 기존 REST API 전송 (REST API → DB 배치) + const apiResult = await BatchExternalDbService.sendDataToRestApi( + firstMapping.to_api_url!, + firstMapping.to_api_key!, + firstMapping.to_table_name, + firstMapping.to_api_method as 'POST' | 'PUT' || 'POST', + mappedData + ); + + if (apiResult.success && apiResult.data) { + insertResult = apiResult.data; + } else { + throw new Error(`REST API 데이터 전송 실패: ${apiResult.message}`); + } + } + } else { + // DB에 데이터 삽입 + insertResult = await BatchService.insertDataToTable( + firstMapping.to_table_name, + mappedData, + firstMapping.to_connection_type as 'internal' | 'external', + firstMapping.to_connection_id || undefined + ); + } + + successRecords += insertResult.successCount; + failedRecords += insertResult.failedCount; + + logger.info(`테이블 처리 완료: ${insertResult.successCount}개 성공, ${insertResult.failedCount}개 실패`); + } catch (error) { + logger.error(`테이블 처리 실패: ${tableKey}`, error); + failedRecords += 1; + } + } + + return { totalRecords, successRecords, failedRecords }; + } + + /** + * 배치 매핑 처리 (기존 메서드 - 사용 안 함) + */ + private static async processBatchMappings(config: any) { + const { batch_mappings } = config; + let totalRecords = 0; + let successRecords = 0; + let failedRecords = 0; + + if (!batch_mappings || batch_mappings.length === 0) { + logger.warn(`배치 매핑이 없습니다: ${config.batch_name}`); + return { totalRecords, successRecords, failedRecords }; + } + + for (const mapping of batch_mappings) { + try { + logger.info(`매핑 처리 시작: ${mapping.from_table_name} -> ${mapping.to_table_name}`); + + // FROM 테이블에서 데이터 조회 + const fromData = await this.getDataFromSource(mapping); + totalRecords += fromData.length; + + // TO 테이블에 데이터 삽입 + const insertResult = await this.insertDataToTarget(mapping, fromData); + successRecords += insertResult.successCount; + failedRecords += insertResult.failedCount; + + logger.info(`매핑 처리 완료: ${insertResult.successCount}개 성공, ${insertResult.failedCount}개 실패`); + } catch (error) { + logger.error(`매핑 처리 실패: ${mapping.from_table_name} -> ${mapping.to_table_name}`, error); + failedRecords += 1; + } + } + + return { totalRecords, successRecords, failedRecords }; + } + + /** + * FROM 테이블에서 데이터 조회 + */ + private static async getDataFromSource(mapping: any) { + try { + if (mapping.from_connection_type === 'internal') { + // 내부 DB에서 조회 + const result = await prisma.$queryRawUnsafe( + `SELECT * FROM ${mapping.from_table_name}` + ); + return result as any[]; + } else { + // 외부 DB에서 조회 (구현 필요) + logger.warn('외부 DB 조회는 아직 구현되지 않았습니다.'); + return []; + } + } catch (error) { + logger.error(`FROM 테이블 데이터 조회 실패: ${mapping.from_table_name}`, error); + throw error; + } + } + + /** + * TO 테이블에 데이터 삽입 + */ + private static async insertDataToTarget(mapping: any, data: any[]) { + let successCount = 0; + let failedCount = 0; + + try { + if (mapping.to_connection_type === 'internal') { + // 내부 DB에 삽입 + for (const record of data) { + try { + // 매핑된 컬럼만 추출 + const mappedData = this.mapColumns(record, mapping); + + await prisma.$executeRawUnsafe( + `INSERT INTO ${mapping.to_table_name} (${Object.keys(mappedData).join(', ')}) VALUES (${Object.values(mappedData).map(() => '?').join(', ')})`, + ...Object.values(mappedData) + ); + successCount++; + } catch (error) { + logger.error(`레코드 삽입 실패:`, error); + failedCount++; + } + } + } else { + // 외부 DB에 삽입 (구현 필요) + logger.warn('외부 DB 삽입은 아직 구현되지 않았습니다.'); + failedCount = data.length; + } + } catch (error) { + logger.error(`TO 테이블 데이터 삽입 실패: ${mapping.to_table_name}`, error); + throw error; + } + + return { successCount, failedCount }; + } + + /** + * 컬럼 매핑 + */ + private static mapColumns(record: any, mapping: any) { + const mappedData: any = {}; + + // 단순한 컬럼 매핑 (실제로는 더 복잡한 로직 필요) + mappedData[mapping.to_column_name] = record[mapping.from_column_name]; + + return mappedData; + } + + /** + * 모든 스케줄 중지 + */ + static async stopAllSchedules() { + try { + for (const [id, task] of this.scheduledTasks) { + task.stop(); + logger.info(`배치 스케줄 중지: ID ${id}`); + } + this.scheduledTasks.clear(); + this.isInitialized = false; + logger.info('모든 배치 스케줄이 중지되었습니다.'); + } catch (error) { + logger.error('배치 스케줄 중지 실패:', error); + } + } + + /** + * 현재 등록된 스케줄 목록 조회 + */ + static getScheduledTasks() { + return Array.from(this.scheduledTasks.keys()); + } +} diff --git a/backend-node/src/services/batchService.ts b/backend-node/src/services/batchService.ts index f20945d8..edac1629 100644 --- a/backend-node/src/services/batchService.ts +++ b/backend-node/src/services/batchService.ts @@ -1,275 +1,807 @@ -// 배치 관리 서비스 -// 작성일: 2024-12-23 +// 배치관리 서비스 +// 작성일: 2024-12-24 -import { PrismaClient } from "@prisma/client"; +import prisma from "../config/database"; import { - BatchJob, - BatchJobFilter, - BatchExecution, - BatchMonitoring, -} from "../types/batchManagement"; - -const prisma = new PrismaClient(); + BatchConfig, + BatchMapping, + BatchConfigFilter, + BatchMappingRequest, + BatchValidationResult, + ApiResponse, + ConnectionInfo, + TableInfo, + ColumnInfo, + CreateBatchConfigRequest, + UpdateBatchConfigRequest, +} from "../types/batchTypes"; +import { BatchExternalDbService } from "./batchExternalDbService"; +import { DbConnectionManager } from "./dbConnectionManager"; export class BatchService { /** - * 배치 작업 목록 조회 + * 배치 설정 목록 조회 */ - static async getBatchJobs(filter: BatchJobFilter): Promise { - const whereCondition: any = { - company_code: filter.company_code || "*", - }; + static async getBatchConfigs( + filter: BatchConfigFilter + ): Promise> { + try { + const where: any = {}; - if (filter.job_name) { - whereCondition.job_name = { - contains: filter.job_name, - mode: "insensitive", + // 필터 조건 적용 + if (filter.is_active) { + where.is_active = filter.is_active; + } + + if (filter.company_code) { + where.company_code = filter.company_code; + } + + // 검색 조건 적용 + if (filter.search && filter.search.trim()) { + where.OR = [ + { + batch_name: { + contains: filter.search.trim(), + mode: "insensitive", + }, + }, + { + description: { + contains: filter.search.trim(), + mode: "insensitive", + }, + }, + ]; + } + + const page = filter.page || 1; + const limit = filter.limit || 10; + const skip = (page - 1) * limit; + + const [batchConfigs, total] = await Promise.all([ + prisma.batch_configs.findMany({ + where, + include: { + batch_mappings: true, + }, + orderBy: [{ is_active: "desc" }, { batch_name: "asc" }], + skip, + take: limit, + }), + prisma.batch_configs.count({ where }), + ]); + + return { + success: true, + data: batchConfigs as BatchConfig[], + pagination: { + page, + limit, + total, + totalPages: Math.ceil(total / limit), + }, + }; + } catch (error) { + console.error("배치 설정 목록 조회 오류:", error); + return { + success: false, + message: "배치 설정 목록 조회에 실패했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류", }; } - - if (filter.job_type) { - whereCondition.job_type = filter.job_type; - } - - if (filter.is_active) { - whereCondition.is_active = filter.is_active === "Y"; - } - - if (filter.search) { - whereCondition.OR = [ - { job_name: { contains: filter.search, mode: "insensitive" } }, - { description: { contains: filter.search, mode: "insensitive" } }, - ]; - } - - const jobs = await prisma.batch_jobs.findMany({ - where: whereCondition, - orderBy: { created_date: "desc" }, - }); - - return jobs.map((job: any) => ({ - ...job, - is_active: job.is_active ? "Y" : "N", - })) as BatchJob[]; } /** - * 배치 작업 상세 조회 + * 특정 배치 설정 조회 */ - static async getBatchJobById(id: number): Promise { - const job = await prisma.batch_jobs.findUnique({ - where: { id }, - }); + static async getBatchConfigById( + id: number + ): Promise> { + try { + const batchConfig = await prisma.batch_configs.findUnique({ + where: { id }, + include: { + batch_mappings: { + orderBy: [ + { from_table_name: "asc" }, + { from_column_name: "asc" }, + { mapping_order: "asc" }, + ], + }, + }, + }); - if (!job) return null; + if (!batchConfig) { + return { + success: false, + message: "배치 설정을 찾을 수 없습니다.", + }; + } - return { - ...job, - is_active: job.is_active ? "Y" : "N", - } as BatchJob; - } - - /** - * 배치 작업 생성 - */ - static async createBatchJob(data: BatchJob): Promise { - const { id, config_json, ...createData } = data; - const job = await prisma.batch_jobs.create({ - data: { - ...createData, - is_active: data.is_active, - config_json: config_json || undefined, - created_date: new Date(), - updated_date: new Date(), - }, - }); - - return { - ...job, - is_active: job.is_active ? "Y" : "N", - } as BatchJob; - } - - /** - * 배치 작업 수정 - */ - static async updateBatchJob( - id: number, - data: Partial - ): Promise { - const updateData: any = { - ...data, - updated_date: new Date(), - }; - - if (data.is_active !== undefined) { - updateData.is_active = data.is_active; + return { + success: true, + data: batchConfig as BatchConfig, + }; + } catch (error) { + console.error("배치 설정 조회 오류:", error); + return { + success: false, + message: "배치 설정 조회에 실패했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류", + }; } - - const job = await prisma.batch_jobs.update({ - where: { id }, - data: updateData, - }); - - return { - ...job, - is_active: job.is_active ? "Y" : "N", - } as BatchJob; } /** - * 배치 작업 삭제 + * 배치 설정 생성 */ - static async deleteBatchJob(id: number): Promise { - await prisma.batch_jobs.delete({ - where: { id }, - }); - } - - /** - * 배치 작업 수동 실행 - */ - static async executeBatchJob(id: number): Promise { - const job = await prisma.batch_jobs.findUnique({ - where: { id }, - }); - - if (!job) { - throw new Error("배치 작업을 찾을 수 없습니다."); - } - - if (!job.is_active) { - throw new Error("비활성화된 배치 작업입니다."); - } - - // 배치 실행 기록 생성 - const execution = await prisma.batch_job_executions.create({ - data: { - job_id: id, - execution_id: `exec_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, - status: "RUNNING", - start_time: new Date(), - created_at: new Date(), - }, - }); - - // 실제 배치 작업 실행 로직은 여기에 구현 - // 현재는 시뮬레이션으로 처리 - setTimeout(async () => { - try { - // 배치 작업 시뮬레이션 - await new Promise((resolve) => setTimeout(resolve, 5000)); - - await prisma.batch_job_executions.update({ - where: { id: execution.id }, + static async createBatchConfig( + data: CreateBatchConfigRequest, + userId?: string + ): Promise> { + try { + // 트랜잭션으로 배치 설정과 매핑 생성 + const result = await prisma.$transaction(async (tx) => { + // 배치 설정 생성 + const batchConfig = await tx.batch_configs.create({ data: { - status: "SUCCESS", - end_time: new Date(), - exit_message: "배치 작업이 성공적으로 완료되었습니다.", + batch_name: data.batchName, + description: data.description, + cron_schedule: data.cronSchedule, + created_by: userId, + updated_by: userId, }, }); - } catch (error) { - await prisma.batch_job_executions.update({ - where: { id: execution.id }, - data: { - status: "FAILED", - end_time: new Date(), - exit_message: - error instanceof Error ? error.message : "알 수 없는 오류", - }, + + // 배치 매핑 생성 + const mappings = await Promise.all( + data.mappings.map((mapping, index) => + tx.batch_mappings.create({ + data: { + batch_config_id: batchConfig.id, + from_connection_type: mapping.from_connection_type, + from_connection_id: mapping.from_connection_id, + from_table_name: mapping.from_table_name, + from_column_name: mapping.from_column_name, + from_column_type: mapping.from_column_type, + from_api_url: mapping.from_api_url, + from_api_key: mapping.from_api_key, + from_api_method: mapping.from_api_method, + to_connection_type: mapping.to_connection_type, + to_connection_id: mapping.to_connection_id, + to_table_name: mapping.to_table_name, + to_column_name: mapping.to_column_name, + to_column_type: mapping.to_column_type, + to_api_url: mapping.to_api_url, + to_api_key: mapping.to_api_key, + to_api_method: mapping.to_api_method, + // to_api_body: mapping.to_api_body, // Request Body 템플릿 추가 - 임시 주석 처리 + mapping_order: mapping.mapping_order || index + 1, + created_by: userId, + }, + }) + ) + ); + + return { + ...batchConfig, + batch_mappings: mappings, + }; + }); + + return { + success: true, + data: result as BatchConfig, + message: "배치 설정이 성공적으로 생성되었습니다.", + }; + } catch (error) { + console.error("배치 설정 생성 오류:", error); + return { + success: false, + message: "배치 설정 생성에 실패했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류", + }; + } + } + + /** + * 배치 설정 수정 + */ + static async updateBatchConfig( + id: number, + data: UpdateBatchConfigRequest, + userId?: string + ): Promise> { + try { + // 기존 배치 설정 확인 + const existingConfig = await prisma.batch_configs.findUnique({ + where: { id }, + include: { batch_mappings: true }, + }); + + if (!existingConfig) { + return { + success: false, + message: "배치 설정을 찾을 수 없습니다.", + }; + } + + // 트랜잭션으로 업데이트 + const result = await prisma.$transaction(async (tx) => { + // 배치 설정 업데이트 + const updateData: any = { + updated_by: userId, + }; + + if (data.batchName) updateData.batch_name = data.batchName; + if (data.description !== undefined) updateData.description = data.description; + if (data.cronSchedule) updateData.cron_schedule = data.cronSchedule; + if (data.isActive !== undefined) updateData.is_active = data.isActive; + + const batchConfig = await tx.batch_configs.update({ + where: { id }, + data: updateData, + }); + + // 매핑이 제공된 경우 기존 매핑 삭제 후 새로 생성 + if (data.mappings) { + await tx.batch_mappings.deleteMany({ + where: { batch_config_id: id }, + }); + + const mappings = await Promise.all( + data.mappings.map((mapping, index) => + tx.batch_mappings.create({ + data: { + batch_config_id: id, + from_connection_type: mapping.from_connection_type, + from_connection_id: mapping.from_connection_id, + from_table_name: mapping.from_table_name, + from_column_name: mapping.from_column_name, + from_column_type: mapping.from_column_type, + to_connection_type: mapping.to_connection_type, + to_connection_id: mapping.to_connection_id, + to_table_name: mapping.to_table_name, + to_column_name: mapping.to_column_name, + to_column_type: mapping.to_column_type, + mapping_order: mapping.mapping_order || index + 1, + created_by: userId, + }, + }) + ) + ); + + return { + ...batchConfig, + batch_mappings: mappings, + }; + } else { + return { + ...batchConfig, + batch_mappings: existingConfig.batch_mappings, + }; + } + }); + + return { + success: true, + data: result as BatchConfig, + message: "배치 설정이 성공적으로 수정되었습니다.", + }; + } catch (error) { + console.error("배치 설정 수정 오류:", error); + return { + success: false, + message: "배치 설정 수정에 실패했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류", + }; + } + } + + /** + * 배치 설정 삭제 (논리 삭제) + */ + static async deleteBatchConfig( + id: number, + userId?: string + ): Promise> { + try { + const existingConfig = await prisma.batch_configs.findUnique({ + where: { id }, + }); + + if (!existingConfig) { + return { + success: false, + message: "배치 설정을 찾을 수 없습니다.", + }; + } + + // 배치 매핑 먼저 삭제 (외래키 제약) + await prisma.batch_mappings.deleteMany({ + where: { batch_config_id: id } + }); + + // 배치 설정 삭제 + await prisma.batch_configs.delete({ + where: { id } + }); + + return { + success: true, + message: "배치 설정이 성공적으로 삭제되었습니다.", + }; + } catch (error) { + console.error("배치 설정 삭제 오류:", error); + return { + success: false, + message: "배치 설정 삭제에 실패했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류", + }; + } + } + + /** + * 사용 가능한 커넥션 목록 조회 + */ + static async getAvailableConnections(): Promise> { + try { + const connections: ConnectionInfo[] = []; + + // 내부 DB 추가 + connections.push({ + type: 'internal', + name: 'Internal Database', + db_type: 'postgresql', + }); + + // 외부 DB 연결 조회 + const externalConnections = await BatchExternalDbService.getAvailableConnections(); + + if (externalConnections.success && externalConnections.data) { + externalConnections.data.forEach((conn) => { + connections.push({ + type: 'external', + id: conn.id, + name: conn.name, + db_type: conn.db_type, + }); }); } - }, 0); - return { - ...execution, - execution_status: execution.status as any, - started_at: execution.start_time, - completed_at: execution.end_time, - error_message: execution.exit_message, - } as BatchExecution; + return { + success: true, + data: connections, + }; + } catch (error) { + console.error("커넥션 목록 조회 오류:", error); + return { + success: false, + message: "커넥션 목록 조회에 실패했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류", + }; + } } /** - * 배치 실행 목록 조회 + * 특정 커넥션의 테이블 목록 조회 */ - static async getBatchExecutions(jobId?: number): Promise { - const whereCondition: any = {}; + static async getTablesFromConnection( + connectionType: 'internal' | 'external', + connectionId?: number + ): Promise> { + try { + let tables: TableInfo[] = []; - if (jobId) { - whereCondition.job_id = jobId; + if (connectionType === 'internal') { + // 내부 DB 테이블 조회 + const result = await prisma.$queryRaw>` + SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'public' + AND table_type = 'BASE TABLE' + ORDER BY table_name + `; + tables = result.map(row => ({ + table_name: row.table_name, + columns: [] + })); + } else if (connectionType === 'external' && connectionId) { + // 외부 DB 테이블 조회 + const tablesResult = await BatchExternalDbService.getTablesFromConnection(connectionType, connectionId); + if (tablesResult.success && tablesResult.data) { + tables = tablesResult.data; + } + } + + return { + success: true, + data: tables, + }; + } catch (error) { + console.error("테이블 목록 조회 오류:", error); + return { + success: false, + message: "테이블 목록 조회에 실패했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류", + }; + } + } + + /** + * 특정 테이블의 컬럼 정보 조회 + */ + static async getTableColumns( + connectionType: 'internal' | 'external', + connectionId: number | undefined, + tableName: string + ): Promise> { + try { + console.log(`[BatchService] getTableColumns 호출:`, { + connectionType, + connectionId, + tableName + }); + + let columns: ColumnInfo[] = []; + + if (connectionType === 'internal') { + // 내부 DB 컬럼 조회 + console.log(`[BatchService] 내부 DB 컬럼 조회 시작: ${tableName}`); + + const result = await prisma.$queryRaw>` + SELECT + column_name, + data_type, + is_nullable, + column_default + FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = ${tableName} + ORDER BY ordinal_position + `; + + console.log(`[BatchService] 내부 DB 컬럼 조회 결과:`, result); + + columns = result.map(row => ({ + column_name: row.column_name, + data_type: row.data_type, + is_nullable: row.is_nullable, + column_default: row.column_default, + })); + } else if (connectionType === 'external' && connectionId) { + // 외부 DB 컬럼 조회 + console.log(`[BatchService] 외부 DB 컬럼 조회 시작: connectionId=${connectionId}, tableName=${tableName}`); + + const columnsResult = await BatchExternalDbService.getTableColumns( + connectionType, + connectionId, + tableName + ); + + console.log(`[BatchService] 외부 DB 컬럼 조회 결과:`, columnsResult); + + if (columnsResult.success && columnsResult.data) { + columns = columnsResult.data; + } + + console.log(`[BatchService] 외부 DB 컬럼:`, columns); + } + + return { + success: true, + data: columns, + }; + } catch (error) { + console.error("컬럼 정보 조회 오류:", error); + return { + success: false, + message: "컬럼 정보 조회에 실패했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류", + }; + } + } + + /** + * 배치 실행 로그 생성 + */ + static async createExecutionLog(data: { + batch_config_id: number; + execution_status: string; + start_time: Date; + total_records: number; + success_records: number; + failed_records: number; + }): Promise { + try { + const executionLog = await prisma.batch_execution_logs.create({ + data: { + batch_config_id: data.batch_config_id, + execution_status: data.execution_status, + start_time: data.start_time, + total_records: data.total_records, + success_records: data.success_records, + failed_records: data.failed_records, + }, + }); + + return executionLog; + } catch (error) { + console.error("배치 실행 로그 생성 오류:", error); + throw error; + } + } + + /** + * 배치 실행 로그 업데이트 + */ + static async updateExecutionLog( + id: number, + data: { + execution_status?: string; + end_time?: Date; + duration_ms?: number; + total_records?: number; + success_records?: number; + failed_records?: number; + error_message?: string; + } + ): Promise { + try { + await prisma.batch_execution_logs.update({ + where: { id }, + data, + }); + } catch (error) { + console.error("배치 실행 로그 업데이트 오류:", error); + throw error; + } + } + + /** + * 테이블에서 데이터 조회 (연결 타입에 따라 내부/외부 DB 구분) + */ + static async getDataFromTable( + tableName: string, + connectionType: 'internal' | 'external' = 'internal', + connectionId?: number + ): Promise { + try { + console.log(`[BatchService] 테이블에서 데이터 조회: ${tableName} (${connectionType}${connectionId ? `:${connectionId}` : ''})`); + + if (connectionType === 'internal') { + // 내부 DB에서 데이터 조회 + const result = await prisma.$queryRawUnsafe(`SELECT * FROM ${tableName} LIMIT 100`); + console.log(`[BatchService] 내부 DB 데이터 조회 결과: ${Array.isArray(result) ? result.length : 0}개 레코드`); + return result as any[]; + } else if (connectionType === 'external' && connectionId) { + // 외부 DB에서 데이터 조회 + const result = await BatchExternalDbService.getDataFromTable(connectionId, tableName); + if (result.success && result.data) { + console.log(`[BatchService] 외부 DB 데이터 조회 결과: ${result.data.length}개 레코드`); + return result.data; + } else { + console.error(`외부 DB 데이터 조회 실패: ${result.message}`); + return []; + } + } else { + throw new Error(`잘못된 연결 타입 또는 연결 ID: ${connectionType}, ${connectionId}`); + } + } catch (error) { + console.error(`테이블 데이터 조회 오류 (${tableName}):`, error); + throw error; + } + } + + /** + * 테이블에서 특정 컬럼들만 조회 (연결 타입에 따라 내부/외부 DB 구분) + */ + static async getDataFromTableWithColumns( + tableName: string, + columns: string[], + connectionType: 'internal' | 'external' = 'internal', + connectionId?: number + ): Promise { + try { + console.log(`[BatchService] 테이블에서 특정 컬럼 데이터 조회: ${tableName} (${columns.join(', ')}) (${connectionType}${connectionId ? `:${connectionId}` : ''})`); + + if (connectionType === 'internal') { + // 내부 DB에서 특정 컬럼만 조회 + const columnList = columns.join(', '); + const result = await prisma.$queryRawUnsafe(`SELECT ${columnList} FROM ${tableName} LIMIT 100`); + console.log(`[BatchService] 내부 DB 특정 컬럼 조회 결과: ${Array.isArray(result) ? result.length : 0}개 레코드`); + return result as any[]; + } else if (connectionType === 'external' && connectionId) { + // 외부 DB에서 특정 컬럼만 조회 + const result = await BatchExternalDbService.getDataFromTableWithColumns(connectionId, tableName, columns); + if (result.success && result.data) { + console.log(`[BatchService] 외부 DB 특정 컬럼 조회 결과: ${result.data.length}개 레코드`); + return result.data; + } else { + console.error(`외부 DB 특정 컬럼 조회 실패: ${result.message}`); + return []; + } + } else { + throw new Error(`잘못된 연결 타입 또는 연결 ID: ${connectionType}, ${connectionId}`); + } + } catch (error) { + console.error(`테이블 특정 컬럼 조회 오류 (${tableName}):`, error); + throw error; + } + } + + /** + * 테이블에 데이터 삽입 (연결 타입에 따라 내부/외부 DB 구분) + */ + static async insertDataToTable( + tableName: string, + data: any[], + connectionType: 'internal' | 'external' = 'internal', + connectionId?: number + ): Promise<{ + successCount: number; + failedCount: number; + }> { + try { + console.log(`[BatchService] 테이블에 데이터 삽입: ${tableName} (${connectionType}${connectionId ? `:${connectionId}` : ''}), ${data.length}개 레코드`); + + if (!data || data.length === 0) { + return { successCount: 0, failedCount: 0 }; + } + + if (connectionType === 'internal') { + // 내부 DB에 데이터 삽입 + let successCount = 0; + let failedCount = 0; + + // 각 레코드를 개별적으로 삽입 (UPSERT 방식으로 중복 처리) + for (const record of data) { + try { + // 동적 UPSERT 쿼리 생성 (PostgreSQL ON CONFLICT 사용) + const columns = Object.keys(record); + const values = Object.values(record).map(value => { + // Date 객체를 ISO 문자열로 변환 (PostgreSQL이 자동으로 파싱) + if (value instanceof Date) { + return value.toISOString(); + } + // JavaScript Date 문자열을 Date 객체로 변환 후 ISO 문자열로 + if (typeof value === 'string') { + const dateRegex = /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{2}\s+\d{4}\s+\d{2}:\d{2}:\d{2}/; + if (dateRegex.test(value)) { + return new Date(value).toISOString(); + } + // ISO 날짜 문자열 형식 체크 (2025-09-24T06:29:01.351Z) + const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/; + if (isoDateRegex.test(value)) { + return new Date(value).toISOString(); + } + } + return value; + }); + + // PostgreSQL 타입 캐스팅을 위한 placeholder 생성 + const placeholders = columns.map((col, index) => { + // 날짜/시간 관련 컬럼명 패턴 체크 + if (col.toLowerCase().includes('date') || + col.toLowerCase().includes('time') || + col.toLowerCase().includes('created') || + col.toLowerCase().includes('updated') || + col.toLowerCase().includes('reg')) { + return `$${index + 1}::timestamp`; + } + return `$${index + 1}`; + }).join(', '); + + // Primary Key 컬럼 추정 (일반적으로 id 또는 첫 번째 컬럼) + const primaryKeyColumn = columns.includes('id') ? 'id' : + columns.includes('user_id') ? 'user_id' : + columns[0]; + + // UPDATE SET 절 생성 (Primary Key 제외) + const updateColumns = columns.filter(col => col !== primaryKeyColumn); + const updateSet = updateColumns.map(col => `${col} = EXCLUDED.${col}`).join(', '); + + let query: string; + if (updateSet) { + // UPSERT: 중복 시 업데이트 + query = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders}) + ON CONFLICT (${primaryKeyColumn}) DO UPDATE SET ${updateSet}`; + } else { + // Primary Key만 있는 경우 중복 시 무시 + query = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders}) + ON CONFLICT (${primaryKeyColumn}) DO NOTHING`; + } + + await prisma.$executeRawUnsafe(query, ...values); + successCount++; + } catch (error) { + console.error(`레코드 UPSERT 실패:`, error); + failedCount++; + } + } + + console.log(`[BatchService] 내부 DB 데이터 삽입 완료: 성공 ${successCount}개, 실패 ${failedCount}개`); + return { successCount, failedCount }; + } else if (connectionType === 'external' && connectionId) { + // 외부 DB에 데이터 삽입 + const result = await BatchExternalDbService.insertDataToTable(connectionId, tableName, data); + if (result.success && result.data) { + console.log(`[BatchService] 외부 DB 데이터 삽입 완료: 성공 ${result.data.successCount}개, 실패 ${result.data.failedCount}개`); + return result.data; + } else { + console.error(`외부 DB 데이터 삽입 실패: ${result.message}`); + return { successCount: 0, failedCount: data.length }; + } + } else { + console.log(`[BatchService] 연결 정보 디버그:`, { connectionType, connectionId }); + throw new Error(`잘못된 연결 타입 또는 연결 ID: ${connectionType}, ${connectionId}`); + } + } catch (error) { + console.error(`테이블 데이터 삽입 오류 (${tableName}):`, error); + throw error; + } + } + + /** + * 배치 매핑 유효성 검사 + */ + private static async validateBatchMappings( + mappings: BatchMapping[] + ): Promise { + const errors: string[] = []; + const warnings: string[] = []; + + if (!mappings || mappings.length === 0) { + errors.push("최소 하나 이상의 매핑이 필요합니다."); + return { isValid: false, errors, warnings }; } - const executions = await prisma.batch_job_executions.findMany({ - where: whereCondition, - orderBy: { start_time: "desc" }, - // include 제거 - 관계가 정의되지 않음 + // n:1 매핑 검사 (여러 FROM이 같은 TO로 매핑되는 것 방지) + const toMappings = new Map(); + + mappings.forEach((mapping, index) => { + const toKey = `${mapping.to_connection_type}:${mapping.to_connection_id || 'internal'}:${mapping.to_table_name}:${mapping.to_column_name}`; + + if (toMappings.has(toKey)) { + errors.push( + `매핑 ${index + 1}: TO 컬럼 '${mapping.to_table_name}.${mapping.to_column_name}'에 중복 매핑이 있습니다. n:1 매핑은 허용되지 않습니다.` + ); + } else { + toMappings.set(toKey, index); + } }); - return executions.map((exec: any) => ({ - ...exec, - execution_status: exec.status, - started_at: exec.start_time, - completed_at: exec.end_time, - error_message: exec.exit_message, - })) as BatchExecution[]; - } - - /** - * 배치 모니터링 정보 조회 - */ - static async getBatchMonitoring(): Promise { - const totalJobs = await prisma.batch_jobs.count(); - const activeJobs = await prisma.batch_jobs.count({ - where: { is_active: "Y" }, + // 1:n 매핑 경고 (같은 FROM에서 여러 TO로 매핑) + const fromMappings = new Map(); + + mappings.forEach((mapping, index) => { + const fromKey = `${mapping.from_connection_type}:${mapping.from_connection_id || 'internal'}:${mapping.from_table_name}:${mapping.from_column_name}`; + + if (!fromMappings.has(fromKey)) { + fromMappings.set(fromKey, []); + } + fromMappings.get(fromKey)!.push(index); }); - const runningExecutions = await prisma.batch_job_executions.count({ - where: { status: "RUNNING" }, - }); - - const recentExecutions = await prisma.batch_job_executions.findMany({ - where: { - created_at: { - gte: new Date(Date.now() - 24 * 60 * 60 * 1000), // 최근 24시간 - }, - }, - orderBy: { start_time: "desc" }, - take: 10, - // include 제거 - 관계가 정의되지 않음 - }); - - const successCount = await prisma.batch_job_executions.count({ - where: { - status: "SUCCESS", - start_time: { - gte: new Date(Date.now() - 24 * 60 * 60 * 1000), - }, - }, - }); - - const failedCount = await prisma.batch_job_executions.count({ - where: { - status: "FAILED", - start_time: { - gte: new Date(Date.now() - 24 * 60 * 60 * 1000), - }, - }, + fromMappings.forEach((indices, fromKey) => { + if (indices.length > 1) { + const [, , tableName, columnName] = fromKey.split(':'); + warnings.push( + `FROM 컬럼 '${tableName}.${columnName}'에서 ${indices.length}개의 TO 컬럼으로 매핑됩니다. (1:n 매핑)` + ); + } }); return { - total_jobs: totalJobs, - active_jobs: activeJobs, - running_jobs: runningExecutions, - failed_jobs_today: failedCount, - successful_jobs_today: successCount, - recent_executions: recentExecutions.map((exec: any) => ({ - ...exec, - execution_status: exec.status, - started_at: exec.start_time, - completed_at: exec.end_time, - error_message: exec.exit_message, - })) as BatchExecution[], + isValid: errors.length === 0, + errors, + warnings, }; } } diff --git a/backend-node/src/services/enhancedDataflowControlService.ts b/backend-node/src/services/enhancedDataflowControlService.ts index a74a65be..6aae33da 100644 --- a/backend-node/src/services/enhancedDataflowControlService.ts +++ b/backend-node/src/services/enhancedDataflowControlService.ts @@ -12,21 +12,14 @@ import { MultiConnectionQueryService } from "./multiConnectionQueryService"; import { logger } from "../utils/logger"; export interface EnhancedControlAction extends ControlAction { - // 🆕 커넥션 정보 추가 - fromConnection?: { - connectionId?: number; - connectionName?: string; - dbType?: string; - }; - toConnection?: { - connectionId?: number; - connectionName?: string; - dbType?: string; - }; + // 🆕 기본 ControlAction 속성들 (상속됨) + id?: number; + actionType?: string; + fromTable: string; - // 🆕 명시적 테이블 정보 - fromTable?: string; - targetTable: string; + // 🆕 추가 속성들 + conditions?: ControlCondition[]; + fieldMappings?: any[]; // 🆕 UPDATE 액션 관련 필드 updateConditions?: UpdateCondition[]; @@ -172,13 +165,20 @@ export class EnhancedDataflowControlService extends DataflowControlService { const enhancedAction = action as EnhancedControlAction; let actionResult: any; + // 커넥션 ID 추출 + const sourceConnectionId = enhancedAction.fromConnection?.connectionId || enhancedAction.fromConnection?.id || 0; + const targetConnectionId = enhancedAction.toConnection?.connectionId || enhancedAction.toConnection?.id || 0; + switch (enhancedAction.actionType) { case "insert": actionResult = await this.executeMultiConnectionInsert( enhancedAction, sourceData, + enhancedAction.fromTable, + enhancedAction.targetTable, sourceConnectionId, - targetConnectionId + targetConnectionId, + null ); break; @@ -186,8 +186,11 @@ export class EnhancedDataflowControlService extends DataflowControlService { actionResult = await this.executeMultiConnectionUpdate( enhancedAction, sourceData, + enhancedAction.fromTable, + enhancedAction.targetTable, sourceConnectionId, - targetConnectionId + targetConnectionId, + null ); break; @@ -195,8 +198,11 @@ export class EnhancedDataflowControlService extends DataflowControlService { actionResult = await this.executeMultiConnectionDelete( enhancedAction, sourceData, + enhancedAction.fromTable, + enhancedAction.targetTable, sourceConnectionId, - targetConnectionId + targetConnectionId, + null ); break; @@ -241,20 +247,21 @@ export class EnhancedDataflowControlService extends DataflowControlService { /** * 🆕 다중 커넥션 INSERT 실행 */ - private async executeMultiConnectionInsert( + async executeMultiConnectionInsert( action: EnhancedControlAction, sourceData: Record, - sourceConnectionId?: number, - targetConnectionId?: number + sourceTable: string, + targetTable: string, + fromConnectionId: number, + toConnectionId: number, + multiConnService: any ): Promise { try { - logger.info(`다중 커넥션 INSERT 실행: action=${action.id}`); + logger.info(`다중 커넥션 INSERT 실행: action=${action.action}`); // 커넥션 ID 결정 - const fromConnId = - sourceConnectionId || action.fromConnection?.connectionId || 0; - const toConnId = - targetConnectionId || action.toConnection?.connectionId || 0; + const fromConnId = fromConnectionId || action.fromConnection?.connectionId || 0; + const toConnId = toConnectionId || action.toConnection?.connectionId || 0; // FROM 테이블에서 소스 데이터 조회 (조건이 있는 경우) let fromData = sourceData; @@ -287,7 +294,7 @@ export class EnhancedDataflowControlService extends DataflowControlService { // 필드 매핑 적용 const mappedData = this.applyFieldMappings( - action.fieldMappings, + action.fieldMappings || [], fromData ); @@ -310,20 +317,21 @@ export class EnhancedDataflowControlService extends DataflowControlService { /** * 🆕 다중 커넥션 UPDATE 실행 */ - private async executeMultiConnectionUpdate( + async executeMultiConnectionUpdate( action: EnhancedControlAction, sourceData: Record, - sourceConnectionId?: number, - targetConnectionId?: number + sourceTable: string, + targetTable: string, + fromConnectionId: number, + toConnectionId: number, + multiConnService: any ): Promise { try { - logger.info(`다중 커넥션 UPDATE 실행: action=${action.id}`); + logger.info(`다중 커넥션 UPDATE 실행: action=${action.action}`); // 커넥션 ID 결정 - const fromConnId = - sourceConnectionId || action.fromConnection?.connectionId || 0; - const toConnId = - targetConnectionId || action.toConnection?.connectionId || 0; + const fromConnId = fromConnectionId || action.fromConnection?.connectionId || 0; + const toConnId = toConnectionId || action.toConnection?.connectionId || 0; // UPDATE 조건 확인 if (!action.updateConditions || action.updateConditions.length === 0) { @@ -382,20 +390,23 @@ export class EnhancedDataflowControlService extends DataflowControlService { /** * 🆕 다중 커넥션 DELETE 실행 */ - private async executeMultiConnectionDelete( + async executeMultiConnectionDelete( action: EnhancedControlAction, sourceData: Record, - sourceConnectionId?: number, - targetConnectionId?: number + sourceTable: string, + targetTable: string, + fromConnectionId: number, + toConnectionId: number, + multiConnService: any ): Promise { try { - logger.info(`다중 커넥션 DELETE 실행: action=${action.id}`); + logger.info(`다중 커넥션 DELETE 실행: action=${action.action}`); // 커넥션 ID 결정 const fromConnId = - sourceConnectionId || action.fromConnection?.connectionId || 0; + fromConnectionId || action.fromConnection?.connectionId || 0; const toConnId = - targetConnectionId || action.toConnection?.connectionId || 0; + toConnectionId || action.toConnection?.connectionId || 0; // DELETE 조건 확인 if (!action.deleteConditions || action.deleteConditions.length === 0) { diff --git a/backend-node/src/services/externalDbConnectionService.ts b/backend-node/src/services/externalDbConnectionService.ts index 5276dfab..96ece2d9 100644 --- a/backend-node/src/services/externalDbConnectionService.ts +++ b/backend-node/src/services/externalDbConnectionService.ts @@ -1,7 +1,7 @@ // 외부 DB 연결 서비스 // 작성일: 2024-12-17 -import { PrismaClient } from "@prisma/client"; +import prisma from "../config/database"; import { ExternalDbConnection, ExternalDbConnectionFilter, @@ -11,9 +11,6 @@ import { import { PasswordEncryption } from "../utils/passwordEncryption"; import { DatabaseConnectorFactory } from "../database/DatabaseConnectorFactory"; -// 🔧 Prisma 클라이언트 중복 생성 방지 - 기존 인스턴스 재사용 -import prisma = require("../config/database"); - export class ExternalDbConnectionService { /** * 외부 DB 연결 목록 조회 @@ -91,23 +88,26 @@ export class ExternalDbConnectionService { try { // 기본 연결 목록 조회 const connectionsResult = await this.getConnections(filter); - + if (!connectionsResult.success || !connectionsResult.data) { return { success: false, - message: "연결 목록 조회에 실패했습니다.", + message: "연결 목록 조회에 실패했습니다." }; } // DB 타입 카테고리 정보 조회 const categories = await prisma.db_type_categories.findMany({ where: { is_active: true }, - orderBy: [{ sort_order: "asc" }, { display_name: "asc" }], + orderBy: [ + { sort_order: 'asc' }, + { display_name: 'asc' } + ] }); // DB 타입별로 그룹화 const groupedConnections: Record = {}; - + // 카테고리 정보를 포함한 그룹 초기화 categories.forEach((category: any) => { groupedConnections[category.type_code] = { @@ -116,36 +116,36 @@ export class ExternalDbConnectionService { display_name: category.display_name, icon: category.icon, color: category.color, - sort_order: category.sort_order, + sort_order: category.sort_order }, - connections: [], + connections: [] }; }); // 연결을 해당 타입 그룹에 배치 - connectionsResult.data.forEach((connection) => { + connectionsResult.data.forEach(connection => { if (groupedConnections[connection.db_type]) { groupedConnections[connection.db_type].connections.push(connection); } else { // 카테고리에 없는 DB 타입인 경우 기타 그룹에 추가 - if (!groupedConnections["other"]) { - groupedConnections["other"] = { + if (!groupedConnections['other']) { + groupedConnections['other'] = { category: { - type_code: "other", - display_name: "기타", - icon: "database", - color: "#6B7280", - sort_order: 999, + type_code: 'other', + display_name: '기타', + icon: 'database', + color: '#6B7280', + sort_order: 999 }, - connections: [], + connections: [] }; } - groupedConnections["other"].connections.push(connection); + groupedConnections['other'].connections.push(connection); } }); // 연결이 없는 빈 그룹 제거 - Object.keys(groupedConnections).forEach((key) => { + Object.keys(groupedConnections).forEach(key => { if (groupedConnections[key].connections.length === 0) { delete groupedConnections[key]; } @@ -154,14 +154,14 @@ export class ExternalDbConnectionService { return { success: true, data: groupedConnections, - message: `DB 타입별로 그룹화된 연결 목록을 조회했습니다.`, + message: `DB 타입별로 그룹화된 연결 목록을 조회했습니다.` }; } catch (error) { console.error("그룹화된 연결 목록 조회 실패:", error); return { success: false, message: "그룹화된 연결 목록 조회 중 오류가 발생했습니다.", - error: error instanceof Error ? error.message : "알 수 없는 오류", + error: error instanceof Error ? error.message : "알 수 없는 오류" }; } } diff --git a/backend-node/src/types/batchExecutionLogTypes.ts b/backend-node/src/types/batchExecutionLogTypes.ts new file mode 100644 index 00000000..d966de7c --- /dev/null +++ b/backend-node/src/types/batchExecutionLogTypes.ts @@ -0,0 +1,64 @@ +// 배치 실행 로그 타입 정의 +// 작성일: 2024-12-24 + +export interface BatchExecutionLog { + id?: number; + batch_config_id: number; + execution_status: 'RUNNING' | 'SUCCESS' | 'FAILED' | 'CANCELLED'; + start_time: Date; + end_time?: Date | null; + duration_ms?: number | null; + total_records?: number | null; + success_records?: number | null; + failed_records?: number | null; + error_message?: string | null; + error_details?: string | null; + server_name?: string | null; + process_id?: string | null; +} + +export interface CreateBatchExecutionLogRequest { + batch_config_id: number; + execution_status: 'RUNNING' | 'SUCCESS' | 'FAILED' | 'CANCELLED'; + start_time?: Date; + end_time?: Date | null; + duration_ms?: number | null; + total_records?: number | null; + success_records?: number | null; + failed_records?: number | null; + error_message?: string | null; + error_details?: string | null; + server_name?: string | null; + process_id?: string | null; +} + +export interface UpdateBatchExecutionLogRequest { + execution_status?: 'RUNNING' | 'SUCCESS' | 'FAILED' | 'CANCELLED'; + end_time?: Date | null; + duration_ms?: number | null; + total_records?: number | null; + success_records?: number | null; + failed_records?: number | null; + error_message?: string | null; + error_details?: string | null; +} + +export interface BatchExecutionLogFilter { + batch_config_id?: number; + execution_status?: string; + start_date?: Date; + end_date?: Date; + page?: number; + limit?: number; +} + +export interface BatchExecutionLogWithConfig extends BatchExecutionLog { + batch_config?: { + id: number; + batch_name: string; + description?: string | null; + cron_schedule: string; + is_active?: string | null; + }; +} + diff --git a/backend-node/src/types/batchTypes.ts b/backend-node/src/types/batchTypes.ts new file mode 100644 index 00000000..e2a676ef --- /dev/null +++ b/backend-node/src/types/batchTypes.ts @@ -0,0 +1,139 @@ +// 배치관리 타입 정의 +// 작성일: 2024-12-24 + +// 배치 타입 정의 +export type BatchType = 'db-to-db' | 'db-to-restapi' | 'restapi-to-db' | 'restapi-to-restapi'; + +export interface BatchTypeOption { + value: BatchType; + label: string; + description: string; +} + +export interface BatchConfig { + id?: number; + batch_name: string; + description?: string; + cron_schedule: string; + is_active?: string; + company_code?: string; + created_date?: Date; + created_by?: string; + updated_date?: Date; + updated_by?: string; + batch_mappings?: BatchMapping[]; +} + +export interface BatchMapping { + id?: number; + batch_config_id?: number; + + // FROM 정보 + from_connection_type: 'internal' | 'external' | 'restapi'; + from_connection_id?: number; + from_table_name: string; // DB: 테이블명, REST API: 엔드포인트 + from_column_name: string; // DB: 컬럼명, REST API: JSON 필드명 + from_column_type?: string; + from_api_method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; // REST API 전용 + from_api_url?: string; // REST API 서버 URL + from_api_key?: string; // REST API 키 + + // TO 정보 + to_connection_type: 'internal' | 'external' | 'restapi'; + to_connection_id?: number; + to_table_name: string; // DB: 테이블명, REST API: 엔드포인트 + to_column_name: string; // DB: 컬럼명, REST API: JSON 필드명 + to_column_type?: string; + to_api_method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; // REST API 전용 + to_api_url?: string; // REST API 서버 URL + to_api_key?: string; // REST API 키 + to_api_body?: string; // Request Body 템플릿 (DB → REST API 배치용) + + mapping_order?: number; + created_date?: Date; + created_by?: string; +} + +export interface BatchConfigFilter { + page?: number; + limit?: number; + batch_name?: string; + is_active?: string; + company_code?: string; + search?: string; +} + +export interface ConnectionInfo { + type: 'internal' | 'external'; + id?: number; + name: string; + db_type?: string; +} + +export interface TableInfo { + table_name: string; + columns: ColumnInfo[]; + description?: string | null; +} + +export interface ColumnInfo { + column_name: string; + data_type: string; + is_nullable?: string; + column_default?: string | null; +} + +export interface BatchMappingRequest { + from_connection_type: 'internal' | 'external' | 'restapi'; + from_connection_id?: number; + from_table_name: string; + from_column_name: string; + from_column_type?: string; + from_api_url?: string; + from_api_key?: string; + from_api_method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; + to_connection_type: 'internal' | 'external' | 'restapi'; + to_connection_id?: number; + to_table_name: string; + to_column_name: string; + to_column_type?: string; + to_api_url?: string; + to_api_key?: string; + to_api_method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; + to_api_body?: string; // Request Body 템플릿 (DB → REST API 배치용) + mapping_order?: number; +} + +export interface CreateBatchConfigRequest { + batchName: string; + description?: string; + cronSchedule: string; + mappings: BatchMappingRequest[]; +} + +export interface UpdateBatchConfigRequest { + batchName?: string; + description?: string; + cronSchedule?: string; + mappings?: BatchMappingRequest[]; + isActive?: string; +} + +export interface BatchValidationResult { + isValid: boolean; + errors: string[]; + warnings?: string[]; +} + +export interface ApiResponse { + success: boolean; + data?: T; + message?: string; + error?: string; + pagination?: { + page: number; + limit: number; + total: number; + totalPages: number; + }; +} diff --git a/backend-node/src/types/oracledb.d.ts b/backend-node/src/types/oracledb.d.ts new file mode 100644 index 00000000..818b6a6f --- /dev/null +++ b/backend-node/src/types/oracledb.d.ts @@ -0,0 +1,18 @@ +declare module 'oracledb' { + export interface Connection { + execute(sql: string, bindParams?: any, options?: any): Promise; + close(): Promise; + } + + export interface ConnectionConfig { + user: string; + password: string; + connectString: string; + } + + export function getConnection(config: ConnectionConfig): Promise; + export function createPool(config: any): Promise; + export function getPool(): any; + export function close(): Promise; +} + diff --git a/backend-node/tsconfig.json b/backend-node/tsconfig.json index 95e2dd18..848784d8 100644 --- a/backend-node/tsconfig.json +++ b/backend-node/tsconfig.json @@ -33,6 +33,6 @@ "@/validators/*": ["src/validators/*"] } }, - "include": ["src/**/*"], + "include": ["src/**/*", "src/types/**/*.d.ts"], "exclude": ["node_modules", "dist", "**/*.test.ts"] } diff --git a/docker/dev/frontend.Dockerfile b/docker/dev/frontend.Dockerfile index 390e8324..fdad92f6 100644 --- a/docker/dev/frontend.Dockerfile +++ b/docker/dev/frontend.Dockerfile @@ -16,5 +16,5 @@ COPY . . # 포트 노출 EXPOSE 3000 -# 개발 서버 시작 -CMD ["npm", "run", "dev"] \ No newline at end of file +# 개발 서버 시작 (Docker에서는 포트 3000 사용) +CMD ["npm", "run", "dev", "--", "-p", "3000"] \ No newline at end of file diff --git a/docs/batch.html b/docs/batch.html new file mode 100644 index 00000000..bc70b3cd --- /dev/null +++ b/docs/batch.html @@ -0,0 +1,585 @@ + + + + + + 배치관리 매핑 시스템 + + + +
+
+ 배치관리 매핑 시스템 +
+ +
+
+ + +
+
+ + +
+
+ +
+
+
FROM (원본 데이터베이스)
+
+
+ 1단계: 컨넥션을 선택하세요 → 2단계: 테이블을 선택하세요 → 3단계: 컬럼을 클릭해서 매핑하세요 +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+
+ +
+
TO (대상 데이터베이스)
+
+
+ FROM에서 컬럼을 선택한 후, 여기서 대상 컬럼을 클릭하면 매핑됩니다 +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+
+
+ + + + +
+ + + + \ No newline at end of file diff --git a/frontend/app/(main)/admin/batch-management-new/page.tsx b/frontend/app/(main)/admin/batch-management-new/page.tsx new file mode 100644 index 00000000..53a840ff --- /dev/null +++ b/frontend/app/(main)/admin/batch-management-new/page.tsx @@ -0,0 +1,1228 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Trash2, Plus, ArrowRight, Save, RefreshCw, Globe, Database, Eye } from "lucide-react"; +import { toast } from "sonner"; +import { BatchManagementAPI } from "@/lib/api/batchManagement"; + +// 타입 정의 +type BatchType = 'db-to-restapi' | 'restapi-to-db' | 'restapi-to-restapi'; + +interface BatchTypeOption { + value: BatchType; + label: string; + description: string; +} + +interface BatchConnectionInfo { + id: number; + name: string; + type: string; +} + +interface BatchColumnInfo { + column_name: string; + data_type: string; + is_nullable: string; +} + +export default function BatchManagementNewPage() { + const router = useRouter(); + + // 기본 상태 + const [batchName, setBatchName] = useState(""); + const [cronSchedule, setCronSchedule] = useState("0 12 * * *"); + const [description, setDescription] = useState(""); + + // 연결 정보 + const [connections, setConnections] = useState([]); + const [toConnection, setToConnection] = useState(null); + const [toTables, setToTables] = useState([]); + const [toTable, setToTable] = useState(""); + const [toColumns, setToColumns] = useState([]); + + // REST API 설정 (REST API → DB용) + const [fromApiUrl, setFromApiUrl] = useState(""); + const [fromApiKey, setFromApiKey] = useState(""); + const [fromEndpoint, setFromEndpoint] = useState(""); + const [fromApiMethod, setFromApiMethod] = useState<'GET'>('GET'); // GET만 지원 + + // DB → REST API용 상태 + const [fromConnection, setFromConnection] = useState(null); + const [fromTables, setFromTables] = useState([]); + const [fromTable, setFromTable] = useState(""); + const [fromColumns, setFromColumns] = useState([]); + const [selectedColumns, setSelectedColumns] = useState([]); // 선택된 컬럼들 + const [dbToApiFieldMapping, setDbToApiFieldMapping] = useState>({}); // DB 컬럼 → API 필드 매핑 + + // REST API 대상 설정 (DB → REST API용) + const [toApiUrl, setToApiUrl] = useState(""); + const [toApiKey, setToApiKey] = useState(""); + const [toEndpoint, setToEndpoint] = useState(""); + const [toApiMethod, setToApiMethod] = useState<'POST' | 'PUT' | 'DELETE'>('POST'); + const [toApiBody, setToApiBody] = useState(''); // Request Body 템플릿 + const [toApiFields, setToApiFields] = useState([]); // TO API 필드 목록 + const [urlPathColumn, setUrlPathColumn] = useState(""); // URL 경로에 사용할 컬럼 (PUT/DELETE용) + + // API 데이터 미리보기 + const [fromApiData, setFromApiData] = useState([]); + const [fromApiFields, setFromApiFields] = useState([]); + + // API 필드 → DB 컬럼 매핑 + const [apiFieldMappings, setApiFieldMappings] = useState>({}); + + // 배치 타입 상태 + const [batchType, setBatchType] = useState('restapi-to-db'); + + // 배치 타입 옵션 + const batchTypeOptions: BatchTypeOption[] = [ + { + value: 'restapi-to-db', + label: 'REST API → DB', + description: 'REST API에서 데이터베이스로 데이터 수집' + }, + { + value: 'db-to-restapi', + label: 'DB → REST API', + description: '데이터베이스에서 REST API로 데이터 전송' + } + ]; + + // 초기 데이터 로드 + useEffect(() => { + loadConnections(); + }, []); + + // 배치 타입 변경 시 상태 초기화 + useEffect(() => { + // 공통 초기화 + setApiFieldMappings({}); + + // REST API → DB 관련 초기화 + setToConnection(null); + setToTables([]); + setToTable(""); + setToColumns([]); + setFromApiUrl(""); + setFromApiKey(""); + setFromEndpoint(""); + setFromApiData([]); + setFromApiFields([]); + + // DB → REST API 관련 초기화 + setFromConnection(null); + setFromTables([]); + setFromTable(""); + setFromColumns([]); + setSelectedColumns([]); + setDbToApiFieldMapping({}); + setToApiUrl(""); + setToApiKey(""); + setToEndpoint(""); + setToApiBody(""); + setToApiFields([]); + }, [batchType]); + + + // 연결 목록 로드 + const loadConnections = async () => { + try { + const result = await BatchManagementAPI.getAvailableConnections(); + setConnections(result || []); + } catch (error) { + console.error("연결 목록 로드 오류:", error); + toast.error("연결 목록을 불러오는데 실패했습니다."); + } + }; + + // TO 연결 변경 핸들러 + const handleToConnectionChange = async (connectionValue: string) => { + let connection: BatchConnectionInfo | null = null; + + if (connectionValue === 'internal') { + // 내부 데이터베이스 선택 + connection = connections.find(conn => conn.type === 'internal') || null; + } else { + // 외부 데이터베이스 선택 + const connectionId = parseInt(connectionValue); + connection = connections.find(conn => conn.id === connectionId) || null; + } + + setToConnection(connection); + setToTable(""); + setToColumns([]); + + if (connection) { + try { + const connectionType = connection.type === 'internal' ? 'internal' : 'external'; + const result = await BatchManagementAPI.getTablesFromConnection(connectionType, connection.id); + const tableNames = Array.isArray(result) + ? result.map((table: any) => typeof table === 'string' ? table : table.table_name || String(table)) + : []; + setToTables(tableNames); + } catch (error) { + console.error("테이블 목록 로드 오류:", error); + toast.error("테이블 목록을 불러오는데 실패했습니다."); + } + } + }; + + // TO 테이블 변경 핸들러 + const handleToTableChange = async (tableName: string) => { + console.log("🔍 테이블 변경:", { tableName, toConnection }); + setToTable(tableName); + setToColumns([]); + + if (toConnection && tableName) { + try { + const connectionType = toConnection.type === 'internal' ? 'internal' : 'external'; + console.log("🔍 컬럼 조회 시작:", { connectionType, connectionId: toConnection.id, tableName }); + + const result = await BatchManagementAPI.getTableColumns(connectionType, tableName, toConnection.id); + console.log("🔍 컬럼 조회 결과:", result); + + if (result && result.length > 0) { + setToColumns(result); + console.log("✅ 컬럼 설정 완료:", result.length, "개"); + } else { + setToColumns([]); + console.log("⚠️ 컬럼이 없음"); + } + } catch (error) { + console.error("❌ 컬럼 목록 로드 오류:", error); + toast.error("컬럼 목록을 불러오는데 실패했습니다."); + setToColumns([]); + } + } + }; + + // FROM 연결 변경 핸들러 (DB → REST API용) + const handleFromConnectionChange = async (connectionValue: string) => { + let connection: BatchConnectionInfo | null = null; + if (connectionValue === 'internal') { + connection = connections.find(conn => conn.type === 'internal') || null; + } else { + const connectionId = parseInt(connectionValue); + connection = connections.find(conn => conn.id === connectionId) || null; + } + setFromConnection(connection); + setFromTable(""); + setFromColumns([]); + + if (connection) { + try { + const connectionType = connection.type === 'internal' ? 'internal' : 'external'; + const result = await BatchManagementAPI.getTablesFromConnection(connectionType, connection.id); + const tableNames = Array.isArray(result) + ? result.map((table: any) => typeof table === 'string' ? table : table.table_name || String(table)) + : []; + setFromTables(tableNames); + } catch (error) { + console.error("테이블 목록 로드 오류:", error); + toast.error("테이블 목록을 불러오는데 실패했습니다."); + } + } + }; + + // FROM 테이블 변경 핸들러 (DB → REST API용) + const handleFromTableChange = async (tableName: string) => { + console.log("🔍 FROM 테이블 변경:", { tableName, fromConnection }); + setFromTable(tableName); + setFromColumns([]); + setSelectedColumns([]); // 선택된 컬럼도 초기화 + setDbToApiFieldMapping({}); // 매핑도 초기화 + + if (fromConnection && tableName) { + try { + const connectionType = fromConnection.type === 'internal' ? 'internal' : 'external'; + console.log("🔍 FROM 컬럼 조회 시작:", { connectionType, connectionId: fromConnection.id, tableName }); + + const result = await BatchManagementAPI.getTableColumns(connectionType, tableName, fromConnection.id); + console.log("🔍 FROM 컬럼 조회 결과:", result); + + if (result && result.length > 0) { + setFromColumns(result); + console.log("✅ FROM 컬럼 설정 완료:", result.length, "개"); + } else { + setFromColumns([]); + console.log("⚠️ FROM 컬럼이 없음"); + } + } catch (error) { + console.error("❌ FROM 컬럼 목록 로드 오류:", error); + toast.error("컬럼 목록을 불러오는데 실패했습니다."); + setFromColumns([]); + } + } + }; + + // TO API 미리보기 (DB → REST API용) + const previewToApiData = async () => { + if (!toApiUrl || !toApiKey || !toEndpoint) { + toast.error("API URL, API Key, 엔드포인트를 모두 입력해주세요."); + return; + } + + try { + console.log("🔍 TO API 미리보기 시작:", { toApiUrl, toApiKey, toEndpoint, toApiMethod }); + + const result = await BatchManagementAPI.previewRestApiData( + toApiUrl, + toApiKey, + toEndpoint, + 'GET' // 미리보기는 항상 GET으로 + ); + + console.log("🔍 TO API 미리보기 결과:", result); + + if (result.fields && result.fields.length > 0) { + setToApiFields(result.fields); + toast.success(`TO API 필드 ${result.fields.length}개를 조회했습니다.`); + } else { + setToApiFields([]); + toast.warning("TO API에서 필드를 찾을 수 없습니다."); + } + } catch (error) { + console.error("❌ TO API 미리보기 오류:", error); + toast.error("TO API 미리보기에 실패했습니다."); + setToApiFields([]); + } + }; + + // REST API 데이터 미리보기 + const previewRestApiData = async () => { + if (!fromApiUrl || !fromApiKey || !fromEndpoint) { + toast.error("API URL, API Key, 엔드포인트를 모두 입력해주세요."); + return; + } + + try { + console.log("REST API 데이터 미리보기 시작..."); + + const result = await BatchManagementAPI.previewRestApiData( + fromApiUrl, + fromApiKey, + fromEndpoint, + fromApiMethod + ); + + console.log("API 미리보기 결과:", result); + console.log("result.fields:", result.fields); + console.log("result.samples:", result.samples); + console.log("result.totalCount:", result.totalCount); + + if (result.fields && result.fields.length > 0) { + console.log("✅ 백엔드에서 fields 제공됨:", result.fields); + setFromApiFields(result.fields); + setFromApiData(result.samples); + + console.log("추출된 필드:", result.fields); + toast.success(`API 데이터 미리보기 완료! ${result.fields.length}개 필드, ${result.totalCount}개 레코드`); + } else if (result.samples && result.samples.length > 0) { + // 백엔드에서 fields를 제대로 보내지 않은 경우, 프론트엔드에서 직접 추출 + console.log("⚠️ 백엔드에서 fields가 없어서 프론트엔드에서 추출"); + const extractedFields = Object.keys(result.samples[0]); + console.log("프론트엔드에서 추출한 필드:", extractedFields); + + setFromApiFields(extractedFields); + setFromApiData(result.samples); + + toast.success(`API 데이터 미리보기 완료! ${extractedFields.length}개 필드, ${result.samples.length}개 레코드`); + } else { + console.log("❌ 데이터가 없음"); + setFromApiFields([]); + setFromApiData([]); + toast.warning("API에서 데이터를 가져올 수 없습니다."); + } + } catch (error) { + console.error("REST API 미리보기 오류:", error); + toast.error("API 데이터 미리보기에 실패했습니다."); + setFromApiFields([]); + setFromApiData([]); + } + }; + + // 배치 설정 저장 + const handleSave = async () => { + if (!batchName.trim()) { + toast.error("배치명을 입력해주세요."); + return; + } + + // 배치 타입별 검증 및 저장 + if (batchType === 'restapi-to-db') { + const mappedFields = Object.keys(apiFieldMappings).filter(field => apiFieldMappings[field]); + if (mappedFields.length === 0) { + toast.error("최소 하나의 API 필드를 DB 컬럼에 매핑해주세요."); + return; + } + + // API 필드 매핑을 배치 매핑 형태로 변환 + const apiMappings = mappedFields.map(apiField => ({ + from_connection_type: 'restapi' as const, + from_table_name: fromEndpoint, // API 엔드포인트 + from_column_name: apiField, // API 필드명 + from_api_url: fromApiUrl, + from_api_key: fromApiKey, + from_api_method: fromApiMethod, + to_connection_type: toConnection?.type === 'internal' ? 'internal' : 'external', + to_connection_id: toConnection?.type === 'internal' ? undefined : toConnection?.id, + to_table_name: toTable, + to_column_name: apiFieldMappings[apiField], // 매핑된 DB 컬럼 + mapping_type: 'direct' as const + })); + + console.log("REST API 배치 설정 저장:", { + batchName, + batchType, + cronSchedule, + description, + apiMappings + }); + + // 실제 API 호출 + try { + const result = await BatchManagementAPI.saveRestApiBatch({ + batchName, + batchType, + cronSchedule, + description, + apiMappings + }); + + if (result.success) { + toast.success(result.message || "REST API 배치 설정이 저장되었습니다."); + setTimeout(() => { + router.push('/admin/batchmng'); + }, 1000); + } else { + toast.error(result.message || "배치 저장에 실패했습니다."); + } + } catch (error) { + console.error("배치 저장 오류:", error); + toast.error("배치 저장 중 오류가 발생했습니다."); + } + return; + } else if (batchType === 'db-to-restapi') { + // DB → REST API 배치 검증 + if (!fromConnection || !fromTable || selectedColumns.length === 0) { + toast.error("소스 데이터베이스, 테이블, 컬럼을 선택해주세요."); + return; + } + + if (!toApiUrl || !toApiKey || !toEndpoint) { + toast.error("대상 API URL, API Key, 엔드포인트를 입력해주세요."); + return; + } + + if ((toApiMethod === 'POST' || toApiMethod === 'PUT') && !toApiBody) { + toast.error("POST/PUT 메서드의 경우 Request Body 템플릿을 입력해주세요."); + return; + } + + // DELETE의 경우 빈 Request Body라도 템플릿 로직을 위해 "{}" 설정 + let finalToApiBody = toApiBody; + if (toApiMethod === 'DELETE' && !finalToApiBody.trim()) { + finalToApiBody = '{}'; + } + + // DB → REST API 매핑 생성 (선택된 컬럼만) + const selectedColumnObjects = fromColumns.filter(column => selectedColumns.includes(column.column_name)); + const dbMappings = selectedColumnObjects.map((column, index) => ({ + from_connection_type: fromConnection.type === 'internal' ? 'internal' : 'external', + from_connection_id: fromConnection.type === 'internal' ? undefined : fromConnection.id, + from_table_name: fromTable, + from_column_name: column.column_name, + from_column_type: column.data_type, + to_connection_type: 'restapi' as const, + to_table_name: toEndpoint, // API 엔드포인트 + to_column_name: dbToApiFieldMapping[column.column_name] || column.column_name, // 매핑된 API 필드명 + to_api_url: toApiUrl, + to_api_key: toApiKey, + to_api_method: toApiMethod, + to_api_body: finalToApiBody, // Request Body 템플릿 + mapping_type: 'template' as const, + mapping_order: index + 1 + })); + + // URL 경로 파라미터 매핑 추가 (PUT/DELETE용) + if ((toApiMethod === 'PUT' || toApiMethod === 'DELETE') && urlPathColumn) { + const urlPathColumnObject = fromColumns.find(col => col.column_name === urlPathColumn); + if (urlPathColumnObject) { + dbMappings.push({ + from_connection_type: fromConnection.type === 'internal' ? 'internal' : 'external', + from_connection_id: fromConnection.type === 'internal' ? undefined : fromConnection.id, + from_table_name: fromTable, + from_column_name: urlPathColumn, + from_column_type: urlPathColumnObject.data_type, + to_connection_type: 'restapi' as const, + to_table_name: toEndpoint, + to_column_name: 'URL_PATH_PARAM', // 특별한 식별자 + to_api_url: toApiUrl, + to_api_key: toApiKey, + to_api_method: toApiMethod, + to_api_body: finalToApiBody, + mapping_type: 'url_path' as const, + mapping_order: 999 // 마지막 순서 + }); + } + } + + console.log("DB → REST API 배치 설정 저장:", { + batchName, + batchType, + cronSchedule, + description, + dbMappings + }); + + // 실제 API 호출 (기존 saveRestApiBatch 재사용) + try { + const result = await BatchManagementAPI.saveRestApiBatch({ + batchName, + batchType, + cronSchedule, + description, + apiMappings: dbMappings + }); + + if (result.success) { + toast.success(result.message || "DB → REST API 배치 설정이 저장되었습니다."); + setTimeout(() => { + router.push('/admin/batchmng'); + }, 1000); + } else { + toast.error(result.message || "배치 저장에 실패했습니다."); + } + } catch (error) { + console.error("배치 저장 오류:", error); + toast.error("배치 저장 중 오류가 발생했습니다."); + } + return; + } + + toast.error("지원하지 않는 배치 타입입니다."); + }; + + return ( +
+
+

고급 배치 생성

+
+ + +
+
+ + {/* 기본 정보 */} + + + 기본 정보 + + + {/* 배치 타입 선택 */} +
+ +
+ {batchTypeOptions.map((option) => ( +
setBatchType(option.value)} + > +
+ {option.value === 'restapi-to-db' ? ( + + ) : ( + + )} +
+
{option.label}
+
{option.description}
+
+
+
+ ))} +
+
+ +
+
+ + setBatchName(e.target.value)} + placeholder="배치명을 입력하세요" + /> +
+
+ + setCronSchedule(e.target.value)} + placeholder="0 12 * * *" + /> +
+
+ +
+ +