diff --git a/backend/.eslintrc.cjs b/backend/.eslintrc.cjs new file mode 100644 index 0000000..69c60c3 --- /dev/null +++ b/backend/.eslintrc.cjs @@ -0,0 +1,21 @@ +module.exports = { + root: true, + env: { node: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + ], + ignorePatterns: ['dist', '.eslintrc.cjs'], + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module', + }, + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], + 'no-console': 'off', + 'prefer-const': 'warn', + '@typescript-eslint/no-require-imports': 'off', + }, +}; diff --git a/backend/package-lock.json b/backend/package-lock.json index ea1e8b9..d1b9ef2 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -32,6 +32,9 @@ "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.6", "@types/uuid": "^9.0.7", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint": "^8.57.1", "http-proxy-middleware": "^3.0.5", "nodemon": "^3.0.2", "ts-node": "^10.9.2", @@ -95,6 +98,157 @@ "node": ">=12" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/eslintrc/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -149,6 +303,44 @@ "node-pre-gyp": "bin/node-pre-gyp" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@scarf/scarf": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", @@ -335,6 +527,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/semver": { + "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" + }, "node_modules/@types/send": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.0.tgz", @@ -393,6 +592,337 @@ "dev": true, "license": "MIT" }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", + "dev": true, + "license": "ISC" + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -413,9 +943,9 @@ } }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "bin": { @@ -425,6 +955,16 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/acorn-walk": { "version": "8.3.4", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", @@ -473,6 +1013,23 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -482,6 +1039,22 @@ "node": ">=8" } }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -535,6 +1108,16 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -665,6 +1248,56 @@ "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", "license": "MIT" }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -699,6 +1332,26 @@ "node": ">=10" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/color-support": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", @@ -785,6 +1438,21 @@ "dev": true, "license": "MIT" }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -794,6 +1462,13 @@ "ms": "2.0.0" } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -838,6 +1513,19 @@ "node": ">=0.3.1" } }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -942,6 +1630,198 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -1028,6 +1908,67 @@ "express": ">= 4.11" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -1059,6 +2000,45 @@ "node": ">= 0.8" } }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, "node_modules/follow-redirects": { "version": "1.15.11", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", @@ -1244,6 +2224,43 @@ "node": ">= 6" } }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -1256,6 +2273,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -1427,6 +2451,16 @@ "node": ">=0.10.0" } }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", @@ -1434,6 +2468,33 @@ "dev": true, "license": "ISC" }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -1515,6 +2576,16 @@ "node": ">=0.12.0" } }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-plain-object": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", @@ -1525,6 +2596,13 @@ "node": ">=0.10.0" } }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -1537,6 +2615,27 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -1586,6 +2685,46 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", @@ -1636,6 +2775,13 @@ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", "license": "MIT" }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.mergewith": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", @@ -1706,6 +2852,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -1826,6 +2982,13 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -2002,6 +3165,69 @@ "license": "MIT", "peer": true }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -2011,6 +3237,16 @@ "node": ">= 0.8" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -2020,12 +3256,32 @@ "node": ">=0.10.0" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-to-regexp": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/pg": { "version": "8.16.3", "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", @@ -2167,6 +3423,16 @@ "node": ">=0.10.0" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -2187,6 +3453,16 @@ "dev": true, "license": "MIT" }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", @@ -2202,6 +3478,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -2260,6 +3557,27 @@ "dev": true, "license": "MIT" }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -2276,6 +3594,30 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -2303,9 +3645,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -2380,6 +3722,29 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -2471,6 +3836,16 @@ "node": ">=10" } }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -2524,6 +3899,19 @@ "node": ">=8" } }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -2631,6 +4019,13 @@ "node": ">=10" } }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -2669,6 +4064,19 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT" }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -2713,6 +4121,32 @@ } } }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -2763,6 +4197,16 @@ "node": ">= 0.8" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -2832,6 +4276,22 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/wide-align": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", @@ -2841,6 +4301,16 @@ "string-width": "^1.0.2 || 2 || 3 || 4" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -2881,6 +4351,19 @@ "node": ">=6" } }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/z-schema": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", diff --git a/backend/package.json b/backend/package.json index b5912df..f666b28 100644 --- a/backend/package.json +++ b/backend/package.json @@ -42,6 +42,9 @@ "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.6", "@types/uuid": "^9.0.7", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint": "^8.57.1", "http-proxy-middleware": "^3.0.5", "nodemon": "^3.0.2", "ts-node": "^10.9.2", diff --git a/backend/src/config/swagger.ts b/backend/src/config/swagger.ts index de972e3..8459c0d 100644 --- a/backend/src/config/swagger.ts +++ b/backend/src/config/swagger.ts @@ -1,5 +1,4 @@ import swaggerJsdoc from 'swagger-jsdoc'; -import { config } from './environment'; const options: swaggerJsdoc.Options = { definition: { diff --git a/backend/src/controllers/apiKeyController.ts b/backend/src/controllers/apiKeyController.ts index 6f3ce15..ce38731 100644 --- a/backend/src/controllers/apiKeyController.ts +++ b/backend/src/controllers/apiKeyController.ts @@ -1,7 +1,6 @@ import { Response } from 'express'; import { AuthRequest } from '../middleware/auth'; import { mainPool } from '../config/database'; -import { v4 as uuidv4 } from 'uuid'; import crypto from 'crypto'; export const getApiKeys = async (req: AuthRequest, res: Response) => { diff --git a/backend/src/controllers/authController.ts b/backend/src/controllers/authController.ts index 4ec148e..0d5a517 100644 --- a/backend/src/controllers/authController.ts +++ b/backend/src/controllers/authController.ts @@ -1,6 +1,6 @@ import { Request, Response } from 'express'; import bcrypt from 'bcrypt'; -import jwt, { SignOptions } from 'jsonwebtoken'; +import jwt from 'jsonwebtoken'; import { mainPool } from '../config/database'; import { config } from '../config/environment'; diff --git a/backend/src/controllers/databaseManagementController.ts b/backend/src/controllers/databaseManagementController.ts index 04fe50e..79d632a 100644 --- a/backend/src/controllers/databaseManagementController.ts +++ b/backend/src/controllers/databaseManagementController.ts @@ -265,6 +265,7 @@ export const testDatabaseConnection = async (req: AuthRequest, res: Response) => const dbType = dbResult.rows[0].type; if (dbType === 'aql') { + // eslint-disable-next-line @typescript-eslint/no-var-requires const { aqlExecutor } = require('../services/AqlExecutor'); const result = await aqlExecutor.testConnection(id, env); diff --git a/backend/src/controllers/dynamicApiController.ts b/backend/src/controllers/dynamicApiController.ts index 2949cb4..6321e13 100644 --- a/backend/src/controllers/dynamicApiController.ts +++ b/backend/src/controllers/dynamicApiController.ts @@ -179,6 +179,7 @@ export const executeDynamicEndpoint = async (req: ApiKeyRequest, res: Response) return res.status(500).json({ error: 'AQL configuration is incomplete' }); } + // eslint-disable-next-line @typescript-eslint/no-var-requires const { aqlExecutor } = require('../services/AqlExecutor'); result = await aqlExecutor.executeAqlQuery(endpoint.database_id, { method: aqlMethod, diff --git a/backend/src/controllers/endpointController.ts b/backend/src/controllers/endpointController.ts index d2a3dea..86b496b 100644 --- a/backend/src/controllers/endpointController.ts +++ b/backend/src/controllers/endpointController.ts @@ -1,7 +1,6 @@ import { Response } from 'express'; import { AuthRequest } from '../middleware/auth'; import { mainPool } from '../config/database'; -import { v4 as uuidv4 } from 'uuid'; import { ExportedEndpoint, ExportedScriptQuery, ScriptExecutionError, Environment } from '../types'; import { encryptEndpointData, decryptEndpointData } from '../services/endpointCrypto'; import { versionService } from '../services/VersionService'; @@ -325,6 +324,7 @@ export const testEndpoint = async (req: AuthRequest, res: Response) => { }); } + // eslint-disable-next-line @typescript-eslint/no-var-requires const { sqlExecutor } = require('../services/SqlExecutor'); const result = await sqlExecutor.executeQuery(database_id, processedQuery, parameters || [], environment); @@ -352,6 +352,7 @@ export const testEndpoint = async (req: AuthRequest, res: Response) => { }); } + // eslint-disable-next-line @typescript-eslint/no-var-requires const { scriptExecutor } = require('../services/ScriptExecutor'); const scriptResult = await scriptExecutor.execute(script_language, script_code, { databaseId: database_id, @@ -383,6 +384,7 @@ export const testEndpoint = async (req: AuthRequest, res: Response) => { }); } + // eslint-disable-next-line @typescript-eslint/no-var-requires const { aqlExecutor } = require('../services/AqlExecutor'); const result = await aqlExecutor.executeAqlQuery(database_id, { method: aql_method, @@ -512,8 +514,8 @@ export const exportEndpoint = async (req: AuthRequest, res: Response) => { const encrypted = encryptEndpointData(exportData); - const safeFileName = endpoint.name.replace(/[^a-zA-Z0-9_\-]/g, '_'); - const encodedFileName = encodeURIComponent(endpoint.name.replace(/[\/\\:*?"<>|]/g, '_')) + '.kabe'; + const safeFileName = endpoint.name.replace(/[^a-zA-Z0-9_-]/g, '_'); + const encodedFileName = encodeURIComponent(endpoint.name.replace(/[/\\:*?"<>|]/g, '_')) + '.kabe'; res.setHeader('Content-Type', 'application/octet-stream'); res.setHeader('Content-Disposition', `attachment; filename="${safeFileName}.kabe"; filename*=UTF-8''${encodedFileName}`); res.send(encrypted); diff --git a/backend/src/controllers/giteaController.ts b/backend/src/controllers/giteaController.ts new file mode 100644 index 0000000..af3e8bd --- /dev/null +++ b/backend/src/controllers/giteaController.ts @@ -0,0 +1,155 @@ +import { Response } from 'express'; +import { AuthRequest } from '../middleware/auth'; +import { settingsService } from '../services/SettingsService'; +import { giteaService } from '../services/GiteaService'; + +export const getSettings = async (req: AuthRequest, res: Response) => { + try { + const settings = await settingsService.getGiteaSettings(); + if (!settings) return res.json({ enabled: false, url: '', token: '', owner: '', repo: '', prod_branch: 'main' }); + // Mask token + const masked = settings.token + ? '****' + settings.token.slice(-4) + : ''; + res.json({ ...settings, token: masked }); + } catch (error: any) { + res.status(500).json({ error: error.message }); + } +}; + +export const updateSettings = async (req: AuthRequest, res: Response) => { + try { + const { enabled, url, token, owner, repo, prod_branch } = req.body; + + // If token is masked (starts with ****), keep existing + let actualToken = token; + if (token?.startsWith('****')) { + const existing = await settingsService.getGiteaSettings(); + actualToken = existing?.token || ''; + } + + await settingsService.setGiteaSettings({ + enabled: enabled ?? false, + url: url || '', + token: actualToken || '', + owner: owner || '', + repo: repo || '', + prod_branch: prod_branch || 'main', + }, req.user!.id); + + giteaService.invalidateCache(); + + res.json({ message: 'Settings saved' }); + } catch (error: any) { + res.status(500).json({ error: error.message }); + } +}; + +export const testGiteaConnection = async (req: AuthRequest, res: Response) => { + try { + const result = await giteaService.testConnection(); + res.json(result); + } catch (error: any) { + res.status(500).json({ success: false, message: error.message }); + } +}; + +export const syncAll = async (req: AuthRequest, res: Response) => { + try { + const result = await giteaService.initializeRepo(); + res.json(result); + } catch (error: any) { + res.status(500).json({ error: error.message }); + } +}; + +export const getSyncStatus = async (req: AuthRequest, res: Response) => { + try { + const status = await giteaService.getSyncStatus(); + res.json(status); + } catch (error: any) { + res.status(500).json({ error: error.message }); + } +}; + +export const listBranches = async (req: AuthRequest, res: Response) => { + try { + const branches = await giteaService.listBranches(); + res.json(branches); + } catch (error: any) { + res.status(500).json({ error: error.message }); + } +}; + +export const createBranch = async (req: AuthRequest, res: Response) => { + try { + const { name, from } = req.body; + if (!name) return res.status(400).json({ error: 'Branch name required' }); + await giteaService.createBranch(name, from); + res.json({ message: `Branch ${name} created` }); + } catch (error: any) { + res.status(500).json({ error: error.message }); + } +}; + +export const deleteBranch = async (req: AuthRequest, res: Response) => { + try { + const { name } = req.params; + await giteaService.deleteBranch(name); + res.json({ message: `Branch ${name} deleted` }); + } catch (error: any) { + res.status(500).json({ error: error.message }); + } +}; + +export const compareBranches = async (req: AuthRequest, res: Response) => { + try { + const { base, head } = req.query as { base: string; head: string }; + if (!base || !head) return res.status(400).json({ error: 'base and head required' }); + const diff = await giteaService.compareBranches(base, head); + res.json(diff); + } catch (error: any) { + res.status(500).json({ error: error.message }); + } +}; + +export const getCommitHistory = async (req: AuthRequest, res: Response) => { + try { + const { path, branch } = req.query as { path?: string; branch?: string }; + const commits = await giteaService.getCommitHistory(path, branch); + res.json(commits); + } catch (error: any) { + res.status(500).json({ error: error.message }); + } +}; + +export const getEndpointInfo = async (req: AuthRequest, res: Response) => { + try { + const { id } = req.params; + const info = await giteaService.getEndpointGiteaInfo(id); + res.json(info || { enabled: false }); + } catch (error: any) { + res.status(500).json({ error: error.message }); + } +}; + +export const createPR = async (req: AuthRequest, res: Response) => { + try { + const { title, head, base, body } = req.body; + if (!title || !head) return res.status(400).json({ error: 'title and head branch required' }); + const pr = await giteaService.createPR(title, head, base, body); + res.json(pr); + } catch (error: any) { + res.status(500).json({ error: error.message }); + } +}; + +export const mergePR = async (req: AuthRequest, res: Response) => { + try { + const { index } = req.params; + await giteaService.mergePR(parseInt(index)); + res.json({ message: 'PR merged' }); + } catch (error: any) { + res.status(500).json({ error: error.message }); + } +}; diff --git a/backend/src/migrations/012_add_app_settings.sql b/backend/src/migrations/012_add_app_settings.sql new file mode 100644 index 0000000..fc028d1 --- /dev/null +++ b/backend/src/migrations/012_add_app_settings.sql @@ -0,0 +1,10 @@ +-- Generic application settings (key-value store) +CREATE TABLE IF NOT EXISTS app_settings ( + key VARCHAR(255) PRIMARY KEY, + value JSONB NOT NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_by UUID REFERENCES users(id) ON DELETE SET NULL +); + +-- Track Gitea commit SHA in endpoint versions +ALTER TABLE endpoint_versions ADD COLUMN IF NOT EXISTS gitea_commit_sha VARCHAR(64); diff --git a/backend/src/routes/gitea.ts b/backend/src/routes/gitea.ts new file mode 100644 index 0000000..8948d73 --- /dev/null +++ b/backend/src/routes/gitea.ts @@ -0,0 +1,48 @@ +import express from 'express'; +import { authMiddleware } from '../middleware/auth'; +import { + getSettings, + updateSettings, + testGiteaConnection, + syncAll, + getSyncStatus, + listBranches, + createBranch, + deleteBranch, + compareBranches, + getCommitHistory, + getEndpointInfo, + createPR, + mergePR, +} from '../controllers/giteaController'; + +const router = express.Router(); + +router.use(authMiddleware); + +// Settings +router.get('/settings', getSettings); +router.put('/settings', updateSettings); +router.post('/test', testGiteaConnection); + +// Sync +router.post('/sync-all', syncAll); +router.get('/sync-status', getSyncStatus); + +// Branches +router.get('/branches', listBranches); +router.post('/branches', createBranch); +router.delete('/branches/:name', deleteBranch); + +// Diff & History +router.get('/compare', compareBranches); +router.get('/commits', getCommitHistory); + +// Endpoint-specific +router.get('/endpoints/:id/info', getEndpointInfo); + +// Pull Requests +router.post('/pulls', createPR); +router.post('/pulls/:index/merge', mergePR); + +export default router; diff --git a/backend/src/server.ts b/backend/src/server.ts index aa94e72..463c497 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -1,6 +1,6 @@ import express, { Express, Request, Response } from 'express'; import cors from 'cors'; -import helmet from 'helmet'; +// import helmet from 'helmet'; // import rateLimit from 'express-rate-limit'; import swaggerUi from 'swagger-ui-express'; import path from 'path'; @@ -21,6 +21,7 @@ import logsRoutes from './routes/logs'; import sqlInterfaceRoutes from './routes/sqlInterface'; import dynamicRoutes from './routes/dynamic'; import syncRoutes from './routes/sync'; +import giteaRoutes from './routes/gitea'; const app: Express = express(); @@ -95,6 +96,7 @@ app.use('/api/users', userRoutes); app.use('/api/logs', logsRoutes); app.use('/api/workbench', sqlInterfaceRoutes); app.use('/api/sync', syncRoutes); +app.use('/api/gitea', giteaRoutes); // Dynamic API routes (user-created endpoints) app.use('/api/v1', dynamicRoutes); @@ -107,8 +109,13 @@ if (config.nodeEnv === 'production') { app.use(express.static(frontendPath, { maxAge: '1d', etag: true, - setHeaders: (res) => { - res.setHeader('Cache-Control', 'public, max-age=86400'); + setHeaders: (res, filePath) => { + // index.html must not be cached — it references hashed assets + if (filePath.endsWith('.html')) { + res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); + } else { + res.setHeader('Cache-Control', 'public, max-age=86400'); + } } })); @@ -116,7 +123,7 @@ if (config.nodeEnv === 'production') { app.get('*', (req: Request, res: Response) => { if (!req.path.startsWith('/api/') && !req.path.startsWith('/api-docs') && req.path !== '/health') { const indexPath = path.join(frontendPath, 'index.html'); - console.log(`📄 Serving index.html for route: ${req.path}`); + res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); res.sendFile(indexPath); } else { res.status(404).json({ error: 'API route not found' }); diff --git a/backend/src/services/GiteaService.ts b/backend/src/services/GiteaService.ts new file mode 100644 index 0000000..f5c21bf --- /dev/null +++ b/backend/src/services/GiteaService.ts @@ -0,0 +1,459 @@ +import { settingsService } from './SettingsService'; +import { mainPool } from '../config/database'; +import { GiteaSettings, GiteaBranch, GiteaCommit, GiteaDiff } from '../types'; + +class GiteaService { + private cachedConfig: GiteaSettings | null = null; + private cacheTime = 0; + private readonly CACHE_TTL = 60000; + + invalidateCache() { + this.cachedConfig = null; + this.cacheTime = 0; + } + + async getConfig(): Promise { + if (this.cachedConfig && Date.now() - this.cacheTime < this.CACHE_TTL) { + return this.cachedConfig; + } + this.cachedConfig = await settingsService.getGiteaSettings(); + this.cacheTime = Date.now(); + return this.cachedConfig; + } + + async isEnabled(): Promise { + const cfg = await this.getConfig(); + return !!(cfg?.enabled && cfg.url && cfg.token && cfg.owner && cfg.repo); + } + + private async api(method: string, path: string, body?: any): Promise { + const cfg = await this.getConfig(); + if (!cfg) throw new Error('Gitea not configured'); + + const url = `${cfg.url.replace(/\/+$/, '')}/api/v1${path}`; + const headers: Record = { + 'Authorization': `token ${cfg.token}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + + const res = await fetch(url, { + method, + headers, + body: body ? JSON.stringify(body) : undefined, + }); + + if (res.status === 204) return null; + if (res.status === 404) return null; + + const data: any = await res.json().catch(() => null); + + if (!res.ok) { + const msg = data?.message || `Gitea API error ${res.status}`; + throw new Error(msg); + } + + return data; + } + + // --- Repo management --- + + async ensureRepo(): Promise { + const cfg = await this.getConfig(); + if (!cfg) throw new Error('Gitea not configured'); + + const existing = await this.api('GET', `/repos/${cfg.owner}/${cfg.repo}`); + if (existing) return; + + // Try creating under user first, then org + try { + await this.api('POST', '/user/repos', { + name: cfg.repo, + description: 'KIS API Builder Endpoints', + private: true, + auto_init: true, + default_branch: cfg.prod_branch || 'main', + }); + } catch { + await this.api('POST', `/orgs/${cfg.owner}/repos`, { + name: cfg.repo, + description: 'KIS API Builder Endpoints', + private: true, + auto_init: true, + default_branch: cfg.prod_branch || 'main', + }); + } + } + + async testConnection(): Promise<{ success: boolean; message: string }> { + try { + const cfg = await this.getConfig(); + if (!cfg) return { success: false, message: 'Gitea not configured' }; + + const user = await this.api('GET', '/user'); + if (!user) return { success: false, message: 'Failed to authenticate' }; + + await this.ensureRepo(); + return { success: true, message: `Connected as ${user.login}, repo ready` }; + } catch (err: any) { + return { success: false, message: err.message }; + } + } + + // --- File operations --- + + private async getFileSha(filePath: string, branch: string): Promise { + const cfg = await this.getConfig(); + if (!cfg) return null; + const data = await this.api('GET', `/repos/${cfg.owner}/${cfg.repo}/contents/${filePath}?ref=${encodeURIComponent(branch)}`); + return data?.sha || null; + } + + private async commitFile(filePath: string, content: string, message: string, branch: string): Promise { + const cfg = await this.getConfig(); + if (!cfg) return null; + + const encoded = Buffer.from(content, 'utf-8').toString('base64'); + const sha = await this.getFileSha(filePath, branch); + + const body: any = { + content: encoded, + message, + branch, + }; + + if (sha) { + body.sha = sha; + const res = await this.api('PUT', `/repos/${cfg.owner}/${cfg.repo}/contents/${filePath}`, body); + return res?.content?.sha || null; + } else { + const res = await this.api('POST', `/repos/${cfg.owner}/${cfg.repo}/contents/${filePath}`, body); + return res?.content?.sha || null; + } + } + + private async deleteFile(filePath: string, branch: string, message: string): Promise { + const cfg = await this.getConfig(); + if (!cfg) return; + + const sha = await this.getFileSha(filePath, branch); + if (!sha) return; + + await this.api('DELETE', `/repos/${cfg.owner}/${cfg.repo}/contents/${filePath}`, { + sha, + message, + branch, + }); + } + + async commitEndpointFiles( + endpoint: any, + branch: string, + message: string + ): Promise { + if (!await this.isEnabled()) return null; + + const folderName = endpoint.folder_name ? this.sanitize(endpoint.folder_name) : '_root'; + const epName = this.sanitize(endpoint.name); + const basePath = `endpoints/${folderName}/${epName}`; + + // endpoint.json + const meta = { + name: endpoint.name, + description: endpoint.description || '', + method: endpoint.method, + path: endpoint.path, + execution_type: endpoint.execution_type || 'sql', + database_name: endpoint.database_name || null, + parameters: endpoint.parameters || [], + is_public: endpoint.is_public || false, + enable_logging: endpoint.enable_logging || false, + detailed_response: endpoint.detailed_response || false, + response_schema: endpoint.response_schema || null, + }; + + let lastSha = await this.commitFile( + `${basePath}/endpoint.json`, + JSON.stringify(meta, null, 2), + message, + branch + ); + + // SQL query + if (endpoint.execution_type === 'sql' && endpoint.sql_query) { + lastSha = await this.commitFile(`${basePath}/query.sql`, endpoint.sql_query, message, branch); + } + + // Script + if (endpoint.execution_type === 'script' && endpoint.script_code) { + const ext = endpoint.script_language === 'python' ? 'py' : 'js'; + lastSha = await this.commitFile(`${basePath}/main.${ext}`, endpoint.script_code, message, branch); + } + + // AQL + if (endpoint.execution_type === 'aql') { + const method = endpoint.aql_method || 'GET'; + const ep = endpoint.aql_endpoint || ''; + const body = endpoint.aql_body || ''; + let url = ep; + const qp = endpoint.aql_query_params || {}; + const params = Object.entries(qp).map(([k, v]) => `${k}=${v}`).join('&'); + if (params) url += (url.includes('?') ? '&' : '?') + params; + const httpContent = `${method} ${url}\nContent-Type: application/json\n${body ? '\n' + body + '\n' : ''}`; + lastSha = await this.commitFile(`${basePath}/request.http`, httpContent, message, branch); + } + + // Script queries + const scriptQueries = endpoint.script_queries || []; + if (endpoint.execution_type === 'script' && scriptQueries.length > 0) { + const queryIndex: any[] = []; + for (const sq of scriptQueries) { + const sqMeta: any = { name: sq.name, database_id: sq.database_id || null }; + if (sq.sql) { + const fileName = `${this.sanitize(sq.name)}.sql`; + await this.commitFile(`${basePath}/queries/${fileName}`, sq.sql, message, branch); + sqMeta.file = fileName; + } + if (sq.aql_method) { + const fileName = `${this.sanitize(sq.name)}.http`; + const httpContent = `${sq.aql_method} ${sq.aql_endpoint || ''}\nContent-Type: application/json\n${sq.aql_body ? '\n' + sq.aql_body + '\n' : ''}`; + await this.commitFile(`${basePath}/queries/${fileName}`, httpContent, message, branch); + sqMeta.file = fileName; + sqMeta.type = 'aql'; + } + queryIndex.push(sqMeta); + } + await this.commitFile(`${basePath}/queries/_index.json`, JSON.stringify(queryIndex, null, 2), message, branch); + } + + return lastSha; + } + + async deleteEndpointFiles(endpointName: string, folderName: string | null, branch: string): Promise { + if (!await this.isEnabled()) return; + + const folder = folderName ? this.sanitize(folderName) : '_root'; + const basePath = `endpoints/${folder}/${this.sanitize(endpointName)}`; + const cfg = await this.getConfig(); + if (!cfg) return; + + // List files in the directory + const contents = await this.api('GET', `/repos/${cfg.owner}/${cfg.repo}/contents/${basePath}?ref=${encodeURIComponent(branch)}`); + if (!Array.isArray(contents)) return; + + for (const file of contents) { + if (file.type === 'dir') { + // Recurse into subdirectories (queries/) + const subContents = await this.api('GET', `/repos/${cfg.owner}/${cfg.repo}/contents/${file.path}?ref=${encodeURIComponent(branch)}`); + if (Array.isArray(subContents)) { + for (const subFile of subContents) { + await this.deleteFile(subFile.path, branch, `Delete ${endpointName}`); + } + } + } else { + await this.deleteFile(file.path, branch, `Delete ${endpointName}`); + } + } + } + + // --- Branch management --- + + async listBranches(): Promise { + if (!await this.isEnabled()) return []; + const cfg = await this.getConfig(); + if (!cfg) return []; + const data = await this.api('GET', `/repos/${cfg.owner}/${cfg.repo}/branches`); + if (!Array.isArray(data)) return []; + return data.map((b: any) => ({ + name: b.name, + commit: { id: b.commit?.id || '', message: b.commit?.message || '', timestamp: b.commit?.timestamp || '' }, + })); + } + + async createBranch(name: string, from?: string): Promise { + if (!await this.isEnabled()) return; + const cfg = await this.getConfig(); + if (!cfg) return; + await this.api('POST', `/repos/${cfg.owner}/${cfg.repo}/branches`, { + new_branch_name: name, + old_branch_name: from || cfg.prod_branch || 'main', + }); + } + + async deleteBranch(name: string): Promise { + if (!await this.isEnabled()) return; + const cfg = await this.getConfig(); + if (!cfg) return; + await this.api('DELETE', `/repos/${cfg.owner}/${cfg.repo}/branches/${encodeURIComponent(name)}`); + } + + // --- History --- + + async getCommitHistory(filePath?: string, branch?: string): Promise { + if (!await this.isEnabled()) return []; + const cfg = await this.getConfig(); + if (!cfg) return []; + + const params = new URLSearchParams(); + if (filePath) params.set('path', filePath); + if (branch) params.set('sha', branch); + params.set('limit', '50'); + + const data = await this.api('GET', `/repos/${cfg.owner}/${cfg.repo}/commits?${params.toString()}`); + if (!Array.isArray(data)) return []; + + return data.map((c: any) => ({ + sha: c.sha, + message: c.commit?.message || '', + author: { + name: c.commit?.author?.name || '', + email: c.commit?.author?.email || '', + date: c.commit?.author?.date || '', + }, + html_url: c.html_url || '', + })); + } + + // --- Diff --- + + async compareBranches(base: string, head: string): Promise { + if (!await this.isEnabled()) return null; + const cfg = await this.getConfig(); + if (!cfg) return null; + + const data = await this.api('GET', `/repos/${cfg.owner}/${cfg.repo}/compare/${encodeURIComponent(base)}...${encodeURIComponent(head)}`); + if (!data) return null; + + return { + total_commits: data.total_commits || 0, + commits: (data.commits || []).map((c: any) => ({ + sha: c.sha, + message: c.commit?.message || '', + author: { name: c.commit?.author?.name || '', email: c.commit?.author?.email || '', date: c.commit?.author?.date || '' }, + html_url: c.html_url || '', + })), + files: (data.files || []).map((f: any) => ({ + filename: f.filename, + status: f.status, + additions: f.additions || 0, + deletions: f.deletions || 0, + patch: f.patch, + })), + }; + } + + // --- Pull Requests --- + + async createPR(title: string, headBranch: string, baseBranch?: string, body?: string): Promise { + if (!await this.isEnabled()) return null; + const cfg = await this.getConfig(); + if (!cfg) return null; + + return this.api('POST', `/repos/${cfg.owner}/${cfg.repo}/pulls`, { + title, + head: headBranch, + base: baseBranch || cfg.prod_branch || 'main', + body: body || '', + }); + } + + async mergePR(prIndex: number, deleteBranch = true): Promise { + if (!await this.isEnabled()) return; + const cfg = await this.getConfig(); + if (!cfg) return; + + await this.api('POST', `/repos/${cfg.owner}/${cfg.repo}/pulls/${prIndex}/merge`, { + Do: 'merge', + delete_branch_after_merge: deleteBranch, + }); + } + + // --- Sync all endpoints --- + + async initializeRepo(): Promise<{ synced: number; errors: string[] }> { + if (!await this.isEnabled()) throw new Error('Gitea not enabled'); + + await this.ensureRepo(); + const cfg = await this.getConfig(); + if (!cfg) throw new Error('Gitea not configured'); + + const result = await mainPool.query( + `SELECT e.*, f.name as folder_name, d.name as database_name + FROM endpoints e + LEFT JOIN folders f ON e.folder_id = f.id + LEFT JOIN databases d ON e.database_id = d.id` + ); + + let synced = 0; + const errors: string[] = []; + + for (const ep of result.rows) { + try { + await this.commitEndpointFiles(ep, cfg.prod_branch || 'main', `Sync: ${ep.name}`); + synced++; + } catch (err: any) { + errors.push(`${ep.name}: ${err.message}`); + } + } + + return { synced, errors }; + } + + // --- Helpers --- + + async getEndpointGiteaInfo(endpointId: string): Promise { + if (!await this.isEnabled()) return null; + const cfg = await this.getConfig(); + if (!cfg) return null; + + const epResult = await mainPool.query( + `SELECT e.name, e.path, f.name as folder_name + FROM endpoints e LEFT JOIN folders f ON e.folder_id = f.id + WHERE e.id = $1`, + [endpointId] + ); + if (epResult.rows.length === 0) return null; + + const ep = epResult.rows[0]; + const folder = ep.folder_name ? this.sanitize(ep.folder_name) : '_root'; + const repoPath = `endpoints/${folder}/${this.sanitize(ep.name)}`; + const fileUrl = `${cfg.url}/${cfg.owner}/${cfg.repo}/src/branch/${cfg.prod_branch}/${repoPath}`; + + const versionResult = await mainPool.query( + `SELECT gitea_commit_sha FROM endpoint_versions + WHERE endpoint_id = $1 AND gitea_commit_sha IS NOT NULL + ORDER BY version_number DESC LIMIT 1`, + [endpointId] + ); + + return { + enabled: true, + url: cfg.url, + owner: cfg.owner, + repo: cfg.repo, + file_url: fileUrl, + repo_path: repoPath, + last_commit_sha: versionResult.rows[0]?.gitea_commit_sha || null, + commit_url: versionResult.rows[0]?.gitea_commit_sha + ? `${cfg.url}/${cfg.owner}/${cfg.repo}/commit/${versionResult.rows[0].gitea_commit_sha}` + : null, + }; + } + + async getSyncStatus(): Promise<{ total: number; synced: number; unsynced: number }> { + const totalResult = await mainPool.query('SELECT COUNT(*) FROM endpoints'); + const syncedResult = await mainPool.query( + `SELECT COUNT(DISTINCT ev.endpoint_id) FROM endpoint_versions ev + WHERE ev.gitea_commit_sha IS NOT NULL` + ); + const total = parseInt(totalResult.rows[0].count); + const synced = parseInt(syncedResult.rows[0].count); + return { total, synced, unsynced: total - synced }; + } + + private sanitize(name: string): string { + return name.replace(/[<>:"/\\|?*]/g, '_').replace(/\s+/g, '_').trim(); + } +} + +export const giteaService = new GiteaService(); diff --git a/backend/src/services/IsolatedScriptExecutor.ts b/backend/src/services/IsolatedScriptExecutor.ts index 67e0f2f..c90217b 100644 --- a/backend/src/services/IsolatedScriptExecutor.ts +++ b/backend/src/services/IsolatedScriptExecutor.ts @@ -185,7 +185,7 @@ export class IsolatedScriptExecutor { // Capped setTimeout/clearTimeout const timerIds = new Set>(); - sandbox.setTimeout = (fn: Function, ms: number, ...args: any[]) => { + sandbox.setTimeout = (fn: (...args: any[]) => void, ms: number, ...args: any[]) => { const cappedMs = Math.min(ms || 0, 30000); const id = setTimeout(() => { timerIds.delete(id); diff --git a/backend/src/services/SettingsService.ts b/backend/src/services/SettingsService.ts new file mode 100644 index 0000000..f0c1b21 --- /dev/null +++ b/backend/src/services/SettingsService.ts @@ -0,0 +1,64 @@ +import { mainPool } from '../config/database'; +import { GiteaSettings } from '../types'; +import { config } from '../config/environment'; +import crypto from 'crypto'; + +const ALGORITHM = 'aes-256-gcm'; + +function getEncryptionKey(): Buffer { + return crypto.createHash('sha256').update(config.jwt.secret + ':settings').digest(); +} + +function encrypt(text: string): string { + const key = getEncryptionKey(); + const iv = crypto.randomBytes(16); + const cipher = crypto.createCipheriv(ALGORITHM, key, iv); + const encrypted = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]); + const authTag = cipher.getAuthTag(); + return iv.toString('hex') + ':' + authTag.toString('hex') + ':' + encrypted.toString('hex'); +} + +function decrypt(data: string): string { + const key = getEncryptionKey(); + const [ivHex, authTagHex, encryptedHex] = data.split(':'); + const iv = Buffer.from(ivHex, 'hex'); + const authTag = Buffer.from(authTagHex, 'hex'); + const encrypted = Buffer.from(encryptedHex, 'hex'); + const decipher = crypto.createDecipheriv(ALGORITHM, key, iv); + decipher.setAuthTag(authTag); + return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString('utf8'); +} + +class SettingsService { + async get(key: string): Promise { + const result = await mainPool.query('SELECT value FROM app_settings WHERE key = $1', [key]); + if (result.rows.length === 0) return null; + return result.rows[0].value as T; + } + + async set(key: string, value: any, userId?: string): Promise { + await mainPool.query( + `INSERT INTO app_settings (key, value, updated_at, updated_by) + VALUES ($1, $2, CURRENT_TIMESTAMP, $3) + ON CONFLICT (key) DO UPDATE SET value = $2, updated_at = CURRENT_TIMESTAMP, updated_by = $3`, + [key, JSON.stringify(value), userId || null] + ); + } + + async getGiteaSettings(): Promise { + const raw = await this.get('gitea'); + if (!raw) return null; + try { + return { ...raw, token: raw.token ? decrypt(raw.token) : '' }; + } catch { + return { ...raw, token: '' }; + } + } + + async setGiteaSettings(settings: GiteaSettings, userId: string): Promise { + const toStore = { ...settings, token: settings.token ? encrypt(settings.token) : '' }; + await this.set('gitea', toStore, userId); + } +} + +export const settingsService = new SettingsService(); diff --git a/backend/src/services/VersionService.ts b/backend/src/services/VersionService.ts index c6a09d7..8eafeab 100644 --- a/backend/src/services/VersionService.ts +++ b/backend/src/services/VersionService.ts @@ -1,6 +1,7 @@ import { mainPool } from '../config/database'; import { EndpointVersion, VersionStatus } from '../types'; import * as crypto from 'crypto'; +import { giteaService } from './GiteaService'; const SNAPSHOT_FIELDS = [ 'name', 'description', 'method', 'path', 'database_id', 'sql_query', @@ -81,6 +82,7 @@ class VersionService { updateParams ); + this.syncToGitea(endpointId, version, status).catch(() => {}); return version; } @@ -140,10 +142,11 @@ class VersionService { [nextVersion, version.id, endpointId] ); + this.syncToGitea(endpointId, version, 'draft').catch(() => {}); return version; } - async publishVersion(versionId: string, userId: string): Promise { + async publishVersion(versionId: string, _userId: string): Promise { const vResult = await mainPool.query( 'SELECT * FROM endpoint_versions WHERE id = $1', [versionId] @@ -188,6 +191,8 @@ class VersionService { versionId, version.endpoint_id ] ); + + this.syncToGitea(version.endpoint_id, version, 'published').catch(() => {}); } async rollbackToVersion( @@ -301,6 +306,46 @@ class VersionService { ); return result.rows[0] || null; } + + async syncToGitea(endpointId: string, version: EndpointVersion, status: VersionStatus): Promise { + try { + if (!await giteaService.isEnabled()) return; + + const cfg = await giteaService.getConfig(); + if (!cfg) return; + + const epResult = await mainPool.query( + `SELECT e.*, f.name as folder_name, d.name as database_name + FROM endpoints e + LEFT JOIN folders f ON e.folder_id = f.id + LEFT JOIN databases d ON e.database_id = d.id + WHERE e.id = $1`, + [endpointId] + ); + if (epResult.rows.length === 0) return; + + const ep = { ...epResult.rows[0], ...version }; + const branch = status === 'published' + ? cfg.prod_branch || 'main' + : `drafts/${giteaService['sanitize'](version.name)}`; + + if (status === 'draft') { + try { await giteaService.createBranch(branch, cfg.prod_branch || 'main'); } catch { /* branch may already exist */ } + } + + const message = `v${version.version_number}: ${version.change_message || status}`; + const sha = await giteaService.commitEndpointFiles(ep, branch, message); + + if (sha) { + await mainPool.query( + 'UPDATE endpoint_versions SET gitea_commit_sha = $1 WHERE id = $2', + [sha, version.id] + ); + } + } catch (err) { + console.error('Gitea sync failed (non-blocking):', err); + } + } } export const versionService = new VersionService(); diff --git a/backend/src/types/index.ts b/backend/src/types/index.ts index 4c10c5a..38e017b 100644 --- a/backend/src/types/index.ts +++ b/backend/src/types/index.ts @@ -138,6 +138,39 @@ export interface IsolatedExecutionResult { queries: QueryExecution[]; } +export interface GiteaSettings { + enabled: boolean; + url: string; + token: string; + owner: string; + repo: string; + prod_branch: string; +} + +export interface GiteaBranch { + name: string; + commit: { id: string; message: string; timestamp: string }; +} + +export interface GiteaCommit { + sha: string; + message: string; + author: { name: string; email: string; date: string }; + html_url: string; +} + +export interface GiteaDiff { + total_commits: number; + commits: GiteaCommit[]; + files: Array<{ + filename: string; + status: string; + additions: number; + deletions: number; + patch?: string; + }>; +} + export class ScriptExecutionError extends Error { logs: LogEntry[]; queries: QueryExecution[]; diff --git a/docker-compose.gitea.yml b/docker-compose.gitea.yml new file mode 100644 index 0000000..aff81a4 --- /dev/null +++ b/docker-compose.gitea.yml @@ -0,0 +1,44 @@ +# ============================================ +# KIS API Builder - Optional Gitea Integration +# ============================================ +# Use alongside the main docker-compose: +# docker compose -f docker-compose.yml -f docker-compose.gitea.yml up -d +# +# For existing Gitea: skip this file, configure via Settings UI +# ============================================ + +services: + gitea: + image: gitea/gitea:1.22-rootless + container_name: kis-api-builder-gitea + restart: unless-stopped + environment: + - GITEA__database__DB_TYPE=sqlite3 + - GITEA__server__ROOT_URL=${GITEA_URL:-http://localhost:${GITEA_PORT:-8010}} + - GITEA__server__HTTP_PORT=3000 + - GITEA__security__INSTALL_LOCK=true + - GITEA__service__DISABLE_REGISTRATION=true + volumes: + - gitea_data:/var/lib/gitea + - gitea_config:/etc/gitea + ports: + - "${GITEA_PORT:-8010}:3000" + networks: + - kis-network + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/api/v1/version"] + interval: 15s + timeout: 5s + retries: 5 + start_period: 30s + + app: + environment: + GITEA_URL: ${GITEA_URL:-http://gitea:3000} + depends_on: + gitea: + condition: service_healthy + +volumes: + gitea_data: + gitea_config: diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs new file mode 100644 index 0000000..fe3748a --- /dev/null +++ b/frontend/.eslintrc.cjs @@ -0,0 +1,19 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + ], + ignorePatterns: ['dist', '.eslintrc.cjs'], + parser: '@typescript-eslint/parser', + plugins: ['react-refresh'], + rules: { + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], + 'no-console': 'off', + 'prefer-const': 'warn', + }, +}; diff --git a/frontend/src/pages/EndpointEditor.tsx b/frontend/src/pages/EndpointEditor.tsx index 6e809a0..ce43742 100644 --- a/frontend/src/pages/EndpointEditor.tsx +++ b/frontend/src/pages/EndpointEditor.tsx @@ -1,9 +1,9 @@ import { useState, useEffect, useMemo } from 'react'; import { useParams, useNavigate, useSearchParams } from 'react-router-dom'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { endpointsApi, foldersApi, databasesApi, versionsApi } from '@/services/api'; +import { endpointsApi, foldersApi, databasesApi, versionsApi, giteaApi } from '@/services/api'; import { EndpointParameter, QueryTestResult, LogEntry, QueryExecution, EndpointVersion } from '@/types'; -import { Plus, Trash2, Play, Edit2, ChevronDown, ChevronUp, ArrowLeft, CheckCircle, XCircle, Clock, Copy, X, Terminal, History, RotateCcw, Upload } from 'lucide-react'; +import { Plus, Trash2, Play, Edit2, ChevronDown, ChevronUp, ArrowLeft, CheckCircle, XCircle, Clock, Copy, X, Terminal, History, RotateCcw, Upload, GitBranch } from 'lucide-react'; import toast from 'react-hot-toast'; import SqlEditor from '@/components/SqlEditor'; import CodeEditor from '@/components/CodeEditor'; @@ -105,7 +105,7 @@ export default function EndpointEditor() { try { const saved = localStorage.getItem(storageKey); if (saved) return JSON.parse(saved).testParams || {}; - } catch {} + } catch { /* ignore corrupt localStorage */ } } return {}; }); @@ -134,7 +134,7 @@ export default function EndpointEditor() { const parsed = JSON.parse(saved); if (parsed.testResult) setTestResult(parsed.testResult); } - } catch {} + } catch { /* ignore */ } } }, [storageKey]); @@ -143,7 +143,7 @@ export default function EndpointEditor() { if (storageKey) { try { localStorage.setItem(storageKey, JSON.stringify({ testParams, testResult })); - } catch {} + } catch { /* ignore */ } } }, [storageKey, testParams, testResult]); @@ -242,6 +242,12 @@ export default function EndpointEditor() { }); const [showVersionHistory, setShowVersionHistory] = useState(false); + const { data: giteaInfo } = useQuery({ + queryKey: ['gitea-info', id], + queryFn: () => giteaApi.getEndpointInfo(id!).then(r => r.data), + enabled: isEditing, + }); + const draftMutation = useMutation({ mutationFn: () => versionsApi.saveDraft(id!, { ...formData, change_message: undefined }), onSuccess: () => { @@ -1099,6 +1105,44 @@ export default function EndpointEditor() { )} + {/* Gitea info */} + {isEditing && giteaInfo?.enabled && ( +
+
+ + Gitea +
+
+ {giteaInfo.file_url && ( + + View in Gitea + + )} + {giteaInfo.last_commit_sha && ( + + )} + {!giteaInfo.last_commit_sha && ( + Not yet synced to Gitea + )} +
+
+ )} + {/* Test results */} {testResult && (
diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index 13ac5da..121cc65 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -1,9 +1,9 @@ import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { usersApi, dbManagementApi } from '@/services/api'; +import { usersApi, dbManagementApi, giteaApi } from '@/services/api'; import { useAuthStore } from '@/stores/authStore'; import toast from 'react-hot-toast'; -import { User, Lock, UserCircle, Database, Plus, Edit2, Trash2, Eye, EyeOff, Users } from 'lucide-react'; +import { User, Lock, UserCircle, Database, Plus, Edit2, Trash2, Eye, EyeOff, Users, GitBranch } from 'lucide-react'; import Dialog from '@/components/Dialog'; import CodeEditor from '@/components/CodeEditor'; @@ -216,7 +216,7 @@ function PasswordTab({ currentUser }: { currentUser: any }) { } function GlobalSettingsTab() { - const [subTab, setSubTab] = useState<'databases' | 'users'>('databases'); + const [subTab, setSubTab] = useState<'databases' | 'users' | 'gitea'>('databases'); return (
@@ -244,11 +244,262 @@ function GlobalSettingsTab() { Пользователи +
{subTab === 'databases' && } {subTab === 'users' && } + {subTab === 'gitea' && } +
+ ); +} + +function GiteaSubTab() { + const queryClient = useQueryClient(); + const [showPassword, setShowPassword] = useState(false); + + const { data: settings, isLoading } = useQuery({ + queryKey: ['gitea-settings'], + queryFn: () => giteaApi.getSettings().then(r => r.data), + }); + + const { data: syncStatus } = useQuery({ + queryKey: ['gitea-sync-status'], + queryFn: () => giteaApi.getSyncStatus().then(r => r.data), + enabled: !!settings?.enabled, + }); + + const { data: branches } = useQuery({ + queryKey: ['gitea-branches'], + queryFn: () => giteaApi.listBranches().then(r => r.data), + enabled: !!settings?.enabled, + }); + + const [formData, setFormData] = useState({ + enabled: false, url: '', token: '', owner: '', repo: '', prod_branch: 'main', + }); + + useState(() => { + if (settings) setFormData(settings); + }); + + // Sync form when settings load + const prevSettings = settings; + if (prevSettings && formData.url === '' && prevSettings.url !== '') { + setFormData(prevSettings); + } + + const saveMutation = useMutation({ + mutationFn: (data: any) => giteaApi.updateSettings(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['gitea-settings'] }); + queryClient.invalidateQueries({ queryKey: ['gitea-branches'] }); + queryClient.invalidateQueries({ queryKey: ['gitea-sync-status'] }); + toast.success('Gitea settings saved'); + }, + onError: () => toast.error('Failed to save Gitea settings'), + }); + + const testMutation = useMutation({ + mutationFn: () => giteaApi.testConnection(), + onSuccess: (res) => { + if (res.data.success) toast.success(res.data.message); + else toast.error(res.data.message); + }, + onError: () => toast.error('Connection test failed'), + }); + + const syncMutation = useMutation({ + mutationFn: () => giteaApi.syncAll(), + onSuccess: (res) => { + toast.success(`Synced ${res.data.synced} endpoints${res.data.errors?.length ? `, ${res.data.errors.length} errors` : ''}`); + queryClient.invalidateQueries({ queryKey: ['gitea-sync-status'] }); + }, + onError: () => toast.error('Sync failed'), + }); + + const createBranchMutation = useMutation({ + mutationFn: (name: string) => giteaApi.createBranch(name), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['gitea-branches'] }); + toast.success('Branch created'); + }, + onError: () => toast.error('Failed to create branch'), + }); + + const deleteBranchMutation = useMutation({ + mutationFn: (name: string) => giteaApi.deleteBranch(name), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['gitea-branches'] }); + toast.success('Branch deleted'); + }, + onError: () => toast.error('Failed to delete branch'), + }); + + if (isLoading) return
; + + return ( +
+
+

Gitea Integration

+

Version control for endpoints via Gitea (branches, merges, diffs, PR)

+
+ + {/* Connection form */} +
+ + +
+
+ + setFormData({ ...formData, url: e.target.value })} + className="input w-full" + placeholder="http://gitea:3000" + /> +
+
+ +
+ setFormData({ ...formData, token: e.target.value })} + className="input w-full pr-10" + placeholder="gitea_api_token" + /> + +
+
+
+ +
+
+ + setFormData({ ...formData, owner: e.target.value })} + className="input w-full" + placeholder="myorg" + /> +
+
+ + setFormData({ ...formData, repo: e.target.value })} + className="input w-full" + placeholder="kis-endpoints" + /> +
+
+ + setFormData({ ...formData, prod_branch: e.target.value })} + className="input w-full" + placeholder="main" + /> +
+
+ +
+ + +
+
+ + {/* Sync status */} + {settings?.enabled && ( +
+
+
+

Sync Status

+ {syncStatus && ( +

+ {syncStatus.synced}/{syncStatus.total} endpoints synced + {syncStatus.unsynced > 0 && ({syncStatus.unsynced} unsynced)} +

+ )} +
+ +
+
+ )} + + {/* Branches */} + {settings?.enabled && branches && ( +
+
+

Branches ({branches.length})

+ +
+
+ {branches.map((b: any) => ( +
+
+ + {b.name} + {b.name === (settings?.prod_branch || 'main') && ( + prod + )} +
+
+ {b.commit?.id?.slice(0, 7)} + {b.name !== (settings?.prod_branch || 'main') && ( + + )} +
+
+ ))} +
+
+ )}
); } diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index d0a831d..dd8272e 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -1,5 +1,5 @@ import axios from 'axios'; -import { AuthResponse, User, Endpoint, Folder, ApiKey, Database, QueryTestResult, ImportPreviewResponse, EndpointVersion } from '@/types'; +import { AuthResponse, User, Endpoint, Folder, ApiKey, Database, QueryTestResult, ImportPreviewResponse, EndpointVersion, GiteaSettings, GiteaBranch, GiteaCommit, GiteaEndpointInfo } from '@/types'; const api = axios.create({ baseURL: '/api', @@ -164,6 +164,23 @@ export const versionsApi = { api.get(`/endpoints/${endpointId}/draft`), }; +// Gitea API +export const giteaApi = { + getSettings: () => api.get('/gitea/settings'), + updateSettings: (data: Partial) => api.put('/gitea/settings', data), + testConnection: () => api.post<{ success: boolean; message: string }>('/gitea/test'), + syncAll: () => api.post<{ synced: number; errors: string[] }>('/gitea/sync-all'), + getSyncStatus: () => api.get<{ total: number; synced: number; unsynced: number }>('/gitea/sync-status'), + listBranches: () => api.get('/gitea/branches'), + createBranch: (name: string, from?: string) => api.post('/gitea/branches', { name, from }), + deleteBranch: (name: string) => api.delete(`/gitea/branches/${encodeURIComponent(name)}`), + compareBranches: (base: string, head: string) => api.get('/gitea/compare', { params: { base, head } }), + getCommitHistory: (path?: string, branch?: string) => api.get('/gitea/commits', { params: { path, branch } }), + getEndpointInfo: (endpointId: string) => api.get(`/gitea/endpoints/${endpointId}/info`), + createPR: (data: { title: string; head: string; base?: string; body?: string }) => api.post('/gitea/pulls', data), + mergePR: (index: number) => api.post(`/gitea/pulls/${index}/merge`), +}; + // Folders API export const foldersApi = { getAll: () => diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 54bb682..bffde2c 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -134,6 +134,38 @@ export interface QueryTestResult { processedQuery?: string; } +export interface GiteaSettings { + enabled: boolean; + url: string; + token: string; + owner: string; + repo: string; + prod_branch: string; +} + +export interface GiteaBranch { + name: string; + commit: { id: string; message: string; timestamp: string }; +} + +export interface GiteaCommit { + sha: string; + message: string; + author: { name: string; email: string; date: string }; + html_url: string; +} + +export interface GiteaEndpointInfo { + enabled: boolean; + url?: string; + owner?: string; + repo?: string; + file_url?: string; + repo_path?: string; + last_commit_sha?: string; + commit_url?: string; +} + export type VersionStatus = 'draft' | 'published' | 'archived'; export interface EndpointVersion { diff --git a/package.json b/package.json index 38e314f..a7141e3 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,9 @@ "start:prod": "cd backend && pm2 start dist/server.js --name kis-api-builder-backend --env production", "reload:prod": "npm run build && cd backend && pm2 reload kis-api-builder-backend", "stop:prod": "cd backend && pm2 stop kis-api-builder-backend", - "delete:prod": "cd backend && pm2 delete kis-api-builder-backend" + "delete:prod": "cd backend && pm2 delete kis-api-builder-backend", + "lint": "cd backend && npx eslint src/ --ext ts && cd ../frontend && npx eslint src/ --ext ts,tsx", + "lint:fix": "cd backend && npx eslint src/ --ext ts --fix && cd ../frontend && npx eslint src/ --ext ts,tsx --fix" }, "devDependencies": { "concurrently": "^8.2.2"