diff --git a/package.json b/package.json index fab04bb4..4dd5470c 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "class-transformer": "0.5.1", "class-transformer-validator": "^0.9.1", "class-validator": "^0.14.0", + "curlconverter": "^4.9.0", "dotenv": "16.0.1", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eedc0820..0a3ab476 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.0' +lockfileVersion: '6.1' settings: autoInstallPeers: true @@ -80,6 +80,9 @@ dependencies: class-validator: specifier: ^0.14.0 version: 0.14.0 + curlconverter: + specifier: ^4.9.0 + version: 4.9.0 dotenv: specifier: 16.0.1 version: 16.0.1 @@ -1711,6 +1714,14 @@ packages: '@jridgewell/trace-mapping': 0.3.9 dev: true + /@curlconverter/tree-sitter-bash@0.0.3: + resolution: {integrity: sha512-w3SfZ5uuGyQKMqCpGhOa7+ptr53zBi+gjE87OSUwozF9Vt6SwToxWlCAifI1rOyUg5156FYfODQ0m2Er0Ys9Bw==} + requiresBuild: true + dependencies: + nan: 2.19.0 + prebuild-install: 7.1.2 + dev: false + /@eslint-community/eslint-utils@4.4.0(eslint@8.47.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3359,7 +3370,6 @@ packages: buffer: 5.7.1 inherits: 2.0.4 readable-stream: 3.6.2 - dev: true /blueimp-md5@2.19.0: resolution: {integrity: sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==} @@ -3471,7 +3481,6 @@ packages: dependencies: base64-js: 1.5.1 ieee754: 1.2.1 - dev: true /buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} @@ -3600,6 +3609,10 @@ packages: fsevents: 2.3.2 dev: true + /chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + dev: false + /ci-info@3.8.0: resolution: {integrity: sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==} engines: {node: '>=8'} @@ -3855,6 +3868,18 @@ packages: engines: {node: '>= 6'} dev: true + /curlconverter@4.9.0: + resolution: {integrity: sha512-cJy7LjfA3LaOyBe+fFQAPND54sbsxSMiL4pPOLDvE8NTAshyeyU02tj/63m6RpSPQZVBlYntou+PhCo/G4CrOg==} + hasBin: true + dependencies: + '@curlconverter/tree-sitter-bash': 0.0.3 + jsesc: 3.0.2 + lossless-json: 2.0.11 + tree-sitter: 0.20.6 + web-tree-sitter: 0.20.8 + yamljs: 0.3.0 + dev: false + /d@1.0.1: resolution: {integrity: sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==} dependencies: @@ -3914,6 +3939,13 @@ packages: engines: {node: '>=0.10.0'} dev: false + /decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + dependencies: + mimic-response: 3.1.0 + dev: false + /dedent@1.5.1: resolution: {integrity: sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==} peerDependencies: @@ -3941,6 +3973,11 @@ packages: regexp.prototype.flags: 1.5.0 dev: true + /deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + dev: false + /deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -4009,6 +4046,11 @@ packages: engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} dev: true + /detect-libc@2.0.2: + resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==} + engines: {node: '>=8'} + dev: false + /detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} @@ -4497,6 +4539,11 @@ packages: engines: {node: '>= 0.8.0'} dev: true + /expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + dev: false + /expect@29.6.2: resolution: {integrity: sha512-iAErsLxJ8C+S02QbLAwgSGSezLQK+XXRDt8IuFXFpwCNw2ECmzZSmjKcCaFVp5VRMk+WAvz6h6jokzEzBFZEuA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -4771,6 +4818,10 @@ packages: resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==} dev: true + /fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + dev: false + /fs-extra@10.1.0: resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} engines: {node: '>=12'} @@ -4848,6 +4899,10 @@ packages: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} + /github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + dev: false + /glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -5159,6 +5214,10 @@ packages: /inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + /ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + dev: false + /inside@1.0.0: resolution: {integrity: sha512-tvFwvS4g7q6iDot/4FjtWFHwwpv6TVvEumbTdLQilk1F07ojakbXPQcvf3kMAlyNDpzKRzn+d33O3RuXODuxZQ==} deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. @@ -5823,6 +5882,12 @@ packages: hasBin: true dev: true + /jsesc@3.0.2: + resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} + engines: {node: '>=6'} + hasBin: true + dev: false + /json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} dev: true @@ -6054,6 +6119,10 @@ packages: resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} dev: false + /lossless-json@2.0.11: + resolution: {integrity: sha512-BP0vn+NGYvzDielvBZaFain/wgeJ1hTvURCqtKvhr1SCPePdaaTanmmcplrHfEJSJOUql7hk4FHwToNJjWRY3g==} + dev: false + /loupe@2.3.6: resolution: {integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==} dependencies: @@ -6192,6 +6261,11 @@ packages: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} + /mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + dev: false + /minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} dependencies: @@ -6219,6 +6293,10 @@ packages: engines: {node: '>=16 || 14 >=14.17'} dev: false + /mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + dev: false + /mkdirp@1.0.4: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} engines: {node: '>=10'} @@ -6289,6 +6367,14 @@ packages: /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + /nan@2.19.0: + resolution: {integrity: sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==} + dev: false + + /napi-build-utils@1.0.2: + resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} + dev: false + /natural-compare-lite@1.4.0: resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} dev: false @@ -6331,6 +6417,13 @@ packages: resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} dev: true + /node-abi@3.56.0: + resolution: {integrity: sha512-fZjdhDOeRcaS+rcpve7XuwHBmktS1nS1gzgghwKUQQ8nTy2FdSDr6ZT8k6YhvlJeHmmQMYiT/IH9hfco5zeW2Q==} + engines: {node: '>=10'} + dependencies: + semver: 7.5.4 + dev: false + /node-fetch@2.6.12: resolution: {integrity: sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==} engines: {node: 4.x || >=6.0.0} @@ -6840,6 +6933,25 @@ packages: resolution: {integrity: sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==} dev: true + /prebuild-install@7.1.2: + resolution: {integrity: sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==} + engines: {node: '>=10'} + hasBin: true + dependencies: + detect-libc: 2.0.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 1.0.2 + node-abi: 3.56.0 + pump: 3.0.0 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.1 + tunnel-agent: 0.6.0 + dev: false + /prelude-ls@1.1.2: resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} engines: {node: '>= 0.8.0'} @@ -6961,6 +7073,16 @@ packages: engines: {node: '>= 0.6'} dev: true + /rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + dev: false + /react-is@18.2.0: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} dev: true @@ -7332,6 +7454,18 @@ packages: engines: {node: '>=14'} dev: false + /simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + dev: false + + /simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + dev: false + /simple-update-notifier@2.0.0: resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} engines: {node: '>=10'} @@ -7554,6 +7688,11 @@ packages: resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} engines: {node: '>=12'} + /strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + dev: false + /strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -7626,6 +7765,26 @@ packages: tslib: 2.6.1 dev: false + /tar-fs@2.1.1: + resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.0 + tar-stream: 2.2.0 + dev: false + + /tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.4 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + dev: false + /test-exclude@6.0.0: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} @@ -7714,6 +7873,14 @@ packages: resolution: {integrity: sha512-/y956gpUo9ZNCb99YjxG7OaslxZWHfCHAUUfshwqOXmxUIvqLjVO581BT+gM59+QV9tFe6/CGG53tsA1Y7RSdg==} dev: true + /tree-sitter@0.20.6: + resolution: {integrity: sha512-GxJodajVpfgb3UREzzIbtA1hyRnTxVbWVXrbC6sk4xTMH5ERMBJk9HJNq4c8jOJeUaIOmLcwg+t6mez/PDvGqg==} + requiresBuild: true + dependencies: + nan: 2.19.0 + prebuild-install: 7.1.2 + dev: false + /ts-api-utils@1.0.1(typescript@4.7.4): resolution: {integrity: sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A==} engines: {node: '>=16.13.0'} @@ -7812,6 +7979,12 @@ packages: /tslib@2.6.1: resolution: {integrity: sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==} + /tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + dependencies: + safe-buffer: 5.2.1 + dev: false + /type-check@0.3.2: resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==} engines: {node: '>= 0.8.0'} @@ -8033,6 +8206,10 @@ packages: engines: {node: '>= 8'} dev: true + /web-tree-sitter@0.20.8: + resolution: {integrity: sha512-weOVgZ3aAARgdnb220GqYuh7+rZU0Ka9k9yfKtGAzEYMa6GgiCzW9JjQRJyCJakvibQW+dfjJdihjInKuuCAUQ==} + dev: false + /webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -8178,6 +8355,14 @@ packages: resolution: {integrity: sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==} engines: {node: '>= 14'} + /yamljs@0.3.0: + resolution: {integrity: sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==} + hasBin: true + dependencies: + argparse: 1.0.10 + glob: 7.2.3 + dev: false + /yargs-parser@18.1.3: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} engines: {node: '>=6'} diff --git a/src/modules/app/app.controller.ts b/src/modules/app/app.controller.ts index 5358e9d1..01ed018e 100644 --- a/src/modules/app/app.controller.ts +++ b/src/modules/app/app.controller.ts @@ -1,14 +1,38 @@ -import { Controller, Get, Param, Res } from "@nestjs/common"; -import { ApiOperation, ApiResponse } from "@nestjs/swagger"; -import { FastifyReply } from "fastify"; +import { + Controller, + Get, + HttpStatus, + Param, + Post, + Req, + Res, + UseGuards, +} from "@nestjs/common"; +import { + ApiBearerAuth, + ApiBody, + ApiConsumes, + ApiHeader, + ApiOperation, + ApiResponse, +} from "@nestjs/swagger"; +import { FastifyReply, FastifyRequest } from "fastify"; import { AppService } from "./app.service"; +import { JwtAuthGuard } from "../common/guards/jwt-auth.guard"; +import { ParserService } from "../common/services/parser.service"; +import { ApiResponseService } from "../common/services/api-response.service"; +import { HttpStatusCode } from "../common/enum/httpStatusCode.enum"; /** * App Controller */ +@ApiBearerAuth() @Controller() export class AppController { - constructor(private appService: AppService) {} + constructor( + private parserService: ParserService, + private appService: AppService, + ) {} @Get("updater/:target/:arch/:currentVersion") @ApiOperation({ @@ -29,4 +53,67 @@ export class AppController { ); return res.status(statusCode).send(data); } + + @Post("curl") + @ApiOperation({ + summary: "Parse Curl", + description: "Parses the provided curl into Sparrow api request schema", + }) + @ApiResponse({ + status: 200, + description: "Curl parsed successfully", + }) + @ApiConsumes("application/x-www-form-urlencoded") + @ApiBody({ + schema: { + properties: { + curl: { + type: "string", + example: "Use sparrow to hit this request", + }, + }, + }, + }) + @UseGuards(JwtAuthGuard) + async parseCurl(@Res() res: FastifyReply, @Req() req: FastifyRequest) { + const parsedRequestData = await this.appService.parseCurl(req); + const responseData = new ApiResponseService( + "Success", + HttpStatusCode.OK, + parsedRequestData, + ); + return res.status(responseData.httpStatusCode).send(responseData); + } + + @Post("/validate/oapi") + @ApiHeader({ + name: "x-oapi-url", + description: "Pass in the curl command.", + allowEmptyValue: false, + }) + @ApiBody({ + description: "Paste your JSON or YAML text", + required: false, + }) + @ApiOperation({ + summary: "Validate JSON/YAML/URL OAPI specification", + description: "You can import a collection from jsonObj", + }) + @ApiResponse({ + status: 200, + description: "Provided OAPI is a valid specification.", + }) + @ApiResponse({ status: 400, description: "Provided OAPI is invalid." }) + async validateOAPI(@Req() request: FastifyRequest, @Res() res: FastifyReply) { + try { + await this.parserService.validateOapi(request); + return res + .status(HttpStatus.OK) + .send({ valid: true, msg: "Provided OAPI is a valid specification." }); + } catch (error) { + return res + .status(HttpStatus.BAD_REQUEST) + .send({ valid: false, msg: "Provided OAPI is invalid." }); + } + } } diff --git a/src/modules/app/app.service.ts b/src/modules/app/app.service.ts index 104a4bef..91e3973f 100644 --- a/src/modules/app/app.service.ts +++ b/src/modules/app/app.service.ts @@ -1,18 +1,41 @@ -import { Injectable } from "@nestjs/common"; +import { BadRequestException, Injectable } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { HttpStatusCode } from "../common/enum/httpStatusCode.enum"; import { UpdaterJsonResponsePayload } from "./payloads/updaterJson.payload"; +import { FastifyRequest } from "fastify"; +import { + AuthModeEnum, + BodyModeEnum, + ItemTypeEnum, + SourceTypeEnum, +} from "../common/models/collection.model"; +import { + AddTo, + TransformedRequest, +} from "../common/models/collection.rxdb.model"; +import { ContextService } from "../common/services/context.service"; /** * Application Service */ @Injectable() export class AppService { + private curlconverterPromise: any = null; /** * Constructor * @param {ConfigService} config configuration service */ - constructor(private config: ConfigService) {} + constructor( + private config: ConfigService, + private contextService: ContextService, + ) {} + + importCurlConverter() { + if (!this.curlconverterPromise) { + this.curlconverterPromise = import("curlconverter"); + } + return this.curlconverterPromise; + } getUpdaterDetails(currentVersion: string): UpdaterJsonResponsePayload { if ( @@ -46,4 +69,279 @@ export class AppService { data: null, }; } + + async formatCurl(curlCommand: string) { + curlCommand = curlCommand.replace(/^curl/i, "curl"); + + // Remove extra spaces and line breaks + curlCommand = curlCommand.replace(/\s+/g, " ").trim(); + + return curlCommand; + } + + async parseCurl(req: FastifyRequest): Promise { + try { + const curlconverter = await this.importCurlConverter(); + const { toJsonString } = curlconverter; + const curl = req.body as string; + const updatedCurl = await this.formatCurl(curl); + if (!curl || !curl.length) { + throw new Error(); + } + return this.transformRequest(JSON.parse(toJsonString(updatedCurl))); + } catch (error) { + console.error("Error parsing :", error); + throw new BadRequestException("Invalid Curl"); + } + } + + async transformRequest(requestObject: any): Promise { + const user = await this.contextService.get("user"); + const keyValueDefaultObj = { + key: "", + value: "", + checked: false, + }; + const formDataFileDefaultObj = { + key: "", + value: "", + checked: false, + base: "", + }; + const transformedObject: TransformedRequest = { + name: requestObject.url || "", + description: "", + type: ItemTypeEnum.REQUEST, + source: SourceTypeEnum.USER, + request: { + method: requestObject.method.toUpperCase(), + url: requestObject.url ?? "", + body: { + raw: "", + urlencoded: [], + formdata: { + text: [], + file: [], + }, + }, + headers: [], + queryParams: [], + auth: { + bearerToken: "", + basicAuth: { + username: "", + password: "", + }, + apiKey: { + authKey: "", + authValue: "", + addTo: AddTo.Header, + }, + }, + selectedRequestBodyType: BodyModeEnum["none"], + selectedRequestAuthType: AuthModeEnum["No Auth"], + }, + createdBy: user.name, + updatedBy: user.name, + createdAt: new Date(), + updatedAt: new Date(), + }; + + // Handle URL with query parameters + if (requestObject.queries) { + const queryParams = []; + for (const [key, value] of Object.entries(requestObject.queries)) { + queryParams.push({ key, value, checked: true }); + if ( + key.toLowerCase() === "api-key" || + key.toLowerCase() === "x-api-key" + ) { + transformedObject.request.auth.apiKey = { + authKey: key, + authValue: value, + addTo: AddTo.QueryParameter, + }; + transformedObject.request.selectedRequestAuthType = + AuthModeEnum["API Key"]; + } + } + transformedObject.request.url = requestObject.raw_url; + transformedObject.request.queryParams = queryParams; + } + + // Handle request body based on Content-Type + if (requestObject.data) { + const contentType = + requestObject.headers["content-type"] || + requestObject.headers["Content-Type"] || + ""; + if (contentType.startsWith("multipart/form-data")) { + const boundary = contentType.split("boundary=")[1]; + const formDataParts = requestObject.data.split(`--${boundary}\r\n`); + formDataParts.shift(); // Remove the first boundary part + + for (const part of formDataParts) { + const lines = part.trim().split("\r\n"); + const disposition = lines[0]; // Content-Disposition line + if (disposition.includes('name="_method"')) { + // Ignore the _method part + continue; + } + const key = disposition.split('name="')[1].split('"')[0]; + let value = ""; + + if (lines.length > 2) { + value = lines.slice(2).join("\r\n").trim(); // Extract value from part content + } + + if (value.includes(boundary)) { + value = ""; + } + + if (disposition.includes('Content-Disposition: form-data; name="')) { + transformedObject.request.body.formdata.text.push({ + key, + value, + checked: true, + }); + } else if ( + disposition.includes( + 'Content-Disposition: form-data; name="file"', + ) && + value.startsWith("/") + ) { + transformedObject.request.body.formdata.file.push({ + key, + value, + checked: true, + base: `#@#${value}`, + }); + } + } + transformedObject.request.selectedRequestBodyType = + BodyModeEnum["multipart/form-data"]; + } else if (contentType.includes("application/json")) { + try { + transformedObject.request.body.raw = JSON.stringify( + requestObject.data, + null, + 2, + ); + } catch (error) { + console.warn("Error parsing request body JSON:", error); + transformedObject.request.body.raw = requestObject.data; + } + transformedObject.request.selectedRequestBodyType = + BodyModeEnum["application/json"]; + } else if (contentType.includes("application/javascript")) { + transformedObject.request.selectedRequestBodyType = + BodyModeEnum["application/javascript"]; + } else if (contentType.includes("text/html")) { + transformedObject.request.selectedRequestBodyType = + BodyModeEnum["text/html"]; + } else if ( + contentType.includes("application/xml") || + contentType.includes("text/xml") + ) { + transformedObject.request.selectedRequestBodyType = + BodyModeEnum["application/xml"]; + } else if (contentType.includes("application/x-www-form-urlencoded")) { + for (const [key, value] of new URLSearchParams(requestObject.data)) { + transformedObject.request.body.urlencoded.push({ + key, + value, + checked: true, + }); + } + transformedObject.request.selectedRequestBodyType = + BodyModeEnum["application/x-www-form-urlencoded"]; + } else { + console.warn(`Unsupported Content-Type: ${contentType}`); + transformedObject.request.body.raw = requestObject.data; + transformedObject.request.selectedRequestBodyType = + BodyModeEnum["text/plain"]; + } + } + + // Handle files from request object + if (requestObject.files) { + for (const [key, filename] of Object.entries(requestObject.files)) { + transformedObject.request.body.formdata.file.push({ + key, + value: filename, + checked: true, + base: `#@#${filename}`, + }); + } + } + + // Handle headers and populate auth details + if (requestObject.headers) { + for (const [key, value] of Object.entries(requestObject.headers)) { + transformedObject.request.headers.push({ key, value, checked: true }); + + // Check for Bearer token + if ( + key.toLowerCase() === "authorization" && + typeof value === "string" && + (value.startsWith("bearer ") || value.startsWith("Bearer ")) + ) { + transformedObject.request.auth.bearerToken = value.slice(7).trim(); + transformedObject.request.selectedRequestAuthType = + AuthModeEnum["Bearer Token"]; + } + + // Check for API key + if ( + key.toLowerCase() === "api-key" || + key.toLowerCase() === "x-api-key" + ) { + transformedObject.request.auth.apiKey = { + authKey: key, + authValue: value, + addTo: AddTo.Header, + }; + transformedObject.request.selectedRequestAuthType = + AuthModeEnum["API Key"]; + } + + // Check for Basic Auth + if ( + key.toLowerCase() === "authorization" && + typeof value === "string" && + (value.startsWith("basic ") || value.startsWith("Basic ")) + ) { + const decodedValue = Buffer.from(value.slice(6), "base64").toString( + "utf8", + ); + const [username, password] = decodedValue.split(":"); + transformedObject.request.auth.basicAuth = { + username, + password, + }; + transformedObject.request.selectedRequestAuthType = + AuthModeEnum["Basic Auth"]; + } + } + } + + //Assign default values + if (!transformedObject.request.headers.length) { + transformedObject.request.headers.push(keyValueDefaultObj); + } + if (!transformedObject.request.queryParams.length) { + transformedObject.request.queryParams.push(keyValueDefaultObj); + } + if (!transformedObject.request.body.formdata.text.length) { + transformedObject.request.body.formdata.text.push(keyValueDefaultObj); + } + if (!transformedObject.request.body.formdata.file.length) { + transformedObject.request.body.formdata.file.push(formDataFileDefaultObj); + } + if (!transformedObject.request.body.urlencoded.length) { + transformedObject.request.body.urlencoded.push(keyValueDefaultObj); + } + + return transformedObject; + } } diff --git a/src/modules/common/config/configuration.ts b/src/modules/common/config/configuration.ts index 71460937..d188d3da 100644 --- a/src/modules/common/config/configuration.ts +++ b/src/modules/common/config/configuration.ts @@ -11,6 +11,8 @@ export default () => ({ userBlacklistPrefix: "BL_", defaultTeamNameSuffix: "'s Team", imageSizeLimit: 102400, + deletedAPILimitInDays: 7, + timeToDaysDivisor: 86400000, refreshTokenSecretKey: process.env.REFRESH_TOKEN_SECRET_KEY, emailValidationCodeExpirationTime: parseInt( process.env.EMAIL_VALIDATION_CODE_EXPIRY_TIME, diff --git a/src/modules/common/enum/database.collection.enum.ts b/src/modules/common/enum/database.collection.enum.ts index 0cd49f88..32660c74 100644 --- a/src/modules/common/enum/database.collection.enum.ts +++ b/src/modules/common/enum/database.collection.enum.ts @@ -6,4 +6,5 @@ export enum Collections { EARLYACCESS = "earlyaccess", ENVIRONMENT = "environment", FEATURES = "features", + BRANCHES = "branches", } diff --git a/src/modules/common/models/branch.model.ts b/src/modules/common/models/branch.model.ts new file mode 100644 index 00000000..53657e17 --- /dev/null +++ b/src/modules/common/models/branch.model.ts @@ -0,0 +1,47 @@ +import { Type } from "class-transformer"; +import { + IsArray, + IsDate, + IsMongoId, + IsNotEmpty, + IsOptional, + IsString, + ValidateNested, +} from "class-validator"; +import { ApiProperty } from "@nestjs/swagger"; +import { CollectionItem } from "./collection.model"; +import { ObjectId } from "mongodb"; + +export class Branch { + @ApiProperty() + @IsString() + @IsNotEmpty() + name: string; + + @ApiProperty() + @IsMongoId() + @IsNotEmpty() + collectionId: ObjectId; + + @ApiProperty({ type: [CollectionItem] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => CollectionItem) + items: CollectionItem[]; + + @IsDate() + @IsOptional() + createdAt?: Date; + + @IsDate() + @IsOptional() + updatedAt?: Date; + + @IsString() + @IsOptional() + createdBy?: string; + + @IsString() + @IsOptional() + updatedBy?: string; +} diff --git a/src/modules/common/models/collection.model.ts b/src/modules/common/models/collection.model.ts index dfda661f..6193fbc8 100644 --- a/src/modules/common/models/collection.model.ts +++ b/src/modules/common/models/collection.model.ts @@ -21,8 +21,10 @@ export enum ItemTypeEnum { REQUEST = "REQUEST", } export enum BodyModeEnum { + "none" = "none", "application/json" = "application/json", "application/xml" = "application/xml", + "application/yaml" = "application/yaml", "application/x-www-form-urlencoded" = "application/x-www-form-urlencoded", "multipart/form-data" = "multipart/form-data", "application/javascript" = "application/javascript", @@ -30,6 +32,13 @@ export enum BodyModeEnum { "text/html" = "text/html", } +export enum AuthModeEnum { + "No Auth" = "No Auth", + "API Key" = "API Key", + "Bearer Token" = "Bearer Token", + "Basic Auth" = "Basic Auth", +} + export enum SourceTypeEnum { SPEC = "SPEC", USER = "USER", @@ -116,9 +125,17 @@ export class RequestMetaData { }) @IsEnum({ BodyModeEnum }) @IsString() - @IsNotEmpty() + @IsOptional() selectedRequestBodyType?: BodyModeEnum; + @ApiProperty({ + enum: AuthModeEnum, + }) + @IsEnum({ AuthModeEnum }) + @IsString() + @IsNotEmpty() + selectedRequestAuthType?: AuthModeEnum; + @ApiProperty({ example: { name: "search", @@ -227,12 +244,39 @@ export class CollectionItem { updatedBy: string; } +export class CollectionBranch { + @ApiProperty({ example: "64f878a0293b1e4415866493" }) + @IsString() + @IsNotEmpty() + id: string; + + @ApiProperty() + @IsString() + @IsNotEmpty() + name: string; +} + export class Collection { @ApiProperty() @IsString() @IsNotEmpty() name: string; + @ApiProperty() + @IsString() + @IsNotEmpty() + description?: string; + + @ApiProperty() + @IsString() + @IsOptional() + primaryBranch?: string; + + @ApiProperty() + @IsString() + @IsOptional() + localRepositoryPath?: string; + @ApiProperty() @IsNumber() @IsNotEmpty() @@ -259,6 +303,13 @@ export class Collection { @IsOptional() activeSyncUrl?: string; + @ApiProperty({ type: [CollectionBranch] }) + @IsArray() + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => CollectionBranch) + branches?: CollectionBranch[]; + @IsOptional() @IsDateString() createdAt?: Date; diff --git a/src/modules/common/models/collection.rxdb.model.ts b/src/modules/common/models/collection.rxdb.model.ts new file mode 100644 index 00000000..e4068ced --- /dev/null +++ b/src/modules/common/models/collection.rxdb.model.ts @@ -0,0 +1,71 @@ +import { + AuthModeEnum, + BodyModeEnum, + ItemTypeEnum, + SourceTypeEnum, +} from "./collection.model"; + +export enum AddTo { + Header = "Header", + QueryParameter = "Query Parameter", +} + +export interface TransformedRequest { + id?: string; + tag?: string; + operationId?: string; + source: SourceTypeEnum; + isDeleted?: boolean; + name: string; + description?: string; + type: ItemTypeEnum; + request: { + selectedRequestBodyType?: BodyModeEnum; + selectedRequestAuthType?: AuthModeEnum; + method: string; + url: string; + body: { + raw?: string; + urlencoded?: KeyValue[]; + formdata?: FormData; + }; + headers?: KeyValue[]; + queryParams?: KeyValue[]; + auth?: Auth; + }; + createdAt: Date; + updatedAt: Date; + createdBy: string; + updatedBy: string; +} + +interface FormData { + text: KeyValue[]; + file: FormDataFileEntry[]; +} + +interface KeyValue { + key: string; + value: string | unknown; + checked: boolean; +} + +interface FormDataFileEntry { + key: string; + value: string | unknown; + checked: boolean; + base: string; +} + +interface Auth { + bearerToken?: string; + basicAuth?: { + username: string; + password: string; + }; + apiKey?: { + authKey: string; + authValue: string | unknown; + addTo: AddTo; + }; +} diff --git a/src/modules/common/models/openapi20.model.ts b/src/modules/common/models/openapi20.model.ts new file mode 100644 index 00000000..20e4717c --- /dev/null +++ b/src/modules/common/models/openapi20.model.ts @@ -0,0 +1,152 @@ +export interface OpenAPI20 { + swagger: string; + info: InfoObject; + host?: string; + basePath?: string; + schemes?: string[]; + consumes?: string[]; + produces?: string[]; + paths: PathsObject; + definitions?: DefinitionsObject; + parameters?: ParametersDefinitionsObject; + responses?: ResponsesDefinitionsObject; + securityDefinitions?: SecurityDefinitionsObject; + security?: SecurityRequirementObject[]; + tags?: TagObject[]; + externalDocs?: ExternalDocumentationObject; +} + +interface InfoObject { + title: string; + description?: string; + termsOfService?: string; + contact?: ContactObject; + license?: LicenseObject; + version: string; +} + +interface ContactObject { + name?: string; + url?: string; + email?: string; +} + +interface LicenseObject { + name: string; + url?: string; +} + +export interface PathsObject { + [path: string]: PathItemObject; +} + +export interface PathItemObject { + $ref?: string; + get?: OperationObject; + put?: OperationObject; + post?: OperationObject; + delete?: OperationObject; + options?: OperationObject; + head?: OperationObject; + patch?: OperationObject; + parameters?: (ParameterObject | ReferenceObject)[]; +} + +export interface OperationObject { + tags?: string[]; + summary?: string; + description?: string; + externalDocs?: ExternalDocumentationObject; + operationId?: string; + consumes?: string[]; + produces?: string[]; + parameters?: (ParameterObject | ReferenceObject)[]; + responses: ResponsesObject; + schemes?: string[]; + deprecated?: boolean; + security?: SecurityRequirementObject[]; +} + +export interface ParameterObject { + type: string; + example: string; + name: string; + in: string; + description?: string; + required?: boolean; + schema?: SchemaObject; +} + +interface ReferenceObject { + $ref: string; +} + +interface ResponsesObject { + [code: string]: ResponseObject | ReferenceObject; +} + +interface ResponseObject { + description: string; + schema?: SchemaObject; +} + +interface SchemaObject { + type: string; + format?: string; + items?: SchemaObject; + properties?: { + [name: string]: SchemaRefObject; + }; + required?: string[]; + enum?: any[]; +} + +interface DefinitionsObject { + [name: string]: SchemaObject; +} + +interface ParametersDefinitionsObject { + [name: string]: ParameterObject; +} + +interface ResponsesDefinitionsObject { + [name: string]: ResponseObject; +} + +interface SecurityDefinitionsObject { + [name: string]: SecuritySchemeObject; +} + +interface SecuritySchemeObject { + type: string; + description?: string; + name: string; + in: string; + flow: string; + authorizationUrl: string; + tokenUrl: string; + scopes: { + [scope: string]: string; + }; +} + +interface SecurityRequirementObject { + [name: string]: string[]; +} + +interface TagObject { + name: string; + description?: string; + externalDocs?: ExternalDocumentationObject; +} + +interface ExternalDocumentationObject { + description?: string; + url: string; +} + +export interface SchemaRefObject extends SchemaObject, ReferenceObject { + example: any; +} + +export interface ParameterRefObject extends ParameterObject, ReferenceObject {} diff --git a/src/modules/common/models/openapi303.model.ts b/src/modules/common/models/openapi303.model.ts index fc5d3da1..1f583f9e 100644 --- a/src/modules/common/models/openapi303.model.ts +++ b/src/modules/common/models/openapi303.model.ts @@ -69,7 +69,7 @@ interface ServerVariableObject { description?: string; } -interface PathItemObject { +export interface PathItemObject { $ref?: string; summary?: string; description?: string; @@ -82,17 +82,17 @@ interface PathItemObject { patch?: OperationObject; trace?: OperationObject; servers?: ServerObject[]; - parameters?: (ParameterObject | ReferenceObject)[]; + parameters?: ParameterRefObject[]; } -interface OperationObject { +export interface OperationObject { tags?: string[]; summary?: string; description?: string; externalDocs?: ExternalDocumentationObject; operationId?: string; - parameters?: (ParameterObject | ReferenceObject)[]; - requestBody?: RequestBodyObject | ReferenceObject; + parameters?: ParameterRefObject[]; + requestBody?: RequestRefObject; responses: { [statusCode: string]: ResponseObject | ReferenceObject; }; @@ -111,15 +111,16 @@ interface ExternalDocumentationObject { export interface ParameterObject { name: string; - in: "query" | "header" | "path" | "cookie"; + in: "query" | "header" | "path" | "cookie" | "body"; description?: string; required?: boolean; deprecated?: boolean; allowEmptyValue?: boolean; style?: string; + type?: string; explode?: boolean; allowReserved?: boolean; - schema?: SchemaObject | ReferenceObject; + schema?: Schema3RefObject; example?: any; examples?: { [exampleName: string]: ExampleObject | ReferenceObject; @@ -129,7 +130,7 @@ export interface ParameterObject { }; } -interface RequestBodyObject { +export interface RequestBodyObject { description?: string; content: { [mediaType: string]: MediaTypeObject; @@ -137,6 +138,14 @@ interface RequestBodyObject { required?: boolean; } +export interface RequestRefObject extends RequestBodyObject, ReferenceObject {} + +export interface Schema3RefObject extends SchemaObject, ReferenceObject { + example: any; +} + +export interface ParameterRefObject extends ParameterObject, ReferenceObject {} + interface ResponseObject { description: string; headers?: { @@ -151,7 +160,7 @@ interface ResponseObject { } interface MediaTypeObject { - schema?: SchemaObject | ReferenceObject; + schema?: Schema3RefObject; example?: any; examples?: { [exampleName: string]: ExampleObject | ReferenceObject; @@ -193,25 +202,25 @@ export interface SchemaObject { maxProperties?: number; minProperties?: number; required?: string[]; - additionalProperties?: boolean | SchemaObject | ReferenceObject; - items?: SchemaObject | ReferenceObject; - allOf?: (SchemaObject | ReferenceObject)[]; - oneOf?: (SchemaObject | ReferenceObject)[]; - anyOf?: (SchemaObject | ReferenceObject)[]; - not?: SchemaObject | ReferenceObject; + additionalProperties?: boolean | Schema3RefObject; + items?: Schema3RefObject; + allOf?: Schema3RefObject[]; + oneOf?: Schema3RefObject[]; + anyOf?: Schema3RefObject[]; + not?: Schema3RefObject; properties?: { - [propertyName: string]: SchemaObject | ReferenceObject; + [propertyName: string]: Schema3RefObject; }; dependencies?: { [propertyName: string]: SchemaObject | string[]; }; - propertyNames?: SchemaObject | ReferenceObject; + propertyNames?: Schema3RefObject; const?: any; contentMediaType?: string; contentEncoding?: string; - if?: SchemaObject | ReferenceObject; - then?: SchemaObject | ReferenceObject; - else?: SchemaObject | ReferenceObject; + if?: Schema3RefObject; + then?: Schema3RefObject; + else?: Schema3RefObject; } interface ExampleObject { @@ -248,7 +257,7 @@ interface HeaderObject { style?: string; explode?: boolean; allowReserved?: boolean; - schema?: SchemaObject | ReferenceObject; + schema?: Schema3RefObject; example?: any; examples?: { [exampleName: string]: ExampleObject | ReferenceObject; diff --git a/src/modules/common/models/user.model.ts b/src/modules/common/models/user.model.ts index df299fc5..75758a17 100644 --- a/src/modules/common/models/user.model.ts +++ b/src/modules/common/models/user.model.ts @@ -2,6 +2,7 @@ import { Type } from "class-transformer"; import { ArrayMaxSize, IsArray, + IsBoolean, IsDate, IsEmail, IsMongoId, @@ -83,6 +84,10 @@ export class User { @IsDate() @IsOptional() verificationCodeTimeStamp?: Date; + + @IsBoolean() + @IsOptional() + isVerificationCodeActive?: boolean; } export class UserDto { diff --git a/src/modules/common/services/helper/oapi2.transformer.ts b/src/modules/common/services/helper/oapi2.transformer.ts new file mode 100644 index 00000000..88d5cb41 --- /dev/null +++ b/src/modules/common/services/helper/oapi2.transformer.ts @@ -0,0 +1,318 @@ +import { v4 as uuidv4 } from "uuid"; +import { + AuthModeEnum, + BodyModeEnum, + ItemTypeEnum, + SourceTypeEnum, +} from "../../models/collection.model"; +import { + OpenAPI20, + OperationObject, + ParameterObject, + PathItemObject, +} from "../../models/openapi20.model"; +import { AddTo, TransformedRequest } from "../../models/collection.rxdb.model"; +import { WithId } from "mongodb"; +import { User } from "../../models/user.model"; +import { + buildExampleValue, + getBaseUrl, + getExampleValue, +} from "./oapi3.transformer"; + +export function createCollectionItems( + openApiDocument: OpenAPI20, + user: WithId, +) { + const collectionItems: TransformedRequest[] = []; + if (openApiDocument.definitions) { + //Get all collection items + for (const [pathName, pathObject] of Object.entries( + openApiDocument.paths, + )) { + const request = transformPath( + pathName, + pathObject, + openApiDocument.securityDefinitions, + user, + ); + collectionItems.push({ + id: uuidv4(), + name: request.name, + tag: request.tag, + type: ItemTypeEnum.REQUEST, + description: request.description, + operationId: request.operationId, + source: SourceTypeEnum.SPEC, + request: request.request, + isDeleted: false, + createdBy: user.name, + updatedBy: user.name, + createdAt: new Date(), + updatedAt: new Date(), + }); + } + } + + const baseUrl = getBaseUrl(openApiDocument); + + //Assigning requests to folders according to their tag + const folderMap = new Map(); + for (const item of collectionItems) { + item.request.url = baseUrl + item.request.url; + let tagDescription = ""; + if (!openApiDocument.tags) { + openApiDocument.tags = [ + { + name: "default", + description: "This is a default folder", + }, + ]; + } + const itemTag = item.tag ?? "default"; + for (const tag of Object.values(openApiDocument?.tags)) { + if (tag.name === itemTag) { + tagDescription = tag.description; + } + } + let folderObj = folderMap.get(itemTag); + if (!folderObj) { + folderObj = {}; + folderObj.name = itemTag; + folderObj.description = tagDescription; + folderObj.isDeleted = false; + folderObj.source = SourceTypeEnum.SPEC; + folderObj.type = ItemTypeEnum.FOLDER; + folderObj.id = uuidv4(); + folderObj.items = []; + } + delete item.tag; + folderObj.items.push(item); + folderMap.set(folderObj.name, folderObj); + } + return folderMap; +} + +function transformPath( + pathName: string, + pathObject: PathItemObject, + security: any, + user: WithId, +) { + const keyValueDefaultObj = { + key: "", + value: "", + checked: false, + }; + const formDataFileDefaultObj = { + key: "", + value: "", + checked: false, + base: "", + }; + const transformedObject: TransformedRequest = { + name: pathName || "", + description: "", + type: ItemTypeEnum.REQUEST, + source: SourceTypeEnum.SPEC, + request: { + method: "", + url: "", + body: { + raw: "", + urlencoded: [], + formdata: { + text: [], + file: [], + }, + }, + headers: [], + queryParams: [], + auth: { + bearerToken: "", + basicAuth: { + username: "", + password: "", + }, + apiKey: { + authKey: "", + authValue: "", + addTo: AddTo.Header, + }, + }, + selectedRequestBodyType: BodyModeEnum["none"], + selectedRequestAuthType: AuthModeEnum["No Auth"], + }, + createdBy: user.name, + updatedBy: user.name, + createdAt: new Date(), + updatedAt: new Date(), + }; + const method = Object.keys(pathObject)[0].toUpperCase(); // Assuming the first key is the HTTP method + const pathItemObject: OperationObject = Object.values(pathObject)[0]; + transformedObject.tag = pathItemObject.tags + ? pathItemObject.tags[0] + : "default"; + + transformedObject.name = pathName; + transformedObject.description = + pathItemObject.summary || pathItemObject.description || ""; // Use summary or description if available + transformedObject.operationId = pathItemObject.operationId; + + transformedObject.request.method = method; + + // Extract URL path and query parameters + const urlParts = pathName.split("/").filter((p) => p != ""); + let url = ""; + for (let i = 0; i < urlParts.length; i++) { + if (urlParts[i].startsWith("{")) { + url += "/{" + urlParts[i].slice(1, -1) + "}"; + } else { + url += "/" + urlParts[i]; + } + if (i + 1 < urlParts.length && urlParts[i + 1].includes("=")) { + const queryParam = urlParts[i + 1].split("="); + transformedObject.request.queryParams.push({ + key: queryParam[0], + value: queryParam[1], + checked: true, + }); + i++; + } + } + transformedObject.request.url = url; + + let consumes: any = null; + if (pathItemObject.consumes) { + consumes = Object.values(pathItemObject.consumes) || []; + if (consumes.includes("application/json")) { + transformedObject.request.selectedRequestBodyType = + BodyModeEnum["application/json"]; + } else if (consumes.includes("application/javascript")) { + transformedObject.request.selectedRequestBodyType = + BodyModeEnum["application/javascript"]; + } else if (consumes.includes("text/html")) { + transformedObject.request.selectedRequestBodyType = + BodyModeEnum["text/html"]; + } else if ( + consumes.includes("application/xml") || + consumes.includes("text/xml") + ) { + transformedObject.request.selectedRequestBodyType = + BodyModeEnum["application/xml"]; + } else if (consumes.includes("application/x-www-form-urlencoded")) { + transformedObject.request.selectedRequestBodyType = + BodyModeEnum["application/x-www-form-urlencoded"]; + } else if (consumes.includes("multipart/form-data")) { + transformedObject.request.selectedRequestBodyType = + BodyModeEnum["multipart/form-data"]; + } + } + + if (security.api_key) { + transformedObject.request.auth.apiKey.authKey = security.api_key.name; + if (security.api_key.in === "header") { + transformedObject.request.headers.push({ + key: security.api_key.name, + value: "", + checked: false, + }); + transformedObject.request.auth.apiKey.addTo = AddTo.Header; + } else if (security.api_key.in === "query") { + transformedObject.request.queryParams.push({ + key: security.api_key.name, + value: "", + checked: false, + }); + transformedObject.request.auth.apiKey.addTo = AddTo.QueryParameter; + } + } + + // Parse request body parameters + const parameters = pathItemObject.parameters || []; + for (const param of Object.values(parameters) as ParameterObject[]) { + const paramIn = param.in; + const paramName = param.name; + const paramValue = param.example || getExampleValue(param.type); // Assuming example value is representative + + switch (paramIn) { + case "body": + if (consumes && consumes.includes("application/json")) { + const schema = param.schema; + if (schema && schema.type === "object") { + const properties = schema.properties || {}; + const bodyObject: any = {}; + for (const [propertyName, property] of Object.entries(properties)) { + const exampleType = property.type; + const exampleValue = property.example; + bodyObject[propertyName] = + exampleValue || + buildExampleValue(property) || + getExampleValue(exampleType); + } + transformedObject.request.body.raw = JSON.stringify(bodyObject); + } + } + break; + case "header": + transformedObject.request.headers.push({ + key: paramName, + value: paramValue, + checked: true, + }); + break; + case "query": + transformedObject.request.queryParams.push({ + key: paramName, + value: paramValue, + checked: false, + }); + break; + case "formData": + if ( + consumes && + consumes.includes("application/x-www-form-urlencoded") + ) { + transformedObject.request.body.urlencoded.push({ + key: paramName, + value: paramValue, + checked: false, + }); + } else if (consumes && consumes.includes("multipart/form-data")) { + if (param.type === "file") { + transformedObject.request.body.formdata.file.push({ + key: paramName, + value: paramValue, + checked: false, + base: "#@#" + paramValue, + }); + } else { + transformedObject.request.body.formdata.text.push({ + key: paramName, + value: paramValue, + checked: false, + }); + } + } + } + } + + //Assign default values + if (!transformedObject.request.headers.length) { + transformedObject.request.headers.push(keyValueDefaultObj); + } + if (!transformedObject.request.queryParams.length) { + transformedObject.request.queryParams.push(keyValueDefaultObj); + } + if (!transformedObject.request.body.formdata.text.length) { + transformedObject.request.body.formdata.text.push(keyValueDefaultObj); + } + if (!transformedObject.request.body.formdata.file.length) { + transformedObject.request.body.formdata.file.push(formDataFileDefaultObj); + } + if (!transformedObject.request.body.urlencoded.length) { + transformedObject.request.body.urlencoded.push(keyValueDefaultObj); + } + + return transformedObject; +} diff --git a/src/modules/common/services/helper/oapi3.transformer.ts b/src/modules/common/services/helper/oapi3.transformer.ts new file mode 100644 index 00000000..cfd55fbf --- /dev/null +++ b/src/modules/common/services/helper/oapi3.transformer.ts @@ -0,0 +1,400 @@ +import { v4 as uuidv4 } from "uuid"; +import { + AuthModeEnum, + BodyModeEnum, + ItemTypeEnum, + SourceTypeEnum, +} from "../../models/collection.model"; +import { OpenAPI20, SchemaRefObject } from "../../models/openapi20.model"; +import { + OpenAPI303, + OperationObject, + PathItemObject, + Schema3RefObject, +} from "../../models/openapi303.model"; +import { AddTo, TransformedRequest } from "../../models/collection.rxdb.model"; +import { WithId } from "mongodb"; +import { User } from "../../models/user.model"; + +export function createCollectionItems( + openApiDocument: OpenAPI303, + user: WithId, +) { + const collectionItems: TransformedRequest[] = []; + + for (const [pathName, pathObject] of Object.entries(openApiDocument.paths)) { + const request = transformPathV3( + pathName, + pathObject, + openApiDocument.components.securitySchemes, + user, + ); + collectionItems.push({ + id: uuidv4(), + name: request.name, + tag: request.tag, + type: ItemTypeEnum.REQUEST, + description: request.description, + operationId: request.operationId, + source: SourceTypeEnum.SPEC, + request: request.request, + isDeleted: false, + createdBy: user.name, + updatedBy: user.name, + createdAt: new Date(), + updatedAt: new Date(), + }); + } + + const baseUrl = getBaseUrl(openApiDocument); + + //Assigning requests to folders according to their tag + const folderMap = new Map(); + for (const item of collectionItems) { + item.request.url = baseUrl + item.request.url; + let tagDescription = ""; + if (!openApiDocument.tags) { + openApiDocument.tags = [ + { + name: "default", + description: "This is a default folder", + }, + ]; + } + const itemTag = item.tag ?? "default"; + for (const tag of Object.values(openApiDocument?.tags)) { + if (tag.name === itemTag) { + tagDescription = tag.description; + } + } + let folderObj = folderMap.get(itemTag); + if (!folderObj) { + folderObj = {}; + folderObj.name = itemTag; + folderObj.description = tagDescription; + folderObj.isDeleted = false; + folderObj.source = SourceTypeEnum.SPEC; + folderObj.type = ItemTypeEnum.FOLDER; + folderObj.id = uuidv4(); + folderObj.items = []; + } + delete item.tag; + folderObj.items.push(item); + folderMap.set(folderObj.name, folderObj); + } + return folderMap; +} + +function transformPathV3( + pathName: string, + pathObject: PathItemObject, + security: any, + user: WithId, +) { + const keyValueDefaultObj = { + key: "", + value: "", + checked: false, + }; + const formDataFileDefaultObj = { + key: "", + value: "", + checked: false, + base: "", + }; + const transformedObject: TransformedRequest = { + name: pathName || "", + description: "", + type: ItemTypeEnum.REQUEST, + source: SourceTypeEnum.SPEC, + request: { + method: "", + url: "", + body: { + raw: "", + urlencoded: [], + formdata: { + text: [], + file: [], + }, + }, + headers: [], + queryParams: [], + auth: { + bearerToken: "", + basicAuth: { + username: "", + password: "", + }, + apiKey: { + authKey: "", + authValue: "", + addTo: AddTo.Header, + }, + }, + selectedRequestBodyType: BodyModeEnum["none"], + selectedRequestAuthType: AuthModeEnum["No Auth"], + }, + createdBy: user.name, + updatedBy: user.name, + createdAt: new Date(), + updatedAt: new Date(), + }; + const method = Object.keys(pathObject)[0].toUpperCase(); + const pathItemObject: OperationObject = Object.values(pathObject)[0]; + transformedObject.tag = pathItemObject.tags + ? pathItemObject.tags[0] + : "default"; + transformedObject.name = pathName; + transformedObject.description = + pathItemObject.summary || pathItemObject.description || ""; + transformedObject.operationId = pathItemObject.operationId; + transformedObject.request.method = method; + + // Extract URL path and query parameters + const urlParts = pathName.split("/").filter((p) => p != ""); + let url = ""; + for (let i = 0; i < urlParts.length; i++) { + if (urlParts[i].startsWith("{")) { + url += "/{" + urlParts[i].slice(1, -1) + "}"; + } else { + url += "/" + urlParts[i]; + } + if (i + 1 < urlParts.length && urlParts[i + 1].includes("=")) { + const queryParam = urlParts[i + 1].split("="); + transformedObject.request.queryParams.push({ + key: queryParam[0], + value: queryParam[1], + checked: true, + }); + i++; + } + } + transformedObject.request.url = url; + + function extractJsonBody(schema: any, bodyObject: { [key: string]: any }) { + if (schema && schema.type === "object") { + let properties = schema.properties || {}; + + if (schema.allOf) { + for (const property of Object.values(schema.allOf) as any) { + if (property.type === "object") { + extractJsonBody(property, bodyObject); + } else if (property.properties) { + properties = property.properties; + extractJsonBody( + { + type: "object", + properties, + }, + bodyObject, + ); + } + } + } + if (properties) { + for (let [propertyName, property] of Object.entries(properties)) { + propertyName = propertyName as string; + const anyProperty = property as any; + if (anyProperty.oneOf) { + if (anyProperty.oneOf[0].type === "object") { + extractJsonBody(anyProperty.oneOf[0], bodyObject); + } else { + property = anyProperty.oneOf[0]; + } + } else if (anyProperty.allOf) { + if (anyProperty.type === "object") { + extractJsonBody(property, bodyObject); + } + } + const exampleType = anyProperty.type; + const exampleValue = anyProperty.example; + bodyObject[propertyName] = + exampleValue || + buildExampleValue(anyProperty) || + getExampleValue(exampleType); + } + } + } + } + + // Extract Request Body + const content = pathItemObject?.requestBody?.content; + if (content) { + const contentKeys = Object.keys(pathItemObject.requestBody.content) || []; + const bodyObject = {}; + for (const key of contentKeys) { + if (key === "application/json") { + const schema = content[key].schema; + extractJsonBody(schema, bodyObject); + transformedObject.request.body.raw = JSON.stringify(bodyObject); + transformedObject.request.selectedRequestBodyType = + BodyModeEnum["application/json"]; + } + if (key === "application/x-www-form-urlencoded") { + const schema = content[key].schema; + if (schema && schema.type === "object") { + const properties = schema.properties || {}; + for (const [propertyName, property] of Object.entries(properties)) { + const exampleType = property.type; + const exampleValue = property.example; // Use example if available + transformedObject.request.body.urlencoded.push({ + key: propertyName, + value: + exampleValue || + buildExampleValue(property) || + getExampleValue(exampleType), + checked: false, + }); + } + } + transformedObject.request.selectedRequestBodyType = + BodyModeEnum["application/x-www-form-urlencoded"]; + } + if (key === "application/octet-stream") { + transformedObject.request.body.formdata.file.push({ + key: "file", + value: "", + checked: false, + base: "#@#", + }); + transformedObject.request.selectedRequestBodyType = + BodyModeEnum["multipart/form-data"]; + } + if (key === "multipart/form-data") { + const schema = content[key].schema; + if (schema && schema.type === "object") { + const properties = schema.properties || {}; + for (const [propertyName, property] of Object.entries(properties)) { + if (property.type === "string" || property.type === "object") { + if (property.format === "binary") { + transformedObject.request.body.formdata.file.push({ + key: propertyName, + value: "", + checked: false, + base: "#@#" + "", + }); + } else { + transformedObject.request.body.formdata.text.push({ + key: propertyName, + value: getExampleValue(property.format), + checked: false, + }); + } + } + } + } + transformedObject.request.selectedRequestBodyType = + BodyModeEnum["multipart/form-data"]; + } + } + } + + if (security.api_key) { + transformedObject.request.auth.apiKey.authKey = security.api_key.name; + if (security.api_key.in === "header") { + transformedObject.request.headers.push({ + key: security.api_key.name, + value: "", + checked: false, + }); + transformedObject.request.auth.apiKey.addTo = AddTo.Header; + } else if (security.api_key.in === "query") { + transformedObject.request.queryParams.push({ + key: security.api_key.name, + value: "", + checked: false, + }); + transformedObject.request.auth.apiKey.addTo = AddTo.QueryParameter; + } + } + + // Parse request body parameters + const parameters = pathItemObject.parameters || []; + for (const param of Object.values(parameters)) { + const paramIn = param.in; + const paramName = param.name; + const paramValue = param.example || getExampleValue(param.type); + + switch (paramIn) { + case "header": + transformedObject.request.headers.push({ + key: paramName, + value: paramValue, + checked: true, + }); + break; + case "query": + transformedObject.request.queryParams.push({ + key: paramName, + value: paramValue, + checked: false, + }); + break; + } + } + + //Assign default values + if (!transformedObject.request.headers.length) { + transformedObject.request.headers.push(keyValueDefaultObj); + } + if (!transformedObject.request.queryParams.length) { + transformedObject.request.queryParams.push(keyValueDefaultObj); + } + if (!transformedObject.request.body.formdata.text.length) { + transformedObject.request.body.formdata.text.push(keyValueDefaultObj); + } + if (!transformedObject.request.body.formdata.file.length) { + transformedObject.request.body.formdata.file.push(formDataFileDefaultObj); + } + if (!transformedObject.request.body.urlencoded.length) { + transformedObject.request.body.urlencoded.push(keyValueDefaultObj); + } + + return transformedObject; +} + +export function getExampleValue(exampleType: string) { + switch (exampleType) { + case "string": + return ""; + case "number": + return 0; + case "integer": + return 0; + case "boolean": + return false; + case "array": + return []; + case "object": + return {}; + default: + return ""; + } +} + +export function buildExampleValue( + property: Schema3RefObject | SchemaRefObject, +) { + if (property.type === "object") { + const nestedProperties = property.properties || {}; + const nestedObject: any = {}; + for (const [nestedPropertyName, nestedProperty] of Object.entries( + nestedProperties, + )) { + nestedObject[nestedPropertyName] = buildExampleValue(nestedProperty); + } + return nestedObject; + } else { + return property.example || getExampleValue(property.type); + } +} + +export function getBaseUrl(openApiDocument: OpenAPI20 | OpenAPI303) { + const basePath = openApiDocument.basePath ? openApiDocument.basePath : ""; + if (openApiDocument.host) { + return "https://" + openApiDocument.host + basePath; + } else { + return "http://localhost:{{PORT}}" + basePath; + } +} diff --git a/src/modules/common/services/helper/parser.helper.ts b/src/modules/common/services/helper/parser.helper.ts new file mode 100644 index 00000000..26f51c9e --- /dev/null +++ b/src/modules/common/services/helper/parser.helper.ts @@ -0,0 +1,93 @@ +export function resolveAllRefs(spec: any) { + if (!spec) return spec; + + if (typeof spec === "object") { + const componentToResolve = spec.components ?? spec.definitions; + const resolvedSpec: { [key: string]: any } = {}; + for (const key in spec) { + if (componentToResolve.schemas) { + resolvedSpec[key] = resolveComponentRef( + spec[key], + componentToResolve, + [], + ); + } else { + resolvedSpec[key] = resolveDefinitionRef(spec[key], componentToResolve); + } + } + return resolvedSpec; + } + + return spec; +} + +function resolveComponentRef(data: any, components: any, cache: any): any { + if (!data) return data; + + if (typeof data === "object") { + if (data.hasOwnProperty("$ref") && typeof data["$ref"] === "string") { + const refPath = data["$ref"]; + if (refPath.startsWith("#/components/schemas/")) { + const schemaName = refPath.split("/").pop(); + if ( + components && + components.schemas && + components.schemas[schemaName] + ) { + if (cache.indexOf(schemaName) !== -1) { + // Circular reference found, replace with undefined + return undefined; + } + cache.push(schemaName); + return resolveComponentRef( + components.schemas[schemaName], + components, + cache, + ); + } else { + console.warn(`Reference "${refPath}" not found in components`); + return data; + } + } else { + return data; + } + } else { + const newData: { [key: string]: any } = {}; + for (const key in data) { + newData[key] = resolveComponentRef(data[key], components, cache); + } + return newData; + } + } + + return data; +} + +function resolveDefinitionRef(data: any, definitions: any): any { + if (!data) return data; + + if (typeof data === "object") { + if (data.hasOwnProperty("$ref") && typeof data["$ref"] === "string") { + const refPath = data["$ref"]; + if (refPath.startsWith("#/definitions/")) { + const schemaName = refPath.split("/").pop(); + if (definitions && definitions[schemaName]) { + return resolveDefinitionRef(definitions[schemaName], definitions); + } else { + console.warn(`Reference "${refPath}" not found in definitions`); + return data; + } + } else { + return data; + } + } else { + const newData: { [key: string]: any } = {}; + for (const key in data) { + newData[key] = resolveDefinitionRef(data[key], definitions); + } + return newData; + } + } + + return data; +} diff --git a/src/modules/common/services/parser.service.ts b/src/modules/common/services/parser.service.ts index 47f9a4ca..bad06305 100644 --- a/src/modules/common/services/parser.service.ts +++ b/src/modules/common/services/parser.service.ts @@ -1,113 +1,69 @@ import SwaggerParser from "@apidevtools/swagger-parser"; -// import * as util from "util"; import { - BodyModeEnum, Collection, CollectionItem, ItemTypeEnum, - RequestBody, - RequestMetaData, SourceTypeEnum, } from "../models/collection.model"; -import { OpenAPI303, ParameterObject } from "../models/openapi303.model"; -import { HTTPMethods } from "fastify"; +import { OpenAPI303 } from "../models/openapi303.model"; import { Injectable } from "@nestjs/common"; import { ContextService } from "./context.service"; -import { v4 as uuidv4 } from "uuid"; import { CollectionService } from "@src/modules/workspace/services/collection.service"; import { WithId } from "mongodb"; +import { resolveAllRefs } from "./helper/parser.helper"; +import { OpenAPI20 } from "../models/openapi20.model"; +import * as oapi2Transformer from "./helper/oapi2.transformer"; +import * as oapi3Transformer from "./helper/oapi3.transformer"; +import { BranchService } from "@src/modules/workspace/services/branch.service"; +import { Branch } from "../models/branch.model"; +import { FastifyRequest } from "fastify"; +import axios from "axios"; +import * as yml from "js-yaml"; +interface ActiveSyncResponsePayload { + collection: WithId; + existingCollection: boolean; +} @Injectable() export class ParserService { constructor( private readonly contextService: ContextService, private readonly collectionService: CollectionService, + private readonly branchService: BranchService, ) {} + async parse( file: string, activeSync?: boolean, workspaceId?: string, activeSyncUrl?: string, + primaryBranch?: string, + currentBranch?: string, + localRepositoryPath?: string, ): Promise<{ collection: WithId; existingCollection: boolean; }> { - const openApiDocument = (await SwaggerParser.parse(file)) as OpenAPI303; - const baseUrl = this.getBaseUrl(openApiDocument); - let existingCollection: WithId | null = null; - const folderObjMap = new Map(); - for (const [key, value] of Object.entries(openApiDocument.paths)) { - //key will be endpoints /put and values will its apis post ,put etc - for (const [innerKey, innerValue] of Object.entries(value)) { - //key will be api methods and values will it's desc - const requestObj: CollectionItem = {} as CollectionItem; - requestObj.name = key; - requestObj.description = innerValue.description; - requestObj.type = ItemTypeEnum.REQUEST; - requestObj.source = SourceTypeEnum.SPEC; - requestObj.id = uuidv4(); - requestObj.isDeleted = false; - requestObj.request = {} as RequestMetaData; - requestObj.request.method = innerKey.toUpperCase() as HTTPMethods; - requestObj.request.operationId = innerValue.operationId; - requestObj.request.url = baseUrl + key; - - if (innerValue.parameters?.length) { - requestObj.request.queryParams = innerValue.parameters.filter( - (param: ParameterObject) => param.in === "query", - ); - requestObj.request.pathParams = innerValue.parameters.filter( - (param: ParameterObject) => param.in === "path", - ); - requestObj.request.headers = innerValue.parameters.filter( - (param: ParameterObject) => param.in === "header", - ); - } - if (innerValue.requestBody) { - requestObj.request.body = []; - const bodyTypes = innerValue.requestBody.content; - for (const [type, schema] of Object.entries(bodyTypes)) { - const body: RequestBody = {} as RequestBody; - body.type = Object.values(BodyModeEnum).find( - (enumMember) => enumMember === type, - ) as BodyModeEnum; - const ref = (schema as any).schema?.$ref; - if (ref) { - const schemaName = ref.slice( - ref.lastIndexOf("/") + 1, - ref.length, - ); - body.schema = openApiDocument.components.schemas[schemaName]; - } else { - body.schema = (schema as any).schema; - } - requestObj.request.body.push(body); - } - } - //Add to a folder - const tag = innerValue.tags ? innerValue.tags[0] : "default"; - const tagArr = - openApiDocument?.tags?.length > 0 && - openApiDocument.tags.filter((tagObj) => { - return tagObj.name === tag; - }); - let folderObj: CollectionItem = folderObjMap.get(tag); - if (!folderObj) { - folderObj = {} as CollectionItem; - folderObj.name = tag; - folderObj.description = tagArr ? tagArr[0].description : ""; - folderObj.isDeleted = false; - folderObj.type = ItemTypeEnum.FOLDER; - folderObj.id = uuidv4(); - folderObj.items = []; - } - folderObj.items.push(requestObj); - folderObjMap.set(folderObj.name, folderObj); - } + let openApiDocument = (await SwaggerParser.parse(file)) as + | OpenAPI303 + | OpenAPI20; + let folderObjMap = new Map(); + const user = await this.contextService.get("user"); + if (openApiDocument.hasOwnProperty("components")) { + openApiDocument = resolveAllRefs(openApiDocument) as OpenAPI303; + folderObjMap = oapi3Transformer.createCollectionItems( + openApiDocument, + user, + ); + } else if (openApiDocument.hasOwnProperty("definitions")) { + openApiDocument = resolveAllRefs(openApiDocument) as OpenAPI20; + folderObjMap = oapi2Transformer.createCollectionItems( + openApiDocument, + user, + ); } - const itemObject = Object.fromEntries(folderObjMap); - let items: CollectionItem[] = []; + const items: CollectionItem[] = []; let totalRequests = 0; for (const key in itemObject) { if (itemObject.hasOwnProperty(key)) { @@ -118,118 +74,263 @@ export class ParserService { items.map((itemObj) => { totalRequests = totalRequests + itemObj.items?.length; }); - const user = await this.contextService.get("user"); + let collection: Collection; if (activeSync) { - let mergedFolderItems: CollectionItem[] = []; - existingCollection = - await this.collectionService.getActiveSyncedCollection( - openApiDocument.info.title, - workspaceId, - ); - if (existingCollection) { - //check on folder level - mergedFolderItems = this.compareAndMerge( - existingCollection.items, - items, - ); - for (let x = 0; x < existingCollection.items?.length; x++) { - const newItem: CollectionItem[] = items.filter((item) => { - return item.name === existingCollection.items[x].name; - }); - //check on request level - const mergedFolderRequests: CollectionItem[] = this.compareAndMerge( - existingCollection.items[x].items, - newItem[0]?.items || [], - ); - mergedFolderItems[x].items = mergedFolderRequests; - } - items = mergedFolderItems; - } - } - const newItems: CollectionItem[] = []; - for (let x = 0; x < items?.length; x++) { - const itemsObj: CollectionItem = { - name: items[x].name, - description: items[x].description, - id: items[x].id, - type: items[x].type, - isDeleted: items[x].isDeleted, - source: SourceTypeEnum.SPEC, - createdBy: user.name, - updatedBy: user.name, + const { collection, existingCollection } = await this.runActiveSyncFlow( + openApiDocument, + workspaceId, + primaryBranch, + currentBranch, + totalRequests, + activeSyncUrl, + items, + localRepositoryPath, + ); + return { + collection, + existingCollection, + }; + } else { + collection = { + name: openApiDocument.info.title, + description: openApiDocument.info.description, + primaryBranch: "", + localRepositoryPath: "", + branches: [], + totalRequests, + items: items, + uuid: openApiDocument.info.title, + activeSync: false, + activeSyncUrl: "", createdAt: new Date(), updatedAt: new Date(), + createdBy: user.name, + updatedBy: user.name, }; - const innerArray: CollectionItem[] = []; - for (let y = 0; y < items[x].items?.length; y++) { - const data = this.handleCircularReference(items[x].items[y]); - innerArray.push(JSON.parse(data)); - } - itemsObj.items = innerArray; - newItems.push(itemsObj); } - const collection: Collection = { - name: openApiDocument.info.title, - totalRequests, - items: newItems, - uuid: openApiDocument.info.title, - createdBy: user.name, - updatedBy: user.name, - activeSync, - activeSyncUrl: activeSyncUrl ?? "", - createdAt: new Date(), - updatedAt: new Date(), + const newCollection = await this.collectionService.importCollection( + collection, + ); + const collectionDetails = await this.collectionService.getCollection( + newCollection.insertedId.toString(), + ); + return { + collection: collectionDetails, + existingCollection: false, }; + } + async runActiveSyncFlow( + openApiDocument: OpenAPI20 | OpenAPI303, + workspaceId: string, + primaryBranch: string, + currentBranch: string, + totalRequests: number, + activeSyncUrl: string, + items: CollectionItem[], + localRepositoryPath: string, + ): Promise { + const collectionTitle = openApiDocument.info.title; + let mergedFolderItems: CollectionItem[] = []; + const existingCollection = + await this.collectionService.getActiveSyncedCollection( + collectionTitle, + workspaceId, + ); if (existingCollection) { - await this.collectionService.updateImportedCollection( + //Get existing branch or create one + const branch = await this.createOrFetchBranch( + currentBranch, existingCollection._id.toString(), - collection, + workspaceId, + items, ); + + //Check items on folder level + mergedFolderItems = this.compareAndMerge(branch.items, items); + for (let x = 0; x < branch.items?.length; x++) { + const newItem: CollectionItem[] = items.filter((item) => { + return item.name === branch.items[x].name; + }); + //Check items on request level + const mergedFolderRequests: CollectionItem[] = this.compareAndMerge( + branch.items[x].items ?? [], + newItem[0]?.items || [], + ); + mergedFolderItems[x].items = mergedFolderRequests; + } + + this.updateItemsInbranch( + workspaceId, + branch._id.toString(), + mergedFolderItems, + ); + + //Update collection Items const updatedCollection = await this.collectionService.getCollection( existingCollection._id.toString(), ); + updatedCollection.items = mergedFolderItems; + + //No need for this as collection will be fetched from branch model + // this.updateItemsInCollection( + // workspaceId, + // existingCollection._id.toString(), + // mergedFolderItems, + // ); + return { collection: updatedCollection, existingCollection: true, }; - } else { - const newCollection = await this.collectionService.importCollection( - collection, - ); - const collectionDetails = await this.collectionService.getCollection( - newCollection.insertedId.toString(), - ); - collectionDetails; - return { - collection: collectionDetails, - existingCollection: false, - }; } + const user = await this.contextService.get("user"); + + const collection: Collection = { + name: collectionTitle, + description: openApiDocument.info.description, + primaryBranch: primaryBranch ?? "", + localRepositoryPath: localRepositoryPath ?? "", + totalRequests, + items: items, + branches: [], + uuid: collectionTitle, + activeSync: true, + activeSyncUrl: activeSyncUrl ?? "", + createdAt: new Date(), + updatedAt: new Date(), + createdBy: user.name, + updatedBy: user.name, + }; + const insertedCollection = await this.collectionService.importCollection( + collection, + ); + const collectionId = insertedCollection.insertedId.toString(); + const branch = await this.branchService.createBranch({ + name: currentBranch, + items: items, + collectionId, + }); + + await this.collectionService.updateBranchArray( + collectionId, + { id: branch.insertedId.toString(), name: currentBranch }, + workspaceId, + ); + + return { + collection: await this.collectionService.getCollection( + insertedCollection.insertedId.toString(), + ), + existingCollection: false, + }; + } + + async createOrFetchBranch( + currentBranch: string, + collectionId: string, + workspaceId: string, + items: CollectionItem[], + ): Promise> { + const existingBranch = await this.collectionService.getActiveSyncedBranch( + collectionId, + currentBranch, + ); + if (existingBranch) { + return existingBranch; + } + const insertedBranch = await this.branchService.createBranch({ + name: currentBranch, + items: items, + collectionId, + }); + const branch = await this.branchService.getBranch( + insertedBranch.insertedId.toString(), + ); + await this.updateBranchInCollection(workspaceId, collectionId, branch); + return branch; } - handleCircularReference(obj: CollectionItem) { - const cache: any = []; - return JSON.stringify(obj, function (key, value) { - if (typeof value === "object" && value !== null) { - if (cache.indexOf(value) !== -1) { - // Circular reference found, replace with undefined - return undefined; + + async updateItemsInbranch( + workspaceId: string, + branchId: string, + items: CollectionItem[], + ) { + await this.branchService.updateBranch(workspaceId, branchId, items); + } + + async updateItemsInCollection( + workspaceId: string, + collectionId: string, + items: CollectionItem[], + ) { + await this.collectionService.updateCollection( + collectionId, + { items }, + workspaceId, + ); + } + + async updateBranchInCollection( + workspaceId: string, + collectionId: string, + branch: WithId, + ) { + await this.collectionService.updateBranchArray( + collectionId, + { id: branch._id.toString(), name: branch.name }, + workspaceId, + ); + } + + async validateOapi(request: FastifyRequest): Promise { + try { + let data: any; + const url = request.headers["x-oapi-url"] || null; + const oapi = request.body; + if (url) { + const response = await axios.get(url as string); + data = response.data; + } else { + try { + data = yml.load(oapi as string); + if (data[0] == "object Object") throw new Error(); + } catch (err) { + data = JSON.stringify(oapi); + data = oapi; } - // Store value in our collection - cache.push(value); } - return value; - }); + await SwaggerParser.parse(data); + return; + } catch (err) { + throw new Error("Invalid OAPI."); + } } + + validateUrlIsALocalhostUrl(url: string): boolean { + const urlObject = new URL(url); // Create a URL object for parsing + + // Check if protocol is http or https (localhost only works with these) + if (!["http:", "https:"].includes(urlObject.protocol)) { + return false; + } + + // Check if hostname is 'localhost' or starts with 127.0.0.1 + return ( + urlObject.hostname === "localhost" || + urlObject.hostname.startsWith("127.0.0.1") + ); + } + compareAndMerge( existingitems: CollectionItem[], newItems: CollectionItem[], ): CollectionItem[] { const newItemMap = newItems ? new Map( - newItems.map((item) => [ + newItems?.map((item) => [ item.type === ItemTypeEnum.FOLDER ? item.name : item.name + item.request?.method, @@ -239,7 +340,7 @@ export class ParserService { : new Map(); const existingItemMap = existingitems ? new Map( - existingitems.map((item) => [ + existingitems?.map((item) => [ item.type === ItemTypeEnum.FOLDER ? item.name : item.name + item.request?.method, @@ -248,7 +349,7 @@ export class ParserService { ) : new Map(); // Merge old and new items while marking deleted - const mergedArray: CollectionItem[] = existingitems.map((existingItem) => { + const mergedArray: CollectionItem[] = existingitems?.map((existingItem) => { if ( newItemMap.has( existingItem.type === ItemTypeEnum.FOLDER @@ -285,12 +386,4 @@ export class ParserService { return mergedArray; } - getBaseUrl(openApiDocument: OpenAPI303): string { - const basePath = openApiDocument.basePath ? openApiDocument.basePath : ""; - if (openApiDocument.host) { - return "https://" + openApiDocument.host + basePath; - } else { - return "http://localhost:{{PORT}}" + basePath; - } - } } diff --git a/src/modules/identity/controllers/team.controller.ts b/src/modules/identity/controllers/team.controller.ts index 30a34dcd..f7c47834 100644 --- a/src/modules/identity/controllers/team.controller.ts +++ b/src/modules/identity/controllers/team.controller.ts @@ -132,7 +132,7 @@ export class TeamController { @ApiResponse({ status: 400, description: "Updated Team Failed" }) async updateTeam( @Param("teamId") teamId: string, - @Body() updateTeamDto: UpdateTeamDto, + @Body() updateTeamDto: Partial, @Res() res: FastifyReply, @UploadedFile() image: MemoryStorageFile, diff --git a/src/modules/identity/controllers/user.controller.ts b/src/modules/identity/controllers/user.controller.ts index e4866b7b..7fd2e09f 100644 --- a/src/modules/identity/controllers/user.controller.ts +++ b/src/modules/identity/controllers/user.controller.ts @@ -31,6 +31,7 @@ import { import { RefreshTokenGuard } from "@src/modules/common/guards/refresh-token.guard"; import { RefreshTokenRequest } from "./auth.controller"; import { JwtAuthGuard } from "@src/modules/common/guards/jwt-auth.guard"; +import { ConfigService } from "@nestjs/config"; /** * User Controller */ @@ -38,7 +39,10 @@ import { JwtAuthGuard } from "@src/modules/common/guards/jwt-auth.guard"; @ApiTags("user") @Controller("api/user") export class UserController { - constructor(private readonly userService: UserService) {} + constructor( + private readonly userService: UserService, + private readonly configService: ConfigService, + ) {} @Post() @ApiOperation({ @@ -100,7 +104,7 @@ export class UserController { @UseGuards(JwtAuthGuard) async updateUser( @Param("userId") id: string, - @Body() updateUserDto: UpdateUserDto, + @Body() updateUserDto: Partial, @Res() res: FastifyReply, ) { const user = await this.userService.updateUser(id, updateUserDto); @@ -196,13 +200,21 @@ export class UserController { @Res() res: FastifyReply, @Body() verifyEmailPayload: VerifyEmailPayload, ) { + const expireTime = this.configService.get( + "app.emailValidationCodeExpirationTime", + ); await this.userService.verifyVerificationCode( verifyEmailPayload.email, verifyEmailPayload.verificationCode, + expireTime, + ); + const data = await this.userService.refreshVerificationCode( + verifyEmailPayload.email, ); const responseData = new ApiResponseService( "Email Verified Successfully", HttpStatusCode.OK, + data, ); return res.status(responseData.httpStatusCode).send(responseData); } @@ -217,6 +229,14 @@ export class UserController { @Res() res: FastifyReply, @Body() updatePasswordPayload: UpdatePasswordPayload, ) { + const expireTime = this.configService.get( + "app.emailValidationCodeExpirationTime", + ); + await this.userService.verifyVerificationCode( + updatePasswordPayload.email, + updatePasswordPayload.verificationCode, + expireTime, + ); await this.userService.updatePassword( updatePasswordPayload.email, updatePasswordPayload.newPassword, diff --git a/src/modules/identity/payloads/resetPassword.payload.ts b/src/modules/identity/payloads/resetPassword.payload.ts index c1561b82..487d81b6 100644 --- a/src/modules/identity/payloads/resetPassword.payload.ts +++ b/src/modules/identity/payloads/resetPassword.payload.ts @@ -1,5 +1,5 @@ import { ApiProperty } from "@nestjs/swagger"; -import { IsEmail, IsNotEmpty, MinLength } from "class-validator"; +import { IsEmail, IsNotEmpty, Matches, MinLength } from "class-validator"; export class ResetPasswordPayload { @ApiProperty({ @@ -49,7 +49,21 @@ export class UpdatePasswordPayload { required: true, example: "newPassword", }) - @MinLength(8) @IsNotEmpty() + @Matches(/(?=.*[0-9])/, { + message: "password must contain at least one digit.", + }) + @Matches(/(?=.*[!@#$%^&*])/, { + message: "password must contain at least one special character (!@#$%^&*).", + }) + @MinLength(8) newPassword: string; + + @ApiProperty({ + required: true, + example: "ABC123", + }) + @MinLength(6) + @IsNotEmpty() + verificationCode: string; } diff --git a/src/modules/identity/repositories/team.repository.ts b/src/modules/identity/repositories/team.repository.ts index fcfe07dc..0e704919 100644 --- a/src/modules/identity/repositories/team.repository.ts +++ b/src/modules/identity/repositories/team.repository.ts @@ -95,7 +95,7 @@ export class TeamRepository { */ async update( id: string, - payload: UpdateTeamDto, + payload: Partial, ): Promise> { const _id = new ObjectId(id); const updatedTeam = await this.db @@ -136,7 +136,7 @@ export class TeamRepository { async updateTeamById( id: ObjectId, - updateParams: TeamDto, + updateParams: Partial, ): Promise> { const updatedTeamParams = { $set: updateParams, diff --git a/src/modules/identity/repositories/user.repository.ts b/src/modules/identity/repositories/user.repository.ts index 62973f1f..c2afd95b 100644 --- a/src/modules/identity/repositories/user.repository.ts +++ b/src/modules/identity/repositories/user.repository.ts @@ -102,7 +102,7 @@ export class UserRepository { */ async updateUser( userId: string, - payload: UpdateUserDto, + payload: Partial, ): Promise> { const _id = new ObjectId(userId); const updatedUser = await this.db @@ -146,7 +146,7 @@ export class UserRepository { async updateUserById( id: ObjectId, - updateParams: UserDto, + updateParams: Partial, ): Promise> { const updatedUserParams = { $set: updateParams, @@ -224,6 +224,18 @@ export class UserRepository { $set: { verificationCode, verificationCodeTimeStamp: new Date(), + isVerificationCodeActive: true, + }, + }, + ); + } + + async expireVerificationCode(email: string): Promise { + await this.db.collection(Collections.USER).findOneAndUpdate( + { email }, + { + $set: { + isVerificationCodeActive: false, }, }, ); diff --git a/src/modules/identity/services/team.service.ts b/src/modules/identity/services/team.service.ts index 077d3c3a..d5fc13f3 100644 --- a/src/modules/identity/services/team.service.ts +++ b/src/modules/identity/services/team.service.ts @@ -120,7 +120,7 @@ export class TeamService { */ async update( id: string, - teamData: UpdateTeamDto, + teamData: Partial, image?: MemoryStorageFile, ): Promise> { const teamOwner = await this.isTeamOwner(id); diff --git a/src/modules/identity/services/user.service.ts b/src/modules/identity/services/user.service.ts index 7f7c975b..e79b0fa9 100644 --- a/src/modules/identity/services/user.service.ts +++ b/src/modules/identity/services/user.service.ts @@ -133,7 +133,7 @@ export class UserService { */ async updateUser( userId: string, - payload: UpdateUserDto, + payload: Partial, ): Promise> { const data = await this.userRepository.updateUser(userId, payload); return data; @@ -269,14 +269,15 @@ export class UserService { async verifyVerificationCode( email: string, verificationCode: string, + expireTime: number, ): Promise { const user = await this.getUserByEmail(email); + if (!user?.isVerificationCodeActive) { + throw new UnauthorizedException(ErrorMessages.Unauthorized); + } if (user?.verificationCode !== verificationCode.toUpperCase()) { throw new UnauthorizedException(ErrorMessages.Unauthorized); } - const expireTime = this.configService.get( - "app.emailValidationCodeExpirationTime", - ); if ( (Date.now() - user.verificationCodeTimeStamp.getTime()) / 1000 > expireTime @@ -285,6 +286,18 @@ export class UserService { } return; } + + async expireVerificationCode(email: string): Promise { + await this.userRepository.expireVerificationCode(email); + return; + } + + async refreshVerificationCode(email: string): Promise { + const verificationCode = this.generateEmailVerificationCode().toUpperCase(); + await this.userRepository.updateVerificationCode(email, verificationCode); + return verificationCode; + } + async updatePassword(email: string, password: string): Promise { const user = await this.getUserByEmailAndPass(email, password); @@ -292,8 +305,10 @@ export class UserService { throw new UnauthorizedException(ErrorMessages.PasswordExist); } await this.userRepository.updatePassword(email, password); + await this.expireVerificationCode(email); return; } + generateEmailVerificationCode(): string { return (Math.random() + 1).toString(36).substring(2, 8); } diff --git a/src/modules/workspace/controllers/collection.controller.ts b/src/modules/workspace/controllers/collection.controller.ts index 840a16d1..9ea71085 100644 --- a/src/modules/workspace/controllers/collection.controller.ts +++ b/src/modules/workspace/controllers/collection.controller.ts @@ -25,6 +25,7 @@ import { ApiResponseService } from "@src/modules/common/services/api-response.se import { HttpStatusCode } from "@src/modules/common/enum/httpStatusCode.enum"; import { WorkspaceService } from "../services/workspace.service"; import { + BranchChangeDto, CollectionRequestDto, FolderPayload, } from "../payloads/collectionRequest.payload"; @@ -53,7 +54,7 @@ export class collectionController { @ApiResponse({ status: 201, description: "Collection Created Successfully" }) @ApiResponse({ status: 400, description: "Create Collection Failed" }) async createCollection( - @Body() createCollectionDto: CreateCollectionDto, + @Body() createCollectionDto: Partial, @Res() res: FastifyReply, ) { const workspaceId = createCollectionDto.workspaceId; @@ -110,7 +111,7 @@ export class collectionController { async updateCollection( @Param("collectionId") collectionId: string, @Param("workspaceId") workspaceId: string, - @Body() updateCollectionDto: UpdateCollectionDto, + @Body() updateCollectionDto: Partial, @Res() res: FastifyReply, ) { await this.collectionService.updateCollection( @@ -171,7 +172,7 @@ export class collectionController { async addFolder( @Param("collectionId") collectionId: string, @Param("workspaceId") workspaceId: string, - @Body() body: FolderPayload, + @Body() body: Partial, @Res() res: FastifyReply, ) { const newFolder = await this.collectionRequestService.addFolder({ @@ -198,7 +199,7 @@ export class collectionController { @Param("collectionId") collectionId: string, @Param("workspaceId") workspaceId: string, @Param("folderId") folderId: string, - @Body() body: FolderPayload, + @Body() body: Partial, @Res() res: FastifyReply, ) { const updatedfolder = await this.collectionRequestService.updateFolder({ @@ -226,13 +227,16 @@ export class collectionController { @Param("collectionId") collectionId: string, @Param("workspaceId") workspaceId: string, @Param("folderId") folderId: string, + @Body() branchNameDto: Partial, @Res() res: FastifyReply, ) { - const response = await this.collectionRequestService.deleteFolder({ - collectionId, - workspaceId, - folderId, - }); + const payload = { + collectionId: collectionId, + workspaceId: workspaceId, + folderId: folderId, + currentBranch: branchNameDto.branchName, + }; + const response = await this.collectionRequestService.deleteFolder(payload); const responseData = new ApiResponseService( "Success", HttpStatusCode.OK, @@ -250,7 +254,7 @@ export class collectionController { @ApiResponse({ status: 200, description: "Request Updated Successfully" }) @ApiResponse({ status: 400, description: "Failed to Update a request" }) async addRequest( - @Body() requestDto: CollectionRequestDto, + @Body() requestDto: Partial, @Res() res: FastifyReply, ) { const collectionId = requestDto.collectionId; @@ -288,7 +292,7 @@ export class collectionController { @ApiResponse({ status: 400, description: "Failed to save request" }) async updateRequest( @Param("requestId") requestId: string, - @Body() requestDto: CollectionRequestDto, + @Body() requestDto: Partial, @Res() res: FastifyReply, ) { const collectionId = requestDto.collectionId; @@ -322,7 +326,7 @@ export class collectionController { @ApiResponse({ status: 400, description: "Failed to delete request" }) async deleteRequest( @Param("requestId") requestId: string, - @Body() requestDto: CollectionRequestDto, + @Body() requestDto: Partial, @Res() res: FastifyReply, ) { const collectionId = requestDto.collectionId; @@ -339,7 +343,7 @@ export class collectionController { collectionId, requestId, noOfRequests, - requestDto.folderId, + requestDto, ); const collection = await this.collectionService.getCollection(collectionId); @@ -350,4 +354,28 @@ export class collectionController { ); return res.status(responseData.httpStatusCode).send(responseData); } + + @Post(":collectionId/branch") + @ApiOperation({ + summary: "Get collection items as per the branch selected", + description: "Switch branch to get collection of that branch", + }) + @ApiResponse({ status: 201, description: "Branch switched Successfully" }) + @ApiResponse({ status: 400, description: "Failed to switch branch" }) + async switchCollectionBranch( + @Param("collectionId") collectionId: string, + @Body() branchChangeDto: BranchChangeDto, + @Res() res: FastifyReply, + ) { + const branch = await this.collectionService.getBranchData( + collectionId, + branchChangeDto.branchName, + ); + const responseData = new ApiResponseService( + "Branch switched Successfully", + HttpStatusCode.OK, + branch, + ); + return res.status(responseData.httpStatusCode).send(responseData); + } } diff --git a/src/modules/workspace/controllers/environment.controller.ts b/src/modules/workspace/controllers/environment.controller.ts index 6ff1c3ba..da785d12 100644 --- a/src/modules/workspace/controllers/environment.controller.ts +++ b/src/modules/workspace/controllers/environment.controller.ts @@ -134,7 +134,7 @@ export class EnvironmentController { async updateEnvironment( @Param("workspaceId") workspaceId: string, @Param("environmentId") environmentId: string, - @Body() updateEnvironmentDto: UpdateEnvironmentDto, + @Body() updateEnvironmentDto: Partial, @Res() res: FastifyReply, ) { await this.environmentService.updateEnvironment( diff --git a/src/modules/workspace/controllers/workspace.controller.ts b/src/modules/workspace/controllers/workspace.controller.ts index 72df9bcb..4c203d66 100644 --- a/src/modules/workspace/controllers/workspace.controller.ts +++ b/src/modules/workspace/controllers/workspace.controller.ts @@ -37,7 +37,6 @@ import { import * as yml from "js-yaml"; import { ParserService } from "@src/modules/common/services/parser.service"; import { CollectionService } from "../services/collection.service"; -import axios from "axios"; import { ImportCollectionDto } from "../payloads/collection.payload"; import { JwtAuthGuard } from "@src/modules/common/guards/jwt-auth.guard"; import { ObjectId } from "mongodb"; @@ -189,7 +188,7 @@ export class WorkSpaceController { @ApiResponse({ status: 400, description: "Update Workspace Failed" }) async updateWorkspace( @Param("workspaceId") workspaceId: string, - @Body() updateWorkspaceDto: UpdateWorkspaceDto, + @Body() updateWorkspaceDto: Partial, @Res() res: FastifyReply, ) { await this.workspaceService.update(workspaceId, updateWorkspaceDto); @@ -359,9 +358,8 @@ export class WorkSpaceController { @Body() importCollectionDto: ImportCollectionDto, ) { const activeSync = importCollectionDto.activeSync ?? false; - const response = await axios.get(importCollectionDto.url); - const data = response.data; - const responseType = response.headers["content-type"]; + const data = importCollectionDto.urlData.data; + const responseType = importCollectionDto.urlData.headers["content-type"]; const dataObj = responseType.includes(BodyModeEnum["application/json"]) ? data : yml.load(data); @@ -371,6 +369,9 @@ export class WorkSpaceController { activeSync, workspaceId, importCollectionDto.url, + importCollectionDto?.primaryBranch, + importCollectionDto?.currentBranch, + importCollectionDto?.localRepositoryPath, ); if (!collectionObj.existingCollection) { await this.workspaceService.addCollectionInWorkSpace(workspaceId, { diff --git a/src/modules/workspace/payloads/branch.payload.ts b/src/modules/workspace/payloads/branch.payload.ts new file mode 100644 index 00000000..fb34f97b --- /dev/null +++ b/src/modules/workspace/payloads/branch.payload.ts @@ -0,0 +1,45 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { CollectionItem } from "@src/modules/common/models/collection.model"; +import { Type } from "class-transformer"; +import { + IsArray, + IsDate, + IsNotEmpty, + IsOptional, + IsString, + ValidateNested, +} from "class-validator"; + +export class createBranchDto { + @IsString() + @ApiProperty({ required: true, example: "Branch name" }) + @IsNotEmpty() + name: string; + + @IsString() + @ApiProperty({ required: true, example: "65ed7a82af45cb59f471a983" }) + @IsNotEmpty() + collectionId: string; + + @ApiProperty({ type: [CollectionItem] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => CollectionItem) + items: CollectionItem[]; +} + +export class UpdateBranchDto { + @ApiProperty({ type: [CollectionItem] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => CollectionItem) + items: CollectionItem[]; + + @IsDate() + @IsOptional() + updatedAt?: Date; + + @IsString() + @IsOptional() + updatedBy?: string; +} diff --git a/src/modules/workspace/payloads/collection.payload.ts b/src/modules/workspace/payloads/collection.payload.ts index baec2bc5..99c4363f 100644 --- a/src/modules/workspace/payloads/collection.payload.ts +++ b/src/modules/workspace/payloads/collection.payload.ts @@ -125,4 +125,30 @@ export class ImportCollectionDto { }) @IsBoolean() activeSync?: boolean; + + @ApiProperty({ example: "C://users/github" }) + @IsString() + @IsOptional() + localRepositoryPath?: string; + + @ApiProperty({ example: "development" }) + @IsString() + @IsOptional() + primaryBranch?: string; + + @ApiProperty({ example: "feat/onboarding-v2" }) + @IsString() + @IsOptional() + currentBranch?: string; + + @ApiProperty() + @IsOptional() + urlData?: any; +} + +export class SwitchCollectionBranchDto { + @ApiProperty({ example: "feat/onboarding-v2" }) + @IsString() + @IsNotEmpty() + currentBranch: string; } diff --git a/src/modules/workspace/payloads/collectionRequest.payload.ts b/src/modules/workspace/payloads/collectionRequest.payload.ts index ea07fb54..cfa8b58d 100644 --- a/src/modules/workspace/payloads/collectionRequest.payload.ts +++ b/src/modules/workspace/payloads/collectionRequest.payload.ts @@ -16,10 +16,31 @@ import { ApiProperty } from "@nestjs/swagger"; import { BodyModeEnum, ItemTypeEnum, + SourceTypeEnum, } from "@src/modules/common/models/collection.model"; // eslint-disable-next-line @typescript-eslint/no-unused-vars +enum ApiKeyParamTypeEnum { + HEADER = "Header", + Query = "Query Parameter", +} +class Auth { + apiKey?: ApiKey; + bearerToken?: string; + basicAuth?: BasicAuth; +} + +class ApiKey { + authKey?: string; + authValue?: string; + addTo: ApiKeyParamTypeEnum; +} +class BasicAuth { + username?: string; + password?: string; +} + export class CollectionRequestBody { @ApiProperty({ example: "application/json" }) @IsEnum(BodyModeEnum) @@ -118,6 +139,25 @@ export class CollectionRequestMetaData { @ValidateNested({ each: true }) @IsOptional() headers?: Params[]; + + @ApiProperty({ + type: [Auth], + example: { + apiKey: { + authKey: "", + authValue: "", + paramType: "header", + }, + bearerToken: "", + basicToken: { + username: "", + password: "", + }, + }, + }) + @Type(() => Auth) + @IsOptional() + auth?: Auth; } export class CollectionRequestItem { @@ -221,24 +261,45 @@ export class CollectionRequestDto { @ApiProperty({ example: "6538e910aa77d958912371f5" }) @IsString() @IsOptional() - folderId: string; + folderId?: string; + + @ApiProperty({ enum: ["SPEC", "USER"] }) + @IsEnum(SourceTypeEnum) + @IsOptional() + @IsString() + source?: SourceTypeEnum; @ApiProperty() @Type(() => CollectionRequestItem) @ValidateNested({ each: true }) items?: CollectionRequestItem; + + @ApiProperty({ example: "main" }) + @IsString() + @IsOptional() + currentBranch?: string; } export class FolderPayload { @ApiProperty({ example: "pet" }) @IsString() @IsOptional() - name: string; + name?: string; @ApiProperty({ example: "Everything about your Pets" }) @IsString() @IsOptional() - description: string; + description?: string; + + @ApiProperty({ example: SourceTypeEnum.USER }) + @IsEnum(SourceTypeEnum) + @IsOptional() + source?: SourceTypeEnum; + + @ApiProperty({ example: "development" }) + @IsString() + @IsOptional() + currentBranch?: string; } export class FolderDto { @@ -254,6 +315,10 @@ export class FolderDto { @IsOptional() description?: string; + @IsEnum(SourceTypeEnum) + @IsOptional() + source?: SourceTypeEnum; + @IsString() @IsNotEmpty() collectionId: string; @@ -261,6 +326,10 @@ export class FolderDto { @IsString() @IsNotEmpty() workspaceId: string; + + @IsString() + @IsOptional() + currentBranch?: string; } export class DeleteFolderDto { @@ -275,4 +344,15 @@ export class DeleteFolderDto { @IsString() @IsNotEmpty() folderId: string; + + @IsString() + @IsOptional() + currentBranch?: string; +} + +export class BranchChangeDto { + @ApiProperty({ example: "development" }) + @IsString() + @IsNotEmpty() + branchName: string; } diff --git a/src/modules/workspace/payloads/workspace.payload.ts b/src/modules/workspace/payloads/workspace.payload.ts index 0eef6efb..9a5d1e8e 100644 --- a/src/modules/workspace/payloads/workspace.payload.ts +++ b/src/modules/workspace/payloads/workspace.payload.ts @@ -12,21 +12,7 @@ import { } from "class-validator"; import { Type } from "class-transformer"; -export class WorkspaceDto { - @IsOptional() - @IsArray() - users?: string[]; - - @IsDateString() - @IsOptional() - createdAt?: Date; - - @IsMongoId() - @IsOptional() - createdBy?: string; -} - -export class CreateWorkspaceDto extends WorkspaceDto { +export class CreateWorkspaceDto { @ApiProperty({ example: "64f878a0293b1e4415866493" }) @IsMongoId() @IsNotEmpty() @@ -38,9 +24,21 @@ export class CreateWorkspaceDto extends WorkspaceDto { @IsString() @IsNotEmpty() name: string; + + @IsOptional() + @IsArray() + users?: string[]; + + @IsDateString() + @IsOptional() + createdAt?: Date; + + @IsMongoId() + @IsOptional() + createdBy?: string; } -export class UpdateWorkspaceDto extends WorkspaceDto { +export class UpdateWorkspaceDto { @ApiProperty({ example: "workspace 1", }) @@ -54,6 +52,18 @@ export class UpdateWorkspaceDto extends WorkspaceDto { @IsString() @IsOptional() description?: string; + + @IsOptional() + @IsArray() + users?: string[]; + + @IsDateString() + @IsOptional() + createdAt?: Date; + + @IsMongoId() + @IsOptional() + createdBy?: string; } export class UserDto { diff --git a/src/modules/workspace/repositories/branch.repository.ts b/src/modules/workspace/repositories/branch.repository.ts new file mode 100644 index 00000000..cd4783fc --- /dev/null +++ b/src/modules/workspace/repositories/branch.repository.ts @@ -0,0 +1,217 @@ +import { BadRequestException, Inject, Injectable } from "@nestjs/common"; + +import { Db, InsertOneResult, ObjectId, UpdateResult, WithId } from "mongodb"; + +import { Collections } from "@src/modules/common/enum/database.collection.enum"; +import { ContextService } from "@src/modules/common/services/context.service"; +import { Branch } from "@src/modules/common/models/branch.model"; +import { + CollectionItem, + ItemTypeEnum, +} from "@src/modules/common/models/collection.model"; +import { UpdateBranchDto } from "../payloads/branch.payload"; +import { ErrorMessages } from "@src/modules/common/enum/error-messages.enum"; +import { + CollectionRequestDto, + CollectionRequestItem, +} from "../payloads/collectionRequest.payload"; + +@Injectable() +export class BranchRepository { + constructor( + @Inject("DATABASE_CONNECTION") private db: Db, + private readonly contextService: ContextService, + ) {} + + async addBranch(branch: Branch): Promise> { + const response = await this.db + .collection(Collections.BRANCHES) + .insertOne(branch); + return response; + } + async updateBranch(branchId: string, items: CollectionItem[]): Promise { + const defaultParams = { + updatedAt: new Date(), + updatedBy: this.contextService.get("user")._id, + }; + await this.db.collection(Collections.BRANCHES).updateOne( + { + _id: new Object(branchId), + }, + { + $set: { + items, + ...defaultParams, + }, + }, + ); + } + async getBranch(branchId: string): Promise> { + const response = await this.db + .collection(Collections.BRANCHES) + .findOne({ _id: new ObjectId(branchId) }); + return response; + } + + async getBranchByCollection( + collectionId: string, + branchName: string, + ): Promise> { + const collectionObjectId = new ObjectId(collectionId); + const response = await this.db + .collection(Collections.BRANCHES) + .findOne({ collectionId: collectionObjectId, name: branchName }); + return response; + } + + async updateBranchById( + branchId: string, + updateParams: UpdateBranchDto, + ): Promise> { + const updatedBranchParams = { + $set: updateParams, + }; + const responseData = await this.db + .collection(Collections.BRANCHES) + .findOneAndUpdate({ _id: new ObjectId(branchId) }, updatedBranchParams); + return responseData.value; + } + + async addRequestInBranch( + collectionId: string, + branchName: string, + request: CollectionItem, + ): Promise> { + const branch = await this.getBranchByCollection(collectionId, branchName); + const _id = branch._id; + const data = await this.db + .collection(Collections.BRANCHES) + .updateOne( + { _id }, + { + $push: { + items: request, + }, + }, + ); + return data; + } + + async addRequestInBranchFolder( + collectionId: string, + branchName: string, + request: CollectionItem, + folderId: string, + ): Promise> { + const branch = await this.getBranchByCollection(collectionId, branchName); + const _id = branch._id; + const isFolderExists = branch.items.some((item) => { + return item.id === folderId; + }); + if (isFolderExists) { + return await this.db.collection(Collections.BRANCHES).updateOne( + { _id, "items.name": request.name }, + { + $push: { "items.$.items": request.items[0] }, + }, + ); + } else { + throw new BadRequestException(ErrorMessages.Unauthorized); + } + } + + async updateRequestInBranch( + collectionId: string, + branchName: string, + requestId: string, + request: Partial, + ): Promise { + const branch = await this.getBranchByCollection(collectionId, branchName); + const _id = branch._id; + const user = await this.contextService.get("user"); + const defaultParams = { + updatedAt: new Date(), + updatedBy: user._id, + }; + if (request.items.type === ItemTypeEnum.REQUEST) { + request.items = { ...request.items, ...defaultParams }; + await this.db.collection(Collections.BRANCHES).updateOne( + { _id, "items.id": requestId }, + { + $set: { + "items.$": request.items, + updatedAt: new Date(), + updatedBy: user._id, + }, + }, + ); + return { ...request.items, id: requestId }; + } else { + request.items.items = { ...request.items.items, ...defaultParams }; + await this.db.collection(Collections.BRANCHES).updateOne( + { + _id, + "items.id": request.folderId, + "items.items.id": requestId, + }, + { + $set: { + "items.$[i].items.$[j]": request.items.items, + updatedAt: new Date(), + updatedBy: user._id, + }, + }, + { + arrayFilters: [{ "i.id": request.folderId }, { "j.id": requestId }], + }, + ); + return { ...request.items.items, id: requestId }; + } + } + + async deleteRequestInBranch( + collectionId: string, + branchName: string, + requestId: string, + folderId?: string, + ): Promise> { + const branch = await this.getBranchByCollection(collectionId, branchName); + const _id = branch._id; + if (folderId) { + return await this.db.collection(Collections.BRANCHES).updateOne( + { + _id, + }, + { + $pull: { + "items.$[i].items": { + id: requestId, + }, + }, + $set: { + updatedAt: new Date(), + updatedBy: this.contextService.get("user")._id, + }, + }, + { + arrayFilters: [{ "i.id": folderId }], + }, + ); + } else { + return await this.db.collection(Collections.BRANCHES).updateOne( + { _id }, + { + $pull: { + items: { + id: requestId, + }, + }, + $set: { + updatedAt: new Date(), + updatedBy: this.contextService.get("user")._id, + }, + }, + ); + } + } +} diff --git a/src/modules/workspace/repositories/collection.repository.ts b/src/modules/workspace/repositories/collection.repository.ts index d19a1f68..6bb4f4a5 100644 --- a/src/modules/workspace/repositories/collection.repository.ts +++ b/src/modules/workspace/repositories/collection.repository.ts @@ -12,6 +12,7 @@ import { import { Collections } from "@src/modules/common/enum/database.collection.enum"; import { ContextService } from "@src/modules/common/services/context.service"; import { + CollectionBranch, Collection, CollectionItem, ItemTypeEnum, @@ -47,7 +48,7 @@ export class CollectionRepository { } async update( id: string, - updateCollectionDto: UpdateCollectionDto, + updateCollectionDto: Partial, ): Promise { const collectionId = new ObjectId(id); const defaultParams = { @@ -62,6 +63,29 @@ export class CollectionRepository { ); return data; } + + async updateBranchArray( + id: string, + branch: CollectionBranch, + ): Promise { + const collectionId = new ObjectId(id); + const defaultParams = { + updatedAt: new Date(), + updatedBy: this.contextService.get("user")._id, + }; + const data = await this.db.collection(Collections.COLLECTION).updateOne( + { _id: collectionId }, + { + $push: { + branches: branch, + }, + $set: { + ...defaultParams, + }, + }, + ); + return data; + } async delete(id: string): Promise { const _id = new ObjectId(id); const data = await this.db @@ -80,7 +104,7 @@ export class CollectionRepository { async updateCollection( id: string, - payload: Collection, + payload: Partial, ): Promise> { const _id = new ObjectId(id); const data = await this.db @@ -148,7 +172,7 @@ export class CollectionRepository { async updateRequest( collectionId: string, requestId: string, - request: CollectionRequestDto, + request: Partial, ): Promise { const _id = new ObjectId(collectionId); const defaultParams = { @@ -262,7 +286,7 @@ export class CollectionRepository { ); const data = await this.db .collection(Collections.COLLECTION) - .findOne({ _id: activeCollection.id, activeSync: true }); + .findOne({ _id: activeCollection?.id, activeSync: true }); return data; } } diff --git a/src/modules/workspace/repositories/environment.repository.ts b/src/modules/workspace/repositories/environment.repository.ts index fd39f641..4c8ed34d 100644 --- a/src/modules/workspace/repositories/environment.repository.ts +++ b/src/modules/workspace/repositories/environment.repository.ts @@ -49,7 +49,7 @@ export class EnvironmentRepository { async update( id: string, - updateEnvironmentDto: UpdateEnvironmentDto, + updateEnvironmentDto: Partial, ): Promise { const environmentId = new ObjectId(id); const defaultParams = { diff --git a/src/modules/workspace/repositories/workspace.repository.ts b/src/modules/workspace/repositories/workspace.repository.ts index 0f13259f..3bd7f352 100644 --- a/src/modules/workspace/repositories/workspace.repository.ts +++ b/src/modules/workspace/repositories/workspace.repository.ts @@ -74,7 +74,7 @@ export class WorkspaceRepository { async updateWorkspaceById( id: ObjectId, - updatedWorkspace: WorkspaceDtoForIdDocument, + updatedWorkspace: Partial, ): Promise> { const response = await this.db .collection(Collections.WORKSPACE) diff --git a/src/modules/workspace/services/branch.service.ts b/src/modules/workspace/services/branch.service.ts new file mode 100644 index 00000000..97a6fda0 --- /dev/null +++ b/src/modules/workspace/services/branch.service.ts @@ -0,0 +1,57 @@ +import { Injectable } from "@nestjs/common"; +import { InsertOneResult, ObjectId, WithId } from "mongodb"; +import { ContextService } from "@src/modules/common/services/context.service"; +import { createBranchDto } from "../payloads/branch.payload"; +import { Branch } from "@src/modules/common/models/branch.model"; +import { BranchRepository } from "../repositories/branch.repository"; +import { CollectionItem } from "@src/modules/common/models/collection.model"; +import { WorkspaceService } from "./workspace.service"; + +/** + * Branch Service + */ + +@Injectable() +export class BranchService { + constructor( + private readonly contextService: ContextService, + private readonly branchRepository: BranchRepository, + private readonly workspaceService: WorkspaceService, + ) {} + + async createBranch( + createBranchDto: createBranchDto, + ): Promise> { + const user = this.contextService.get("user"); + const newBranch: Branch = { + name: createBranchDto.name, + items: createBranchDto.items, + collectionId: new ObjectId(createBranchDto.collectionId), + createdBy: user._id, + updatedBy: user._id, + createdAt: new Date(), + updatedAt: new Date(), + }; + const branch = await this.branchRepository.addBranch(newBranch); + return branch; + } + + async updateBranch( + workspaceId: string, + branchId: string, + items: CollectionItem[], + ): Promise { + await this.workspaceService.IsWorkspaceAdminOrEditor(workspaceId); + const updatedParams = { + items: items, + updatedAt: new Date(), + updatedBy: this.contextService.get("user")._id, + }; + await this.branchRepository.updateBranchById(branchId, updatedParams); + } + + async getBranch(branchId: string): Promise> { + const branch = await this.branchRepository.getBranch(branchId); + return branch; + } +} diff --git a/src/modules/workspace/services/collection-request.service.ts b/src/modules/workspace/services/collection-request.service.ts index 4643ef07..ae5fda70 100644 --- a/src/modules/workspace/services/collection-request.service.ts +++ b/src/modules/workspace/services/collection-request.service.ts @@ -22,6 +22,9 @@ import { } from "@src/modules/common/models/collection.model"; import { CollectionService } from "./collection.service"; import { WorkspaceService } from "./workspace.service"; +import { BranchRepository } from "../repositories/branch.repository"; +import { UpdateBranchDto } from "../payloads/branch.payload"; +import { Branch } from "@src/modules/common/models/branch.model"; @Injectable() export class CollectionRequestService { constructor( @@ -30,9 +33,10 @@ export class CollectionRequestService { private readonly contextService: ContextService, private readonly collectionService: CollectionService, private readonly workspaceService: WorkspaceService, + private readonly branchRepository: BranchRepository, ) {} - async addFolder(payload: FolderDto): Promise { + async addFolder(payload: Partial): Promise { await this.workspaceService.IsWorkspaceAdminOrEditor(payload.workspaceId); const user = await this.contextService.get("user"); const uuid = uuidv4(); @@ -48,7 +52,7 @@ export class CollectionRequestService { name: payload.name, description: payload.description ?? "", type: ItemTypeEnum.FOLDER, - source: SourceTypeEnum.USER, + source: payload.source ?? SourceTypeEnum.USER, isDeleted: false, items: [], createdBy: user.name, @@ -61,10 +65,38 @@ export class CollectionRequestService { payload.collectionId, collection, ); + if (payload?.currentBranch) { + const branch = await this.branchRepository.getBranchByCollection( + payload.collectionId, + payload.currentBranch, + ); + if (!branch) { + throw new BadRequestException("Branch Not Found"); + } + branch.items.push(updatedFolder); + const updatedBranch: UpdateBranchDto = { + items: branch.items, + updatedAt: new Date(), + updatedBy: this.contextService.get("user")._id, + }; + await this.branchRepository.updateBranchById( + branch._id.toString(), + updatedBranch, + ); + } return updatedFolder; } - async updateFolder(payload: FolderDto): Promise { + async isFolderExist(branch: Branch, id: string): Promise { + for (let i = 0; i < branch.items.length; i++) { + if (branch.items[i].id === id) { + return i; + } + } + throw new BadRequestException("Folder Doesn't Exist"); + } + + async updateFolder(payload: Partial): Promise { await this.workspaceService.IsWorkspaceAdminOrEditor(payload.workspaceId); const user = await this.contextService.get("user"); await this.checkPermission(payload.workspaceId, user._id); @@ -82,6 +114,28 @@ export class CollectionRequestService { payload.collectionId, collection, ); + if (payload?.currentBranch) { + const branch = await this.branchRepository.getBranchByCollection( + payload.collectionId, + payload.currentBranch, + ); + if (!branch) { + throw new BadRequestException("Branch Not Found"); + } + const index = await this.isFolderExist(branch, payload.folderId); + branch.items[index].name = payload.name ?? branch.items[index].name; + branch.items[index].description = + payload.description ?? branch.items[index].description; + const updatedBranch: UpdateBranchDto = { + items: branch.items, + updatedAt: new Date(), + updatedBy: user._id, + }; + await this.branchRepository.updateBranchById( + branch._id.toString(), + updatedBranch, + ); + } return collection.items[index]; } @@ -105,6 +159,28 @@ export class CollectionRequestService { payload.collectionId, collection, ); + if (payload?.currentBranch) { + const branch = await this.branchRepository.getBranchByCollection( + payload.collectionId, + payload.currentBranch, + ); + if (!branch) { + throw new BadRequestException("Branch Not Found"); + } + const updatedBranchItems = branch.items.filter( + (item) => item.id !== payload.folderId, + ); + branch.items = updatedBranchItems; + const updatedBranch: UpdateBranchDto = { + items: branch.items, + updatedAt: new Date(), + updatedBy: user._id, + }; + await this.branchRepository.updateBranchById( + branch._id.toString(), + updatedBranch, + ); + } return data; } @@ -130,7 +206,7 @@ export class CollectionRequestService { } async addRequest( collectionId: string, - request: CollectionRequestDto, + request: Partial, noOfRequests: number, userName: string, folderId?: string, @@ -141,7 +217,7 @@ export class CollectionRequestService { name: request.items.name, type: request.items.type, description: request.items.description, - source: SourceTypeEnum.USER, + source: request.source ?? SourceTypeEnum.USER, isDeleted: false, createdBy: userName, updatedBy: userName, @@ -155,6 +231,13 @@ export class CollectionRequestService { requestObj, noOfRequests, ); + if (request?.currentBranch) { + await this.branchRepository.addRequestInBranch( + collectionId, + request.currentBranch, + requestObj, + ); + } return requestObj; } else { requestObj.items = [ @@ -178,6 +261,14 @@ export class CollectionRequestService { noOfRequests, folderId, ); + if (request?.currentBranch) { + await this.branchRepository.addRequestInBranchFolder( + collectionId, + request.currentBranch, + requestObj, + folderId, + ); + } return requestObj.items[0]; } } @@ -185,27 +276,45 @@ export class CollectionRequestService { async updateRequest( collectionId: string, requestId: string, - request: CollectionRequestDto, + request: Partial, ): Promise { - return await this.collectionReposistory.updateRequest( + const collection = await this.collectionReposistory.updateRequest( collectionId, requestId, request, ); + if (request?.currentBranch) { + await this.branchRepository.updateRequestInBranch( + collectionId, + request.currentBranch, + requestId, + request, + ); + } + return collection; } async deleteRequest( collectionId: string, requestId: string, noOfRequests: number, - folderId?: string, + requestDto: Partial, ): Promise> { - return await this.collectionReposistory.deleteRequest( + const collection = await this.collectionReposistory.deleteRequest( collectionId, requestId, noOfRequests, - folderId, + requestDto?.folderId, ); + if (requestDto.currentBranch) { + await this.branchRepository.deleteRequestInBranch( + collectionId, + requestDto.currentBranch, + requestId, + requestDto.folderId, + ); + } + return collection; } async getNoOfRequest(collectionId: string): Promise { diff --git a/src/modules/workspace/services/collection.service.ts b/src/modules/workspace/services/collection.service.ts index 6138db33..e992f506 100644 --- a/src/modules/workspace/services/collection.service.ts +++ b/src/modules/workspace/services/collection.service.ts @@ -13,22 +13,32 @@ import { UpdateResult, WithId, } from "mongodb"; -import { Collection } from "@src/modules/common/models/collection.model"; +import { + Collection, + CollectionBranch, + ItemTypeEnum, +} from "@src/modules/common/models/collection.model"; import { ContextService } from "@src/modules/common/services/context.service"; import { ErrorMessages } from "@src/modules/common/enum/error-messages.enum"; import { WorkspaceService } from "./workspace.service"; +import { BranchRepository } from "../repositories/branch.repository"; +import { Branch } from "@src/modules/common/models/branch.model"; +import { UpdateBranchDto } from "../payloads/branch.payload"; +import { ConfigService } from "@nestjs/config"; @Injectable() export class CollectionService { constructor( - private readonly collectionReposistory: CollectionRepository, - private readonly workspaceReposistory: WorkspaceRepository, + private readonly collectionRepository: CollectionRepository, + private readonly workspaceRepository: WorkspaceRepository, + private readonly branchRepository: BranchRepository, private readonly contextService: ContextService, private readonly workspaceService: WorkspaceService, + private readonly configService: ConfigService, ) {} async createCollection( - createCollectionDto: CreateCollectionDto, + createCollectionDto: Partial, ): Promise { await this.workspaceService.IsWorkspaceAdminOrEditor( createCollectionDto.workspaceId, @@ -45,24 +55,24 @@ export class CollectionService { createdAt: new Date(), updatedAt: new Date(), }; - const collection = await this.collectionReposistory.addCollection( + const collection = await this.collectionRepository.addCollection( newCollection, ); return collection; } async getCollection(id: string): Promise> { - return await this.collectionReposistory.get(id); + return await this.collectionRepository.get(id); } async getAllCollections(id: string): Promise[]> { const user = await this.contextService.get("user"); await this.checkPermission(id, user._id); - const workspace = await this.workspaceReposistory.get(id); + const workspace = await this.workspaceRepository.get(id); const collections = []; for (let i = 0; i < workspace.collection?.length; i++) { - const collection = await this.collectionReposistory.get( + const collection = await this.collectionRepository.get( workspace.collection[i].id.toString(), ); collections.push(collection); @@ -74,14 +84,27 @@ export class CollectionService { title: string, workspaceId: string, ): Promise> { - return await this.collectionReposistory.getActiveSyncedCollection( + return await this.collectionRepository.getActiveSyncedCollection( title, workspaceId, ); } + async getActiveSyncedBranch( + id: string, + name: string, + ): Promise | null> { + const collection = await this.getCollection(id); + for (const branch of collection.branches) { + if (branch.name === name) { + return await this.branchRepository.getBranch(branch.id); + } + } + return null; + } + async checkPermission(workspaceId: string, userid: ObjectId): Promise { - const workspace = await this.workspaceReposistory.get(workspaceId); + const workspace = await this.workspaceRepository.get(workspaceId); const hasPermission = workspace.users.some((user) => { return user.id.toString() === userid.toString(); }); @@ -91,20 +114,36 @@ export class CollectionService { } async updateCollection( collectionId: string, - updateCollectionDto: UpdateCollectionDto, + updateCollectionDto: Partial, workspaceId: string, ): Promise { await this.workspaceService.IsWorkspaceAdminOrEditor(workspaceId); const user = await this.contextService.get("user"); await this.checkPermission(workspaceId, user._id); - await this.collectionReposistory.get(collectionId); - const data = await this.collectionReposistory.update( + await this.collectionRepository.get(collectionId); + const data = await this.collectionRepository.update( collectionId, updateCollectionDto, ); return data; } + async updateBranchArray( + collectionId: string, + branch: CollectionBranch, + workspaceId: string, + ): Promise { + await this.workspaceService.IsWorkspaceAdminOrEditor(workspaceId); + const user = await this.contextService.get("user"); + await this.checkPermission(workspaceId, user._id); + await this.collectionRepository.get(collectionId); + const data = await this.collectionRepository.updateBranchArray( + collectionId, + branch, + ); + return data; + } + async deleteCollection( id: string, workspaceId: string, @@ -112,16 +151,66 @@ export class CollectionService { await this.workspaceService.IsWorkspaceAdminOrEditor(workspaceId); const user = await this.contextService.get("user"); await this.checkPermission(workspaceId, user._id); - const data = await this.collectionReposistory.delete(id); + const data = await this.collectionRepository.delete(id); return data; } async importCollection(collection: Collection): Promise { - return await this.collectionReposistory.addCollection(collection); + return await this.collectionRepository.addCollection(collection); } async updateImportedCollection( id: string, - collection: Collection, + collection: Partial, ): Promise> { - return await this.collectionReposistory.updateCollection(id, collection); + return await this.collectionRepository.updateCollection(id, collection); + } + + async getBranchData( + collectionId: string, + branchName: string, + ): Promise | void> { + const branch = await this.branchRepository.getBranchByCollection( + collectionId, + branchName, + ); + for (let index = 0; index < branch?.items.length; index++) { + if (branch?.items[index].type === ItemTypeEnum.FOLDER) { + for (let flag = 0; flag < branch.items[index].items.length; flag++) { + const deletedDate = new Date( + branch.items[index].items[flag].updatedAt, + ); + const currentDate = new Date(); + const diff = currentDate.getTime() - deletedDate.getTime(); + const differenceInDays = + diff / this.configService.get("app.timeToDaysDivisor"); + if ( + branch.items[index].items[flag].isDeleted && + differenceInDays > + this.configService.get("app.deletedAPILimitInDays") + ) { + branch.items[index].items.splice(flag, 1); + } + } + } else { + const deletedDate = new Date(branch.items[index].updatedAt); + const currentDate = new Date(); + const diff = currentDate.getTime() - deletedDate.getTime(); + if ( + branch.items[index].isDeleted && + diff > this.configService.get("app.deletedAPILimitInDays") + ) { + branch.items.splice(index, 1); + } + } + } + const updatedBranch: UpdateBranchDto = { + items: branch.items, + updatedAt: new Date(), + updatedBy: this.contextService.get("user")._id, + }; + await this.branchRepository.updateBranchById( + branch._id.toJSON(), + updatedBranch, + ); + return branch; } } diff --git a/src/modules/workspace/services/environment.service.ts b/src/modules/workspace/services/environment.service.ts index 3bbe95d8..a17bf666 100644 --- a/src/modules/workspace/services/environment.service.ts +++ b/src/modules/workspace/services/environment.service.ts @@ -136,7 +136,7 @@ export class EnvironmentService { */ async updateEnvironment( environmentId: string, - updateEnvironmentDto: UpdateEnvironmentDto, + updateEnvironmentDto: Partial, workspaceId: string, ): Promise { await this.isWorkspaceAdminorEditor(workspaceId); diff --git a/src/modules/workspace/services/workspace.service.ts b/src/modules/workspace/services/workspace.service.ts index f8b327e2..8c88a5dc 100644 --- a/src/modules/workspace/services/workspace.service.ts +++ b/src/modules/workspace/services/workspace.service.ts @@ -22,7 +22,6 @@ import { TeamRole, WorkspaceRole } from "@src/modules/common/enum/roles.enum"; import { TeamRepository } from "@src/modules/identity/repositories/team.repository"; import { CollectionDto } from "@src/modules/common/models/collection.model"; -import { Logger } from "nestjs-pino"; import { UserRepository } from "@src/modules/identity/repositories/user.repository"; import { DefaultEnvironment, @@ -57,7 +56,6 @@ export class WorkspaceService { private readonly userRepository: UserRepository, private readonly teamService: TeamService, private readonly configService: ConfigService, - private readonly logger: Logger, ) {} async get(id: string): Promise> { @@ -302,7 +300,7 @@ export class WorkspaceService { */ async update( id: string, - updates: UpdateWorkspaceDto, + updates: Partial, ): Promise> { const workspace = await this.IsWorkspaceAdminOrEditor(id); const data = await this.workspaceRepository.update(id, updates); diff --git a/src/modules/workspace/workspace.module.ts b/src/modules/workspace/workspace.module.ts index 5c9bf4ed..f4cde875 100644 --- a/src/modules/workspace/workspace.module.ts +++ b/src/modules/workspace/workspace.module.ts @@ -19,6 +19,8 @@ import { DemoteAdminHandler } from "./handlers/demoteAdmin.handlers"; import { FeatureController } from "./controllers/feature.controller"; import { FeatureService } from "./services/feature.service"; import { FeatureRepository } from "./repositories/feature.repository"; +import { BranchService } from "./services/branch.service"; +import { BranchRepository } from "./repositories/branch.repository"; @Module({ imports: [IdentityModule], providers: [ @@ -37,6 +39,8 @@ import { FeatureRepository } from "./repositories/feature.repository"; EnvironmentRepository, FeatureService, FeatureRepository, + BranchService, + BranchRepository, ], exports: [ CollectionService, @@ -46,6 +50,8 @@ import { FeatureRepository } from "./repositories/feature.repository"; EnvironmentRepository, FeatureService, FeatureRepository, + BranchService, + BranchRepository, ], controllers: [ WorkSpaceController, diff --git a/tsconfig.json b/tsconfig.json index 74e5aff1..00bb8253 100755 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,13 +1,13 @@ { "compilerOptions": { - "module": "commonjs", + "module": "nodenext", "declaration": true, "removeComments": true, "noLib": false, "allowSyntheticDefaultImports": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, - "target": "es2017", + "target": "ESNext", "sourceMap": true, "outDir": "./dist", "baseUrl": "./", @@ -29,7 +29,5 @@ }, "resolveJsonModule": true }, - "exclude": [ - "node_modules" - ] -} + "exclude": ["node_modules", "./dist/**/*"] +} \ No newline at end of file