From fe4e45a6ca86155ff73a35841bee7d97c1c96079 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Mon, 29 Jan 2024 11:33:37 +0200 Subject: [PATCH 001/114] init: start crud api task --- .editorconfig | 9 + .eslintrc.cjs | 79 + .gitignore | 3 + .lintstagedrc.cjs | 6 + nodemon.json | 6 + package-lock.json | 4295 +++++++++++++++++++++++++++++++++++++++++++ package.json | 29 + prettier.config.cjs | 8 + server.ts | 1 + tsconfig.json | 22 + 10 files changed, 4458 insertions(+) create mode 100644 .editorconfig create mode 100644 .eslintrc.cjs create mode 100644 .lintstagedrc.cjs create mode 100644 nodemon.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 prettier.config.cjs create mode 100644 server.ts create mode 100644 tsconfig.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ab561f3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..1bd784d --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,79 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true, es6: true }, + extends: [ + 'airbnb', + 'airbnb-typescript', + 'airbnb/hooks', + 'plugin:@typescript-eslint/recommended', + 'plugin:import/recommended', + 'plugin:node/recommended', + 'prettier', + ], + ignorePatterns: [ + '.eslintrc.cjs', + 'prettier.config.js', + 'node_modules', + 'environment.d.ts', + ], + plugins: ['import', 'prettier', '@typescript-eslint'], + parser: '@typescript-eslint/parser', + parserOptions: { + project: ['tsconfig.json'], + tsconfigRootDir: __dirname, + ecmaVersion: 'latest', + sourceType: 'module', + }, + rules: { + '@typescript-eslint/no-var-requires': 'off', + 'class-methods-use-this': 'off', + 'no-process-exit': 'off', + 'node/no-unpublished-import': 'off', + 'node/no-missing-import': 'off', + 'node/no-unsupported-features/es-syntax': [ + 'error', + { + ignores: ['modules'], + }, + ], + 'sort-imports': [ + 'error', + { ignoreCase: true, ignoreDeclarationSort: true }, + ], + 'import/prefer-default-export': 'off', + 'import/no-extraneous-dependencies': 'off', + 'import/extensions': ['error', 'never'], + 'import/order': [ + 'error', + { + groups: [ + 'builtin', + 'external', + 'internal', + ['sibling', 'parent'], + 'index', + ], + pathGroups: [ + { + pattern: 'node', + group: 'external', + position: 'before', + }, + ], + pathGroupsExcludedImportTypes: ['internal'], + 'newlines-between': 'always', + alphabetize: { + order: 'asc', + caseInsensitive: true, + }, + }, + ], + }, + settings: { + 'import/resolver': { + node: { + extensions: ['.ts'], + }, + }, + }, +}; diff --git a/.gitignore b/.gitignore index c6bba59..5ec221f 100644 --- a/.gitignore +++ b/.gitignore @@ -122,6 +122,9 @@ dist # Stores VSCode versions used for testing VSCode extensions .vscode-test +# IDE +.idea + # yarn v2 .yarn/cache .yarn/unplugged diff --git a/.lintstagedrc.cjs b/.lintstagedrc.cjs new file mode 100644 index 0000000..18b9785 --- /dev/null +++ b/.lintstagedrc.cjs @@ -0,0 +1,6 @@ +module.exports = { + '**/*.ts': [ + 'pnpm lint:fix', + 'pnpm format:fix', + ], +}; diff --git a/nodemon.json b/nodemon.json new file mode 100644 index 0000000..e1548a6 --- /dev/null +++ b/nodemon.json @@ -0,0 +1,6 @@ +{ + "watch": ["./"], + "ext": "ts", + "ignore": ["node_modules"], + "exec": "ts-node-dev server.ts" +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..006cc6d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4295 @@ +{ + "name": "crud-api", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "crud-api", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@types/node": "^20.11.10", + "@typescript-eslint/eslint-plugin": "^6.19.1", + "@typescript-eslint/parser": "^6.19.1", + "eslint": "^8.56.0", + "eslint-config-airbnb": "^19.0.4", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-config-airbnb-typescript": "^17.1.0", + "eslint-config-prettier": "^9.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prettier": "^5.1.3", + "nodemon": "^3.0.3", + "prettier": "^3.2.4", + "ts-node-dev": "^2.0.0", + "typescript": "^5.3.3" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", + "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", + "dev": true, + "peer": true, + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "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, + "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/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", + "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", + "dev": true + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "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, + "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, + "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, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.11.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.10.tgz", + "integrity": "sha512-rZEfe/hJSGYmdfX9tvcPMYeYPW2sNl50nsw4jZmRcaG0HIAb0WYEpsB05GOb53vjqpyE9GUhlDQ4jLSoB5q9kg==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/semver": { + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", + "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", + "dev": true + }, + "node_modules/@types/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==", + "dev": true + }, + "node_modules/@types/strip-json-comments": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz", + "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.19.1.tgz", + "integrity": "sha512-roQScUGFruWod9CEyoV5KlCYrubC/fvG8/1zXuT0WTcxX87GnMMmnksMwSg99lo1xiKrBzw2icsJPMAw1OtKxg==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.19.1", + "@typescript-eslint/type-utils": "6.19.1", + "@typescript-eslint/utils": "6.19.1", + "@typescript-eslint/visitor-keys": "6.19.1", + "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/parser": { + "version": "6.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.19.1.tgz", + "integrity": "sha512-WEfX22ziAh6pRE9jnbkkLGp/4RhTpffr2ZK5bJ18M8mIfA8A+k97U9ZyaXCEJRlmMHh7R9MJZWXp/r73DzINVQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "6.19.1", + "@typescript-eslint/types": "6.19.1", + "@typescript-eslint/typescript-estree": "6.19.1", + "@typescript-eslint/visitor-keys": "6.19.1", + "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/scope-manager": { + "version": "6.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.19.1.tgz", + "integrity": "sha512-4CdXYjKf6/6aKNMSly/BP4iCSOpvMmqtDzRtqFyyAae3z5kkqEjKndR5vDHL8rSuMIIWP8u4Mw4VxLyxZW6D5w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.19.1", + "@typescript-eslint/visitor-keys": "6.19.1" + }, + "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.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.19.1.tgz", + "integrity": "sha512-0vdyld3ecfxJuddDjACUvlAeYNrHP/pDeQk2pWBR2ESeEzQhg52DF53AbI9QCBkYE23lgkhLCZNkHn2hEXXYIg==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "6.19.1", + "@typescript-eslint/utils": "6.19.1", + "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/types": { + "version": "6.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.19.1.tgz", + "integrity": "sha512-6+bk6FEtBhvfYvpHsDgAL3uo4BfvnTnoge5LrrCj2eJN8g3IJdLTD4B/jK3Q6vo4Ql/Hoip9I8aB6fF+6RfDqg==", + "dev": true, + "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.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.19.1.tgz", + "integrity": "sha512-aFdAxuhzBFRWhy+H20nYu19+Km+gFfwNO4TEqyszkMcgBDYQjmPJ61erHxuT2ESJXhlhrO7I5EFIlZ+qGR8oVA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.19.1", + "@typescript-eslint/visitor-keys": "6.19.1", + "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/utils": { + "version": "6.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.19.1.tgz", + "integrity": "sha512-JvjfEZuP5WoMqwh9SPAPDSHSg9FBHHGhjPugSRxu5jMfjvBpq5/sGTD+9M9aQ5sh6iJ8AY/Kk/oUYVEMAPwi7w==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.19.1", + "@typescript-eslint/types": "6.19.1", + "@typescript-eslint/typescript-estree": "6.19.1", + "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.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.19.1.tgz", + "integrity": "sha512-gkdtIO+xSO/SmI0W68DBg4u1KElmIUo3vXzgHyGPs6cxgB0sa3TlptRAAE0hUY1hM6FcDKEv7aIwiTGm76cXfQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.19.1", + "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.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "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, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "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", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "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, + "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", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", + "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz", + "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.2.tgz", + "integrity": "sha512-HuQCHOlk1Weat5jzStICBCd83NxiIMwqDg/dHEsoefabn/hJRj5pVdWcPUSpRrwhwxZOsQassMpgN/xRYFBMIg==", + "dev": true, + "peer": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.2.1" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", + "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "peer": true + }, + "node_modules/asynciterator.prototype": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz", + "integrity": "sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==", + "dev": true, + "peer": true, + "dependencies": { + "has-symbols": "^1.0.3" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz", + "integrity": "sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", + "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==", + "dev": true, + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "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, + "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/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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, + "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 + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/confusing-browser-globals": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", + "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", + "dev": true + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "peer": true + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "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, + "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", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dynamic-dedupe": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", + "integrity": "sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==", + "dev": true, + "dependencies": { + "xtend": "^4.0.0" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "peer": true + }, + "node_modules/es-abstract": { + "version": "1.22.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", + "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.2", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.5", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.2", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.12", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "safe-array-concat": "^1.0.1", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz", + "integrity": "sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g==", + "dev": true, + "peer": true, + "dependencies": { + "asynciterator.prototype": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.1", + "es-set-tostringtag": "^2.0.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.2.1", + "globalthis": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "iterator.prototype": "^1.1.2", + "safe-array-concat": "^1.0.1" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", + "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2", + "has-tostringtag": "^1.0.0", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", + "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.56.0", + "@humanwhocodes/config-array": "^0.11.13", + "@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-config-airbnb": { + "version": "19.0.4", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-19.0.4.tgz", + "integrity": "sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==", + "dev": true, + "dependencies": { + "eslint-config-airbnb-base": "^15.0.0", + "object.assign": "^4.1.2", + "object.entries": "^1.1.5" + }, + "engines": { + "node": "^10.12.0 || ^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^7.32.0 || ^8.2.0", + "eslint-plugin-import": "^2.25.3", + "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-react": "^7.28.0", + "eslint-plugin-react-hooks": "^4.3.0" + } + }, + "node_modules/eslint-config-airbnb-base": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz", + "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==", + "dev": true, + "dependencies": { + "confusing-browser-globals": "^1.0.10", + "object.assign": "^4.1.2", + "object.entries": "^1.1.5", + "semver": "^6.3.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "peerDependencies": { + "eslint": "^7.32.0 || ^8.2.0", + "eslint-plugin-import": "^2.25.2" + } + }, + "node_modules/eslint-config-airbnb-base/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-config-airbnb-typescript": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-typescript/-/eslint-config-airbnb-typescript-17.1.0.tgz", + "integrity": "sha512-GPxI5URre6dDpJ0CtcthSZVBAfI+Uw7un5OYNVxP2EYi3H81Jw701yFP7AU+/vCE7xBtFmjge7kfhhk4+RAiig==", + "dev": true, + "dependencies": { + "eslint-config-airbnb-base": "^15.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^5.13.0 || ^6.0.0", + "@typescript-eslint/parser": "^5.0.0 || ^6.0.0", + "eslint": "^7.32.0 || ^8.2.0", + "eslint-plugin-import": "^2.25.3" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", + "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", + "dev": true, + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-es": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz", + "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==", + "dev": true, + "dependencies": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + }, + "engines": { + "node": ">=8.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=4.19.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", + "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.7", + "array.prototype.findlastindex": "^1.2.3", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.8.0", + "hasown": "^2.0.0", + "is-core-module": "^2.13.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.7", + "object.groupby": "^1.0.1", + "object.values": "^1.1.7", + "semver": "^6.3.1", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz", + "integrity": "sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/runtime": "^7.23.2", + "aria-query": "^5.3.0", + "array-includes": "^3.1.7", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "=4.7.0", + "axobject-query": "^3.2.1", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "es-iterator-helpers": "^1.0.15", + "hasown": "^2.0.0", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.entries": "^1.1.7", + "object.fromentries": "^2.0.7" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-node": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", + "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", + "dev": true, + "dependencies": { + "eslint-plugin-es": "^3.0.0", + "eslint-utils": "^2.0.0", + "ignore": "^5.1.1", + "minimatch": "^3.0.4", + "resolve": "^1.10.1", + "semver": "^6.1.0" + }, + "engines": { + "node": ">=8.10.0" + }, + "peerDependencies": { + "eslint": ">=5.16.0" + } + }, + "node_modules/eslint-plugin-node/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-node/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-node/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", + "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.33.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz", + "integrity": "sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==", + "dev": true, + "peer": true, + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flatmap": "^1.3.1", + "array.prototype.tosorted": "^1.1.1", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.0.12", + "estraverse": "^5.3.0", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.6", + "object.fromentries": "^2.0.6", + "object.hasown": "^1.1.2", + "object.values": "^1.1.6", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.4", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.8" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", + "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "peer": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "peer": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "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, + "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-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "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, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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, + "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.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "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, + "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, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "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.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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 + }, + "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 + }, + "node_modules/fastq": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.0.tgz", + "integrity": "sha512-zGygtijUMT7jnk3h26kUms3BkSDp4IfIKjmnqI2tvx6nuBfiF1UqOxbnLfzdv+apBy+53oaImsKtMw/xYbW+1w==", + "dev": true, + "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, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=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, + "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, + "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.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "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.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "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 + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", + "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", + "dev": true, + "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", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "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, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/internal-slot": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", + "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", + "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", + "dev": true, + "peer": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", + "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", + "dev": true, + "peer": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dev": true, + "peer": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", + "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "dev": true, + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", + "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "dev": true, + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.11" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", + "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "dev": true, + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", + "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "dev": true, + "peer": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/iterator.prototype": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", + "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", + "dev": true, + "peer": true, + "dependencies": { + "define-properties": "^1.2.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "reflect.getprototypeof": "^1.0.4", + "set-function-name": "^2.0.1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "peer": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "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 + }, + "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 + }, + "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 + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "peer": true, + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "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, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", + "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==", + "dev": true, + "peer": true + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "peer": true, + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "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, + "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, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "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, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "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, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "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 + }, + "node_modules/nodemon": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.3.tgz", + "integrity": "sha512-7jH/NXbFPxVaMwmBCC2B9F/V6X1VkEdNgx3iu9jji8WxWcvhMWkmhNWhI5077zknOnZnBzba9hZP6bCPJLSReQ==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "dev": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.7.tgz", + "integrity": "sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", + "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", + "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1" + } + }, + "node_modules/object.hasown": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.3.tgz", + "integrity": "sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA==", + "dev": true, + "peer": true, + "dependencies": { + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", + "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "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, + "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, + "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, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "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, + "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", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz", + "integrity": "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "peer": true, + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "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, + "engines": { + "node": ">=6" + } + }, + "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" + } + ] + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "peer": true + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", + "integrity": "sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==", + "dev": true, + "peer": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "globalthis": "^1.0.3", + "which-builtin-type": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true, + "peer": true + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "set-function-name": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "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", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "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" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.0.tgz", + "integrity": "sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "get-intrinsic": "^1.2.2", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz", + "integrity": "sha512-83S9w6eFq12BBIJYvjMux6/dkirb8+4zJRA9cxNBVb7Wq5fJBW+Xze48WqR8pxua7bDuAaaAxtVVd4Idjp1dBQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "get-intrinsic": "^1.2.2", + "is-regex": "^1.1.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", + "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.1", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.2", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz", + "integrity": "sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ==", + "dev": true, + "peer": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "regexp.prototype.flags": "^1.5.0", + "set-function-name": "^2.0.0", + "side-channel": "^1.0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", + "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", + "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", + "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "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, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/synckit": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", + "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "dev": true, + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "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 + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "dependencies": { + "nopt": "~1.0.10" + }, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-api-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", + "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "dev": true, + "engines": { + "node": ">=16.13.0" + }, + "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", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node-dev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-2.0.0.tgz", + "integrity": "sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.1", + "dynamic-dedupe": "^0.3.0", + "minimist": "^1.2.6", + "mkdirp": "^1.0.4", + "resolve": "^1.0.0", + "rimraf": "^2.6.1", + "source-map-support": "^0.5.12", + "tree-kill": "^1.2.2", + "ts-node": "^10.4.0", + "tsconfig": "^7.0.0" + }, + "bin": { + "ts-node-dev": "lib/bin.js", + "tsnd": "lib/bin.js" + }, + "engines": { + "node": ">=0.8.0" + }, + "peerDependencies": { + "node-notifier": "*", + "typescript": "*" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/ts-node-dev/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/tsconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", + "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", + "dev": true, + "dependencies": { + "@types/strip-bom": "^3.0.0", + "@types/strip-json-comments": "0.0.30", + "strip-bom": "^3.0.0", + "strip-json-comments": "^2.0.0" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "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, + "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, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "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, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "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, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz", + "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==", + "dev": true, + "peer": true, + "dependencies": { + "function.prototype.name": "^1.1.5", + "has-tostringtag": "^1.0.0", + "is-async-function": "^2.0.0", + "is-date-object": "^1.0.5", + "is-finalizationregistry": "^1.0.2", + "is-generator-function": "^1.0.10", + "is-regex": "^1.1.4", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", + "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "dev": true, + "peer": true, + "dependencies": { + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-weakmap": "^2.0.1", + "is-weakset": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", + "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.4", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "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, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..585bbe2 --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "crud-api", + "version": "1.0.0", + "description": "", + "main": "server.ts", + "scripts": { + "start": "nodemon" + }, + "author": "Bohdan Shcherbyna", + "license": "ISC", + "devDependencies": { + "@types/node": "^20.11.10", + "@typescript-eslint/eslint-plugin": "^6.19.1", + "@typescript-eslint/parser": "^6.19.1", + "eslint": "^8.56.0", + "eslint-config-airbnb": "^19.0.4", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-config-airbnb-typescript": "^17.1.0", + "eslint-config-prettier": "^9.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prettier": "^5.1.3", + "nodemon": "^3.0.3", + "prettier": "^3.2.4", + "ts-node-dev": "^2.0.0", + "typescript": "^5.3.3" + } +} diff --git a/prettier.config.cjs b/prettier.config.cjs new file mode 100644 index 0000000..18d32e9 --- /dev/null +++ b/prettier.config.cjs @@ -0,0 +1,8 @@ +module.exports = { + tabWidth: 2, + singleQuote: true, + arrowParens: 'always', + trailingComma: 'all', + bracketSpacing: true, + bracketSameLine: true, +}; diff --git a/server.ts b/server.ts new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/server.ts @@ -0,0 +1 @@ + diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..f0eb255 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["ESNext"], + "module": "NodeNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "NodeNext", + "allowImportingTsExtensions": false, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + }, +} From 5df9464a3afa485e34f3f11da62ea6864873b745 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Mon, 29 Jan 2024 11:59:29 +0200 Subject: [PATCH 002/114] feat: add env --- .env.example | 1 + environment.d.ts | 5 +++++ package-lock.json | 13 +++++++++++++ package.json | 1 + server.ts | 2 +- 5 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 .env.example create mode 100644 environment.d.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..eb0d788 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +PORT=YOUR_PORT diff --git a/environment.d.ts b/environment.d.ts new file mode 100644 index 0000000..fb06342 --- /dev/null +++ b/environment.d.ts @@ -0,0 +1,5 @@ +namespace NodeJS { + interface ProcessEnv { + PORT: string; + } +} diff --git a/package-lock.json b/package-lock.json index 006cc6d..eedda9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@types/node": "^20.11.10", "@typescript-eslint/eslint-plugin": "^6.19.1", "@typescript-eslint/parser": "^6.19.1", + "dotenv": "^16.4.1", "eslint": "^8.56.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb-base": "^15.0.0", @@ -1090,6 +1091,18 @@ "node": ">=6.0.0" } }, + "node_modules/dotenv": { + "version": "16.4.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.1.tgz", + "integrity": "sha512-CjA3y+Dr3FyFDOAMnxZEGtnW9KBR2M0JvvUtXNW+dYJL5ROWxP9DUHCwgFqpMk0OXCc0ljhaNTr2w/kutYIcHQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, "node_modules/dynamic-dedupe": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", diff --git a/package.json b/package.json index 585bbe2..4efb70f 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "@types/node": "^20.11.10", "@typescript-eslint/eslint-plugin": "^6.19.1", "@typescript-eslint/parser": "^6.19.1", + "dotenv": "^16.4.1", "eslint": "^8.56.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb-base": "^15.0.0", diff --git a/server.ts b/server.ts index 8b13789..0b172b7 100644 --- a/server.ts +++ b/server.ts @@ -1 +1 @@ - +import 'dotenv/config'; From 1ead72f086b3f7748302eb4d2c29549cbb38ddb0 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Mon, 29 Jan 2024 11:59:49 +0200 Subject: [PATCH 003/114] chore: adjust nodemon cofig --- nodemon.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodemon.json b/nodemon.json index e1548a6..3adf8bc 100644 --- a/nodemon.json +++ b/nodemon.json @@ -1,5 +1,5 @@ { - "watch": ["./"], + "watch": ["./**/*.ts"], "ext": "ts", "ignore": ["node_modules"], "exec": "ts-node-dev server.ts" From 7041e91631f7626b3dae9704f3c6a96c3e240ee4 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Mon, 29 Jan 2024 13:07:10 +0200 Subject: [PATCH 004/114] chore: remove tsconfig comments --- tsconfig.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index f0eb255..6f789d9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,14 +6,12 @@ "module": "NodeNext", "skipLibCheck": true, - /* Bundler mode */ "moduleResolution": "NodeNext", "allowImportingTsExtensions": false, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, - /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, From f2fdf3f43183d7aa2c30b438bf1bcc5295d9ce9c Mon Sep 17 00:00:00 2001 From: Bohdan Date: Mon, 29 Jan 2024 13:07:35 +0200 Subject: [PATCH 005/114] feat: add more env variables --- .env.example | 1 + environment.d.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/.env.example b/.env.example index eb0d788..6f9e4f5 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,2 @@ PORT=YOUR_PORT +NODE_ENV=YOUR_ENV diff --git a/environment.d.ts b/environment.d.ts index fb06342..a345f6f 100644 --- a/environment.d.ts +++ b/environment.d.ts @@ -1,5 +1,6 @@ namespace NodeJS { interface ProcessEnv { PORT: string; + NODE_ENV: string; } } From 6c74abac6280c5ff12b7c32dda8a924324cfa2ab Mon Sep 17 00:00:00 2001 From: Bohdan Date: Mon, 29 Jan 2024 13:07:56 +0200 Subject: [PATCH 006/114] chore: add more npm scripts --- package-lock.json | 22 ++++++++++++++++++++++ package.json | 12 +++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index eedda9b..2214204 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@types/node": "^20.11.10", "@typescript-eslint/eslint-plugin": "^6.19.1", "@typescript-eslint/parser": "^6.19.1", + "cross-env": "^7.0.3", "dotenv": "^16.4.1", "eslint": "^8.56.0", "eslint-config-airbnb": "^19.0.4", @@ -26,6 +27,9 @@ "prettier": "^3.2.4", "ts-node-dev": "^2.0.0", "typescript": "^5.3.3" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -973,6 +977,24 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", diff --git a/package.json b/package.json index 4efb70f..44bb46d 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,17 @@ "version": "1.0.0", "description": "", "main": "server.ts", + "engines": { + "node": ">=20.0.0" + }, "scripts": { - "start": "nodemon" + "start:dev": "nodemon", + "start:prod": "npx cross-env NODE_ENV=production & nodemon", + "lint": "eslint . --ext ts --report-unused-disable-directives --max-warnings 0", + "lint:fix": "eslint --fix --ext ts", + "format": "prettier \"./**/*.{ts,css}\"", + "format:fix": "prettier --write \"./**/*.{ts,css}\"", + "type-check": "tsc --noEmit" }, "author": "Bohdan Shcherbyna", "license": "ISC", @@ -12,6 +21,7 @@ "@types/node": "^20.11.10", "@typescript-eslint/eslint-plugin": "^6.19.1", "@typescript-eslint/parser": "^6.19.1", + "cross-env": "^7.0.3", "dotenv": "^16.4.1", "eslint": "^8.56.0", "eslint-config-airbnb": "^19.0.4", From e536ebbc728b1bd17ebe36e28d9aadca88cfacd1 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Mon, 29 Jan 2024 14:56:27 +0200 Subject: [PATCH 007/114] feat: add test endpoint --- data/data.ts | 10 ++++++++++ server.ts | 21 +++++++++++++++++++++ types/types.ts | 8 ++++++++ 3 files changed, 39 insertions(+) create mode 100644 data/data.ts create mode 100644 types/types.ts diff --git a/data/data.ts b/data/data.ts new file mode 100644 index 0000000..65d93f3 --- /dev/null +++ b/data/data.ts @@ -0,0 +1,10 @@ +import { UserList } from '../types/types'; + +export const users: UserList = [ + { + id: '1', + age: 19, + username: 'Quiddle', + hobbies: ['programming'], + }, +]; diff --git a/server.ts b/server.ts index 0b172b7..8f9c3d0 100644 --- a/server.ts +++ b/server.ts @@ -1 +1,22 @@ import 'dotenv/config'; + +import http from 'http'; +import { URL } from 'url'; + +import { users } from './data/data'; + +const server = http.createServer((req, res) => { + const baseURL = `http://${req.headers.host}/`; + const endpoint = req?.url ?? '/api'; + const { pathname } = new URL(endpoint, baseURL); + + if (pathname === '/api/users') + res + .writeHead(200, { 'Content-type': 'application/json' }) + .end(JSON.stringify(users)); +}); + +const port = Number(process.env.PORT); +server.listen(port, '127.0.0.1', () => { + console.log(`App running on port ${port}...`); +}); diff --git a/types/types.ts b/types/types.ts new file mode 100644 index 0000000..2754824 --- /dev/null +++ b/types/types.ts @@ -0,0 +1,8 @@ +export type User = { + id: string; + username: string; + age: number; + hobbies: string[] | []; +}; + +export type UserList = User[]; From 030cfb860455d944727b9bb7a44425e80923c132 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Mon, 29 Jan 2024 14:58:21 +0200 Subject: [PATCH 008/114] chore: change npm script --- nodemon.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nodemon.json b/nodemon.json index 3adf8bc..9d45c8d 100644 --- a/nodemon.json +++ b/nodemon.json @@ -2,5 +2,5 @@ "watch": ["./**/*.ts"], "ext": "ts", "ignore": ["node_modules"], - "exec": "ts-node-dev server.ts" + "exec": "server.ts" } diff --git a/package.json b/package.json index 44bb46d..fa785dd 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "node": ">=20.0.0" }, "scripts": { - "start:dev": "nodemon", + "start:dev": "ts-node-dev nodemon", "start:prod": "npx cross-env NODE_ENV=production & nodemon", "lint": "eslint . --ext ts --report-unused-disable-directives --max-warnings 0", "lint:fix": "eslint --fix --ext ts", From cc0fe4686f8563d9ffbfeab4bac7450c8e573a95 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Mon, 29 Jan 2024 16:57:21 +0200 Subject: [PATCH 009/114] feat: configure webpack --- .env.example | 1 - .gitignore | 4 + environment.d.ts | 1 - nodemon.json | 3 +- package-lock.json | 1147 ++++++++++++++++++++++++++++++++++++++++++++- package.json | 13 +- tsconfig.json | 4 +- webpack.config.ts | 47 ++ 8 files changed, 1207 insertions(+), 13 deletions(-) create mode 100644 webpack.config.ts diff --git a/.env.example b/.env.example index 6f9e4f5..eb0d788 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1 @@ PORT=YOUR_PORT -NODE_ENV=YOUR_ENV diff --git a/.gitignore b/.gitignore index 5ec221f..154f447 100644 --- a/.gitignore +++ b/.gitignore @@ -131,3 +131,7 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +# Build + +build diff --git a/environment.d.ts b/environment.d.ts index a345f6f..fb06342 100644 --- a/environment.d.ts +++ b/environment.d.ts @@ -1,6 +1,5 @@ namespace NodeJS { interface ProcessEnv { PORT: string; - NODE_ENV: string; } } diff --git a/nodemon.json b/nodemon.json index 9d45c8d..68c9d7d 100644 --- a/nodemon.json +++ b/nodemon.json @@ -1,6 +1,5 @@ { "watch": ["./**/*.ts"], "ext": "ts", - "ignore": ["node_modules"], - "exec": "server.ts" + "ignore": ["node_modules"] } diff --git a/package-lock.json b/package-lock.json index 2214204..5c5a714 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,14 @@ "version": "1.0.0", "license": "ISC", "devDependencies": { + "@types/dotenv-webpack": "^7.0.7", "@types/node": "^20.11.10", + "@types/webpack": "^5.28.5", "@typescript-eslint/eslint-plugin": "^6.19.1", "@typescript-eslint/parser": "^6.19.1", "cross-env": "^7.0.3", "dotenv": "^16.4.1", + "dotenv-webpack": "^8.0.1", "eslint": "^8.56.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb-base": "^15.0.0", @@ -25,8 +28,11 @@ "eslint-plugin-prettier": "^5.1.3", "nodemon": "^3.0.3", "prettier": "^3.2.4", + "ts-loader": "^9.5.1", "ts-node-dev": "^2.0.0", - "typescript": "^5.3.3" + "typescript": "^5.3.3", + "webpack": "^5.90.0", + "webpack-cli": "^5.1.4" }, "engines": { "node": ">=20.0.0" @@ -66,6 +72,15 @@ "node": ">=12" } }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -199,6 +214,20 @@ "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", @@ -208,6 +237,25 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", @@ -295,6 +343,43 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true }, + "node_modules/@types/dotenv-webpack": { + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/@types/dotenv-webpack/-/dotenv-webpack-7.0.7.tgz", + "integrity": "sha512-tltVokFUeYuSjNmHc6N892Asu/JIQcnH2iUF5A29/VKqv9opq6KlrmnKd/Lt/bBikV/z0YN2K0kguTwWirYCMQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "tapable": "^2.2.0", + "webpack": "^5" + } + }, + "node_modules/@types/eslint": { + "version": "8.56.2", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.2.tgz", + "integrity": "sha512-uQDwm1wFHmbBbCZCqAlq6Do9LYwByNZHWzXppSnay9SuwJ+VRbjkbLABer54kcPnMSlG6Fdiy2yaFXm/z9Z5gw==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -334,6 +419,17 @@ "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", "dev": true }, + "node_modules/@types/webpack": { + "version": "5.28.5", + "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-5.28.5.tgz", + "integrity": "sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "tapable": "^2.2.0", + "webpack": "^5" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.19.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.19.1.tgz", @@ -530,6 +626,208 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", + "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", + "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", + "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", + "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-opt": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6", + "@webassemblyjs/wast-printer": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", + "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", + "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", + "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", + "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -548,6 +846,15 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "dev": true, + "peerDependencies": { + "acorn": "^8" + } + }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -582,6 +889,15 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -857,6 +1173,38 @@ "node": ">=8" } }, + "node_modules/browserslist": { + "version": "4.22.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.3.tgz", + "integrity": "sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001580", + "electron-to-chromium": "^1.4.648", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -886,6 +1234,26 @@ "node": ">=6" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001581", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001581.tgz", + "integrity": "sha512-whlTkwhqV2tUmP3oYhtNfaWGYHDdS3JYFQBKXxcUR9qqPWsRhFHhoISO2Xnl/g0xyKzht9mI1LZpiNWfMzHixQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -941,6 +1309,29 @@ "node": ">= 6" } }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -959,6 +1350,18 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1125,6 +1528,39 @@ "url": "https://github.com/motdotla/dotenv?sponsor=1" } }, + "node_modules/dotenv-defaults": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dotenv-defaults/-/dotenv-defaults-2.0.2.tgz", + "integrity": "sha512-iOIzovWfsUHU91L5i8bJce3NYK5JXeAwH50Jh6+ARUdLiiGlYWfGw6UkzsYqaXZH/hjE/eCd/PlfM/qqyK0AMg==", + "dev": true, + "dependencies": { + "dotenv": "^8.2.0" + } + }, + "node_modules/dotenv-defaults/node_modules/dotenv": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", + "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/dotenv-webpack": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/dotenv-webpack/-/dotenv-webpack-8.0.1.tgz", + "integrity": "sha512-CdrgfhZOnx4uB18SgaoP9XHRN2v48BbjuXQsZY5ixs5A8579NxQkmMxRtI7aTwSiSQcM2ao12Fdu+L3ZS3bG4w==", + "dev": true, + "dependencies": { + "dotenv-defaults": "^2.0.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "webpack": "^4 || ^5" + } + }, "node_modules/dynamic-dedupe": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", @@ -1134,6 +1570,12 @@ "xtend": "^4.0.0" } }, + "node_modules/electron-to-chromium": { + "version": "1.4.648", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.648.tgz", + "integrity": "sha512-EmFMarXeqJp9cUKu/QEciEApn0S/xRcpZWuAm32U7NgoZCimjsilKXHRO9saeEW55eHZagIDg6XTUOv32w9pjg==", + "dev": true + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -1141,6 +1583,31 @@ "dev": true, "peer": true }, + "node_modules/enhanced-resolve": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/envinfo": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.0.tgz", + "integrity": "sha512-G9/6xF1FPbIw0TtalAMaVPpiq2aDEuKLXM314jPVAO9r2fo2a4BLqMNkmRS7O/xPPZ+COAhGIz3ETvHEV3eUcg==", + "dev": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/es-abstract": { "version": "1.22.3", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", @@ -1217,6 +1684,12 @@ "safe-array-concat": "^1.0.1" } }, + "node_modules/es-module-lexer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz", + "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==", + "dev": true + }, "node_modules/es-set-tostringtag": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", @@ -1257,6 +1730,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -1926,6 +2408,15 @@ "node": ">=0.10.0" } }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1978,6 +2469,15 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "engines": { + "node": ">= 4.9.1" + } + }, "node_modules/fastq": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.0.tgz", @@ -2027,6 +2527,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, "node_modules/flat-cache": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", @@ -2175,6 +2684,12 @@ "node": ">=10.13.0" } }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, "node_modules/glob/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2259,6 +2774,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -2377,6 +2898,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "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", @@ -2416,10 +2956,19 @@ "node": ">= 0.4" } }, - "node_modules/is-array-buffer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", - "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", "dev": true, "dependencies": { "call-bind": "^1.0.2", @@ -2630,6 +3179,18 @@ "node": ">=8" } }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -2761,6 +3322,15 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/iterator.prototype": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", @@ -2775,6 +3345,35 @@ "set-function-name": "^2.0.1" } }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2800,6 +3399,12 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -2849,6 +3454,15 @@ "json-buffer": "3.0.1" } }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/language-subtag-registry": { "version": "0.3.22", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", @@ -2882,6 +3496,15 @@ "node": ">= 0.8.0" } }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "engines": { + "node": ">=6.11.5" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2934,6 +3557,12 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -2956,6 +3585,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", @@ -3004,6 +3654,18 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, "node_modules/nodemon": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.3.tgz", @@ -3275,6 +3937,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -3329,6 +4000,12 @@ "node": ">=8" } }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -3341,6 +4018,70 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -3424,6 +4165,15 @@ } ] }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -3443,6 +4193,18 @@ "node": ">=8.10.0" } }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", @@ -3517,6 +4279,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3592,6 +4375,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/safe-regex-test": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz", @@ -3609,6 +4412,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -3624,6 +4445,15 @@ "node": ">=10" } }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, "node_modules/set-function-length": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", @@ -3654,6 +4484,18 @@ "node": ">= 0.4" } }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -3868,6 +4710,77 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/terser": { + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz", + "integrity": "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.20", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.26.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", + "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -3919,6 +4832,35 @@ "typescript": ">=4.2.0" } }, + "node_modules/ts-loader": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz", + "integrity": "sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-loader/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -4176,6 +5118,36 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "dev": true }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -4191,6 +5163,165 @@ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "dev": true }, + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.90.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.0.tgz", + "integrity": "sha512-bdmyXRCXeeNIePv6R6tGPyy20aUobw4Zy8r0LUS2EWO+U+Ke/gYDgsCh7bl5rB6jPpr4r0SZa6dPxBxLooDT3w==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.5", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.9.0", + "browserslist": "^4.21.10", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.15.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4284,6 +5415,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index fa785dd..f76b496 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,9 @@ "node": ">=20.0.0" }, "scripts": { - "start:dev": "ts-node-dev nodemon", - "start:prod": "npx cross-env NODE_ENV=production & nodemon", + "start:dev": "ts-node-dev nodemon server.ts", + "start:prod": "npm run build && node ./build/bundle.js", + "build": "webpack --config ./webpack.config.ts --env mode=production", "lint": "eslint . --ext ts --report-unused-disable-directives --max-warnings 0", "lint:fix": "eslint --fix --ext ts", "format": "prettier \"./**/*.{ts,css}\"", @@ -18,11 +19,14 @@ "author": "Bohdan Shcherbyna", "license": "ISC", "devDependencies": { + "@types/dotenv-webpack": "^7.0.7", "@types/node": "^20.11.10", + "@types/webpack": "^5.28.5", "@typescript-eslint/eslint-plugin": "^6.19.1", "@typescript-eslint/parser": "^6.19.1", "cross-env": "^7.0.3", "dotenv": "^16.4.1", + "dotenv-webpack": "^8.0.1", "eslint": "^8.56.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb-base": "^15.0.0", @@ -34,7 +38,10 @@ "eslint-plugin-prettier": "^5.1.3", "nodemon": "^3.0.3", "prettier": "^3.2.4", + "ts-loader": "^9.5.1", "ts-node-dev": "^2.0.0", - "typescript": "^5.3.3" + "typescript": "^5.3.3", + "webpack": "^5.90.0", + "webpack-cli": "^5.1.4" } } diff --git a/tsconfig.json b/tsconfig.json index 6f789d9..5ae0767 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,11 +10,13 @@ "allowImportingTsExtensions": false, "resolveJsonModule": true, "isolatedModules": true, - "noEmit": true, + "noEmit": false, "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, }, } diff --git a/webpack.config.ts b/webpack.config.ts new file mode 100644 index 0000000..b297511 --- /dev/null +++ b/webpack.config.ts @@ -0,0 +1,47 @@ +import { resolve } from 'path'; + +import Dotenv from 'dotenv-webpack'; +import { Configuration } from 'webpack'; + +type Mode = 'production' | 'development'; + +type Envoirment = { + mode: Mode; +}; + +export default (env: Envoirment) => { + const config: Configuration = { + mode: env.mode, + devtool: false, + target: 'node', + entry: resolve(__dirname, 'server.ts'), + output: { + filename: 'bundle.js', + path: resolve(__dirname, 'build'), + }, + module: { + rules: [ + { + test: /\.ts$/, + use: 'ts-loader', + exclude: /node_modules/, + }, + ], + }, + resolve: { + extensions: ['.ts', '.js'], + }, + node: { + global: false, + __filename: false, + __dirname: false, + }, + plugins: [ + new Dotenv({ + safe: true, + }), + ], + }; + + return config; +}; From a52c414e6032363003383e4d5e1625ef600d67eb Mon Sep 17 00:00:00 2001 From: Bohdan Date: Mon, 29 Jan 2024 17:00:23 +0200 Subject: [PATCH 010/114] chore: change app start script --- nodemon.json | 3 ++- package.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/nodemon.json b/nodemon.json index 68c9d7d..3adf8bc 100644 --- a/nodemon.json +++ b/nodemon.json @@ -1,5 +1,6 @@ { "watch": ["./**/*.ts"], "ext": "ts", - "ignore": ["node_modules"] + "ignore": ["node_modules"], + "exec": "ts-node-dev server.ts" } diff --git a/package.json b/package.json index f76b496..6762eeb 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "node": ">=20.0.0" }, "scripts": { - "start:dev": "ts-node-dev nodemon server.ts", + "start:dev": "nodemon", "start:prod": "npm run build && node ./build/bundle.js", "build": "webpack --config ./webpack.config.ts --env mode=production", "lint": "eslint . --ext ts --report-unused-disable-directives --max-warnings 0", From 82061eb14f9e24c7ae61efb35da5aff52b8f45d3 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Mon, 29 Jan 2024 18:02:26 +0200 Subject: [PATCH 011/114] feat: add enums --- types/enums.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 types/enums.ts diff --git a/types/enums.ts b/types/enums.ts new file mode 100644 index 0000000..80b59fa --- /dev/null +++ b/types/enums.ts @@ -0,0 +1,9 @@ +export enum StatusCode { + SUCCESS = 200, + CREATED = 201, + NO_CONTENT = 204, + NOT_FOUND = 404, + BAD_REQUEST = 400, + UNAUTHORIZED = 403, + INTERNAL_SERVER_ERROR = 500, +} From 6a1266dad4522132125002d747dda1222c568ca1 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Mon, 29 Jan 2024 18:02:52 +0200 Subject: [PATCH 012/114] chore: update env config --- .env.example | 1 + environment.d.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/.env.example b/.env.example index eb0d788..2601514 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,2 @@ PORT=YOUR_PORT +HOST=YOUR_HOST diff --git a/environment.d.ts b/environment.d.ts index fb06342..18c097a 100644 --- a/environment.d.ts +++ b/environment.d.ts @@ -1,5 +1,6 @@ namespace NodeJS { interface ProcessEnv { PORT: string; + HOST: string; } } From 627c3e3049fa090fcd85ebed6373dbc43bd11a3a Mon Sep 17 00:00:00 2001 From: Bohdan Date: Mon, 29 Jan 2024 18:03:16 +0200 Subject: [PATCH 013/114] chore: add prettier eslint plugin --- .eslintrc.cjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 1bd784d..c7ff182 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -1,6 +1,6 @@ module.exports = { root: true, - env: { browser: true, es2020: true, es6: true }, + env: { es2020: true, es6: true }, extends: [ 'airbnb', 'airbnb-typescript', @@ -8,7 +8,7 @@ module.exports = { 'plugin:@typescript-eslint/recommended', 'plugin:import/recommended', 'plugin:node/recommended', - 'prettier', + 'plugin:prettier/recommended', ], ignorePatterns: [ '.eslintrc.cjs', From 3b27b51f7186af58510e2aa3807425b9aa24ca7e Mon Sep 17 00:00:00 2001 From: Bohdan Date: Mon, 29 Jan 2024 18:03:43 +0200 Subject: [PATCH 014/114] refactor: move all status code to enum --- server.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/server.ts b/server.ts index 8f9c3d0..b3189b2 100644 --- a/server.ts +++ b/server.ts @@ -1,9 +1,8 @@ import 'dotenv/config'; - import http from 'http'; -import { URL } from 'url'; import { users } from './data/data'; +import { StatusCode } from './types/enums'; const server = http.createServer((req, res) => { const baseURL = `http://${req.headers.host}/`; @@ -12,11 +11,18 @@ const server = http.createServer((req, res) => { if (pathname === '/api/users') res - .writeHead(200, { 'Content-type': 'application/json' }) + .writeHead(StatusCode.SUCCESS, { 'Content-type': 'application/json' }) + .end(JSON.stringify(users)); + + if (pathname === '/api/users') + res + .writeHead(StatusCode.SUCCESS, { 'Content-type': 'application/json' }) .end(JSON.stringify(users)); }); const port = Number(process.env.PORT); -server.listen(port, '127.0.0.1', () => { +const host = process.env.HOST; +server.listen(port, host, () => { + // eslint-disable-next-line no-console console.log(`App running on port ${port}...`); }); From d21ff17212d96a0379da3036f252f05496fc88f6 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Mon, 29 Jan 2024 18:06:19 +0200 Subject: [PATCH 015/114] refactor: remove unused file --- .lintstagedrc.cjs | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 .lintstagedrc.cjs diff --git a/.lintstagedrc.cjs b/.lintstagedrc.cjs deleted file mode 100644 index 18b9785..0000000 --- a/.lintstagedrc.cjs +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - '**/*.ts': [ - 'pnpm lint:fix', - 'pnpm format:fix', - ], -}; From 5e1981ee57674fad52f665a40bc16e9ca745b620 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Mon, 29 Jan 2024 18:57:01 +0200 Subject: [PATCH 016/114] chore: add uuid package --- package-lock.json | 22 ++++++++++++++++++++++ package.json | 4 ++++ 2 files changed, 26 insertions(+) diff --git a/package-lock.json b/package-lock.json index 5c5a714..e3731f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,13 @@ "name": "crud-api", "version": "1.0.0", "license": "ISC", + "dependencies": { + "uuid": "^9.0.1" + }, "devDependencies": { "@types/dotenv-webpack": "^7.0.7", "@types/node": "^20.11.10", + "@types/uuid": "^9.0.8", "@types/webpack": "^5.28.5", "@typescript-eslint/eslint-plugin": "^6.19.1", "@typescript-eslint/parser": "^6.19.1", @@ -419,6 +423,12 @@ "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", "dev": true }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "dev": true + }, "node_modules/@types/webpack": { "version": "5.28.5", "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-5.28.5.tgz", @@ -5157,6 +5167,18 @@ "punycode": "^2.1.0" } }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", diff --git a/package.json b/package.json index 6762eeb..ddb2e30 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "devDependencies": { "@types/dotenv-webpack": "^7.0.7", "@types/node": "^20.11.10", + "@types/uuid": "^9.0.8", "@types/webpack": "^5.28.5", "@typescript-eslint/eslint-plugin": "^6.19.1", "@typescript-eslint/parser": "^6.19.1", @@ -43,5 +44,8 @@ "typescript": "^5.3.3", "webpack": "^5.90.0", "webpack-cli": "^5.1.4" + }, + "dependencies": { + "uuid": "^9.0.1" } } From 552c7b99c83db4470bed48bd6181b44345902efe Mon Sep 17 00:00:00 2001 From: Bohdan Date: Mon, 29 Jan 2024 18:57:16 +0200 Subject: [PATCH 017/114] feat: implement Get users --- data/data.ts | 2 +- server.ts | 29 +++++++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/data/data.ts b/data/data.ts index 65d93f3..376a1c3 100644 --- a/data/data.ts +++ b/data/data.ts @@ -2,7 +2,7 @@ import { UserList } from '../types/types'; export const users: UserList = [ { - id: '1', + id: 'e8e06aa1-cba7-5af0-be15-703774ccfb3b', age: 19, username: 'Quiddle', hobbies: ['programming'], diff --git a/server.ts b/server.ts index b3189b2..b33ed1a 100644 --- a/server.ts +++ b/server.ts @@ -1,6 +1,8 @@ import 'dotenv/config'; import http from 'http'; +import * as uuid from 'uuid'; + import { users } from './data/data'; import { StatusCode } from './types/enums'; @@ -9,15 +11,38 @@ const server = http.createServer((req, res) => { const endpoint = req?.url ?? '/api'; const { pathname } = new URL(endpoint, baseURL); + // Get all users if (pathname === '/api/users') res .writeHead(StatusCode.SUCCESS, { 'Content-type': 'application/json' }) .end(JSON.stringify(users)); + // Get user by id + else if ( + pathname.slice(0, pathname.lastIndexOf('/')) === '/api/users' && + pathname.slice(pathname.lastIndexOf('/') + 1) !== 'users' + ) { + const id = pathname.slice(pathname.lastIndexOf('/') + 1); + + if (!uuid.validate(id)) { + res + .writeHead(StatusCode.BAD_REQUEST) + .end('The provided id is not valid uuid'); + return; + } + + const user = users.filter((usr) => usr.id === id); + + if (!user) { + res.writeHead(StatusCode.NOT_FOUND).end('User not found!'); + return; + } - if (pathname === '/api/users') res .writeHead(StatusCode.SUCCESS, { 'Content-type': 'application/json' }) - .end(JSON.stringify(users)); + .end(JSON.stringify(user)); + } else { + res.writeHead(StatusCode.SUCCESS).end('The route does not exist!'); + } }); const port = Number(process.env.PORT); From 10d8030e935b95d4a3eff32f9c5ca449d2848458 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Mon, 29 Jan 2024 22:30:35 +0200 Subject: [PATCH 018/114] refactor: abstract part of the logic in the api class --- server.ts | 20 ++++++++++++----- types/types.ts | 4 ++++ utils/ApiFeatures.ts | 52 ++++++++++++++++++++++++++++++++++++++++++++ utils/Response.ts | 42 +++++++++++++++++++++++++++++++++++ 4 files changed, 112 insertions(+), 6 deletions(-) create mode 100644 utils/ApiFeatures.ts create mode 100644 utils/Response.ts diff --git a/server.ts b/server.ts index b33ed1a..ba71cfb 100644 --- a/server.ts +++ b/server.ts @@ -5,19 +5,27 @@ import * as uuid from 'uuid'; import { users } from './data/data'; import { StatusCode } from './types/enums'; +import ApiFeatures from './utils/ApiFeatures'; const server = http.createServer((req, res) => { const baseURL = `http://${req.headers.host}/`; const endpoint = req?.url ?? '/api'; const { pathname } = new URL(endpoint, baseURL); + const api = new ApiFeatures(req, res); + + api.route('/api/users').get((_, resp) => { + resp.status(StatusCode.SUCCESS).json(users); + }); + // Get all users - if (pathname === '/api/users') - res - .writeHead(StatusCode.SUCCESS, { 'Content-type': 'application/json' }) - .end(JSON.stringify(users)); - // Get user by id - else if ( + // if (pathname === '/api/users') + // res + // .writeHead(StatusCode.SUCCESS, { 'Content-type': 'application/json' }) + // .end(JSON.stringify(users)); + // // Get user by id + // else + if ( pathname.slice(0, pathname.lastIndexOf('/')) === '/api/users' && pathname.slice(pathname.lastIndexOf('/') + 1) !== 'users' ) { diff --git a/types/types.ts b/types/types.ts index 2754824..6c55a07 100644 --- a/types/types.ts +++ b/types/types.ts @@ -1,3 +1,5 @@ +import { IncomingMessage } from 'http'; + export type User = { id: string; username: string; @@ -6,3 +8,5 @@ export type User = { }; export type UserList = User[]; + +export type Req = IncomingMessage; diff --git a/utils/ApiFeatures.ts b/utils/ApiFeatures.ts new file mode 100644 index 0000000..19a6b1d --- /dev/null +++ b/utils/ApiFeatures.ts @@ -0,0 +1,52 @@ +import { Req } from '../types/types'; +// eslint-disable-next-line import/order +import Response, { JsonFn, Res, StatusFn } from './Response'; + +export type ExtendedRes = Res & { + json: JsonFn; + status: StatusFn; +}; + +class ApiFeatures { + private req: Req; + + private res: ExtendedRes; + + private routeStr: string = ''; + + constructor(req: Req, res: Res) { + const response = new Response(res); + + this.req = req; + this.res = this.extendRes(res, response.json, response.status); + } + + route(route: string) { + this.routeStr = route; + return this; + } + + get(cb: (req: Req, res: ExtendedRes) => void) { + const baseURL = `http://${this.req.headers.host}/`; + const endpoint = this.req?.url ?? ''; + + const { pathname } = new URL(endpoint, baseURL); + + if (this.routeStr === pathname) { + cb(this.req, this.res); + } + } + + private extendRes(res: Res, json: JsonFn, status: StatusFn) { + return Object.defineProperties(res, { + json: { + value: json, + }, + status: { + value: status, + }, + }) as ExtendedRes; + } +} + +export default ApiFeatures; diff --git a/utils/Response.ts b/utils/Response.ts new file mode 100644 index 0000000..abf485b --- /dev/null +++ b/utils/Response.ts @@ -0,0 +1,42 @@ +import { IncomingMessage, ServerResponse } from 'http'; + +import { StatusCode } from '../types/enums'; + +export type Res = ServerResponse & { + req: IncomingMessage; +}; + +export type JsonFn = (body: unknown) => Response; + +export type StatusFn = (status?: StatusCode) => Response; + +class Response { + private statusCode: StatusCode = StatusCode.SUCCESS; + + private res: Res; + + constructor(res: Res) { + this.res = res; + } + + json: JsonFn = (body: unknown) => { + let responseBody = body; + + if (typeof body !== 'string') { + responseBody = JSON.stringify(body); + } + + this.res + .writeHead(this.statusCode, { 'Content-type': 'application/json' }) + .end(responseBody); + + return this; + }; + + status: StatusFn = (status?: StatusCode) => { + if (status) this.statusCode = status; + return this; + }; +} + +export default Response; From aaa0556e090b6c847c07aed26736588ce2f5f726 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Mon, 29 Jan 2024 23:10:07 +0200 Subject: [PATCH 019/114] refactor: encapsulate routes logic in separate class --- server.ts | 63 +++++++++++++++++++------------------------- utils/ApiFeatures.ts | 20 +++----------- utils/Route.ts | 30 +++++++++++++++++++++ 3 files changed, 61 insertions(+), 52 deletions(-) create mode 100644 utils/Route.ts diff --git a/server.ts b/server.ts index ba71cfb..6926a80 100644 --- a/server.ts +++ b/server.ts @@ -1,16 +1,14 @@ import 'dotenv/config'; import http from 'http'; -import * as uuid from 'uuid'; - import { users } from './data/data'; import { StatusCode } from './types/enums'; import ApiFeatures from './utils/ApiFeatures'; const server = http.createServer((req, res) => { - const baseURL = `http://${req.headers.host}/`; - const endpoint = req?.url ?? '/api'; - const { pathname } = new URL(endpoint, baseURL); + // const baseURL = `http://${req.headers.host}/`; + // const endpoint = req?.url ?? '/api'; + // const { pathname } = new URL(endpoint, baseURL); const api = new ApiFeatures(req, res); @@ -18,39 +16,32 @@ const server = http.createServer((req, res) => { resp.status(StatusCode.SUCCESS).json(users); }); - // Get all users - // if (pathname === '/api/users') + // if ( + // pathname.slice(0, pathname.lastIndexOf('/')) === '/api/users' && + // pathname.slice(pathname.lastIndexOf('/') + 1) !== 'users' + // ) { + // const id = pathname.slice(pathname.lastIndexOf('/') + 1); + + // if (!uuid.validate(id)) { + // res + // .writeHead(StatusCode.BAD_REQUEST) + // .end('The provided id is not valid uuid'); + // return; + // } + + // const user = users.filter((usr) => usr.id === id); + + // if (!user) { + // res.writeHead(StatusCode.NOT_FOUND).end('User not found!'); + // return; + // } + // res // .writeHead(StatusCode.SUCCESS, { 'Content-type': 'application/json' }) - // .end(JSON.stringify(users)); - // // Get user by id - // else - if ( - pathname.slice(0, pathname.lastIndexOf('/')) === '/api/users' && - pathname.slice(pathname.lastIndexOf('/') + 1) !== 'users' - ) { - const id = pathname.slice(pathname.lastIndexOf('/') + 1); - - if (!uuid.validate(id)) { - res - .writeHead(StatusCode.BAD_REQUEST) - .end('The provided id is not valid uuid'); - return; - } - - const user = users.filter((usr) => usr.id === id); - - if (!user) { - res.writeHead(StatusCode.NOT_FOUND).end('User not found!'); - return; - } - - res - .writeHead(StatusCode.SUCCESS, { 'Content-type': 'application/json' }) - .end(JSON.stringify(user)); - } else { - res.writeHead(StatusCode.SUCCESS).end('The route does not exist!'); - } + // .end(JSON.stringify(user)); + // } else { + // res.writeHead(StatusCode.SUCCESS).end('The route does not exist!'); + // } }); const port = Number(process.env.PORT); diff --git a/utils/ApiFeatures.ts b/utils/ApiFeatures.ts index 19a6b1d..a39573b 100644 --- a/utils/ApiFeatures.ts +++ b/utils/ApiFeatures.ts @@ -1,6 +1,8 @@ -import { Req } from '../types/types'; // eslint-disable-next-line import/order +import { Req } from '../types/types'; import Response, { JsonFn, Res, StatusFn } from './Response'; +// eslint-disable-next-line import/no-cycle +import Route from './Route'; export type ExtendedRes = Res & { json: JsonFn; @@ -12,8 +14,6 @@ class ApiFeatures { private res: ExtendedRes; - private routeStr: string = ''; - constructor(req: Req, res: Res) { const response = new Response(res); @@ -22,19 +22,7 @@ class ApiFeatures { } route(route: string) { - this.routeStr = route; - return this; - } - - get(cb: (req: Req, res: ExtendedRes) => void) { - const baseURL = `http://${this.req.headers.host}/`; - const endpoint = this.req?.url ?? ''; - - const { pathname } = new URL(endpoint, baseURL); - - if (this.routeStr === pathname) { - cb(this.req, this.res); - } + return new Route(route, this.req, this.res); } private extendRes(res: Res, json: JsonFn, status: StatusFn) { diff --git a/utils/Route.ts b/utils/Route.ts new file mode 100644 index 0000000..96c2e80 --- /dev/null +++ b/utils/Route.ts @@ -0,0 +1,30 @@ +/* eslint-disable */ +import { Req } from '../types/types'; +import { ExtendedRes } from './ApiFeatures'; + +class Route { + private route: string; + + private req: Req; + + private res: ExtendedRes; + + constructor(route: string, req: Req, res: ExtendedRes) { + this.route = route; + this.req = req; + this.res = res; + } + + get(cb: (req: Req, res: ExtendedRes) => void) { + const baseURL = `http://${this.req.headers.host}/`; + const endpoint = this.req?.url ?? ''; + + const { pathname } = new URL(endpoint, baseURL); + + if (this.route === pathname) { + cb(this.req, this.res); + } + } +} + +export default Route; From e2c70283857b1345b6387c3956222cb169d7e618 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Mon, 29 Jan 2024 23:12:03 +0200 Subject: [PATCH 020/114] refactor: rename "ApiFeatures" class to "Api" --- server.ts | 4 ++-- utils/{ApiFeatures.ts => Api.ts} | 4 ++-- utils/Route.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) rename utils/{ApiFeatures.ts => Api.ts} (94%) diff --git a/server.ts b/server.ts index 6926a80..eb4845d 100644 --- a/server.ts +++ b/server.ts @@ -3,14 +3,14 @@ import http from 'http'; import { users } from './data/data'; import { StatusCode } from './types/enums'; -import ApiFeatures from './utils/ApiFeatures'; +import Api from './utils/Api'; const server = http.createServer((req, res) => { // const baseURL = `http://${req.headers.host}/`; // const endpoint = req?.url ?? '/api'; // const { pathname } = new URL(endpoint, baseURL); - const api = new ApiFeatures(req, res); + const api = new Api(req, res); api.route('/api/users').get((_, resp) => { resp.status(StatusCode.SUCCESS).json(users); diff --git a/utils/ApiFeatures.ts b/utils/Api.ts similarity index 94% rename from utils/ApiFeatures.ts rename to utils/Api.ts index a39573b..744cfc3 100644 --- a/utils/ApiFeatures.ts +++ b/utils/Api.ts @@ -9,7 +9,7 @@ export type ExtendedRes = Res & { status: StatusFn; }; -class ApiFeatures { +class Api { private req: Req; private res: ExtendedRes; @@ -37,4 +37,4 @@ class ApiFeatures { } } -export default ApiFeatures; +export default Api; diff --git a/utils/Route.ts b/utils/Route.ts index 96c2e80..b5a779f 100644 --- a/utils/Route.ts +++ b/utils/Route.ts @@ -1,6 +1,6 @@ /* eslint-disable */ import { Req } from '../types/types'; -import { ExtendedRes } from './ApiFeatures'; +import { ExtendedRes } from './Api'; class Route { private route: string; From c734579b157ea636269a66ee1cdbb3866e5bd617 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Tue, 30 Jan 2024 17:50:48 +0200 Subject: [PATCH 021/114] chore: change nodemon script --- nodemon.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodemon.json b/nodemon.json index 3adf8bc..f1dcdce 100644 --- a/nodemon.json +++ b/nodemon.json @@ -2,5 +2,5 @@ "watch": ["./**/*.ts"], "ext": "ts", "ignore": ["node_modules"], - "exec": "ts-node-dev server.ts" + "exec": "ts-node-dev --files server.ts" } From 999918f8ba698400ec9485cfa26408e21a756685 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Tue, 30 Jan 2024 18:02:18 +0200 Subject: [PATCH 022/114] refactor: implement api routes --- server.ts | 65 +++++++++++++++++++++++++------------------------- types/types.ts | 11 +++++++++ utils/Api.ts | 9 +------ utils/Route.ts | 59 ++++++++++++++++++++++++++++++++++++++------- 4 files changed, 94 insertions(+), 50 deletions(-) diff --git a/server.ts b/server.ts index eb4845d..ec08923 100644 --- a/server.ts +++ b/server.ts @@ -1,47 +1,46 @@ import 'dotenv/config'; import http from 'http'; +import * as uuid from 'uuid'; + import { users } from './data/data'; import { StatusCode } from './types/enums'; import Api from './utils/Api'; -const server = http.createServer((req, res) => { - // const baseURL = `http://${req.headers.host}/`; - // const endpoint = req?.url ?? '/api'; - // const { pathname } = new URL(endpoint, baseURL); +// TODO: handle not found route - const api = new Api(req, res); +const server = http.createServer((request, response) => { + const api = new Api(request, response); - api.route('/api/users').get((_, resp) => { - resp.status(StatusCode.SUCCESS).json(users); + api.route('/api/users').get((_, res) => { + res.status(StatusCode.SUCCESS).json(users); }); - // if ( - // pathname.slice(0, pathname.lastIndexOf('/')) === '/api/users' && - // pathname.slice(pathname.lastIndexOf('/') + 1) !== 'users' - // ) { - // const id = pathname.slice(pathname.lastIndexOf('/') + 1); - - // if (!uuid.validate(id)) { - // res - // .writeHead(StatusCode.BAD_REQUEST) - // .end('The provided id is not valid uuid'); - // return; - // } - - // const user = users.filter((usr) => usr.id === id); - - // if (!user) { - // res.writeHead(StatusCode.NOT_FOUND).end('User not found!'); - // return; - // } - - // res - // .writeHead(StatusCode.SUCCESS, { 'Content-type': 'application/json' }) - // .end(JSON.stringify(user)); - // } else { - // res.writeHead(StatusCode.SUCCESS).end('The route does not exist!'); - // } + api.route('/api/users/:id').get((req, res) => { + const { id } = req.route; + + if (!uuid.validate(id)) { + res.status(StatusCode.BAD_REQUEST).json({ + status: 'fail', + message: 'The provided id is not valid uuid', + }); + + return; + } + + const user = users.find((usr) => usr.id === id); + + if (!user) { + res.status(StatusCode.NOT_FOUND).json({ + status: 'fail', + message: 'User not found', + }); + + return; + } + + res.status(StatusCode.SUCCESS).json(user); + }); }); const port = Number(process.env.PORT); diff --git a/types/types.ts b/types/types.ts index 6c55a07..fa5b322 100644 --- a/types/types.ts +++ b/types/types.ts @@ -1,5 +1,7 @@ import { IncomingMessage } from 'http'; +import { JsonFn, Res, StatusFn } from '../utils/Response'; + export type User = { id: string; username: string; @@ -10,3 +12,12 @@ export type User = { export type UserList = User[]; export type Req = IncomingMessage; + +export type ExtendedRes = Res & { + json: JsonFn; + status: StatusFn; +}; + +export type ExtendedReq = Req & { + route: { [key: string]: string }; +}; diff --git a/utils/Api.ts b/utils/Api.ts index 744cfc3..e939327 100644 --- a/utils/Api.ts +++ b/utils/Api.ts @@ -1,13 +1,6 @@ -// eslint-disable-next-line import/order -import { Req } from '../types/types'; import Response, { JsonFn, Res, StatusFn } from './Response'; -// eslint-disable-next-line import/no-cycle import Route from './Route'; - -export type ExtendedRes = Res & { - json: JsonFn; - status: StatusFn; -}; +import { ExtendedRes, Req } from '../types/types'; class Api { private req: Req; diff --git a/utils/Route.ts b/utils/Route.ts index b5a779f..a3daba0 100644 --- a/utils/Route.ts +++ b/utils/Route.ts @@ -1,30 +1,71 @@ -/* eslint-disable */ -import { Req } from '../types/types'; -import { ExtendedRes } from './Api'; +import { ExtendedReq, ExtendedRes, Req } from '../types/types'; class Route { private route: string; - private req: Req; + private req: ExtendedReq; private res: ExtendedRes; + private endpoint: string; + + private baseUrl: string; + constructor(route: string, req: Req, res: ExtendedRes) { this.route = route; - this.req = req; + this.req = this.extendReq(req); this.res = res; + + this.endpoint = this.req?.url ?? ''; + this.baseUrl = `http://${this.req.headers.host}/`; } - get(cb: (req: Req, res: ExtendedRes) => void) { - const baseURL = `http://${this.req.headers.host}/`; - const endpoint = this.req?.url ?? ''; + get(cb: (req: ExtendedReq, res: ExtendedRes) => void) { + if (this.isDynamyc()) { + const routeWithoutId = this.excludeId(this.route); + const endpointWithoutId = this.excludeId(this.endpoint); + const { pathname } = new URL(endpointWithoutId, this.baseUrl); + + if (routeWithoutId === pathname) { + this.injectId(); + cb(this.req, this.res); + } - const { pathname } = new URL(endpoint, baseURL); + return; + } + + const { pathname } = new URL(this.endpoint, this.baseUrl); if (this.route === pathname) { cb(this.req, this.res); } } + + private injectId() { + this.req.route = { + [this.getId(this.route)]: this.getId(this.endpoint), + }; + } + + private getId(url: string) { + return url.slice(this.endpoint.lastIndexOf('/') + 1).replace(':', ''); + } + + private excludeId(url: string) { + return url.slice(0, this.endpoint.lastIndexOf('/')); + } + + private isDynamyc() { + return this.route.slice(this.route.lastIndexOf('/') + 1).startsWith(':'); + } + + private extendReq(req: Req) { + return Object.defineProperty(req, 'route', { + value: {}, + configurable: true, + writable: true, + }); + } } export default Route; From cb6be3a8ef5f4c1fca095800aded37351ceb7813 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Tue, 30 Jan 2024 19:13:44 +0200 Subject: [PATCH 023/114] refactor: change to utilize immutability --- utils/Api.ts | 26 +++++++++++--------------- utils/Response.ts | 6 +++--- utils/Route.ts | 19 +++++++++---------- 3 files changed, 23 insertions(+), 28 deletions(-) diff --git a/utils/Api.ts b/utils/Api.ts index e939327..ecb1b2d 100644 --- a/utils/Api.ts +++ b/utils/Api.ts @@ -1,32 +1,28 @@ -import Response, { JsonFn, Res, StatusFn } from './Response'; +import Response, { Res } from './Response'; import Route from './Route'; import { ExtendedRes, Req } from '../types/types'; class Api { - private req: Req; + private readonly req: Req; - private res: ExtendedRes; + private readonly res: ExtendedRes; constructor(req: Req, res: Res) { - const response = new Response(res); - this.req = req; - this.res = this.extendRes(res, response.json, response.status); + this.res = this.extendRes(res); } route(route: string) { return new Route(route, this.req, this.res); } - private extendRes(res: Res, json: JsonFn, status: StatusFn) { - return Object.defineProperties(res, { - json: { - value: json, - }, - status: { - value: status, - }, - }) as ExtendedRes; + private extendRes(res: Res) { + const response = new Response(res); + return { + ...res, + json: response.json, + status: response.status, + }; } } diff --git a/utils/Response.ts b/utils/Response.ts index abf485b..80f5a96 100644 --- a/utils/Response.ts +++ b/utils/Response.ts @@ -1,8 +1,8 @@ -import { IncomingMessage, ServerResponse } from 'http'; +import {IncomingMessage, ServerResponse} from 'http'; -import { StatusCode } from '../types/enums'; +import {StatusCode} from '../types/enums'; -export type Res = ServerResponse & { +export type Res = ServerResponse & { req: IncomingMessage; }; diff --git a/utils/Route.ts b/utils/Route.ts index a3daba0..1f37c47 100644 --- a/utils/Route.ts +++ b/utils/Route.ts @@ -1,15 +1,15 @@ import { ExtendedReq, ExtendedRes, Req } from '../types/types'; class Route { - private route: string; + private readonly route: string; - private req: ExtendedReq; + private readonly req: ExtendedReq; - private res: ExtendedRes; + private readonly res: ExtendedRes; - private endpoint: string; + private readonly endpoint: string; - private baseUrl: string; + private readonly baseUrl: string; constructor(route: string, req: Req, res: ExtendedRes) { this.route = route; @@ -60,11 +60,10 @@ class Route { } private extendReq(req: Req) { - return Object.defineProperty(req, 'route', { - value: {}, - configurable: true, - writable: true, - }); + return { + ...req, + route: {}, + }; } } From 3ad8ee60ce20e32a8ba5d128410e32a8c27489f0 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Tue, 30 Jan 2024 19:19:11 +0200 Subject: [PATCH 024/114] fix: url assign --- utils/Response.ts | 4 ++-- utils/Route.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/utils/Response.ts b/utils/Response.ts index 80f5a96..8c921ad 100644 --- a/utils/Response.ts +++ b/utils/Response.ts @@ -1,6 +1,6 @@ -import {IncomingMessage, ServerResponse} from 'http'; +import { IncomingMessage, ServerResponse } from 'http'; -import {StatusCode} from '../types/enums'; +import { StatusCode } from '../types/enums'; export type Res = ServerResponse & { req: IncomingMessage; diff --git a/utils/Route.ts b/utils/Route.ts index 1f37c47..f09dd9b 100644 --- a/utils/Route.ts +++ b/utils/Route.ts @@ -16,8 +16,8 @@ class Route { this.req = this.extendReq(req); this.res = res; - this.endpoint = this.req?.url ?? ''; - this.baseUrl = `http://${this.req.headers.host}/`; + this.endpoint = req?.url ?? ''; + this.baseUrl = `http://${req.headers.host}/`; } get(cb: (req: ExtendedReq, res: ExtendedRes) => void) { From 5a350406f683720ea88b7059efc15af2a073528e Mon Sep 17 00:00:00 2001 From: Bohdan Date: Tue, 30 Jan 2024 19:38:18 +0200 Subject: [PATCH 025/114] fix: router issue --- server.ts | 2 +- utils/Route.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server.ts b/server.ts index ec08923..a10b9b0 100644 --- a/server.ts +++ b/server.ts @@ -47,5 +47,5 @@ const port = Number(process.env.PORT); const host = process.env.HOST; server.listen(port, host, () => { // eslint-disable-next-line no-console - console.log(`App running on port ${port}...`); + process.stdout.write(`App running on port ${port}...`); }); diff --git a/utils/Route.ts b/utils/Route.ts index f09dd9b..5c39ab9 100644 --- a/utils/Route.ts +++ b/utils/Route.ts @@ -48,11 +48,11 @@ class Route { } private getId(url: string) { - return url.slice(this.endpoint.lastIndexOf('/') + 1).replace(':', ''); + return url.slice(url.lastIndexOf('/') + 1).replace(':', ''); } private excludeId(url: string) { - return url.slice(0, this.endpoint.lastIndexOf('/')); + return url.slice(0, url.lastIndexOf('/')); } private isDynamyc() { From a15d7b148c18a7f40a3c6670d8ab13b9e7b357e3 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Tue, 30 Jan 2024 22:23:23 +0200 Subject: [PATCH 026/114] move all user related implementation to user controller --- controllers/usersController.ts | 35 +++++++++++++++++++++++++++++++++ server.ts | 36 +++------------------------------- 2 files changed, 38 insertions(+), 33 deletions(-) create mode 100644 controllers/usersController.ts diff --git a/controllers/usersController.ts b/controllers/usersController.ts new file mode 100644 index 0000000..8aed7bd --- /dev/null +++ b/controllers/usersController.ts @@ -0,0 +1,35 @@ +import * as uuid from 'uuid'; + +import { users } from '../data/data'; +import { StatusCode } from '../types/enums'; +import { ExtendedReq, ExtendedRes } from '../types/types'; + +export const getUserList = (_req: ExtendedReq, res: ExtendedRes) => { + res.status(StatusCode.SUCCESS).json(users); +}; + +export const getUser = (req: ExtendedReq, res: ExtendedRes) => { + const { id } = req.route; + + if (!uuid.validate(id)) { + res.status(StatusCode.BAD_REQUEST).json({ + status: 'fail', + message: 'The provided id is not valid uuid', + }); + + return; + } + + const user = users.find((usr) => usr.id === id); + + if (!user) { + res.status(StatusCode.NOT_FOUND).json({ + status: 'fail', + message: 'User not found', + }); + + return; + } + + res.status(StatusCode.SUCCESS).json(user); +}; diff --git a/server.ts b/server.ts index a10b9b0..2d97c36 100644 --- a/server.ts +++ b/server.ts @@ -1,10 +1,7 @@ import 'dotenv/config'; import http from 'http'; -import * as uuid from 'uuid'; - -import { users } from './data/data'; -import { StatusCode } from './types/enums'; +import { getUser, getUserList } from './controllers/usersController'; import Api from './utils/Api'; // TODO: handle not found route @@ -12,35 +9,8 @@ import Api from './utils/Api'; const server = http.createServer((request, response) => { const api = new Api(request, response); - api.route('/api/users').get((_, res) => { - res.status(StatusCode.SUCCESS).json(users); - }); - - api.route('/api/users/:id').get((req, res) => { - const { id } = req.route; - - if (!uuid.validate(id)) { - res.status(StatusCode.BAD_REQUEST).json({ - status: 'fail', - message: 'The provided id is not valid uuid', - }); - - return; - } - - const user = users.find((usr) => usr.id === id); - - if (!user) { - res.status(StatusCode.NOT_FOUND).json({ - status: 'fail', - message: 'User not found', - }); - - return; - } - - res.status(StatusCode.SUCCESS).json(user); - }); + api.route('/api/users').get(getUserList); + api.route('/api/users/:id').get(getUser); }); const port = Number(process.env.PORT); From 600c221a9fdbff899d6e4dbf5d7bc8265c785400 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Tue, 30 Jan 2024 22:28:28 +0200 Subject: [PATCH 027/114] refactor: remove hardcoded types --- utils/Response.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/Response.ts b/utils/Response.ts index 8c921ad..3058291 100644 --- a/utils/Response.ts +++ b/utils/Response.ts @@ -19,7 +19,7 @@ class Response { this.res = res; } - json: JsonFn = (body: unknown) => { + json = (body: unknown) => { let responseBody = body; if (typeof body !== 'string') { From fab65ea9535e82ef19adc19972abdbef4bffd206 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Tue, 30 Jan 2024 23:37:26 +0200 Subject: [PATCH 028/114] feat: implement creating user --- controllers/usersController.ts | 25 +++++++++++++- server.ts | 10 ++++-- types/enums.ts | 5 +++ types/types.ts | 1 + utils/Api.ts | 13 +++++--- utils/Route.ts | 61 +++++++++++++++++++++++++++++----- 6 files changed, 98 insertions(+), 17 deletions(-) diff --git a/controllers/usersController.ts b/controllers/usersController.ts index 8aed7bd..d73e607 100644 --- a/controllers/usersController.ts +++ b/controllers/usersController.ts @@ -2,7 +2,7 @@ import * as uuid from 'uuid'; import { users } from '../data/data'; import { StatusCode } from '../types/enums'; -import { ExtendedReq, ExtendedRes } from '../types/types'; +import { ExtendedReq, ExtendedRes, User } from '../types/types'; export const getUserList = (_req: ExtendedReq, res: ExtendedRes) => { res.status(StatusCode.SUCCESS).json(users); @@ -33,3 +33,26 @@ export const getUser = (req: ExtendedReq, res: ExtendedRes) => { res.status(StatusCode.SUCCESS).json(user); }; + +export const createUser = (req: ExtendedReq, res: ExtendedRes) => { + // TODO: validate body fields + + const { body } = req; + + if (body) { + const user = { id: uuid.v4(), ...body }; + users.push(user as unknown as User); + + res.status(StatusCode.CREATED).json({ + status: 'success', + data: user, + }); + + return; + } + + res.status(StatusCode.BAD_REQUEST).json({ + status: 'fail', + message: 'Cannot create user', + }); +}; diff --git a/server.ts b/server.ts index 2d97c36..58261d3 100644 --- a/server.ts +++ b/server.ts @@ -1,7 +1,11 @@ import 'dotenv/config'; import http from 'http'; -import { getUser, getUserList } from './controllers/usersController'; +import { + createUser, + getUser, + getUserList, +} from './controllers/usersController'; import Api from './utils/Api'; // TODO: handle not found route @@ -9,7 +13,7 @@ import Api from './utils/Api'; const server = http.createServer((request, response) => { const api = new Api(request, response); - api.route('/api/users').get(getUserList); + api.route('/api/users').get(getUserList).post(createUser); api.route('/api/users/:id').get(getUser); }); @@ -17,5 +21,5 @@ const port = Number(process.env.PORT); const host = process.env.HOST; server.listen(port, host, () => { // eslint-disable-next-line no-console - process.stdout.write(`App running on port ${port}...`); + console.log(`App running on port ${port}...`); }); diff --git a/types/enums.ts b/types/enums.ts index 80b59fa..3fd798c 100644 --- a/types/enums.ts +++ b/types/enums.ts @@ -7,3 +7,8 @@ export enum StatusCode { UNAUTHORIZED = 403, INTERNAL_SERVER_ERROR = 500, } + +export enum HttpMethods { + GET = 'GET', + POST = 'POST', +} diff --git a/types/types.ts b/types/types.ts index fa5b322..bc929af 100644 --- a/types/types.ts +++ b/types/types.ts @@ -20,4 +20,5 @@ export type ExtendedRes = Res & { export type ExtendedReq = Req & { route: { [key: string]: string }; + body?: Record; }; diff --git a/utils/Api.ts b/utils/Api.ts index ecb1b2d..0bb817e 100644 --- a/utils/Api.ts +++ b/utils/Api.ts @@ -18,11 +18,14 @@ class Api { private extendRes(res: Res) { const response = new Response(res); - return { - ...res, - json: response.json, - status: response.status, - }; + return Object.defineProperties(res, { + json: { + value: response.json, + }, + status: { + value: response.status, + }, + }); } } diff --git a/utils/Route.ts b/utils/Route.ts index 5c39ab9..c5cab8f 100644 --- a/utils/Route.ts +++ b/utils/Route.ts @@ -1,5 +1,8 @@ +import { HttpMethods } from '../types/enums'; import { ExtendedReq, ExtendedRes, Req } from '../types/types'; +type Cb = (req: ExtendedReq, res: ExtendedRes) => void; + class Route { private readonly route: string; @@ -13,14 +16,18 @@ class Route { constructor(route: string, req: Req, res: ExtendedRes) { this.route = route; - this.req = this.extendReq(req); + this.req = req; this.res = res; this.endpoint = req?.url ?? ''; this.baseUrl = `http://${req.headers.host}/`; + + this.extendReq('route', {}); } - get(cb: (req: ExtendedReq, res: ExtendedRes) => void) { + get(cb: Cb) { + if (this.req.method !== HttpMethods.GET) return this; + if (this.isDynamyc()) { const routeWithoutId = this.excludeId(this.route); const endpointWithoutId = this.excludeId(this.endpoint); @@ -31,14 +38,51 @@ class Route { cb(this.req, this.res); } - return; + return this; + } + + const { pathname } = new URL(this.endpoint, this.baseUrl); + + if (this.route === pathname) { + cb(this.req, this.res); } + return this; + } + + async post(cb: Cb) { + if (this.req.method !== HttpMethods.POST) return this; + const { pathname } = new URL(this.endpoint, this.baseUrl); if (this.route === pathname) { + const body = await this.getBody(); + this.extendReq('body', body); cb(this.req, this.res); } + + return this; + } + + private getBody() { + return new Promise((resolve, reject) => { + const bodyChunks: Uint8Array[] = []; + + this.req + .on('data', (chunk) => { + bodyChunks.push(chunk); + }) + .on('error', (e) => { + reject(e); + }) + .on('end', () => { + const body = Buffer.concat(bodyChunks).toString(); + resolve(JSON.parse(body)); + }) + .on('error', (e) => { + reject(e); + }); + }); } private injectId() { @@ -59,11 +103,12 @@ class Route { return this.route.slice(this.route.lastIndexOf('/') + 1).startsWith(':'); } - private extendReq(req: Req) { - return { - ...req, - route: {}, - }; + private extendReq(field: string, value: unknown) { + return Object.defineProperty(this.req, field, { + value, + writable: true, + configurable: true, + }); } } From 426be711268fff4956852f45430943e97dded2b4 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Wed, 31 Jan 2024 14:06:05 +0200 Subject: [PATCH 029/114] feat: implement response body valdiation & error handling --- controllers/usersController.ts | 37 ++++++++++++++++++-------- models/user/const.ts | 1 + models/user/utils/findMissingFields.ts | 13 +++++++++ models/user/utils/isUser.ts | 12 +++++++++ utils/Route.ts | 19 +++++++++---- 5 files changed, 66 insertions(+), 16 deletions(-) create mode 100644 models/user/const.ts create mode 100644 models/user/utils/findMissingFields.ts create mode 100644 models/user/utils/isUser.ts diff --git a/controllers/usersController.ts b/controllers/usersController.ts index d73e607..3f2e74c 100644 --- a/controllers/usersController.ts +++ b/controllers/usersController.ts @@ -1,6 +1,8 @@ import * as uuid from 'uuid'; import { users } from '../data/data'; +import findMissingFields from '../models/user/utils/findMissingFields'; +import isUser from '../models/user/utils/isUser'; import { StatusCode } from '../types/enums'; import { ExtendedReq, ExtendedRes, User } from '../types/types'; @@ -34,25 +36,38 @@ export const getUser = (req: ExtendedReq, res: ExtendedRes) => { res.status(StatusCode.SUCCESS).json(user); }; -export const createUser = (req: ExtendedReq, res: ExtendedRes) => { - // TODO: validate body fields +export const createUser = ( + req: ExtendedReq, + res: ExtendedRes, + error?: Error, +) => { + if (error) { + res.status(StatusCode.BAD_REQUEST).json({ + status: 'error', + message: error.message, + }); + + return; + } const { body } = req; - if (body) { - const user = { id: uuid.v4(), ...body }; - users.push(user as unknown as User); + if (!body || !isUser(body)) { + const missingFields = findMissingFields(body); - res.status(StatusCode.CREATED).json({ - status: 'success', - data: user, + res.status(StatusCode.BAD_REQUEST).json({ + status: 'fail', + message: `The provided data is missing neccessary fields (${missingFields.join(', ')})`, }); return; } - res.status(StatusCode.BAD_REQUEST).json({ - status: 'fail', - message: 'Cannot create user', + const user = { id: uuid.v4(), ...body }; + users.push(user); + + res.status(StatusCode.CREATED).json({ + status: 'success', + data: user, }); }; diff --git a/models/user/const.ts b/models/user/const.ts new file mode 100644 index 0000000..699e5af --- /dev/null +++ b/models/user/const.ts @@ -0,0 +1 @@ +export const REQUIRED_USER_FIELDS = ['username', 'age', 'hobbies']; diff --git a/models/user/utils/findMissingFields.ts b/models/user/utils/findMissingFields.ts new file mode 100644 index 0000000..28966bc --- /dev/null +++ b/models/user/utils/findMissingFields.ts @@ -0,0 +1,13 @@ +import { REQUIRED_USER_FIELDS } from '../const'; + +const findMissingFields = (body: Record | undefined) => { + if (!body) return REQUIRED_USER_FIELDS; + + return REQUIRED_USER_FIELDS.map((field) => { + const isFieldExist = Object.keys(body).includes(field); + if (isFieldExist) return null; + return field; + }).filter(Boolean); +}; + +export default findMissingFields; diff --git a/models/user/utils/isUser.ts b/models/user/utils/isUser.ts new file mode 100644 index 0000000..a6aa4ed --- /dev/null +++ b/models/user/utils/isUser.ts @@ -0,0 +1,12 @@ +import { User } from '../../../types/types'; +import { REQUIRED_USER_FIELDS } from '../const'; + +const isUser = ( + body: Record | Omit, +): body is Omit => { + return REQUIRED_USER_FIELDS.every((field) => + Object.keys(body).includes(field), + ); +}; + +export default isUser; diff --git a/utils/Route.ts b/utils/Route.ts index c5cab8f..2545520 100644 --- a/utils/Route.ts +++ b/utils/Route.ts @@ -1,7 +1,7 @@ import { HttpMethods } from '../types/enums'; import { ExtendedReq, ExtendedRes, Req } from '../types/types'; -type Cb = (req: ExtendedReq, res: ExtendedRes) => void; +type Cb = (req: ExtendedReq, res: ExtendedRes, error?: Error) => void; class Route { private readonly route: string; @@ -56,9 +56,13 @@ class Route { const { pathname } = new URL(this.endpoint, this.baseUrl); if (this.route === pathname) { - const body = await this.getBody(); - this.extendReq('body', body); - cb(this.req, this.res); + try { + const body = await this.getBody(); + this.extendReq('body', body); + cb(this.req, this.res); + } catch (e) { + cb(this.req, this.res, e as Error); + } } return this; @@ -77,7 +81,12 @@ class Route { }) .on('end', () => { const body = Buffer.concat(bodyChunks).toString(); - resolve(JSON.parse(body)); + + if (body) { + resolve(JSON.parse(body)); + } else { + reject(new Error('The body data does not exist!')); + } }) .on('error', (e) => { reject(e); From 421b3ff369e00b5e32631819c442860668368e81 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Wed, 31 Jan 2024 14:06:43 +0200 Subject: [PATCH 030/114] fix: remove unnecessary import --- controllers/usersController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controllers/usersController.ts b/controllers/usersController.ts index 3f2e74c..7fa8cda 100644 --- a/controllers/usersController.ts +++ b/controllers/usersController.ts @@ -4,7 +4,7 @@ import { users } from '../data/data'; import findMissingFields from '../models/user/utils/findMissingFields'; import isUser from '../models/user/utils/isUser'; import { StatusCode } from '../types/enums'; -import { ExtendedReq, ExtendedRes, User } from '../types/types'; +import { ExtendedReq, ExtendedRes } from '../types/types'; export const getUserList = (_req: ExtendedReq, res: ExtendedRes) => { res.status(StatusCode.SUCCESS).json(users); From 2cd1329551d9b1af00712280f8eabdac8e9d73ff Mon Sep 17 00:00:00 2001 From: Bohdan Date: Wed, 31 Jan 2024 14:10:28 +0200 Subject: [PATCH 031/114] refactor: remove unused code --- controllers/usersController.ts | 17 ++--------------- utils/Route.ts | 14 +++++--------- 2 files changed, 7 insertions(+), 24 deletions(-) diff --git a/controllers/usersController.ts b/controllers/usersController.ts index 7fa8cda..45572b6 100644 --- a/controllers/usersController.ts +++ b/controllers/usersController.ts @@ -36,20 +36,7 @@ export const getUser = (req: ExtendedReq, res: ExtendedRes) => { res.status(StatusCode.SUCCESS).json(user); }; -export const createUser = ( - req: ExtendedReq, - res: ExtendedRes, - error?: Error, -) => { - if (error) { - res.status(StatusCode.BAD_REQUEST).json({ - status: 'error', - message: error.message, - }); - - return; - } - +export const createUser = (req: ExtendedReq, res: ExtendedRes) => { const { body } = req; if (!body || !isUser(body)) { @@ -57,7 +44,7 @@ export const createUser = ( res.status(StatusCode.BAD_REQUEST).json({ status: 'fail', - message: `The provided data is missing neccessary fields (${missingFields.join(', ')})`, + message: `The provided data is missing necessary fields (${missingFields.join(', ')})`, }); return; diff --git a/utils/Route.ts b/utils/Route.ts index 2545520..bbbf720 100644 --- a/utils/Route.ts +++ b/utils/Route.ts @@ -1,7 +1,7 @@ import { HttpMethods } from '../types/enums'; import { ExtendedReq, ExtendedRes, Req } from '../types/types'; -type Cb = (req: ExtendedReq, res: ExtendedRes, error?: Error) => void; +type Cb = (req: ExtendedReq, res: ExtendedRes) => void; class Route { private readonly route: string; @@ -56,13 +56,9 @@ class Route { const { pathname } = new URL(this.endpoint, this.baseUrl); if (this.route === pathname) { - try { - const body = await this.getBody(); - this.extendReq('body', body); - cb(this.req, this.res); - } catch (e) { - cb(this.req, this.res, e as Error); - } + const body = await this.getBody(); + this.extendReq('body', body); + cb(this.req, this.res); } return this; @@ -85,7 +81,7 @@ class Route { if (body) { resolve(JSON.parse(body)); } else { - reject(new Error('The body data does not exist!')); + resolve(null); } }) .on('error', (e) => { From 8c336f96d3be54b0ac724b1d2cbd3310006877fd Mon Sep 17 00:00:00 2001 From: Bohdan Date: Wed, 31 Jan 2024 15:25:32 +0200 Subject: [PATCH 032/114] feat: implement updating user --- controllers/usersController.ts | 52 ++++++++++++++++++++++++++++++++-- server.ts | 3 +- types/enums.ts | 1 + utils/Route.ts | 33 ++++++++++++++++++++- 4 files changed, 85 insertions(+), 4 deletions(-) diff --git a/controllers/usersController.ts b/controllers/usersController.ts index 45572b6..2d651fc 100644 --- a/controllers/usersController.ts +++ b/controllers/usersController.ts @@ -4,7 +4,7 @@ import { users } from '../data/data'; import findMissingFields from '../models/user/utils/findMissingFields'; import isUser from '../models/user/utils/isUser'; import { StatusCode } from '../types/enums'; -import { ExtendedReq, ExtendedRes } from '../types/types'; +import { ExtendedReq, ExtendedRes, User } from '../types/types'; export const getUserList = (_req: ExtendedReq, res: ExtendedRes) => { res.status(StatusCode.SUCCESS).json(users); @@ -44,7 +44,7 @@ export const createUser = (req: ExtendedReq, res: ExtendedRes) => { res.status(StatusCode.BAD_REQUEST).json({ status: 'fail', - message: `The provided data is missing necessary fields (${missingFields.join(', ')})`, + message: `The provided data is missing required fields (${missingFields.join(', ')})`, }); return; @@ -58,3 +58,51 @@ export const createUser = (req: ExtendedReq, res: ExtendedRes) => { data: user, }); }; + +export const updateUser = (req: ExtendedReq, res: ExtendedRes) => { + const { + body, + route: { id }, + } = req; + + // TODO: abstract this logic in middleware + if (!uuid.validate(id)) { + res.status(StatusCode.BAD_REQUEST).json({ + status: 'fail', + message: 'The provided id is not valid uuid', + }); + + return; + } + + if (!body || !isUser(body)) { + const missingFields = findMissingFields(body); + + res.status(StatusCode.BAD_REQUEST).json({ + status: 'fail', + message: `The provided data is missing required fields (${missingFields.join(', ')})`, + }); + + return; + } + + const relatedUser = users.find((usr) => usr.id === id); + const relatedUserIndex = users.findIndex((usr) => usr.id === id); + + if (!relatedUser) { + res.status(StatusCode.NOT_FOUND).json({ + status: 'fail', + message: 'User not found', + }); + + return; + } + + const updatedUser = { ...relatedUser, ...body } as User; + users[relatedUserIndex] = updatedUser; + + res.status(StatusCode.SUCCESS).json({ + status: 'success', + data: updatedUser, + }); +}; diff --git a/server.ts b/server.ts index 58261d3..349e6e1 100644 --- a/server.ts +++ b/server.ts @@ -5,6 +5,7 @@ import { createUser, getUser, getUserList, + updateUser, } from './controllers/usersController'; import Api from './utils/Api'; @@ -14,7 +15,7 @@ const server = http.createServer((request, response) => { const api = new Api(request, response); api.route('/api/users').get(getUserList).post(createUser); - api.route('/api/users/:id').get(getUser); + api.route('/api/users/:id').get(getUser).put(updateUser); }); const port = Number(process.env.PORT); diff --git a/types/enums.ts b/types/enums.ts index 3fd798c..b0e4cc6 100644 --- a/types/enums.ts +++ b/types/enums.ts @@ -11,4 +11,5 @@ export enum StatusCode { export enum HttpMethods { GET = 'GET', POST = 'POST', + PUT = 'PUT', } diff --git a/utils/Route.ts b/utils/Route.ts index bbbf720..05de6c6 100644 --- a/utils/Route.ts +++ b/utils/Route.ts @@ -64,6 +64,35 @@ class Route { return this; } + async put(cb: Cb) { + if (this.req.method !== HttpMethods.PUT) return this; + + if (this.isDynamyc()) { + const routeWithoutId = this.excludeId(this.route); + const endpointWithoutId = this.excludeId(this.endpoint); + const { pathname } = new URL(endpointWithoutId, this.baseUrl); + + if (routeWithoutId === pathname) { + this.injectId(); + const body = await this.getBody(); + this.extendReq('body', body); + cb(this.req, this.res); + } + + return this; + } + + const { pathname } = new URL(this.endpoint, this.baseUrl); + + if (this.route === pathname) { + const body = await this.getBody(); + this.extendReq('body', body); + cb(this.req, this.res); + } + + return this; + } + private getBody() { return new Promise((resolve, reject) => { const bodyChunks: Uint8Array[] = []; @@ -79,7 +108,9 @@ class Route { const body = Buffer.concat(bodyChunks).toString(); if (body) { - resolve(JSON.parse(body)); + const parsed = JSON.parse(body); + delete parsed?.id; + resolve(parsed); } else { resolve(null); } From 09d038d4eeadbbbde85e86a4572dff8dee94a777 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Wed, 31 Jan 2024 15:40:51 +0200 Subject: [PATCH 033/114] fix: public api to not be async --- utils/Route.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/utils/Route.ts b/utils/Route.ts index 05de6c6..63fc40f 100644 --- a/utils/Route.ts +++ b/utils/Route.ts @@ -64,7 +64,7 @@ class Route { return this; } - async put(cb: Cb) { + put(cb: Cb) { if (this.req.method !== HttpMethods.PUT) return this; if (this.isDynamyc()) { @@ -74,9 +74,10 @@ class Route { if (routeWithoutId === pathname) { this.injectId(); - const body = await this.getBody(); - this.extendReq('body', body); - cb(this.req, this.res); + this.getBody().then((body) => { + this.extendReq('body', body); + cb(this.req, this.res); + }); } return this; @@ -85,14 +86,17 @@ class Route { const { pathname } = new URL(this.endpoint, this.baseUrl); if (this.route === pathname) { - const body = await this.getBody(); - this.extendReq('body', body); - cb(this.req, this.res); + this.getBody().then((body) => { + this.extendReq('body', body); + cb(this.req, this.res); + }); } return this; } + delete(/* cb: Cb */) {} + private getBody() { return new Promise((resolve, reject) => { const bodyChunks: Uint8Array[] = []; From 7c0edacda2ae8087026e8898c1c2eef84ca8d9e5 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Wed, 31 Jan 2024 16:12:00 +0200 Subject: [PATCH 034/114] feat: implement deleting user --- controllers/usersController.ts | 32 ++++++++++++++++++++++++++++++++ server.ts | 3 ++- types/enums.ts | 1 + utils/Route.ts | 25 ++++++++++++++++++++++++- 4 files changed, 59 insertions(+), 2 deletions(-) diff --git a/controllers/usersController.ts b/controllers/usersController.ts index 2d651fc..63d061b 100644 --- a/controllers/usersController.ts +++ b/controllers/usersController.ts @@ -106,3 +106,35 @@ export const updateUser = (req: ExtendedReq, res: ExtendedRes) => { data: updatedUser, }); }; + +export const deleteUser = (req: ExtendedReq, res: ExtendedRes) => { + const { id } = req.route; + + // TODO: abstract this logic in middleware + if (!uuid.validate(id)) { + res.status(StatusCode.BAD_REQUEST).json({ + status: 'fail', + message: 'The provided id is not valid uuid', + }); + + return; + } + + const userDeleteIndex = users.findIndex((usr) => usr.id === id); + + if (userDeleteIndex === -1) { + res.status(StatusCode.NOT_FOUND).json({ + status: 'fail', + message: 'User not found', + }); + + return; + } + + delete users[userDeleteIndex]; + + res.status(StatusCode.NO_CONTENT).json({ + status: 'success', + data: null, + }); +}; diff --git a/server.ts b/server.ts index 349e6e1..4de23d0 100644 --- a/server.ts +++ b/server.ts @@ -3,6 +3,7 @@ import http from 'http'; import { createUser, + deleteUser, getUser, getUserList, updateUser, @@ -15,7 +16,7 @@ const server = http.createServer((request, response) => { const api = new Api(request, response); api.route('/api/users').get(getUserList).post(createUser); - api.route('/api/users/:id').get(getUser).put(updateUser); + api.route('/api/users/:id').get(getUser).put(updateUser).delete(deleteUser); }); const port = Number(process.env.PORT); diff --git a/types/enums.ts b/types/enums.ts index b0e4cc6..d12648e 100644 --- a/types/enums.ts +++ b/types/enums.ts @@ -12,4 +12,5 @@ export enum HttpMethods { GET = 'GET', POST = 'POST', PUT = 'PUT', + DELETE = 'DELETE', } diff --git a/utils/Route.ts b/utils/Route.ts index 63fc40f..cdc1509 100644 --- a/utils/Route.ts +++ b/utils/Route.ts @@ -95,7 +95,30 @@ class Route { return this; } - delete(/* cb: Cb */) {} + delete(cb: Cb) { + if (this.req.method !== HttpMethods.DELETE) return this; + + if (this.isDynamyc()) { + const routeWithoutId = this.excludeId(this.route); + const endpointWithoutId = this.excludeId(this.endpoint); + const { pathname } = new URL(endpointWithoutId, this.baseUrl); + + if (routeWithoutId === pathname) { + this.injectId(); + cb(this.req, this.res); + } + + return this; + } + + const { pathname } = new URL(this.endpoint, this.baseUrl); + + if (this.route === pathname) { + cb(this.req, this.res); + } + + return this; + } private getBody() { return new Promise((resolve, reject) => { From 9ea7992fb2c141a8d8d1efca0e79acaf9cc63294 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Wed, 31 Jan 2024 16:55:24 +0200 Subject: [PATCH 035/114] fix: change async/await to then/catch --- utils/Route.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/utils/Route.ts b/utils/Route.ts index cdc1509..f102397 100644 --- a/utils/Route.ts +++ b/utils/Route.ts @@ -1,7 +1,5 @@ import { HttpMethods } from '../types/enums'; -import { ExtendedReq, ExtendedRes, Req } from '../types/types'; - -type Cb = (req: ExtendedReq, res: ExtendedRes) => void; +import { Cb, ExtendedReq, ExtendedRes, Req } from '../types/types'; class Route { private readonly route: string; @@ -50,15 +48,16 @@ class Route { return this; } - async post(cb: Cb) { + post(cb: Cb) { if (this.req.method !== HttpMethods.POST) return this; const { pathname } = new URL(this.endpoint, this.baseUrl); if (this.route === pathname) { - const body = await this.getBody(); - this.extendReq('body', body); - cb(this.req, this.res); + this.getBody().then((body) => { + this.extendReq('body', body); + cb(this.req, this.res); + }); } return this; From 1df50d4dd7bec595984196c648e14f555dd0c496 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Wed, 31 Jan 2024 17:22:26 +0200 Subject: [PATCH 036/114] feat: implement not found route --- controllers/usersController.ts | 9 ++++++++- server.ts | 2 ++ types/types.ts | 2 ++ utils/Api.ts | 4 ++++ 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/controllers/usersController.ts b/controllers/usersController.ts index 63d061b..2761339 100644 --- a/controllers/usersController.ts +++ b/controllers/usersController.ts @@ -4,7 +4,7 @@ import { users } from '../data/data'; import findMissingFields from '../models/user/utils/findMissingFields'; import isUser from '../models/user/utils/isUser'; import { StatusCode } from '../types/enums'; -import { ExtendedReq, ExtendedRes, User } from '../types/types'; +import { ExtendedReq, ExtendedRes, Req, User } from '../types/types'; export const getUserList = (_req: ExtendedReq, res: ExtendedRes) => { res.status(StatusCode.SUCCESS).json(users); @@ -138,3 +138,10 @@ export const deleteUser = (req: ExtendedReq, res: ExtendedRes) => { data: null, }); }; + +export const notFound = (_req: Req, res: ExtendedRes) => { + res.status(StatusCode.NOT_FOUND).json({ + status: 'fail', + message: 'The route does not exist', + }); +}; diff --git a/server.ts b/server.ts index 4de23d0..148cc31 100644 --- a/server.ts +++ b/server.ts @@ -6,6 +6,7 @@ import { deleteUser, getUser, getUserList, + notFound, updateUser, } from './controllers/usersController'; import Api from './utils/Api'; @@ -17,6 +18,7 @@ const server = http.createServer((request, response) => { api.route('/api/users').get(getUserList).post(createUser); api.route('/api/users/:id').get(getUser).put(updateUser).delete(deleteUser); + api.use(notFound); }); const port = Number(process.env.PORT); diff --git a/types/types.ts b/types/types.ts index bc929af..5869e98 100644 --- a/types/types.ts +++ b/types/types.ts @@ -22,3 +22,5 @@ export type ExtendedReq = Req & { route: { [key: string]: string }; body?: Record; }; + +export type Cb = (req: ExtendedReq, res: ExtendedRes) => void; diff --git a/utils/Api.ts b/utils/Api.ts index 0bb817e..a417b21 100644 --- a/utils/Api.ts +++ b/utils/Api.ts @@ -16,6 +16,10 @@ class Api { return new Route(route, this.req, this.res); } + use(cb: (req: Req, res: ExtendedRes) => void) { + if (!this.res.writableEnded) cb(this.req, this.res); + } + private extendRes(res: Res) { const response = new Response(res); return Object.defineProperties(res, { From 791e8c9d3c03440a4514eb6ed1d0a6c3cad0bfce Mon Sep 17 00:00:00 2001 From: Bohdan Date: Fri, 2 Feb 2024 22:36:11 +0200 Subject: [PATCH 037/114] refactor: change api injections stage --- controllers/usersController.ts | 41 +++++----------- models/user/utils/findMissingFields.ts | 3 +- server.ts | 11 +++-- types/types.ts | 6 ++- utils/Api.ts | 24 +++++++-- utils/Response.ts | 2 + utils/Route.ts | 68 ++++++++++++++++++-------- 7 files changed, 95 insertions(+), 60 deletions(-) diff --git a/controllers/usersController.ts b/controllers/usersController.ts index 2761339..a2cffa8 100644 --- a/controllers/usersController.ts +++ b/controllers/usersController.ts @@ -6,6 +6,17 @@ import isUser from '../models/user/utils/isUser'; import { StatusCode } from '../types/enums'; import { ExtendedReq, ExtendedRes, Req, User } from '../types/types'; +export const validateId = (req: ExtendedReq, res: ExtendedRes) => { + const { id } = req.route; + + if (!id || uuid.validate(id)) return; + + res.status(StatusCode.BAD_REQUEST).json({ + status: 'fail', + message: 'The provided id is not valid uuid', + }); +}; + export const getUserList = (_req: ExtendedReq, res: ExtendedRes) => { res.status(StatusCode.SUCCESS).json(users); }; @@ -13,15 +24,6 @@ export const getUserList = (_req: ExtendedReq, res: ExtendedRes) => { export const getUser = (req: ExtendedReq, res: ExtendedRes) => { const { id } = req.route; - if (!uuid.validate(id)) { - res.status(StatusCode.BAD_REQUEST).json({ - status: 'fail', - message: 'The provided id is not valid uuid', - }); - - return; - } - const user = users.find((usr) => usr.id === id); if (!user) { @@ -49,6 +51,7 @@ export const createUser = (req: ExtendedReq, res: ExtendedRes) => { return; } + console.log(body); const user = { id: uuid.v4(), ...body }; users.push(user); @@ -65,16 +68,6 @@ export const updateUser = (req: ExtendedReq, res: ExtendedRes) => { route: { id }, } = req; - // TODO: abstract this logic in middleware - if (!uuid.validate(id)) { - res.status(StatusCode.BAD_REQUEST).json({ - status: 'fail', - message: 'The provided id is not valid uuid', - }); - - return; - } - if (!body || !isUser(body)) { const missingFields = findMissingFields(body); @@ -110,16 +103,6 @@ export const updateUser = (req: ExtendedReq, res: ExtendedRes) => { export const deleteUser = (req: ExtendedReq, res: ExtendedRes) => { const { id } = req.route; - // TODO: abstract this logic in middleware - if (!uuid.validate(id)) { - res.status(StatusCode.BAD_REQUEST).json({ - status: 'fail', - message: 'The provided id is not valid uuid', - }); - - return; - } - const userDeleteIndex = users.findIndex((usr) => usr.id === id); if (userDeleteIndex === -1) { diff --git a/models/user/utils/findMissingFields.ts b/models/user/utils/findMissingFields.ts index 28966bc..621a935 100644 --- a/models/user/utils/findMissingFields.ts +++ b/models/user/utils/findMissingFields.ts @@ -1,6 +1,7 @@ +import { RequestBody } from '../../../types/types'; import { REQUIRED_USER_FIELDS } from '../const'; -const findMissingFields = (body: Record | undefined) => { +const findMissingFields = (body: RequestBody) => { if (!body) return REQUIRED_USER_FIELDS; return REQUIRED_USER_FIELDS.map((field) => { diff --git a/server.ts b/server.ts index 148cc31..9c5ea49 100644 --- a/server.ts +++ b/server.ts @@ -6,8 +6,8 @@ import { deleteUser, getUser, getUserList, - notFound, updateUser, + validateId, } from './controllers/usersController'; import Api from './utils/Api'; @@ -17,8 +17,13 @@ const server = http.createServer((request, response) => { const api = new Api(request, response); api.route('/api/users').get(getUserList).post(createUser); - api.route('/api/users/:id').get(getUser).put(updateUser).delete(deleteUser); - api.use(notFound); + api + .route('/api/users/:id') + .use(validateId) + .get(getUser) + .put(updateUser) + .delete(deleteUser); + // .use(notFound); }); const port = Number(process.env.PORT); diff --git a/types/types.ts b/types/types.ts index 5869e98..1d299c9 100644 --- a/types/types.ts +++ b/types/types.ts @@ -13,14 +13,16 @@ export type UserList = User[]; export type Req = IncomingMessage; +export type RequestBody = Record | null; + export type ExtendedRes = Res & { json: JsonFn; status: StatusFn; }; export type ExtendedReq = Req & { - route: { [key: string]: string }; - body?: Record; + route: { [key: string]: string | null }; + body: RequestBody; }; export type Cb = (req: ExtendedReq, res: ExtendedRes) => void; diff --git a/utils/Api.ts b/utils/Api.ts index a417b21..eba83c5 100644 --- a/utils/Api.ts +++ b/utils/Api.ts @@ -1,23 +1,37 @@ import Response, { Res } from './Response'; import Route from './Route'; -import { ExtendedRes, Req } from '../types/types'; +import { Cb, ExtendedReq, ExtendedRes, Req } from '../types/types'; class Api { - private readonly req: Req; + private readonly req: ExtendedReq; private readonly res: ExtendedRes; constructor(req: Req, res: Res) { - this.req = req; + this.req = req as ExtendedReq; this.res = this.extendRes(res); + + this.extendReq('route', {}); + this.extendReq('body', null); } route(route: string) { return new Route(route, this.req, this.res); } - use(cb: (req: Req, res: ExtendedRes) => void) { - if (!this.res.writableEnded) cb(this.req, this.res); + use(cb: Cb) { + if (!this.res.writableEnded) { + cb(this.req, this.res); + } + return this; + } + + private extendReq(field: string, value: unknown) { + return Object.defineProperty(this.req, field, { + value, + writable: true, + configurable: true, + }); } private extendRes(res: Res) { diff --git a/utils/Response.ts b/utils/Response.ts index 3058291..41a4376 100644 --- a/utils/Response.ts +++ b/utils/Response.ts @@ -20,6 +20,8 @@ class Response { } json = (body: unknown) => { + if (this.res.writableEnded) return this; + let responseBody = body; if (typeof body !== 'string') { diff --git a/utils/Route.ts b/utils/Route.ts index f102397..2423691 100644 --- a/utils/Route.ts +++ b/utils/Route.ts @@ -1,5 +1,5 @@ import { HttpMethods } from '../types/enums'; -import { Cb, ExtendedReq, ExtendedRes, Req } from '../types/types'; +import { Cb, ExtendedReq, ExtendedRes, Req, RequestBody } from '../types/types'; class Route { private readonly route: string; @@ -12,6 +12,10 @@ class Route { private readonly baseUrl: string; + private isPending: boolean = false; + + private middlewareQueue: Cb[] = []; + constructor(route: string, req: Req, res: ExtendedRes) { this.route = route; this.req = req; @@ -20,7 +24,7 @@ class Route { this.endpoint = req?.url ?? ''; this.baseUrl = `http://${req.headers.host}/`; - this.extendReq('route', {}); + this.injectId(); } get(cb: Cb) { @@ -32,7 +36,6 @@ class Route { const { pathname } = new URL(endpointWithoutId, this.baseUrl); if (routeWithoutId === pathname) { - this.injectId(); cb(this.req, this.res); } @@ -55,8 +58,12 @@ class Route { if (this.route === pathname) { this.getBody().then((body) => { - this.extendReq('body', body); + this.req.body = body; cb(this.req, this.res); + this.isPending = false; + this.middlewareQueue.forEach((callback) => + callback(this.req, this.res), + ); }); } @@ -72,11 +79,17 @@ class Route { const { pathname } = new URL(endpointWithoutId, this.baseUrl); if (routeWithoutId === pathname) { - this.injectId(); - this.getBody().then((body) => { - this.extendReq('body', body); - cb(this.req, this.res); - }); + this.getBody() + .then((body) => { + this.req.body = body; + cb(this.req, this.res); + }) + .finally(() => { + this.isPending = false; + this.middlewareQueue.forEach((callback) => + callback(this.req, this.res), + ); + }); } return this; @@ -86,8 +99,12 @@ class Route { if (this.route === pathname) { this.getBody().then((body) => { - this.extendReq('body', body); + this.req.body = body; cb(this.req, this.res); + this.isPending = false; + this.middlewareQueue.forEach((callback) => + callback(this.req, this.res), + ); }); } @@ -103,7 +120,6 @@ class Route { const { pathname } = new URL(endpointWithoutId, this.baseUrl); if (routeWithoutId === pathname) { - this.injectId(); cb(this.req, this.res); } @@ -119,7 +135,19 @@ class Route { return this; } + use(cb: Cb) { + if (!this.res.writableEnded && this.isPending) { + this.middlewareQueue.push(cb); + return this; + } + + if (!this.res.writableEnded) cb(this.req, this.res); + return this; + } + private getBody() { + this.isPending = true; + return new Promise((resolve, reject) => { const bodyChunks: Uint8Array[] = []; @@ -148,8 +176,16 @@ class Route { } private injectId() { + const routeWithoutId = this.excludeId(this.route); + const endpointWithoutId = this.excludeId(this.endpoint); + const { pathname } = new URL(endpointWithoutId, this.baseUrl); + + const isDynamic = routeWithoutId === pathname; + this.req.route = { - [this.getId(this.route)]: this.getId(this.endpoint), + [this.getId(this.route)]: isDynamic + ? this.getId(this.req?.url ?? '') + : null, }; } @@ -164,14 +200,6 @@ class Route { private isDynamyc() { return this.route.slice(this.route.lastIndexOf('/') + 1).startsWith(':'); } - - private extendReq(field: string, value: unknown) { - return Object.defineProperty(this.req, field, { - value, - writable: true, - configurable: true, - }); - } } export default Route; From b40808bbdc8ecfbf354c17febf5e23d8f37d2bfe Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sat, 3 Feb 2024 15:04:52 +0200 Subject: [PATCH 038/114] refactor: change api working logic --- controllers/usersController.ts | 1 - server.ts | 22 +--- utils/Api.ts | 106 ++++++++++++++--- utils/Route.ts | 203 ++++----------------------------- 4 files changed, 124 insertions(+), 208 deletions(-) diff --git a/controllers/usersController.ts b/controllers/usersController.ts index a2cffa8..128d7bf 100644 --- a/controllers/usersController.ts +++ b/controllers/usersController.ts @@ -51,7 +51,6 @@ export const createUser = (req: ExtendedReq, res: ExtendedRes) => { return; } - console.log(body); const user = { id: uuid.v4(), ...body }; users.push(user); diff --git a/server.ts b/server.ts index 9c5ea49..e621006 100644 --- a/server.ts +++ b/server.ts @@ -1,34 +1,24 @@ import 'dotenv/config'; -import http from 'http'; - import { createUser, deleteUser, getUser, getUserList, updateUser, - validateId, + // validateId, } from './controllers/usersController'; import Api from './utils/Api'; // TODO: handle not found route -const server = http.createServer((request, response) => { - const api = new Api(request, response); - - api.route('/api/users').get(getUserList).post(createUser); - api - .route('/api/users/:id') - .use(validateId) - .get(getUser) - .put(updateUser) - .delete(deleteUser); - // .use(notFound); -}); +const api = new Api(); const port = Number(process.env.PORT); const host = process.env.HOST; -server.listen(port, host, () => { +api.listen(port, host, () => { // eslint-disable-next-line no-console console.log(`App running on port ${port}...`); }); + +api.route('/api/users').get(getUserList).post(createUser); +api.route('/api/users/:id').get(getUser).put(updateUser).delete(deleteUser); diff --git a/utils/Api.ts b/utils/Api.ts index eba83c5..bfd38aa 100644 --- a/utils/Api.ts +++ b/utils/Api.ts @@ -1,33 +1,100 @@ +import http from 'http'; + import Response, { Res } from './Response'; import Route from './Route'; +import { HttpMethods } from '../types/enums'; import { Cb, ExtendedReq, ExtendedRes, Req } from '../types/types'; class Api { - private readonly req: ExtendedReq; + private readonly handlersTable: + | Record> + | Record = {}; + + listen(port: number, host: string, cb: () => void) { + http + .createServer((req: Req, res: Res) => { + const method = req.method as HttpMethods | undefined; + const extendedReq = req as ExtendedReq; + const extendedRes = this.extendRes(res); + + this.extendReq(req, 'route', {}); + this.extendReq(req, 'body', null); + + const endpoint = req?.url ?? ''; + const baseUrl = `http://${req.headers.host}/`; + const { pathname } = new URL(endpoint, baseUrl); + let routeHandler = this.handlersTable[pathname]; + let routeEndpoint = ( + Object.keys(this.handlersTable).find((key) => key === pathname) + ); - private readonly res: ExtendedRes; + if (!routeHandler) { + const matchedDynamicRoute = Object.keys( + this.handlersTable, + ).find((key) => { + return ( + pathname.slice(0, pathname.lastIndexOf('/')) === + key.slice(0, key.lastIndexOf('/')) + ); + }); - constructor(req: Req, res: Res) { - this.req = req as ExtendedReq; - this.res = this.extendRes(res); + routeHandler = this.handlersTable[matchedDynamicRoute]; + routeEndpoint = matchedDynamicRoute; + } - this.extendReq('route', {}); - this.extendReq('body', null); + this.injectId(routeEndpoint, endpoint, extendedReq); + this.getBody(extendedReq).then((body) => { + extendedReq.body = >body; + if (routeHandler && method) { + routeHandler[method]?.(extendedReq, extendedRes); + } + }); + }) + .listen(port, host, cb); } route(route: string) { - return new Route(route, this.req, this.res); + return new Route(route, this.handlersTable); } - use(cb: Cb) { - if (!this.res.writableEnded) { - cb(this.req, this.res); + /* + use(cb: Cb, req: ExtendedReq, res: ExtendedRes) { + if (!res.writableEnded) { + cb(req, res); } return this; } + */ - private extendReq(field: string, value: unknown) { - return Object.defineProperty(this.req, field, { + private getBody(req: ExtendedReq) { + return new Promise((resolve, reject) => { + const bodyChunks: Uint8Array[] = []; + req + .on('data', (chunk) => { + bodyChunks.push(chunk); + }) + .on('error', (e) => { + reject(e); + }) + .on('end', () => { + const body = Buffer.concat(bodyChunks).toString(); + + if (body) { + const parsed = JSON.parse(body); + delete parsed?.id; + resolve(parsed); + } else { + resolve(null); + } + }) + .on('error', (e) => { + reject(e); + }); + }); + } + + private extendReq(req: Req, field: string, value: unknown) { + return Object.defineProperty(req, field, { value, writable: true, configurable: true, @@ -45,6 +112,19 @@ class Api { }, }); } + + private injectId(route: string, endpoint: string, req: ExtendedReq) { + const isDynamic = route.includes(':'); + if (!isDynamic) return; + + req.route = { + [this.getId(route)]: this.getId(endpoint), + }; + } + + private getId(url: string) { + return url.slice(url.lastIndexOf('/') + 1).replace(':', ''); + } } export default Api; diff --git a/utils/Route.ts b/utils/Route.ts index 2423691..e2249fe 100644 --- a/utils/Route.ts +++ b/utils/Route.ts @@ -1,204 +1,51 @@ import { HttpMethods } from '../types/enums'; -import { Cb, ExtendedReq, ExtendedRes, Req, RequestBody } from '../types/types'; +import { Cb } from '../types/types'; class Route { private readonly route: string; - private readonly req: ExtendedReq; + private readonly routeTable: + | Record>> + | Record = {}; - private readonly res: ExtendedRes; - - private readonly endpoint: string; - - private readonly baseUrl: string; - - private isPending: boolean = false; - - private middlewareQueue: Cb[] = []; - - constructor(route: string, req: Req, res: ExtendedRes) { + constructor( + route: string, + routeTable: Record> | Record, + ) { this.route = route; - this.req = req; - this.res = res; - - this.endpoint = req?.url ?? ''; - this.baseUrl = `http://${req.headers.host}/`; - - this.injectId(); + this.routeTable = routeTable; } get(cb: Cb) { - if (this.req.method !== HttpMethods.GET) return this; - - if (this.isDynamyc()) { - const routeWithoutId = this.excludeId(this.route); - const endpointWithoutId = this.excludeId(this.endpoint); - const { pathname } = new URL(endpointWithoutId, this.baseUrl); - - if (routeWithoutId === pathname) { - cb(this.req, this.res); - } - - return this; - } - - const { pathname } = new URL(this.endpoint, this.baseUrl); - - if (this.route === pathname) { - cb(this.req, this.res); - } - + this.routeTable[this.route] = { + ...this.routeTable[this.route], + [HttpMethods.GET]: cb, + }; return this; } post(cb: Cb) { - if (this.req.method !== HttpMethods.POST) return this; - - const { pathname } = new URL(this.endpoint, this.baseUrl); - - if (this.route === pathname) { - this.getBody().then((body) => { - this.req.body = body; - cb(this.req, this.res); - this.isPending = false; - this.middlewareQueue.forEach((callback) => - callback(this.req, this.res), - ); - }); - } - + this.routeTable[this.route] = { + ...this.routeTable[this.route], + [HttpMethods.POST]: cb, + }; return this; } put(cb: Cb) { - if (this.req.method !== HttpMethods.PUT) return this; - - if (this.isDynamyc()) { - const routeWithoutId = this.excludeId(this.route); - const endpointWithoutId = this.excludeId(this.endpoint); - const { pathname } = new URL(endpointWithoutId, this.baseUrl); - - if (routeWithoutId === pathname) { - this.getBody() - .then((body) => { - this.req.body = body; - cb(this.req, this.res); - }) - .finally(() => { - this.isPending = false; - this.middlewareQueue.forEach((callback) => - callback(this.req, this.res), - ); - }); - } - - return this; - } - - const { pathname } = new URL(this.endpoint, this.baseUrl); - - if (this.route === pathname) { - this.getBody().then((body) => { - this.req.body = body; - cb(this.req, this.res); - this.isPending = false; - this.middlewareQueue.forEach((callback) => - callback(this.req, this.res), - ); - }); - } - + this.routeTable[this.route] = { + ...this.routeTable[this.route], + [HttpMethods.PUT]: cb, + }; return this; } delete(cb: Cb) { - if (this.req.method !== HttpMethods.DELETE) return this; - - if (this.isDynamyc()) { - const routeWithoutId = this.excludeId(this.route); - const endpointWithoutId = this.excludeId(this.endpoint); - const { pathname } = new URL(endpointWithoutId, this.baseUrl); - - if (routeWithoutId === pathname) { - cb(this.req, this.res); - } - - return this; - } - - const { pathname } = new URL(this.endpoint, this.baseUrl); - - if (this.route === pathname) { - cb(this.req, this.res); - } - - return this; - } - - use(cb: Cb) { - if (!this.res.writableEnded && this.isPending) { - this.middlewareQueue.push(cb); - return this; - } - - if (!this.res.writableEnded) cb(this.req, this.res); - return this; - } - - private getBody() { - this.isPending = true; - - return new Promise((resolve, reject) => { - const bodyChunks: Uint8Array[] = []; - - this.req - .on('data', (chunk) => { - bodyChunks.push(chunk); - }) - .on('error', (e) => { - reject(e); - }) - .on('end', () => { - const body = Buffer.concat(bodyChunks).toString(); - - if (body) { - const parsed = JSON.parse(body); - delete parsed?.id; - resolve(parsed); - } else { - resolve(null); - } - }) - .on('error', (e) => { - reject(e); - }); - }); - } - - private injectId() { - const routeWithoutId = this.excludeId(this.route); - const endpointWithoutId = this.excludeId(this.endpoint); - const { pathname } = new URL(endpointWithoutId, this.baseUrl); - - const isDynamic = routeWithoutId === pathname; - - this.req.route = { - [this.getId(this.route)]: isDynamic - ? this.getId(this.req?.url ?? '') - : null, + this.routeTable[this.route] = { + ...this.routeTable[this.route], + [HttpMethods.DELETE]: cb, }; - } - - private getId(url: string) { - return url.slice(url.lastIndexOf('/') + 1).replace(':', ''); - } - - private excludeId(url: string) { - return url.slice(0, url.lastIndexOf('/')); - } - - private isDynamyc() { - return this.route.slice(this.route.lastIndexOf('/') + 1).startsWith(':'); + return this; } } From 3493e4b519afc0f49452f51dd07bd978d4a37867 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sat, 3 Feb 2024 16:09:34 +0200 Subject: [PATCH 039/114] refactor: implement to run all routes as a middleware --- server.ts | 5 ++- types/types.ts | 9 +++++ utils/Api.ts | 94 ++++++++++++++++++++++++++++++-------------------- utils/Route.ts | 40 ++++++++++++++++++--- 4 files changed, 105 insertions(+), 43 deletions(-) diff --git a/server.ts b/server.ts index e621006..8b591fd 100644 --- a/server.ts +++ b/server.ts @@ -4,8 +4,9 @@ import { deleteUser, getUser, getUserList, + notFound, updateUser, - // validateId, + validateId, } from './controllers/usersController'; import Api from './utils/Api'; @@ -20,5 +21,7 @@ api.listen(port, host, () => { console.log(`App running on port ${port}...`); }); +api.use(validateId); api.route('/api/users').get(getUserList).post(createUser); api.route('/api/users/:id').get(getUser).put(updateUser).delete(deleteUser); +api.use(notFound); diff --git a/types/types.ts b/types/types.ts index 1d299c9..2f5d86f 100644 --- a/types/types.ts +++ b/types/types.ts @@ -1,5 +1,6 @@ import { IncomingMessage } from 'http'; +import { HttpMethods } from './enums'; import { JsonFn, Res, StatusFn } from '../utils/Response'; export type User = { @@ -26,3 +27,11 @@ export type ExtendedReq = Req & { }; export type Cb = (req: ExtendedReq, res: ExtendedRes) => void; + +export type RouteTable = Record>; + +export type MiddlewareQueue = (Cb | RouteTable)[]; + +export type HandlersTable = + | Record> + | Record; diff --git a/utils/Api.ts b/utils/Api.ts index bfd38aa..da45142 100644 --- a/utils/Api.ts +++ b/utils/Api.ts @@ -3,68 +3,88 @@ import http from 'http'; import Response, { Res } from './Response'; import Route from './Route'; import { HttpMethods } from '../types/enums'; -import { Cb, ExtendedReq, ExtendedRes, Req } from '../types/types'; +import { + Cb, + ExtendedReq, + ExtendedRes, + HandlersTable, + MiddlewareQueue, + Req, +} from '../types/types'; class Api { - private readonly handlersTable: - | Record> - | Record = {}; + private readonly handlersTable: HandlersTable = {}; + + private readonly middlewareQueue: MiddlewareQueue = []; listen(port: number, host: string, cb: () => void) { http .createServer((req: Req, res: Res) => { - const method = req.method as HttpMethods | undefined; + const requestHttpMethod = req.method as HttpMethods | undefined; const extendedReq = req as ExtendedReq; const extendedRes = this.extendRes(res); + const endpoint = extendedReq?.url ?? ''; this.extendReq(req, 'route', {}); this.extendReq(req, 'body', null); - const endpoint = req?.url ?? ''; - const baseUrl = `http://${req.headers.host}/`; - const { pathname } = new URL(endpoint, baseUrl); - let routeHandler = this.handlersTable[pathname]; - let routeEndpoint = ( - Object.keys(this.handlersTable).find((key) => key === pathname) - ); - - if (!routeHandler) { - const matchedDynamicRoute = Object.keys( - this.handlersTable, - ).find((key) => { - return ( - pathname.slice(0, pathname.lastIndexOf('/')) === - key.slice(0, key.lastIndexOf('/')) - ); - }); - - routeHandler = this.handlersTable[matchedDynamicRoute]; - routeEndpoint = matchedDynamicRoute; - } - + const { routeHandler, routeEndpoint } = + this.getRouteHandlerAndRouteEndpoint(req); this.injectId(routeEndpoint, endpoint, extendedReq); + this.getBody(extendedReq).then((body) => { extendedReq.body = >body; - if (routeHandler && method) { - routeHandler[method]?.(extendedReq, extendedRes); - } + + this.middlewareQueue.forEach((middleware) => { + if (typeof middleware === 'function') { + middleware(extendedReq, extendedRes); + } else if (routeHandler && requestHttpMethod && routeHandler) { + routeHandler(extendedReq, extendedRes); + } + }); }); }) .listen(port, host, cb); } route(route: string) { - return new Route(route, this.handlersTable); + return new Route(route, this.handlersTable, this.middlewareQueue); } - /* - use(cb: Cb, req: ExtendedReq, res: ExtendedRes) { - if (!res.writableEnded) { - cb(req, res); - } + use(cb: Cb) { + this.middlewareQueue.push(cb); return this; } - */ + + private getRouteHandlerAndRouteEndpoint(req: Req) { + const method = req.method as HttpMethods | undefined; + const endpoint = req?.url ?? ''; + const baseUrl = `http://${req.headers.host}/`; + const { pathname } = new URL(endpoint, baseUrl); + let routeHandler = this.handlersTable[pathname]; + let routeEndpoint = ( + Object.keys(this.handlersTable).find((key) => key === pathname) + ); + + if (!routeHandler) { + const matchedDynamicRoute = Object.keys(this.handlersTable).find( + (key) => { + return ( + pathname.slice(0, pathname.lastIndexOf('/')) === + key.slice(0, key.lastIndexOf('/')) + ); + }, + ); + + routeHandler = this.handlersTable[matchedDynamicRoute]; + routeEndpoint = matchedDynamicRoute; + } + + return { + routeEndpoint, + routeHandler: routeHandler?.[method as HttpMethods], + }; + } private getBody(req: ExtendedReq) { return new Promise((resolve, reject) => { diff --git a/utils/Route.ts b/utils/Route.ts index e2249fe..aa60adf 100644 --- a/utils/Route.ts +++ b/utils/Route.ts @@ -1,19 +1,21 @@ import { HttpMethods } from '../types/enums'; -import { Cb } from '../types/types'; +import { Cb, HandlersTable, MiddlewareQueue, RouteTable } from '../types/types'; class Route { private readonly route: string; - private readonly routeTable: - | Record>> - | Record = {}; + private readonly routeTable: RouteTable; + + private readonly middlewareQueue: MiddlewareQueue; constructor( route: string, - routeTable: Record> | Record, + routeTable: HandlersTable, + middlewareQueue: MiddlewareQueue, ) { this.route = route; this.routeTable = routeTable; + this.middlewareQueue = middlewareQueue; } get(cb: Cb) { @@ -21,6 +23,13 @@ class Route { ...this.routeTable[this.route], [HttpMethods.GET]: cb, }; + + const isRouteTableAlreadyInMiddleware = this.middlewareQueue.some((m) => + Object.is(this.routeTable, m), + ); + + if (isRouteTableAlreadyInMiddleware) return this; + this.middlewareQueue.push(this.routeTable); return this; } @@ -29,6 +38,13 @@ class Route { ...this.routeTable[this.route], [HttpMethods.POST]: cb, }; + + const isRouteTableAlreadyInMiddleware = this.middlewareQueue.some((m) => + Object.is(this.routeTable, m), + ); + + if (isRouteTableAlreadyInMiddleware) return this; + this.middlewareQueue.push(this.routeTable); return this; } @@ -37,6 +53,13 @@ class Route { ...this.routeTable[this.route], [HttpMethods.PUT]: cb, }; + + const isRouteTableAlreadyInMiddleware = this.middlewareQueue.some((m) => + Object.is(this.routeTable, m), + ); + + if (isRouteTableAlreadyInMiddleware) return this; + this.middlewareQueue.push(this.routeTable); return this; } @@ -45,6 +68,13 @@ class Route { ...this.routeTable[this.route], [HttpMethods.DELETE]: cb, }; + + const isRouteTableAlreadyInMiddleware = this.middlewareQueue.some((m) => + Object.is(this.routeTable, m), + ); + + if (isRouteTableAlreadyInMiddleware) return this; + this.middlewareQueue.push(this.routeTable); return this; } } From 7c9f64b84df8b0b27ace82535925a38c0d3ae936 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sat, 3 Feb 2024 16:10:05 +0200 Subject: [PATCH 040/114] refactor: remove TODO --- server.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/server.ts b/server.ts index 8b591fd..a7abfcd 100644 --- a/server.ts +++ b/server.ts @@ -10,8 +10,6 @@ import { } from './controllers/usersController'; import Api from './utils/Api'; -// TODO: handle not found route - const api = new Api(); const port = Number(process.env.PORT); From 0eaab59a4d8be04448c9c74c77fd8080a4038fac Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sat, 3 Feb 2024 16:11:13 +0200 Subject: [PATCH 041/114] refactor: re-arrange server.ts code --- server.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/server.ts b/server.ts index a7abfcd..815fe11 100644 --- a/server.ts +++ b/server.ts @@ -11,15 +11,14 @@ import { import Api from './utils/Api'; const api = new Api(); - const port = Number(process.env.PORT); const host = process.env.HOST; -api.listen(port, host, () => { - // eslint-disable-next-line no-console - console.log(`App running on port ${port}...`); -}); api.use(validateId); api.route('/api/users').get(getUserList).post(createUser); api.route('/api/users/:id').get(getUser).put(updateUser).delete(deleteUser); api.use(notFound); + +api.listen(port, host, () => { + process.stdout.write(`App running on port ${port}...`); +}); From 20eda7b550bfa1e870fde88547119d894245c4c7 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sat, 3 Feb 2024 16:14:39 +0200 Subject: [PATCH 042/114] refactor: change extendRes method api --- utils/Api.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/utils/Api.ts b/utils/Api.ts index da45142..ea611a4 100644 --- a/utils/Api.ts +++ b/utils/Api.ts @@ -21,13 +21,10 @@ class Api { http .createServer((req: Req, res: Res) => { const requestHttpMethod = req.method as HttpMethods | undefined; - const extendedReq = req as ExtendedReq; + const extendedReq = this.extendReq(req); const extendedRes = this.extendRes(res); const endpoint = extendedReq?.url ?? ''; - this.extendReq(req, 'route', {}); - this.extendReq(req, 'body', null); - const { routeHandler, routeEndpoint } = this.getRouteHandlerAndRouteEndpoint(req); this.injectId(routeEndpoint, endpoint, extendedReq); @@ -113,11 +110,14 @@ class Api { }); } - private extendReq(req: Req, field: string, value: unknown) { - return Object.defineProperty(req, field, { - value, - writable: true, - configurable: true, + private extendReq(req: Req) { + return Object.defineProperties(req, { + route: { + value: {}, + }, + body: { + value: null, + }, }); } From 4253e485c9391d0349d3cfa03cc54d32f5330173 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sat, 3 Feb 2024 16:16:16 +0200 Subject: [PATCH 043/114] refactor: code cleanup & readability improve --- utils/Api.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/utils/Api.ts b/utils/Api.ts index ea611a4..5b96d4d 100644 --- a/utils/Api.ts +++ b/utils/Api.ts @@ -20,14 +20,13 @@ class Api { listen(port: number, host: string, cb: () => void) { http .createServer((req: Req, res: Res) => { - const requestHttpMethod = req.method as HttpMethods | undefined; const extendedReq = this.extendReq(req); const extendedRes = this.extendRes(res); - const endpoint = extendedReq?.url ?? ''; + const requestEndpoint = extendedReq?.url ?? ''; const { routeHandler, routeEndpoint } = this.getRouteHandlerAndRouteEndpoint(req); - this.injectId(routeEndpoint, endpoint, extendedReq); + this.injectId(routeEndpoint, requestEndpoint, extendedReq); this.getBody(extendedReq).then((body) => { extendedReq.body = >body; @@ -35,7 +34,7 @@ class Api { this.middlewareQueue.forEach((middleware) => { if (typeof middleware === 'function') { middleware(extendedReq, extendedRes); - } else if (routeHandler && requestHttpMethod && routeHandler) { + } else if (routeHandler && routeHandler) { routeHandler(extendedReq, extendedRes); } }); @@ -54,7 +53,7 @@ class Api { } private getRouteHandlerAndRouteEndpoint(req: Req) { - const method = req.method as HttpMethods | undefined; + const method = req.method; const endpoint = req?.url ?? ''; const baseUrl = `http://${req.headers.host}/`; const { pathname } = new URL(endpoint, baseUrl); From 6a402e003e8dabd55cf190e5d15b57255641dbb8 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sat, 3 Feb 2024 16:28:23 +0200 Subject: [PATCH 044/114] refactor: change api private methods to use this --- utils/Api.ts | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/utils/Api.ts b/utils/Api.ts index 5b96d4d..ade1838 100644 --- a/utils/Api.ts +++ b/utils/Api.ts @@ -17,25 +17,34 @@ class Api { private readonly middlewareQueue: MiddlewareQueue = []; + private req: ExtendedReq = {}; + + private res: ExtendedRes = {}; + listen(port: number, host: string, cb: () => void) { http .createServer((req: Req, res: Res) => { - const extendedReq = this.extendReq(req); - const extendedRes = this.extendRes(res); - const requestEndpoint = extendedReq?.url ?? ''; + this.req = this.extendReq(req); + this.res = this.extendRes(res); + const requestEndpoint = req?.url ?? ''; const { routeHandler, routeEndpoint } = this.getRouteHandlerAndRouteEndpoint(req); - this.injectId(routeEndpoint, requestEndpoint, extendedReq); + this.injectId(routeEndpoint, requestEndpoint); - this.getBody(extendedReq).then((body) => { - extendedReq.body = >body; + // Getting the body is async operation. So we want to get body first, + // then call middlewares or route handlers + this.getBody().then((body) => { + this.req.body = >body; + // using middlewareQueue with routeTable inside. + // In order to make sure that if .use() method called BEFORE any route + // e.g. ID validation, we want it to run right before route handler (get, post, put...) this.middlewareQueue.forEach((middleware) => { if (typeof middleware === 'function') { - middleware(extendedReq, extendedRes); - } else if (routeHandler && routeHandler) { - routeHandler(extendedReq, extendedRes); + middleware(this.req, this.res); + } else if (routeHandler) { + routeHandler(this.req, this.res); } }); }); @@ -82,10 +91,10 @@ class Api { }; } - private getBody(req: ExtendedReq) { + private getBody() { return new Promise((resolve, reject) => { const bodyChunks: Uint8Array[] = []; - req + this.req .on('data', (chunk) => { bodyChunks.push(chunk); }) @@ -132,11 +141,11 @@ class Api { }); } - private injectId(route: string, endpoint: string, req: ExtendedReq) { + private injectId(route: string, endpoint: string) { const isDynamic = route.includes(':'); if (!isDynamic) return; - req.route = { + this.req.route = { [this.getId(route)]: this.getId(endpoint), }; } From edca9a58ff84c7de4aa2f5656d1831621484427a Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sat, 3 Feb 2024 16:29:43 +0200 Subject: [PATCH 045/114] refactor: minor code style changes --- utils/Api.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/utils/Api.ts b/utils/Api.ts index ade1838..83d7487 100644 --- a/utils/Api.ts +++ b/utils/Api.ts @@ -21,15 +21,16 @@ class Api { private res: ExtendedRes = {}; - listen(port: number, host: string, cb: () => void) { + public listen(port: number, host: string, cb: () => void) { http - .createServer((req: Req, res: Res) => { + .createServer((req, res) => { this.req = this.extendReq(req); this.res = this.extendRes(res); - const requestEndpoint = req?.url ?? ''; + const requestEndpoint = req?.url ?? ''; const { routeHandler, routeEndpoint } = this.getRouteHandlerAndRouteEndpoint(req); + this.injectId(routeEndpoint, requestEndpoint); // Getting the body is async operation. So we want to get body first, @@ -52,11 +53,11 @@ class Api { .listen(port, host, cb); } - route(route: string) { + public route(route: string) { return new Route(route, this.handlersTable, this.middlewareQueue); } - use(cb: Cb) { + public use(cb: Cb) { this.middlewareQueue.push(cb); return this; } From 20ffda328bf1c0ea10d1e068cd813e3962c21586 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sat, 3 Feb 2024 17:20:41 +0200 Subject: [PATCH 046/114] refactor: add explicit public keyword --- utils/Response.ts | 8 ++++---- utils/Route.ts | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/utils/Response.ts b/utils/Response.ts index 41a4376..5398580 100644 --- a/utils/Response.ts +++ b/utils/Response.ts @@ -13,13 +13,13 @@ export type StatusFn = (status?: StatusCode) => Response; class Response { private statusCode: StatusCode = StatusCode.SUCCESS; - private res: Res; + private readonly res: Res; - constructor(res: Res) { + public constructor(res: Res) { this.res = res; } - json = (body: unknown) => { + public json = (body: unknown) => { if (this.res.writableEnded) return this; let responseBody = body; @@ -35,7 +35,7 @@ class Response { return this; }; - status: StatusFn = (status?: StatusCode) => { + public status: StatusFn = (status?: StatusCode) => { if (status) this.statusCode = status; return this; }; diff --git a/utils/Route.ts b/utils/Route.ts index aa60adf..fdca9a2 100644 --- a/utils/Route.ts +++ b/utils/Route.ts @@ -8,7 +8,7 @@ class Route { private readonly middlewareQueue: MiddlewareQueue; - constructor( + public constructor( route: string, routeTable: HandlersTable, middlewareQueue: MiddlewareQueue, @@ -18,7 +18,7 @@ class Route { this.middlewareQueue = middlewareQueue; } - get(cb: Cb) { + public get(cb: Cb) { this.routeTable[this.route] = { ...this.routeTable[this.route], [HttpMethods.GET]: cb, @@ -33,7 +33,7 @@ class Route { return this; } - post(cb: Cb) { + public post(cb: Cb) { this.routeTable[this.route] = { ...this.routeTable[this.route], [HttpMethods.POST]: cb, @@ -48,7 +48,7 @@ class Route { return this; } - put(cb: Cb) { + public put(cb: Cb) { this.routeTable[this.route] = { ...this.routeTable[this.route], [HttpMethods.PUT]: cb, @@ -63,7 +63,7 @@ class Route { return this; } - delete(cb: Cb) { + public delete(cb: Cb) { this.routeTable[this.route] = { ...this.routeTable[this.route], [HttpMethods.DELETE]: cb, From 22874b459df4c5d3fd3f9b276f2c47c9e39626c0 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sat, 3 Feb 2024 17:41:34 +0200 Subject: [PATCH 047/114] refactor: abstract body validation into separate middleware --- controllers/usersController.ts | 34 +++++++--------------------------- server.ts | 5 +++-- utils/validateId.ts | 15 +++++++++++++++ 3 files changed, 25 insertions(+), 29 deletions(-) create mode 100644 utils/validateId.ts diff --git a/controllers/usersController.ts b/controllers/usersController.ts index 128d7bf..2e034d7 100644 --- a/controllers/usersController.ts +++ b/controllers/usersController.ts @@ -6,14 +6,16 @@ import isUser from '../models/user/utils/isUser'; import { StatusCode } from '../types/enums'; import { ExtendedReq, ExtendedRes, Req, User } from '../types/types'; -export const validateId = (req: ExtendedReq, res: ExtendedRes) => { - const { id } = req.route; +export const validateBody = (req: ExtendedReq, res: ExtendedRes) => { + const { body } = req; + + if (body && isUser(body)) return; - if (!id || uuid.validate(id)) return; + const missingFields = findMissingFields(body); res.status(StatusCode.BAD_REQUEST).json({ status: 'fail', - message: 'The provided id is not valid uuid', + message: `The provided data is missing required fields (${missingFields.join(', ')})`, }); }; @@ -39,18 +41,7 @@ export const getUser = (req: ExtendedReq, res: ExtendedRes) => { }; export const createUser = (req: ExtendedReq, res: ExtendedRes) => { - const { body } = req; - - if (!body || !isUser(body)) { - const missingFields = findMissingFields(body); - - res.status(StatusCode.BAD_REQUEST).json({ - status: 'fail', - message: `The provided data is missing required fields (${missingFields.join(', ')})`, - }); - - return; - } + const body = req.body as unknown as Omit; const user = { id: uuid.v4(), ...body }; users.push(user); @@ -67,17 +58,6 @@ export const updateUser = (req: ExtendedReq, res: ExtendedRes) => { route: { id }, } = req; - if (!body || !isUser(body)) { - const missingFields = findMissingFields(body); - - res.status(StatusCode.BAD_REQUEST).json({ - status: 'fail', - message: `The provided data is missing required fields (${missingFields.join(', ')})`, - }); - - return; - } - const relatedUser = users.find((usr) => usr.id === id); const relatedUserIndex = users.findIndex((usr) => usr.id === id); diff --git a/server.ts b/server.ts index 815fe11..a8009cd 100644 --- a/server.ts +++ b/server.ts @@ -6,15 +6,16 @@ import { getUserList, notFound, updateUser, - validateId, + validateBody, } from './controllers/usersController'; import Api from './utils/Api'; +import { validateId } from './utils/validateId'; const api = new Api(); const port = Number(process.env.PORT); const host = process.env.HOST; -api.use(validateId); +api.use(validateId).use(validateBody); api.route('/api/users').get(getUserList).post(createUser); api.route('/api/users/:id').get(getUser).put(updateUser).delete(deleteUser); api.use(notFound); diff --git a/utils/validateId.ts b/utils/validateId.ts new file mode 100644 index 0000000..839334d --- /dev/null +++ b/utils/validateId.ts @@ -0,0 +1,15 @@ +import * as uuid from 'uuid'; + +import { StatusCode } from '../types/enums'; +import { ExtendedReq, ExtendedRes } from '../types/types'; + +export const validateId = (req: ExtendedReq, res: ExtendedRes) => { + const { id } = req.route; + + if (!id || uuid.validate(id)) return; + + res.status(StatusCode.BAD_REQUEST).json({ + status: 'fail', + message: 'The provided id is not valid uuid', + }); +}; From 0b7217e2de09c9248b54e1db8fdcc3ed29b42b90 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sat, 3 Feb 2024 17:57:07 +0200 Subject: [PATCH 048/114] refactor: change to not mutate original request/response --- utils/Api.ts | 52 +++++++++++++++++++++++---------------------- utils/validateId.ts | 2 +- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/utils/Api.ts b/utils/Api.ts index 83d7487..32f2f5f 100644 --- a/utils/Api.ts +++ b/utils/Api.ts @@ -10,6 +10,7 @@ import { HandlersTable, MiddlewareQueue, Req, + RequestBody, } from '../types/types'; class Api { @@ -24,19 +25,19 @@ class Api { public listen(port: number, host: string, cb: () => void) { http .createServer((req, res) => { - this.req = this.extendReq(req); - this.res = this.extendRes(res); - const requestEndpoint = req?.url ?? ''; const { routeHandler, routeEndpoint } = this.getRouteHandlerAndRouteEndpoint(req); + this.extendReq(req); + this.extendRes(res); this.injectId(routeEndpoint, requestEndpoint); // Getting the body is async operation. So we want to get body first, // then call middlewares or route handlers this.getBody().then((body) => { - this.req.body = >body; + this.injectBody(body); + console.log(this.req.body, this.req.route); // using middlewareQueue with routeTable inside. // In order to make sure that if .use() method called BEFORE any route @@ -92,7 +93,7 @@ class Api { }; } - private getBody() { + private getBody(): Promise { return new Promise((resolve, reject) => { const bodyChunks: Uint8Array[] = []; this.req @@ -120,34 +121,35 @@ class Api { } private extendReq(req: Req) { - return Object.defineProperties(req, { - route: { - value: {}, - }, - body: { - value: null, - }, - }); + this.req = { + ...req, + route: {}, + body: null, + }; } private extendRes(res: Res) { const response = new Response(res); - return Object.defineProperties(res, { - json: { - value: response.json, - }, - status: { - value: response.status, - }, - }); + this.res = { + ...this.res, + json: response.json, + status: response.status, + }; + } + + private injectBody(body: RequestBody) { + this.req = { ...this.req, body }; } private injectId(route: string, endpoint: string) { - const isDynamic = route.includes(':'); - if (!isDynamic) return; + const isDynamicRoute = route.includes(':'); + if (!isDynamicRoute) return; - this.req.route = { - [this.getId(route)]: this.getId(endpoint), + this.req = { + ...this.req, + route: { + [this.getId(route)]: this.getId(endpoint), + }, }; } diff --git a/utils/validateId.ts b/utils/validateId.ts index 839334d..91293e7 100644 --- a/utils/validateId.ts +++ b/utils/validateId.ts @@ -4,7 +4,7 @@ import { StatusCode } from '../types/enums'; import { ExtendedReq, ExtendedRes } from '../types/types'; export const validateId = (req: ExtendedReq, res: ExtendedRes) => { - const { id } = req.route; + const id = req.route?.id; if (!id || uuid.validate(id)) return; From 3fbe6f42cba5f140ba2aa420668fd7c29cfb5ec5 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sat, 3 Feb 2024 18:40:46 +0200 Subject: [PATCH 049/114] fix: delete user to not left anything --- controllers/usersController.ts | 39 +++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/controllers/usersController.ts b/controllers/usersController.ts index 2e034d7..4ece7ee 100644 --- a/controllers/usersController.ts +++ b/controllers/usersController.ts @@ -6,19 +6,6 @@ import isUser from '../models/user/utils/isUser'; import { StatusCode } from '../types/enums'; import { ExtendedReq, ExtendedRes, Req, User } from '../types/types'; -export const validateBody = (req: ExtendedReq, res: ExtendedRes) => { - const { body } = req; - - if (body && isUser(body)) return; - - const missingFields = findMissingFields(body); - - res.status(StatusCode.BAD_REQUEST).json({ - status: 'fail', - message: `The provided data is missing required fields (${missingFields.join(', ')})`, - }); -}; - export const getUserList = (_req: ExtendedReq, res: ExtendedRes) => { res.status(StatusCode.SUCCESS).json(users); }; @@ -41,7 +28,18 @@ export const getUser = (req: ExtendedReq, res: ExtendedRes) => { }; export const createUser = (req: ExtendedReq, res: ExtendedRes) => { - const body = req.body as unknown as Omit; + const { body } = req; + + if (!body || !isUser(body)) { + const missingFields = findMissingFields(body); + + res.status(StatusCode.BAD_REQUEST).json({ + status: 'fail', + message: `The provided data is missing required fields (${missingFields.join(', ')})`, + }); + + return; + } const user = { id: uuid.v4(), ...body }; users.push(user); @@ -58,6 +56,17 @@ export const updateUser = (req: ExtendedReq, res: ExtendedRes) => { route: { id }, } = req; + if (!body || !isUser(body)) { + const missingFields = findMissingFields(body); + + res.status(StatusCode.BAD_REQUEST).json({ + status: 'fail', + message: `The provided data is missing required fields (${missingFields.join(', ')})`, + }); + + return; + } + const relatedUser = users.find((usr) => usr.id === id); const relatedUserIndex = users.findIndex((usr) => usr.id === id); @@ -93,7 +102,7 @@ export const deleteUser = (req: ExtendedReq, res: ExtendedRes) => { return; } - delete users[userDeleteIndex]; + users.splice(userDeleteIndex, 1); res.status(StatusCode.NO_CONTENT).json({ status: 'success', From e6bb8d4f410e931da99b556e4640e9236a3561b3 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sat, 3 Feb 2024 18:41:05 +0200 Subject: [PATCH 050/114] refactor: change to not validate body in the middleware --- server.ts | 3 +-- utils/Api.ts | 63 +++++++++++++++++++++++++++++----------------------- 2 files changed, 36 insertions(+), 30 deletions(-) diff --git a/server.ts b/server.ts index a8009cd..994de23 100644 --- a/server.ts +++ b/server.ts @@ -6,7 +6,6 @@ import { getUserList, notFound, updateUser, - validateBody, } from './controllers/usersController'; import Api from './utils/Api'; import { validateId } from './utils/validateId'; @@ -15,7 +14,7 @@ const api = new Api(); const port = Number(process.env.PORT); const host = process.env.HOST; -api.use(validateId).use(validateBody); +api.use(validateId); api.route('/api/users').get(getUserList).post(createUser); api.route('/api/users/:id').get(getUser).put(updateUser).delete(deleteUser); api.use(notFound); diff --git a/utils/Api.ts b/utils/Api.ts index 32f2f5f..a70c574 100644 --- a/utils/Api.ts +++ b/utils/Api.ts @@ -25,19 +25,19 @@ class Api { public listen(port: number, host: string, cb: () => void) { http .createServer((req, res) => { + this.req = this.extendReq(req); + this.res = this.extendRes(res); + const requestEndpoint = req?.url ?? ''; const { routeHandler, routeEndpoint } = - this.getRouteHandlerAndRouteEndpoint(req); + this.getRouteHandlerAndRouteEndpoint(); - this.extendReq(req); - this.extendRes(res); this.injectId(routeEndpoint, requestEndpoint); // Getting the body is async operation. So we want to get body first, // then call middlewares or route handlers this.getBody().then((body) => { this.injectBody(body); - console.log(this.req.body, this.req.route); // using middlewareQueue with routeTable inside. // In order to make sure that if .use() method called BEFORE any route @@ -63,15 +63,14 @@ class Api { return this; } - private getRouteHandlerAndRouteEndpoint(req: Req) { - const method = req.method; - const endpoint = req?.url ?? ''; - const baseUrl = `http://${req.headers.host}/`; + private getRouteHandlerAndRouteEndpoint() { + const method = this.req.method; + const endpoint = this.req?.url ?? ''; + const baseUrl = `http://${this.req.headers.host}/`; const { pathname } = new URL(endpoint, baseUrl); let routeHandler = this.handlersTable[pathname]; - let routeEndpoint = ( - Object.keys(this.handlersTable).find((key) => key === pathname) - ); + let routeEndpoint = + Object.keys(this.handlersTable).find((key) => key === pathname) ?? ''; if (!routeHandler) { const matchedDynamicRoute = Object.keys(this.handlersTable).find( @@ -84,7 +83,7 @@ class Api { ); routeHandler = this.handlersTable[matchedDynamicRoute]; - routeEndpoint = matchedDynamicRoute; + routeEndpoint = matchedDynamicRoute ?? ''; } return { @@ -121,35 +120,43 @@ class Api { } private extendReq(req: Req) { - this.req = { - ...req, - route: {}, - body: null, - }; + return Object.defineProperties(req, { + route: { + value: {}, + configurable: true, + writable: true, + }, + body: { + value: null, + configurable: true, + writable: true, + }, + }); } private extendRes(res: Res) { const response = new Response(res); - this.res = { - ...this.res, - json: response.json, - status: response.status, - }; + + return Object.defineProperties(res, { + json: { + value: response.json, + }, + status: { + value: response.status, + }, + }); } private injectBody(body: RequestBody) { - this.req = { ...this.req, body }; + this.req.body = body; } private injectId(route: string, endpoint: string) { const isDynamicRoute = route.includes(':'); if (!isDynamicRoute) return; - this.req = { - ...this.req, - route: { - [this.getId(route)]: this.getId(endpoint), - }, + this.req.route = { + [this.getId(route)]: this.getId(endpoint), }; } From d845105775b22d83ed3deebc56929e9dca32b4d3 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sun, 4 Feb 2024 17:01:42 +0200 Subject: [PATCH 051/114] feat: add vitest --- package-lock.json | 1339 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 5 +- vitest.config.ts | 9 + 3 files changed, 1352 insertions(+), 1 deletion(-) create mode 100644 vitest.config.ts diff --git a/package-lock.json b/package-lock.json index e3731f9..f2e0fbd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "ts-loader": "^9.5.1", "ts-node-dev": "^2.0.0", "typescript": "^5.3.3", + "vitest": "^1.2.2", "webpack": "^5.90.0", "webpack-cli": "^5.1.4" }, @@ -85,6 +86,374 @@ "node": ">=10.0.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -218,6 +587,18 @@ "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", @@ -323,6 +704,181 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.6.tgz", + "integrity": "sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.6.tgz", + "integrity": "sha512-T14aNLpqJ5wzKNf5jEDpv5zgyIqcpn1MlwCrUXLrwoADr2RkWA0vOWP4XxbO9aiO3dvMCQICZdKeDrFl7UMClw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.6.tgz", + "integrity": "sha512-CqNNAyhRkTbo8VVZ5R85X73H3R5NX9ONnKbXuHisGWC0qRbTTxnF1U4V9NafzJbgGM0sHZpdO83pLPzq8uOZFw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.6.tgz", + "integrity": "sha512-zRDtdJuRvA1dc9Mp6BWYqAsU5oeLixdfUvkTHuiYOHwqYuQ4YgSmi6+/lPvSsqc/I0Omw3DdICx4Tfacdzmhog==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.6.tgz", + "integrity": "sha512-oNk8YXDDnNyG4qlNb6is1ojTOGL/tRhbbKeE/YuccItzerEZT68Z9gHrY3ROh7axDc974+zYAPxK5SH0j/G+QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.6.tgz", + "integrity": "sha512-Z3O60yxPtuCYobrtzjo0wlmvDdx2qZfeAWTyfOjEDqd08kthDKexLpV97KfAeUXPosENKd8uyJMRDfFMxcYkDQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.6.tgz", + "integrity": "sha512-gpiG0qQJNdYEVad+1iAsGAbgAnZ8j07FapmnIAQgODKcOTjLEWM9sRb+MbQyVsYCnA0Im6M6QIq6ax7liws6eQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.6.tgz", + "integrity": "sha512-+uCOcvVmFUYvVDr27aiyun9WgZk0tXe7ThuzoUTAukZJOwS5MrGbmSlNOhx1j80GdpqbOty05XqSl5w4dQvcOA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.6.tgz", + "integrity": "sha512-HUNqM32dGzfBKuaDUBqFB7tP6VMN74eLZ33Q9Y1TBqRDn+qDonkAUyKWwF9BR9unV7QUzffLnz9GrnKvMqC/fw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.6.tgz", + "integrity": "sha512-ch7M+9Tr5R4FK40FHQk8VnML0Szi2KRujUgHXd/HjuH9ifH72GUmw6lStZBo3c3GB82vHa0ZoUfjfcM7JiiMrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.6.tgz", + "integrity": "sha512-VD6qnR99dhmTQ1mJhIzXsRcTBvTjbfbGGwKAHcu+52cVl15AC/kplkhxzW/uT0Xl62Y/meBKDZvoJSJN+vTeGA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.6.tgz", + "integrity": "sha512-J9AFDq/xiRI58eR2NIDfyVmTYGyIZmRcvcAoJ48oDld/NTR8wyiPUu2X/v1navJ+N/FGg68LEbX3Ejd6l8B7MQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.6.tgz", + "integrity": "sha512-jqzNLhNDvIZOrt69Ce4UjGRpXJBzhUBzawMwnaDAwyHriki3XollsewxWzOzz+4yOFDkuJHtTsZFwMxhYJWmLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -636,6 +1192,102 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, + "node_modules/@vitest/expect": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.2.2.tgz", + "integrity": "sha512-3jpcdPAD7LwHUUiT2pZTj2U82I2Tcgg2oVPvKxhn6mDI2On6tfvPQTjAI4628GUGDZrCm4Zna9iQHm5cEexOAg==", + "dev": true, + "dependencies": { + "@vitest/spy": "1.2.2", + "@vitest/utils": "1.2.2", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.2.2.tgz", + "integrity": "sha512-JctG7QZ4LSDXr5CsUweFgcpEvrcxOV1Gft7uHrvkQ+fsAVylmWQvnaAr/HDp3LAH1fztGMQZugIheTWjaGzYIg==", + "dev": true, + "dependencies": { + "@vitest/utils": "1.2.2", + "p-limit": "^5.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/p-limit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/runner/node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/snapshot": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.2.2.tgz", + "integrity": "sha512-SmGY4saEw1+bwE1th6S/cZmPxz/Q4JWsl7LvbQIky2tKE35US4gd0Mjzqfr84/4OD0tikGWaWdMja/nWL5NIPA==", + "dev": true, + "dependencies": { + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.2.2.tgz", + "integrity": "sha512-k9Gcahssw8d7X3pSLq3e3XEu/0L78mUkCjivUqCQeXJm9clfXR/Td8+AP+VC1O6fKPIDLcHDTAmBOINVuv6+7g==", + "dev": true, + "dependencies": { + "tinyspy": "^2.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.2.2.tgz", + "integrity": "sha512-WKITBHLsBHlpjnDQahr+XK6RE7MiAsgrIkr0pGhQ9ygoxBfUeG0lUG5iLlzqjmKSlBv3+j5EGsriBzh+C3Tq9g==", + "dev": true, + "dependencies": { + "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", @@ -1098,6 +1750,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -1221,6 +1882,15 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", @@ -1264,6 +1934,24 @@ } ] }, + "node_modules/chai": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", + "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.0.8" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1280,6 +1968,18 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -1446,6 +2146,18 @@ } } }, + "node_modules/deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1502,6 +2214,15 @@ "node": ">=0.3.1" } }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -1740,6 +2461,44 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -2409,6 +3168,15 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -2427,6 +3195,29 @@ "node": ">=0.8.x" } }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2631,6 +3422,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/get-intrinsic": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", @@ -2646,6 +3446,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-symbol-description": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", @@ -2877,6 +3689,15 @@ "node": ">= 0.4" } }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, "node_modules/ignore": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", @@ -3239,6 +4060,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-string": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", @@ -3439,6 +4272,12 @@ "json5": "lib/cli.js" } }, + "node_modules/jsonc-parser": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", + "dev": true + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -3515,6 +4354,22 @@ "node": ">=6.11.5" } }, + "node_modules/local-pkg": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", + "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", + "dev": true, + "dependencies": { + "mlly": "^1.4.2", + "pkg-types": "^1.0.3" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -3549,6 +4404,15 @@ "loose-envify": "cli.js" } }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.1" + } + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -3561,6 +4425,18 @@ "node": ">=10" } }, + "node_modules/magic-string": { + "version": "0.30.6", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.6.tgz", + "integrity": "sha512-n62qCLbPjNjyo+owKtveQxZFZTBm+Ms6YoGD23Wew6Vw337PElFNifQpknPruVRQV57kVShPnLGo9vWxVhpPvA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -3616,6 +4492,18 @@ "node": ">= 0.6" } }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", @@ -3652,12 +4540,42 @@ "node": ">=10" } }, + "node_modules/mlly": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.5.0.tgz", + "integrity": "sha512-NPVQvAY1xr1QoVeG0cy8yUYC7FQcOx6evl/RjT1wL5FvzPnzOysoqB/jmx/DhssT2dYa8nxECLAaFI/+gVLhDQ==", + "dev": true, + "dependencies": { + "acorn": "^8.11.3", + "pathe": "^1.1.2", + "pkg-types": "^1.0.3", + "ufo": "^1.3.2" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -3771,6 +4689,33 @@ "node": ">=0.10.0" } }, + "node_modules/npm-run-path": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz", + "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -3900,6 +4845,21 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -4010,6 +4970,21 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -4092,6 +5067,45 @@ "node": ">=8" } }, + "node_modules/pkg-types": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", + "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", + "dev": true, + "dependencies": { + "jsonc-parser": "^3.2.0", + "mlly": "^1.2.0", + "pathe": "^1.1.0" + } + }, + "node_modules/postcss": { + "version": "8.4.33", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", + "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -4128,6 +5142,38 @@ "node": ">=6.0.0" } }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -4344,6 +5390,38 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rollup": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.6.tgz", + "integrity": "sha512-05lzkCS2uASX0CiLFybYfVkwNbKZG5NFQ6Go0VWyogFTXXbR039UVsegViTntkk4OglHBdF54ccApXRRuXRbsg==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.9.6", + "@rollup/rollup-android-arm64": "4.9.6", + "@rollup/rollup-darwin-arm64": "4.9.6", + "@rollup/rollup-darwin-x64": "4.9.6", + "@rollup/rollup-linux-arm-gnueabihf": "4.9.6", + "@rollup/rollup-linux-arm64-gnu": "4.9.6", + "@rollup/rollup-linux-arm64-musl": "4.9.6", + "@rollup/rollup-linux-riscv64-gnu": "4.9.6", + "@rollup/rollup-linux-x64-gnu": "4.9.6", + "@rollup/rollup-linux-x64-musl": "4.9.6", + "@rollup/rollup-win32-arm64-msvc": "4.9.6", + "@rollup/rollup-win32-ia32-msvc": "4.9.6", + "@rollup/rollup-win32-x64-msvc": "4.9.6", + "fsevents": "~2.3.2" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -4541,6 +5619,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -4571,6 +5667,15 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -4581,6 +5686,18 @@ "source-map": "^0.6.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "node_modules/std-env": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", + "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", + "dev": true + }, "node_modules/string.prototype.matchall": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz", @@ -4668,6 +5785,18 @@ "node": ">=4" } }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -4680,6 +5809,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz", + "integrity": "sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==", + "dev": true, + "dependencies": { + "acorn": "^8.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -4797,6 +5938,30 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/tinybench": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.6.0.tgz", + "integrity": "sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==", + "dev": true + }, + "node_modules/tinypool": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.2.tgz", + "integrity": "sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz", + "integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5011,6 +6176,15 @@ "node": ">= 0.8.0" } }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -5101,6 +6275,12 @@ "node": ">=14.17" } }, + "node_modules/ufo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.2.tgz", + "integrity": "sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==", + "dev": true + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -5185,6 +6365,149 @@ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "dev": true }, + "node_modules/vite": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz", + "integrity": "sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==", + "dev": true, + "dependencies": { + "esbuild": "^0.19.3", + "postcss": "^8.4.32", + "rollup": "^4.2.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.2.2.tgz", + "integrity": "sha512-1as4rDTgVWJO3n1uHmUYqq7nsFgINQ9u+mRcXpjeOMJUmviqNKjcZB7UfRZrlM7MjYXMKpuWp5oGkjaFLnjawg==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.2.2.tgz", + "integrity": "sha512-d5Ouvrnms3GD9USIK36KG8OZ5bEvKEkITFtnGv56HFaSlbItJuYr7hv2Lkn903+AvRAgSixiamozUVfORUekjw==", + "dev": true, + "dependencies": { + "@vitest/expect": "1.2.2", + "@vitest/runner": "1.2.2", + "@vitest/snapshot": "1.2.2", + "@vitest/spy": "1.2.2", + "@vitest/utils": "1.2.2", + "acorn-walk": "^8.3.2", + "cac": "^6.7.14", + "chai": "^4.3.10", + "debug": "^4.3.4", + "execa": "^8.0.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^1.3.0", + "tinybench": "^2.5.1", + "tinypool": "^0.8.2", + "vite": "^5.0.0", + "vite-node": "1.2.2", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "^1.0.0", + "@vitest/ui": "^1.0.0", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", @@ -5437,6 +6760,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", + "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wildcard": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", diff --git a/package.json b/package.json index ddb2e30..3730a29 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "description": "", "main": "server.ts", + "type": "module", "engines": { "node": ">=20.0.0" }, @@ -14,7 +15,8 @@ "lint:fix": "eslint --fix --ext ts", "format": "prettier \"./**/*.{ts,css}\"", "format:fix": "prettier --write \"./**/*.{ts,css}\"", - "type-check": "tsc --noEmit" + "type-check": "tsc --noEmit", + "test": "vitest --config ./vitest.config.ts" }, "author": "Bohdan Shcherbyna", "license": "ISC", @@ -42,6 +44,7 @@ "ts-loader": "^9.5.1", "ts-node-dev": "^2.0.0", "typescript": "^5.3.3", + "vitest": "^1.2.2", "webpack": "^5.90.0", "webpack-cli": "^5.1.4" }, diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..b09ee9c --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,9 @@ +/// +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + css: false, + }, +}); From 1ba5d4f1d4b34e9cf0bcbd8c5c67f2226a14666f Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sun, 4 Feb 2024 17:23:17 +0200 Subject: [PATCH 052/114] fix: import issues --- .eslintrc.cjs | 7 ++++++- tsconfig.json | 8 +++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index c7ff182..20ec087 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -72,7 +72,12 @@ module.exports = { settings: { 'import/resolver': { node: { - extensions: ['.ts'], + extensions: ['.js', '.jsx', '.ts', '.tsx'], + moduleDirectory: ['node_modules', './'], + }, + typescript: { + alwaysTryTypes: true, + project: 'tsconfig.json', }, }, }, diff --git a/tsconfig.json b/tsconfig.json index 5ae0767..4f50a28 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,12 +1,12 @@ { "compilerOptions": { - "target": "ESNext", + "target": "es5", "useDefineForClassFields": true, "lib": ["ESNext"], - "module": "NodeNext", + "module": "esnext", "skipLibCheck": true, - "moduleResolution": "NodeNext", + "moduleResolution": "bundler", "allowImportingTsExtensions": false, "resolveJsonModule": true, "isolatedModules": true, @@ -19,4 +19,6 @@ "allowSyntheticDefaultImports": true, "esModuleInterop": true, }, + "include": ["**/*.ts"], + "exclude": ["node_modules"] } From b4123f527986fd16b2715e8f56486a259c9598fb Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sun, 4 Feb 2024 17:23:33 +0200 Subject: [PATCH 053/114] chore: add typescript import resolver --- data/data.ts | 11 ++--------- package-lock.json | 47 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + test/sum.test.ts | 11 +++++++++++ types/types.ts | 4 ++-- 5 files changed, 63 insertions(+), 11 deletions(-) create mode 100644 test/sum.test.ts diff --git a/data/data.ts b/data/data.ts index 376a1c3..3b15366 100644 --- a/data/data.ts +++ b/data/data.ts @@ -1,10 +1,3 @@ -import { UserList } from '../types/types'; +import { UserList } from '../types/types.js'; -export const users: UserList = [ - { - id: 'e8e06aa1-cba7-5af0-be15-703774ccfb3b', - age: 19, - username: 'Quiddle', - hobbies: ['programming'], - }, -]; +export const users: UserList = []; diff --git a/package-lock.json b/package-lock.json index f2e0fbd..7b4b7a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "eslint-config-airbnb-typescript": "^17.1.0", "eslint-config-prettier": "^9.1.0", "eslint-import-resolver-node": "^0.3.9", + "eslint-import-resolver-typescript": "^3.6.1", "eslint-plugin-import": "^2.29.1", "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^5.1.3", @@ -2671,6 +2672,31 @@ "ms": "^2.1.1" } }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.1.tgz", + "integrity": "sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "enhanced-resolve": "^5.12.0", + "eslint-module-utils": "^2.7.4", + "fast-glob": "^3.3.1", + "get-tsconfig": "^4.5.0", + "is-core-module": "^2.11.0", + "is-glob": "^4.0.3" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*" + } + }, "node_modules/eslint-module-utils": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", @@ -3474,6 +3500,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.2.tgz", + "integrity": "sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==", + "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -5365,6 +5403,15 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", diff --git a/package.json b/package.json index 3730a29..b805f43 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "eslint-config-airbnb-typescript": "^17.1.0", "eslint-config-prettier": "^9.1.0", "eslint-import-resolver-node": "^0.3.9", + "eslint-import-resolver-typescript": "^3.6.1", "eslint-plugin-import": "^2.29.1", "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^5.1.3", diff --git a/test/sum.test.ts b/test/sum.test.ts new file mode 100644 index 0000000..344d3ae --- /dev/null +++ b/test/sum.test.ts @@ -0,0 +1,11 @@ +import { describe, expect, it } from 'vitest'; + +export function sum(a: number, b: number) { + return a + b; +} + +describe('test test', () => { + it('should sum', () => { + expect(sum(1, 1)).toBe(2); + }); +}); diff --git a/types/types.ts b/types/types.ts index 2f5d86f..c94da43 100644 --- a/types/types.ts +++ b/types/types.ts @@ -1,7 +1,7 @@ import { IncomingMessage } from 'http'; -import { HttpMethods } from './enums'; -import { JsonFn, Res, StatusFn } from '../utils/Response'; +import { HttpMethods } from './enums.js'; +import { JsonFn, Res, StatusFn } from '../utils/Response.js'; export type User = { id: string; From e2891b7aff7cae7990c3e7b21f251e9543dfb1aa Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sun, 4 Feb 2024 18:41:17 +0200 Subject: [PATCH 054/114] refactor: rename modules --- server.ts | 14 +++++++------- types/types.ts | 2 +- utils/{Api.ts => app.ts} | 16 +++++++++------- utils/{Response.ts => response.ts} | 0 utils/{Route.ts => route.ts} | 0 5 files changed, 17 insertions(+), 15 deletions(-) rename utils/{Api.ts => app.ts} (94%) rename utils/{Response.ts => response.ts} (100%) rename utils/{Route.ts => route.ts} (100%) diff --git a/server.ts b/server.ts index 994de23..5ce3332 100644 --- a/server.ts +++ b/server.ts @@ -7,18 +7,18 @@ import { notFound, updateUser, } from './controllers/usersController'; -import Api from './utils/Api'; +import App from './utils/app'; import { validateId } from './utils/validateId'; -const api = new Api(); +const app = new App(); const port = Number(process.env.PORT); const host = process.env.HOST; -api.use(validateId); -api.route('/api/users').get(getUserList).post(createUser); -api.route('/api/users/:id').get(getUser).put(updateUser).delete(deleteUser); -api.use(notFound); +app.use(validateId); +app.route('/api/users').get(getUserList).post(createUser); +app.route('/api/users/:id').get(getUser).put(updateUser).delete(deleteUser); +app.use(notFound); -api.listen(port, host, () => { +app.listen(port, host, () => { process.stdout.write(`App running on port ${port}...`); }); diff --git a/types/types.ts b/types/types.ts index c94da43..2c6384c 100644 --- a/types/types.ts +++ b/types/types.ts @@ -1,7 +1,7 @@ import { IncomingMessage } from 'http'; import { HttpMethods } from './enums.js'; -import { JsonFn, Res, StatusFn } from '../utils/Response.js'; +import { JsonFn, Res, StatusFn } from '../utils/response.js'; export type User = { id: string; diff --git a/utils/Api.ts b/utils/app.ts similarity index 94% rename from utils/Api.ts rename to utils/app.ts index a70c574..c45cd14 100644 --- a/utils/Api.ts +++ b/utils/app.ts @@ -1,7 +1,7 @@ -import http from 'http'; +import http, { Server } from 'http'; -import Response, { Res } from './Response'; -import Route from './Route'; +import Response, { Res } from './response'; +import Route from './route'; import { HttpMethods } from '../types/enums'; import { Cb, @@ -13,7 +13,7 @@ import { RequestBody, } from '../types/types'; -class Api { +class App { private readonly handlersTable: HandlersTable = {}; private readonly middlewareQueue: MiddlewareQueue = []; @@ -22,8 +22,10 @@ class Api { private res: ExtendedRes = {}; - public listen(port: number, host: string, cb: () => void) { - http + server: Server = {}; + + public listen(port: number, host: string, cb?: () => void) { + this.server = http .createServer((req, res) => { this.req = this.extendReq(req); this.res = this.extendRes(res); @@ -165,4 +167,4 @@ class Api { } } -export default Api; +export default App; diff --git a/utils/Response.ts b/utils/response.ts similarity index 100% rename from utils/Response.ts rename to utils/response.ts diff --git a/utils/Route.ts b/utils/route.ts similarity index 100% rename from utils/Route.ts rename to utils/route.ts From 4c9e273df6a1537d5966fbfbd8057c06fd11331e Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sun, 4 Feb 2024 18:41:39 +0200 Subject: [PATCH 055/114] feat: add vitest env config --- vitest.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/vitest.config.ts b/vitest.config.ts index b09ee9c..ad9f120 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -5,5 +5,6 @@ export default defineConfig({ test: { globals: true, css: false, + setupFiles: ['dotenv/config'], }, }); From c99c2728cb6601dc762aaaea8afba238b08cbcad Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sun, 4 Feb 2024 18:41:54 +0200 Subject: [PATCH 056/114] refactor: remove mock test --- test/sum.test.ts | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 test/sum.test.ts diff --git a/test/sum.test.ts b/test/sum.test.ts deleted file mode 100644 index 344d3ae..0000000 --- a/test/sum.test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -export function sum(a: number, b: number) { - return a + b; -} - -describe('test test', () => { - it('should sum', () => { - expect(sum(1, 1)).toBe(2); - }); -}); From ab21cb6f6749d917b840c8e8d2710a8cd5580a6f Mon Sep 17 00:00:00 2001 From: Bohdan Date: Tue, 6 Feb 2024 23:17:43 +0200 Subject: [PATCH 057/114] chore: update eslint config --- .eslintrc.cjs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 20ec087..b214a89 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -73,11 +73,6 @@ module.exports = { 'import/resolver': { node: { extensions: ['.js', '.jsx', '.ts', '.tsx'], - moduleDirectory: ['node_modules', './'], - }, - typescript: { - alwaysTryTypes: true, - project: 'tsconfig.json', }, }, }, From fff185964950ed01cccd0bc5575ee05f4fb48f80 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Tue, 6 Feb 2024 23:18:01 +0200 Subject: [PATCH 058/114] refactor: update app api for testing --- utils/app.ts | 65 ++++++++++++++++++++++++++++------------------------ 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/utils/app.ts b/utils/app.ts index c45cd14..3a609ff 100644 --- a/utils/app.ts +++ b/utils/app.ts @@ -22,38 +22,15 @@ class App { private res: ExtendedRes = {}; - server: Server = {}; + public server: Server = {}; + + public createServer() { + this.server = http.createServer(this.handleRequest); + return this.server; + } public listen(port: number, host: string, cb?: () => void) { - this.server = http - .createServer((req, res) => { - this.req = this.extendReq(req); - this.res = this.extendRes(res); - - const requestEndpoint = req?.url ?? ''; - const { routeHandler, routeEndpoint } = - this.getRouteHandlerAndRouteEndpoint(); - - this.injectId(routeEndpoint, requestEndpoint); - - // Getting the body is async operation. So we want to get body first, - // then call middlewares or route handlers - this.getBody().then((body) => { - this.injectBody(body); - - // using middlewareQueue with routeTable inside. - // In order to make sure that if .use() method called BEFORE any route - // e.g. ID validation, we want it to run right before route handler (get, post, put...) - this.middlewareQueue.forEach((middleware) => { - if (typeof middleware === 'function') { - middleware(this.req, this.res); - } else if (routeHandler) { - routeHandler(this.req, this.res); - } - }); - }); - }) - .listen(port, host, cb); + this.server = http.createServer(this.handleRequest).listen(port, host, cb); } public route(route: string) { @@ -65,6 +42,34 @@ class App { return this; } + private handleRequest = (req: Req, res: Res) => { + this.req = this.extendReq(req); + this.res = this.extendRes(res); + + const requestEndpoint = req?.url ?? ''; + const { routeHandler, routeEndpoint } = + this.getRouteHandlerAndRouteEndpoint(); + + this.injectId(routeEndpoint, requestEndpoint); + + // Getting the body is async operation. So we want to get body first, + // then call middlewares or route handlers + this.getBody().then((body) => { + this.injectBody(body); + + // using middlewareQueue with routeTable inside. + // In order to make sure that if .use() method called BEFORE any route + // e.g. ID validation, we want it to run right before route handler (get, post, put...) + this.middlewareQueue.forEach((middleware) => { + if (typeof middleware === 'function') { + middleware(this.req, this.res); + } else if (routeHandler) { + routeHandler(this.req, this.res); + } + }); + }); + }; + private getRouteHandlerAndRouteEndpoint() { const method = this.req.method; const endpoint = this.req?.url ?? ''; From a2912afd77f7bfa3626ab87537a7f31f17812bed Mon Sep 17 00:00:00 2001 From: Bohdan Date: Tue, 6 Feb 2024 23:18:34 +0200 Subject: [PATCH 059/114] chore: remove eslint resolver package --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index b805f43..3730a29 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,6 @@ "eslint-config-airbnb-typescript": "^17.1.0", "eslint-config-prettier": "^9.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-import-resolver-typescript": "^3.6.1", "eslint-plugin-import": "^2.29.1", "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^5.1.3", From 1ac639fea9302c2bdf6fc90596282aa19afde4b9 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Tue, 6 Feb 2024 23:18:58 +0200 Subject: [PATCH 060/114] chore: add supertest package --- package-lock.json | 254 +++++++++++++++++++++++++++++++++++++--------- package.json | 5 +- 2 files changed, 210 insertions(+), 49 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7b4b7a7..c938a01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "devDependencies": { "@types/dotenv-webpack": "^7.0.7", "@types/node": "^20.11.10", + "@types/supertest": "^6.0.2", "@types/uuid": "^9.0.8", "@types/webpack": "^5.28.5", "@typescript-eslint/eslint-plugin": "^6.19.1", @@ -27,12 +28,12 @@ "eslint-config-airbnb-typescript": "^17.1.0", "eslint-config-prettier": "^9.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-import-resolver-typescript": "^3.6.1", "eslint-plugin-import": "^2.29.1", "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^5.1.3", "nodemon": "^3.0.3", "prettier": "^3.2.4", + "supertest": "^6.3.4", "ts-loader": "^9.5.1", "ts-node-dev": "^2.0.0", "typescript": "^5.3.3", @@ -904,6 +905,12 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true + }, "node_modules/@types/dotenv-webpack": { "version": "7.0.7", "resolved": "https://registry.npmjs.org/@types/dotenv-webpack/-/dotenv-webpack-7.0.7.tgz", @@ -953,6 +960,12 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true + }, "node_modules/@types/node": { "version": "20.11.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.10.tgz", @@ -980,6 +993,27 @@ "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", "dev": true }, + "node_modules/@types/superagent": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.3.tgz", + "integrity": "sha512-R/CfN6w2XsixLb1Ii8INfn+BT9sGPvw74OavfkW4SwY+jeUcAwLZv2+bXLJkndnimxjEBm0RPHgcjW9pLCa8cw==", + "dev": true, + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*" + } + }, + "node_modules/@types/supertest": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.2.tgz", + "integrity": "sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg==", + "dev": true, + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, "node_modules/@types/uuid": { "version": "9.0.8", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", @@ -1751,6 +1785,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true + }, "node_modules/assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -1777,6 +1817,12 @@ "has-symbols": "^1.0.3" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, "node_modules/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -2067,12 +2113,33 @@ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2085,6 +2152,12 @@ "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", "dev": true }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -2196,6 +2269,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -2206,6 +2288,16 @@ "node": ">=6" } }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -2672,31 +2764,6 @@ "ms": "^2.1.1" } }, - "node_modules/eslint-import-resolver-typescript": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.1.tgz", - "integrity": "sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==", - "dev": true, - "dependencies": { - "debug": "^4.3.4", - "enhanced-resolve": "^5.12.0", - "eslint-module-utils": "^2.7.4", - "fast-glob": "^3.3.1", - "get-tsconfig": "^4.5.0", - "is-core-module": "^2.11.0", - "is-glob": "^4.0.3" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts" - }, - "peerDependencies": { - "eslint": "*", - "eslint-plugin-import": "*" - } - }, "node_modules/eslint-module-utils": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", @@ -3296,6 +3363,12 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true + }, "node_modules/fastest-levenshtein": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", @@ -3392,6 +3465,35 @@ "is-callable": "^1.1.3" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formidable": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz", + "integrity": "sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==", + "dev": true, + "dependencies": { + "dezalgo": "^1.0.4", + "hexoid": "^1.0.0", + "once": "^1.4.0", + "qs": "^6.11.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3500,18 +3602,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-tsconfig": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.2.tgz", - "integrity": "sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==", - "dev": true, - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -3727,6 +3817,15 @@ "node": ">= 0.4" } }, + "node_modules/hexoid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", + "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/human-signals": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", @@ -4496,6 +4595,15 @@ "node": ">= 8" } }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/micromatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", @@ -4509,6 +4617,18 @@ "node": ">=8.6" } }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -5239,6 +5359,21 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "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", @@ -5403,15 +5538,6 @@ "node": ">=4" } }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -5868,6 +5994,40 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/superagent": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.1.2.tgz", + "integrity": "sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==", + "dev": true, + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.4", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^2.1.2", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=6.4.0 <13 || >=14" + } + }, + "node_modules/supertest": { + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.4.tgz", + "integrity": "sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw==", + "dev": true, + "dependencies": { + "methods": "^1.1.2", + "superagent": "^8.1.2" + }, + "engines": { + "node": ">=6.4.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", diff --git a/package.json b/package.json index 3730a29..bc97267 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,6 @@ "version": "1.0.0", "description": "", "main": "server.ts", - "type": "module", "engines": { "node": ">=20.0.0" }, @@ -16,13 +15,14 @@ "format": "prettier \"./**/*.{ts,css}\"", "format:fix": "prettier --write \"./**/*.{ts,css}\"", "type-check": "tsc --noEmit", - "test": "vitest --config ./vitest.config.ts" + "test": "vitest --config vitest.config.mts" }, "author": "Bohdan Shcherbyna", "license": "ISC", "devDependencies": { "@types/dotenv-webpack": "^7.0.7", "@types/node": "^20.11.10", + "@types/supertest": "^6.0.2", "@types/uuid": "^9.0.8", "@types/webpack": "^5.28.5", "@typescript-eslint/eslint-plugin": "^6.19.1", @@ -41,6 +41,7 @@ "eslint-plugin-prettier": "^5.1.3", "nodemon": "^3.0.3", "prettier": "^3.2.4", + "supertest": "^6.3.4", "ts-loader": "^9.5.1", "ts-node-dev": "^2.0.0", "typescript": "^5.3.3", From 797fa7d064514281ff2325f38996561554cb60fa Mon Sep 17 00:00:00 2001 From: Bohdan Date: Tue, 6 Feb 2024 23:19:21 +0200 Subject: [PATCH 061/114] chore: update tsconfig --- tsconfig.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 4f50a28..680022e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,10 +3,10 @@ "target": "es5", "useDefineForClassFields": true, "lib": ["ESNext"], - "module": "esnext", + "module": "NodeNext", "skipLibCheck": true, - "moduleResolution": "bundler", + "moduleResolution": "NodeNext", "allowImportingTsExtensions": false, "resolveJsonModule": true, "isolatedModules": true, @@ -19,6 +19,4 @@ "allowSyntheticDefaultImports": true, "esModuleInterop": true, }, - "include": ["**/*.ts"], - "exclude": ["node_modules"] } From 34bfb50337ca4f5fd18d9f0754059dddfb9f6ea2 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Wed, 7 Feb 2024 09:57:28 +0200 Subject: [PATCH 062/114] fix: response to match one style --- controllers/usersController.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/controllers/usersController.ts b/controllers/usersController.ts index 4ece7ee..190d303 100644 --- a/controllers/usersController.ts +++ b/controllers/usersController.ts @@ -6,8 +6,13 @@ import isUser from '../models/user/utils/isUser'; import { StatusCode } from '../types/enums'; import { ExtendedReq, ExtendedRes, Req, User } from '../types/types'; +// TODO: add JSDoc comments + export const getUserList = (_req: ExtendedReq, res: ExtendedRes) => { - res.status(StatusCode.SUCCESS).json(users); + res.status(StatusCode.SUCCESS).json({ + status: 'success', + data: users, + }); }; export const getUser = (req: ExtendedReq, res: ExtendedRes) => { @@ -24,7 +29,10 @@ export const getUser = (req: ExtendedReq, res: ExtendedRes) => { return; } - res.status(StatusCode.SUCCESS).json(user); + res.status(StatusCode.SUCCESS).json({ + status: 'success', + data: user, + }); }; export const createUser = (req: ExtendedReq, res: ExtendedRes) => { From 1c064912cd2eaaf9e7b69a2608da2da4cded742d Mon Sep 17 00:00:00 2001 From: Bohdan Date: Wed, 7 Feb 2024 09:57:47 +0200 Subject: [PATCH 063/114] feat: add TODO --- server.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/server.ts b/server.ts index 5ce3332..8475e31 100644 --- a/server.ts +++ b/server.ts @@ -14,6 +14,7 @@ const app = new App(); const port = Number(process.env.PORT); const host = process.env.HOST; +// TODO: encapsulate routes in enum app.use(validateId); app.route('/api/users').get(getUserList).post(createUser); app.route('/api/users/:id').get(getUser).put(updateUser).delete(deleteUser); From 5d2bffb881190ece6c5f77300b2e9beb7cd34390 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Wed, 7 Feb 2024 09:58:17 +0200 Subject: [PATCH 064/114] chore: change vitest config to .mts extension --- vitest.config.ts => vitest.config.mts | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename vitest.config.ts => vitest.config.mts (100%) diff --git a/vitest.config.ts b/vitest.config.mts similarity index 100% rename from vitest.config.ts rename to vitest.config.mts From 79c767b439fc26af5fe9d511aa3e16aafd7dac7f Mon Sep 17 00:00:00 2001 From: Bohdan Date: Wed, 7 Feb 2024 09:58:34 +0200 Subject: [PATCH 065/114] feat: add basic test scenario --- test/api.test.ts | 64 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 test/api.test.ts diff --git a/test/api.test.ts b/test/api.test.ts new file mode 100644 index 0000000..8a66e35 --- /dev/null +++ b/test/api.test.ts @@ -0,0 +1,64 @@ +import supertest from 'supertest'; +import { describe, expect, it } from 'vitest'; + +import { + createUser, + deleteUser, + getUser, + getUserList, + notFound, + updateUser, +} from '../controllers/usersController'; +import { StatusCode } from '../types/enums'; +import App from '../utils/app'; +import { validateId } from '../utils/validateId'; + +const app = new App(); + +app.use(validateId); +app.route('/api/users').get(getUserList).post(createUser); +app.route('/api/users/:id').get(getUser).put(updateUser).delete(deleteUser); +app.use(notFound); + +const server = supertest(app.createServer()); +const uploadUserData = { + age: 24, + username: 'Wassup', + hobbies: ['check', 'test', 'cool'], +}; +let uploadUserId = ''; + +describe('Test scenario 1', () => { + it('GET api/users', async () => { + const res = await server.get('/api/users').expect(StatusCode.SUCCESS); + expect(res.body).toMatchObject({ + status: 'success', + data: [], + }); + }); + + it('POST api/users', async () => { + const res = await server + .post('/api/users') + .send(uploadUserData) + .expect(StatusCode.CREATED); + + expect(res.body).toStrictEqual({ + status: 'success', + data: { ...uploadUserData, id: res.body.data.id }, + }); + + uploadUserId = res.body.data.id; + }); + + it('GET api/users/{userId}', async () => { + const res = await server + .get(`/api/users/${uploadUserId}`) + .expect(StatusCode.SUCCESS); + + expect(res.body).toStrictEqual({ + status: 'success', + data: { ...uploadUserData, id: uploadUserId }, + }); + }); +}); From 8455a7594cccb691f1514e39662089674a328eed Mon Sep 17 00:00:00 2001 From: Bohdan Date: Wed, 7 Feb 2024 10:47:05 +0200 Subject: [PATCH 066/114] feat: encapsulate routes in enum --- server.ts | 6 +++--- test/api.test.ts | 12 ++++++------ types/enums.ts | 5 +++++ 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/server.ts b/server.ts index 8475e31..aca6dae 100644 --- a/server.ts +++ b/server.ts @@ -7,6 +7,7 @@ import { notFound, updateUser, } from './controllers/usersController'; +import { Routes } from './types/enums'; import App from './utils/app'; import { validateId } from './utils/validateId'; @@ -14,10 +15,9 @@ const app = new App(); const port = Number(process.env.PORT); const host = process.env.HOST; -// TODO: encapsulate routes in enum app.use(validateId); -app.route('/api/users').get(getUserList).post(createUser); -app.route('/api/users/:id').get(getUser).put(updateUser).delete(deleteUser); +app.route(Routes.USERS).get(getUserList).post(createUser); +app.route(Routes.USERS_ID).get(getUser).put(updateUser).delete(deleteUser); app.use(notFound); app.listen(port, host, () => { diff --git a/test/api.test.ts b/test/api.test.ts index 8a66e35..9928414 100644 --- a/test/api.test.ts +++ b/test/api.test.ts @@ -9,15 +9,15 @@ import { notFound, updateUser, } from '../controllers/usersController'; -import { StatusCode } from '../types/enums'; +import { Routes, StatusCode } from '../types/enums'; import App from '../utils/app'; import { validateId } from '../utils/validateId'; const app = new App(); app.use(validateId); -app.route('/api/users').get(getUserList).post(createUser); -app.route('/api/users/:id').get(getUser).put(updateUser).delete(deleteUser); +app.route(Routes.USERS).get(getUserList).post(createUser); +app.route(Routes.USERS_ID).get(getUser).put(updateUser).delete(deleteUser); app.use(notFound); const server = supertest(app.createServer()); @@ -30,7 +30,7 @@ let uploadUserId = ''; describe('Test scenario 1', () => { it('GET api/users', async () => { - const res = await server.get('/api/users').expect(StatusCode.SUCCESS); + const res = await server.get(Routes.USERS).expect(StatusCode.SUCCESS); expect(res.body).toMatchObject({ status: 'success', data: [], @@ -39,7 +39,7 @@ describe('Test scenario 1', () => { it('POST api/users', async () => { const res = await server - .post('/api/users') + .post(Routes.USERS) .send(uploadUserData) .expect(StatusCode.CREATED); @@ -53,7 +53,7 @@ describe('Test scenario 1', () => { it('GET api/users/{userId}', async () => { const res = await server - .get(`/api/users/${uploadUserId}`) + .get(`${Routes.USERS}/${uploadUserId}`) .expect(StatusCode.SUCCESS); expect(res.body).toStrictEqual({ diff --git a/types/enums.ts b/types/enums.ts index d12648e..0b9a2ca 100644 --- a/types/enums.ts +++ b/types/enums.ts @@ -14,3 +14,8 @@ export enum HttpMethods { PUT = 'PUT', DELETE = 'DELETE', } + +export enum Routes { + USERS = '/api/users', + USERS_ID = '/api/users/:id', +} From 960097e703d10290830992f2faf182f96a3e5855 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Wed, 7 Feb 2024 10:55:52 +0200 Subject: [PATCH 067/114] refactor: change response style --- controllers/usersController.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/controllers/usersController.ts b/controllers/usersController.ts index 190d303..1892d9c 100644 --- a/controllers/usersController.ts +++ b/controllers/usersController.ts @@ -11,7 +11,10 @@ import { ExtendedReq, ExtendedRes, Req, User } from '../types/types'; export const getUserList = (_req: ExtendedReq, res: ExtendedRes) => { res.status(StatusCode.SUCCESS).json({ status: 'success', - data: users, + results: users.length, + data: { + users, + }, }); }; @@ -31,7 +34,9 @@ export const getUser = (req: ExtendedReq, res: ExtendedRes) => { res.status(StatusCode.SUCCESS).json({ status: 'success', - data: user, + data: { + user, + }, }); }; @@ -54,7 +59,9 @@ export const createUser = (req: ExtendedReq, res: ExtendedRes) => { res.status(StatusCode.CREATED).json({ status: 'success', - data: user, + data: { + user, + }, }); }; @@ -92,7 +99,9 @@ export const updateUser = (req: ExtendedReq, res: ExtendedRes) => { res.status(StatusCode.SUCCESS).json({ status: 'success', - data: updatedUser, + data: { + user: updatedUser, + }, }); }; From 5dab7cc78ca9e1947167525918ab202ca487e89d Mon Sep 17 00:00:00 2001 From: Bohdan Date: Wed, 7 Feb 2024 11:00:12 +0200 Subject: [PATCH 068/114] fix: test to match new response style --- test/api.test.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/test/api.test.ts b/test/api.test.ts index 9928414..ff85f17 100644 --- a/test/api.test.ts +++ b/test/api.test.ts @@ -33,7 +33,9 @@ describe('Test scenario 1', () => { const res = await server.get(Routes.USERS).expect(StatusCode.SUCCESS); expect(res.body).toMatchObject({ status: 'success', - data: [], + data: { + users: [], + }, }); }); @@ -42,13 +44,16 @@ describe('Test scenario 1', () => { .post(Routes.USERS) .send(uploadUserData) .expect(StatusCode.CREATED); + const { id } = res.body.data.user; expect(res.body).toStrictEqual({ status: 'success', - data: { ...uploadUserData, id: res.body.data.id }, + data: { + user: { ...uploadUserData, id }, + }, }); - uploadUserId = res.body.data.id; + uploadUserId = id; }); it('GET api/users/{userId}', async () => { @@ -58,7 +63,9 @@ describe('Test scenario 1', () => { expect(res.body).toStrictEqual({ status: 'success', - data: { ...uploadUserData, id: uploadUserId }, + data: { + user: { ...uploadUserData, id: uploadUserId }, + }, }); }); }); From 457ef3ae1736062cef1c97f873819933de33ad62 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Wed, 7 Feb 2024 14:49:48 +0200 Subject: [PATCH 069/114] feat: complete test scenario 1 --- test/api.test.ts | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/test/api.test.ts b/test/api.test.ts index ff85f17..379d8b2 100644 --- a/test/api.test.ts +++ b/test/api.test.ts @@ -26,6 +26,11 @@ const uploadUserData = { username: 'Wassup', hobbies: ['check', 'test', 'cool'], }; +const updatedUserData = { + age: 12, + username: 'updated name', + hobbies: ['updated hobby 1', 'updated hobby 2', 'updated hobby 3'], +}; let uploadUserId = ''; describe('Test scenario 1', () => { @@ -68,4 +73,37 @@ describe('Test scenario 1', () => { }, }); }); + + it('PUT api/users/{userId}', async () => { + const res = await server + .put(`${Routes.USERS}/${uploadUserId}`) + .send(updatedUserData) + .expect(StatusCode.SUCCESS); + + expect(res.body).toStrictEqual({ + status: 'success', + data: { + user: { ...updatedUserData, id: uploadUserId }, + }, + }); + }); + + it('DELETE api/users/{userId}', async () => { + const res = await server + .delete(`${Routes.USERS}/${uploadUserId}`) + .expect(StatusCode.NO_CONTENT); + + expect(res.body).toStrictEqual(''); + }); + + it('GET api/users/{userId}', async () => { + const res = await server + .get(`${Routes.USERS}/${uploadUserId}`) + .expect(StatusCode.NOT_FOUND); + + expect(res.body).toStrictEqual({ + status: 'fail', + message: 'User not found', + }); + }); }); From 867c268fde1d1801045dc1d9a02d53e9174f0578 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Wed, 7 Feb 2024 14:51:41 +0200 Subject: [PATCH 070/114] refactor: rename test file from "api.test.ts" to "test-scenario-1.test.ts" --- test/{api.test.ts => api-scenario-1.test.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/{api.test.ts => api-scenario-1.test.ts} (100%) diff --git a/test/api.test.ts b/test/api-scenario-1.test.ts similarity index 100% rename from test/api.test.ts rename to test/api-scenario-1.test.ts From 90669eb768033a4fda2caccf2ff695ffbecfaa3c Mon Sep 17 00:00:00 2001 From: Bohdan Date: Wed, 7 Feb 2024 15:31:00 +0200 Subject: [PATCH 071/114] refactor: rearrange files --- controllers/usersController.ts | 20 ++++++++++--------- data/data.ts | 2 +- models/user/{ => lib}/const.ts | 0 .../user/{ => lib}/utils/findMissingFields.ts | 2 +- models/user/{ => lib}/utils/isUser.ts | 2 +- models/user/usersModel.ts | 8 ++++++++ types/types.ts | 13 ++---------- 7 files changed, 24 insertions(+), 23 deletions(-) rename models/user/{ => lib}/const.ts (100%) rename models/user/{ => lib}/utils/findMissingFields.ts (86%) rename models/user/{ => lib}/utils/isUser.ts (85%) create mode 100644 models/user/usersModel.ts diff --git a/controllers/usersController.ts b/controllers/usersController.ts index 1892d9c..42ad9a9 100644 --- a/controllers/usersController.ts +++ b/controllers/usersController.ts @@ -1,12 +1,14 @@ import * as uuid from 'uuid'; import { users } from '../data/data'; -import findMissingFields from '../models/user/utils/findMissingFields'; -import isUser from '../models/user/utils/isUser'; +import findMissingFields from '../models/user/lib/utils/findMissingFields'; +import isUser from '../models/user/lib/utils/isUser'; +import { User } from '../models/user/usersModel'; import { StatusCode } from '../types/enums'; -import { ExtendedReq, ExtendedRes, Req, User } from '../types/types'; +import { ExtendedReq, ExtendedRes, Req } from '../types/types'; // TODO: add JSDoc comments +// TODO: add emoji response export const getUserList = (_req: ExtendedReq, res: ExtendedRes) => { res.status(StatusCode.SUCCESS).json({ @@ -44,17 +46,17 @@ export const createUser = (req: ExtendedReq, res: ExtendedRes) => { const { body } = req; if (!body || !isUser(body)) { - const missingFields = findMissingFields(body); + const missingFields = findMissingFields(body).join(', '); res.status(StatusCode.BAD_REQUEST).json({ status: 'fail', - message: `The provided data is missing required fields (${missingFields.join(', ')})`, + message: `The provided data is missing required fields (${missingFields})`, }); return; } - const user = { id: uuid.v4(), ...body }; + const user = { id: uuid.v4(), ...body }; users.push(user); res.status(StatusCode.CREATED).json({ @@ -72,11 +74,11 @@ export const updateUser = (req: ExtendedReq, res: ExtendedRes) => { } = req; if (!body || !isUser(body)) { - const missingFields = findMissingFields(body); + const missingFields = findMissingFields(body).join(', '); res.status(StatusCode.BAD_REQUEST).json({ status: 'fail', - message: `The provided data is missing required fields (${missingFields.join(', ')})`, + message: `The provided data is missing required fields (${missingFields})`, }); return; @@ -94,7 +96,7 @@ export const updateUser = (req: ExtendedReq, res: ExtendedRes) => { return; } - const updatedUser = { ...relatedUser, ...body } as User; + const updatedUser = { ...relatedUser, ...body }; users[relatedUserIndex] = updatedUser; res.status(StatusCode.SUCCESS).json({ diff --git a/data/data.ts b/data/data.ts index 3b15366..a5bc971 100644 --- a/data/data.ts +++ b/data/data.ts @@ -1,3 +1,3 @@ -import { UserList } from '../types/types.js'; +import { UserList } from '../models/user/usersModel'; export const users: UserList = []; diff --git a/models/user/const.ts b/models/user/lib/const.ts similarity index 100% rename from models/user/const.ts rename to models/user/lib/const.ts diff --git a/models/user/utils/findMissingFields.ts b/models/user/lib/utils/findMissingFields.ts similarity index 86% rename from models/user/utils/findMissingFields.ts rename to models/user/lib/utils/findMissingFields.ts index 621a935..3f7658d 100644 --- a/models/user/utils/findMissingFields.ts +++ b/models/user/lib/utils/findMissingFields.ts @@ -1,4 +1,4 @@ -import { RequestBody } from '../../../types/types'; +import { RequestBody } from '../../../../types/types'; import { REQUIRED_USER_FIELDS } from '../const'; const findMissingFields = (body: RequestBody) => { diff --git a/models/user/utils/isUser.ts b/models/user/lib/utils/isUser.ts similarity index 85% rename from models/user/utils/isUser.ts rename to models/user/lib/utils/isUser.ts index a6aa4ed..9db9973 100644 --- a/models/user/utils/isUser.ts +++ b/models/user/lib/utils/isUser.ts @@ -1,4 +1,4 @@ -import { User } from '../../../types/types'; +import { User } from '../../usersModel'; import { REQUIRED_USER_FIELDS } from '../const'; const isUser = ( diff --git a/models/user/usersModel.ts b/models/user/usersModel.ts new file mode 100644 index 0000000..2754824 --- /dev/null +++ b/models/user/usersModel.ts @@ -0,0 +1,8 @@ +export type User = { + id: string; + username: string; + age: number; + hobbies: string[] | []; +}; + +export type UserList = User[]; diff --git a/types/types.ts b/types/types.ts index 2c6384c..67ceedd 100644 --- a/types/types.ts +++ b/types/types.ts @@ -1,16 +1,7 @@ import { IncomingMessage } from 'http'; -import { HttpMethods } from './enums.js'; -import { JsonFn, Res, StatusFn } from '../utils/response.js'; - -export type User = { - id: string; - username: string; - age: number; - hobbies: string[] | []; -}; - -export type UserList = User[]; +import { HttpMethods } from './enums'; +import { JsonFn, Res, StatusFn } from '../utils/response'; export type Req = IncomingMessage; From 79184ee18990831974fbb4983b2fbc78b67c136f Mon Sep 17 00:00:00 2001 From: Bohdan Date: Wed, 7 Feb 2024 15:32:19 +0200 Subject: [PATCH 072/114] fix: to clear data after test complete --- test/api-scenario-1.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/api-scenario-1.test.ts b/test/api-scenario-1.test.ts index 379d8b2..46fa291 100644 --- a/test/api-scenario-1.test.ts +++ b/test/api-scenario-1.test.ts @@ -1,5 +1,5 @@ import supertest from 'supertest'; -import { describe, expect, it } from 'vitest'; +import { afterAll, describe, expect, it } from 'vitest'; import { createUser, @@ -9,6 +9,7 @@ import { notFound, updateUser, } from '../controllers/usersController'; +import { users } from '../data/data'; import { Routes, StatusCode } from '../types/enums'; import App from '../utils/app'; import { validateId } from '../utils/validateId'; @@ -34,6 +35,10 @@ const updatedUserData = { let uploadUserId = ''; describe('Test scenario 1', () => { + afterAll(() => { + users.splice(0); + }); + it('GET api/users', async () => { const res = await server.get(Routes.USERS).expect(StatusCode.SUCCESS); expect(res.body).toMatchObject({ From d3a5c1e750ffc8d8e67e345760def230dce75592 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Wed, 7 Feb 2024 15:39:01 +0200 Subject: [PATCH 073/114] refactor: abstract data clearance in separate function --- test/api-scenario-1.test.ts | 6 ++---- utils/clearData.ts | 11 +++++++++++ 2 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 utils/clearData.ts diff --git a/test/api-scenario-1.test.ts b/test/api-scenario-1.test.ts index 46fa291..3c9a75a 100644 --- a/test/api-scenario-1.test.ts +++ b/test/api-scenario-1.test.ts @@ -9,9 +9,9 @@ import { notFound, updateUser, } from '../controllers/usersController'; -import { users } from '../data/data'; import { Routes, StatusCode } from '../types/enums'; import App from '../utils/app'; +import clearData from '../utils/clearData'; import { validateId } from '../utils/validateId'; const app = new App(); @@ -35,9 +35,7 @@ const updatedUserData = { let uploadUserId = ''; describe('Test scenario 1', () => { - afterAll(() => { - users.splice(0); - }); + afterAll(clearData); it('GET api/users', async () => { const res = await server.get(Routes.USERS).expect(StatusCode.SUCCESS); diff --git a/utils/clearData.ts b/utils/clearData.ts new file mode 100644 index 0000000..e171ce3 --- /dev/null +++ b/utils/clearData.ts @@ -0,0 +1,11 @@ +import { users } from '../data/data'; + +/** + * Clears the users data by removing all elements. + * @return {void} - Returns nothing. + */ +const clearData = () => { + users.splice(0); +}; + +export default clearData; From 3a85c8ddb279d5fe6f4cfd3cbefdc8e535b072ec Mon Sep 17 00:00:00 2001 From: Bohdan Date: Wed, 7 Feb 2024 15:48:03 +0200 Subject: [PATCH 074/114] feat: add test scenario 2 --- test/api-scenario-2.test.ts | 79 +++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 test/api-scenario-2.test.ts diff --git a/test/api-scenario-2.test.ts b/test/api-scenario-2.test.ts new file mode 100644 index 0000000..724aaaf --- /dev/null +++ b/test/api-scenario-2.test.ts @@ -0,0 +1,79 @@ +import supertest from 'supertest'; +import { afterAll, describe, expect, it } from 'vitest'; + +import { + createUser, + deleteUser, + getUser, + getUserList, + notFound, + updateUser, +} from '../controllers/usersController'; +import { Routes, StatusCode } from '../types/enums'; +import App from '../utils/app'; +import clearData from '../utils/clearData'; +import { validateId } from '../utils/validateId'; + +const app = new App(); + +app.use(validateId); +app.route(Routes.USERS).get(getUserList).post(createUser); +app.route(Routes.USERS_ID).get(getUser).put(updateUser).delete(deleteUser); +app.use(notFound); + +const server = supertest(app.createServer()); +const updatedUserData = { + age: 12, + username: 'updated name', + hobbies: ['updated hobby 1', 'updated hobby 2', 'updated hobby 3'], +}; +const uploadUserId = ''; + +describe('Test scenario 2', () => { + afterAll(clearData); + + it('GET api/users should return empty array of users', async () => { + const res = await server.get(Routes.USERS).expect(StatusCode.SUCCESS); + expect(res.body).toMatchObject({ + status: 'success', + results: 0, + data: { + users: [], + }, + }); + }); + + it('GET api/users/{userId} should return user not found', async () => { + const res = await server + .get(`${Routes.USERS}/${uploadUserId}`) + .expect(StatusCode.NOT_FOUND); + + expect(res.body).toStrictEqual({ + status: 'fail', + message: 'User not found', + }); + }); + + it('PUT api/users/{userId} should return user not found', async () => { + const res = await server + .put(`${Routes.USERS}/${uploadUserId}`) + .send(updatedUserData) + .expect(StatusCode.NOT_FOUND); + + expect(res.body).toStrictEqual({ + status: 'fail', + message: 'User not found', + }); + }); + + it('DELETE api/users/{userId} should return user not found', async () => { + const res = await server + .delete(`${Routes.USERS}/${uploadUserId}`) + .expect(StatusCode.NOT_FOUND); + + expect(res.body).toStrictEqual({ + status: 'fail', + message: 'User not found', + }); + }); +}); From 6ec8c58e920e07575cf6bd1e2a588dfbf4b5fd52 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Wed, 7 Feb 2024 15:56:34 +0200 Subject: [PATCH 075/114] fix: typo --- webpack.config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webpack.config.ts b/webpack.config.ts index b297511..fdc37e0 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -5,11 +5,11 @@ import { Configuration } from 'webpack'; type Mode = 'production' | 'development'; -type Envoirment = { +type Environment = { mode: Mode; }; -export default (env: Envoirment) => { +export default (env: Environment) => { const config: Configuration = { mode: env.mode, devtool: false, From 31e1f9bfc6a79b41f333ed68b8804378e40ff9a7 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Wed, 7 Feb 2024 16:02:13 +0200 Subject: [PATCH 076/114] feat: add 3 test scenario --- controllers/usersController.ts | 2 +- test/api-scenario-3.test.ts | 102 +++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 test/api-scenario-3.test.ts diff --git a/controllers/usersController.ts b/controllers/usersController.ts index 42ad9a9..491c143 100644 --- a/controllers/usersController.ts +++ b/controllers/usersController.ts @@ -96,7 +96,7 @@ export const updateUser = (req: ExtendedReq, res: ExtendedRes) => { return; } - const updatedUser = { ...relatedUser, ...body }; + const updatedUser = ({ ...relatedUser, ...body }) as User; users[relatedUserIndex] = updatedUser; res.status(StatusCode.SUCCESS).json({ diff --git a/test/api-scenario-3.test.ts b/test/api-scenario-3.test.ts new file mode 100644 index 0000000..28b3657 --- /dev/null +++ b/test/api-scenario-3.test.ts @@ -0,0 +1,102 @@ +import supertest from 'supertest'; +import { afterAll, describe, expect, it } from 'vitest'; + +import { + createUser, + deleteUser, + getUser, + getUserList, + notFound, + updateUser, +} from '../controllers/usersController'; +import { Routes, StatusCode } from '../types/enums'; +import App from '../utils/app'; +import clearData from '../utils/clearData'; +import { validateId } from '../utils/validateId'; + +const app = new App(); + +app.use(validateId); +app.route(Routes.USERS).get(getUserList).post(createUser); +app.route(Routes.USERS_ID).get(getUser).put(updateUser).delete(deleteUser); +app.use(notFound); + +const server = supertest(app.createServer()); +const uploadUserData = { + age: 24, + username: 'Wassup', + hobbies: ['check', 'test', 'cool'], +}; +let uploadUserId = ''; + +describe('Test scenario 2', () => { + afterAll(clearData); + + it('GET api/users should return empty array of users', async () => { + const res = await server.get(Routes.USERS).expect(StatusCode.SUCCESS); + expect(res.body).toMatchObject({ + status: 'success', + results: 0, + data: { + users: [], + }, + }); + }); + + it('POST api/users should create user', async () => { + const res = await server + .post(Routes.USERS) + .send(uploadUserData) + .expect(StatusCode.CREATED); + const { id } = res.body.data.user; + + expect(res.body).toStrictEqual({ + status: 'success', + data: { + user: { ...uploadUserData, id }, + }, + }); + uploadUserId = id; + }); + + it('PUT api/users/{userId} should return invalid user data with missing name', async () => { + const res = await server + .put(`${Routes.USERS}/${uploadUserId}`) + .send({ + age: 1, + hobbies: [], + }) + .expect(StatusCode.BAD_REQUEST); + + expect(res.body).toStrictEqual({ + status: 'fail', + message: `The provided data is missing required fields (username)`, + }); + }); + + it('PUT api/users/{userId} should return invalid user data with missing name and age', async () => { + const res = await server + .put(`${Routes.USERS}/${uploadUserId}`) + .send({ + hobbies: [], + }) + .expect(StatusCode.BAD_REQUEST); + + expect(res.body).toStrictEqual({ + status: 'fail', + message: `The provided data is missing required fields (username, age)`, + }); + }); + + it('PUT api/users/{userId} should return invalid user data with missing name, age and hobbies', async () => { + const res = await server + .put(`${Routes.USERS}/${uploadUserId}`) + .send({}) + .expect(StatusCode.BAD_REQUEST); + + expect(res.body).toStrictEqual({ + status: 'fail', + message: `The provided data is missing required fields (username, age, hobbies)`, + }); + }); +}); From 2d45d1de88ece4ac7b142b0608d14f81164f4f19 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sat, 10 Feb 2024 19:38:34 +0200 Subject: [PATCH 077/114] refactor: change app logic to work with async handlers --- utils/app.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/utils/app.ts b/utils/app.ts index 3a609ff..dfddb55 100644 --- a/utils/app.ts +++ b/utils/app.ts @@ -54,19 +54,21 @@ class App { // Getting the body is async operation. So we want to get body first, // then call middlewares or route handlers - this.getBody().then((body) => { + this.getBody().then(async (body) => { this.injectBody(body); // using middlewareQueue with routeTable inside. // In order to make sure that if .use() method called BEFORE any route // e.g. ID validation, we want it to run right before route handler (get, post, put...) - this.middlewareQueue.forEach((middleware) => { + + // eslint-disable-next-line + for await (const middleware of this.middlewareQueue) { if (typeof middleware === 'function') { middleware(this.req, this.res); } else if (routeHandler) { - routeHandler(this.req, this.res); + await routeHandler(this.req, this.res); } - }); + } }); }; From 3f1760003b5052040bb5d985da14baf2bc3a8b70 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sat, 10 Feb 2024 19:44:08 +0200 Subject: [PATCH 078/114] feat: add more types --- models/user/usersModel.ts | 4 ++++ types/types.ts | 12 +++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/models/user/usersModel.ts b/models/user/usersModel.ts index 2754824..3c03aed 100644 --- a/models/user/usersModel.ts +++ b/models/user/usersModel.ts @@ -5,4 +5,8 @@ export type User = { hobbies: string[] | []; }; +export type RequestUser = Omit; + export type UserList = User[]; + +export type UserId = User['id']; diff --git a/types/types.ts b/types/types.ts index 67ceedd..6cec421 100644 --- a/types/types.ts +++ b/types/types.ts @@ -1,6 +1,7 @@ import { IncomingMessage } from 'http'; -import { HttpMethods } from './enums'; +import { DBCommands, HttpMethods } from './enums'; +import { RequestUser } from '../models/user/usersModel'; import { JsonFn, Res, StatusFn } from '../utils/response'; export type Req = IncomingMessage; @@ -26,3 +27,12 @@ export type MiddlewareQueue = (Cb | RouteTable)[]; export type HandlersTable = | Record> | Record; + +export type WorkerArgs = [RequestUser & string, RequestUser]; + +export type WorkerMessage = { + command: DBCommands; + args: WorkerArgs; +}; + +export type WorkerRequestData = { hostname: string; port: number }; From 496a0cfdc69857ac94db701b73f5ef2de1651345 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sat, 10 Feb 2024 19:44:25 +0200 Subject: [PATCH 079/114] refactor: remove data file --- data/data.ts | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 data/data.ts diff --git a/data/data.ts b/data/data.ts deleted file mode 100644 index a5bc971..0000000 --- a/data/data.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { UserList } from '../models/user/usersModel'; - -export const users: UserList = []; From 242c42b0b7f6c08f0febed3d00067957601a8002 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sat, 10 Feb 2024 19:45:19 +0200 Subject: [PATCH 080/114] feat: implement multi mode to run multiple instances of the app --- package.json | 5 +++ server.ts | 84 +++++++++++++++++++++++++++++++++++++---- utils/forwardRequest.ts | 29 ++++++++++++++ utils/isMulti.ts | 9 +++++ 4 files changed, 119 insertions(+), 8 deletions(-) create mode 100644 utils/forwardRequest.ts create mode 100644 utils/isMulti.ts diff --git a/package.json b/package.json index bc97267..b504ffb 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "scripts": { "start:dev": "nodemon", "start:prod": "npm run build && node ./build/bundle.js", + "start:multi": "nodemon -- --multi=true", "build": "webpack --config ./webpack.config.ts --env mode=production", "lint": "eslint . --ext ts --report-unused-disable-directives --max-warnings 0", "lint:fix": "eslint --fix --ext ts", @@ -53,3 +54,7 @@ "uuid": "^9.0.1" } } + + + + diff --git a/server.ts b/server.ts index aca6dae..512b931 100644 --- a/server.ts +++ b/server.ts @@ -1,4 +1,8 @@ import 'dotenv/config'; +import cluster from 'cluster'; +import http from 'http'; +import { availableParallelism } from 'node:os'; + import { createUser, deleteUser, @@ -7,19 +11,83 @@ import { notFound, updateUser, } from './controllers/usersController'; +import db from './db/db'; import { Routes } from './types/enums'; +import { WorkerMessage } from './types/types'; import App from './utils/app'; +import forwardRequest from './utils/forwardRequest'; +import isMulti from './utils/isMulti'; import { validateId } from './utils/validateId'; -const app = new App(); +const { isPrimary } = cluster; + +const numCPUs = availableParallelism(); const port = Number(process.env.PORT); const host = process.env.HOST; +const workerPorts: number[] = []; +const isMultiMode = isMulti(); +const isMasterProcess = isPrimary && isMultiMode; +let currentWorkerNum = 0; + +if (isMasterProcess) { + // Master process // load balancer code + http + .createServer((req, res) => { + const workerCreds = { + hostname: host, + port: workerPorts[currentWorkerNum], + }; + + forwardRequest(req, res, workerCreds); + + if (currentWorkerNum === workerPorts.length - 1) { + currentWorkerNum = 0; + return; + } + currentWorkerNum += 1; + }) + .listen(port, host, () => { + process.stdout.write(`The load balancer is running on port ${port}...\n`); + }); + + for (let i = 1; i < numCPUs; i += 1) { + const workersPort = port + i; + const worker = cluster.fork({ PORT: workersPort }); + workerPorts.push(workersPort); + + worker.on('message', (msg: WorkerMessage) => { + if (!('command' in msg)) return; + + const workerArgs = msg.args; + const dbAction = db[msg.command]; + dbAction(...workerArgs).then((data) => { + worker.send({ res: data }); + }); + }); + } +} else if (isMultiMode) { + // Worker's code + const app = new App(); + const workerPort = Number(process.env.PORT); + + app.use(validateId); + app.route(Routes.USERS).get(getUserList).post(createUser); + app.route(Routes.USERS_ID).get(getUser).put(updateUser).delete(deleteUser); + app.use(notFound); + + app.listen(workerPort, host, () => { + process.stdout.write(`App running on port ${workerPort}...\n`); + }); +} else { + // Base app + const app = new App(); -app.use(validateId); -app.route(Routes.USERS).get(getUserList).post(createUser); -app.route(Routes.USERS_ID).get(getUser).put(updateUser).delete(deleteUser); -app.use(notFound); + app.use(validateId); + app.route(Routes.USERS).get(getUserList).post(createUser); + app.route(Routes.USERS_ID).get(getUser).put(updateUser).delete(deleteUser); + app.use(notFound); -app.listen(port, host, () => { - process.stdout.write(`App running on port ${port}...`); -}); + app.listen(port, host, () => { + process.stdout.write(`App running on port ${port}...`); + }); +} diff --git a/utils/forwardRequest.ts b/utils/forwardRequest.ts new file mode 100644 index 0000000..a0269ee --- /dev/null +++ b/utils/forwardRequest.ts @@ -0,0 +1,29 @@ +import http from 'http'; + +import { Res } from './response'; +import { Req, WorkerRequestData } from '../types/types'; + +/** + * Forwards the request to the worker with the given data. + * @param {Req} req - The incoming request object. + * @param {Res} res - The outgoing response object. + * @param {WorkerRequestData} workerData - The data of the worker to forward the request to. + */ +const forwardRequest = (req: Req, res: Res, workerData: WorkerRequestData) => { + const options = { + hostname: workerData.hostname, + port: workerData.port, + path: req.url, + method: req.method, + headers: req.headers, + }; + + const proxy = http.request(options, (workerRes) => { + res.writeHead(workerRes.statusCode as number, workerRes.headers); + workerRes.pipe(res); + }); + + req.pipe(proxy); +}; + +export default forwardRequest; diff --git a/utils/isMulti.ts b/utils/isMulti.ts new file mode 100644 index 0000000..814af42 --- /dev/null +++ b/utils/isMulti.ts @@ -0,0 +1,9 @@ +/** + * Checks if the application is running in multi mode. + * @returns {boolean} True if running in the multi mode. + */ +const isMulti = () => { + return process.argv.slice(2).at(0)?.replace('--multi=', '') === 'true'; +}; + +export default isMulti; From 9eceb692d427aa1fe5d5f87100b312ee0637c03a Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sat, 10 Feb 2024 19:47:56 +0200 Subject: [PATCH 081/114] refactor: modify app to share db state across all workers --- controllers/usersController.ts | 41 ++++++++------------ db/db.ts | 69 ++++++++++++++++++++++++++++++++++ types/enums.ts | 8 ++++ utils/connectDB.ts | 27 +++++++++++++ 4 files changed, 120 insertions(+), 25 deletions(-) create mode 100644 db/db.ts create mode 100644 utils/connectDB.ts diff --git a/controllers/usersController.ts b/controllers/usersController.ts index 491c143..0105355 100644 --- a/controllers/usersController.ts +++ b/controllers/usersController.ts @@ -1,16 +1,16 @@ -import * as uuid from 'uuid'; - -import { users } from '../data/data'; +import db from '../db/db'; import findMissingFields from '../models/user/lib/utils/findMissingFields'; import isUser from '../models/user/lib/utils/isUser'; -import { User } from '../models/user/usersModel'; import { StatusCode } from '../types/enums'; import { ExtendedReq, ExtendedRes, Req } from '../types/types'; // TODO: add JSDoc comments // TODO: add emoji response +// TODO: handle server errors + +export const getUserList = async (_req: ExtendedReq, res: ExtendedRes) => { + const users = await db.getUserList(); -export const getUserList = (_req: ExtendedReq, res: ExtendedRes) => { res.status(StatusCode.SUCCESS).json({ status: 'success', results: users.length, @@ -20,10 +20,9 @@ export const getUserList = (_req: ExtendedReq, res: ExtendedRes) => { }); }; -export const getUser = (req: ExtendedReq, res: ExtendedRes) => { +export const getUser = async (req: ExtendedReq, res: ExtendedRes) => { const { id } = req.route; - - const user = users.find((usr) => usr.id === id); + const user = await db.getUser(id); if (!user) { res.status(StatusCode.NOT_FOUND).json({ @@ -42,7 +41,7 @@ export const getUser = (req: ExtendedReq, res: ExtendedRes) => { }); }; -export const createUser = (req: ExtendedReq, res: ExtendedRes) => { +export const createUser = async (req: ExtendedReq, res: ExtendedRes) => { const { body } = req; if (!body || !isUser(body)) { @@ -56,8 +55,7 @@ export const createUser = (req: ExtendedReq, res: ExtendedRes) => { return; } - const user = { id: uuid.v4(), ...body }; - users.push(user); + const user = await db.createUser(body); res.status(StatusCode.CREATED).json({ status: 'success', @@ -67,7 +65,7 @@ export const createUser = (req: ExtendedReq, res: ExtendedRes) => { }); }; -export const updateUser = (req: ExtendedReq, res: ExtendedRes) => { +export const updateUser = async (req: ExtendedReq, res: ExtendedRes) => { const { body, route: { id }, @@ -84,10 +82,9 @@ export const updateUser = (req: ExtendedReq, res: ExtendedRes) => { return; } - const relatedUser = users.find((usr) => usr.id === id); - const relatedUserIndex = users.findIndex((usr) => usr.id === id); + const updatedUser = await db.updateUser(id, body); - if (!relatedUser) { + if (!updatedUser) { res.status(StatusCode.NOT_FOUND).json({ status: 'fail', message: 'User not found', @@ -96,9 +93,6 @@ export const updateUser = (req: ExtendedReq, res: ExtendedRes) => { return; } - const updatedUser = ({ ...relatedUser, ...body }) as User; - users[relatedUserIndex] = updatedUser; - res.status(StatusCode.SUCCESS).json({ status: 'success', data: { @@ -107,12 +101,11 @@ export const updateUser = (req: ExtendedReq, res: ExtendedRes) => { }); }; -export const deleteUser = (req: ExtendedReq, res: ExtendedRes) => { +export const deleteUser = async (req: ExtendedReq, res: ExtendedRes) => { const { id } = req.route; + const isDeleted = await db.deleteUser(id); - const userDeleteIndex = users.findIndex((usr) => usr.id === id); - - if (userDeleteIndex === -1) { + if (!isDeleted) { res.status(StatusCode.NOT_FOUND).json({ status: 'fail', message: 'User not found', @@ -121,15 +114,13 @@ export const deleteUser = (req: ExtendedReq, res: ExtendedRes) => { return; } - users.splice(userDeleteIndex, 1); - res.status(StatusCode.NO_CONTENT).json({ status: 'success', data: null, }); }; -export const notFound = (_req: Req, res: ExtendedRes) => { +export const notFound = async (_req: Req, res: ExtendedRes) => { res.status(StatusCode.NOT_FOUND).json({ status: 'fail', message: 'The route does not exist', diff --git a/db/db.ts b/db/db.ts new file mode 100644 index 0000000..8b23301 --- /dev/null +++ b/db/db.ts @@ -0,0 +1,69 @@ +import cluster from 'cluster'; + +import * as uuid from 'uuid'; + +import { RequestUser, UserId, UserList } from '../models/user/usersModel'; +import { DBCommands } from '../types/enums'; +import connectDB from '../utils/connectDB'; +import isMulti from '../utils/isMulti'; + +const isWorkerThread = isMulti() && !cluster.isPrimary; + +class DB { + private readonly users: UserList = []; + + public getUserList = async () => { + if (isWorkerThread) { + return connectDB(DBCommands.GET_USER_LIST); + } + + return this.users; + }; + + public getUser = async (id: UserId | null) => { + if (isWorkerThread) { + return connectDB(DBCommands.GET_USER, id); + } + + const user = this.users.find((usr) => usr.id === id) ?? null; + return user; + }; + + public createUser = async (userData: RequestUser) => { + if (isWorkerThread) { + return connectDB(DBCommands.CREATE_USER, userData); + } + + const user = { id: uuid.v4(), ...userData }; + this.users.push(user); + return user; + }; + + public updateUser = async (id: UserId | null, userData: RequestUser) => { + if (isWorkerThread) { + return connectDB(DBCommands.UPDATE_USER, id, userData); + } + + const relatedUser = this.users.find((usr) => usr.id === id); + if (!relatedUser) return null; + + const relatedUserIndex = this.users.findIndex((usr) => usr.id === id); + const updatedUser = { ...relatedUser, ...userData }; + this.users[relatedUserIndex] = updatedUser; + + return updatedUser; + }; + + public deleteUser = async (id: UserId | null) => { + if (isWorkerThread) { + return connectDB(DBCommands.DELETE_USER, id); + } + + const userDeleteIndex = this.users.findIndex((usr) => usr.id === id); + if (userDeleteIndex === -1) return false; + this.users.splice(userDeleteIndex, 1); + return true; + }; +} + +export default new DB(); diff --git a/types/enums.ts b/types/enums.ts index 0b9a2ca..e0408ab 100644 --- a/types/enums.ts +++ b/types/enums.ts @@ -19,3 +19,11 @@ export enum Routes { USERS = '/api/users', USERS_ID = '/api/users/:id', } + +export enum DBCommands { + GET_USER_LIST = 'getUserList', + GET_USER = 'getUser', + CREATE_USER = 'createUser', + UPDATE_USER = 'updateUser', + DELETE_USER = 'deleteUser', +} diff --git a/utils/connectDB.ts b/utils/connectDB.ts new file mode 100644 index 0000000..c929b3f --- /dev/null +++ b/utils/connectDB.ts @@ -0,0 +1,27 @@ +import { User, UserList } from '../models/user/usersModel'; +import { DBCommands } from '../types/enums'; + +/** + * Connects to the database and executes the given command with the given arguments. + * @template - TData The type of the data to be returned, which must extend User or UserList. + * @param {DBCommands} command - The command to be executed on the database. + * @param {...unknown[]} args - The arguments to be passed to the command. + * @returns {Promise | never} - A promise that resolves to the data of type TData, or throws an error if no response is received from the database. + * @throws - Error is thrown if database returns nothing + */ +const connectDB = ( + command: DBCommands, + ...args: unknown[] +): Promise | never => + new Promise((resolve, reject) => { + process.send?.({ command, args }); + process.on('message', (msg: { res: TData }) => { + if ('res' in msg) { + resolve(msg.res); + } else { + reject(new Error('Could not get response from DB!')); + } + }); + }); + +export default connectDB; From dc20479a73e8ac846105f22975069329205232dd Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sun, 11 Feb 2024 10:57:54 +0200 Subject: [PATCH 082/114] refactor: change CB type to include async behaviour --- types/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/types.ts b/types/types.ts index 6cec421..6ed8f01 100644 --- a/types/types.ts +++ b/types/types.ts @@ -18,7 +18,7 @@ export type ExtendedReq = Req & { body: RequestBody; }; -export type Cb = (req: ExtendedReq, res: ExtendedRes) => void; +export type Cb = (req: ExtendedReq, res: ExtendedRes) => void | Promise; export type RouteTable = Record>; From ae168acf727ba419ce7dd8c059e83c8b4b97d50b Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sun, 11 Feb 2024 10:58:33 +0200 Subject: [PATCH 083/114] refactor: replace clear data with new DB API --- db/db.ts | 4 ++++ test/api-scenario-1.test.ts | 4 ++-- test/api-scenario-2.test.ts | 4 ++-- test/api-scenario-3.test.ts | 4 ++-- utils/clearData.ts | 11 ----------- 5 files changed, 10 insertions(+), 17 deletions(-) delete mode 100644 utils/clearData.ts diff --git a/db/db.ts b/db/db.ts index 8b23301..622afe4 100644 --- a/db/db.ts +++ b/db/db.ts @@ -64,6 +64,10 @@ class DB { this.users.splice(userDeleteIndex, 1); return true; }; + + public clearData = () => { + this.users.splice(0); + }; } export default new DB(); diff --git a/test/api-scenario-1.test.ts b/test/api-scenario-1.test.ts index 3c9a75a..1e58367 100644 --- a/test/api-scenario-1.test.ts +++ b/test/api-scenario-1.test.ts @@ -9,9 +9,9 @@ import { notFound, updateUser, } from '../controllers/usersController'; +import db from '../db/db'; import { Routes, StatusCode } from '../types/enums'; import App from '../utils/app'; -import clearData from '../utils/clearData'; import { validateId } from '../utils/validateId'; const app = new App(); @@ -35,7 +35,7 @@ const updatedUserData = { let uploadUserId = ''; describe('Test scenario 1', () => { - afterAll(clearData); + afterAll(db.clearData); it('GET api/users', async () => { const res = await server.get(Routes.USERS).expect(StatusCode.SUCCESS); diff --git a/test/api-scenario-2.test.ts b/test/api-scenario-2.test.ts index 724aaaf..d34b93c 100644 --- a/test/api-scenario-2.test.ts +++ b/test/api-scenario-2.test.ts @@ -9,9 +9,9 @@ import { notFound, updateUser, } from '../controllers/usersController'; +import db from '../db/db'; import { Routes, StatusCode } from '../types/enums'; import App from '../utils/app'; -import clearData from '../utils/clearData'; import { validateId } from '../utils/validateId'; const app = new App(); @@ -30,7 +30,7 @@ const updatedUserData = { const uploadUserId = ''; describe('Test scenario 2', () => { - afterAll(clearData); + afterAll(db.clearData); it('GET api/users should return empty array of users', async () => { const res = await server.get(Routes.USERS).expect(StatusCode.SUCCESS); diff --git a/test/api-scenario-3.test.ts b/test/api-scenario-3.test.ts index 28b3657..d9cd3bc 100644 --- a/test/api-scenario-3.test.ts +++ b/test/api-scenario-3.test.ts @@ -9,9 +9,9 @@ import { notFound, updateUser, } from '../controllers/usersController'; +import db from '../db/db'; import { Routes, StatusCode } from '../types/enums'; import App from '../utils/app'; -import clearData from '../utils/clearData'; import { validateId } from '../utils/validateId'; const app = new App(); @@ -30,7 +30,7 @@ const uploadUserData = { let uploadUserId = ''; describe('Test scenario 2', () => { - afterAll(clearData); + afterAll(db.clearData); it('GET api/users should return empty array of users', async () => { const res = await server.get(Routes.USERS).expect(StatusCode.SUCCESS); diff --git a/utils/clearData.ts b/utils/clearData.ts deleted file mode 100644 index e171ce3..0000000 --- a/utils/clearData.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { users } from '../data/data'; - -/** - * Clears the users data by removing all elements. - * @return {void} - Returns nothing. - */ -const clearData = () => { - users.splice(0); -}; - -export default clearData; From 99f21bce68c013905aae02f2bf8eebb07ec6e031 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sun, 11 Feb 2024 10:59:57 +0200 Subject: [PATCH 084/114] refactor: rename function from "connectDB" to "connectMasterDB" --- db/db.ts | 12 ++++++------ utils/{connectDB.ts => connectMasterDB.ts} | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) rename utils/{connectDB.ts => connectMasterDB.ts} (91%) diff --git a/db/db.ts b/db/db.ts index 622afe4..fd5c4d6 100644 --- a/db/db.ts +++ b/db/db.ts @@ -4,7 +4,7 @@ import * as uuid from 'uuid'; import { RequestUser, UserId, UserList } from '../models/user/usersModel'; import { DBCommands } from '../types/enums'; -import connectDB from '../utils/connectDB'; +import connectMasterDB from '../utils/connectMasterDB'; import isMulti from '../utils/isMulti'; const isWorkerThread = isMulti() && !cluster.isPrimary; @@ -14,7 +14,7 @@ class DB { public getUserList = async () => { if (isWorkerThread) { - return connectDB(DBCommands.GET_USER_LIST); + return connectMasterDB(DBCommands.GET_USER_LIST); } return this.users; @@ -22,7 +22,7 @@ class DB { public getUser = async (id: UserId | null) => { if (isWorkerThread) { - return connectDB(DBCommands.GET_USER, id); + return connectMasterDB(DBCommands.GET_USER, id); } const user = this.users.find((usr) => usr.id === id) ?? null; @@ -31,7 +31,7 @@ class DB { public createUser = async (userData: RequestUser) => { if (isWorkerThread) { - return connectDB(DBCommands.CREATE_USER, userData); + return connectMasterDB(DBCommands.CREATE_USER, userData); } const user = { id: uuid.v4(), ...userData }; @@ -41,7 +41,7 @@ class DB { public updateUser = async (id: UserId | null, userData: RequestUser) => { if (isWorkerThread) { - return connectDB(DBCommands.UPDATE_USER, id, userData); + return connectMasterDB(DBCommands.UPDATE_USER, id, userData); } const relatedUser = this.users.find((usr) => usr.id === id); @@ -56,7 +56,7 @@ class DB { public deleteUser = async (id: UserId | null) => { if (isWorkerThread) { - return connectDB(DBCommands.DELETE_USER, id); + return connectMasterDB(DBCommands.DELETE_USER, id); } const userDeleteIndex = this.users.findIndex((usr) => usr.id === id); diff --git a/utils/connectDB.ts b/utils/connectMasterDB.ts similarity index 91% rename from utils/connectDB.ts rename to utils/connectMasterDB.ts index c929b3f..a61a6e5 100644 --- a/utils/connectDB.ts +++ b/utils/connectMasterDB.ts @@ -9,7 +9,7 @@ import { DBCommands } from '../types/enums'; * @returns {Promise | never} - A promise that resolves to the data of type TData, or throws an error if no response is received from the database. * @throws - Error is thrown if database returns nothing */ -const connectDB = ( +const connectMasterDB = ( command: DBCommands, ...args: unknown[] ): Promise | never => @@ -24,4 +24,4 @@ const connectDB = ( }); }); -export default connectDB; +export default connectMasterDB; From 96fdabe04fef00bc9ea232d689f1196d5d145146 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sun, 11 Feb 2024 11:06:15 +0200 Subject: [PATCH 085/114] refactor: abstract workers initialization in separate function --- utils/initWorkers.ts | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 utils/initWorkers.ts diff --git a/utils/initWorkers.ts b/utils/initWorkers.ts new file mode 100644 index 0000000..da5fa19 --- /dev/null +++ b/utils/initWorkers.ts @@ -0,0 +1,37 @@ +import cluster from 'cluster'; +import { availableParallelism } from 'node:os'; + +import db from '../db/db'; +import { WorkerMessage } from '../types/types'; + +// TODO: Move port to constants +const NUM_CPUS = availableParallelism(); +const port = Number(process.env.PORT); + +/** + * Initializes the cluster workers based on CPU cores with instance of the app on master PORT + n. + * @returns {number[]} An array of the ports assigned to the workers. + */ +const initWorkers = () => { + const workerPorts: number[] = []; + + for (let i = 1; i < NUM_CPUS; i += 1) { + const workersPort = port + i; + const worker = cluster.fork({ PORT: workersPort }); + workerPorts.push(workersPort); + + worker.on('message', (msg: WorkerMessage) => { + if (!('command' in msg)) return; + + const workerArgs = msg.args; + const dbAction = db[msg.command]; + dbAction(...workerArgs).then((data) => { + worker.send({ res: data }); + }); + }); + } + + return workerPorts; +}; + +export default initWorkers; From 7b5c80cdc971f681169f910ce953a68a8e44928f Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sun, 11 Feb 2024 11:06:48 +0200 Subject: [PATCH 086/114] refactor: abstract which worker will handle next request logic in separate fn --- utils/defineNextWorker.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 utils/defineNextWorker.ts diff --git a/utils/defineNextWorker.ts b/utils/defineNextWorker.ts new file mode 100644 index 0000000..6e588f4 --- /dev/null +++ b/utils/defineNextWorker.ts @@ -0,0 +1,26 @@ +const host = process.env.HOST; +let currentWorkerNum = 0; + +/** + * Defines the next worker to forward the request to, based on the current worker number using Round-robin algorithm. + * @param {number[]} workerPorts - An array of the all worker ports. + * @returns {WorkerRequestData} - An object containing the hostname and port of the next worker. + */ +const defineNextWorker = (workerPorts: number[]) => { + const isLastWorker = currentWorkerNum === workerPorts.length - 1; + const currWorkerPort = workerPorts[currentWorkerNum]; + const workerCreds = { + hostname: host, + port: currWorkerPort, + }; + + if (isLastWorker) { + currentWorkerNum = 0; + } else { + currentWorkerNum += 1; + } + + return workerCreds; +}; + +export default defineNextWorker; From dcc29cb6a526430ee3a3e4d170ec29a0ad348474 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sun, 11 Feb 2024 11:08:07 +0200 Subject: [PATCH 087/114] refactor: clean up server file --- server.ts | 39 ++++++--------------------------------- 1 file changed, 6 insertions(+), 33 deletions(-) diff --git a/server.ts b/server.ts index 512b931..0cecf71 100644 --- a/server.ts +++ b/server.ts @@ -1,7 +1,6 @@ import 'dotenv/config'; import cluster from 'cluster'; import http from 'http'; -import { availableParallelism } from 'node:os'; import { createUser, @@ -11,60 +10,34 @@ import { notFound, updateUser, } from './controllers/usersController'; -import db from './db/db'; import { Routes } from './types/enums'; -import { WorkerMessage } from './types/types'; import App from './utils/app'; +import defineNextWorker from './utils/defineNextWorker'; import forwardRequest from './utils/forwardRequest'; +import initWorkers from './utils/initWorkers'; import isMulti from './utils/isMulti'; import { validateId } from './utils/validateId'; const { isPrimary } = cluster; -const numCPUs = availableParallelism(); const port = Number(process.env.PORT); const host = process.env.HOST; -const workerPorts: number[] = []; const isMultiMode = isMulti(); const isMasterProcess = isPrimary && isMultiMode; -let currentWorkerNum = 0; +let workerPorts: number[] = []; if (isMasterProcess) { // Master process // load balancer code + workerPorts = initWorkers(); + http .createServer((req, res) => { - const workerCreds = { - hostname: host, - port: workerPorts[currentWorkerNum], - }; - + const workerCreds = defineNextWorker(workerPorts); forwardRequest(req, res, workerCreds); - - if (currentWorkerNum === workerPorts.length - 1) { - currentWorkerNum = 0; - return; - } - currentWorkerNum += 1; }) .listen(port, host, () => { process.stdout.write(`The load balancer is running on port ${port}...\n`); }); - - for (let i = 1; i < numCPUs; i += 1) { - const workersPort = port + i; - const worker = cluster.fork({ PORT: workersPort }); - workerPorts.push(workersPort); - - worker.on('message', (msg: WorkerMessage) => { - if (!('command' in msg)) return; - - const workerArgs = msg.args; - const dbAction = db[msg.command]; - dbAction(...workerArgs).then((data) => { - worker.send({ res: data }); - }); - }); - } } else if (isMultiMode) { // Worker's code const app = new App(); From be0b55e4e6accafaae1efbd14eaabe0e079e328c Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sun, 11 Feb 2024 11:29:50 +0200 Subject: [PATCH 088/114] refactor: encapsulate app initialization in separate fn --- server.ts | 39 +++------------------------------------ utils/initApp.ts | 29 +++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 36 deletions(-) create mode 100644 utils/initApp.ts diff --git a/server.ts b/server.ts index 0cecf71..06a7364 100644 --- a/server.ts +++ b/server.ts @@ -2,21 +2,11 @@ import 'dotenv/config'; import cluster from 'cluster'; import http from 'http'; -import { - createUser, - deleteUser, - getUser, - getUserList, - notFound, - updateUser, -} from './controllers/usersController'; -import { Routes } from './types/enums'; -import App from './utils/app'; import defineNextWorker from './utils/defineNextWorker'; import forwardRequest from './utils/forwardRequest'; +import initApp from './utils/initApp'; import initWorkers from './utils/initWorkers'; import isMulti from './utils/isMulti'; -import { validateId } from './utils/validateId'; const { isPrimary } = cluster; @@ -27,7 +17,7 @@ const isMasterProcess = isPrimary && isMultiMode; let workerPorts: number[] = []; if (isMasterProcess) { - // Master process // load balancer code + // Master process | load balancer code workerPorts = initWorkers(); http @@ -38,29 +28,6 @@ if (isMasterProcess) { .listen(port, host, () => { process.stdout.write(`The load balancer is running on port ${port}...\n`); }); -} else if (isMultiMode) { - // Worker's code - const app = new App(); - const workerPort = Number(process.env.PORT); - - app.use(validateId); - app.route(Routes.USERS).get(getUserList).post(createUser); - app.route(Routes.USERS_ID).get(getUser).put(updateUser).delete(deleteUser); - app.use(notFound); - - app.listen(workerPort, host, () => { - process.stdout.write(`App running on port ${workerPort}...\n`); - }); } else { - // Base app - const app = new App(); - - app.use(validateId); - app.route(Routes.USERS).get(getUserList).post(createUser); - app.route(Routes.USERS_ID).get(getUser).put(updateUser).delete(deleteUser); - app.use(notFound); - - app.listen(port, host, () => { - process.stdout.write(`App running on port ${port}...`); - }); + initApp(); } diff --git a/utils/initApp.ts b/utils/initApp.ts new file mode 100644 index 0000000..e9a558e --- /dev/null +++ b/utils/initApp.ts @@ -0,0 +1,29 @@ +import App from './app'; +import { validateId } from './validateId'; +import { + createUser, + deleteUser, + getUser, + getUserList, + notFound, + updateUser, +} from '../controllers/usersController'; +import { Routes } from '../types/enums'; + +const host = process.env.HOST; + +const initApp = () => { + const app = new App(); + const port = Number(process.env.PORT); + + app.use(validateId); + app.route(Routes.USERS).get(getUserList).post(createUser); + app.route(Routes.USERS_ID).get(getUser).put(updateUser).delete(deleteUser); + app.use(notFound); + + app.listen(port, host, () => { + process.stdout.write(`App running on port ${port}...\n`); + }); +}; + +export default initApp; From b28c7f8c42d3b2ce149e2113e24c5fe596516910 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sun, 11 Feb 2024 11:41:58 +0200 Subject: [PATCH 089/114] refactor: rearrange files --- db/db.ts | 4 +-- lib/const.ts | 2 ++ {utils => lib/utils}/app.ts | 5 +-- {utils => lib/utils}/connectMasterDB.ts | 4 +-- {utils => lib/utils}/defineNextWorker.ts | 0 {utils => lib/utils}/forwardRequest.ts | 2 +- {utils => lib/utils}/initWorkers.ts | 4 +-- {utils => lib/utils}/isMulti.ts | 0 {utils => lib/utils}/response.ts | 2 +- {utils => lib/utils}/route.ts | 9 ++++-- {utils => lib/utils}/validateId.ts | 4 +-- server.ts | 39 +++++++++++++++++------- test/api-scenario-1.test.ts | 4 +-- test/api-scenario-2.test.ts | 4 +-- test/api-scenario-3.test.ts | 4 +-- types/types.ts | 2 +- utils/initApp.ts | 29 ------------------ 17 files changed, 57 insertions(+), 61 deletions(-) create mode 100644 lib/const.ts rename {utils => lib/utils}/app.ts (97%) rename {utils => lib/utils}/connectMasterDB.ts (89%) rename {utils => lib/utils}/defineNextWorker.ts (100%) rename {utils => lib/utils}/forwardRequest.ts (92%) rename {utils => lib/utils}/initWorkers.ts (91%) rename {utils => lib/utils}/isMulti.ts (100%) rename {utils => lib/utils}/response.ts (94%) rename {utils => lib/utils}/route.ts (93%) rename {utils => lib/utils}/validateId.ts (72%) delete mode 100644 utils/initApp.ts diff --git a/db/db.ts b/db/db.ts index fd5c4d6..dda841c 100644 --- a/db/db.ts +++ b/db/db.ts @@ -2,10 +2,10 @@ import cluster from 'cluster'; import * as uuid from 'uuid'; +import connectMasterDB from '../lib/utils/connectMasterDB'; +import isMulti from '../lib/utils/isMulti'; import { RequestUser, UserId, UserList } from '../models/user/usersModel'; import { DBCommands } from '../types/enums'; -import connectMasterDB from '../utils/connectMasterDB'; -import isMulti from '../utils/isMulti'; const isWorkerThread = isMulti() && !cluster.isPrimary; diff --git a/lib/const.ts b/lib/const.ts new file mode 100644 index 0000000..8c28293 --- /dev/null +++ b/lib/const.ts @@ -0,0 +1,2 @@ +export const { HOST } = process.env; +export const PORT = Number(process.env.PORT); diff --git a/utils/app.ts b/lib/utils/app.ts similarity index 97% rename from utils/app.ts rename to lib/utils/app.ts index dfddb55..d26bdd4 100644 --- a/utils/app.ts +++ b/lib/utils/app.ts @@ -2,7 +2,7 @@ import http, { Server } from 'http'; import Response, { Res } from './response'; import Route from './route'; -import { HttpMethods } from '../types/enums'; +import { HttpMethods } from '../../types/enums'; import { Cb, ExtendedReq, @@ -11,7 +11,7 @@ import { MiddlewareQueue, Req, RequestBody, -} from '../types/types'; +} from '../../types/types'; class App { private readonly handlersTable: HandlersTable = {}; @@ -61,6 +61,7 @@ class App { // In order to make sure that if .use() method called BEFORE any route // e.g. ID validation, we want it to run right before route handler (get, post, put...) + // FIXME: remove eslint disable line // eslint-disable-next-line for await (const middleware of this.middlewareQueue) { if (typeof middleware === 'function') { diff --git a/utils/connectMasterDB.ts b/lib/utils/connectMasterDB.ts similarity index 89% rename from utils/connectMasterDB.ts rename to lib/utils/connectMasterDB.ts index a61a6e5..741958a 100644 --- a/utils/connectMasterDB.ts +++ b/lib/utils/connectMasterDB.ts @@ -1,5 +1,5 @@ -import { User, UserList } from '../models/user/usersModel'; -import { DBCommands } from '../types/enums'; +import { User, UserList } from '../../models/user/usersModel'; +import { DBCommands } from '../../types/enums'; /** * Connects to the database and executes the given command with the given arguments. diff --git a/utils/defineNextWorker.ts b/lib/utils/defineNextWorker.ts similarity index 100% rename from utils/defineNextWorker.ts rename to lib/utils/defineNextWorker.ts diff --git a/utils/forwardRequest.ts b/lib/utils/forwardRequest.ts similarity index 92% rename from utils/forwardRequest.ts rename to lib/utils/forwardRequest.ts index a0269ee..461be40 100644 --- a/utils/forwardRequest.ts +++ b/lib/utils/forwardRequest.ts @@ -1,7 +1,7 @@ import http from 'http'; import { Res } from './response'; -import { Req, WorkerRequestData } from '../types/types'; +import { Req, WorkerRequestData } from '../../types/types'; /** * Forwards the request to the worker with the given data. diff --git a/utils/initWorkers.ts b/lib/utils/initWorkers.ts similarity index 91% rename from utils/initWorkers.ts rename to lib/utils/initWorkers.ts index da5fa19..6d935c1 100644 --- a/utils/initWorkers.ts +++ b/lib/utils/initWorkers.ts @@ -1,8 +1,8 @@ import cluster from 'cluster'; import { availableParallelism } from 'node:os'; -import db from '../db/db'; -import { WorkerMessage } from '../types/types'; +import db from '../../db/db'; +import { WorkerMessage } from '../../types/types'; // TODO: Move port to constants const NUM_CPUS = availableParallelism(); diff --git a/utils/isMulti.ts b/lib/utils/isMulti.ts similarity index 100% rename from utils/isMulti.ts rename to lib/utils/isMulti.ts diff --git a/utils/response.ts b/lib/utils/response.ts similarity index 94% rename from utils/response.ts rename to lib/utils/response.ts index 5398580..cad87d3 100644 --- a/utils/response.ts +++ b/lib/utils/response.ts @@ -1,6 +1,6 @@ import { IncomingMessage, ServerResponse } from 'http'; -import { StatusCode } from '../types/enums'; +import { StatusCode } from '../../types/enums'; export type Res = ServerResponse & { req: IncomingMessage; diff --git a/utils/route.ts b/lib/utils/route.ts similarity index 93% rename from utils/route.ts rename to lib/utils/route.ts index fdca9a2..a97fbdd 100644 --- a/utils/route.ts +++ b/lib/utils/route.ts @@ -1,5 +1,10 @@ -import { HttpMethods } from '../types/enums'; -import { Cb, HandlersTable, MiddlewareQueue, RouteTable } from '../types/types'; +import { HttpMethods } from '../../types/enums'; +import { + Cb, + HandlersTable, + MiddlewareQueue, + RouteTable, +} from '../../types/types'; class Route { private readonly route: string; diff --git a/utils/validateId.ts b/lib/utils/validateId.ts similarity index 72% rename from utils/validateId.ts rename to lib/utils/validateId.ts index 91293e7..0e788c8 100644 --- a/utils/validateId.ts +++ b/lib/utils/validateId.ts @@ -1,7 +1,7 @@ import * as uuid from 'uuid'; -import { StatusCode } from '../types/enums'; -import { ExtendedReq, ExtendedRes } from '../types/types'; +import { StatusCode } from '../../types/enums'; +import { ExtendedReq, ExtendedRes } from '../../types/types'; export const validateId = (req: ExtendedReq, res: ExtendedRes) => { const id = req.route?.id; diff --git a/server.ts b/server.ts index 06a7364..1e836ed 100644 --- a/server.ts +++ b/server.ts @@ -2,16 +2,24 @@ import 'dotenv/config'; import cluster from 'cluster'; import http from 'http'; -import defineNextWorker from './utils/defineNextWorker'; -import forwardRequest from './utils/forwardRequest'; -import initApp from './utils/initApp'; -import initWorkers from './utils/initWorkers'; -import isMulti from './utils/isMulti'; +import { + createUser, + deleteUser, + getUser, + getUserList, + notFound, + updateUser, +} from './controllers/usersController'; +import { HOST, PORT } from './lib/const'; +import App from './lib/utils/app'; +import defineNextWorker from './lib/utils/defineNextWorker'; +import forwardRequest from './lib/utils/forwardRequest'; +import initWorkers from './lib/utils/initWorkers'; +import isMulti from './lib/utils/isMulti'; +import { validateId } from './lib/utils/validateId'; +import { Routes } from './types/enums'; const { isPrimary } = cluster; - -const port = Number(process.env.PORT); -const host = process.env.HOST; const isMultiMode = isMulti(); const isMasterProcess = isPrimary && isMultiMode; let workerPorts: number[] = []; @@ -25,9 +33,18 @@ if (isMasterProcess) { const workerCreds = defineNextWorker(workerPorts); forwardRequest(req, res, workerCreds); }) - .listen(port, host, () => { - process.stdout.write(`The load balancer is running on port ${port}...\n`); + .listen(PORT, HOST, () => { + process.stdout.write(`The load balancer is running on port ${PORT}...\n`); }); } else { - initApp(); + const app = new App(); + + app.use(validateId); + app.route(Routes.USERS).get(getUserList).post(createUser); + app.route(Routes.USERS_ID).get(getUser).put(updateUser).delete(deleteUser); + app.use(notFound); + + app.listen(PORT, HOST, () => { + process.stdout.write(`App running on port ${PORT}...\n`); + }); } diff --git a/test/api-scenario-1.test.ts b/test/api-scenario-1.test.ts index 1e58367..e76c5cf 100644 --- a/test/api-scenario-1.test.ts +++ b/test/api-scenario-1.test.ts @@ -10,9 +10,9 @@ import { updateUser, } from '../controllers/usersController'; import db from '../db/db'; +import App from '../lib/utils/app'; +import { validateId } from '../lib/utils/validateId'; import { Routes, StatusCode } from '../types/enums'; -import App from '../utils/app'; -import { validateId } from '../utils/validateId'; const app = new App(); diff --git a/test/api-scenario-2.test.ts b/test/api-scenario-2.test.ts index d34b93c..253a6e8 100644 --- a/test/api-scenario-2.test.ts +++ b/test/api-scenario-2.test.ts @@ -10,9 +10,9 @@ import { updateUser, } from '../controllers/usersController'; import db from '../db/db'; +import App from '../lib/utils/app'; +import { validateId } from '../lib/utils/validateId'; import { Routes, StatusCode } from '../types/enums'; -import App from '../utils/app'; -import { validateId } from '../utils/validateId'; const app = new App(); diff --git a/test/api-scenario-3.test.ts b/test/api-scenario-3.test.ts index d9cd3bc..60f15ea 100644 --- a/test/api-scenario-3.test.ts +++ b/test/api-scenario-3.test.ts @@ -10,9 +10,9 @@ import { updateUser, } from '../controllers/usersController'; import db from '../db/db'; +import App from '../lib/utils/app'; +import { validateId } from '../lib/utils/validateId'; import { Routes, StatusCode } from '../types/enums'; -import App from '../utils/app'; -import { validateId } from '../utils/validateId'; const app = new App(); diff --git a/types/types.ts b/types/types.ts index 6ed8f01..f49b582 100644 --- a/types/types.ts +++ b/types/types.ts @@ -1,8 +1,8 @@ import { IncomingMessage } from 'http'; import { DBCommands, HttpMethods } from './enums'; +import { JsonFn, Res, StatusFn } from '../lib/utils/response'; import { RequestUser } from '../models/user/usersModel'; -import { JsonFn, Res, StatusFn } from '../utils/response'; export type Req = IncomingMessage; diff --git a/utils/initApp.ts b/utils/initApp.ts deleted file mode 100644 index e9a558e..0000000 --- a/utils/initApp.ts +++ /dev/null @@ -1,29 +0,0 @@ -import App from './app'; -import { validateId } from './validateId'; -import { - createUser, - deleteUser, - getUser, - getUserList, - notFound, - updateUser, -} from '../controllers/usersController'; -import { Routes } from '../types/enums'; - -const host = process.env.HOST; - -const initApp = () => { - const app = new App(); - const port = Number(process.env.PORT); - - app.use(validateId); - app.route(Routes.USERS).get(getUserList).post(createUser); - app.route(Routes.USERS_ID).get(getUser).put(updateUser).delete(deleteUser); - app.use(notFound); - - app.listen(port, host, () => { - process.stdout.write(`App running on port ${port}...\n`); - }); -}; - -export default initApp; From 0ef07650e394372efeb37a52656ab8b74e9b217c Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sun, 11 Feb 2024 11:46:21 +0200 Subject: [PATCH 090/114] refactor: replace port with const variable --- lib/utils/initWorkers.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/utils/initWorkers.ts b/lib/utils/initWorkers.ts index 6d935c1..470ecc7 100644 --- a/lib/utils/initWorkers.ts +++ b/lib/utils/initWorkers.ts @@ -3,10 +3,9 @@ import { availableParallelism } from 'node:os'; import db from '../../db/db'; import { WorkerMessage } from '../../types/types'; +import { PORT } from '../const'; -// TODO: Move port to constants const NUM_CPUS = availableParallelism(); -const port = Number(process.env.PORT); /** * Initializes the cluster workers based on CPU cores with instance of the app on master PORT + n. @@ -16,7 +15,7 @@ const initWorkers = () => { const workerPorts: number[] = []; for (let i = 1; i < NUM_CPUS; i += 1) { - const workersPort = port + i; + const workersPort = PORT + i; const worker = cluster.fork({ PORT: workersPort }); workerPorts.push(workersPort); From 3d3994dab46d2eb16c841263f8c40296a6ce07d8 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sun, 11 Feb 2024 11:50:15 +0200 Subject: [PATCH 091/114] fix: eslint rule --- .eslintrc.cjs | 1 + lib/utils/app.ts | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index b214a89..55e2ac2 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -28,6 +28,7 @@ module.exports = { '@typescript-eslint/no-var-requires': 'off', 'class-methods-use-this': 'off', 'no-process-exit': 'off', + 'no-restricted-syntax': 'off', 'node/no-unpublished-import': 'off', 'node/no-missing-import': 'off', 'node/no-unsupported-features/es-syntax': [ diff --git a/lib/utils/app.ts b/lib/utils/app.ts index d26bdd4..8e612c8 100644 --- a/lib/utils/app.ts +++ b/lib/utils/app.ts @@ -60,9 +60,6 @@ class App { // using middlewareQueue with routeTable inside. // In order to make sure that if .use() method called BEFORE any route // e.g. ID validation, we want it to run right before route handler (get, post, put...) - - // FIXME: remove eslint disable line - // eslint-disable-next-line for await (const middleware of this.middlewareQueue) { if (typeof middleware === 'function') { middleware(this.req, this.res); From 84c54d30f234621aeb6dfdc897a0e39bf7077f6a Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sun, 11 Feb 2024 11:59:26 +0200 Subject: [PATCH 092/114] refactor: add user controllers JSDoc comments --- controllers/usersController.ts | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/controllers/usersController.ts b/controllers/usersController.ts index 0105355..91df178 100644 --- a/controllers/usersController.ts +++ b/controllers/usersController.ts @@ -4,10 +4,14 @@ import isUser from '../models/user/lib/utils/isUser'; import { StatusCode } from '../types/enums'; import { ExtendedReq, ExtendedRes, Req } from '../types/types'; -// TODO: add JSDoc comments // TODO: add emoji response // TODO: handle server errors +/** + * Gets the list of users from the database and sends it as a JSON response. + * @param {ExtendedReq} _req - The incoming request object, which is not used in this function. + * @param {ExtendedRes} res - The outgoing response object, which is used to send the JSON response. + */ export const getUserList = async (_req: ExtendedReq, res: ExtendedRes) => { const users = await db.getUserList(); @@ -20,6 +24,11 @@ export const getUserList = async (_req: ExtendedReq, res: ExtendedRes) => { }); }; +/** + * Gets the user with the given id from the database and sends it as a JSON response. + * @param {ExtendedReq} req - The incoming request object, which contains the id of the user in the route property. + * @param {ExtendedRes} res - The outgoing response object, which is used to send the JSON response. + */ export const getUser = async (req: ExtendedReq, res: ExtendedRes) => { const { id } = req.route; const user = await db.getUser(id); @@ -41,6 +50,11 @@ export const getUser = async (req: ExtendedReq, res: ExtendedRes) => { }); }; +/** + * Creates a new user in the database with the given data and sends it as a JSON response. + * @param {ExtendedReq} req - The incoming request object, which contains the user data in the body property. + * @param {ExtendedRes} res - The outgoing response object, which is used to send the JSON response. + */ export const createUser = async (req: ExtendedReq, res: ExtendedRes) => { const { body } = req; @@ -65,6 +79,11 @@ export const createUser = async (req: ExtendedReq, res: ExtendedRes) => { }); }; +/** + * Updates the user with the given id in the database with the given data and sends it as a JSON response. + * @param {ExtendedReq} req - The incoming request object, which contains the user data in the body property and the user id in the route property. + * @param {ExtendedRes} res - The outgoing response object, which is used to send the JSON response. + */ export const updateUser = async (req: ExtendedReq, res: ExtendedRes) => { const { body, @@ -101,6 +120,11 @@ export const updateUser = async (req: ExtendedReq, res: ExtendedRes) => { }); }; +/** + * Deletes the user with the given id from the database and sends a JSON response. + * @param {ExtendedReq} req - The incoming request object, which contains the id of the user in the route property. + * @param {ExtendedRes} res - The outgoing response object, which is used to send the JSON response. + */ export const deleteUser = async (req: ExtendedReq, res: ExtendedRes) => { const { id } = req.route; const isDeleted = await db.deleteUser(id); @@ -120,6 +144,11 @@ export const deleteUser = async (req: ExtendedReq, res: ExtendedRes) => { }); }; +/** + * Sends a JSON response with a 404 status code and a message indicating that the route does not exist. + * @param {Req} _req - The incoming request object, which is not used in this function. + * @param {ExtendedRes} res - The outgoing response object, which is used to send the JSON response. + */ export const notFound = async (_req: Req, res: ExtendedRes) => { res.status(StatusCode.NOT_FOUND).json({ status: 'fail', From c36a92b01cc9ceefb209b884621f0c2ed95b0ec6 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sun, 11 Feb 2024 12:57:07 +0200 Subject: [PATCH 093/114] refactor: handle internal server errors --- controllers/usersController.ts | 206 ++++++++++++++++++++------------- 1 file changed, 126 insertions(+), 80 deletions(-) diff --git a/controllers/usersController.ts b/controllers/usersController.ts index 91df178..43d3127 100644 --- a/controllers/usersController.ts +++ b/controllers/usersController.ts @@ -1,3 +1,5 @@ +import { isNativeError } from 'node:util/types'; + import db from '../db/db'; import findMissingFields from '../models/user/lib/utils/findMissingFields'; import isUser from '../models/user/lib/utils/isUser'; @@ -5,7 +7,6 @@ import { StatusCode } from '../types/enums'; import { ExtendedReq, ExtendedRes, Req } from '../types/types'; // TODO: add emoji response -// TODO: handle server errors /** * Gets the list of users from the database and sends it as a JSON response. @@ -13,15 +14,24 @@ import { ExtendedReq, ExtendedRes, Req } from '../types/types'; * @param {ExtendedRes} res - The outgoing response object, which is used to send the JSON response. */ export const getUserList = async (_req: ExtendedReq, res: ExtendedRes) => { - const users = await db.getUserList(); - - res.status(StatusCode.SUCCESS).json({ - status: 'success', - results: users.length, - data: { - users, - }, - }); + try { + const users = await db.getUserList(); + + res.status(StatusCode.SUCCESS).json({ + status: 'success', + results: users.length, + data: { + users, + }, + }); + } catch (e) { + if (!isNativeError(e)) return; + + res.status(StatusCode.INTERNAL_SERVER_ERROR).json({ + status: 'fail', + message: `๐Ÿ’ฅ Internal server error! (${e.message})`, + }); + } }; /** @@ -30,24 +40,33 @@ export const getUserList = async (_req: ExtendedReq, res: ExtendedRes) => { * @param {ExtendedRes} res - The outgoing response object, which is used to send the JSON response. */ export const getUser = async (req: ExtendedReq, res: ExtendedRes) => { - const { id } = req.route; - const user = await db.getUser(id); + try { + const { id } = req.route; + const user = await db.getUser(id); + + if (!user) { + res.status(StatusCode.NOT_FOUND).json({ + status: 'fail', + message: 'User not found', + }); + + return; + } + + res.status(StatusCode.SUCCESS).json({ + status: 'success', + data: { + user, + }, + }); + } catch (e) { + if (!isNativeError(e)) return; - if (!user) { - res.status(StatusCode.NOT_FOUND).json({ + res.status(StatusCode.INTERNAL_SERVER_ERROR).json({ status: 'fail', - message: 'User not found', + message: `๐Ÿ’ฅ Internal server error! (${e.message})`, }); - - return; } - - res.status(StatusCode.SUCCESS).json({ - status: 'success', - data: { - user, - }, - }); }; /** @@ -56,27 +75,36 @@ export const getUser = async (req: ExtendedReq, res: ExtendedRes) => { * @param {ExtendedRes} res - The outgoing response object, which is used to send the JSON response. */ export const createUser = async (req: ExtendedReq, res: ExtendedRes) => { - const { body } = req; + try { + const { body } = req; - if (!body || !isUser(body)) { - const missingFields = findMissingFields(body).join(', '); + if (!body || !isUser(body)) { + const missingFields = findMissingFields(body).join(', '); - res.status(StatusCode.BAD_REQUEST).json({ - status: 'fail', - message: `The provided data is missing required fields (${missingFields})`, - }); + res.status(StatusCode.BAD_REQUEST).json({ + status: 'fail', + message: `The provided data is missing required fields (${missingFields})`, + }); - return; - } + return; + } - const user = await db.createUser(body); + const user = await db.createUser(body); - res.status(StatusCode.CREATED).json({ - status: 'success', - data: { - user, - }, - }); + res.status(StatusCode.CREATED).json({ + status: 'success', + data: { + user, + }, + }); + } catch (e) { + if (!isNativeError(e)) return; + + res.status(StatusCode.INTERNAL_SERVER_ERROR).json({ + status: 'fail', + message: `๐Ÿ’ฅ Internal server error! (${e.message})`, + }); + } }; /** @@ -85,39 +113,48 @@ export const createUser = async (req: ExtendedReq, res: ExtendedRes) => { * @param {ExtendedRes} res - The outgoing response object, which is used to send the JSON response. */ export const updateUser = async (req: ExtendedReq, res: ExtendedRes) => { - const { - body, - route: { id }, - } = req; - - if (!body || !isUser(body)) { - const missingFields = findMissingFields(body).join(', '); - - res.status(StatusCode.BAD_REQUEST).json({ - status: 'fail', - message: `The provided data is missing required fields (${missingFields})`, + try { + const { + body, + route: { id }, + } = req; + + if (!body || !isUser(body)) { + const missingFields = findMissingFields(body).join(', '); + + res.status(StatusCode.BAD_REQUEST).json({ + status: 'fail', + message: `The provided data is missing required fields (${missingFields})`, + }); + + return; + } + + const updatedUser = await db.updateUser(id, body); + + if (!updatedUser) { + res.status(StatusCode.NOT_FOUND).json({ + status: 'fail', + message: 'User not found', + }); + + return; + } + + res.status(StatusCode.SUCCESS).json({ + status: 'success', + data: { + user: updatedUser, + }, }); + } catch (e) { + if (!isNativeError(e)) return; - return; - } - - const updatedUser = await db.updateUser(id, body); - - if (!updatedUser) { - res.status(StatusCode.NOT_FOUND).json({ + res.status(StatusCode.INTERNAL_SERVER_ERROR).json({ status: 'fail', - message: 'User not found', + message: `๐Ÿ’ฅ Internal server error! (${e.message})`, }); - - return; } - - res.status(StatusCode.SUCCESS).json({ - status: 'success', - data: { - user: updatedUser, - }, - }); }; /** @@ -126,22 +163,31 @@ export const updateUser = async (req: ExtendedReq, res: ExtendedRes) => { * @param {ExtendedRes} res - The outgoing response object, which is used to send the JSON response. */ export const deleteUser = async (req: ExtendedReq, res: ExtendedRes) => { - const { id } = req.route; - const isDeleted = await db.deleteUser(id); + try { + const { id } = req.route; + const isDeleted = await db.deleteUser(id); + + if (!isDeleted) { + res.status(StatusCode.NOT_FOUND).json({ + status: 'fail', + message: 'User not found', + }); + + return; + } + + res.status(StatusCode.NO_CONTENT).json({ + status: 'success', + data: null, + }); + } catch (e) { + if (!isNativeError(e)) return; - if (!isDeleted) { - res.status(StatusCode.NOT_FOUND).json({ + res.status(StatusCode.INTERNAL_SERVER_ERROR).json({ status: 'fail', - message: 'User not found', + message: `๐Ÿ’ฅ Internal server error! (${e.message})`, }); - - return; } - - res.status(StatusCode.NO_CONTENT).json({ - status: 'success', - data: null, - }); }; /** From c7391fa94f3dfa83343c414fc3fd27feb43647cc Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sun, 11 Feb 2024 12:57:18 +0200 Subject: [PATCH 094/114] refactor: handle json parse error --- lib/utils/app.ts | 48 ++++++++++++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/lib/utils/app.ts b/lib/utils/app.ts index 8e612c8..10a3951 100644 --- a/lib/utils/app.ts +++ b/lib/utils/app.ts @@ -1,8 +1,9 @@ import http, { Server } from 'http'; +import { isNativeError } from 'node:util/types'; import Response, { Res } from './response'; import Route from './route'; -import { HttpMethods } from '../../types/enums'; +import { HttpMethods, StatusCode } from '../../types/enums'; import { Cb, ExtendedReq, @@ -54,20 +55,28 @@ class App { // Getting the body is async operation. So we want to get body first, // then call middlewares or route handlers - this.getBody().then(async (body) => { - this.injectBody(body); - - // using middlewareQueue with routeTable inside. - // In order to make sure that if .use() method called BEFORE any route - // e.g. ID validation, we want it to run right before route handler (get, post, put...) - for await (const middleware of this.middlewareQueue) { - if (typeof middleware === 'function') { - middleware(this.req, this.res); - } else if (routeHandler) { - await routeHandler(this.req, this.res); + this.getBody() + .then(async (body) => { + this.injectBody(body); + + // using middlewareQueue with routeTable inside. + // In order to make sure that if .use() method called BEFORE any route + // e.g. ID validation, we want it to run right before route handler (get, post, put...) + for await (const middleware of this.middlewareQueue) { + if (typeof middleware === 'function') { + middleware(this.req, this.res); + } else if (routeHandler) { + await routeHandler(this.req, this.res); + } } - } - }); + }) + .catch((e) => { + if (!isNativeError(e)) return; + this.res.status(StatusCode.BAD_REQUEST).json({ + status: 'failed', + message: e.message, + }); + }); }; private getRouteHandlerAndRouteEndpoint() { @@ -111,13 +120,16 @@ class App { }) .on('end', () => { const body = Buffer.concat(bodyChunks).toString(); + let parsed: RequestBody = {}; + + if (!body) resolve(null); - if (body) { - const parsed = JSON.parse(body); + try { + parsed = JSON.parse(body); delete parsed?.id; resolve(parsed); - } else { - resolve(null); + } catch (e) { + reject(new Error('Invalid JSON!')); } }) .on('error', (e) => { From 5184b58cdf76ee7aa19e0359ffe2eef6ae5633c4 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sun, 11 Feb 2024 13:00:50 +0200 Subject: [PATCH 095/114] refactor: change error message --- lib/utils/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils/app.ts b/lib/utils/app.ts index 10a3951..ac3203e 100644 --- a/lib/utils/app.ts +++ b/lib/utils/app.ts @@ -74,7 +74,7 @@ class App { if (!isNativeError(e)) return; this.res.status(StatusCode.BAD_REQUEST).json({ status: 'failed', - message: e.message, + message: `๐Ÿ’ฅ Body parsing error! ${e.message}`, }); }); }; From 8b5d467feaab357fb24bbd6f1c41ca11aaf624d1 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sun, 11 Feb 2024 16:05:22 +0200 Subject: [PATCH 096/114] refactor: remove comment --- server.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/server.ts b/server.ts index 1e836ed..6261f58 100644 --- a/server.ts +++ b/server.ts @@ -25,7 +25,6 @@ const isMasterProcess = isPrimary && isMultiMode; let workerPorts: number[] = []; if (isMasterProcess) { - // Master process | load balancer code workerPorts = initWorkers(); http From 555946ab9cb852b55d9faa9567fc347d4fe0544d Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sun, 11 Feb 2024 16:23:31 +0200 Subject: [PATCH 097/114] refactor: add JSDoc comment to validateId fn --- lib/utils/validateId.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/utils/validateId.ts b/lib/utils/validateId.ts index 0e788c8..e6bcc2a 100644 --- a/lib/utils/validateId.ts +++ b/lib/utils/validateId.ts @@ -3,6 +3,11 @@ import * as uuid from 'uuid'; import { StatusCode } from '../../types/enums'; import { ExtendedReq, ExtendedRes } from '../../types/types'; +/** + * Validates the id in the request route and sends a JSON response with a 400 status code if it is not a valid uuid. + * @param {ExtendedReq} req - The incoming request object, which contains the id in the route property. + * @param {ExtendedRes} res - The outgoing response object, which is used to send the JSON response. + */ export const validateId = (req: ExtendedReq, res: ExtendedRes) => { const id = req.route?.id; From 848d624048c3c65e025fde5c5d71e53ac5671c33 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sun, 11 Feb 2024 16:55:55 +0200 Subject: [PATCH 098/114] fix: to ignore unnecessary fields from user request --- lib/utils/app.ts | 20 +++++++++++++------- models/user/lib/utils/isUser.ts | 8 ++++---- types/types.ts | 2 +- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/lib/utils/app.ts b/lib/utils/app.ts index ac3203e..5e6d352 100644 --- a/lib/utils/app.ts +++ b/lib/utils/app.ts @@ -74,7 +74,7 @@ class App { if (!isNativeError(e)) return; this.res.status(StatusCode.BAD_REQUEST).json({ status: 'failed', - message: `๐Ÿ’ฅ Body parsing error! ${e.message}`, + message: e.message, }); }); }; @@ -120,16 +120,14 @@ class App { }) .on('end', () => { const body = Buffer.concat(bodyChunks).toString(); - let parsed: RequestBody = {}; - if (!body) resolve(null); try { - parsed = JSON.parse(body); - delete parsed?.id; - resolve(parsed); + const parsed = JSON.parse(body) as RequestBody; + const filteredBody = this.filterBody(parsed); + resolve(filteredBody); } catch (e) { - reject(new Error('Invalid JSON!')); + reject(new Error('๐Ÿ’ฅ Invalid JSON provided!')); } }) .on('error', (e) => { @@ -166,6 +164,14 @@ class App { }); } + private filterBody(body: RequestBody) { + return { + ...(Number.isFinite(body?.age) && { age: body?.age }), + ...(body?.hobbies && { hobbies: body?.hobbies }), + ...(body?.username && { username: body?.username }), + }; + } + private injectBody(body: RequestBody) { this.req.body = body; } diff --git a/models/user/lib/utils/isUser.ts b/models/user/lib/utils/isUser.ts index 9db9973..1b17fe0 100644 --- a/models/user/lib/utils/isUser.ts +++ b/models/user/lib/utils/isUser.ts @@ -1,9 +1,9 @@ -import { User } from '../../usersModel'; +import { RequestBody } from '../../../../types/types'; +import { RequestUser } from '../../usersModel'; import { REQUIRED_USER_FIELDS } from '../const'; -const isUser = ( - body: Record | Omit, -): body is Omit => { +const isUser = (body: RequestBody | RequestUser): body is RequestUser => { + if (!body) return false; return REQUIRED_USER_FIELDS.every((field) => Object.keys(body).includes(field), ); diff --git a/types/types.ts b/types/types.ts index f49b582..cb3e1bb 100644 --- a/types/types.ts +++ b/types/types.ts @@ -6,7 +6,7 @@ import { RequestUser } from '../models/user/usersModel'; export type Req = IncomingMessage; -export type RequestBody = Record | null; +export type RequestBody = Record | null; export type ExtendedRes = Res & { json: JsonFn; From 86e2bf78aaa9172e72b479bef53fa6c1ff19ab3d Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sun, 11 Feb 2024 17:29:23 +0200 Subject: [PATCH 099/114] fix: body parsing --- lib/utils/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils/app.ts b/lib/utils/app.ts index 5e6d352..8cc5966 100644 --- a/lib/utils/app.ts +++ b/lib/utils/app.ts @@ -166,7 +166,7 @@ class App { private filterBody(body: RequestBody) { return { - ...(Number.isFinite(body?.age) && { age: body?.age }), + ...(body?.age !== undefined && { age: body?.age }), ...(body?.hobbies && { hobbies: body?.hobbies }), ...(body?.username && { username: body?.username }), }; From 61914f1a43b2ba26acb240a706f530baa0475884 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sun, 11 Feb 2024 17:29:39 +0200 Subject: [PATCH 100/114] feat: implement body type validation --- controllers/usersController.ts | 13 +++++++++++++ models/user/lib/utils/findWrongTypes.ts | 23 +++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 models/user/lib/utils/findWrongTypes.ts diff --git a/controllers/usersController.ts b/controllers/usersController.ts index 43d3127..edda60f 100644 --- a/controllers/usersController.ts +++ b/controllers/usersController.ts @@ -2,6 +2,7 @@ import { isNativeError } from 'node:util/types'; import db from '../db/db'; import findMissingFields from '../models/user/lib/utils/findMissingFields'; +import findWrongTypes from '../models/user/lib/utils/findWrongTypes'; import isUser from '../models/user/lib/utils/isUser'; import { StatusCode } from '../types/enums'; import { ExtendedReq, ExtendedRes, Req } from '../types/types'; @@ -89,6 +90,18 @@ export const createUser = async (req: ExtendedReq, res: ExtendedRes) => { return; } + const userWrongTypes = findWrongTypes(body); + if (userWrongTypes.length !== 0) { + const wrongTypes = userWrongTypes.join(', '); + + res.status(StatusCode.BAD_REQUEST).json({ + status: 'fail', + message: `The provided data is containing wrong types (${wrongTypes})`, + }); + + return; + } + const user = await db.createUser(body); res.status(StatusCode.CREATED).json({ diff --git a/models/user/lib/utils/findWrongTypes.ts b/models/user/lib/utils/findWrongTypes.ts new file mode 100644 index 0000000..b67b149 --- /dev/null +++ b/models/user/lib/utils/findWrongTypes.ts @@ -0,0 +1,23 @@ +import { RequestUser } from '../../usersModel'; + +const findWrongTypes = (user: RequestUser) => { + return Object.entries(user) + .map(([key, value]) => { + if (key === 'age' && typeof value !== 'number') { + return key; + } + + if (key === 'username' && typeof value !== 'string') { + return key; + } + + if (key === 'hobbies' && !Array.isArray(value)) { + return key; + } + + return null; + }) + .filter(Boolean); +}; + +export default findWrongTypes; From 717a2572eef98336bee82f8065638c73647c088a Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sun, 11 Feb 2024 17:38:15 +0200 Subject: [PATCH 101/114] feat: implement update user body type validation --- controllers/usersController.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/controllers/usersController.ts b/controllers/usersController.ts index edda60f..922d4b3 100644 --- a/controllers/usersController.ts +++ b/controllers/usersController.ts @@ -143,6 +143,18 @@ export const updateUser = async (req: ExtendedReq, res: ExtendedRes) => { return; } + const userWrongTypes = findWrongTypes(body); + if (userWrongTypes.length !== 0) { + const wrongTypes = userWrongTypes.join(', '); + + res.status(StatusCode.BAD_REQUEST).json({ + status: 'fail', + message: `The provided data is containing wrong types (${wrongTypes})`, + }); + + return; + } + const updatedUser = await db.updateUser(id, body); if (!updatedUser) { From 15d7c8f230b82abb03cb431d83b5f91c7b5755c3 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sun, 11 Feb 2024 17:47:15 +0200 Subject: [PATCH 102/114] feat: add emoji errors --- controllers/usersController.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/controllers/usersController.ts b/controllers/usersController.ts index 922d4b3..9c031aa 100644 --- a/controllers/usersController.ts +++ b/controllers/usersController.ts @@ -7,8 +7,6 @@ import isUser from '../models/user/lib/utils/isUser'; import { StatusCode } from '../types/enums'; import { ExtendedReq, ExtendedRes, Req } from '../types/types'; -// TODO: add emoji response - /** * Gets the list of users from the database and sends it as a JSON response. * @param {ExtendedReq} _req - The incoming request object, which is not used in this function. @@ -48,7 +46,7 @@ export const getUser = async (req: ExtendedReq, res: ExtendedRes) => { if (!user) { res.status(StatusCode.NOT_FOUND).json({ status: 'fail', - message: 'User not found', + message: '๐Ÿ˜ฏ User not found', }); return; @@ -84,7 +82,7 @@ export const createUser = async (req: ExtendedReq, res: ExtendedRes) => { res.status(StatusCode.BAD_REQUEST).json({ status: 'fail', - message: `The provided data is missing required fields (${missingFields})`, + message: `๐Ÿ“› The provided data is missing required fields (${missingFields})`, }); return; @@ -96,7 +94,7 @@ export const createUser = async (req: ExtendedReq, res: ExtendedRes) => { res.status(StatusCode.BAD_REQUEST).json({ status: 'fail', - message: `The provided data is containing wrong types (${wrongTypes})`, + message: `๐Ÿšซ The provided data is containing wrong types (${wrongTypes})`, }); return; @@ -137,7 +135,7 @@ export const updateUser = async (req: ExtendedReq, res: ExtendedRes) => { res.status(StatusCode.BAD_REQUEST).json({ status: 'fail', - message: `The provided data is missing required fields (${missingFields})`, + message: `๐Ÿ“› The provided data is missing required fields (${missingFields})`, }); return; @@ -149,7 +147,7 @@ export const updateUser = async (req: ExtendedReq, res: ExtendedRes) => { res.status(StatusCode.BAD_REQUEST).json({ status: 'fail', - message: `The provided data is containing wrong types (${wrongTypes})`, + message: `๐Ÿšซ The provided data is containing wrong types (${wrongTypes})`, }); return; @@ -160,7 +158,7 @@ export const updateUser = async (req: ExtendedReq, res: ExtendedRes) => { if (!updatedUser) { res.status(StatusCode.NOT_FOUND).json({ status: 'fail', - message: 'User not found', + message: '๐Ÿ˜ฏ User not found', }); return; @@ -195,7 +193,7 @@ export const deleteUser = async (req: ExtendedReq, res: ExtendedRes) => { if (!isDeleted) { res.status(StatusCode.NOT_FOUND).json({ status: 'fail', - message: 'User not found', + message: '๐Ÿ˜ฏ User not found', }); return; @@ -223,6 +221,6 @@ export const deleteUser = async (req: ExtendedReq, res: ExtendedRes) => { export const notFound = async (_req: Req, res: ExtendedRes) => { res.status(StatusCode.NOT_FOUND).json({ status: 'fail', - message: 'The route does not exist', + message: '๐Ÿƒ The route does not exist', }); }; From a08c49bd519aeef00203db21068d14af7fc4e212 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sun, 11 Feb 2024 17:54:28 +0200 Subject: [PATCH 103/114] fix: test scenarios --- test/api-scenario-1.test.ts | 2 +- test/api-scenario-2.test.ts | 6 +++--- test/api-scenario-3.test.ts | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/api-scenario-1.test.ts b/test/api-scenario-1.test.ts index e76c5cf..7b275f2 100644 --- a/test/api-scenario-1.test.ts +++ b/test/api-scenario-1.test.ts @@ -106,7 +106,7 @@ describe('Test scenario 1', () => { expect(res.body).toStrictEqual({ status: 'fail', - message: 'User not found', + message: '๐Ÿ˜ฏ User not found', }); }); }); diff --git a/test/api-scenario-2.test.ts b/test/api-scenario-2.test.ts index 253a6e8..003976a 100644 --- a/test/api-scenario-2.test.ts +++ b/test/api-scenario-2.test.ts @@ -50,7 +50,7 @@ describe('Test scenario 2', () => { expect(res.body).toStrictEqual({ status: 'fail', - message: 'User not found', + message: '๐Ÿ˜ฏ User not found', }); }); @@ -62,7 +62,7 @@ describe('Test scenario 2', () => { expect(res.body).toStrictEqual({ status: 'fail', - message: 'User not found', + message: '๐Ÿ˜ฏ User not found', }); }); @@ -73,7 +73,7 @@ describe('Test scenario 2', () => { expect(res.body).toStrictEqual({ status: 'fail', - message: 'User not found', + message: '๐Ÿ˜ฏ User not found', }); }); }); diff --git a/test/api-scenario-3.test.ts b/test/api-scenario-3.test.ts index 60f15ea..de80589 100644 --- a/test/api-scenario-3.test.ts +++ b/test/api-scenario-3.test.ts @@ -70,7 +70,7 @@ describe('Test scenario 2', () => { expect(res.body).toStrictEqual({ status: 'fail', - message: `The provided data is missing required fields (username)`, + message: `๐Ÿ“› The provided data is missing required fields (username)`, }); }); @@ -84,7 +84,7 @@ describe('Test scenario 2', () => { expect(res.body).toStrictEqual({ status: 'fail', - message: `The provided data is missing required fields (username, age)`, + message: `๐Ÿ“› The provided data is missing required fields (username, age)`, }); }); @@ -96,7 +96,7 @@ describe('Test scenario 2', () => { expect(res.body).toStrictEqual({ status: 'fail', - message: `The provided data is missing required fields (username, age, hobbies)`, + message: `๐Ÿ“› The provided data is missing required fields (username, age, hobbies)`, }); }); }); From e7118c3328cf2c79e2c27b707680a635644cd155 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sun, 11 Feb 2024 22:21:29 +0200 Subject: [PATCH 104/114] docs: add API readme --- README.md | 161 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 160 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7e4967d..0e36434 100644 --- a/README.md +++ b/README.md @@ -1 +1,160 @@ -# node-crud-api \ No newline at end of file +# ๐Ÿ’š Node CRUD API + +*๐Ÿฆฅ RS-School task.* + +# Getting Started ๐Ÿš€ +To run the project locally, you would have to download zip file with our repository or clone it to your computer. โœจ + +## Setup and Running โš ๏ธ + +What things do you need to do in order to run our project locally? ๐Ÿค” + +* Use node 20 LTS โšก +* Installed [.git](https://git-scm.com/) on your computer. โœŒ๏ธ +* Code Editor of your choice. ๐Ÿ“ +* Installed [npm](https://www.npmjs.com/). ๐Ÿ“ฆ + +## Installation And Preparation ๐Ÿ”ฎ + +First make sure you have all the things listed in the previous section. Then clone our repository to your computer: ๐Ÿ‘Œ + +``` +git clone https://github.com/Quiddlee/node-crud-api.git +``` + +or download zip file manually with our repository. + +Navigate into project folder and run ๐Ÿ“ฆ: + +``` +npm install +``` + +Finally run a development server: ๐Ÿคฉ +``` +npm run start:dev +``` +Aaaaand you're done! ๐ŸŽ‰๐Ÿฅณ + +## Available Scripts ๐Ÿฅ‘ + +Here you can find all the scripts that are available in our project. ๐Ÿฆš + +Start the app in `base` mode: โœ… + +``` +npm run start:dev +``` + +Start the app in `multi` mode: ๐Ÿชญ + +``` +npm run start:multi +``` + +Start the app in `prod` mode: ๐Ÿชถ + +``` +npm run start:prod +``` + +Lint the app with `eslint`: ๐Ÿฆš + +``` +npm run lint +``` + +Lint adn fix the app errors with `eslint`: ๐Ÿจ + +``` +npm run lint:fix +``` + +Format the App with **Prettier**: ๐Ÿงน + +``` +npm run format +``` + +Format the App with **Prettier** fix: ๐Ÿƒ + +``` +npm run format:fix +``` + +Type check the App with **TypeScript**: ๐Ÿฆ + +``` +npm run type-check +``` + +Run unit-tests with **Vitest**: ๐Ÿงช + +``` +npm run test +``` +# Working with API ๐Ÿณ + +## API endpoints ๐Ÿฆ‰ +The API has the following endpoints: + +| Method | Endpoint | Description | +|---------|:--------------:|------------------------------------:| +| GET | /users | Get all the users from the database | +| GET | /users/:id | Get a single user by ID | +| POST | /users | Create a new user in the database | +| PUT | /users/:id | Update a user by ID | +| DELETE | /users/:id | Delete a user by ID | + +## Request body ๐Ÿฅ‘ + +| Endpoint | Body | Example | +|----------|:----------:|----------------------------------------------------------------------------------------------------------------------------:| +| POST | /users | An object with the username age and hobbies ```{"username": "user", "age": 20, "hobbies": ["cooking", "sport"]}``` | +| PUT | /users/:id | An object with the updated username age and hobbies ```{"username": "updated user", "age": 30, "hobbies": ["updated hobbie"]}``` | + +## Response format ๐Ÿ‡ + +| Field | Type | Description | +|---------|:-------------------:|-----------------------------------------------------------------------------:| +| status | "success" or "fail" | Indicates whether the request was successful or not | +| message | string | in case of failed result the response will contain message why it is failed | +| data | Object or Array | The data returned by the request | + +## Response examples ๐Ÿ‹ + +**GET /users** + +```json + { + "status": "success", + "data": { + "users": [ + { + "username": "user", + "age": 20, + "hobbies": ["coking", "sport", "programming"] + }, + { + "username": "user2", + "age": 21, + "hobbies": ["sport", "programming"] + }, + { + "username": "user3", + "age": 22, + "hobbies": ["hobbie"] + } + ] + } +} +``` + +**GET /users/:id Error case** + +```json + { + "status": "fail", + "message": "๐Ÿ˜ฏ User not found" +} +``` From 6c83995009ac6458227152203fb3772ad1eb8c9f Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sun, 11 Feb 2024 22:30:36 +0200 Subject: [PATCH 105/114] chore: add API postman collection --- RSS_Node.js_Crud_API.postman_collection.json | 190 +++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 RSS_Node.js_Crud_API.postman_collection.json diff --git a/RSS_Node.js_Crud_API.postman_collection.json b/RSS_Node.js_Crud_API.postman_collection.json new file mode 100644 index 0000000..677293a --- /dev/null +++ b/RSS_Node.js_Crud_API.postman_collection.json @@ -0,0 +1,190 @@ +{ + "info": { + "_postman_id": "65586818-b71e-46a9-abfb-1847cd0390c5", + "name": "RSS Node.js Crud API", + "description": "This template contains a boilerplate for documentation that you can quickly customize and reuse.\n\n### How to use this template:\n\n- Replace the content given brackets (()) with your API's details.\n- Tips are formatted in `codespan` - feel free to read and remove them.\n \n\n---\n\n`Start with a brief overview of what your API offers.`\n\nThe ((product name)) provides many API products, tools, and resources that enable you to ((add product value here)).\n\n`You can also list the APIs you offer, link to the relevant pages, or do both in this section.`\n\n## **Getting started guide**\n\n`List the steps or points required to start using your APIs. Make sure to cover everything required to reach success with your API as quickly as possible.`\n\nTo start using the ((add APIs here)), you need to -\n\n`The points given below are from The Postman API's documentation. You can reference it to write your own getting started guide.`\n\n- You must use a valid API Key to send requests to the API endpoints. You can get your API key from Postman's [integrations dashboard](https://go.postman.co/settings/me/api-keys).\n- The API has [rate and usage limits](https://postman.postman.co/workspace/Collection-Templates~6311738d-2e70-441f-ae12-78caf078c5b7/collection/22517504-e9c28f47-1253-44af-a2f3-20dce4da1f18?ctx=documentation#rate-and-usage-limits).\n- The API only responds to HTTPS-secured communications. Any requests sent via HTTP return an HTTP 301 redirect to the corresponding HTTPS resources.\n- The API returns request responses in JSON format. When an API request returns an error, it is sent in the JSON response as an error key.\n \n\n## Authentication\n\n`Add details on the authorization keys/tokens required, steps that cover how to get them, and the relevant error codes.`\n\nThe ((product name)) API uses ((add your API's authorization type)) for authentication.\n\n`The details given below are from the Postman API's documentation. You can reference it to write your own authentication section.`\n\nPostman uses API keys for authentication. You can generate a Postman API key in the [API keys](https://postman.postman.co/settings/me/api-keys) section of your Postman account settings.\n\nYou must include an API key in each request to the Postman API with the X-Api-Key request header.\n\n### Authentication error response\n\nIf an API key is missing, malformed, or invalid, you will receive an HTTP 401 Unauthorized response code.\n\n## Rate and usage limits\n\n`Use this section to cover your APIs' terms of use. Include API limits, constraints, and relevant error codes, so consumers understand the permitted API usage and practices.`\n\n`The example given below is from The Postman API's documentation. Use it as a reference to write your APIs' terms of use.`\n\nAPI access rate limits apply at a per-API key basis in unit time. The limit is 300 requests per minute. Also, depending on your plan, you may have usage limits. If you exceed either limit, your request will return an HTTP 429 Too Many Requests status code.\n\nEach API response returns the following set of headers to help you identify your use status:\n\n| Header | Description |\n| --- | --- |\n| `X-RateLimit-Limit` | The maximum number of requests that the consumer is permitted to make per minute. |\n| `X-RateLimit-Remaining` | The number of requests remaining in the current rate limit window. |\n| `X-RateLimit-Reset` | The time at which the current rate limit window resets in UTC epoch seconds. |\n\n### 503 response\n\nAn HTTP `503` response from our servers indicates there is an unexpected spike in API access traffic. The server is usually operational within the next five minutes. If the outage persists or you receive any other form of an HTTP `5XX` error, [contact support](https://support.postman.com/hc/en-us/requests/new/).\n\n### **Need some help?**\n\n`Add links that customers can refer to whenever they need help.`\n\nIn case you have questions, go through our tutorials ((link to your video or help documentation here)). Or visit our FAQ page ((link to the relevant page)).\n\nOr you can check out our community forum, thereโ€™s a good chance our community has an answer for you. Visit our developer forum ((link to developer forum)) to review topics, ask questions, and learn from others.\n\n`You can also document or add links to libraries, code examples, and other resources needed to make a request.`", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "28968368" + }, + "item": [ + { + "name": "User", + "item": [ + { + "name": "Get Users", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{BASE_URL}}:{{PORT}}/{{USERS_ROUTE}}", + "host": [ + "{{BASE_URL}}" + ], + "port": "{{PORT}}", + "path": [ + "{{USERS_ROUTE}}" + ] + } + }, + "response": [] + }, + { + "name": "Get User", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{BASE_URL}}:{{PORT}}/{{USERS_ROUTE}}/{{USER_ID}}", + "host": [ + "{{BASE_URL}}" + ], + "port": "{{PORT}}", + "path": [ + "{{USERS_ROUTE}}", + "{{USER_ID}}" + ] + } + }, + "response": [] + }, + { + "name": "Create User", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": \"e8e06aa1-cba7-5af0-be15-703774ccfb3b\",\r\n \"age\": 0,\r\n \"username\": \"test\",\r\n \"hobbies\": [\r\n \"test\"\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{BASE_URL}}:{{PORT}}/{{USERS_ROUTE}}", + "host": [ + "{{BASE_URL}}" + ], + "port": "{{PORT}}", + "path": [ + "{{USERS_ROUTE}}" + ] + } + }, + "response": [] + }, + { + "name": "Update User", + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"id\": \"e8e06aa1-cba7-5af0-be15-703774ccfb3b\",\r\n \"age\": 12,\r\n \"username\": \"changed username\",\r\n \"hobbies\": [\r\n \"the hobbies changed\"\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{BASE_URL}}:{{PORT}}/{{USERS_ROUTE}}/{{USER_ID}}", + "host": [ + "{{BASE_URL}}" + ], + "port": "{{PORT}}", + "path": [ + "{{USERS_ROUTE}}", + "{{USER_ID}}" + ] + } + }, + "response": [] + }, + { + "name": "Delete User", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{BASE_URL}}:{{PORT}}/{{USERS_ROUTE}}/{{USER_ID}}", + "host": [ + "{{BASE_URL}}" + ], + "port": "{{PORT}}", + "path": [ + "{{USERS_ROUTE}}", + "{{USER_ID}}" + ] + } + }, + "response": [] + } + ], + "description": "The `/me` endpoints let you manage information about the authenticated user." + }, + { + "name": "Not Found", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{BASE_URL}}:{{PORT}}/api/some-non/existing/resource", + "host": [ + "{{BASE_URL}}" + ], + "port": "{{PORT}}", + "path": [ + "api", + "some-non", + "existing", + "resource" + ] + } + }, + "response": [] + } + ], + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "key", + "value": "X-API-Key", + "type": "string" + }, + { + "key": "value", + "value": "{{token}}", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "baseUrl", + "value": "https://farming-simulator.pstmn.io" + } + ] +} \ No newline at end of file From 1b274f30af9104498c8f0a1791d5fae74ba66604 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sun, 11 Feb 2024 22:31:52 +0200 Subject: [PATCH 106/114] chore: add API postman collection --- ...PI.postman_collection.json => crud_api.postman_collection.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename RSS_Node.js_Crud_API.postman_collection.json => crud_api.postman_collection.json (100%) diff --git a/RSS_Node.js_Crud_API.postman_collection.json b/crud_api.postman_collection.json similarity index 100% rename from RSS_Node.js_Crud_API.postman_collection.json rename to crud_api.postman_collection.json From f757d54e7ea87afda115a342f67b818bd415d5e5 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sun, 11 Feb 2024 22:36:25 +0200 Subject: [PATCH 107/114] fix: test describe typo --- test/api-scenario-3.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/api-scenario-3.test.ts b/test/api-scenario-3.test.ts index de80589..c4ef5c5 100644 --- a/test/api-scenario-3.test.ts +++ b/test/api-scenario-3.test.ts @@ -29,7 +29,7 @@ const uploadUserData = { }; let uploadUserId = ''; -describe('Test scenario 2', () => { +describe('Test scenario 3', () => { afterAll(db.clearData); it('GET api/users should return empty array of users', async () => { From 60329f2c72c3dba295839d66f05d81462c23e848 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sun, 11 Feb 2024 23:11:03 +0200 Subject: [PATCH 108/114] docs: fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0e36434..1429aa0 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,7 @@ The API has the following endpoints: { "username": "user", "age": 20, - "hobbies": ["coking", "sport", "programming"] + "hobbies": ["cooking", "sport", "programming"] }, { "username": "user2", From 08476ea9b29c23515e5ab0b599036a8f93ec1c02 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Mon, 12 Feb 2024 16:47:11 +0200 Subject: [PATCH 109/114] chore: update postman collection --- crud_api.postman_collection.json | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/crud_api.postman_collection.json b/crud_api.postman_collection.json index 677293a..6b87b74 100644 --- a/crud_api.postman_collection.json +++ b/crud_api.postman_collection.json @@ -49,12 +49,33 @@ }, { "name": "Create User", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "const response = pm.response.json();\r", + "pm.environment.set(\"USER_ID\", response.data.user.id);" + ], + "type": "text/javascript" + } + } + ], "request": { "method": "POST", "header": [], "body": { "mode": "raw", - "raw": "{\r\n \"id\": \"e8e06aa1-cba7-5af0-be15-703774ccfb3b\",\r\n \"age\": 0,\r\n \"username\": \"test\",\r\n \"hobbies\": [\r\n \"test\"\r\n ]\r\n}", + "raw": "{\r\n \"id\": \"e8e06aa1-cba7-5af0-be15-703774ccfb3b\",\r\n \"age\": 1,\r\n \"username\": \"test\",\r\n \"hobbies\": [\r\n \"test\"\r\n ]\r\n}", "options": { "raw": { "language": "json" @@ -81,7 +102,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\r\n \"id\": \"e8e06aa1-cba7-5af0-be15-703774ccfb3b\",\r\n \"age\": 12,\r\n \"username\": \"changed username\",\r\n \"hobbies\": [\r\n \"the hobbies changed\"\r\n ]\r\n}", + "raw": "{\r\n \"id\": \"0aa5d95a-f25e-411a-8086-fb55d3fc9d67\",\r\n \"age\": 12,\r\n \"username\": \"changed username\",\r\n \"hobbies\": [\r\n \"the hobbies changed\"\r\n ]\r\n}", "options": { "raw": { "language": "json" @@ -89,14 +110,14 @@ } }, "url": { - "raw": "{{BASE_URL}}:{{PORT}}/{{USERS_ROUTE}}/{{USER_ID}}", + "raw": "{{BASE_URL}}:{{PORT}}/{{USERS_ROUTE}}/ec7db8fe-f18a-4053-be75-6f9e507eb638", "host": [ "{{BASE_URL}}" ], "port": "{{PORT}}", "path": [ "{{USERS_ROUTE}}", - "{{USER_ID}}" + "ec7db8fe-f18a-4053-be75-6f9e507eb638" ] } }, From 6bf079370f0c9888c10f040f127009d9de7a72c2 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Mon, 12 Feb 2024 16:47:53 +0200 Subject: [PATCH 110/114] docs: add postman collection description --- README.md | 3 +++ public/postman.jpg | Bin 0 -> 231209 bytes 2 files changed, 3 insertions(+) create mode 100644 public/postman.jpg diff --git a/README.md b/README.md index 1429aa0..cc74ef5 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,9 @@ Run unit-tests with **Vitest**: ๐Ÿงช npm run test ``` # Working with API ๐Ÿณ +In the project root folder you can find postman collection that will make your life easy working with this API ๐Ÿ˜‰ + +![postman.jpg](./public/postman.jpg) ## API endpoints ๐Ÿฆ‰ The API has the following endpoints: diff --git a/public/postman.jpg b/public/postman.jpg new file mode 100644 index 0000000000000000000000000000000000000000..12d5a0073c0d2e40801a73f9176db8e9e39b65d7 GIT binary patch literal 231209 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

