From c6ded15b7c04824e1b518d07795b459bba430ab3 Mon Sep 17 00:00:00 2001 From: "github-classroom[bot]" <66690702+github-classroom[bot]@users.noreply.github.com> Date: Wed, 24 Apr 2024 16:49:38 +0000 Subject: [PATCH 01/37] Setting up GitHub Classroom Feedback From 5cf6be8bbefa1c5f57539f6a8e327c54bd0a6b11 Mon Sep 17 00:00:00 2001 From: carusi99 Date: Wed, 24 Apr 2024 14:59:31 -0500 Subject: [PATCH 02/37] create files --- package-lock.json | 198 ++++++++++++++++++++++++++-------------------- package.json | 5 +- 2 files changed, 117 insertions(+), 86 deletions(-) diff --git a/package-lock.json b/package-lock.json index d9e8ba6..2fdbd20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,9 @@ "license": "ISC", "dependencies": { "dotenv": "^16.3.1", - "express": "^4.18.2", - "pg": "^8.11.3" + "express": "^4.19.2", + "pg": "^8.11.5", + "zod": "^3.23.4" }, "devDependencies": { "@types/express": "^4.17.21", @@ -270,12 +271,12 @@ } }, "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "dependencies": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -283,7 +284,7 @@ "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", - "raw-body": "2.5.1", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -314,14 +315,6 @@ "node": ">=8" } }, - "node_modules/buffer-writer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", - "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==", - "engines": { - "node": ">=4" - } - }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -331,13 +324,18 @@ } }, "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -396,9 +394,9 @@ } }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "engines": { "node": ">= 0.6" } @@ -423,16 +421,19 @@ } }, "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/depd": { @@ -485,6 +486,25 @@ "node": ">= 0.8" } }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -499,16 +519,16 @@ } }, "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -607,15 +627,19 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dependencies": { + "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -653,20 +677,20 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dependencies": { - "get-intrinsic": "^1.2.2" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", "engines": { "node": ">= 0.4" }, @@ -686,9 +710,9 @@ } }, "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dependencies": { "function-bind": "^1.1.2" }, @@ -977,11 +1001,6 @@ "node": ">= 0.8" } }, - "node_modules/packet-reader": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", - "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" - }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -996,15 +1015,13 @@ "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, "node_modules/pg": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz", - "integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==", + "version": "8.11.5", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.5.tgz", + "integrity": "sha512-jqgNHSKL5cbDjFlHyYsCXmQDrfIX/3RsNwYqpd4N0Kt8niLuNoRNH+aazv6cOd43gPh9Y4DjQCtb+X0MH0Hvnw==", "dependencies": { - "buffer-writer": "2.0.0", - "packet-reader": "1.0.0", - "pg-connection-string": "^2.6.2", - "pg-pool": "^3.6.1", - "pg-protocol": "^1.6.0", + "pg-connection-string": "^2.6.4", + "pg-pool": "^3.6.2", + "pg-protocol": "^1.6.1", "pg-types": "^2.1.0", "pgpass": "1.x" }, @@ -1030,9 +1047,9 @@ "optional": true }, "node_modules/pg-connection-string": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", - "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==" + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.4.tgz", + "integrity": "sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==" }, "node_modules/pg-int8": { "version": "1.0.1", @@ -1052,17 +1069,17 @@ } }, "node_modules/pg-pool": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz", - "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.2.tgz", + "integrity": "sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==", "peerDependencies": { "pg": ">=8.0" } }, "node_modules/pg-protocol": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz", - "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==" + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.1.tgz", + "integrity": "sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==" }, "node_modules/pg-types": { "version": "4.0.1", @@ -1238,9 +1255,9 @@ } }, "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -1345,15 +1362,16 @@ } }, "node_modules/set-function-length": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", - "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dependencies": { - "define-data-property": "^1.1.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.2", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -1365,13 +1383,17 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1581,6 +1603,14 @@ "engines": { "node": ">=6" } + }, + "node_modules/zod": { + "version": "3.23.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.4.tgz", + "integrity": "sha512-/AtWOKbBgjzEYYQRNfoGKHObgfAZag6qUJX1VbHo2PRBgS+wfWagEY2mizjfyAPcGesrJOcx/wcl0L9WnVrHFw==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index de985c1..2a9a464 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,9 @@ }, "dependencies": { "dotenv": "^16.3.1", - "express": "^4.18.2", - "pg": "^8.11.3" + "express": "^4.19.2", + "pg": "^8.11.5", + "zod": "^3.23.4" }, "devDependencies": { "@types/express": "^4.17.21", From 40081f052e5c5bfd09e77d02d398ade6b013646e Mon Sep 17 00:00:00 2001 From: carusi99 Date: Wed, 24 Apr 2024 15:48:59 -0500 Subject: [PATCH 03/37] folders and files --- src/app.ts | 0 src/data/likes.data.ts | 0 src/data/posts.data.ts | 0 src/data/users.data.ts | 0 src/middlewares/error.ts | 0 src/routers/likes.routers.ts | 0 src/routers/posts.routers.ts | 0 src/routers/users.routers.ts | 0 src/services/likes.service.ts | 0 src/services/posts.service.ts | 0 src/services/users.service.ts | 0 src/tests/likes.test.ts | 0 src/tests/posts.test.ts | 0 src/tests/users.test.ts | 0 14 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/app.ts create mode 100644 src/data/likes.data.ts create mode 100644 src/data/posts.data.ts create mode 100644 src/data/users.data.ts create mode 100644 src/middlewares/error.ts create mode 100644 src/routers/likes.routers.ts create mode 100644 src/routers/posts.routers.ts create mode 100644 src/routers/users.routers.ts create mode 100644 src/services/likes.service.ts create mode 100644 src/services/posts.service.ts create mode 100644 src/services/users.service.ts create mode 100644 src/tests/likes.test.ts create mode 100644 src/tests/posts.test.ts create mode 100644 src/tests/users.test.ts diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/data/likes.data.ts b/src/data/likes.data.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/data/posts.data.ts b/src/data/posts.data.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/data/users.data.ts b/src/data/users.data.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/middlewares/error.ts b/src/middlewares/error.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/routers/likes.routers.ts b/src/routers/likes.routers.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/routers/posts.routers.ts b/src/routers/posts.routers.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/routers/users.routers.ts b/src/routers/users.routers.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/services/likes.service.ts b/src/services/likes.service.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/services/posts.service.ts b/src/services/posts.service.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/services/users.service.ts b/src/services/users.service.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/tests/likes.test.ts b/src/tests/likes.test.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/tests/posts.test.ts b/src/tests/posts.test.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/tests/users.test.ts b/src/tests/users.test.ts new file mode 100644 index 0000000..e69de29 From a1e87f25f489f94a79c18d5a31d51803449cfb3c Mon Sep 17 00:00:00 2001 From: carusi99 Date: Thu, 25 Apr 2024 08:03:10 -0500 Subject: [PATCH 04/37] migration implementation --- src/data/auth-data.ts | 0 src/db/index.ts | 22 +++++++++++++++++++ src/db/scripts/dbCreate.ts | 21 +++++++++++++++++++ src/db/scripts/dbDrop.ts | 37 ++++++++++++++++++++++++++++++++ src/db/scripts/dbMigrate.ts | 42 +++++++++++++++++++++++++++++++++++++ src/db/scripts/dbSeed.ts | 23 ++++++++++++++++++++ 6 files changed, 145 insertions(+) create mode 100644 src/data/auth-data.ts create mode 100644 src/db/index.ts create mode 100644 src/db/scripts/dbCreate.ts create mode 100644 src/db/scripts/dbDrop.ts create mode 100644 src/db/scripts/dbMigrate.ts create mode 100644 src/db/scripts/dbSeed.ts diff --git a/src/data/auth-data.ts b/src/data/auth-data.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/db/index.ts b/src/db/index.ts new file mode 100644 index 0000000..0c3daac --- /dev/null +++ b/src/db/index.ts @@ -0,0 +1,22 @@ +import "dotenv/config"; +import { Client, Pool } from "pg"; + +export const pool = new Pool({ + host: process.env["PGHOST"], + port: Number(process.env["PGPORT"]), + database: process.env["PGDATABASE"], + user: process.env["PGUSER"], + password: process.env["PGPASSWORD"], +}); + +export const query = (text: string, params?: (string | number | boolean)[]) => { + return pool.query(text, params); +}; + +export const adminClient = new Client({ + host: process.env["PGHOST"], + port: Number(process.env["PGPORT"]), + database: process.env["PGUSER"], + user: process.env["PGUSER"], + password: process.env["PGPASSWORD"], +}); diff --git a/src/db/scripts/dbCreate.ts b/src/db/scripts/dbCreate.ts new file mode 100644 index 0000000..03e379f --- /dev/null +++ b/src/db/scripts/dbCreate.ts @@ -0,0 +1,21 @@ +import { configDotenv } from "dotenv"; +import { adminClient } from ".."; + +if (process.env["NODE_ENV"] === "test") { + configDotenv({ path: ".env.test" }); +} else { + configDotenv(); +} + +const dbName = process.env["PGDATABASE"]; + +adminClient.connect(); + +adminClient.query(`CREATE DATABASE "${dbName}"`, (err) => { + if (err) { + console.error("Error al crear la base de datos", err.stack); + } else { + console.log(`Base de datos "${dbName}" creada exitosamente`); + } + adminClient.end(); +}); diff --git a/src/db/scripts/dbDrop.ts b/src/db/scripts/dbDrop.ts new file mode 100644 index 0000000..3863390 --- /dev/null +++ b/src/db/scripts/dbDrop.ts @@ -0,0 +1,37 @@ +import { configDotenv } from "dotenv"; +import { adminClient } from ".."; +import fs from "node:fs"; +import path from "node:path"; + +if (process.env["NODE_ENV"] === "test") { + configDotenv({ path: ".env.test" }); +} else { + configDotenv(); +} + +const migrationsFileName = + process.env["NODE_ENV"] === "test" + ? "migrations.test.json" + : "migrations.json"; + +const dbName = process.env["PGDATABASE"]; +adminClient.connect(); + +adminClient.query(`DROP DATABASE IF EXISTS "${dbName}"`, (err) => { + if (err) { + console.error("Error al eliminar la base de datos", err.stack); + } else { + console.log(`Base de datos "${dbName}" eliminada exitosamente`); + try { + fs.unlinkSync( + path.join(__dirname, "..", "migrations", migrationsFileName) + ); + } catch { + console.log( + "No se pudo eliminar el archivo de migraciones", + migrationsFileName + ); + } + } + adminClient.end(); +}); diff --git a/src/db/scripts/dbMigrate.ts b/src/db/scripts/dbMigrate.ts new file mode 100644 index 0000000..9f76104 --- /dev/null +++ b/src/db/scripts/dbMigrate.ts @@ -0,0 +1,42 @@ +import { configDotenv } from "dotenv"; +import path from "node:path"; +import fs from "node:fs"; +import { query, pool } from ".."; +import { JSONStorage, Umzug } from "umzug"; + +if (process.env["NODE_ENV"] === "test") { + configDotenv({ path: ".env.test" }); +} else { + configDotenv(); +} + +const migrationsFileName = + process.env["NODE_ENV"] === "test" + ? "migrations.test.json" + : "migrations.json"; + +const migrator = new Umzug({ + migrations: { glob: path.join(__dirname, "..", "migrations", "*.ts") }, + context: { query }, + storage: new JSONStorage({ + path: path.join(__dirname, "..", "migrations", migrationsFileName), + }), + logger: console, + create: { + folder: path.join(__dirname, "..", "migrations"), + template: (filepath) => [ + [ + filepath, + fs + .readFileSync( + path.join(__dirname, "..", "template/migration-template.ts") + ) + .toString(), + ], + ], + }, +}); + +export type Migration = typeof migrator._types.migration; + +migrator.runAsCLI().then(() => pool.end()); diff --git a/src/db/scripts/dbSeed.ts b/src/db/scripts/dbSeed.ts new file mode 100644 index 0000000..507dab6 --- /dev/null +++ b/src/db/scripts/dbSeed.ts @@ -0,0 +1,23 @@ +import { configDotenv } from "dotenv"; +import { query, pool } from ".."; + +if (process.env["NODE_ENV"] === "test") { + configDotenv({ path: ".env.test" }); +} else { + configDotenv(); +} + +query(`INSERT INTO products (name, price, category) + SELECT + 'Product ' || s.id, -- Genera un nombre de producto concatenando una cadena con el ID + ROUND((RANDOM() * 1000)) * 100, -- Genera un precio aleatorio entre 0 y 10000 + CASE + WHEN s.id % 3 = 0 THEN 'Electronic' + WHEN s.id % 3 = 1 THEN 'Clothing' + ELSE 'Food' + END -- Asigna categorías de manera aleatoria + FROM generate_series(1, 200) AS s(id); + `).then(() => { + console.log("Products inserted"); + pool.end(); +}); From 2a75ef22dd1f73a08694423545f661de02b66587 Mon Sep 17 00:00:00 2001 From: carusi99 Date: Thu, 25 Apr 2024 09:35:09 -0500 Subject: [PATCH 05/37] create database --- package-lock.json | 421 ++++++++++++++++++++++++++++++++++-- package.json | 7 +- src/db/scripts/dbCreate.ts | 8 +- src/db/scripts/dbDrop.ts | 26 +-- src/db/scripts/dbMigrate.ts | 19 +- src/db/scripts/dbSeed.ts | 23 -- 6 files changed, 420 insertions(+), 84 deletions(-) delete mode 100644 src/db/scripts/dbSeed.ts diff --git a/package-lock.json b/package-lock.json index 2fdbd20..094b7c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "dotenv": "^16.3.1", "express": "^4.19.2", "pg": "^8.11.5", + "umzug": "^3.8.0", "zod": "^3.23.4" }, "devDependencies": { @@ -60,6 +61,109 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rushstack/node-core-library": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-4.1.0.tgz", + "integrity": "sha512-qz4JFBZJCf1YN5cAXa1dP6Mki/HrsQxc/oYGAGx29dF2cwF2YMxHoly0FBhMw3IEnxo5fMj0boVfoHVBkpkx/w==", + "dependencies": { + "fs-extra": "~7.0.1", + "import-lazy": "~4.0.0", + "jju": "~1.4.0", + "resolve": "~1.22.1", + "semver": "~7.5.4", + "z-schema": "~5.0.2" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@rushstack/terminal": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.10.1.tgz", + "integrity": "sha512-C6Vi/m/84IYJTkfzmXr1+W8Wi3MmBjVF/q3za91Gb3VYjKbpALHVxY6FgH625AnDe5Z0Kh4MHKWA3Z7bqgAezA==", + "dependencies": { + "@rushstack/node-core-library": "4.1.0", + "supports-color": "~8.1.1" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@rushstack/terminal/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@rushstack/terminal/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/@rushstack/ts-command-line": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.19.2.tgz", + "integrity": "sha512-cqmXXmBEBlzo9WtyUrHtF9e6kl0LvBY7aTSVX4jfnBfXWZQWnPq9JTFPlQZ+L/ZwjZ4HrNwQsOVvhe9oOucZkw==", + "dependencies": { + "@rushstack/terminal": "0.10.1", + "@types/argparse": "1.0.38", + "argparse": "~1.0.9", + "string-argv": "~0.3.1" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -84,6 +188,11 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true }, + "node_modules/@types/argparse": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz", + "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==" + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -143,7 +252,7 @@ "version": "20.11.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.5.tgz", "integrity": "sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==", - "dev": true, + "devOptional": true, "dependencies": { "undici-types": "~5.26.4" } @@ -250,6 +359,14 @@ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", "dev": true }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -307,7 +424,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, "dependencies": { "fill-range": "^7.0.1" }, @@ -368,6 +484,15 @@ "fsevents": "~2.3.2" } }, + "node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "optional": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -478,6 +603,17 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, "node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -559,11 +695,33 @@ "node": ">= 0.10.0" } }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -604,6 +762,19 @@ "node": ">= 0.6" } }, + "node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -648,7 +819,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -667,6 +837,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -752,6 +927,14 @@ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", "dev": true }, + "node_modules/import-lazy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", + "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", + "engines": { + "node": ">=8" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -777,11 +960,21 @@ "node": ">=8" } }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -790,7 +983,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -802,16 +994,37 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "engines": { "node": ">=0.12.0" } }, + "node_modules/jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==" + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -838,6 +1051,14 @@ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "engines": { + "node": ">= 8" + } + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -846,6 +1067,18 @@ "node": ">= 0.6" } }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -1009,6 +1242,11 @@ "node": ">= 0.8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, "node_modules/path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", @@ -1161,7 +1399,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "engines": { "node": ">=8.6" }, @@ -1169,6 +1406,14 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pony-cause": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/pony-cause/-/pony-cause-2.1.11.tgz", + "integrity": "sha512-M7LhCsdNbNgiLYiP4WjsfLUuFmCfnjdF6jKe2R9NKl4WFN+HZPGHJZ9lnLP7f9ZnKe3U9nuWD0szirmj+migUg==", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/postgres-array": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz", @@ -1246,6 +1491,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -1280,6 +1544,53 @@ "node": ">=8.10.0" } }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1308,7 +1619,6 @@ "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -1419,6 +1729,11 @@ "node": ">= 10.x" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -1427,6 +1742,14 @@ "node": ">= 0.8" } }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -1439,11 +1762,21 @@ "node": ">=4" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "dependencies": { "is-number": "^7.0.0" }, @@ -1514,6 +1847,17 @@ } } }, + "node_modules/type-fest": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.17.0.tgz", + "integrity": "sha512-9flrz1zkfLRH3jO3bLflmTxryzKMxVa7841VeMgBaNQGY6vH4RCcpN/sQLB7mQQYh1GZ5utT2deypMuCy4yicw==", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -1539,6 +1883,21 @@ "node": ">=14.17" } }, + "node_modules/umzug": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/umzug/-/umzug-3.8.0.tgz", + "integrity": "sha512-FRBvdZxllW3eUzsqG3CIfgOVChUONrKNZozNOJfvmcfBn5pMKcJjICuMMEsDLHYa/aqd7a2NtXfYEG86XHe1lQ==", + "dependencies": { + "@rushstack/ts-command-line": "^4.12.2", + "emittery": "^0.13.0", + "fast-glob": "^3.3.2", + "pony-cause": "^2.1.4", + "type-fest": "^4.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -1549,7 +1908,15 @@ "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "devOptional": true + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "engines": { + "node": ">= 4.0.0" + } }, "node_modules/unpipe": { "version": "1.0.0", @@ -1573,6 +1940,14 @@ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "dev": true }, + "node_modules/validator": { + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -1592,8 +1967,7 @@ "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yn": { "version": "3.1.1", @@ -1604,6 +1978,25 @@ "node": ">=6" } }, + "node_modules/z-schema": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", + "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + }, + "bin": { + "z-schema": "bin/z-schema" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "commander": "^9.4.1" + } + }, "node_modules/zod": { "version": "3.23.4", "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.4.tgz", diff --git a/package.json b/package.json index 2a9a464..ee1490e 100644 --- a/package.json +++ b/package.json @@ -7,12 +7,17 @@ "dev": "nodemon src/index.ts", "build": "tsc", "start": "node build/index.js", - "test": "echo \"Error: no test specified\" && exit 1" + "test": "echo \"Error: no test specified\" && exit 1", + "db:migrate": "ts-node src/db/scripts/dbMigrate.ts", + "db:create": "ts-node src/db/scripts/dbCreate.ts", + "db:drop": "ts-node src/db/scripts/dbDrop.ts", + "db:reset": "npm run db:drop && npm run db:create && npm run db:migrate up" }, "dependencies": { "dotenv": "^16.3.1", "express": "^4.19.2", "pg": "^8.11.5", + "umzug": "^3.8.0", "zod": "^3.23.4" }, "devDependencies": { diff --git a/src/db/scripts/dbCreate.ts b/src/db/scripts/dbCreate.ts index 03e379f..471f2ed 100644 --- a/src/db/scripts/dbCreate.ts +++ b/src/db/scripts/dbCreate.ts @@ -1,12 +1,6 @@ -import { configDotenv } from "dotenv"; +import "dotenv/config"; import { adminClient } from ".."; -if (process.env["NODE_ENV"] === "test") { - configDotenv({ path: ".env.test" }); -} else { - configDotenv(); -} - const dbName = process.env["PGDATABASE"]; adminClient.connect(); diff --git a/src/db/scripts/dbDrop.ts b/src/db/scripts/dbDrop.ts index 3863390..bc326ea 100644 --- a/src/db/scripts/dbDrop.ts +++ b/src/db/scripts/dbDrop.ts @@ -1,20 +1,8 @@ -import { configDotenv } from "dotenv"; +import "dotenv/config"; import { adminClient } from ".."; -import fs from "node:fs"; -import path from "node:path"; - -if (process.env["NODE_ENV"] === "test") { - configDotenv({ path: ".env.test" }); -} else { - configDotenv(); -} - -const migrationsFileName = - process.env["NODE_ENV"] === "test" - ? "migrations.test.json" - : "migrations.json"; const dbName = process.env["PGDATABASE"]; + adminClient.connect(); adminClient.query(`DROP DATABASE IF EXISTS "${dbName}"`, (err) => { @@ -22,16 +10,6 @@ adminClient.query(`DROP DATABASE IF EXISTS "${dbName}"`, (err) => { console.error("Error al eliminar la base de datos", err.stack); } else { console.log(`Base de datos "${dbName}" eliminada exitosamente`); - try { - fs.unlinkSync( - path.join(__dirname, "..", "migrations", migrationsFileName) - ); - } catch { - console.log( - "No se pudo eliminar el archivo de migraciones", - migrationsFileName - ); - } } adminClient.end(); }); diff --git a/src/db/scripts/dbMigrate.ts b/src/db/scripts/dbMigrate.ts index 9f76104..3a52ef8 100644 --- a/src/db/scripts/dbMigrate.ts +++ b/src/db/scripts/dbMigrate.ts @@ -1,25 +1,14 @@ -import { configDotenv } from "dotenv"; +import "dotenv/config"; import path from "node:path"; import fs from "node:fs"; -import { query, pool } from ".."; +import { query } from ".."; import { JSONStorage, Umzug } from "umzug"; -if (process.env["NODE_ENV"] === "test") { - configDotenv({ path: ".env.test" }); -} else { - configDotenv(); -} - -const migrationsFileName = - process.env["NODE_ENV"] === "test" - ? "migrations.test.json" - : "migrations.json"; - const migrator = new Umzug({ migrations: { glob: path.join(__dirname, "..", "migrations", "*.ts") }, context: { query }, storage: new JSONStorage({ - path: path.join(__dirname, "..", "migrations", migrationsFileName), + path: path.join(__dirname, "..", "migrations", "migrations.json"), }), logger: console, create: { @@ -39,4 +28,4 @@ const migrator = new Umzug({ export type Migration = typeof migrator._types.migration; -migrator.runAsCLI().then(() => pool.end()); +migrator.runAsCLI(); diff --git a/src/db/scripts/dbSeed.ts b/src/db/scripts/dbSeed.ts deleted file mode 100644 index 507dab6..0000000 --- a/src/db/scripts/dbSeed.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { configDotenv } from "dotenv"; -import { query, pool } from ".."; - -if (process.env["NODE_ENV"] === "test") { - configDotenv({ path: ".env.test" }); -} else { - configDotenv(); -} - -query(`INSERT INTO products (name, price, category) - SELECT - 'Product ' || s.id, -- Genera un nombre de producto concatenando una cadena con el ID - ROUND((RANDOM() * 1000)) * 100, -- Genera un precio aleatorio entre 0 y 10000 - CASE - WHEN s.id % 3 = 0 THEN 'Electronic' - WHEN s.id % 3 = 1 THEN 'Clothing' - ELSE 'Food' - END -- Asigna categorías de manera aleatoria - FROM generate_series(1, 200) AS s(id); - `).then(() => { - console.log("Products inserted"); - pool.end(); -}); From 2761da860488146544f7a21658d917fb6e2a7287 Mon Sep 17 00:00:00 2001 From: carusi99 Date: Thu, 25 Apr 2024 11:59:10 -0500 Subject: [PATCH 06/37] create table users migration --- .../migrations/2024.04.25T16.33.34.users.ts | 20 +++++++++++++++++++ .../migrations/2024.04.25T16.40.52.posts.ts | 8 ++++++++ .../migrations/2024.04.25T16.44.31.likes.ts | 8 ++++++++ src/db/template/migration-template.ts | 8 ++++++++ 4 files changed, 44 insertions(+) create mode 100644 src/db/migrations/2024.04.25T16.33.34.users.ts create mode 100644 src/db/migrations/2024.04.25T16.40.52.posts.ts create mode 100644 src/db/migrations/2024.04.25T16.44.31.likes.ts create mode 100644 src/db/template/migration-template.ts diff --git a/src/db/migrations/2024.04.25T16.33.34.users.ts b/src/db/migrations/2024.04.25T16.33.34.users.ts new file mode 100644 index 0000000..29c5313 --- /dev/null +++ b/src/db/migrations/2024.04.25T16.33.34.users.ts @@ -0,0 +1,20 @@ +import { Migration } from "../scripts/dbMigrate"; + +export const up: Migration = async (params) => { + return params.context.query(`CREATE TABLE Users ( + id SERIAL PRIMARY KEY, + username VARCHAR(255) UNIQUE NOT NULL, + password VARCHAR(255) NOT NULL, + email VARCHAR(255) UNIQUE, + firstName VARCHAR(255), + lastName VARCHAR(255), + role VARCHAR(20) NOT NULL DEFAULT 'user', + createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updatedAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); +`); +}; + +export const down: Migration = async (params) => { + return params.context.query(`DROP TABLE products;`); +}; diff --git a/src/db/migrations/2024.04.25T16.40.52.posts.ts b/src/db/migrations/2024.04.25T16.40.52.posts.ts new file mode 100644 index 0000000..9d708bd --- /dev/null +++ b/src/db/migrations/2024.04.25T16.40.52.posts.ts @@ -0,0 +1,8 @@ +import { Migration } from "../scripts/dbMigrate"; + +export const up: Migration = async (params) => { + params.context.query(`RAISE EXCEPTION 'up migration not implemented'`); +}; +export const down: Migration = async (params) => { + params.context.query(`RAISE EXCEPTION 'down migration not implemented'`); +}; diff --git a/src/db/migrations/2024.04.25T16.44.31.likes.ts b/src/db/migrations/2024.04.25T16.44.31.likes.ts new file mode 100644 index 0000000..9d708bd --- /dev/null +++ b/src/db/migrations/2024.04.25T16.44.31.likes.ts @@ -0,0 +1,8 @@ +import { Migration } from "../scripts/dbMigrate"; + +export const up: Migration = async (params) => { + params.context.query(`RAISE EXCEPTION 'up migration not implemented'`); +}; +export const down: Migration = async (params) => { + params.context.query(`RAISE EXCEPTION 'down migration not implemented'`); +}; diff --git a/src/db/template/migration-template.ts b/src/db/template/migration-template.ts new file mode 100644 index 0000000..9d708bd --- /dev/null +++ b/src/db/template/migration-template.ts @@ -0,0 +1,8 @@ +import { Migration } from "../scripts/dbMigrate"; + +export const up: Migration = async (params) => { + params.context.query(`RAISE EXCEPTION 'up migration not implemented'`); +}; +export const down: Migration = async (params) => { + params.context.query(`RAISE EXCEPTION 'down migration not implemented'`); +}; From bf14bf6af0849329f5810bda0e3dbaaa55f532a6 Mon Sep 17 00:00:00 2001 From: carusi99 Date: Thu, 25 Apr 2024 12:41:34 -0500 Subject: [PATCH 07/37] ompleted migrations --- src/db/migrations/2024.04.25T16.33.34.users.ts | 2 +- src/db/migrations/2024.04.25T16.40.52.posts.ts | 13 +++++++++++-- src/db/migrations/2024.04.25T16.44.31.likes.ts | 13 +++++++++++-- src/db/migrations/migrations.json | 5 +++++ 4 files changed, 28 insertions(+), 5 deletions(-) create mode 100644 src/db/migrations/migrations.json diff --git a/src/db/migrations/2024.04.25T16.33.34.users.ts b/src/db/migrations/2024.04.25T16.33.34.users.ts index 29c5313..0727130 100644 --- a/src/db/migrations/2024.04.25T16.33.34.users.ts +++ b/src/db/migrations/2024.04.25T16.33.34.users.ts @@ -16,5 +16,5 @@ export const up: Migration = async (params) => { }; export const down: Migration = async (params) => { - return params.context.query(`DROP TABLE products;`); + return params.context.query(`DROP TABLE users;`); }; diff --git a/src/db/migrations/2024.04.25T16.40.52.posts.ts b/src/db/migrations/2024.04.25T16.40.52.posts.ts index 9d708bd..db306db 100644 --- a/src/db/migrations/2024.04.25T16.40.52.posts.ts +++ b/src/db/migrations/2024.04.25T16.40.52.posts.ts @@ -1,8 +1,17 @@ import { Migration } from "../scripts/dbMigrate"; export const up: Migration = async (params) => { - params.context.query(`RAISE EXCEPTION 'up migration not implemented'`); + return params.context.query(`CREATE TABLE Posts ( + id SERIAL PRIMARY KEY, + userId INTEGER NOT NULL, + content TEXT NOT NULL, + createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updatedAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (userId) REFERENCES Users(id) +); +`); }; + export const down: Migration = async (params) => { - params.context.query(`RAISE EXCEPTION 'down migration not implemented'`); + return params.context.query(`DROP TABLE posts;`); }; diff --git a/src/db/migrations/2024.04.25T16.44.31.likes.ts b/src/db/migrations/2024.04.25T16.44.31.likes.ts index 9d708bd..2db1537 100644 --- a/src/db/migrations/2024.04.25T16.44.31.likes.ts +++ b/src/db/migrations/2024.04.25T16.44.31.likes.ts @@ -1,8 +1,17 @@ import { Migration } from "../scripts/dbMigrate"; export const up: Migration = async (params) => { - params.context.query(`RAISE EXCEPTION 'up migration not implemented'`); + return params.context.query(`CREATE TABLE Likes ( + id SERIAL PRIMARY KEY, + postId INTEGER NOT NULL, + userId INTEGER NOT NULL, + createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (postId) REFERENCES Posts(id), + FOREIGN KEY (userId) REFERENCES Users(id) +); +`); }; + export const down: Migration = async (params) => { - params.context.query(`RAISE EXCEPTION 'down migration not implemented'`); + return params.context.query(`DROP TABLE likes;`); }; diff --git a/src/db/migrations/migrations.json b/src/db/migrations/migrations.json new file mode 100644 index 0000000..dd92995 --- /dev/null +++ b/src/db/migrations/migrations.json @@ -0,0 +1,5 @@ +[ + "2024.04.25T16.33.34.users.ts", + "2024.04.25T16.40.52.posts.ts", + "2024.04.25T16.44.31.likes.ts" +] \ No newline at end of file From 2c875a8a6b76f7b5f683d04f234197a3dd2d3938 Mon Sep 17 00:00:00 2001 From: carusi99 Date: Thu, 25 Apr 2024 12:49:52 -0500 Subject: [PATCH 08/37] authenticate and authorize --- src/data/auth-data.ts | 22 ++++++++++++++ src/middlewares/authenticate.ts | 46 +++++++++++++++++++++++++++++ src/middlewares/authorize.ts | 16 ++++++++++ src/models/auth.ts | 24 +++++++++++++++ src/routers/auth-router.ts | 52 +++++++++++++++++++++++++++++++++ src/services/auth-service.ts | 30 +++++++++++++++++++ 6 files changed, 190 insertions(+) create mode 100644 src/middlewares/authenticate.ts create mode 100644 src/middlewares/authorize.ts create mode 100644 src/models/auth.ts create mode 100644 src/routers/auth-router.ts create mode 100644 src/services/auth-service.ts diff --git a/src/data/auth-data.ts b/src/data/auth-data.ts index e69de29..b04a723 100644 --- a/src/data/auth-data.ts +++ b/src/data/auth-data.ts @@ -0,0 +1,22 @@ +import { query } from "../db"; +import { User } from "../models/auth"; + +export async function createUser( + username: string, + password: string, + role: string +): Promise { + return ( + await query( + "INSERT INTO users (username, password, role) VALUES ($1, $2, $3) RETURNING *", + [username, password, role] + ) + ).rows[0]; +} + +export async function getUserByUsername( + username: string +): Promise { + return (await query("SELECT * FROM users WHERE username=$1", [username])) + .rows[0]; +} diff --git a/src/middlewares/authenticate.ts b/src/middlewares/authenticate.ts new file mode 100644 index 0000000..9d13654 --- /dev/null +++ b/src/middlewares/authenticate.ts @@ -0,0 +1,46 @@ +import { NextFunction, Request, Response } from "express"; +import jwt from "jsonwebtoken"; +import { ApiError } from "./error"; + +// Extendemos el objeto Request para incluir la propiedad user +declare global { + namespace Express { + interface Request { + userId?: number; + userRole?: string; + } + } +} + +const jwtSecret = "ultra-secret"; + +export function authenticateHandler( + req: Request, + _res: Response, + next: NextFunction +) { + console.log("Middleware authenticateHandler alcanzado"); // Agrega este registro de consola + + const token = req.headers.authorization?.split(" ")[1]; + + if (!token) { + console.log("Token no encontrado en las cabeceras"); // Agrega este registro de consola + return next(new ApiError("No autorizado", 401)); + } + + try { + const payload = jwt.verify(token, jwtSecret) as { + userId: number; + userRole: string; + iat: number; + exp: number; + }; + + req.userId = payload.userId; + req.userRole = payload.userRole; + next(); + } catch (error: any) { + console.log("Error al verificar el token:", error.message); // Agrega este registro de consola + return next(new ApiError("No autorizado", 401)); + } +} diff --git a/src/middlewares/authorize.ts b/src/middlewares/authorize.ts new file mode 100644 index 0000000..7061421 --- /dev/null +++ b/src/middlewares/authorize.ts @@ -0,0 +1,16 @@ +import { NextFunction, Request, Response } from "express"; +import { User } from "../models/auth"; +import { ApiError } from "./error"; + +export function authorize(...allowedRoles: User["role"][]) { + return async (req: Request, _res: Response, next: NextFunction) => { + const role = req.userRole; + if (!role) return next(new ApiError("No autorizado", 401)); + + if (allowedRoles.includes(role as User["role"])) { + next(); + } else { + next(new ApiError("Acceso denegado", 403)); + } + }; +} diff --git a/src/models/auth.ts b/src/models/auth.ts new file mode 100644 index 0000000..b5409e5 --- /dev/null +++ b/src/models/auth.ts @@ -0,0 +1,24 @@ +import { z } from "zod"; + +export const userSchema = z.object({ + username: z.string({ + required_error: "Username es requerido", + invalid_type_error: "Username debe ser un string", + }), + + password: z + .string({ + required_error: "Password es requerido", + invalid_type_error: "Password debe ser un string", + }) + .min(6, "Password debe tener al menos 6 caracteres"), + role: z + .enum(["admin", "user"], { + errorMap: () => ({ message: "El rol debe ser admin o user" }), + }) + .default("user"), +}); + +export type UserParams = z.infer; + +export type User = UserParams & { id: number }; diff --git a/src/routers/auth-router.ts b/src/routers/auth-router.ts new file mode 100644 index 0000000..53fdf46 --- /dev/null +++ b/src/routers/auth-router.ts @@ -0,0 +1,52 @@ +import express from "express"; +import { validationHandler } from "../middlewares/validation"; +import { userSchema } from "../models/auth"; +import { createUser, validateCredentials } from "../services/auth-service"; +import jwt from "jsonwebtoken"; + +const jwtSecret = "ultra-secret"; + +const authRouter = express.Router(); + +//POST/register: + +authRouter.post( + "/register", + validationHandler(userSchema), + async (req, res, next) => { + try { + const newUser = await createUser(req.body); + res.status(201).json({ + ok: true, + message: "Usuario registrado exitosamente", + data: { + id: newUser.id, + username: newUser.username, + role: newUser.role, + }, + }); + } catch (error) { + next(error); + } + } +); + +//POST/Login: + +authRouter.post("/login", async (req, res, next) => { + try { + const user = await validateCredentials(req.body); + // console.log(req.session); + const payload = { userId: user.id, userRole: user.role }; + const token = jwt.sign(payload, jwtSecret, { expiresIn: "60m" }); + res.json({ + ok: true, + message: "Login exitoso", + data: { token: token }, + }); + } catch (error) { + next(error); + } +}); + +export default authRouter; diff --git a/src/services/auth-service.ts b/src/services/auth-service.ts new file mode 100644 index 0000000..d824365 --- /dev/null +++ b/src/services/auth-service.ts @@ -0,0 +1,30 @@ +import bcrypt from "bcrypt"; +import { User, UserParams } from "../models/auth"; +import * as userDB from "../data/auth-data"; +import { ApiError } from "../middlewares/error"; + +export async function createUser(data: UserParams): Promise { + const { username, password, role } = data; + + const user = await userDB.getUserByUsername(username); + if (user) { + throw new ApiError("El username ya está registrado", 400); + } + + const costFactor = 10; + const hashedPassword = await bcrypt.hash(password, costFactor); + const newUser = await userDB.createUser(username, hashedPassword, role); + return newUser; +} + +export async function validateCredentials( + credentials: UserParams +): Promise { + const { username, password } = credentials; + const user = await userDB.getUserByUsername(username); + const isValid = await bcrypt.compare(password, user?.password || ""); + if (!user || !isValid) { + throw new ApiError("Credenciales incorrectas", 400); + } + return user; +} From ca5249e0801c2148425634f5e1843b0316634b6e Mon Sep 17 00:00:00 2001 From: carusi99 Date: Thu, 25 Apr 2024 18:11:47 -0500 Subject: [PATCH 09/37] migration to users and posts table --- package-lock.json | 630 +++++++++++++++++- package.json | 3 + src/data/auth-data.ts | 37 +- .../2024.04.25T22.02.49.seed-user.ts | 60 ++ .../2024.04.25T22.42.33.seed-posts.ts | 46 ++ src/db/migrations/migrations.json | 4 +- src/middlewares/error.ts | 38 ++ src/middlewares/validation.ts | 32 + src/models/auth.ts | 33 +- src/models/user.models.ts | 0 src/routers/auth-router.ts | 2 +- src/services/auth-service.ts | 20 +- 12 files changed, 867 insertions(+), 38 deletions(-) create mode 100644 src/db/migrations/2024.04.25T22.02.49.seed-user.ts create mode 100644 src/db/migrations/2024.04.25T22.42.33.seed-posts.ts create mode 100644 src/middlewares/validation.ts create mode 100644 src/models/user.models.ts diff --git a/package-lock.json b/package-lock.json index 094b7c7..85964b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,13 +9,16 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "bcrypt": "^5.1.1", "dotenv": "^16.3.1", "express": "^4.19.2", + "jsonwebtoken": "^9.0.2", "pg": "^8.11.5", "umzug": "^3.8.0", "zod": "^3.23.4" }, "devDependencies": { + "@faker-js/faker": "^8.4.1", "@types/express": "^4.17.21", "@types/node": "^20.10.6", "@types/pg": "^8.10.9", @@ -36,6 +39,22 @@ "node": ">=12" } }, + "node_modules/@faker-js/faker": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz", + "integrity": "sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0", + "npm": ">=6.14.13" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", @@ -61,6 +80,39 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -304,8 +356,7 @@ "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "node_modules/accepts": { "version": "1.3.8", @@ -340,6 +391,46 @@ "node": ">=0.4.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -353,6 +444,23 @@ "node": ">= 8" } }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -375,8 +483,20 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } }, "node_modules/binary-extensions": { "version": "2.2.0", @@ -414,7 +534,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -431,6 +550,11 @@ "node": ">=8" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -484,6 +608,22 @@ "fsevents": "~2.3.2" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "bin": { + "color-support": "bin.js" + } + }, "node_modules/commander": { "version": "9.5.0", "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", @@ -496,8 +636,12 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" }, "node_modules/content-disposition": { "version": "0.5.4", @@ -561,6 +705,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -578,6 +727,14 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "engines": { + "node": ">=8" + } + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -598,6 +755,14 @@ "url": "https://github.com/motdotla/dotenv?sponsor=1" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -614,6 +779,11 @@ "url": "https://github.com/sindresorhus/emittery?sponsor=1" } }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, "node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -775,6 +945,33 @@ "node": ">=6 <7 || >=8" } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -797,6 +994,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/get-intrinsic": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", @@ -815,6 +1031,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -884,6 +1119,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -910,6 +1150,39 @@ "node": ">= 0.8" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -935,6 +1208,15 @@ "node": ">=8" } }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -979,6 +1261,14 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -1011,16 +1301,96 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, "node_modules/lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -1032,6 +1402,28 @@ "node": ">=10" } }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -1113,7 +1505,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -1121,6 +1512,48 @@ "node": "*" } }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -1134,6 +1567,30 @@ "node": ">= 0.6" } }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/nodemon": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.3.tgz", @@ -1209,6 +1666,25 @@ "node": ">=0.10.0" } }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", @@ -1234,6 +1710,14 @@ "node": ">= 0.8" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1242,6 +1726,14 @@ "node": ">= 0.8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -1532,6 +2024,19 @@ "node": ">= 0.8" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -1569,6 +2074,20 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -1671,6 +2190,11 @@ "node": ">= 0.8.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -1709,6 +2233,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -1742,6 +2271,14 @@ "node": ">= 0.8" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-argv": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", @@ -1750,6 +2287,30 @@ "node": ">=0.6.19" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -1773,6 +2334,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -1804,6 +2381,11 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -1926,6 +2508,11 @@ "node": ">= 0.8" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -1956,6 +2543,33 @@ "node": ">= 0.8" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index ee1490e..066d6fd 100644 --- a/package.json +++ b/package.json @@ -14,13 +14,16 @@ "db:reset": "npm run db:drop && npm run db:create && npm run db:migrate up" }, "dependencies": { + "bcrypt": "^5.1.1", "dotenv": "^16.3.1", "express": "^4.19.2", + "jsonwebtoken": "^9.0.2", "pg": "^8.11.5", "umzug": "^3.8.0", "zod": "^3.23.4" }, "devDependencies": { + "@faker-js/faker": "^8.4.1", "@types/express": "^4.17.21", "@types/node": "^20.10.6", "@types/pg": "^8.10.9", diff --git a/src/data/auth-data.ts b/src/data/auth-data.ts index b04a723..c34ad3f 100644 --- a/src/data/auth-data.ts +++ b/src/data/auth-data.ts @@ -1,22 +1,29 @@ -import { query } from "../db"; -import { User } from "../models/auth"; +import * as db from "../db"; +import { UserParams, User } from "../models/auth"; -export async function createUser( - username: string, - password: string, - role: string -): Promise { - return ( - await query( - "INSERT INTO users (username, password, role) VALUES ($1, $2, $3) RETURNING *", - [username, password, role] - ) - ).rows[0]; +export async function createUsers(user: UserParams): Promise { + const now = new Date(); + const query = + "INSERT INTO users (username, password, email, firstName, lastName, role, createdAt, updatedAt) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *"; + const queryParams: (string | Date | undefined)[] = [ + user.username, + user.password, + user.email, + user.firstName, + user.lastName, + user.role, + now, + now, + ]; + + const result = await db.query(query, queryParams); + return result.rows[0]; } export async function getUserByUsername( username: string ): Promise { - return (await query("SELECT * FROM users WHERE username=$1", [username])) - .rows[0]; + return ( + await db.query("SELECT * FROM users WHERE username=$1", [username]) + ).rows[0]; } diff --git a/src/db/migrations/2024.04.25T22.02.49.seed-user.ts b/src/db/migrations/2024.04.25T22.02.49.seed-user.ts new file mode 100644 index 0000000..7695f1e --- /dev/null +++ b/src/db/migrations/2024.04.25T22.02.49.seed-user.ts @@ -0,0 +1,60 @@ +import { Migration } from "../scripts/dbMigrate"; +import { faker } from "@faker-js/faker"; + +export type User = { + username: string; + password: string; + email: string; + firstName: string | null; + lastName: string | null; + role: string; + createdAt: Date; + updatedAt: Date; +}; + +export function generateUser(): User { + const username = faker.internet.userName(); + const password = faker.internet.password(); + const email = faker.internet.email(); + const firstName = faker.person.firstName(); + const lastName = faker.person.lastName(); + const role = faker.helpers.arrayElement(["user", "admin"]); + const createdAt = faker.date.past(); + const updatedAt = faker.date.recent(); + + return { + username, + password, + email, + firstName, + lastName, + role, + createdAt, + updatedAt, + }; +} + +export const up: Migration = async (params) => { + const users: User[] = []; + for (let i = 0; i < 50; i++) { + users.push(generateUser()); + } + + const values = users + .map( + (user) => + `('${user.username}', '${user.password}', '${user.email}', ${ + user.firstName ? `'${user.firstName}'` : "NULL" + }, ${ + user.lastName ? `'${user.lastName}'` : "NULL" + }, '${user.role}', '${user.createdAt.toISOString()}', '${user.updatedAt.toISOString()}')` + ) + .join(", "); + const sqlQuery = `INSERT INTO Users (username, password, email, firstName, lastName, role, createdAt, updatedAt) VALUES ${values};`; + + return await params.context.query(sqlQuery); +}; + +export const down: Migration = async (params) => { + return params.context.query(`DELETE FROM users;`); +}; diff --git a/src/db/migrations/2024.04.25T22.42.33.seed-posts.ts b/src/db/migrations/2024.04.25T22.42.33.seed-posts.ts new file mode 100644 index 0000000..c74168c --- /dev/null +++ b/src/db/migrations/2024.04.25T22.42.33.seed-posts.ts @@ -0,0 +1,46 @@ +import { Migration } from "../scripts/dbMigrate"; +import { faker } from "@faker-js/faker"; + +export type Post = { + userId: number; + content: string; + createdAt: Date; + updatedAt: Date; +}; + +export function generatePost(userId: number): Post { + const now = new Date(); + const content = faker.lorem.paragraph(); + return { + userId, + content, + createdAt: now, + updatedAt: now, + }; +} + +export const up: Migration = async (params) => { + // Obtener la lista de usuarios de la base de datos + const usersResult = await params.context.query("SELECT id FROM users;"); + const users = usersResult.rows; + + // Crear un post para cada usuario + const posts: Post[] = users.map((user: { id: number }) => { + return generatePost(user.id); + }); + + // Insertar los posts en la base de datos + const values = posts + .map( + (post) => + `(${post.userId}, '${post.content}', '${post.createdAt.toISOString()}', '${post.updatedAt.toISOString()}')` + ) + .join(", "); + const sqlQuery = `INSERT INTO posts (userId, content, createdAt, updatedAt) VALUES ${values};`; + + return await params.context.query(sqlQuery); +}; + +export const down: Migration = async (params) => { + return await params.context.query(`DELETE FROM posts;`); +}; diff --git a/src/db/migrations/migrations.json b/src/db/migrations/migrations.json index dd92995..c02b572 100644 --- a/src/db/migrations/migrations.json +++ b/src/db/migrations/migrations.json @@ -1,5 +1,7 @@ [ "2024.04.25T16.33.34.users.ts", "2024.04.25T16.40.52.posts.ts", - "2024.04.25T16.44.31.likes.ts" + "2024.04.25T16.44.31.likes.ts", + "2024.04.25T22.02.49.seed-user.ts", + "2024.04.25T22.42.33.seed-posts.ts" ] \ No newline at end of file diff --git a/src/middlewares/error.ts b/src/middlewares/error.ts index e69de29..a44edbe 100644 --- a/src/middlewares/error.ts +++ b/src/middlewares/error.ts @@ -0,0 +1,38 @@ +import { NextFunction, Request, Response } from "express"; + +export class ApiError extends Error { + status: number; + details?: Record; + + constructor(message: string, status: number, details?: Record) { + super(message); + this.status = status; + this.details = details; + } +} + +export default function errorHandler( + error: Error, + _req: Request, + res: Response, + _next: NextFunction +) { + console.log("Error handler!"); + if (error instanceof ApiError) { + res.status(error.status).json({ + ok: false, + error: { + message: error.message, + details: error.details, + }, + }); + } else { + console.log(error); + res.status(500).json({ + ok: false, + error: { + message: "Error interno del servidor", + }, + }); + } +} diff --git a/src/middlewares/validation.ts b/src/middlewares/validation.ts new file mode 100644 index 0000000..f9afd4e --- /dev/null +++ b/src/middlewares/validation.ts @@ -0,0 +1,32 @@ +import { ZodSchema, ZodError, ZodIssue } from "zod"; +import { ApiError } from "./error"; +import { NextFunction, Request, Response } from "express"; + +export function validationHandler(schema: ZodSchema) { + return async (req: Request, _res: Response, next: NextFunction) => { + try { + const body = schema.parse(req.body); + req.body = body; + next(); + } catch (error) { + if (error instanceof ZodError) { + console.log(error); + next( + new ApiError("Error de validación", 400, formatIssues(error.issues)) + ); + } else { + next(error); + } + } + }; +} + +function formatIssues(issues: ZodIssue[]) { + const formattedIssues: Record = {}; + + issues.forEach((issue) => { + formattedIssues[issue.path.join(".")] = issue.message; + }); + + return formattedIssues; +} diff --git a/src/models/auth.ts b/src/models/auth.ts index b5409e5..69f0047 100644 --- a/src/models/auth.ts +++ b/src/models/auth.ts @@ -12,13 +12,36 @@ export const userSchema = z.object({ invalid_type_error: "Password debe ser un string", }) .min(6, "Password debe tener al menos 6 caracteres"), - role: z - .enum(["admin", "user"], { - errorMap: () => ({ message: "El rol debe ser admin o user" }), - }) - .default("user"), + + email: z.string({ + required_error: "Email es requerido", + invalid_type_error: "Email debe ser un string", + }).email({ message: "El formato del correo electrónico es inválido" }).optional(), + + firstName: z.string({ + invalid_type_error: "Nombre debe ser un string", + }).optional(), + + lastName: z.string({ + invalid_type_error: "Apellido debe ser un string", + }).optional(), + + role: z.enum(["admin", "user"], { + errorMap: () => ({ message: "El rol debe ser admin o user" }), + }).default("user"), + + createdAt: z.date({ + required_error: "CreatedAt es requerido", + invalid_type_error: "CreatedAt debe ser una fecha", + }).default(() => new Date()), + + updatedAt: z.date({ + required_error: "UpdatedAt es requerido", + invalid_type_error: "UpdatedAt debe ser una fecha", + }).default(() => new Date()), }); export type UserParams = z.infer; export type User = UserParams & { id: number }; + diff --git a/src/models/user.models.ts b/src/models/user.models.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/routers/auth-router.ts b/src/routers/auth-router.ts index 53fdf46..4a7e589 100644 --- a/src/routers/auth-router.ts +++ b/src/routers/auth-router.ts @@ -11,7 +11,7 @@ const authRouter = express.Router(); //POST/register: authRouter.post( - "/register", + "/signup", validationHandler(userSchema), async (req, res, next) => { try { diff --git a/src/services/auth-service.ts b/src/services/auth-service.ts index d824365..b65b461 100644 --- a/src/services/auth-service.ts +++ b/src/services/auth-service.ts @@ -1,19 +1,20 @@ import bcrypt from "bcrypt"; -import { User, UserParams } from "../models/auth"; +import { User, UserParams } from "../models/auth.ts"; import * as userDB from "../data/auth-data"; -import { ApiError } from "../middlewares/error"; +import { ApiError } from "../middlewares/error.ts"; export async function createUser(data: UserParams): Promise { const { username, password, role } = data; + // Verifica si el usuario ya existe después de hashear la contraseña + const costFactor = 10; + const hashedPassword = await bcrypt.hash(password, costFactor); const user = await userDB.getUserByUsername(username); if (user) { throw new ApiError("El username ya está registrado", 400); } - const costFactor = 10; - const hashedPassword = await bcrypt.hash(password, costFactor); - const newUser = await userDB.createUser(username, hashedPassword, role); + const newUser = await userDB.createUsers({ ...data, password: hashedPassword }); return newUser; } @@ -22,9 +23,12 @@ export async function validateCredentials( ): Promise { const { username, password } = credentials; const user = await userDB.getUserByUsername(username); - const isValid = await bcrypt.compare(password, user?.password || ""); - if (!user || !isValid) { + if (!user) { + throw new ApiError("Credenciales incorrectas", 400); + } + const isValid = await bcrypt.compare(password, user.password); + if (!isValid) { throw new ApiError("Credenciales incorrectas", 400); } return user; -} +} \ No newline at end of file From f4f464d04b9d0af794881eee6664e9934ac13e79 Mon Sep 17 00:00:00 2001 From: carusi99 Date: Thu, 25 Apr 2024 21:34:46 -0500 Subject: [PATCH 10/37] login and signup --- package-lock.json | 20 +++++++++++++++ package.json | 2 ++ src/app.ts | 15 +++++++++++ src/data/auth-data.ts | 45 +++++++++++++++++++++++++------- src/index.ts | 19 +++++++++++--- src/models/auth.ts | 8 +++--- src/routers/auth-router.ts | 2 +- src/services/auth-service.ts | 34 ------------------------ src/services/auth.service.ts | 50 ++++++++++++++++++++++++++++++++++++ 9 files changed, 143 insertions(+), 52 deletions(-) delete mode 100644 src/services/auth-service.ts create mode 100644 src/services/auth.service.ts diff --git a/package-lock.json b/package-lock.json index 85964b7..1db2411 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,9 @@ }, "devDependencies": { "@faker-js/faker": "^8.4.1", + "@types/bcrypt": "^5.0.2", "@types/express": "^4.17.21", + "@types/jsonwebtoken": "^9.0.6", "@types/node": "^20.10.6", "@types/pg": "^8.10.9", "nodemon": "^3.0.2", @@ -245,6 +247,15 @@ "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz", "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==" }, + "node_modules/@types/bcrypt": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", + "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -294,6 +305,15 @@ "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", "dev": true }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.6.tgz", + "integrity": "sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", diff --git a/package.json b/package.json index 066d6fd..47d3dda 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,9 @@ }, "devDependencies": { "@faker-js/faker": "^8.4.1", + "@types/bcrypt": "^5.0.2", "@types/express": "^4.17.21", + "@types/jsonwebtoken": "^9.0.6", "@types/node": "^20.10.6", "@types/pg": "^8.10.9", "nodemon": "^3.0.2", diff --git a/src/app.ts b/src/app.ts index e69de29..917d1c1 100644 --- a/src/app.ts +++ b/src/app.ts @@ -0,0 +1,15 @@ +import express from "express"; +import { configDotenv } from "dotenv"; +import authRouter from "./routers/auth-router" + +if (process.env["NODE_ENV"] === "test") { + configDotenv({ path: ".env.test" }); +} else { + configDotenv(); +} + +export const app = express(); + +app.use(express.json()); + +app.use(authRouter); diff --git a/src/data/auth-data.ts b/src/data/auth-data.ts index c34ad3f..84dd062 100644 --- a/src/data/auth-data.ts +++ b/src/data/auth-data.ts @@ -1,25 +1,52 @@ import * as db from "../db"; +import { ApiError } from "../middlewares/error"; import { UserParams, User } from "../models/auth"; export async function createUsers(user: UserParams): Promise { const now = new Date(); const query = - "INSERT INTO users (username, password, email, firstName, lastName, role, createdAt, updatedAt) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *"; - const queryParams: (string | Date | undefined)[] = [ + `INSERT INTO users + (username, password, email, firstname, lastname, role, createdat, updatedat) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + RETURNING id, username, role, firstname, lastname, createdat, updatedat`; + + // Reemplazar los valores null por un valor por defecto + const queryParams = [ user.username, user.password, - user.email, - user.firstName, - user.lastName, + user.email || '', // Reemplazar null con un valor por defecto, como una cadena vacía '' + user.firstname || '', // Reemplazar null con un valor por defecto + user.lastname || '', // Reemplazar null con un valor por defecto user.role, - now, - now, + now.toISOString(), // Convertir Date a string + now.toISOString(), // Convertir Date a string ]; - const result = await db.query(query, queryParams); - return result.rows[0]; + try { + const result = await db.query(query, queryParams); + + // Verificar si se insertó correctamente y si se devolvieron los campos esperados + if (result.rows.length === 0) { + throw new ApiError('No se pudo insertar el usuario', 400); + } + + // Devolver el usuario insertado con todos los campos + return { + id: result.rows[0].id, + username: result.rows[0].username, + password: '', // Puedes inicializarla con un valor vacío o cualquier otro valor predeterminado + firstname: result.rows[0].firstName, + lastname: result.rows[0].lastName, + role: result.rows[0].role, + createdat: result.rows[0].createdat, + updatedat: result.rows[0].updatedat + }; + } catch (error) { + throw new ApiError('Error al insertar el usuario', 400); + } } + export async function getUserByUsername( username: string ): Promise { diff --git a/src/index.ts b/src/index.ts index da6d23f..37f0849 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,17 @@ -import express from "express"; +import { app } from "./app"; +import { pool } from "./db"; -const app = express(); -const port = 5500; +const port = 7000; -app.listen(port, () => console.log(`Escuchando al puerto ${port}`)); +// Manejar cierre de la aplicación +const gracefulShutdown = () => { + pool.end(() => { + console.log("\nApplication ended gracefully"); + process.exit(0); + }); +}; +// Eventos de cierre para que no se queden conexiones abiertas +process.on("SIGINT", gracefulShutdown); +process.on("SIGTERM", gracefulShutdown); + +app.listen(port, () => console.log(`Escuchando al puerto ${port}`)); \ No newline at end of file diff --git a/src/models/auth.ts b/src/models/auth.ts index 69f0047..245977b 100644 --- a/src/models/auth.ts +++ b/src/models/auth.ts @@ -18,11 +18,11 @@ export const userSchema = z.object({ invalid_type_error: "Email debe ser un string", }).email({ message: "El formato del correo electrónico es inválido" }).optional(), - firstName: z.string({ + firstname: z.string({ invalid_type_error: "Nombre debe ser un string", }).optional(), - lastName: z.string({ + lastname: z.string({ invalid_type_error: "Apellido debe ser un string", }).optional(), @@ -30,12 +30,12 @@ export const userSchema = z.object({ errorMap: () => ({ message: "El rol debe ser admin o user" }), }).default("user"), - createdAt: z.date({ + createdat: z.date({ required_error: "CreatedAt es requerido", invalid_type_error: "CreatedAt debe ser una fecha", }).default(() => new Date()), - updatedAt: z.date({ + updatedat: z.date({ required_error: "UpdatedAt es requerido", invalid_type_error: "UpdatedAt debe ser una fecha", }).default(() => new Date()), diff --git a/src/routers/auth-router.ts b/src/routers/auth-router.ts index 4a7e589..27ecb83 100644 --- a/src/routers/auth-router.ts +++ b/src/routers/auth-router.ts @@ -1,7 +1,7 @@ import express from "express"; import { validationHandler } from "../middlewares/validation"; import { userSchema } from "../models/auth"; -import { createUser, validateCredentials } from "../services/auth-service"; +import { createUser, validateCredentials } from "../services/auth.service"; import jwt from "jsonwebtoken"; const jwtSecret = "ultra-secret"; diff --git a/src/services/auth-service.ts b/src/services/auth-service.ts deleted file mode 100644 index b65b461..0000000 --- a/src/services/auth-service.ts +++ /dev/null @@ -1,34 +0,0 @@ -import bcrypt from "bcrypt"; -import { User, UserParams } from "../models/auth.ts"; -import * as userDB from "../data/auth-data"; -import { ApiError } from "../middlewares/error.ts"; - -export async function createUser(data: UserParams): Promise { - const { username, password, role } = data; - - // Verifica si el usuario ya existe después de hashear la contraseña - const costFactor = 10; - const hashedPassword = await bcrypt.hash(password, costFactor); - const user = await userDB.getUserByUsername(username); - if (user) { - throw new ApiError("El username ya está registrado", 400); - } - - const newUser = await userDB.createUsers({ ...data, password: hashedPassword }); - return newUser; -} - -export async function validateCredentials( - credentials: UserParams -): Promise { - const { username, password } = credentials; - const user = await userDB.getUserByUsername(username); - if (!user) { - throw new ApiError("Credenciales incorrectas", 400); - } - const isValid = await bcrypt.compare(password, user.password); - if (!isValid) { - throw new ApiError("Credenciales incorrectas", 400); - } - return user; -} \ No newline at end of file diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts new file mode 100644 index 0000000..e75b9b3 --- /dev/null +++ b/src/services/auth.service.ts @@ -0,0 +1,50 @@ +import bcrypt from "bcrypt"; +import { User, UserParams } from "../models/auth"; +import * as userDB from "../data/auth-data"; +import { ApiError } from "../middlewares/error"; + +export async function createUser(data: UserParams): Promise { + const { username, password, role } = data; + + // Verificar la longitud de la contraseña + if (password.length < 6) { + throw new ApiError("La contraseña debe tener al menos 6 caracteres", 400); + } + + // Verificar la existencia del usuario + try { + const user = await userDB.getUserByUsername(username); + if (user) { + throw new ApiError("El username ya está registrado", 400); + } + } catch (error) { + throw new ApiError("Error al verificar la existencia del usuario", 500); + } + + // Hashear la contraseña + const costFactor = 10; + const hashedPassword = await bcrypt.hash(password, costFactor); + + // Crear el nuevo usuario + try { + const newUser = await userDB.createUsers({ ...data, password: hashedPassword }); + return newUser; + } catch (error) { + throw new ApiError("Error al crear el usuario", 500); + } +} + +export async function validateCredentials( + credentials: UserParams +): Promise { + const { username, password } = credentials; + const user = await userDB.getUserByUsername(username); + if (!user) { + throw new ApiError("Credenciales incorrectas", 400); + } + const isValid = await bcrypt.compare(password, user.password); + if (!isValid) { + throw new ApiError("Credenciales incorrectas", 400); + } + return user; +} \ No newline at end of file From 9fada7ac2d1b81837bba7ab4b04e9a74f0622af6 Mon Sep 17 00:00:00 2001 From: carusi99 Date: Fri, 26 Apr 2024 10:36:55 -0500 Subject: [PATCH 11/37] get/post --- src/app.ts | 2 ++ src/data/posts.data.ts | 40 ++++++++++++++++++++++++++++ src/data/utils.ts | 13 +++++++++ src/models/posts.ts | 21 +++++++++++++++ src/routers/posts.routers.ts | 50 +++++++++++++++++++++++++++++++++++ src/services/posts.service.ts | 13 +++++++++ 6 files changed, 139 insertions(+) create mode 100644 src/data/utils.ts create mode 100644 src/models/posts.ts diff --git a/src/app.ts b/src/app.ts index 917d1c1..c640c13 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,6 +1,7 @@ import express from "express"; import { configDotenv } from "dotenv"; import authRouter from "./routers/auth-router" +import postsRouter from "./routers/posts.routers" if (process.env["NODE_ENV"] === "test") { configDotenv({ path: ".env.test" }); @@ -13,3 +14,4 @@ export const app = express(); app.use(express.json()); app.use(authRouter); +app.use(postsRouter) diff --git a/src/data/posts.data.ts b/src/data/posts.data.ts index e69de29..9c4b5ab 100644 --- a/src/data/posts.data.ts +++ b/src/data/posts.data.ts @@ -0,0 +1,40 @@ +import * as db from "../db"; +import { Post } from "../models/posts"; +import { PostFilters } from "../models/posts"; + +export async function getPosts(page: number, limit: number, filters: PostFilters = {}, orderBy: string = 'createdAt', order: string = 'asc'): Promise { + try { + let query = 'SELECT * FROM posts'; + const queryParams: any[] = []; + + if (filters.username) { + query += ' WHERE username = $1'; + queryParams.push(filters.username); + } + + query += ` ORDER BY ${orderBy} ${order}`; + query += ` LIMIT ${limit} OFFSET ${(page - 1) * limit}`; + + const { rows } = await db.query(query, queryParams); + return rows; + } catch (error) { + throw new Error('Error al obtener los posts desde la base de datos'); + } +} + +export async function getTotalPosts(username?: string): Promise { + try { + let query = 'SELECT COUNT(*) FROM posts'; + const queryParams: any[] = []; + + if (username) { + query += ' WHERE username = $1'; + queryParams.push(username); + } + + const { rows } = await db.query(query, queryParams); + return parseInt(rows[0].count, 10); + } catch (error) { + throw new Error('Error al obtener el total de posts desde la base de datos'); + } +} diff --git a/src/data/utils.ts b/src/data/utils.ts new file mode 100644 index 0000000..6408c12 --- /dev/null +++ b/src/data/utils.ts @@ -0,0 +1,13 @@ +export function pagination(page: number, limit: number): string { + const offset = (page - 1) * limit; + return `LIMIT ${limit} OFFSET ${offset}`; +} + + +export function sorting(query: string, sort?: string): string { + if (sort) { + const [sortCol, sortDir] = sort.split("_"); + query += ` ORDER BY "${sortCol}" ${sortDir.toUpperCase() || "ASC"}`; + } + return query; +} diff --git a/src/models/posts.ts b/src/models/posts.ts new file mode 100644 index 0000000..da7c761 --- /dev/null +++ b/src/models/posts.ts @@ -0,0 +1,21 @@ +export interface Post { + id: number; + content: string; + createdAt: Date; + updatedAt: Date; + username: string; + likesCount: number; + nextPage: number | null; // Cambiado de 'null' a 'number | null' + previousPage: number | null; // Cambiado de 'null' a 'number | null' +} + +// post-filter.ts +export interface PostFilters { + username?: string; +} + +export interface PostWithPagination extends Post { + // nextPage y previousPage ahora son definidos como 'number | null' + nextPage: number | null; + previousPage: number | null; +} diff --git a/src/routers/posts.routers.ts b/src/routers/posts.routers.ts index e69de29..3da2046 100644 --- a/src/routers/posts.routers.ts +++ b/src/routers/posts.routers.ts @@ -0,0 +1,50 @@ +import express from "express"; +import { getPosts, getTotalPosts } from "../data/posts.data"; +import { pagination } from "../data/utils"; +import { PostFilters } from "../models/posts"; + +const postsRouter = express.Router(); + +postsRouter.get('/', async (req, res) => { + try { + const { page = 1, limit = 10, username, orderBy = 'createdAt', order = 'asc' } = req.query; + const pageInt = parseInt(page as string, 10); + const limitInt = parseInt(limit as string, 10); + + // Aplicar paginación + const totalItems = await getTotalPosts(username as string); + const totalPages = Math.ceil(totalItems / limitInt); + const paginationInfo = pagination(pageInt, limitInt); + + let filters: PostFilters = {}; + if (typeof username === 'string' && username !== '') { + filters.username = username; + } + + const posts = await getPosts( + pageInt, + limitInt, + filters, + orderBy as string, + order as string + ); + + res.status(200).json({ + ok: true, + data: posts, + pagination: { + page: pageInt, + pageSize: limitInt, + totalItems: totalItems, + totalPages: totalPages, + nextPage: posts.length === limitInt ? pageInt + 1 : null, + previousPage: pageInt > 1 ? pageInt - 1 : null + } + }); + } catch (error) { + console.error('Error al obtener los posts:', error); + res.status(500).json({ ok: false, message: 'Error al obtener los posts' }); + } +}); + +export default postsRouter; diff --git a/src/services/posts.service.ts b/src/services/posts.service.ts index e69de29..31253fd 100644 --- a/src/services/posts.service.ts +++ b/src/services/posts.service.ts @@ -0,0 +1,13 @@ + +import { Post, PostFilters } from '../models/posts'; +import { ApiError } from '../middlewares/error'; +import * as postData from '../data/posts.data'; + +export async function getPostsbyUsername(filters: PostFilters = {}, orderBy: string = 'createdAt', order: string = 'asc', page: number = 1, limit: number = 10): Promise { + try { + const posts = await postData.getPosts(page, limit, filters, orderBy, order); + return posts; + } catch (error) { + throw new ApiError('Error al obtener los posts desde la base de datos', 400); + } +} From 6572dfdb819336437669ed09ebf42b1f2e716dad Mon Sep 17 00:00:00 2001 From: carusi99 Date: Fri, 26 Apr 2024 17:53:12 -0500 Subject: [PATCH 12/37] create /me --- src/app.ts | 2 + src/data/users.data.ts | 32 ++++++++++++++ src/middlewares/authenticate.ts | 12 +++--- src/models/auth.ts | 4 ++ src/routers/users.routers.ts | 76 +++++++++++++++++++++++++++++++++ src/services/users.service.ts | 19 +++++++++ 6 files changed, 139 insertions(+), 6 deletions(-) diff --git a/src/app.ts b/src/app.ts index c640c13..50b9224 100644 --- a/src/app.ts +++ b/src/app.ts @@ -2,6 +2,7 @@ import express from "express"; import { configDotenv } from "dotenv"; import authRouter from "./routers/auth-router" import postsRouter from "./routers/posts.routers" +import userRouter from "./routers/users.routers"; if (process.env["NODE_ENV"] === "test") { configDotenv({ path: ".env.test" }); @@ -15,3 +16,4 @@ app.use(express.json()); app.use(authRouter); app.use(postsRouter) +app.use(userRouter); diff --git a/src/data/users.data.ts b/src/data/users.data.ts index e69de29..0116006 100644 --- a/src/data/users.data.ts +++ b/src/data/users.data.ts @@ -0,0 +1,32 @@ +import { User, UserParams, Userupdate } from "../models/auth"; +import * as db from "../db"; + +export async function getUser(id: number): Promise { + return (await db.query("SELECT * FROM users WHERE id = $1", [id])).rows[0]; + } + + + export async function updateUser({ + id, + fieldsToUpdate, + }: Userupdate): Promise { + if (!id || Object.keys(fieldsToUpdate).length === 0) { + throw new Error("No se proporcionaron datos para actualizar"); + } + const fields = ["email", "firstname", "lastname"] + const validFields = Object.keys(fieldsToUpdate).filter( + (field) => fields.includes(field) + ) + const setClauses = validFields.map((field, index) => `${field} = $${index + 1}`) + const updateQuery = `UPDATE users SET ${setClauses.join(", ")} WHERE id = $${ + validFields.length + 1 + } RETURNING *`; + + const params = [...validFields.map((field) => fieldsToUpdate[field]), id]; + const result = await db.query(updateQuery, params); + return result.rows[0]; // Devuelve el usuario actualizado + } + + export async function deleteUser(id: number): Promise { + return (await db.query("DELETE FROM users WHERE id = $1 RETURNING *", [id])).rows[0]; + } \ No newline at end of file diff --git a/src/middlewares/authenticate.ts b/src/middlewares/authenticate.ts index 9d13654..5de2652 100644 --- a/src/middlewares/authenticate.ts +++ b/src/middlewares/authenticate.ts @@ -12,19 +12,19 @@ declare global { } } -const jwtSecret = "ultra-secret"; +export const jwtSecret = "ultra-secret"; export function authenticateHandler( req: Request, _res: Response, next: NextFunction ) { - console.log("Middleware authenticateHandler alcanzado"); // Agrega este registro de consola + console.log("Middleware authenticateHandler alcanzado"); const token = req.headers.authorization?.split(" ")[1]; if (!token) { - console.log("Token no encontrado en las cabeceras"); // Agrega este registro de consola + console.log("Token no encontrado en las cabeceras"); return next(new ApiError("No autorizado", 401)); } @@ -36,11 +36,11 @@ export function authenticateHandler( exp: number; }; - req.userId = payload.userId; + req.userId = payload.userId; // Asignar el ID de usuario extraído del token a req.userId req.userRole = payload.userRole; next(); } catch (error: any) { - console.log("Error al verificar el token:", error.message); // Agrega este registro de consola + console.log("Error al verificar el token:", error.message); return next(new ApiError("No autorizado", 401)); } -} +} \ No newline at end of file diff --git a/src/models/auth.ts b/src/models/auth.ts index 245977b..ba4c3cb 100644 --- a/src/models/auth.ts +++ b/src/models/auth.ts @@ -45,3 +45,7 @@ export type UserParams = z.infer; export type User = UserParams & { id: number }; +export interface Userupdate{ + id: number; + fieldsToUpdate: Record; +} \ No newline at end of file diff --git a/src/routers/users.routers.ts b/src/routers/users.routers.ts index e69de29..5c3b2a3 100644 --- a/src/routers/users.routers.ts +++ b/src/routers/users.routers.ts @@ -0,0 +1,76 @@ +import express from "express"; +import { getUsers, updateUsers, deleteUsers } from "../services/users.service"; +import { ApiError } from "../middlewares/error"; +import { authenticateHandler } from "../middlewares/authenticate"; +import { User } from "../models/auth"; + +const userRouter = express.Router(); + +userRouter.get("/me", authenticateHandler, async (req, res, next) => { + if (req.userId === undefined) { + return res.status(401).json({ ok: false, message: "No autorizado" }); + } + try { + const user = await getUsers(req.userId); + if (!user) { + return res.status(404).json({ ok: false, message: "Usuario no encontrado" }); + } + res.json({ + ok: true, + data: { + id: user.id, + username: user.username, + firstName: user.firstname, + email: user.email, + role: user.role, + createdAt: user.createdat, + updatedAt: user.updatedat + } + }); + } catch (error) { + next(error); + } +}); + +userRouter.patch("/me", authenticateHandler, async (req, res, next) => { + if (req.userId === undefined) { + return res.status(401).json({ ok: false, message: "No autorizado" }); + } + try { + const user: User = req.body; + const updatedUser = await updateUsers(req.userId, user); + if (!user) { + return res.status(404).json({ ok: false, message: "Usuario no encontrado" }); + } + res.json({ + ok: true, + data: { + id: user.id, + username: user.username, + firstName: user.firstname, + email: user.email, + role: user.role, + createdAt: user.createdat, + updatedAt: user.updatedat + } + }); + } catch (error) { + next(error); + } +}); + +userRouter.delete("/me", authenticateHandler, async (req, res, next) => { + if (req.userId === undefined) { + return res.status(401).json({ ok: false, message: "No autorizado" }); + } + try { + const result = await deleteUsers(req.userId); + if (!result) { + return res.status(404).json({ ok: false, message: "Usuario no encontrado" }); + } + res.json({ ok: true, message: "Usuario eliminado exitosamente" }); + } catch (error) { + next(error); + } +}) +export default userRouter; \ No newline at end of file diff --git a/src/services/users.service.ts b/src/services/users.service.ts index e69de29..88aa513 100644 --- a/src/services/users.service.ts +++ b/src/services/users.service.ts @@ -0,0 +1,19 @@ +import { User} from "../models/auth"; +import * as userDB from "../data/users.data"; + +export async function getUsers(id: number): Promise { + return await userDB.getUser(id); +} + +export async function updateUsers(id: number, user: User){ + const updatedUser = { + id, + fieldsToUpdate: user + } +const result: User = await userDB.updateUser(updatedUser); +return result +} + +export async function deleteUsers(id: number): Promise { + return await userDB.deleteUser(id); +} \ No newline at end of file From e062e873a4661f4df1a4e432b7da5a14400c2999 Mon Sep 17 00:00:00 2001 From: carusi99 Date: Sat, 27 Apr 2024 10:12:15 -0500 Subject: [PATCH 13/37] create /like --- src/app.ts | 4 ++ src/data/likes.data.ts | 45 ++++++++++++++++++ src/data/posts.data.ts | 74 ++++++++++++++++++++++++++++ src/data/users.data.ts | 6 ++- src/models/posts.ts | 6 --- src/routers/likes.routers.ts | 30 ++++++++++++ src/routers/posts.routers.ts | 90 ++++++++++++++++++++++++++++++++++- src/routers/users.routers.ts | 33 +++++++------ src/services/likes.service.ts | 21 ++++++++ src/services/posts.service.ts | 32 +++++++++++++ src/services/users.service.ts | 2 +- 11 files changed, 318 insertions(+), 25 deletions(-) diff --git a/src/app.ts b/src/app.ts index 50b9224..20dc756 100644 --- a/src/app.ts +++ b/src/app.ts @@ -3,6 +3,7 @@ import { configDotenv } from "dotenv"; import authRouter from "./routers/auth-router" import postsRouter from "./routers/posts.routers" import userRouter from "./routers/users.routers"; +import likeRouter from "./routers/likes.routers"; if (process.env["NODE_ENV"] === "test") { configDotenv({ path: ".env.test" }); @@ -14,6 +15,9 @@ export const app = express(); app.use(express.json()); + app.use(authRouter); app.use(postsRouter) app.use(userRouter); +app.use(likeRouter) + diff --git a/src/data/likes.data.ts b/src/data/likes.data.ts index e69de29..7586248 100644 --- a/src/data/likes.data.ts +++ b/src/data/likes.data.ts @@ -0,0 +1,45 @@ +import { Post } from '../models/posts'; +import * as db from "../db"; + +export async function likePost(postId: number): Promise { + try { + // Verificar si el post existe + const postExistsQuery = 'SELECT * FROM posts WHERE id = $1'; + const postExistsParams = [postId]; + const { rows: existingPosts } = await db.query(postExistsQuery, postExistsParams); + + if (existingPosts.length === 0) { + throw new Error('El post especificado no existe'); + } + + // Actualizar el recuento de likes + const query = 'UPDATE posts SET likes_count = likes_count + 1 WHERE id = $1 RETURNING *'; + + const params = [postId]; + const { rows } = await db.query(query, params); + return rows[0]; + } catch (error) { + throw error; + } +} + +export async function unlikePost(postId: number): Promise { + try { + // Verificar si el post existe + const postExistsQuery = 'SELECT * FROM posts WHERE id = $1'; + const postExistsParams = [postId]; + const { rows: existingPosts } = await db.query(postExistsQuery, postExistsParams); + + if (existingPosts.length === 0) { + throw new Error('El post especificado no existe'); + } + + // Actualizar el recuento de likes + const query = 'UPDATE posts SET likes_count = likes_count - 1 WHERE id = $1 RETURNING *'; + const params = [postId]; + const { rows } = await db.query(query, params); + return rows[0]; + } catch (error) { + throw error; + } +} diff --git a/src/data/posts.data.ts b/src/data/posts.data.ts index 9c4b5ab..c201949 100644 --- a/src/data/posts.data.ts +++ b/src/data/posts.data.ts @@ -1,4 +1,5 @@ import * as db from "../db"; +import { ApiError } from "../middlewares/error"; import { Post } from "../models/posts"; import { PostFilters } from "../models/posts"; @@ -35,6 +36,79 @@ export async function getTotalPosts(username?: string): Promise { const { rows } = await db.query(query, queryParams); return parseInt(rows[0].count, 10); } catch (error) { + console.error('Error al obtener el total de posts desde la base de datos:', error); throw new Error('Error al obtener el total de posts desde la base de datos'); } } + + +export async function getPostsByUsernameFromDatabase(username?: string): Promise { + try { + let query = ` + SELECT posts.* + FROM posts + JOIN users ON posts.userid = users.id + WHERE users.username = $1 + `; + const queryParams: any[] = [username]; + + const { rows } = await db.query(query, queryParams); + return rows; // Devuelve los posts encontrados + } catch (error) { + throw new ApiError('Error al obtener los posts desde la base de datos', 401); + } +} + + +export async function createPost(userId: number, content: string): Promise { + try { + const query = `INSERT INTO posts (userid, content, createdat, updatedat) VALUES ($1, $2, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) RETURNING *`; + const params = [userId, content]; + const { rows } = await db.query(query, params); + + return rows[0]; + } catch (error) { + throw new Error("Error al crear el post en la base de datos"); + } +} + +export async function editPost(postId: number, content: string): Promise { + try { + const query = `UPDATE posts SET content = $1, updatedat = CURRENT_TIMESTAMP WHERE id = $2 RETURNING *`; + const params = [content, postId]; + const { rows } = await db.query(query, params); + + if (rows.length === 0) { + throw new ApiError("El post no existe", 404); + } + + return rows[0]; + } catch (error) { + throw new ApiError("Error al editar el post en la base de datos", 500); + } +} + +export async function checkIfUserExists(username: string): Promise { + try { + const query = 'SELECT COUNT(*) AS count FROM users WHERE username = $1'; + const params = [username]; + const { rows } = await db.query(query, params); + const userCount = parseInt(rows[0].count, 10); + return userCount > 0; // Devuelve true si el contador es mayor que 0, indicando que el usuario existe + } catch (error) { + console.error('Error al verificar si el usuario existe:', error); + throw new Error('Error al verificar si el usuario existe'); + } +} + +export async function getPostById(postId: number) { + try { + const query = 'SELECT * FROM posts WHERE id = $1'; + const queryParams = [postId]; + const { rows } = await db.query(query, queryParams); + return rows[0]; // Devuelve el primer post encontrado, si existe + } catch (error) { + throw new Error('Error al obtener el post desde la base de datos'); + } +} + diff --git a/src/data/users.data.ts b/src/data/users.data.ts index 0116006..cec1c8c 100644 --- a/src/data/users.data.ts +++ b/src/data/users.data.ts @@ -1,8 +1,10 @@ -import { User, UserParams, Userupdate } from "../models/auth"; +import { User} from "../models/auth"; +import { Userupdate } from "../models/auth"; import * as db from "../db"; export async function getUser(id: number): Promise { - return (await db.query("SELECT * FROM users WHERE id = $1", [id])).rows[0]; + const result = await db.query("SELECT * FROM users WHERE id = $1", [id]) + return result.rows[0]; } diff --git a/src/models/posts.ts b/src/models/posts.ts index da7c761..6f92634 100644 --- a/src/models/posts.ts +++ b/src/models/posts.ts @@ -13,9 +13,3 @@ export interface Post { export interface PostFilters { username?: string; } - -export interface PostWithPagination extends Post { - // nextPage y previousPage ahora son definidos como 'number | null' - nextPage: number | null; - previousPage: number | null; -} diff --git a/src/routers/likes.routers.ts b/src/routers/likes.routers.ts index e69de29..04c535c 100644 --- a/src/routers/likes.routers.ts +++ b/src/routers/likes.routers.ts @@ -0,0 +1,30 @@ + +import express from "express"; +import {likePostInPostsData , unlikePostInPostsData } from "../services/likes.service"; +import { authenticateHandler } from "../middlewares/authenticate"; +const likeRouter = express.Router(); + +likeRouter.post('/posts/:postId/like', authenticateHandler, async (req, res) => { + try { + const postId = parseInt(req.params.postId); + const likedPost = await likePostInPostsData(postId); + res.status(200).json({ ok: true, data: likedPost }); + } catch (error) { + console.error('Error al dar like al post:', error); + res.status(400).json({ ok: false, message: 'Error al dar like al post' }); + } + }); + + // Eliminar like de un post + likeRouter.delete('/posts/:postId/like', authenticateHandler, async (req, res) => { + try { + const postId = parseInt(req.params.postId); + const unlikedPost = await unlikePostInPostsData(postId); + res.status(200).json({ ok: true, data: unlikedPost }); + } catch (error) { + console.error('Error al eliminar like del post:', error); + res.status(400).json({ ok: false, message: 'Error al eliminar like del post' }); + } + }); + + export default likeRouter; \ No newline at end of file diff --git a/src/routers/posts.routers.ts b/src/routers/posts.routers.ts index 3da2046..409815a 100644 --- a/src/routers/posts.routers.ts +++ b/src/routers/posts.routers.ts @@ -1,7 +1,8 @@ import express from "express"; -import { getPosts, getTotalPosts } from "../data/posts.data"; +import { getPosts, getTotalPosts, getPostsByUsernameFromDatabase, createPost, editPost, checkIfUserExists, getPostById } from "../data/posts.data"; import { pagination } from "../data/utils"; import { PostFilters } from "../models/posts"; +import { authenticateHandler } from "../middlewares/authenticate"; const postsRouter = express.Router(); @@ -47,4 +48,91 @@ postsRouter.get('/', async (req, res) => { } }); +postsRouter.get('/:username', async (req, res) => { + try { + const { username } = req.params; + const { page = 1, limit = 10, orderBy = 'createdAt', order = 'asc' } = req.query; + const pageInt = parseInt(page as string, 10); + const limitInt = parseInt(limit as string, 10); + + // Verificar si el usuario existe + const userExists = await checkIfUserExists(username); + + if (userExists) { + const posts = await getPostsByUsernameFromDatabase(username); + + if (posts) { // Verificar si se devolvieron posts + res.status(200).json({ + ok: true, + data: posts, + pagination: { + page: pageInt, + pageSize: limitInt, + nextPage: posts.length === limitInt ? pageInt + 1 : null, + previousPage: pageInt > 1 ? pageInt - 1 : null + } + }); + } else { + res.status(404).json({ ok: false, message: 'No se encontraron posts para el usuario especificado' }); + } + } else { + res.status(404).json({ ok: false, message: 'El usuario especificado no existe' }); + } + } catch (error) { + console.error('Error al obtener los posts:', error); + res.status(500).json({ ok: false, message: 'Error al obtener los posts' }); + } +}); + + +postsRouter.post("/posts", authenticateHandler, async (req, res) => { + try { + const { content } = req.body; + + // Verificar si el usuario está autenticado + if (!req.userId) { + return res.status(401).json({ ok: false, message: "Usuario no autenticado" }); + } + + // Verificar si se proporciona el contenido del post + if (!content || content.trim() === "") { + return res.status(400).json({ ok: false, message: "El contenido del post no puede estar vacío" }); + } + + // Crear el nuevo post si el usuario está autenticado + const newPost = await createPost(req.userId, content); + + // Responder con el nuevo post creado + res.status(201).json({ ok: true, data: newPost }); + } catch (error) { + console.error("Error al crear el post:", error); + res.status(500).json({ ok: false, message: "Error al crear el post" }); + } +}); + + +postsRouter.patch("/:id", authenticateHandler, async (req, res) => { + const postId = parseInt(req.params.id); + const { content } = req.body; + + try { + // Verificar si el post existe + const existingPost = await getPostById(postId); + if (!existingPost) { + return res.status(404).json({ ok: false, message: "El post no existe" }); + } + + if (!content || content.trim() === "") { + return res.status(400).json({ ok: false, message: "El contenido actualizado del post no puede estar vacío" }); + } + + const updatedPost = await editPost(postId, content); + + res.status(200).json({ ok: true, data: updatedPost }); + } catch (error) { + console.error("Error al editar el post:", error); + res.status(500).json({ ok: false, message: "Error al editar el post" }); + } +}); + export default postsRouter; diff --git a/src/routers/users.routers.ts b/src/routers/users.routers.ts index 5c3b2a3..6928487 100644 --- a/src/routers/users.routers.ts +++ b/src/routers/users.routers.ts @@ -1,4 +1,4 @@ -import express from "express"; +import express, { json } from "express"; import { getUsers, updateUsers, deleteUsers } from "../services/users.service"; import { ApiError } from "../middlewares/error"; import { authenticateHandler } from "../middlewares/authenticate"; @@ -15,18 +15,21 @@ userRouter.get("/me", authenticateHandler, async (req, res, next) => { if (!user) { return res.status(404).json({ ok: false, message: "Usuario no encontrado" }); } - res.json({ - ok: true, - data: { - id: user.id, - username: user.username, - firstName: user.firstname, - email: user.email, - role: user.role, - createdAt: user.createdat, - updatedAt: user.updatedat - } - }); + // Verificar si user está definido antes de acceder a sus propiedades + if (user) { + res.json({ + ok: true, + data: { + id: user.id, + username: user.username, + firstName: user.firstname, + email: user.email, + role: user.role, + createdAt: user.createdat, + updatedAt: user.updatedat + } + }); + } } catch (error) { next(error); } @@ -39,10 +42,10 @@ userRouter.patch("/me", authenticateHandler, async (req, res, next) => { try { const user: User = req.body; const updatedUser = await updateUsers(req.userId, user); - if (!user) { + if (!updatedUser) { return res.status(404).json({ ok: false, message: "Usuario no encontrado" }); } - res.json({ + res.status(200).json({ ok: true, data: { id: user.id, diff --git a/src/services/likes.service.ts b/src/services/likes.service.ts index e69de29..95f1d2d 100644 --- a/src/services/likes.service.ts +++ b/src/services/likes.service.ts @@ -0,0 +1,21 @@ +import { Post } from "../models/posts"; +import * as db from "../data/likes.data"; + +export async function likePostInPostsData(postId: number): Promise { + try { + const likedPost = await db.likePost(postId); + return likedPost; + } catch (error) { + throw error; + } + } + + // Eliminar like de un post + export async function unlikePostInPostsData(postId: number): Promise { + try { + const unlikedPost = await db.unlikePost(postId); + return unlikedPost; + } catch (error) { + throw error; + } + } diff --git a/src/services/posts.service.ts b/src/services/posts.service.ts index 31253fd..a2a523c 100644 --- a/src/services/posts.service.ts +++ b/src/services/posts.service.ts @@ -2,6 +2,7 @@ import { Post, PostFilters } from '../models/posts'; import { ApiError } from '../middlewares/error'; import * as postData from '../data/posts.data'; +import { createPost, getPostById, getPostsByUsernameFromDatabase} from '../data/posts.data'; export async function getPostsbyUsername(filters: PostFilters = {}, orderBy: string = 'createdAt', order: string = 'asc', page: number = 1, limit: number = 10): Promise { try { @@ -11,3 +12,34 @@ export async function getPostsbyUsername(filters: PostFilters = {}, orderBy: str throw new ApiError('Error al obtener los posts desde la base de datos', 400); } } + +export async function getPostsUsername(username: string): Promise { + try { + const posts = await getPostsByUsernameFromDatabase(username); + return posts; + } catch (error) { + throw new ApiError('Error al obtener los posts desde la base de datos', 400); + } +} + +export async function createNewPost(userId: number, content: string): Promise { + try { + const newPost = await createPost(userId, content); + return newPost; + } catch (error) { + throw new Error("Error al crear el nuevo post"); + } +} + +// Editar un post existente +export async function getPost(postId: number) { + try { + const post = await getPostById(postId); + if (!post) { + throw new Error('El post no existe'); + } + return post; + } catch (error) { + throw new Error('Error al obtener el post'); + } +} diff --git a/src/services/users.service.ts b/src/services/users.service.ts index 88aa513..a16c079 100644 --- a/src/services/users.service.ts +++ b/src/services/users.service.ts @@ -11,7 +11,7 @@ export async function updateUsers(id: number, user: User){ fieldsToUpdate: user } const result: User = await userDB.updateUser(updatedUser); -return result +return result; } export async function deleteUsers(id: number): Promise { From ffb8f12fefd143c58453e583c77adb233dc797a0 Mon Sep 17 00:00:00 2001 From: carusi99 Date: Sat, 27 Apr 2024 11:06:18 -0500 Subject: [PATCH 14/37] arrangements --- src/data/auth-data.ts | 15 +++++++++------ src/data/likes.data.ts | 6 ++++-- src/data/posts.data.ts | 24 ++++++++++++++---------- src/models/user.models.ts | 0 src/routers/auth-router.ts | 4 +--- src/routers/likes.routers.ts | 5 ++++- src/routers/posts.routers.ts | 7 +++++-- src/services/auth.service.ts | 1 + 8 files changed, 38 insertions(+), 24 deletions(-) delete mode 100644 src/models/user.models.ts diff --git a/src/data/auth-data.ts b/src/data/auth-data.ts index 84dd062..39010a7 100644 --- a/src/data/auth-data.ts +++ b/src/data/auth-data.ts @@ -2,6 +2,8 @@ import * as db from "../db"; import { ApiError } from "../middlewares/error"; import { UserParams, User } from "../models/auth"; + +//CREA UN NUEVO USUARIO export async function createUsers(user: UserParams): Promise { const now = new Date(); const query = @@ -14,12 +16,12 @@ export async function createUsers(user: UserParams): Promise { const queryParams = [ user.username, user.password, - user.email || '', // Reemplazar null con un valor por defecto, como una cadena vacía '' - user.firstname || '', // Reemplazar null con un valor por defecto - user.lastname || '', // Reemplazar null con un valor por defecto + user.email || '', + user.firstname || '', + user.lastname || '', user.role, - now.toISOString(), // Convertir Date a string - now.toISOString(), // Convertir Date a string + now.toISOString(), + now.toISOString(), ]; try { @@ -34,7 +36,7 @@ export async function createUsers(user: UserParams): Promise { return { id: result.rows[0].id, username: result.rows[0].username, - password: '', // Puedes inicializarla con un valor vacío o cualquier otro valor predeterminado + password: '', firstname: result.rows[0].firstName, lastname: result.rows[0].lastName, role: result.rows[0].role, @@ -47,6 +49,7 @@ export async function createUsers(user: UserParams): Promise { } +//OBTIENE EL USUARIO POR SU NOMBRE DE USERNAME export async function getUserByUsername( username: string ): Promise { diff --git a/src/data/likes.data.ts b/src/data/likes.data.ts index 7586248..c3ad806 100644 --- a/src/data/likes.data.ts +++ b/src/data/likes.data.ts @@ -1,6 +1,7 @@ import { Post } from '../models/posts'; import * as db from "../db"; +//DAR LIKE A UN POST export async function likePost(postId: number): Promise { try { // Verificar si el post existe @@ -12,7 +13,7 @@ export async function likePost(postId: number): Promise { throw new Error('El post especificado no existe'); } - // Actualizar el recuento de likes + // Actualizar likes const query = 'UPDATE posts SET likes_count = likes_count + 1 WHERE id = $1 RETURNING *'; const params = [postId]; @@ -23,6 +24,7 @@ export async function likePost(postId: number): Promise { } } +//ELIMINAR LIKE export async function unlikePost(postId: number): Promise { try { // Verificar si el post existe @@ -34,7 +36,7 @@ export async function unlikePost(postId: number): Promise { throw new Error('El post especificado no existe'); } - // Actualizar el recuento de likes + // Actualizar likes const query = 'UPDATE posts SET likes_count = likes_count - 1 WHERE id = $1 RETURNING *'; const params = [postId]; const { rows } = await db.query(query, params); diff --git a/src/data/posts.data.ts b/src/data/posts.data.ts index c201949..c15622a 100644 --- a/src/data/posts.data.ts +++ b/src/data/posts.data.ts @@ -3,6 +3,8 @@ import { ApiError } from "../middlewares/error"; import { Post } from "../models/posts"; import { PostFilters } from "../models/posts"; + +//OBTENER POSTS export async function getPosts(page: number, limit: number, filters: PostFilters = {}, orderBy: string = 'createdAt', order: string = 'asc'): Promise { try { let query = 'SELECT * FROM posts'; @@ -19,10 +21,10 @@ export async function getPosts(page: number, limit: number, filters: PostFilters const { rows } = await db.query(query, queryParams); return rows; } catch (error) { - throw new Error('Error al obtener los posts desde la base de datos'); + throw new ApiError('Error al obtener los posts desde la base de datos', 401); } } - +//OBTENER TOTAL DE POSTS export async function getTotalPosts(username?: string): Promise { try { let query = 'SELECT COUNT(*) FROM posts'; @@ -37,11 +39,11 @@ export async function getTotalPosts(username?: string): Promise { return parseInt(rows[0].count, 10); } catch (error) { console.error('Error al obtener el total de posts desde la base de datos:', error); - throw new Error('Error al obtener el total de posts desde la base de datos'); + throw new ApiError('Error al obtener el total de posts desde la base de datos', 400); } } - +//OBTENER POSTS POR USUARIO export async function getPostsByUsernameFromDatabase(username?: string): Promise { try { let query = ` @@ -55,11 +57,11 @@ export async function getPostsByUsernameFromDatabase(username?: string): Promise const { rows } = await db.query(query, queryParams); return rows; // Devuelve los posts encontrados } catch (error) { - throw new ApiError('Error al obtener los posts desde la base de datos', 401); + throw new ApiError('Error al obtener los posts del usuario', 401); } } - +//CREAR POST export async function createPost(userId: number, content: string): Promise { try { const query = `INSERT INTO posts (userid, content, createdat, updatedat) VALUES ($1, $2, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) RETURNING *`; @@ -68,10 +70,11 @@ export async function createPost(userId: number, content: string): Promise return rows[0]; } catch (error) { - throw new Error("Error al crear el post en la base de datos"); + throw new ApiError("Error al crear el post en la base de datos", 400); } } +//EDITAR POST export async function editPost(postId: number, content: string): Promise { try { const query = `UPDATE posts SET content = $1, updatedat = CURRENT_TIMESTAMP WHERE id = $2 RETURNING *`; @@ -87,7 +90,7 @@ export async function editPost(postId: number, content: string): Promise { throw new ApiError("Error al editar el post en la base de datos", 500); } } - +//VERIFICAR SI EL USUARIO EXISTE export async function checkIfUserExists(username: string): Promise { try { const query = 'SELECT COUNT(*) AS count FROM users WHERE username = $1'; @@ -97,10 +100,11 @@ export async function checkIfUserExists(username: string): Promise { return userCount > 0; // Devuelve true si el contador es mayor que 0, indicando que el usuario existe } catch (error) { console.error('Error al verificar si el usuario existe:', error); - throw new Error('Error al verificar si el usuario existe'); + throw new ApiError('Error al verificar si el usuario existe', 400); } } +//BUSCAR POST POR ID export async function getPostById(postId: number) { try { const query = 'SELECT * FROM posts WHERE id = $1'; @@ -108,7 +112,7 @@ export async function getPostById(postId: number) { const { rows } = await db.query(query, queryParams); return rows[0]; // Devuelve el primer post encontrado, si existe } catch (error) { - throw new Error('Error al obtener el post desde la base de datos'); + throw new ApiError('Error al obtener el post desde la base de datos', 400); } } diff --git a/src/models/user.models.ts b/src/models/user.models.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/routers/auth-router.ts b/src/routers/auth-router.ts index 27ecb83..cbd893f 100644 --- a/src/routers/auth-router.ts +++ b/src/routers/auth-router.ts @@ -3,13 +3,12 @@ import { validationHandler } from "../middlewares/validation"; import { userSchema } from "../models/auth"; import { createUser, validateCredentials } from "../services/auth.service"; import jwt from "jsonwebtoken"; +import { ApiError } from "../middlewares/error"; const jwtSecret = "ultra-secret"; - const authRouter = express.Router(); //POST/register: - authRouter.post( "/signup", validationHandler(userSchema), @@ -32,7 +31,6 @@ authRouter.post( ); //POST/Login: - authRouter.post("/login", async (req, res, next) => { try { const user = await validateCredentials(req.body); diff --git a/src/routers/likes.routers.ts b/src/routers/likes.routers.ts index 04c535c..44cf1bf 100644 --- a/src/routers/likes.routers.ts +++ b/src/routers/likes.routers.ts @@ -2,8 +2,11 @@ import express from "express"; import {likePostInPostsData , unlikePostInPostsData } from "../services/likes.service"; import { authenticateHandler } from "../middlewares/authenticate"; + + const likeRouter = express.Router(); +//DAR LIKE A UN POST likeRouter.post('/posts/:postId/like', authenticateHandler, async (req, res) => { try { const postId = parseInt(req.params.postId); @@ -15,7 +18,7 @@ likeRouter.post('/posts/:postId/like', authenticateHandler, async (req, res) => } }); - // Eliminar like de un post + //ELIMINAR LIKE likeRouter.delete('/posts/:postId/like', authenticateHandler, async (req, res) => { try { const postId = parseInt(req.params.postId); diff --git a/src/routers/posts.routers.ts b/src/routers/posts.routers.ts index 409815a..4a5e29e 100644 --- a/src/routers/posts.routers.ts +++ b/src/routers/posts.routers.ts @@ -6,6 +6,7 @@ import { authenticateHandler } from "../middlewares/authenticate"; const postsRouter = express.Router(); +//TRAE LOS POSTS postsRouter.get('/', async (req, res) => { try { const { page = 1, limit = 10, username, orderBy = 'createdAt', order = 'asc' } = req.query; @@ -48,6 +49,8 @@ postsRouter.get('/', async (req, res) => { } }); + +//TRAE LOS POSTS DE UN USUARIO postsRouter.get('/:username', async (req, res) => { try { const { username } = req.params; @@ -84,7 +87,7 @@ postsRouter.get('/:username', async (req, res) => { } }); - +//CREA UN NUEVO POST postsRouter.post("/posts", authenticateHandler, async (req, res) => { try { const { content } = req.body; @@ -110,7 +113,7 @@ postsRouter.post("/posts", authenticateHandler, async (req, res) => { } }); - +//EDITA UN POST postsRouter.patch("/:id", authenticateHandler, async (req, res) => { const postId = parseInt(req.params.id); const { content } = req.body; diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index e75b9b3..58edbb3 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -34,6 +34,7 @@ export async function createUser(data: UserParams): Promise { } } +//VALIDAR CREDENCIALES export async function validateCredentials( credentials: UserParams ): Promise { From 5dbc902e6b4e6fc385c9e91edfeac90e89d2b437 Mon Sep 17 00:00:00 2001 From: carusi99 Date: Sat, 27 Apr 2024 23:15:27 -0500 Subject: [PATCH 15/37] changes --- src/data/likes.data.ts | 77 ++++++++++++++++------------------ src/data/posts.data.ts | 78 +++++++++++++++++------------------ src/data/users.data.ts | 73 +++++++++++++++++++++----------- src/models/auth.ts | 4 +- src/models/like.ts | 6 +++ src/models/posts.ts | 3 +- src/models/users.ts | 39 ++++++++++++++++++ src/routers/likes.routers.ts | 48 +++++++++++++++++---- src/routers/posts.routers.ts | 6 +-- src/routers/users.routers.ts | 43 ++++++++++--------- src/services/likes.service.ts | 27 ++++++------ src/services/users.service.ts | 8 ++-- 12 files changed, 254 insertions(+), 158 deletions(-) create mode 100644 src/models/like.ts create mode 100644 src/models/users.ts diff --git a/src/data/likes.data.ts b/src/data/likes.data.ts index c3ad806..04c8aa1 100644 --- a/src/data/likes.data.ts +++ b/src/data/likes.data.ts @@ -1,47 +1,40 @@ -import { Post } from '../models/posts'; -import * as db from "../db"; +import { query } from '../db'; // Ajusta la ruta de tu módulo de base de datos +import { Like } from '../models/like'; +import { Post } from '../models/posts'; // Ajusta la importación según la ruta correcta -//DAR LIKE A UN POST -export async function likePost(postId: number): Promise { - try { - // Verificar si el post existe - const postExistsQuery = 'SELECT * FROM posts WHERE id = $1'; - const postExistsParams = [postId]; - const { rows: existingPosts } = await db.query(postExistsQuery, postExistsParams); - - if (existingPosts.length === 0) { - throw new Error('El post especificado no existe'); - } - - // Actualizar likes - const query = 'UPDATE posts SET likes_count = likes_count + 1 WHERE id = $1 RETURNING *'; +// DAR LIKE A UN POST +export async function createLikePost(like: Like): Promise { + const { userid, postid, createdat } = like; + const likeResult = await query( + "INSERT INTO likes (userId, postId, createdat) VALUES ($1, $2, $3)", + [userid, postid, createdat] + ); - const params = [postId]; - const { rows } = await db.query(query, params); - return rows[0]; - } catch (error) { - throw error; - } + const postResult = await query( + "SELECT p.id, p.content, p.createdat, p.updatedat, u.username, COALESCE(COUNT(pl.*), 0) AS likesCount FROM posts AS p JOIN users AS u ON u.id = p.userid LEFT JOIN likes AS pl ON pl.postid = p.id WHERE p.id = $1 GROUP BY p.id, u.username", + [postid] + ); + + return { + ...postResult.rows[0], + like: likeResult.rows[0], +  }; } -//ELIMINAR LIKE -export async function unlikePost(postId: number): Promise { - try { - // Verificar si el post existe - const postExistsQuery = 'SELECT * FROM posts WHERE id = $1'; - const postExistsParams = [postId]; - const { rows: existingPosts } = await db.query(postExistsQuery, postExistsParams); - - if (existingPosts.length === 0) { - throw new Error('El post especificado no existe'); - } - - // Actualizar likes - const query = 'UPDATE posts SET likes_count = likes_count - 1 WHERE id = $1 RETURNING *'; - const params = [postId]; - const { rows } = await db.query(query, params); - return rows[0]; - } catch (error) { - throw error; - } +// ELIMINAR LIKE +export async function unlikePost( + postId: number, + userId: number +): Promise { + await query("DELETE FROM likes WHERE postid = $1 AND userid = $2", [ + postId, + userId, + ]); + + const postResult = await query( + "SELECT p.id, p.content, p.createdat, p.updatedat, u.username, COALESCE(COUNT(pl.*), 0) AS likesCount FROM posts AS p JOIN users AS u ON u.id = p.userid LEFT JOIN likes AS pl ON pl.postid = p.id WHERE p.id = $1 GROUP BY p.id, u.username", + [postId] + ); + + return postResult.rows[0] || null; } diff --git a/src/data/posts.data.ts b/src/data/posts.data.ts index c15622a..c102550 100644 --- a/src/data/posts.data.ts +++ b/src/data/posts.data.ts @@ -3,38 +3,24 @@ import { ApiError } from "../middlewares/error"; import { Post } from "../models/posts"; import { PostFilters } from "../models/posts"; - -//OBTENER POSTS export async function getPosts(page: number, limit: number, filters: PostFilters = {}, orderBy: string = 'createdAt', order: string = 'asc'): Promise { try { - let query = 'SELECT * FROM posts'; - const queryParams: any[] = []; - - if (filters.username) { - query += ' WHERE username = $1'; - queryParams.push(filters.username); - } - - query += ` ORDER BY ${orderBy} ${order}`; - query += ` LIMIT ${limit} OFFSET ${(page - 1) * limit}`; - - const { rows } = await db.query(query, queryParams); - return rows; + // Utiliza getPostsByUsernameFromDatabase para obtener los posts junto con su conteo de likes + return await getPostsByUsernameFromDatabase(filters.username as string, page, limit, orderBy, order); } catch (error) { throw new ApiError('Error al obtener los posts desde la base de datos', 401); } } -//OBTENER TOTAL DE POSTS export async function getTotalPosts(username?: string): Promise { try { let query = 'SELECT COUNT(*) FROM posts'; const queryParams: any[] = []; - + if (username) { query += ' WHERE username = $1'; queryParams.push(username); } - + const { rows } = await db.query(query, queryParams); return parseInt(rows[0].count, 10); } catch (error) { @@ -43,38 +29,47 @@ export async function getTotalPosts(username?: string): Promise { } } -//OBTENER POSTS POR USUARIO -export async function getPostsByUsernameFromDatabase(username?: string): Promise { +export async function getPostsByUsernameFromDatabase(username: string, page: number, limit: number, orderBy: string = 'createdAt', order: string = 'asc'): Promise { try { let query = ` - SELECT posts.* - FROM posts - JOIN users ON posts.userid = users.id - WHERE users.username = $1 - `; - const queryParams: any[] = [username]; - + SELECT p.id, p.content, p.createdat, p.updatedat, u.username, + COALESCE(SUM(CASE WHEN pl.id IS NOT NULL THEN 1 ELSE 0 END), 0) AS likeCount + FROM posts AS p + JOIN users AS u ON u.id=p.userid + LEFT JOIN likes AS pl ON pl.postid = p.id`; + + const queryParams: any[] = []; + + if (username) { + query += ' WHERE u.username = $1'; + queryParams.push(username); + } + + query += ` GROUP BY p.id, u.username`; + query += ` ORDER BY ${orderBy} ${order}`; + query += ` LIMIT ${limit} OFFSET ${(page - 1) * limit}`; + const { rows } = await db.query(query, queryParams); - return rows; // Devuelve los posts encontrados + return rows; } catch (error) { throw new ApiError('Error al obtener los posts del usuario', 401); } } -//CREAR POST + export async function createPost(userId: number, content: string): Promise { try { - const query = `INSERT INTO posts (userid, content, createdat, updatedat) VALUES ($1, $2, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) RETURNING *`; - const params = [userId, content]; - const { rows } = await db.query(query, params); + const result = await db.query( + `INSERT INTO posts (userid, content) VALUES ($1,$2) RETURNING id,content,createdat,updatedat,(SELECT u.username FROM users AS u WHERE u.id =$1) AS username, 0 AS likesCount`, + [userId, content] + ); - return rows[0]; + return result.rows[0]; } catch (error) { throw new ApiError("Error al crear el post en la base de datos", 400); } } -//EDITAR POST export async function editPost(postId: number, content: string): Promise { try { const query = `UPDATE posts SET content = $1, updatedat = CURRENT_TIMESTAMP WHERE id = $2 RETURNING *`; @@ -90,29 +85,32 @@ export async function editPost(postId: number, content: string): Promise { throw new ApiError("Error al editar el post en la base de datos", 500); } } -//VERIFICAR SI EL USUARIO EXISTE + export async function checkIfUserExists(username: string): Promise { try { const query = 'SELECT COUNT(*) AS count FROM users WHERE username = $1'; const params = [username]; const { rows } = await db.query(query, params); const userCount = parseInt(rows[0].count, 10); - return userCount > 0; // Devuelve true si el contador es mayor que 0, indicando que el usuario existe + return userCount > 0; } catch (error) { console.error('Error al verificar si el usuario existe:', error); throw new ApiError('Error al verificar si el usuario existe', 400); } } -//BUSCAR POST POR ID -export async function getPostById(postId: number) { +export async function getPostById(postId: number): Promise { try { const query = 'SELECT * FROM posts WHERE id = $1'; const queryParams = [postId]; const { rows } = await db.query(query, queryParams); - return rows[0]; // Devuelve el primer post encontrado, si existe + + if (rows.length === 0) { + throw new ApiError('El post no existe', 404); + } + + return rows[0]; } catch (error) { throw new ApiError('Error al obtener el post desde la base de datos', 400); } } - diff --git a/src/data/users.data.ts b/src/data/users.data.ts index cec1c8c..c465450 100644 --- a/src/data/users.data.ts +++ b/src/data/users.data.ts @@ -1,34 +1,61 @@ -import { User} from "../models/auth"; -import { Userupdate } from "../models/auth"; +import { User} from "../models/users"; +import { UpdateUserParams } from "../models/users"; import * as db from "../db"; +import { ApiError } from "../middlewares/error"; + +//OBTIENE EL USUARIO POR SU ID export async function getUser(id: number): Promise { - const result = await db.query("SELECT * FROM users WHERE id = $1", [id]) - return result.rows[0]; + try { + const query = "SELECT * FROM users WHERE id = $1"; + const { rows } = await db.query(query, [id]); + + // Verificar si se encontraron resultados + if (rows.length > 0) { + return rows[0]; + } else { + return undefined; // Devolver undefined en lugar de null + } + } catch (error) { + console.error('Error al obtener el usuario:', error); + throw new ApiError('Error al obtener el usuario', 500); } - +} + - export async function updateUser({ - id, - fieldsToUpdate, - }: Userupdate): Promise { - if (!id || Object.keys(fieldsToUpdate).length === 0) { - throw new Error("No se proporcionaron datos para actualizar"); - } - const fields = ["email", "firstname", "lastname"] - const validFields = Object.keys(fieldsToUpdate).filter( - (field) => fields.includes(field) - ) - const setClauses = validFields.map((field, index) => `${field} = $${index + 1}`) - const updateQuery = `UPDATE users SET ${setClauses.join(", ")} WHERE id = $${ - validFields.length + 1 - } RETURNING *`; - const params = [...validFields.map((field) => fieldsToUpdate[field]), id]; - const result = await db.query(updateQuery, params); - return result.rows[0]; // Devuelve el usuario actualizado +//ACTUALIZA UN USUARIO + +// Función para actualizar un usuario en la base de datos +export async function updateUser(userId: number, newData: UpdateUserParams): Promise { + try { + const query = ` + UPDATE users + SET username = $1, email = $2 + WHERE id = $3 + RETURNING *;`; // Consulta SQL para actualizar el usuario y devolver los datos actualizados + + const username = newData.username ?? ''; // Si newData.username es undefined, se asigna una cadena vacía +const email = newData.email ?? ''; // Si newData.email es undefined, se asigna una cadena vacía + +const params: (string | number | boolean)[] = [username, email, userId]; + + // Parámetros para la consulta SQL + const { rows } = await db.query(query, params); // Ejecuta la consulta SQL con los parámetros y obtén el resultado + + // Devuelve el usuario actualizado + return rows[0]; + } catch (error) { + // En caso de error, registra el error y lanza una instancia de ApiError + console.error('Error al actualizar el usuario:', error); + throw new ApiError('Error al actualizar el usuario', 500); } +} + + + + //ELIMINA UN USUARIO export async function deleteUser(id: number): Promise { return (await db.query("DELETE FROM users WHERE id = $1 RETURNING *", [id])).rows[0]; } \ No newline at end of file diff --git a/src/models/auth.ts b/src/models/auth.ts index ba4c3cb..f25c93a 100644 --- a/src/models/auth.ts +++ b/src/models/auth.ts @@ -48,4 +48,6 @@ export type User = UserParams & { id: number }; export interface Userupdate{ id: number; fieldsToUpdate: Record; -} \ No newline at end of file +} + + diff --git a/src/models/like.ts b/src/models/like.ts new file mode 100644 index 0000000..a96040a --- /dev/null +++ b/src/models/like.ts @@ -0,0 +1,6 @@ +export interface Like { + id?: number; + postid: number; + userid: number; + createdat: string; + } \ No newline at end of file diff --git a/src/models/posts.ts b/src/models/posts.ts index 6f92634..f316476 100644 --- a/src/models/posts.ts +++ b/src/models/posts.ts @@ -1,10 +1,8 @@ export interface Post { - id: number; content: string; createdAt: Date; updatedAt: Date; username: string; - likesCount: number; nextPage: number | null; // Cambiado de 'null' a 'number | null' previousPage: number | null; // Cambiado de 'null' a 'number | null' } @@ -13,3 +11,4 @@ export interface Post { export interface PostFilters { username?: string; } + diff --git a/src/models/users.ts b/src/models/users.ts new file mode 100644 index 0000000..ae3ac57 --- /dev/null +++ b/src/models/users.ts @@ -0,0 +1,39 @@ +import { z } from "zod"; + +export const userSchema = z.object({ + username: z + .string({ + required_error: "Username es requerido", + invalid_type_error: "Username debe ser un string", + }) + .optional(), + email: z + .string({ + invalid_type_error: "Email debe ser un string", + }) + .email({ message: "El correo electrónico debe ser valido" }) + .optional(), + firstname: z + .string({ + invalid_type_error: "firstName debe ser un string", + }) + .optional(), + lastname: z + .string({ + invalid_type_error: "lastName debe ser un string", + }) + .optional(), + createdat: z.string().optional(), + updatedat: z.string().optional(), +}); + +export type UserParams = z.infer; + +export type User = UserParams & { id: number }; + +export interface UpdateUserParams { + id: number; + fieldsToUpdate: User; + email?: string; + username?: string; + } diff --git a/src/routers/likes.routers.ts b/src/routers/likes.routers.ts index 44cf1bf..b429e9f 100644 --- a/src/routers/likes.routers.ts +++ b/src/routers/likes.routers.ts @@ -1,7 +1,10 @@ - import express from "express"; -import {likePostInPostsData , unlikePostInPostsData } from "../services/likes.service"; +import {likePostInPostsData } from "../services/likes.service"; +import { unlikePostInPostsData as unlikePostInPostsDataService } from "../services/likes.service"; import { authenticateHandler } from "../middlewares/authenticate"; +import { unlikePost } from "../data/likes.data"; +import { Post } from "../models/posts"; + const likeRouter = express.Router(); @@ -9,9 +12,20 @@ const likeRouter = express.Router(); //DAR LIKE A UN POST likeRouter.post('/posts/:postId/like', authenticateHandler, async (req, res) => { try { - const postId = parseInt(req.params.postId); - const likedPost = await likePostInPostsData(postId); - res.status(200).json({ ok: true, data: likedPost }); + const userId = req.userId; + const { postId } = req.params; + const createdat = new Date().toISOString(); + const like = { + postid: Number(postId), + userid: Number(userId), + createdat + + } + const likedPost = await likePostInPostsData(like); + res.status(200).json({ + ok: true, + data: likedPost + }); } catch (error) { console.error('Error al dar like al post:', error); res.status(400).json({ ok: false, message: 'Error al dar like al post' }); @@ -19,11 +33,29 @@ likeRouter.post('/posts/:postId/like', authenticateHandler, async (req, res) => }); //ELIMINAR LIKE + export async function unlikePostInPostsData( + postId: number, + userId: number + ): Promise { + return unlikePost(postId, userId); + } + likeRouter.delete('/posts/:postId/like', authenticateHandler, async (req, res) => { try { - const postId = parseInt(req.params.postId); - const unlikedPost = await unlikePostInPostsData(postId); - res.status(200).json({ ok: true, data: unlikedPost }); + const userId = req.userId; + const { postId } = req.params; + const unlikedPost = await unlikePostInPostsDataService (Number(postId), Number(userId)); + + if (!unlikedPost) { + // Si no se encontró el post, devuelve un error 404 + res.status(404).json({ ok: false, message: 'Post not found' }); + return; + } + + res.status(200).json({ + ok: true, + data: unlikedPost, + }); } catch (error) { console.error('Error al eliminar like del post:', error); res.status(400).json({ ok: false, message: 'Error al eliminar like del post' }); diff --git a/src/routers/posts.routers.ts b/src/routers/posts.routers.ts index 4a5e29e..01717b0 100644 --- a/src/routers/posts.routers.ts +++ b/src/routers/posts.routers.ts @@ -62,7 +62,7 @@ postsRouter.get('/:username', async (req, res) => { const userExists = await checkIfUserExists(username); if (userExists) { - const posts = await getPostsByUsernameFromDatabase(username); + const posts = await getPostsByUsernameFromDatabase(username, pageInt, limitInt, orderBy as string, order as string); if (posts) { // Verificar si se devolvieron posts res.status(200).json({ @@ -113,8 +113,8 @@ postsRouter.post("/posts", authenticateHandler, async (req, res) => { } }); -//EDITA UN POST -postsRouter.patch("/:id", authenticateHandler, async (req, res) => { +//EDITA UN POST +postsRouter.patch("/posts/:id", authenticateHandler, async (req, res) => { const postId = parseInt(req.params.id); const { content } = req.body; diff --git a/src/routers/users.routers.ts b/src/routers/users.routers.ts index 6928487..a16cd00 100644 --- a/src/routers/users.routers.ts +++ b/src/routers/users.routers.ts @@ -2,36 +2,36 @@ import express, { json } from "express"; import { getUsers, updateUsers, deleteUsers } from "../services/users.service"; import { ApiError } from "../middlewares/error"; import { authenticateHandler } from "../middlewares/authenticate"; -import { User } from "../models/auth"; +import { User } from "../models/users"; const userRouter = express.Router(); userRouter.get("/me", authenticateHandler, async (req, res, next) => { - if (req.userId === undefined) { - return res.status(401).json({ ok: false, message: "No autorizado" }); - } try { + if (req.userId === undefined) { + return res.status(401).json({ ok: false, message: "No autorizado" }); + } + const user = await getUsers(req.userId); + if (!user) { return res.status(404).json({ ok: false, message: "Usuario no encontrado" }); } - // Verificar si user está definido antes de acceder a sus propiedades - if (user) { - res.json({ - ok: true, - data: { - id: user.id, - username: user.username, - firstName: user.firstname, - email: user.email, - role: user.role, - createdAt: user.createdat, - updatedAt: user.updatedat - } - }); - } + + // Devuelve los datos del usuario si está definido + res.json({ + ok: true, + data: { + id: user.id, + username: user.username, + firstName: user.firstname, + email: user.email, + createdAt: user.createdat, + updatedAt: user.updatedat + } + }); } catch (error) { - next(error); + next(error); // Maneja cualquier error que ocurra durante la obtención del usuario } }); @@ -41,7 +41,7 @@ userRouter.patch("/me", authenticateHandler, async (req, res, next) => { } try { const user: User = req.body; - const updatedUser = await updateUsers(req.userId, user); + const updatedUser = await updateUsers(req.userId, req.body); if (!updatedUser) { return res.status(404).json({ ok: false, message: "Usuario no encontrado" }); } @@ -52,7 +52,6 @@ userRouter.patch("/me", authenticateHandler, async (req, res, next) => { username: user.username, firstName: user.firstname, email: user.email, - role: user.role, createdAt: user.createdat, updatedAt: user.updatedat } diff --git a/src/services/likes.service.ts b/src/services/likes.service.ts index 95f1d2d..a7c010c 100644 --- a/src/services/likes.service.ts +++ b/src/services/likes.service.ts @@ -1,21 +1,20 @@ import { Post } from "../models/posts"; -import * as db from "../data/likes.data"; +import { createLikePost, unlikePost } from "../data/likes.data"; +import { Like } from "../models/like"; -export async function likePostInPostsData(postId: number): Promise { - try { - const likedPost = await db.likePost(postId); +export async function likePostInPostsData(like: Like): Promise { + + const likedPost = await createLikePost(like); return likedPost; - } catch (error) { - throw error; - } - } + } + // Eliminar like de un post - export async function unlikePostInPostsData(postId: number): Promise { - try { - const unlikedPost = await db.unlikePost(postId); + export async function unlikePostInPostsData( + postId: number, + userId: number + ): Promise { + const unlikedPost = await unlikePost(postId, userId); return unlikedPost; - } catch (error) { - throw error; } - } + diff --git a/src/services/users.service.ts b/src/services/users.service.ts index a16c079..cb4c6f7 100644 --- a/src/services/users.service.ts +++ b/src/services/users.service.ts @@ -1,16 +1,18 @@ -import { User} from "../models/auth"; +import { User} from "../models/users"; import * as userDB from "../data/users.data"; export async function getUsers(id: number): Promise { - return await userDB.getUser(id); + const user = await userDB.getUser(id); + return user; // getUser ya devuelve undefined si no se encuentra ningún usuario } + export async function updateUsers(id: number, user: User){ const updatedUser = { id, fieldsToUpdate: user } -const result: User = await userDB.updateUser(updatedUser); +const result: User = await userDB.updateUser(id,updatedUser); return result; } From 9faee7aaab7e757a25de57963b7ad88caf577d30 Mon Sep 17 00:00:00 2001 From: carusi99 Date: Mon, 29 Apr 2024 17:32:55 -0500 Subject: [PATCH 16/37] create file .env test --- .env.test | 0 src/data/auth-data.ts | 3 --- src/routers/likes.routers.ts | 2 -- 3 files changed, 5 deletions(-) create mode 100644 .env.test diff --git a/.env.test b/.env.test new file mode 100644 index 0000000..e69de29 diff --git a/src/data/auth-data.ts b/src/data/auth-data.ts index 39010a7..8ef11c6 100644 --- a/src/data/auth-data.ts +++ b/src/data/auth-data.ts @@ -2,7 +2,6 @@ import * as db from "../db"; import { ApiError } from "../middlewares/error"; import { UserParams, User } from "../models/auth"; - //CREA UN NUEVO USUARIO export async function createUsers(user: UserParams): Promise { const now = new Date(); @@ -47,8 +46,6 @@ export async function createUsers(user: UserParams): Promise { throw new ApiError('Error al insertar el usuario', 400); } } - - //OBTIENE EL USUARIO POR SU NOMBRE DE USERNAME export async function getUserByUsername( username: string diff --git a/src/routers/likes.routers.ts b/src/routers/likes.routers.ts index b429e9f..ad31311 100644 --- a/src/routers/likes.routers.ts +++ b/src/routers/likes.routers.ts @@ -5,8 +5,6 @@ import { authenticateHandler } from "../middlewares/authenticate"; import { unlikePost } from "../data/likes.data"; import { Post } from "../models/posts"; - - const likeRouter = express.Router(); //DAR LIKE A UN POST From b4a9d0d8358e810b6f65e07c0c478f082f882d0e Mon Sep 17 00:00:00 2001 From: carusi99 Date: Mon, 29 Apr 2024 17:35:17 -0500 Subject: [PATCH 17/37] database test --- .env.test | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.env.test b/.env.test index e69de29..d3be6a7 100644 --- a/.env.test +++ b/.env.test @@ -0,0 +1,6 @@ +PGHOST=localhost +PGDATABASE=redes +PGPORT=5432 +PGUSER=postgres +PGPASSWORD=postgres +PGADMINDATABASE=postgres \ No newline at end of file From 7cbd45373a7ee1a5d93de3791c196dcf4a07f3ca Mon Sep 17 00:00:00 2001 From: carusi99 Date: Fri, 3 May 2024 17:24:35 -0500 Subject: [PATCH 18/37] me queries --- src/data/posts.data.ts | 42 ++++++++++++++++++++++++++++------- src/data/users.data.ts | 22 ++++-------------- src/data/utils.ts | 23 +++++++++++++++++++ src/routers/users.routers.ts | 35 +++++++++++++---------------- src/services/users.service.ts | 2 +- 5 files changed, 77 insertions(+), 47 deletions(-) diff --git a/src/data/posts.data.ts b/src/data/posts.data.ts index c102550..c36aa8e 100644 --- a/src/data/posts.data.ts +++ b/src/data/posts.data.ts @@ -2,15 +2,40 @@ import * as db from "../db"; import { ApiError } from "../middlewares/error"; import { Post } from "../models/posts"; import { PostFilters } from "../models/posts"; - -export async function getPosts(page: number, limit: number, filters: PostFilters = {}, orderBy: string = 'createdAt', order: string = 'asc'): Promise { - try { - // Utiliza getPostsByUsernameFromDatabase para obtener los posts junto con su conteo de likes - return await getPostsByUsernameFromDatabase(filters.username as string, page, limit, orderBy, order); - } catch (error) { - throw new ApiError('Error al obtener los posts desde la base de datos', 401); - } +import { filtering, sorting } from "./utils"; + +export async function getPosts( + page: number, + limit: number, + filters: PostFilters = {}, + orderBy: string = 'createdAt', + order: string = 'asc'): + Promise { + try { + // Utiliza getPostsByUsernameFromDatabase para obtener los posts junto con su conteo de likes + let query = "SELECT * FROM posts"; + const queryParams: (string | boolean | number)[] = []; + + // Filtering + query = filtering(query, filters, queryParams); + // Sorting + query += ` ORDER BY ${orderBy} ${order}`; + + // Pagination + if (page && limit) { + const offset = (page - 1) * limit; + query += ` LIMIT $${queryParams.length + 1} OFFSET $${queryParams.length + 2}`; + queryParams.push(limit, offset); + } + + const result = await db.query(query, queryParams); + return result.rows; + } catch (error) { + console.error('Error al obtener los posts:', error); + throw new ApiError('Error al obtener los posts', 500); + } } + export async function getTotalPosts(username?: string): Promise { try { let query = 'SELECT COUNT(*) FROM posts'; @@ -114,3 +139,4 @@ export async function getPostById(postId: number): Promise { throw new ApiError('Error al obtener el post desde la base de datos', 400); } } + diff --git a/src/data/users.data.ts b/src/data/users.data.ts index c465450..9645a2a 100644 --- a/src/data/users.data.ts +++ b/src/data/users.data.ts @@ -4,25 +4,11 @@ import * as db from "../db"; import { ApiError } from "../middlewares/error"; -//OBTIENE EL USUARIO POR SU ID -export async function getUser(id: number): Promise { - try { - const query = "SELECT * FROM users WHERE id = $1"; - const { rows } = await db.query(query, [id]); - - // Verificar si se encontraron resultados - if (rows.length > 0) { - return rows[0]; - } else { - return undefined; // Devolver undefined en lugar de null - } - } catch (error) { - console.error('Error al obtener el usuario:', error); - throw new ApiError('Error al obtener el usuario', 500); - } +//OBTIENE EL USUARIO POR EL TOKEN +export async function getUser(id: number): Promise { + const result = await db.query("SELECT * FROM users WHERE id=$1", [id]); + return result.rows[0]; } - - //ACTUALIZA UN USUARIO diff --git a/src/data/utils.ts b/src/data/utils.ts index 6408c12..ccc11a0 100644 --- a/src/data/utils.ts +++ b/src/data/utils.ts @@ -1,3 +1,5 @@ +import { PostFilters } from "../models/posts"; + export function pagination(page: number, limit: number): string { const offset = (page - 1) * limit; return `LIMIT ${limit} OFFSET ${offset}`; @@ -11,3 +13,24 @@ export function sorting(query: string, sort?: string): string { } return query; } + +export function filtering( + query: string, + filters: PostFilters, + queryParams: (string | boolean | number)[] +): string { + let updatedQuery = query; + Object.entries(filters).forEach(([key, value], index) => { + if (value) { + queryParams.push(value); + if (index === 0) { + updatedQuery += ` WHERE "${key}" = $${queryParams.length}`; + } else { + updatedQuery += ` AND "${key}" = $${queryParams.length}`; + } + } + }); + + return updatedQuery; +} + diff --git a/src/routers/users.routers.ts b/src/routers/users.routers.ts index a16cd00..79e59af 100644 --- a/src/routers/users.routers.ts +++ b/src/routers/users.routers.ts @@ -7,32 +7,27 @@ import { User } from "../models/users"; const userRouter = express.Router(); userRouter.get("/me", authenticateHandler, async (req, res, next) => { + if (req.userId === undefined) { + return next(new ApiError("Unauthorized", 401)); + } try { - if (req.userId === undefined) { - return res.status(401).json({ ok: false, message: "No autorizado" }); - } - - const user = await getUsers(req.userId); - - if (!user) { - return res.status(404).json({ ok: false, message: "Usuario no encontrado" }); - } - - // Devuelve los datos del usuario si está definido - res.json({ + const profile = await getUsers(req.userId); + return res.json({ ok: true, data: { - id: user.id, - username: user.username, - firstName: user.firstname, - email: user.email, - createdAt: user.createdat, - updatedAt: user.updatedat - } + id: profile.id, + username: profile.username, + email: profile.email, + firstName: profile.firstname, + lastName: profile.lastname, + createdAt: profile.createdat, + updatedAt: profile.updatedat, + }, }); } catch (error) { - next(error); // Maneja cualquier error que ocurra durante la obtención del usuario + next(new ApiError("Unauthorized", 401)); } + }); userRouter.patch("/me", authenticateHandler, async (req, res, next) => { diff --git a/src/services/users.service.ts b/src/services/users.service.ts index cb4c6f7..9747dbc 100644 --- a/src/services/users.service.ts +++ b/src/services/users.service.ts @@ -1,7 +1,7 @@ import { User} from "../models/users"; import * as userDB from "../data/users.data"; -export async function getUsers(id: number): Promise { +export async function getUsers(id: number): Promise { const user = await userDB.getUser(id); return user; // getUser ya devuelve undefined si no se encuentra ningún usuario } From 0ac33a4c86e0b7c8b2193d6f796762e370186719 Mon Sep 17 00:00:00 2001 From: carusi99 Date: Sat, 4 May 2024 17:52:56 -0500 Subject: [PATCH 19/37] file utils.ts TRUNCATE --- package-lock.json | 170 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 3 +- src/db/utils.ts | 8 +++ 3 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 src/db/utils.ts diff --git a/package-lock.json b/package-lock.json index 1db2411..cbe7703 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "@types/node": "^20.10.6", "@types/pg": "^8.10.9", "nodemon": "^3.0.2", + "supertest": "^7.0.0", "ts-node": "^10.9.2", "typescript": "^5.3.3" } @@ -500,6 +501,18 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -644,6 +657,18 @@ "color-support": "bin.js" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "9.5.0", "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", @@ -653,6 +678,15 @@ "node": "^12.20.0 || >=14" } }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -695,6 +729,12 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -725,6 +765,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -755,6 +804,16 @@ "node": ">=8" } }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -900,6 +959,12 @@ "node": ">=8.6.0" } }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -936,6 +1001,34 @@ "node": ">= 0.8" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formidable": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.1.tgz", + "integrity": "sha512-WJWKelbRHN41m5dumb0/k8TeAx7Id/y3a+Z7QfhxP/htI9Js5zYaEDtG8uMgG0vM0lOlqnmjE99/kfpOYi/0Og==", + "dev": true, + "dependencies": { + "dezalgo": "^1.0.4", + "hexoid": "^1.0.0", + "once": "^1.4.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -1155,6 +1248,15 @@ "node": ">= 0.4" } }, + "node_modules/hexoid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", + "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -2331,6 +2433,74 @@ "node": ">=8" } }, + "node_modules/superagent": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-9.0.2.tgz", + "integrity": "sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==", + "dev": true, + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.4", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^3.5.1", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/superagent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/superagent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/supertest": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.0.0.tgz", + "integrity": "sha512-qlsr7fIC0lSddmA3tzojvzubYxvlGtzumcdHgPwbFWMISQwL22MhM2Y3LNt+6w9Yyx7559VW5ab70dgphm8qQA==", + "dev": true, + "dependencies": { + "methods": "^1.1.2", + "superagent": "^9.0.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", diff --git a/package.json b/package.json index 47d3dda..39e59d8 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "dev": "nodemon src/index.ts", "build": "tsc", "start": "node build/index.js", - "test": "echo \"Error: no test specified\" && exit 1", + "test": "NODE_ENV=test vitest", "db:migrate": "ts-node src/db/scripts/dbMigrate.ts", "db:create": "ts-node src/db/scripts/dbCreate.ts", "db:drop": "ts-node src/db/scripts/dbDrop.ts", @@ -30,6 +30,7 @@ "@types/node": "^20.10.6", "@types/pg": "^8.10.9", "nodemon": "^3.0.2", + "supertest": "^7.0.0", "ts-node": "^10.9.2", "typescript": "^5.3.3" }, diff --git a/src/db/utils.ts b/src/db/utils.ts new file mode 100644 index 0000000..1cd0188 --- /dev/null +++ b/src/db/utils.ts @@ -0,0 +1,8 @@ +import { query } from "."; // Adjust the import path to your actual db module + +export const truncateTable = async (tableName: string): Promise => { + // La sentencia TRUNCATE TABLE es usada para borrar todos los registros de una tabla. + // La sentencia RESTART IDENTITY sirve para que se borre la secuencia de la tabla y comience en uno + // CASCADE opción que se asegura que cualquier referencia foranea tambien se borre. + await query(`TRUNCATE TABLE ${tableName} RESTART IDENTITY CASCADE`); +}; From 3824a6d7ba34e76d83c1012911de3b7ce6cd4a56 Mon Sep 17 00:00:00 2001 From: carusi99 Date: Sat, 4 May 2024 18:09:45 -0500 Subject: [PATCH 20/37] .env.test --- .env.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.test b/.env.test index d3be6a7..8dcfc65 100644 --- a/.env.test +++ b/.env.test @@ -1,5 +1,5 @@ PGHOST=localhost -PGDATABASE=redes +PGDATABASE=redes-test PGPORT=5432 PGUSER=postgres PGPASSWORD=postgres From f9a41328507ac808dc3193d564d4bc52bd5070b9 Mon Sep 17 00:00:00 2001 From: carusi99 Date: Sat, 4 May 2024 18:15:36 -0500 Subject: [PATCH 21/37] file dbSeed.ts --- src/db/scripts/dbSeed.ts | 46 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/db/scripts/dbSeed.ts diff --git a/src/db/scripts/dbSeed.ts b/src/db/scripts/dbSeed.ts new file mode 100644 index 0000000..1e2821f --- /dev/null +++ b/src/db/scripts/dbSeed.ts @@ -0,0 +1,46 @@ +import { configDotenv } from "dotenv"; +import { query, pool } from ".."; + +if (process.env["NODE_ENV"] === "test") { + configDotenv({ path: ".env.test" }); +} else { + configDotenv(); +} + +query(` +-- Insertar datos de ejemplo en la tabla Users +INSERT INTO Users (username, password, email, firstName, lastName, role, createdAt, updatedAt) +SELECT + 'user' || s.id, -- Genera un nombre de usuario concatenando una cadena con el ID + 'password' || s.id, -- Genera una contraseña concatenando una cadena con el ID + 'user' || s.id || '@example.com', -- Genera un correo electrónico concatenando una cadena con el ID + 'Nombre' || s.id, -- Genera un nombre concatenando una cadena con el ID + 'Apellido' || s.id, -- Genera un apellido concatenando una cadena con el ID + CASE + WHEN s.id % 5 = 0 THEN 'admin' + ELSE 'user' + END, -- Asigna roles de manera aleatoria + NOW(), -- Fecha y hora de creación + NOW() -- Fecha y hora de actualización +FROM generate_series(1, 50) AS s(id); + +-- Insertar datos de ejemplo en la tabla Posts +INSERT INTO Posts (userId, content, createdAt, updatedAt) +SELECT + (s.id % 50) + 1, -- ID de usuario aleatorio + 'Contenido del post ' || s.id, -- Genera un contenido de post concatenando una cadena con el ID + NOW(), -- Fecha y hora de creación + NOW() -- Fecha y hora de actualización +FROM generate_series(1, 100) AS s(id); + +-- Insertar datos de ejemplo en la tabla Likes +INSERT INTO Likes (postId, userId, createdAt) +SELECT + (s.id % 100) + 1, -- ID de post aleatorio + (s.id % 50) + 1, -- ID de usuario aleatorio + NOW() -- Fecha y hora del like +FROM generate_series(1, 200) AS s(id); + `).then(() => { + console.log("Products inserted"); + pool.end(); +}); From 5556a16afb58d478fbb2ae73408377db8e5997e1 Mon Sep 17 00:00:00 2001 From: carusi99 Date: Sat, 4 May 2024 18:25:26 -0500 Subject: [PATCH 22/37] file dbSeed.ts --- src/db/scripts/dbSeed.ts | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/db/scripts/dbSeed.ts b/src/db/scripts/dbSeed.ts index 1e2821f..cc49f4d 100644 --- a/src/db/scripts/dbSeed.ts +++ b/src/db/scripts/dbSeed.ts @@ -23,23 +23,6 @@ SELECT NOW(), -- Fecha y hora de creación NOW() -- Fecha y hora de actualización FROM generate_series(1, 50) AS s(id); - --- Insertar datos de ejemplo en la tabla Posts -INSERT INTO Posts (userId, content, createdAt, updatedAt) -SELECT - (s.id % 50) + 1, -- ID de usuario aleatorio - 'Contenido del post ' || s.id, -- Genera un contenido de post concatenando una cadena con el ID - NOW(), -- Fecha y hora de creación - NOW() -- Fecha y hora de actualización -FROM generate_series(1, 100) AS s(id); - --- Insertar datos de ejemplo en la tabla Likes -INSERT INTO Likes (postId, userId, createdAt) -SELECT - (s.id % 100) + 1, -- ID de post aleatorio - (s.id % 50) + 1, -- ID de usuario aleatorio - NOW() -- Fecha y hora del like -FROM generate_series(1, 200) AS s(id); `).then(() => { console.log("Products inserted"); pool.end(); From 2a50109e52c3316adf39f27508453b2b112dfa4f Mon Sep 17 00:00:00 2001 From: carusi99 Date: Sat, 4 May 2024 18:25:59 -0500 Subject: [PATCH 23/37] file dbSeed.ts table posts --- src/db/scripts/dbSeed.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/db/scripts/dbSeed.ts b/src/db/scripts/dbSeed.ts index cc49f4d..cc07ee0 100644 --- a/src/db/scripts/dbSeed.ts +++ b/src/db/scripts/dbSeed.ts @@ -23,6 +23,16 @@ SELECT NOW(), -- Fecha y hora de creación NOW() -- Fecha y hora de actualización FROM generate_series(1, 50) AS s(id); + +-- Insertar datos de ejemplo en la tabla Posts +INSERT INTO Posts (userId, content, createdAt, updatedAt) +SELECT + (s.id % 50) + 1, -- ID de usuario aleatorio + 'Contenido del post ' || s.id, -- Genera un contenido de post concatenando una cadena con el ID + NOW(), -- Fecha y hora de creación + NOW() -- Fecha y hora de actualización +FROM generate_series(1, 100) AS s(id); + `).then(() => { console.log("Products inserted"); pool.end(); From 98bff58b93dacbc2ffb2780c41a2599b366c0201 Mon Sep 17 00:00:00 2001 From: carusi99 Date: Sat, 4 May 2024 18:58:04 -0500 Subject: [PATCH 24/37] test users --- src/db/utils.ts | 2 +- src/tests/users.test.ts | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/db/utils.ts b/src/db/utils.ts index 1cd0188..4e6cd43 100644 --- a/src/db/utils.ts +++ b/src/db/utils.ts @@ -1,6 +1,6 @@ import { query } from "."; // Adjust the import path to your actual db module -export const truncateTable = async (tableName: string): Promise => { +export const truncateTables = async (tableName: string): Promise => { // La sentencia TRUNCATE TABLE es usada para borrar todos los registros de una tabla. // La sentencia RESTART IDENTITY sirve para que se borre la secuencia de la tabla y comience en uno // CASCADE opción que se asegura que cualquier referencia foranea tambien se borre. diff --git a/src/tests/users.test.ts b/src/tests/users.test.ts index e69de29..a2d45b3 100644 --- a/src/tests/users.test.ts +++ b/src/tests/users.test.ts @@ -0,0 +1,20 @@ +import { describe, beforeEach, it, expect } from "vitest"; +import request from "supertest"; +import { app } from "../app"; // Importa tu aplicación Express +import { truncateTables } from "../db/utils"; // Funciones auxiliares para configurar la base de datos +import * as db from "../db"; + +describe("Backend Testing", () => { + // Antes de cada prueba, limpiar y configurar la base de datos + beforeEach(async () => { + // Limpia todas las tablas relevantes + await truncateTables(["Users", "Posts", "Likes"]); + + // Inserta datos de prueba en las tablas + await db.query(` + INSERT INTO Users (username, password, email, firstName, lastName, role, createdAt, updatedAt) + VALUES ('user1', 'password1', 'user1@example.com', 'John', 'Doe', 'user', NOW(), NOW()), + ('user2', 'password2', NULL, 'Jane', NULL, 'admin', NOW(), NOW()); + `); + }); + From fa563e120682dfac82f350e59709d72fc102165e Mon Sep 17 00:00:00 2001 From: carusi99 Date: Sat, 4 May 2024 19:14:51 -0500 Subject: [PATCH 25/37] test users should get all users --- package-lock.json | 1411 ++++++++++++++++++++++++++++++++++++++- package.json | 1 + src/db/utils.ts | 2 +- src/tests/users.test.ts | 11 +- 4 files changed, 1416 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index cbe7703..099afd2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "jsonwebtoken": "^9.0.2", "pg": "^8.11.5", "umzug": "^3.8.0", + "vitest": "^1.6.0", "zod": "^3.23.4" }, "devDependencies": { @@ -42,6 +43,351 @@ "node": ">=12" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "cpu": [ + "mips64el" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@faker-js/faker": { "version": "8.4.1", "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz", @@ -58,6 +404,17 @@ "npm": ">=6.14.13" } }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", @@ -70,8 +427,7 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.9", @@ -148,6 +504,198 @@ "node": ">= 8" } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz", + "integrity": "sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.17.2.tgz", + "integrity": "sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz", + "integrity": "sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.17.2.tgz", + "integrity": "sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.17.2.tgz", + "integrity": "sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.17.2.tgz", + "integrity": "sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.17.2.tgz", + "integrity": "sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.17.2.tgz", + "integrity": "sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.17.2.tgz", + "integrity": "sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.17.2.tgz", + "integrity": "sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.17.2.tgz", + "integrity": "sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.17.2.tgz", + "integrity": "sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.17.2.tgz", + "integrity": "sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.17.2.tgz", + "integrity": "sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.17.2.tgz", + "integrity": "sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz", + "integrity": "sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rushstack/node-core-library": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-4.1.0.tgz", @@ -219,6 +767,11 @@ "string-argv": "~0.3.1" } }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==" + }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -276,6 +829,11 @@ "@types/node": "*" } }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" + }, "node_modules/@types/express": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", @@ -374,6 +932,70 @@ "@types/node": "*" } }, + "node_modules/@vitest/expect": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz", + "integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==", + "dependencies": { + "@vitest/spy": "1.6.0", + "@vitest/utils": "1.6.0", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.0.tgz", + "integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==", + "dependencies": { + "@vitest/utils": "1.6.0", + "p-limit": "^5.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz", + "integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==", + "dependencies": { + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz", + "integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==", + "dependencies": { + "tinyspy": "^2.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.0.tgz", + "integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==", + "dependencies": { + "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -395,7 +1017,6 @@ "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", - "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -407,7 +1028,6 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -452,6 +1072,17 @@ "node": ">=8" } }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -507,6 +1138,14 @@ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "dev": true }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "engines": { + "node": "*" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -596,6 +1235,14 @@ "node": ">= 0.8" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -614,6 +1261,34 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/chai": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", + "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.0.8" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -692,6 +1367,11 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/confbox": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.7.tgz", + "integrity": "sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==" + }, "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -741,6 +1421,19 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -749,6 +1442,17 @@ "ms": "2.0.0" } }, + "node_modules/deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -823,6 +1527,14 @@ "node": ">=0.3.1" } }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/dotenv": { "version": "16.3.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", @@ -890,11 +1602,56 @@ "node": ">= 0.4" } }, + "node_modules/esbuild": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -903,6 +1660,39 @@ "node": ">= 0.6" } }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/express": { "version": "4.19.2", "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", @@ -1089,7 +1879,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -1126,6 +1915,14 @@ "node": ">=10" } }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "engines": { + "node": "*" + } + }, "node_modules/get-intrinsic": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", @@ -1144,6 +1941,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -1305,6 +2113,14 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "engines": { + "node": ">=16.17.0" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -1410,11 +2226,32 @@ "node": ">=0.12.0" } }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, "node_modules/jju": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==" }, + "node_modules/js-tokens": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.0.tgz", + "integrity": "sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==" + }, "node_modules/jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -1468,6 +2305,21 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/local-pkg": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", + "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", + "dependencies": { + "mlly": "^1.4.2", + "pkg-types": "^1.0.3" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", @@ -1513,6 +2365,14 @@ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -1524,6 +2384,14 @@ "node": ">=10" } }, + "node_modules/magic-string": { + "version": "0.30.10", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", + "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -1565,6 +2433,11 @@ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -1623,6 +2496,17 @@ "node": ">= 0.6" } }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -1676,11 +2560,39 @@ "node": ">=10" } }, + "node_modules/mlly": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.0.tgz", + "integrity": "sha512-U9SDaXGEREBYQgfejV97coK0UL1r+qnF2SyO9A3qcI8MzKnsIFKHNVEkrDyNncQTKQQumsasmeq84eNMdBfsNQ==", + "dependencies": { + "acorn": "^8.11.3", + "pathe": "^1.1.2", + "pkg-types": "^1.1.0", + "ufo": "^1.5.3" + } + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -1785,7 +2697,32 @@ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/npmlog": { @@ -1840,6 +2777,34 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1856,6 +2821,14 @@ "node": ">=0.10.0" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -1866,6 +2839,19 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==" + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "engines": { + "node": "*" + } + }, "node_modules/pg": { "version": "8.11.5", "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.5.tgz", @@ -2009,6 +2995,11 @@ "split2": "^4.1.0" } }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -2020,6 +3011,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkg-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.0.tgz", + "integrity": "sha512-/RpmvKdxKf8uILTtoOhAgf30wYbP2Qw+L9p3Rvshx1JZVX+XQNZQFjlbmGHEGIm4CkVPlSn+NXmIM8+9oWQaSA==", + "dependencies": { + "confbox": "^0.1.7", + "mlly": "^1.6.1", + "pathe": "^1.1.2" + } + }, "node_modules/pony-cause": { "version": "2.1.11", "resolved": "https://registry.npmjs.org/pony-cause/-/pony-cause-2.1.11.tgz", @@ -2028,6 +3029,33 @@ "node": ">=12.0.0" } }, + "node_modules/postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/postgres-array": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz", @@ -2073,6 +3101,19 @@ "integrity": "sha512-VdlZoocy5lCP0c/t66xAfclglEapXPCIVhqqJRncYpvbCgImF0w67aPKfbqUMr72tO2k5q0TdTZwCLjPTI6C9g==", "dev": true }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -2146,6 +3187,11 @@ "node": ">= 0.8" } }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -2210,6 +3256,40 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rollup": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz", + "integrity": "sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==", + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.17.2", + "@rollup/rollup-android-arm64": "4.17.2", + "@rollup/rollup-darwin-arm64": "4.17.2", + "@rollup/rollup-darwin-x64": "4.17.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.17.2", + "@rollup/rollup-linux-arm-musleabihf": "4.17.2", + "@rollup/rollup-linux-arm64-gnu": "4.17.2", + "@rollup/rollup-linux-arm64-musl": "4.17.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.17.2", + "@rollup/rollup-linux-riscv64-gnu": "4.17.2", + "@rollup/rollup-linux-s390x-gnu": "4.17.2", + "@rollup/rollup-linux-x64-gnu": "4.17.2", + "@rollup/rollup-linux-x64-musl": "4.17.2", + "@rollup/rollup-win32-arm64-msvc": "4.17.2", + "@rollup/rollup-win32-ia32-msvc": "4.17.2", + "@rollup/rollup-win32-x64-msvc": "4.17.2", + "fsevents": "~2.3.2" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -2338,6 +3418,25 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", @@ -2355,6 +3454,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==" + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -2372,6 +3476,14 @@ "node": ">=10" } }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -2385,6 +3497,11 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -2393,6 +3510,11 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", + "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==" + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -2433,6 +3555,28 @@ "node": ">=8" } }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.0.tgz", + "integrity": "sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==", + "dependencies": { + "js-tokens": "^9.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/superagent": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/superagent/-/superagent-9.0.2.tgz", @@ -2540,6 +3684,27 @@ "node": ">=10" } }, + "node_modules/tinybench": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.8.0.tgz", + "integrity": "sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==" + }, + "node_modules/tinypool": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", + "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -2619,6 +3784,14 @@ } } }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "engines": { + "node": ">=4" + } + }, "node_modules/type-fest": { "version": "4.17.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.17.0.tgz", @@ -2655,6 +3828,11 @@ "node": ">=14.17" } }, + "node_modules/ufo": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz", + "integrity": "sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==" + }, "node_modules/umzug": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/umzug/-/umzug-3.8.0.tgz", @@ -2733,6 +3911,187 @@ "node": ">= 0.8" } }, + "node_modules/vite": { + "version": "5.2.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz", + "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==", + "dependencies": { + "esbuild": "^0.20.1", + "postcss": "^8.4.38", + "rollup": "^4.13.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz", + "integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/vite-node/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/vitest": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz", + "integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==", + "dependencies": { + "@vitest/expect": "1.6.0", + "@vitest/runner": "1.6.0", + "@vitest/snapshot": "1.6.0", + "@vitest/spy": "1.6.0", + "@vitest/utils": "1.6.0", + "acorn-walk": "^8.3.2", + "chai": "^4.3.10", + "debug": "^4.3.4", + "execa": "^8.0.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "tinybench": "^2.5.1", + "tinypool": "^0.8.3", + "vite": "^5.0.0", + "vite-node": "1.6.0", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "1.6.0", + "@vitest/ui": "1.6.0", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -2747,6 +4106,35 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", + "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wide-align": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", @@ -2782,6 +4170,17 @@ "node": ">=6" } }, + "node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/z-schema": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", diff --git a/package.json b/package.json index 39e59d8..05e692d 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "jsonwebtoken": "^9.0.2", "pg": "^8.11.5", "umzug": "^3.8.0", + "vitest": "^1.6.0", "zod": "^3.23.4" }, "devDependencies": { diff --git a/src/db/utils.ts b/src/db/utils.ts index 4e6cd43..1cd0188 100644 --- a/src/db/utils.ts +++ b/src/db/utils.ts @@ -1,6 +1,6 @@ import { query } from "."; // Adjust the import path to your actual db module -export const truncateTables = async (tableName: string): Promise => { +export const truncateTable = async (tableName: string): Promise => { // La sentencia TRUNCATE TABLE es usada para borrar todos los registros de una tabla. // La sentencia RESTART IDENTITY sirve para que se borre la secuencia de la tabla y comience en uno // CASCADE opción que se asegura que cualquier referencia foranea tambien se borre. diff --git a/src/tests/users.test.ts b/src/tests/users.test.ts index a2d45b3..1fe9539 100644 --- a/src/tests/users.test.ts +++ b/src/tests/users.test.ts @@ -1,14 +1,14 @@ import { describe, beforeEach, it, expect } from "vitest"; import request from "supertest"; import { app } from "../app"; // Importa tu aplicación Express -import { truncateTables } from "../db/utils"; // Funciones auxiliares para configurar la base de datos +import { truncateTable } from "../db/utils"; // Funciones auxiliares para configurar la base de datos import * as db from "../db"; describe("Backend Testing", () => { // Antes de cada prueba, limpiar y configurar la base de datos beforeEach(async () => { // Limpia todas las tablas relevantes - await truncateTables(["Users", "Posts", "Likes"]); + await truncateTable("Users"); // Inserta datos de prueba en las tablas await db.query(` @@ -18,3 +18,10 @@ describe("Backend Testing", () => { `); }); + it("should get all users", async () => { + const response = await request(app).get("/me"); + expect(response.statusCode).toBe(200); + expect(response.body.ok).toBeTruthy(); + expect(response.body.data).toHaveLength(2); + }); + }); From 4e6356aa35036aa3f15e658e457db34f89f4d99c Mon Sep 17 00:00:00 2001 From: carusi99 Date: Sat, 4 May 2024 19:21:21 -0500 Subject: [PATCH 26/37] test posts --- src/tests/posts.test.ts | 20 ++++++++++++++++++++ src/tests/users.test.ts | 30 ++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/src/tests/posts.test.ts b/src/tests/posts.test.ts index e69de29..6d23936 100644 --- a/src/tests/posts.test.ts +++ b/src/tests/posts.test.ts @@ -0,0 +1,20 @@ +import { describe, beforeEach, it, expect } from "vitest"; +import request from "supertest"; +import { app } from "../app"; // Importa tu aplicación Express +import { truncateTable } from "../db/utils"; // Funciones auxiliares para configurar la base de datos +import * as db from "../db"; + +describe("Backend Testing", () => { + // Antes de cada prueba, limpiar y configurar la base de datos + beforeEach(async () => { + // Limpia todas las tablas relevantes + await truncateTable("Posts"); + + // Inserta datos de prueba en las tablas + await db.query(` + INSERT INTO Posts (userId, content, createdAt, updatedAt) + VALUES (1, 'Post 1 by user 1', NOW(), NOW()), + (2, 'Post 1 by user 2', NOW(), NOW()); + `); + }); + \ No newline at end of file diff --git a/src/tests/users.test.ts b/src/tests/users.test.ts index 1fe9539..a25b18c 100644 --- a/src/tests/users.test.ts +++ b/src/tests/users.test.ts @@ -24,4 +24,34 @@ describe("Backend Testing", () => { expect(response.body.ok).toBeTruthy(); expect(response.body.data).toHaveLength(2); }); + + it("should update a user", async () => { + const userIdToUpdate = 1; + const updatedUserData = { + firstName: "Updated", + lastName: "User", + email: "updateduser@example.com", + role: "admin" + }; + const response = await request(app).patch(`/users/${userIdToUpdate}`).send(updatedUserData); + expect(response.statusCode).toBe(200); + expect(response.body.ok).toBeTruthy(); + // Verificar que los datos actualizados coincidan con los enviados + expect(response.body.data.firstName).toBe(updatedUserData.firstName); + expect(response.body.data.lastName).toBe(updatedUserData.lastName); + expect(response.body.data.email).toBe(updatedUserData.email); + expect(response.body.data.role).toBe(updatedUserData.role); + }); + + it("should delete a user", async () => { + const userIdToDelete = 1; + const response = await request(app).delete(`/users/${userIdToDelete}`); + expect(response.statusCode).toBe(200); + expect(response.body.ok).toBeTruthy(); + // Verificar que el usuario haya sido eliminado + const deletedUserResponse = await request(app).get(`/users/${userIdToDelete}`); + expect(deletedUserResponse.statusCode).toBe(404); + expect(deletedUserResponse.body.ok).toBeFalsy(); + expect(deletedUserResponse.body.error).toBe("User not found"); + }); }); From 1e1bf05c4a3c91e7982fdd543dde90fd99c5bd79 Mon Sep 17 00:00:00 2001 From: carusi99 Date: Sat, 4 May 2024 19:28:23 -0500 Subject: [PATCH 27/37] test posts should get all posts --- src/tests/posts.test.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/tests/posts.test.ts b/src/tests/posts.test.ts index 6d23936..617f8aa 100644 --- a/src/tests/posts.test.ts +++ b/src/tests/posts.test.ts @@ -17,4 +17,10 @@ describe("Backend Testing", () => { (2, 'Post 1 by user 2', NOW(), NOW()); `); }); - \ No newline at end of file + it("should get all posts", async () => { + const response = await request(app).get("/posts"); + expect(response.statusCode).toBe(200); + expect(response.body.ok).toBeTruthy(); + expect(response.body.data).toHaveLength(2); + }); +}); From f867210ecd23bd74177a56017835adb22ad081ed Mon Sep 17 00:00:00 2001 From: carusi99 Date: Sat, 4 May 2024 19:29:30 -0500 Subject: [PATCH 28/37] test posts should get posts by username --- src/tests/posts.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/tests/posts.test.ts b/src/tests/posts.test.ts index 617f8aa..59560ba 100644 --- a/src/tests/posts.test.ts +++ b/src/tests/posts.test.ts @@ -23,4 +23,12 @@ describe("Backend Testing", () => { expect(response.body.ok).toBeTruthy(); expect(response.body.data).toHaveLength(2); }); + it("should get posts by username", async () => { + const response = await request(app).get("/posts/user1"); + expect(response.statusCode).toBe(200); + expect(response.body.ok).toBeTruthy(); + expect(response.body.data).toHaveLength(1); + expect(response.body.data[0].username).toBe("user1"); + }); + }); From 2f401d7e2d031ccf101bfd8a50d1d054b334756d Mon Sep 17 00:00:00 2001 From: carusi99 Date: Sat, 4 May 2024 19:30:37 -0500 Subject: [PATCH 29/37] test posts should create a new post --- src/tests/posts.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/tests/posts.test.ts b/src/tests/posts.test.ts index 59560ba..b27e6c7 100644 --- a/src/tests/posts.test.ts +++ b/src/tests/posts.test.ts @@ -31,4 +31,14 @@ describe("Backend Testing", () => { expect(response.body.data[0].username).toBe("user1"); }); + it("should create a new post", async () => { + const newPost = { + content: "New post content" + }; + const response = await request(app).post("/posts").send(newPost); + expect(response.statusCode).toBe(201); + expect(response.body.ok).toBeTruthy(); + expect(response.body.data.content).toBe(newPost.content); + }); + }); From 7392b983d9d3f9f7e2b2dbf2698fc900d2900ec8 Mon Sep 17 00:00:00 2001 From: carusi99 Date: Sat, 4 May 2024 19:32:51 -0500 Subject: [PATCH 30/37] test posts should update a post --- src/tests/posts.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/tests/posts.test.ts b/src/tests/posts.test.ts index b27e6c7..ebde4ea 100644 --- a/src/tests/posts.test.ts +++ b/src/tests/posts.test.ts @@ -40,5 +40,15 @@ describe("Backend Testing", () => { expect(response.body.ok).toBeTruthy(); expect(response.body.data.content).toBe(newPost.content); }); + it("should update a post", async () => { + const postIdToUpdate = 1; + const updatedPostData = { + content: "Updated post content" + }; + const response = await request(app).patch(`/posts/${postIdToUpdate}`).send(updatedPostData); + expect(response.statusCode).toBe(200); + expect(response.body.ok).toBeTruthy(); + expect(response.body.data.content).toBe(updatedPostData.content); + }); }); From 311f9d2ee2aaf5c73a41f065111dbc8f2c94b789 Mon Sep 17 00:00:00 2001 From: carusi99 Date: Sat, 4 May 2024 19:36:50 -0500 Subject: [PATCH 31/37] test likes --- src/tests/likes.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/tests/likes.test.ts b/src/tests/likes.test.ts index e69de29..6c3038f 100644 --- a/src/tests/likes.test.ts +++ b/src/tests/likes.test.ts @@ -0,0 +1,12 @@ +import { describe, beforeEach, it, expect } from "vitest"; +import request from "supertest"; +import { app } from "../app"; // Importa tu aplicación Express +import { truncateTable } from "../db/utils"; // Funciones auxiliares para configurar la base de datos +import * as db from "../db"; + +describe("Backend Testing", () => { + // Antes de cada prueba, limpiar y configurar la base de datos + beforeEach(async () => { + // Limpia todas las tablas relevantes + await truncateTable("likes"); + \ No newline at end of file From 969d2a9478282f9733f100647b686d0bead6e034 Mon Sep 17 00:00:00 2001 From: carusi99 Date: Sat, 4 May 2024 19:38:43 -0500 Subject: [PATCH 32/37] test likes table --- src/tests/likes.test.ts | 7 +++- src/tests/users.test.ts | 78 ++++++++++++++++++++++------------------- 2 files changed, 47 insertions(+), 38 deletions(-) diff --git a/src/tests/likes.test.ts b/src/tests/likes.test.ts index 6c3038f..2d428b3 100644 --- a/src/tests/likes.test.ts +++ b/src/tests/likes.test.ts @@ -9,4 +9,9 @@ describe("Backend Testing", () => { beforeEach(async () => { // Limpia todas las tablas relevantes await truncateTable("likes"); - \ No newline at end of file + await db.query(` + INSERT INTO Likes (postId, userId, createdAt) + VALUES (1, 1, NOW()), + (1, 2, NOW()); + `); +}); \ No newline at end of file diff --git a/src/tests/users.test.ts b/src/tests/users.test.ts index a25b18c..2d88cc2 100644 --- a/src/tests/users.test.ts +++ b/src/tests/users.test.ts @@ -16,42 +16,46 @@ describe("Backend Testing", () => { VALUES ('user1', 'password1', 'user1@example.com', 'John', 'Doe', 'user', NOW(), NOW()), ('user2', 'password2', NULL, 'Jane', NULL, 'admin', NOW(), NOW()); `); - }); - - it("should get all users", async () => { - const response = await request(app).get("/me"); - expect(response.statusCode).toBe(200); - expect(response.body.ok).toBeTruthy(); - expect(response.body.data).toHaveLength(2); - }); + }); - it("should update a user", async () => { - const userIdToUpdate = 1; - const updatedUserData = { - firstName: "Updated", - lastName: "User", - email: "updateduser@example.com", - role: "admin" - }; - const response = await request(app).patch(`/users/${userIdToUpdate}`).send(updatedUserData); - expect(response.statusCode).toBe(200); - expect(response.body.ok).toBeTruthy(); - // Verificar que los datos actualizados coincidan con los enviados - expect(response.body.data.firstName).toBe(updatedUserData.firstName); - expect(response.body.data.lastName).toBe(updatedUserData.lastName); - expect(response.body.data.email).toBe(updatedUserData.email); - expect(response.body.data.role).toBe(updatedUserData.role); - }); + it("should get all users", async () => { + const response = await request(app).get("/me"); + expect(response.statusCode).toBe(200); + expect(response.body.ok).toBeTruthy(); + expect(response.body.data).toHaveLength(2); + }); - it("should delete a user", async () => { - const userIdToDelete = 1; - const response = await request(app).delete(`/users/${userIdToDelete}`); - expect(response.statusCode).toBe(200); - expect(response.body.ok).toBeTruthy(); - // Verificar que el usuario haya sido eliminado - const deletedUserResponse = await request(app).get(`/users/${userIdToDelete}`); - expect(deletedUserResponse.statusCode).toBe(404); - expect(deletedUserResponse.body.ok).toBeFalsy(); - expect(deletedUserResponse.body.error).toBe("User not found"); - }); - }); + it("should update a user", async () => { + const userIdToUpdate = 1; + const updatedUserData = { + firstName: "Updated", + lastName: "User", + email: "updateduser@example.com", + role: "admin", + }; + const response = await request(app) + .patch(`/users/${userIdToUpdate}`) + .send(updatedUserData); + expect(response.statusCode).toBe(200); + expect(response.body.ok).toBeTruthy(); + // Verificar que los datos actualizados coincidan con los enviados + expect(response.body.data.firstName).toBe(updatedUserData.firstName); + expect(response.body.data.lastName).toBe(updatedUserData.lastName); + expect(response.body.data.email).toBe(updatedUserData.email); + expect(response.body.data.role).toBe(updatedUserData.role); + }); + + it("should delete a user", async () => { + const userIdToDelete = 1; + const response = await request(app).delete(`/users/${userIdToDelete}`); + expect(response.statusCode).toBe(200); + expect(response.body.ok).toBeTruthy(); + // Verificar que el usuario haya sido eliminado + const deletedUserResponse = await request(app).get( + `/users/${userIdToDelete}` + ); + expect(deletedUserResponse.statusCode).toBe(404); + expect(deletedUserResponse.body.ok).toBeFalsy(); + expect(deletedUserResponse.body.error).toBe("User not found"); + }); +}); From 6b9ae5b0b61efdbf227f34d4affdd78da212d53b Mon Sep 17 00:00:00 2001 From: carusi99 Date: Sat, 4 May 2024 19:39:44 -0500 Subject: [PATCH 33/37] test likes should like a post --- src/tests/likes.test.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/tests/likes.test.ts b/src/tests/likes.test.ts index 2d428b3..765b619 100644 --- a/src/tests/likes.test.ts +++ b/src/tests/likes.test.ts @@ -14,4 +14,12 @@ describe("Backend Testing", () => { VALUES (1, 1, NOW()), (1, 2, NOW()); `); -}); \ No newline at end of file +}); +it("should like a post", async () => { + const response = await request(app).post("/posts/1/like").set('Authorization', 'Bearer your-auth-token'); + expect(response.statusCode).toBe(200); + expect(response.body.ok).toBeTruthy(); + expect(response.body.data.postid).toBe(1); + expect(response.body.data.userid).toBe(1); // Assuming user 1 liked the post + }); +}) \ No newline at end of file From 01bca29dae0b286e4c223432723f0c8a8b9c7dcb Mon Sep 17 00:00:00 2001 From: carusi99 Date: Sat, 4 May 2024 19:41:07 -0500 Subject: [PATCH 34/37] test likes should unlike a post --- src/tests/likes.test.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/tests/likes.test.ts b/src/tests/likes.test.ts index 765b619..f406903 100644 --- a/src/tests/likes.test.ts +++ b/src/tests/likes.test.ts @@ -20,6 +20,14 @@ it("should like a post", async () => { expect(response.statusCode).toBe(200); expect(response.body.ok).toBeTruthy(); expect(response.body.data.postid).toBe(1); - expect(response.body.data.userid).toBe(1); // Assuming user 1 liked the post + expect(response.body.data.userid).toBe(1); }); + it("should unlike a post", async () => { + const response = await request(app).delete("/posts/1/like").set('Authorization', 'Bearer your-auth-token'); + expect(response.statusCode).toBe(200); + expect(response.body.ok).toBeTruthy(); + expect(response.body.data.postid).toBe(1); + expect(response.body.data.userid).toBe(1); + }); + }) \ No newline at end of file From b3c5f948cc3cdb19d3d0036e556fd030244ac0c7 Mon Sep 17 00:00:00 2001 From: Paola <151582174+carusi99@users.noreply.github.com> Date: Mon, 6 May 2024 13:48:59 -0500 Subject: [PATCH 35/37] Create README.md --- README.md | 111 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..5150112 --- /dev/null +++ b/README.md @@ -0,0 +1,111 @@ + +## RESTful API para Gestión de Posts + +Esta API fue creada para perimitir a los usuarios interactuar con publicaciones (Posts) en una red social, ofreciendo diferentes operaciones según si el usuario está registrado o no. + + + +# Requerimientos Técnicos +- **Lenguaje**: TypeScript +- **Backend**: Express.js +- **Autenticación/Autorización**: JWT +- **Arquitectura**: Tres capas (routers, servicios, acceso a datos) +- **Base de Datos**: PostgreSQL +- **Acceso a Datos**: Uso de pg +- **Migraciones**: Implementación con umzug +- **Testing**: Pruebas de endpoints con vitest y supertest +- **Validación de Input**: Uso de Zod +- **Manejo de Errores**: Middleware centralizado +- **Manejo de Entorno**: Uso de variables de entorno con - dotenv +## Características Principales +### Esquema de Base de Datos +La base de datos incluye tres tablas principales: Users, Posts y Likes, cada una con sus respectivos campos y relaciones. + +### Especificación de API +La API ofrece endpoints para visualizar posts, interactuar con usuarios registrados, gestionar perfiles de usuario, y registro y autenticación de usuarios. + +Para cada endpoint se detalla su descripción, parámetros, respuesta y ejemplos de uso. + + + +## Instalación + +- Asegúrese de que sus versiones de NodeJS y npm estén actualizadas para Express ^4.19.2 + +- Instalar dependencias: npm install or yarn Una pequeña introducción sobre la instalación. + +Clone este repositorio + +```bash + git clone https://github.com/codeableorg/postable-carusi99.git +``` + +Ir al directorio del proyecto + +```bash + cd my-project +``` + +Instalar dependencias + +```bash + npm install +``` + +```bash + Configura las variables de entorno en un archivo .env. +``` + +Iniciar el servidor + +```bash + npm run start +``` +## Cómo probar tu Api de Posts +### utiliza los comandos que estan en tu Package.json para utilizar las migraciones crear tu base de datos, e insertar información a ella. +![Captura de pantalla 2024-05-06 133924](https://github.com/carusi99/Notes/assets/151582174/405c2717-0c92-42fe-b5fe-f54a5e71e35c) + +#### Así ejecutas los comandos en la terminal +![Captura de pantalla 2024-05-06 133702](https://github.com/carusi99/Notes/assets/151582174/7460f738-cac8-450e-a787-73ee0e4ca55b) + +> [!IMPORTANT] +> debes tener instalada la base de datos de código abierto PostgreSQL. + +#### Cómo puedes verificar los servivios: + +👩‍💻 Insomnia es una aplicación de escritorio multiplataforma que te permite probar y depurar API RESTful y otros servicios web de una manera fácil y eficiente. + +- Instalación de Insomnia: Primero, descarga e instala Insomnia desde su sitio web oficial: [Insomnia](https://insomnia.rest/download) + +- Crear un nuevo espacio de trabajo: Abre Insomnia y crea un nuevo espacio de trabajo si aún no tienes uno. - Puedes nombrarlo como desees y organizar tus solicitudes en él. +- Crear una nueva solicitud: Dentro de tu espacio de trabajo, puedes crear una nueva solicitud haciendo clic en el botón "+" en la barra lateral izquierda y seleccionando "New Request". Luego, ingresa la URL de la solicitud y selecciona el método HTTP adecuado (GET, POST, etc.). + +![Captura de pantalla 2024-05-06 134530](https://github.com/carusi99/Notes/assets/151582174/3af76f2c-27e9-43f7-ba2f-368032f96cc2) + +## Paquetes usados: + +- [bcrypt (^5.1.1)](https://www.npmjs.com/package/bcrypt) +- [dotenv (^16.3.1)](https://www.npmjs.com/package/dotenv) +- [express (^4.19.2)](https://www.npmjs.com/package/express) +- [jsonwebtoken (^9.0.2)](https://www.npmjs.com/package/jsonwebtoken) +- [pg (^8.11.5)](https://www.npmjs.com/package/pg) +- [umzug (^3.8.0)](https://www.npmjs.com/package/umzug) +- [vitest (^1.6.0)](https://www.npmjs.com/package/vitest) +- [zod (^3.23.4)](https://www.npmjs.com/package/zod) + + + + + +## Guía de Contribución +Si deseas contribuir al proyecto, sigue estos pasos: + +- Realiza un fork del repositorio. +- Crea una nueva rama (git checkout -b feature/nueva-funcionalidad). +- Realiza tus cambios y haz commits (git commit -am 'Agrega nueva funcionalidad'). +- Sube tus cambios a la rama (git push origin feature/nueva-funcionalidad). +- Crea un nuevo Pull Request. + +## Créditos +Este proyecto fue desarrollado por paolapachecocarusi@gmail.com y se basa en las especificaciones proporcionadas por Codeable Academy. + From ab8b947291f59dd1793e289e3e05af7c38662ffc Mon Sep 17 00:00:00 2001 From: carusi99 Date: Thu, 9 May 2024 18:34:47 -0500 Subject: [PATCH 36/37] changes in posts --- .gitignore | 3 +- src/data/posts.data.ts | 189 ++++++---------- src/db/index.ts | 9 +- .../2024.04.25T22.02.49.seed-user.ts | 2 +- src/db/migrations/migrations.test.json | 5 + src/db/scripts/dbCreate.ts | 10 +- src/db/scripts/dbDrop.ts | 27 ++- src/db/scripts/dbMigrate.ts | 19 +- src/db/scripts/dbSeed.ts | 7 + src/index.ts | 2 +- src/models/posts.ts | 4 + src/routers/posts.routers.ts | 207 +++++++----------- src/routers/users.routers.ts | 55 +++-- src/services/posts.service.ts | 75 +++---- src/tests/posts.test.ts | 2 +- src/tests/users.test.ts | 2 +- 16 files changed, 302 insertions(+), 316 deletions(-) create mode 100644 src/db/migrations/migrations.test.json diff --git a/.gitignore b/.gitignore index 1dcef2d..23d4afe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules -.env \ No newline at end of file +.env +.env.test \ No newline at end of file diff --git a/src/data/posts.data.ts b/src/data/posts.data.ts index c36aa8e..270ae2d 100644 --- a/src/data/posts.data.ts +++ b/src/data/posts.data.ts @@ -1,142 +1,91 @@ import * as db from "../db"; import { ApiError } from "../middlewares/error"; -import { Post } from "../models/posts"; +import { Post, UpdatePostParams } from "../models/posts"; import { PostFilters } from "../models/posts"; import { filtering, sorting } from "./utils"; -export async function getPosts( - page: number, - limit: number, - filters: PostFilters = {}, - orderBy: string = 'createdAt', - order: string = 'asc'): - Promise { - try { - // Utiliza getPostsByUsernameFromDatabase para obtener los posts junto con su conteo de likes - let query = "SELECT * FROM posts"; - const queryParams: (string | boolean | number)[] = []; - - // Filtering - query = filtering(query, filters, queryParams); - // Sorting - query += ` ORDER BY ${orderBy} ${order}`; - - // Pagination - if (page && limit) { - const offset = (page - 1) * limit; - query += ` LIMIT $${queryParams.length + 1} OFFSET $${queryParams.length + 2}`; - queryParams.push(limit, offset); - } - - const result = await db.query(query, queryParams); - return result.rows; - } catch (error) { - console.error('Error al obtener los posts:', error); - throw new ApiError('Error al obtener los posts', 500); - } -} - -export async function getTotalPosts(username?: string): Promise { - try { - let query = 'SELECT COUNT(*) FROM posts'; - const queryParams: any[] = []; - - if (username) { - query += ' WHERE username = $1'; - queryParams.push(username); - } - - const { rows } = await db.query(query, queryParams); - return parseInt(rows[0].count, 10); - } catch (error) { - console.error('Error al obtener el total de posts desde la base de datos:', error); - throw new ApiError('Error al obtener el total de posts desde la base de datos', 400); - } +//POST/posts: +export async function createPostDB(id: number, post: Post) { + const { content } = post; + const result = await db.query( + "INSERT INTO posts (userid, content) VALUES ($1,$2) RETURNING id,content,createdat,updatedat,(SELECT u.username FROM users AS u WHERE u.id =$1) AS username, 0 AS likesCount", + [id, content] + ); + return result.rows[0]; } -export async function getPostsByUsernameFromDatabase(username: string, page: number, limit: number, orderBy: string = 'createdAt', order: string = 'asc'): Promise { - try { - let query = ` - SELECT p.id, p.content, p.createdat, p.updatedat, u.username, - COALESCE(SUM(CASE WHEN pl.id IS NOT NULL THEN 1 ELSE 0 END), 0) AS likeCount - FROM posts AS p - JOIN users AS u ON u.id=p.userid - LEFT JOIN likes AS pl ON pl.postid = p.id`; - - const queryParams: any[] = []; - - if (username) { - query += ' WHERE u.username = $1'; - queryParams.push(username); - } - - query += ` GROUP BY p.id, u.username`; - query += ` ORDER BY ${orderBy} ${order}`; - query += ` LIMIT ${limit} OFFSET ${(page - 1) * limit}`; - - const { rows } = await db.query(query, queryParams); - return rows; - } catch (error) { - throw new ApiError('Error al obtener los posts del usuario', 401); +//PATCH/posts:id: +export async function updatePostDB({ + id, + fieldsToUpdate, +}: UpdatePostParams): Promise { + if (!id || Object.keys(fieldsToUpdate).length === 0) { + throw new Error("No se proporcionaron datos para actualizar"); } -} + const entries = Object.entries(fieldsToUpdate); + const setClauses = entries.map(([key, _], index) => `${key} = $${index + 1}`); -export async function createPost(userId: number, content: string): Promise { - try { - const result = await db.query( - `INSERT INTO posts (userid, content) VALUES ($1,$2) RETURNING id,content,createdat,updatedat,(SELECT u.username FROM users AS u WHERE u.id =$1) AS username, 0 AS likesCount`, - [userId, content] - ); + const updateQuery = `UPDATE posts SET ${setClauses.join( + ", " + )}, updatedat = NOW() WHERE id = $${ + entries.length + 1 + } RETURNING *, (SELECT username FROM users WHERE id = posts.userid) AS username`; - return result.rows[0]; - } catch (error) { - throw new ApiError("Error al crear el post en la base de datos", 400); - } -} + const params = [...entries.map(([, value]) => value), id]; -export async function editPost(postId: number, content: string): Promise { - try { - const query = `UPDATE posts SET content = $1, updatedat = CURRENT_TIMESTAMP WHERE id = $2 RETURNING *`; - const params = [content, postId]; - const { rows } = await db.query(query, params); + const result = await db.query(updateQuery, params); - if (rows.length === 0) { - throw new ApiError("El post no existe", 404); - } + return result.rows[0]; +} - return rows[0]; - } catch (error) { - throw new ApiError("Error al editar el post en la base de datos", 500); +export async function getPostsFromDB( + filters: PostFilters = {}, + sort?: string, + page?: number, + limit?: number +): Promise { + let query = + "SELECT p.id,p.content,p.createdat,p.updatedat,u.username, COALESCE(SUM(CASE WHEN pl.id IS NOT NULL THEN 1 ELSE 0 END),0) AS LikesCount FROM posts AS p JOIN users AS u ON u.id=p.userid LEFT JOIN likes AS pl ON pl.postid = p.id GROUP BY p.id, u.username"; + const queryParams: (string | boolean | number)[] = []; + + //Filtering: + query = filtering(query, filters, queryParams); + + //Sorting: + query = sorting(query, sort); + + //Pagination: + if (page && limit) { + const offset = (page - 1) * limit; + query += ` LIMIT ${limit} OFFSET ${offset}`; } + + const result = await db.query(query, queryParams); + return result.rows; } -export async function checkIfUserExists(username: string): Promise { - try { - const query = 'SELECT COUNT(*) AS count FROM users WHERE username = $1'; - const params = [username]; - const { rows } = await db.query(query, params); - const userCount = parseInt(rows[0].count, 10); - return userCount > 0; - } catch (error) { - console.error('Error al verificar si el usuario existe:', error); - throw new ApiError('Error al verificar si el usuario existe', 400); - } +export async function getPostsCountFromDB( + filters: PostFilters = {} +): Promise { + let query = "SELECT COUNT(*) FROM posts"; + const queryParams: (string | boolean | number)[] = []; + // Filtering + query = filtering(query, filters, queryParams); + + const result = await db.query(query, queryParams); + return Number(result.rows[0].count); } -export async function getPostById(postId: number): Promise { - try { - const query = 'SELECT * FROM posts WHERE id = $1'; - const queryParams = [postId]; - const { rows } = await db.query(query, queryParams); - - if (rows.length === 0) { - throw new ApiError('El post no existe', 404); - } - - return rows[0]; - } catch (error) { - throw new ApiError('Error al obtener el post desde la base de datos', 400); +//GET/posts/:username: +export async function getPostsByUsernameFromDB(username: string) { + const result = await db.query( + "SELECT p.id,p.content,p.createdat,p.updatedat,u.username, COALESCE(SUM(CASE WHEN pl.id IS NOT NULL THEN 1 ELSE 0 END),0) AS LikesCount FROM posts AS p JOIN users AS u ON u.id=p.userid LEFT JOIN likes AS pl ON pl.postid = p.id WHERE username =$1 GROUP BY p.id, u.username;", + [username] + ); + if (result.rows.length === 0) { + throw new Error("No posts for this user"); } + return result.rows; } diff --git a/src/db/index.ts b/src/db/index.ts index 0c3daac..4db43ad 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -1,5 +1,12 @@ -import "dotenv/config"; + import { Client, Pool } from "pg"; +import { configDotenv } from "dotenv"; + +if (process.env["NODE_ENV"] === "test") { + configDotenv({ path: ".env.test" }); +} else { + configDotenv(); +} export const pool = new Pool({ host: process.env["PGHOST"], diff --git a/src/db/migrations/2024.04.25T22.02.49.seed-user.ts b/src/db/migrations/2024.04.25T22.02.49.seed-user.ts index 7695f1e..c2f15a7 100644 --- a/src/db/migrations/2024.04.25T22.02.49.seed-user.ts +++ b/src/db/migrations/2024.04.25T22.02.49.seed-user.ts @@ -51,7 +51,7 @@ export const up: Migration = async (params) => { ) .join(", "); const sqlQuery = `INSERT INTO Users (username, password, email, firstName, lastName, role, createdAt, updatedAt) VALUES ${values};`; - + console.log(sqlQuery); return await params.context.query(sqlQuery); }; diff --git a/src/db/migrations/migrations.test.json b/src/db/migrations/migrations.test.json new file mode 100644 index 0000000..dd92995 --- /dev/null +++ b/src/db/migrations/migrations.test.json @@ -0,0 +1,5 @@ +[ + "2024.04.25T16.33.34.users.ts", + "2024.04.25T16.40.52.posts.ts", + "2024.04.25T16.44.31.likes.ts" +] \ No newline at end of file diff --git a/src/db/scripts/dbCreate.ts b/src/db/scripts/dbCreate.ts index 471f2ed..9e7dc47 100644 --- a/src/db/scripts/dbCreate.ts +++ b/src/db/scripts/dbCreate.ts @@ -1,5 +1,13 @@ -import "dotenv/config"; import { adminClient } from ".."; +import { configDotenv } from "dotenv"; + +if (process.env["NODE_ENV"] === "test") { + console.log("Creando base de datos de prueba"); + configDotenv({ path: ".env.test" }); + console.log(process.env["PGDATABASE"]) +} else { + configDotenv(); +} const dbName = process.env["PGDATABASE"]; diff --git a/src/db/scripts/dbDrop.ts b/src/db/scripts/dbDrop.ts index bc326ea..ee22471 100644 --- a/src/db/scripts/dbDrop.ts +++ b/src/db/scripts/dbDrop.ts @@ -1,5 +1,17 @@ -import "dotenv/config"; import { adminClient } from ".."; +import { configDotenv } from "dotenv"; +import fs from "node:fs"; +import path from "node:path"; + +if (process.env["NODE_ENV"] === "test") { + configDotenv({ path: ".env.test" }); +} else { + configDotenv(); +} +const migrationsFileName = + process.env["NODE_ENV"] === "test" + ? "migrations.test.json" + : "migrations.json"; const dbName = process.env["PGDATABASE"]; @@ -11,5 +23,16 @@ adminClient.query(`DROP DATABASE IF EXISTS "${dbName}"`, (err) => { } else { console.log(`Base de datos "${dbName}" eliminada exitosamente`); } - adminClient.end(); + try { + fs.unlinkSync( + path.join(__dirname, "..", "migrations", migrationsFileName) + ); + } catch { + console.log( + "No se pudo eliminar el archivo de migraciones", + migrationsFileName + ); + } +adminClient.end(); }); + diff --git a/src/db/scripts/dbMigrate.ts b/src/db/scripts/dbMigrate.ts index 3a52ef8..95f338f 100644 --- a/src/db/scripts/dbMigrate.ts +++ b/src/db/scripts/dbMigrate.ts @@ -1,14 +1,25 @@ -import "dotenv/config"; +import { configDotenv } from "dotenv"; import path from "node:path"; import fs from "node:fs"; -import { query } from ".."; +import { query, pool } from ".."; import { JSONStorage, Umzug } from "umzug"; +if (process.env["NODE_ENV"] === "test") { + configDotenv({ path: ".env.test" }); +} else { + configDotenv(); +} + +const migrationsFileName = + process.env["NODE_ENV"] === "test" + ? "migrations.test.json" + : "migrations.json"; + const migrator = new Umzug({ migrations: { glob: path.join(__dirname, "..", "migrations", "*.ts") }, context: { query }, storage: new JSONStorage({ - path: path.join(__dirname, "..", "migrations", "migrations.json"), + path: path.join(__dirname, "..", "migrations", migrationsFileName), }), logger: console, create: { @@ -28,4 +39,4 @@ const migrator = new Umzug({ export type Migration = typeof migrator._types.migration; -migrator.runAsCLI(); +migrator.runAsCLI().then(() => pool.end()); \ No newline at end of file diff --git a/src/db/scripts/dbSeed.ts b/src/db/scripts/dbSeed.ts index cc07ee0..1e2821f 100644 --- a/src/db/scripts/dbSeed.ts +++ b/src/db/scripts/dbSeed.ts @@ -33,6 +33,13 @@ SELECT NOW() -- Fecha y hora de actualización FROM generate_series(1, 100) AS s(id); +-- Insertar datos de ejemplo en la tabla Likes +INSERT INTO Likes (postId, userId, createdAt) +SELECT + (s.id % 100) + 1, -- ID de post aleatorio + (s.id % 50) + 1, -- ID de usuario aleatorio + NOW() -- Fecha y hora del like +FROM generate_series(1, 200) AS s(id); `).then(() => { console.log("Products inserted"); pool.end(); diff --git a/src/index.ts b/src/index.ts index 37f0849..494abaa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import { app } from "./app"; import { pool } from "./db"; -const port = 7000; +const port = 7500; // Manejar cierre de la aplicación const gracefulShutdown = () => { diff --git a/src/models/posts.ts b/src/models/posts.ts index f316476..cf4f7e8 100644 --- a/src/models/posts.ts +++ b/src/models/posts.ts @@ -12,3 +12,7 @@ export interface PostFilters { username?: string; } +export interface UpdatePostParams { + id: number; + fieldsToUpdate: Record; +} \ No newline at end of file diff --git a/src/routers/posts.routers.ts b/src/routers/posts.routers.ts index 01717b0..09e248f 100644 --- a/src/routers/posts.routers.ts +++ b/src/routers/posts.routers.ts @@ -1,141 +1,102 @@ -import express from "express"; -import { getPosts, getTotalPosts, getPostsByUsernameFromDatabase, createPost, editPost, checkIfUserExists, getPostById } from "../data/posts.data"; -import { pagination } from "../data/utils"; -import { PostFilters } from "../models/posts"; +import express, { NextFunction, Request, Response } from "express"; import { authenticateHandler } from "../middlewares/authenticate"; +import { ApiError } from "../middlewares/error"; +import { Post, PostFilters } from "../models/posts"; +import { + createPost, + updatePost, + getPostsByUsername, + getPosts, + getPostsCount +} from "../services/posts.service"; const postsRouter = express.Router(); -//TRAE LOS POSTS -postsRouter.get('/', async (req, res) => { - try { - const { page = 1, limit = 10, username, orderBy = 'createdAt', order = 'asc' } = req.query; - const pageInt = parseInt(page as string, 10); - const limitInt = parseInt(limit as string, 10); - - // Aplicar paginación - const totalItems = await getTotalPosts(username as string); - const totalPages = Math.ceil(totalItems / limitInt); - const paginationInfo = pagination(pageInt, limitInt); - - let filters: PostFilters = {}; - if (typeof username === 'string' && username !== '') { - filters.username = username; +//POST/posts: +postsRouter.post( + "/", + authenticateHandler, + async (req: Request, res: Response, next: NextFunction) => { + if (req.userId === undefined) { + return next(new ApiError("Unauthorized", 401)); } - - const posts = await getPosts( - pageInt, - limitInt, - filters, - orderBy as string, - order as string - ); - - res.status(200).json({ - ok: true, - data: posts, - pagination: { - page: pageInt, - pageSize: limitInt, - totalItems: totalItems, - totalPages: totalPages, - nextPage: posts.length === limitInt ? pageInt + 1 : null, - previousPage: pageInt > 1 ? pageInt - 1 : null - } - }); - } catch (error) { - console.error('Error al obtener los posts:', error); - res.status(500).json({ ok: false, message: 'Error al obtener los posts' }); - } -}); - - -//TRAE LOS POSTS DE UN USUARIO -postsRouter.get('/:username', async (req, res) => { - try { - const { username } = req.params; - const { page = 1, limit = 10, orderBy = 'createdAt', order = 'asc' } = req.query; - const pageInt = parseInt(page as string, 10); - const limitInt = parseInt(limit as string, 10); - - // Verificar si el usuario existe - const userExists = await checkIfUserExists(username); - - if (userExists) { - const posts = await getPostsByUsernameFromDatabase(username, pageInt, limitInt, orderBy as string, order as string); - - if (posts) { // Verificar si se devolvieron posts - res.status(200).json({ - ok: true, - data: posts, - pagination: { - page: pageInt, - pageSize: limitInt, - nextPage: posts.length === limitInt ? pageInt + 1 : null, - previousPage: pageInt > 1 ? pageInt - 1 : null - } - }); - } else { - res.status(404).json({ ok: false, message: 'No se encontraron posts para el usuario especificado' }); - } - } else { - res.status(404).json({ ok: false, message: 'El usuario especificado no existe' }); + try { + const postData: Post = req.body; + const newPost = await createPost(req.userId, postData); + res.status(201).json({ + ok: true, + data: newPost, + }); + } catch (error) { + next(new ApiError("Bad Request", 400)); } - } catch (error) { - console.error('Error al obtener los posts:', error); - res.status(500).json({ ok: false, message: 'Error al obtener los posts' }); } -}); - -//CREA UN NUEVO POST -postsRouter.post("/posts", authenticateHandler, async (req, res) => { - try { - const { content } = req.body; - - // Verificar si el usuario está autenticado - if (!req.userId) { - return res.status(401).json({ ok: false, message: "Usuario no autenticado" }); - } - - // Verificar si se proporciona el contenido del post - if (!content || content.trim() === "") { - return res.status(400).json({ ok: false, message: "El contenido del post no puede estar vacío" }); +); + +//PATCH/posts/:id: + +postsRouter.patch( + "/:id", + authenticateHandler, + async (req: Request, res: Response, next: NextFunction) => { + try { + const postData: Post = req.body; + const { id } = req.params; + const post = await updatePost(Number(id), postData); + res.json({ + ok: true, + data: post, + }); + } catch (error) { + console.log(error); + next(new ApiError("Bad Request", 400)); } - - // Crear el nuevo post si el usuario está autenticado - const newPost = await createPost(req.userId, content); - - // Responder con el nuevo post creado - res.status(201).json({ ok: true, data: newPost }); - } catch (error) { - console.error("Error al crear el post:", error); - res.status(500).json({ ok: false, message: "Error al crear el post" }); } +); + +postsRouter.get("/posts", async (req: Request, res: Response) => { + const filters: PostFilters = { + username: req.query["username"] as string, + }; + const sort = req.query["sort"] as string | undefined; + const page = Number(req.query["page"]) || 1; + const limit = Number(req.query["limit"]) || 10; + + const posts = await getPosts(filters, sort, page, limit); + + //pagination: + const totalItems: number = await getPostsCount(filters); + const totalPages = Math.ceil(totalItems / limit); + + res.json({ + ok: true, + data: [{ posts: posts }], + pagination: { + page: 1, + pageSize: 10, + totalItems: 20, + totalPages: 2, + nextPage: page < totalPages ? page + 1 : null, + previousPage: page > 1 ? page - 1 : null, + }, + }); }); -//EDITA UN POST -postsRouter.patch("/posts/:id", authenticateHandler, async (req, res) => { - const postId = parseInt(req.params.id); - const { content } = req.body; +//GET/posts/:username?page=2&limit=5 +postsRouter.get("/posts/:username", async (req: Request, res: Response) => { try { - // Verificar si el post existe - const existingPost = await getPostById(postId); - if (!existingPost) { - return res.status(404).json({ ok: false, message: "El post no existe" }); - } - - if (!content || content.trim() === "") { - return res.status(400).json({ ok: false, message: "El contenido actualizado del post no puede estar vacío" }); - } - - const updatedPost = await editPost(postId, content); - - res.status(200).json({ ok: true, data: updatedPost }); + const { username } = req.params; + const result = await getPostsByUsername(username); + res.json({ + ok: true, + result: result, + }); } catch (error) { - console.error("Error al editar el post:", error); - res.status(500).json({ ok: false, message: "Error al editar el post" }); + res.status(404).json({ ok: false, message: "User doesn't exist" }); } }); export default postsRouter; + + diff --git a/src/routers/users.routers.ts b/src/routers/users.routers.ts index 79e59af..7c56426 100644 --- a/src/routers/users.routers.ts +++ b/src/routers/users.routers.ts @@ -30,31 +30,40 @@ userRouter.get("/me", authenticateHandler, async (req, res, next) => { }); -userRouter.patch("/me", authenticateHandler, async (req, res, next) => { - if (req.userId === undefined) { - return res.status(401).json({ ok: false, message: "No autorizado" }); - } - try { - const user: User = req.body; - const updatedUser = await updateUsers(req.userId, req.body); - if (!updatedUser) { - return res.status(404).json({ ok: false, message: "Usuario no encontrado" }); +//PATCH/me: +userRouter.patch( + "/", + authenticateHandler, + async (req, res, next) => { + if (req.userId === undefined) { + return next(new ApiError("Unauthorized", 401)); + } + try { + const userData: User = req.body; + const profile = await updateUsers(req.userId, userData); + res.json({ + ok: true, + message: "User updated successfully", + data: { + id: profile.id, + username: profile.username, + email: profile.email, + firstName: profile.firstname, + lastName: profile.lastname, + createdAt: profile.createdat, + updatedAt: profile.updatedat, + }, + }); + } catch (error) { + next( + new ApiError( + "Bad request: only firstName, lastName or email can be edited", + 401 + ) + ); } - res.status(200).json({ - ok: true, - data: { - id: user.id, - username: user.username, - firstName: user.firstname, - email: user.email, - createdAt: user.createdat, - updatedAt: user.updatedat - } - }); - } catch (error) { - next(error); } -}); +); userRouter.delete("/me", authenticateHandler, async (req, res, next) => { if (req.userId === undefined) { diff --git a/src/services/posts.service.ts b/src/services/posts.service.ts index a2a523c..bb3cc4a 100644 --- a/src/services/posts.service.ts +++ b/src/services/posts.service.ts @@ -1,45 +1,46 @@ +import { + createPostDB, + getPostsByUsernameFromDB, + getPostsCountFromDB, + getPostsFromDB, + updatePostDB, +} from "../data/posts.data"; +import { Post, PostFilters } from "../models/posts"; -import { Post, PostFilters } from '../models/posts'; -import { ApiError } from '../middlewares/error'; -import * as postData from '../data/posts.data'; -import { createPost, getPostById, getPostsByUsernameFromDatabase} from '../data/posts.data'; - -export async function getPostsbyUsername(filters: PostFilters = {}, orderBy: string = 'createdAt', order: string = 'asc', page: number = 1, limit: number = 10): Promise { - try { - const posts = await postData.getPosts(page, limit, filters, orderBy, order); - return posts; - } catch (error) { - throw new ApiError('Error al obtener los posts desde la base de datos', 400); - } +//POST/posts: +export async function createPost(id: number, post: Post) { + const newPost: Post = await createPostDB(id, post); + return newPost; } -export async function getPostsUsername(username: string): Promise { - try { - const posts = await getPostsByUsernameFromDatabase(username); - return posts; - } catch (error) { - throw new ApiError('Error al obtener los posts desde la base de datos', 400); - } +//PATCH/posts/:id: +export async function updatePost(id: number, post: Post) { + const dataPost = { + id, + fieldsToUpdate: post, + }; + const updatePost: Post = await updatePostDB(dataPost); + return updatePost; } -export async function createNewPost(userId: number, content: string): Promise { - try { - const newPost = await createPost(userId, content); - return newPost; - } catch (error) { - throw new Error("Error al crear el nuevo post"); - } +//GET/posts +export async function getPosts( + filters: PostFilters = {}, + sort?: string, + page: number = 1, + limit: number = 10 +): Promise { + return await getPostsFromDB(filters, sort, page, limit); } -// Editar un post existente -export async function getPost(postId: number) { - try { - const post = await getPostById(postId); - if (!post) { - throw new Error('El post no existe'); - } - return post; - } catch (error) { - throw new Error('Error al obtener el post'); - } +export async function getPostsCount( + filters: PostFilters = {} +): Promise { + return getPostsCountFromDB(filters); } + +//GET/posts/:username: +export async function getPostsByUsername(username: string) { + const posts = await getPostsByUsernameFromDB(username); + return posts; +} \ No newline at end of file diff --git a/src/tests/posts.test.ts b/src/tests/posts.test.ts index ebde4ea..de64bd7 100644 --- a/src/tests/posts.test.ts +++ b/src/tests/posts.test.ts @@ -8,7 +8,7 @@ describe("Backend Testing", () => { // Antes de cada prueba, limpiar y configurar la base de datos beforeEach(async () => { // Limpia todas las tablas relevantes - await truncateTable("Posts"); + await truncateTable("posts"); // Inserta datos de prueba en las tablas await db.query(` diff --git a/src/tests/users.test.ts b/src/tests/users.test.ts index 2d88cc2..1c11be2 100644 --- a/src/tests/users.test.ts +++ b/src/tests/users.test.ts @@ -8,7 +8,7 @@ describe("Backend Testing", () => { // Antes de cada prueba, limpiar y configurar la base de datos beforeEach(async () => { // Limpia todas las tablas relevantes - await truncateTable("Users"); + await truncateTable("users"); // Inserta datos de prueba en las tablas await db.query(` From 166c5494e36455839f4b60fce8334d10f1e5cb7b Mon Sep 17 00:00:00 2001 From: carusi99 Date: Mon, 13 May 2024 13:16:14 -0500 Subject: [PATCH 37/37] changes in logic --- src/data/users.data.ts | 47 +++++++------ src/models/posts.ts | 26 +++---- src/models/users.ts | 3 +- src/routers/users.routers.ts | 2 +- src/services/users.service.ts | 14 ++-- src/tests/likes.test.ts | 33 --------- src/tests/posts.test.ts | 54 -------------- src/tests/try.test.ts | 129 ++++++++++++++++++++++++++++++++++ src/tests/users.test.ts | 61 ---------------- 9 files changed, 177 insertions(+), 192 deletions(-) delete mode 100644 src/tests/likes.test.ts delete mode 100644 src/tests/posts.test.ts create mode 100644 src/tests/try.test.ts delete mode 100644 src/tests/users.test.ts diff --git a/src/data/users.data.ts b/src/data/users.data.ts index 9645a2a..c2ac09a 100644 --- a/src/data/users.data.ts +++ b/src/data/users.data.ts @@ -12,30 +12,31 @@ export async function getUser(id: number): Promise { //ACTUALIZA UN USUARIO -// Función para actualizar un usuario en la base de datos -export async function updateUser(userId: number, newData: UpdateUserParams): Promise { - try { - const query = ` - UPDATE users - SET username = $1, email = $2 - WHERE id = $3 - RETURNING *;`; // Consulta SQL para actualizar el usuario y devolver los datos actualizados - - const username = newData.username ?? ''; // Si newData.username es undefined, se asigna una cadena vacía -const email = newData.email ?? ''; // Si newData.email es undefined, se asigna una cadena vacía - -const params: (string | number | boolean)[] = [username, email, userId]; - - // Parámetros para la consulta SQL - const { rows } = await db.query(query, params); // Ejecuta la consulta SQL con los parámetros y obtén el resultado - - // Devuelve el usuario actualizado - return rows[0]; - } catch (error) { - // En caso de error, registra el error y lanza una instancia de ApiError - console.error('Error al actualizar el usuario:', error); - throw new ApiError('Error al actualizar el usuario', 500); +export async function editUser({ + id, + fieldsToUpdate, +}: UpdateUserParams): Promise { + if (!id || Object.keys(fieldsToUpdate).length === 0) { + throw new Error("No se proporcionaron datos para actualizar"); } + const allowedFields = ["email", "firstname", "lastname"]; // Lista de campos permitidos para actualizar + const validFieldsToUpdate = Object.keys(fieldsToUpdate).filter((field) => + allowedFields.includes(field) + ); + const setClauses = validFieldsToUpdate.map( + (field, index) => `${field} = $${index + 1}` + ); + const updateQuery = `UPDATE users SET ${setClauses.join(", ")} WHERE id = $${ + validFieldsToUpdate.length + 1 + } RETURNING *`; + + const params = [ + ...validFieldsToUpdate.map((field) => fieldsToUpdate[field]), + id, + ]; + const result = await db.query(updateQuery, params); + + return result.rows[0]; // Devuelve el usuario actualizado } diff --git a/src/models/posts.ts b/src/models/posts.ts index cf4f7e8..db8f498 100644 --- a/src/models/posts.ts +++ b/src/models/posts.ts @@ -1,17 +1,19 @@ -export interface Post { - content: string; - createdAt: Date; - updatedAt: Date; - username: string; - nextPage: number | null; // Cambiado de 'null' a 'number | null' - previousPage: number | null; // Cambiado de 'null' a 'number | null' -} +import { z } from "zod"; -// post-filter.ts -export interface PostFilters { - username?: string; -} +export const postSchema = z.object({ + content: z.string(), + createdat: z.string(), + updatedat: z.string(), + username: z.string(), +}); + +export type PostParams = z.infer; +export type Post = PostParams & { id?: number }; + +export type PostFilters = { + username?: string; +}; export interface UpdatePostParams { id: number; fieldsToUpdate: Record; diff --git a/src/models/users.ts b/src/models/users.ts index ae3ac57..c8af64b 100644 --- a/src/models/users.ts +++ b/src/models/users.ts @@ -25,11 +25,12 @@ export const userSchema = z.object({ .optional(), createdat: z.string().optional(), updatedat: z.string().optional(), + }); export type UserParams = z.infer; -export type User = UserParams & { id: number }; +export type User = UserParams & { id: number, [key: string]: any; }; export interface UpdateUserParams { id: number; diff --git a/src/routers/users.routers.ts b/src/routers/users.routers.ts index 7c56426..f2f27fe 100644 --- a/src/routers/users.routers.ts +++ b/src/routers/users.routers.ts @@ -32,7 +32,7 @@ userRouter.get("/me", authenticateHandler, async (req, res, next) => { //PATCH/me: userRouter.patch( - "/", + "/user/me", authenticateHandler, async (req, res, next) => { if (req.userId === undefined) { diff --git a/src/services/users.service.ts b/src/services/users.service.ts index 9747dbc..eb0bb4b 100644 --- a/src/services/users.service.ts +++ b/src/services/users.service.ts @@ -7,13 +7,13 @@ export async function getUsers(id: number): Promise { } -export async function updateUsers(id: number, user: User){ - const updatedUser = { - id, - fieldsToUpdate: user - } -const result: User = await userDB.updateUser(id,updatedUser); -return result; +export async function updateUsers(id: number, user: User) { + const dataUser = { + id, + fieldsToUpdate: user, + }; + const updateProfile: User = await userDB.editUser(dataUser); + return updateProfile; } export async function deleteUsers(id: number): Promise { diff --git a/src/tests/likes.test.ts b/src/tests/likes.test.ts deleted file mode 100644 index f406903..0000000 --- a/src/tests/likes.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { describe, beforeEach, it, expect } from "vitest"; -import request from "supertest"; -import { app } from "../app"; // Importa tu aplicación Express -import { truncateTable } from "../db/utils"; // Funciones auxiliares para configurar la base de datos -import * as db from "../db"; - -describe("Backend Testing", () => { - // Antes de cada prueba, limpiar y configurar la base de datos - beforeEach(async () => { - // Limpia todas las tablas relevantes - await truncateTable("likes"); - await db.query(` - INSERT INTO Likes (postId, userId, createdAt) - VALUES (1, 1, NOW()), - (1, 2, NOW()); - `); -}); -it("should like a post", async () => { - const response = await request(app).post("/posts/1/like").set('Authorization', 'Bearer your-auth-token'); - expect(response.statusCode).toBe(200); - expect(response.body.ok).toBeTruthy(); - expect(response.body.data.postid).toBe(1); - expect(response.body.data.userid).toBe(1); - }); - it("should unlike a post", async () => { - const response = await request(app).delete("/posts/1/like").set('Authorization', 'Bearer your-auth-token'); - expect(response.statusCode).toBe(200); - expect(response.body.ok).toBeTruthy(); - expect(response.body.data.postid).toBe(1); - expect(response.body.data.userid).toBe(1); - }); - -}) \ No newline at end of file diff --git a/src/tests/posts.test.ts b/src/tests/posts.test.ts deleted file mode 100644 index de64bd7..0000000 --- a/src/tests/posts.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { describe, beforeEach, it, expect } from "vitest"; -import request from "supertest"; -import { app } from "../app"; // Importa tu aplicación Express -import { truncateTable } from "../db/utils"; // Funciones auxiliares para configurar la base de datos -import * as db from "../db"; - -describe("Backend Testing", () => { - // Antes de cada prueba, limpiar y configurar la base de datos - beforeEach(async () => { - // Limpia todas las tablas relevantes - await truncateTable("posts"); - - // Inserta datos de prueba en las tablas - await db.query(` - INSERT INTO Posts (userId, content, createdAt, updatedAt) - VALUES (1, 'Post 1 by user 1', NOW(), NOW()), - (2, 'Post 1 by user 2', NOW(), NOW()); - `); - }); - it("should get all posts", async () => { - const response = await request(app).get("/posts"); - expect(response.statusCode).toBe(200); - expect(response.body.ok).toBeTruthy(); - expect(response.body.data).toHaveLength(2); - }); - it("should get posts by username", async () => { - const response = await request(app).get("/posts/user1"); - expect(response.statusCode).toBe(200); - expect(response.body.ok).toBeTruthy(); - expect(response.body.data).toHaveLength(1); - expect(response.body.data[0].username).toBe("user1"); - }); - - it("should create a new post", async () => { - const newPost = { - content: "New post content" - }; - const response = await request(app).post("/posts").send(newPost); - expect(response.statusCode).toBe(201); - expect(response.body.ok).toBeTruthy(); - expect(response.body.data.content).toBe(newPost.content); - }); - it("should update a post", async () => { - const postIdToUpdate = 1; - const updatedPostData = { - content: "Updated post content" - }; - const response = await request(app).patch(`/posts/${postIdToUpdate}`).send(updatedPostData); - expect(response.statusCode).toBe(200); - expect(response.body.ok).toBeTruthy(); - expect(response.body.data.content).toBe(updatedPostData.content); - }); - -}); diff --git a/src/tests/try.test.ts b/src/tests/try.test.ts new file mode 100644 index 0000000..eeb9bb9 --- /dev/null +++ b/src/tests/try.test.ts @@ -0,0 +1,129 @@ +import { describe, beforeEach, it, expect } from "vitest"; +import request from "supertest"; +import { truncateTable } from "../db/utils"; +import { app } from "../app"; +import * as db from "../db"; +const jwt = require("jsonwebtoken"); +const jwtSecret = "ultra-secret"; + +const testUsers = [ + { + id: 1, + username: "usuario 1", + password: 123456, + email: "prueba@mail.com", + firstname: "Testino", + lastname: "diprueba", + role: "user", + }, + { + id: 2, + username: "usuario 2", + password: 123456, + email: "testino@mail.com", + firstname: "Diprueba", + lastname: "Test", + role: "user", + }, +]; + +const testPosts = [ + { userid: 1, content: "Im a testing" }, + { userid: 2, content: "Hello world" }, +]; + +//USERS TABLE: +beforeEach(async () => { + await truncateTable("users"); + const values = testUsers + .map( + (user) => + `('${user.username}','${user.firstname}','${user.lastname}','${user.email}','${user.password}','${user.role}')` + ) + .join(", "); + let query = `INSERT INTO users (username, firstname, lastname,email, password, role) VALUES ${values} RETURNING *`; + + await db.query(query); +}); + +//POSTS TABLE: +describe("posts API", () => { + beforeEach(async () => { + await truncateTable("posts"); + const values = testPosts + .map((post) => `('${post.userid}','${post.content}')`) + .join(", "); + let query = `INSERT INTO posts (userid, content) VALUES ${values} RETURNING id,content,createdat,updatedat,(SELECT u.username FROM users AS u WHERE u.id =posts.userid) AS username, 0 AS likesCount`; + + await db.query(query); + }); + + // GET/posts: + it("should get all posts", async () => { + const response = await request(app).get("/posts"); + expect(response.statusCode).toBe(200); + expect(response.body.ok).toBeTruthy(); + expect(response.body.data).toHaveLength(1); + }); + + //GET/posts/:usename: + it("should get the user posts with username", async () => { + const response = await request(app).get("/posts/usuario 1"); + expect(response.statusCode).toBe(200); + expect(response.body.ok).toBeTruthy(); + }); + + //POST/posts: + it("should create a post", async () => { + //TOKEN + const payload = { userId: 1, userRole: "user" }; + const token = jwt.sign(payload, jwtSecret, { expiresIn: "120m" }); + + const postData = { content: "new user" }; + let response = await request(app) + .post("/") + .set("Authorization", `Bearer ${token}`) + .send(postData); + expect(response.statusCode).toBe(201); + expect(response.body.data).toMatchObject(postData); + }); + + //PATCH/posts/:id: + it("should update a post", async () => { + //TOKEN: + const payload = { userId: 1, userRole: "user" }; + const token = jwt.sign(payload, jwtSecret, { expiresIn: "120m" }); + + const updates = { content: "im a update" }; + const response = await request(app) + .patch("/2") + .set("Authorization", `Bearer ${token}`) + .send(updates); + expect(response.statusCode).toBe(200); + expect(response.body.data).toMatchObject(updates); + }); + + //POST/:postId/like + it("should give like to a post", async () => { + //TOKEN: + const payload = { userId: 1, userRole: "user" }; + const token = jwt.sign(payload, jwtSecret, { expiresIn: "120m" }); + + const response = await request(app) + .post("/posts/1/like") + .set("Authorization", `Bearer ${token}`); + expect(response.statusCode).toBe(200); + }); + + //DELETE/:postId/like: + it("should delete the like post", async () => { + //TOKEN: + const payload = { userId: 1, userRole: "user" }; + const token = jwt.sign(payload, jwtSecret, { expiresIn: "120m" }); + + const response = await request(app) + .delete("/posts/1/like") + .set("Authorization", `Bearer ${token}`); + expect(response.statusCode).toBe(200); + }); +}); \ No newline at end of file diff --git a/src/tests/users.test.ts b/src/tests/users.test.ts deleted file mode 100644 index 1c11be2..0000000 --- a/src/tests/users.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { describe, beforeEach, it, expect } from "vitest"; -import request from "supertest"; -import { app } from "../app"; // Importa tu aplicación Express -import { truncateTable } from "../db/utils"; // Funciones auxiliares para configurar la base de datos -import * as db from "../db"; - -describe("Backend Testing", () => { - // Antes de cada prueba, limpiar y configurar la base de datos - beforeEach(async () => { - // Limpia todas las tablas relevantes - await truncateTable("users"); - - // Inserta datos de prueba en las tablas - await db.query(` - INSERT INTO Users (username, password, email, firstName, lastName, role, createdAt, updatedAt) - VALUES ('user1', 'password1', 'user1@example.com', 'John', 'Doe', 'user', NOW(), NOW()), - ('user2', 'password2', NULL, 'Jane', NULL, 'admin', NOW(), NOW()); - `); - }); - - it("should get all users", async () => { - const response = await request(app).get("/me"); - expect(response.statusCode).toBe(200); - expect(response.body.ok).toBeTruthy(); - expect(response.body.data).toHaveLength(2); - }); - - it("should update a user", async () => { - const userIdToUpdate = 1; - const updatedUserData = { - firstName: "Updated", - lastName: "User", - email: "updateduser@example.com", - role: "admin", - }; - const response = await request(app) - .patch(`/users/${userIdToUpdate}`) - .send(updatedUserData); - expect(response.statusCode).toBe(200); - expect(response.body.ok).toBeTruthy(); - // Verificar que los datos actualizados coincidan con los enviados - expect(response.body.data.firstName).toBe(updatedUserData.firstName); - expect(response.body.data.lastName).toBe(updatedUserData.lastName); - expect(response.body.data.email).toBe(updatedUserData.email); - expect(response.body.data.role).toBe(updatedUserData.role); - }); - - it("should delete a user", async () => { - const userIdToDelete = 1; - const response = await request(app).delete(`/users/${userIdToDelete}`); - expect(response.statusCode).toBe(200); - expect(response.body.ok).toBeTruthy(); - // Verificar que el usuario haya sido eliminado - const deletedUserResponse = await request(app).get( - `/users/${userIdToDelete}` - ); - expect(deletedUserResponse.statusCode).toBe(404); - expect(deletedUserResponse.body.ok).toBeFalsy(); - expect(deletedUserResponse.body.error).toBe("User not found"); - }); -});