%+_n}p9ag!40Y>0?? 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Ő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 literal 0 HcmV?d00001 From a7eab0ecc1fed6bf2431a895248ecf787a99d403 Mon Sep 17 00:00:00 2001 From: Bohdan Date: Mon, 12 Feb 2024 17:14:03 +0200 Subject: [PATCH 111/114] docs: fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cc74ef5..fa67983 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ Run unit-tests with **Vitest**: ๐Ÿงช npm run test ``` # Working with API ๐Ÿณ -In the project root folder you can find postman collection that will make your life easy working with this API ๐Ÿ˜‰ +In the project root folder, you can find a Postman collection that will make your life easier while working with the API. ๐Ÿ˜‰ ![postman.jpg](./public/postman.jpg) From 5643fe243ef5b30757b673fb16b6eea167cda58b Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 14 Feb 2024 13:23:43 +0200 Subject: [PATCH 112/114] docs: mention .env file --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index fa67983..b7253d7 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,16 @@ Navigate into project folder and run ๐Ÿ“ฆ: npm install ``` +Create ```.env``` file in the root of the project and add your commercetools credentials ๐Ÿ”ฅ. + +You can find ```.env.example``` as an example file in the project root or follow the lines below ๐Ÿบ: + + +```dotenv +PORT=YOUR_PORT +HOST=YOUR_HOST +``` + Finally run a development server: ๐Ÿคฉ ``` npm run start:dev From 3e387b6e7a6ed49a3c0daffabc005154df2793cf Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 14 Feb 2024 13:25:19 +0200 Subject: [PATCH 113/114] docs: fix .env spelling --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index b7253d7..91bd130 100644 --- a/README.md +++ b/README.md @@ -30,11 +30,10 @@ Navigate into project folder and run ๐Ÿ“ฆ: npm install ``` -Create ```.env``` file in the root of the project and add your commercetools credentials ๐Ÿ”ฅ. +Create ```.env``` file in the root of the project and add all necessary variables ๐Ÿ”ฅ. You can find ```.env.example``` as an example file in the project root or follow the lines below ๐Ÿบ: - ```dotenv PORT=YOUR_PORT HOST=YOUR_HOST From de636ea4bb5caf15a622b2cd7ca6cb1f35f0aaf9 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 14 Feb 2024 13:25:42 +0200 Subject: [PATCH 114/114] docs: fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 91bd130..30ef71d 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ *๐Ÿฆฅ RS-School task.* # Getting Started ๐Ÿš€ -To run the project locally, you would have to download zip file with our repository or clone it to your computer. โœจ +To run the project locally, you would have to download zip file with the repository or clone it to your computer. โœจ ## Setup and Running โš ๏ธ