diff --git a/.gitignore b/.gitignore index 2ea351a..a2c550f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ node_modules coverage # env +.env env/* !env/.env.example @@ -23,4 +24,5 @@ build npm-debug.log* yarn-debug.log* -yarn-error.log* \ No newline at end of file +yarn-error.log* +/src/generated/prisma diff --git a/README.md b/README.md index aad707d..08c0c38 100644 --- a/README.md +++ b/README.md @@ -17,4 +17,4 @@ 1. Node.js 2. express -3. mongoDB +3. ~~mongoDB~~ diff --git a/env/.env.example b/env/.env.example index 2090559..1eee4f5 100644 --- a/env/.env.example +++ b/env/.env.example @@ -1,3 +1,4 @@ NODE_ENV= PORT= -MONGO_URI= \ No newline at end of file +DATABASE_URL= +FRONT_URL= \ No newline at end of file diff --git a/package.json b/package.json index 114629c..f53ea4b 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "type": "module", "scripts": { "dev": "nodemon --env-file=./env/.env.development src/server.js", - "start": "node --env-file=/etc/secrets/.env.production src/server.js", + "start": "node src/server.js", + "seed": "node scripts/seed.js", "format": "prettier --write src/**/*.js", "format:check": "prettier --check src/**/*.js" }, @@ -19,14 +20,17 @@ "npm": "^11.6.0" }, "dependencies": { + "@prisma/client": "^6.16.2", + "@faker-js/faker": "^10.0.0", "express": "^5.1.0", - "mongoose": "^8.18.2", "zod": "^4.1.11" }, "devDependencies": { "@eslint/js": "^9.36.0", + "@faker-js/faker": "^10.0.0", "eslint": "^9.36.0", "nodemon": "^3.1.10", - "prettier": "^3.6.2" + "prettier": "^3.6.2", + "prisma": "^6.16.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 84f887f..4cf54a4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,12 +8,15 @@ importers: .: dependencies: + '@faker-js/faker': + specifier: ^10.0.0 + version: 10.0.0 + '@prisma/client': + specifier: ^6.16.2 + version: 6.16.2(prisma@6.16.2) express: specifier: ^5.1.0 version: 5.1.0 - mongoose: - specifier: ^8.18.2 - version: 8.18.2 zod: specifier: ^4.1.11 version: 4.1.11 @@ -23,13 +26,16 @@ importers: version: 9.36.0 eslint: specifier: ^9.36.0 - version: 9.36.0 + version: 9.36.0(jiti@2.6.0) nodemon: specifier: ^3.1.10 version: 3.1.10 prettier: specifier: ^3.6.2 version: 3.6.2 + prisma: + specifier: ^6.16.2 + version: 6.16.2 packages: @@ -71,6 +77,10 @@ packages: resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@faker-js/faker@10.0.0': + resolution: {integrity: sha512-UollFEUkVXutsaP+Vndjxar40Gs5JL2HeLcl8xO1QAjJgOdhc3OmBFWyEylS+RddWaaBiAzH+5/17PLQJwDiLw==} + engines: {node: ^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0, npm: '>=10'} + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -87,8 +97,38 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} - '@mongodb-js/saslprep@1.3.0': - resolution: {integrity: sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==} + '@prisma/client@6.16.2': + resolution: {integrity: sha512-E00PxBcalMfYO/TWnXobBVUai6eW/g5OsifWQsQDzJYm7yaY+IRLo7ZLsaefi0QkTpxfuhFcQ/w180i6kX3iJw==} + engines: {node: '>=18.18'} + peerDependencies: + prisma: '*' + typescript: '>=5.1.0' + peerDependenciesMeta: + prisma: + optional: true + typescript: + optional: true + + '@prisma/config@6.16.2': + resolution: {integrity: sha512-mKXSUrcqXj0LXWPmJsK2s3p9PN+aoAbyMx7m5E1v1FufofR1ZpPoIArjjzOIm+bJRLLvYftoNYLx1tbHgF9/yg==} + + '@prisma/debug@6.16.2': + resolution: {integrity: sha512-bo4/gA/HVV6u8YK2uY6glhNsJ7r+k/i5iQ9ny/3q5bt9ijCj7WMPUwfTKPvtEgLP+/r26Z686ly11hhcLiQ8zA==} + + '@prisma/engines-version@6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43': + resolution: {integrity: sha512-ThvlDaKIVrnrv97ujNFDYiQbeMQpLa0O86HFA2mNoip4mtFqM7U5GSz2ie1i2xByZtvPztJlNRgPsXGeM/kqAA==} + + '@prisma/engines@6.16.2': + resolution: {integrity: sha512-7yf3AjfPUgsg/l7JSu1iEhsmZZ/YE00yURPjTikqm2z4btM0bCl2coFtTGfeSOWbQMmq45Jab+53yGUIAT1sjA==} + + '@prisma/fetch-engine@6.16.2': + resolution: {integrity: sha512-wPnZ8DMRqpgzye758ZvfAMiNJRuYpz+rhgEBZi60ZqDIgOU2694oJxiuu3GKFeYeR/hXxso4/2oBC243t/whxQ==} + + '@prisma/get-platform@6.16.2': + resolution: {integrity: sha512-U/P36Uke5wS7r1+omtAgJpEB94tlT4SdlgaeTc6HVTTT93pXj7zZ+B/cZnmnvjcNPfWddgoDx8RLjmQwqGDYyA==} + + '@standard-schema/spec@1.0.0': + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -96,12 +136,6 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/webidl-conversions@7.0.3': - resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==} - - '@types/whatwg-url@11.0.5': - resolution: {integrity: sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==} - accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} @@ -148,14 +182,18 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - bson@6.10.4: - resolution: {integrity: sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==} - engines: {node: '>=16.20.1'} - bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} + c12@3.1.0: + resolution: {integrity: sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==} + peerDependencies: + magicast: ^0.3.5 + peerDependenciesMeta: + magicast: + optional: true + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -176,6 +214,13 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + citty@0.1.6: + resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -186,6 +231,13 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + confbox@0.2.2: + resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + content-disposition@1.0.0: resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} engines: {node: '>= 0.6'} @@ -218,10 +270,24 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deepmerge-ts@7.1.5: + resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==} + engines: {node: '>=16.0.0'} + + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -229,6 +295,13 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + effect@3.16.12: + resolution: {integrity: sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==} + + empathic@2.0.0: + resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} + engines: {node: '>=14'} + encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} @@ -302,6 +375,13 @@ packages: resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} engines: {node: '>= 18'} + exsolve@1.0.7: + resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} + + fast-check@3.23.2: + resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} + engines: {node: '>=8.0.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -358,6 +438,10 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + giget@2.0.0: + resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==} + hasBin: true + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -446,6 +530,10 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + jiti@2.6.0: + resolution: {integrity: sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ==} + hasBin: true + js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true @@ -459,10 +547,6 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - kareem@2.6.3: - resolution: {integrity: sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==} - engines: {node: '>=12.0.0'} - keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -485,9 +569,6 @@ packages: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} - memory-pager@1.5.0: - resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} - merge-descriptors@2.0.0: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} engines: {node: '>=18'} @@ -503,48 +584,6 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - mongodb-connection-string-url@3.0.2: - resolution: {integrity: sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==} - - mongodb@6.18.0: - resolution: {integrity: sha512-fO5ttN9VC8P0F5fqtQmclAkgXZxbIkYRTUi1j8JO6IYwvamkhtYDilJr35jOPELR49zqCJgXZWwCtW7B+TM8vQ==} - engines: {node: '>=16.20.1'} - peerDependencies: - '@aws-sdk/credential-providers': ^3.188.0 - '@mongodb-js/zstd': ^1.1.0 || ^2.0.0 - gcp-metadata: ^5.2.0 - kerberos: ^2.0.1 - mongodb-client-encryption: '>=6.0.0 <7' - snappy: ^7.2.2 - socks: ^2.7.1 - peerDependenciesMeta: - '@aws-sdk/credential-providers': - optional: true - '@mongodb-js/zstd': - optional: true - gcp-metadata: - optional: true - kerberos: - optional: true - mongodb-client-encryption: - optional: true - snappy: - optional: true - socks: - optional: true - - mongoose@8.18.2: - resolution: {integrity: sha512-gA6GFlshOHUdNyw9OQTmMLSGzVOPbcbjaSZ1dvR5iMp668N2UUznTuzgTY6V6Q41VtBc4kmL/qqML1RNgXB5Fg==} - engines: {node: '>=16.20.1'} - - mpath@0.9.0: - resolution: {integrity: sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==} - engines: {node: '>=4.0.0'} - - mquery@5.0.0: - resolution: {integrity: sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==} - engines: {node: '>=14.0.0'} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -555,6 +594,9 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + nodemon@3.1.10: resolution: {integrity: sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==} engines: {node: '>=10'} @@ -564,10 +606,18 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + nypm@0.6.2: + resolution: {integrity: sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==} + engines: {node: ^14.16.0 || >=16.10.0} + hasBin: true + object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -606,10 +656,19 @@ packages: path-to-regexp@8.3.0: resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} + pkg-types@2.3.0: + resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -619,6 +678,16 @@ packages: engines: {node: '>=14'} hasBin: true + prisma@6.16.2: + resolution: {integrity: sha512-aRvldGE5UUJTtVmFiH3WfNFNiqFlAtePUxcI0UEGlnXCX7DqhiMT5TRYwncHFeA/Reca5W6ToXXyCMTeFPdSXA==} + engines: {node: '>=18.18'} + hasBin: true + peerDependencies: + typescript: '>=5.1.0' + peerDependenciesMeta: + typescript: + optional: true + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -630,6 +699,9 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + qs@6.14.0: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} @@ -642,10 +714,17 @@ packages: resolution: {integrity: sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==} engines: {node: '>= 0.10'} + rc9@2.1.2: + resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} + readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -700,16 +779,10 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} - sift@17.1.3: - resolution: {integrity: sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==} - simple-update-notifier@2.0.0: resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} engines: {node: '>=10'} - sparse-bitfield@3.0.3: - resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==} - statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} @@ -730,6 +803,9 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + tinyexec@1.0.1: + resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -742,10 +818,6 @@ packages: resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==} hasBin: true - tr46@5.1.1: - resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} - engines: {node: '>=18'} - type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -768,14 +840,6 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} - webidl-conversions@7.0.0: - resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} - engines: {node: '>=12'} - - whatwg-url@14.2.0: - resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} - engines: {node: '>=18'} - which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -797,9 +861,9 @@ packages: snapshots: - '@eslint-community/eslint-utils@4.9.0(eslint@9.36.0)': + '@eslint-community/eslint-utils@4.9.0(eslint@9.36.0(jiti@2.6.0))': dependencies: - eslint: 9.36.0 + eslint: 9.36.0(jiti@2.6.0) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} @@ -841,6 +905,8 @@ snapshots: '@eslint/core': 0.15.2 levn: 0.4.1 + '@faker-js/faker@10.0.0': {} + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -852,19 +918,45 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} - '@mongodb-js/saslprep@1.3.0': + '@prisma/client@6.16.2(prisma@6.16.2)': + optionalDependencies: + prisma: 6.16.2 + + '@prisma/config@6.16.2': dependencies: - sparse-bitfield: 3.0.3 + c12: 3.1.0 + deepmerge-ts: 7.1.5 + effect: 3.16.12 + empathic: 2.0.0 + transitivePeerDependencies: + - magicast - '@types/estree@1.0.8': {} + '@prisma/debug@6.16.2': {} - '@types/json-schema@7.0.15': {} + '@prisma/engines-version@6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43': {} - '@types/webidl-conversions@7.0.3': {} + '@prisma/engines@6.16.2': + dependencies: + '@prisma/debug': 6.16.2 + '@prisma/engines-version': 6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43 + '@prisma/fetch-engine': 6.16.2 + '@prisma/get-platform': 6.16.2 + + '@prisma/fetch-engine@6.16.2': + dependencies: + '@prisma/debug': 6.16.2 + '@prisma/engines-version': 6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43 + '@prisma/get-platform': 6.16.2 - '@types/whatwg-url@11.0.5': + '@prisma/get-platform@6.16.2': dependencies: - '@types/webidl-conversions': 7.0.3 + '@prisma/debug': 6.16.2 + + '@standard-schema/spec@1.0.0': {} + + '@types/estree@1.0.8': {} + + '@types/json-schema@7.0.15': {} accepts@2.0.0: dependencies: @@ -922,10 +1014,23 @@ snapshots: dependencies: fill-range: 7.1.1 - bson@6.10.4: {} - bytes@3.1.2: {} + c12@3.1.0: + dependencies: + chokidar: 4.0.3 + confbox: 0.2.2 + defu: 6.1.4 + dotenv: 16.6.1 + exsolve: 1.0.7 + giget: 2.0.0 + jiti: 2.6.0 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 1.0.0 + pkg-types: 2.3.0 + rc9: 2.1.2 + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -955,6 +1060,14 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + citty@0.1.6: + dependencies: + consola: 3.4.2 + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -963,6 +1076,10 @@ snapshots: concat-map@0.0.1: {} + confbox@0.2.2: {} + + consola@3.4.2: {} + content-disposition@1.0.0: dependencies: safe-buffer: 5.2.1 @@ -987,8 +1104,16 @@ snapshots: deep-is@0.1.4: {} + deepmerge-ts@7.1.5: {} + + defu@6.1.4: {} + depd@2.0.0: {} + destr@2.0.5: {} + + dotenv@16.6.1: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -997,6 +1122,13 @@ snapshots: ee-first@1.1.1: {} + effect@3.16.12: + dependencies: + '@standard-schema/spec': 1.0.0 + fast-check: 3.23.2 + + empathic@2.0.0: {} + encodeurl@2.0.0: {} es-define-property@1.0.1: {} @@ -1020,9 +1152,9 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@9.36.0: + eslint@9.36.0(jiti@2.6.0): dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0(jiti@2.6.0)) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.21.0 '@eslint/config-helpers': 0.3.1 @@ -1057,6 +1189,8 @@ snapshots: minimatch: 3.1.2 natural-compare: 1.4.0 optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.0 transitivePeerDependencies: - supports-color @@ -1112,6 +1246,12 @@ snapshots: transitivePeerDependencies: - supports-color + exsolve@1.0.7: {} + + fast-check@3.23.2: + dependencies: + pure-rand: 6.1.0 + fast-deep-equal@3.1.3: {} fast-json-stable-stringify@2.1.0: {} @@ -1176,6 +1316,15 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 + giget@2.0.0: + dependencies: + citty: 0.1.6 + consola: 3.4.2 + defu: 6.1.4 + node-fetch-native: 1.6.7 + nypm: 0.6.2 + pathe: 2.0.3 + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -1245,6 +1394,8 @@ snapshots: isexe@2.0.0: {} + jiti@2.6.0: {} + js-yaml@4.1.0: dependencies: argparse: 2.0.1 @@ -1255,8 +1406,6 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} - kareem@2.6.3: {} - keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -1276,8 +1425,6 @@ snapshots: media-typer@1.1.0: {} - memory-pager@1.5.0: {} - merge-descriptors@2.0.0: {} mime-db@1.54.0: {} @@ -1290,50 +1437,14 @@ snapshots: dependencies: brace-expansion: 1.1.12 - mongodb-connection-string-url@3.0.2: - dependencies: - '@types/whatwg-url': 11.0.5 - whatwg-url: 14.2.0 - - mongodb@6.18.0: - dependencies: - '@mongodb-js/saslprep': 1.3.0 - bson: 6.10.4 - mongodb-connection-string-url: 3.0.2 - - mongoose@8.18.2: - dependencies: - bson: 6.10.4 - kareem: 2.6.3 - mongodb: 6.18.0 - mpath: 0.9.0 - mquery: 5.0.0 - ms: 2.1.3 - sift: 17.1.3 - transitivePeerDependencies: - - '@aws-sdk/credential-providers' - - '@mongodb-js/zstd' - - gcp-metadata - - kerberos - - mongodb-client-encryption - - snappy - - socks - - supports-color - - mpath@0.9.0: {} - - mquery@5.0.0: - dependencies: - debug: 4.4.3(supports-color@5.5.0) - transitivePeerDependencies: - - supports-color - ms@2.1.3: {} natural-compare@1.4.0: {} negotiator@1.0.0: {} + node-fetch-native@1.6.7: {} + nodemon@3.1.10: dependencies: chokidar: 3.6.0 @@ -1349,8 +1460,18 @@ snapshots: normalize-path@3.0.0: {} + nypm@0.6.2: + dependencies: + citty: 0.1.6 + consola: 3.4.2 + pathe: 2.0.3 + pkg-types: 2.3.0 + tinyexec: 1.0.1 + object-inspect@1.13.4: {} + ohash@2.0.11: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -1388,12 +1509,29 @@ snapshots: path-to-regexp@8.3.0: {} + pathe@2.0.3: {} + + perfect-debounce@1.0.0: {} + picomatch@2.3.1: {} + pkg-types@2.3.0: + dependencies: + confbox: 0.2.2 + exsolve: 1.0.7 + pathe: 2.0.3 + prelude-ls@1.2.1: {} prettier@3.6.2: {} + prisma@6.16.2: + dependencies: + '@prisma/config': 6.16.2 + '@prisma/engines': 6.16.2 + transitivePeerDependencies: + - magicast + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -1403,6 +1541,8 @@ snapshots: punycode@2.3.1: {} + pure-rand@6.1.0: {} + qs@6.14.0: dependencies: side-channel: 1.1.0 @@ -1416,10 +1556,17 @@ snapshots: iconv-lite: 0.7.0 unpipe: 1.0.0 + rc9@2.1.2: + dependencies: + defu: 6.1.4 + destr: 2.0.5 + readdirp@3.6.0: dependencies: picomatch: 2.3.1 + readdirp@4.1.2: {} + resolve-from@4.0.0: {} router@2.2.0: @@ -1499,16 +1646,10 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 - sift@17.1.3: {} - simple-update-notifier@2.0.0: dependencies: semver: 7.7.2 - sparse-bitfield@3.0.3: - dependencies: - memory-pager: 1.5.0 - statuses@2.0.1: {} statuses@2.0.2: {} @@ -1523,6 +1664,8 @@ snapshots: dependencies: has-flag: 4.0.0 + tinyexec@1.0.1: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -1531,10 +1674,6 @@ snapshots: touch@3.1.1: {} - tr46@5.1.1: - dependencies: - punycode: 2.3.1 - type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -1555,13 +1694,6 @@ snapshots: vary@1.1.2: {} - webidl-conversions@7.0.0: {} - - whatwg-url@14.2.0: - dependencies: - tr46: 5.1.1 - webidl-conversions: 7.0.0 - which@2.0.2: dependencies: isexe: 2.0.0 diff --git a/prisma/migrations/20250930050719_init/migration.sql b/prisma/migrations/20250930050719_init/migration.sql new file mode 100644 index 0000000..4ee1efc --- /dev/null +++ b/prisma/migrations/20250930050719_init/migration.sql @@ -0,0 +1,46 @@ +-- CreateTable +CREATE TABLE "public"."Article" ( + "id" TEXT NOT NULL, + "title" TEXT NOT NULL, + "content" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Article_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."Product" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT NOT NULL, + "price" DECIMAL(65,30) NOT NULL, + "tags" TEXT[], + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Product_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."Comment" ( + "id" TEXT NOT NULL, + "content" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "articleId" TEXT, + "productId" TEXT, + + CONSTRAINT "Comment_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "Comment_articleId_idx" ON "public"."Comment"("articleId"); + +-- CreateIndex +CREATE INDEX "Comment_productId_idx" ON "public"."Comment"("productId"); + +-- AddForeignKey +ALTER TABLE "public"."Comment" ADD CONSTRAINT "Comment_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "public"."Article"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Comment" ADD CONSTRAINT "Comment_productId_fkey" FOREIGN KEY ("productId") REFERENCES "public"."Product"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20251001011055_add_update_at_on_comment/migration.sql b/prisma/migrations/20251001011055_add_update_at_on_comment/migration.sql new file mode 100644 index 0000000..4b4d3c0 --- /dev/null +++ b/prisma/migrations/20251001011055_add_update_at_on_comment/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `updatedAt` to the `Comment` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "public"."Comment" ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL; diff --git a/prisma/migrations/20251001043423_add_parent_type_on_comment/migration.sql b/prisma/migrations/20251001043423_add_parent_type_on_comment/migration.sql new file mode 100644 index 0000000..d40cc2e --- /dev/null +++ b/prisma/migrations/20251001043423_add_parent_type_on_comment/migration.sql @@ -0,0 +1,11 @@ +/* + Warnings: + + - Added the required column `parent` to the `Comment` table without a default value. This is not possible if the table is not empty. + +*/ +-- CreateEnum +CREATE TYPE "public"."ParentType" AS ENUM ('Article', 'Product'); + +-- AlterTable +ALTER TABLE "public"."Comment" ADD COLUMN "parent" "public"."ParentType" NOT NULL; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..cba7d6b --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,55 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? +// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model Article { + id String @id @default(cuid()) + title String + content String @db.Text + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + comments Comment[] +} + +model Product { + id String @id @default(cuid()) + name String + description String @db.Text + price Decimal + tags String[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + comments Comment[] +} + +enum ParentType { + Article + Product +} + +model Comment { + id String @id @default(cuid()) + content String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + parent ParentType + // Article의 댓글인 경우 + articleId String? + article Article? @relation(fields: [articleId], references: [id], onDelete: Cascade) + // Product의 댓글인 경우 + productId String? + product Product? @relation(fields: [productId], references: [id], onDelete: Cascade) + @@index([articleId]) + @@index([productId]) +} \ No newline at end of file diff --git a/scripts/seed.js b/scripts/seed.js new file mode 100644 index 0000000..ffab3ef --- /dev/null +++ b/scripts/seed.js @@ -0,0 +1,92 @@ +import { PrismaClient } from '@prisma/client'; +import { faker } from '@faker-js/faker'; + +const prisma = new PrismaClient(); + +async function main() { + console.log( + '시딩을 시작합니다... (모든 작업은 단일 트랜잭션으로 실행됩니다)', + ); + + await prisma.$transaction(async (tx) => { + // 1. 기존 데이터 삭제 + console.log('기존 데이터를 삭제합니다...'); + await tx.comment.deleteMany(); + await tx.article.deleteMany(); + await tx.product.deleteMany(); + console.log('기존 데이터 삭제 완료.'); + + const DATA_LENGTH = 15; + const paragraphCount = Math.max(1, Math.floor(Math.random() * 5)); + + // 2. Article 15개 생성 + console.log('Article 데이터를 생성합니다...'); + const articleCreatePromises = Array.from({ length: DATA_LENGTH }).map(() => + tx.article.create({ + data: { + title: faker.lorem.sentence({ min: 5, max: 10 }), + content: faker.lorem.paragraphs(paragraphCount), + }, + }), + ); + const articles = await Promise.all(articleCreatePromises); + console.log(`${articles.length}개의 Article이 생성되었습니다.`); + + // 3. Product 15개 생성 + console.log('Product 데이터를 생성합니다...'); + const productCreatePromises = Array.from({ length: DATA_LENGTH }).map(() => + tx.product.create({ + data: { + name: faker.commerce.productName({ max: 10 }), + description: faker.commerce.productDescription({ min: 10, max: 100 }), + price: faker.commerce.price({ min: 10000, max: 200000 }), + tags: Array.from({ length: Math.floor(Math.random() * 3) + 1 }, () => + faker.commerce.department(), + ), + }, + }), + ); + const products = await Promise.all(productCreatePromises); + console.log(`${products.length}개의 Product가 생성되었습니다.`); + + // 4. Article에 대한 Comment 생성 + console.log('Article에 대한 Comment를 생성합니다...'); + const articleCommentsData = articles.flatMap((article) => + Array.from({ length: 3 }).map(() => ({ + content: faker.lorem.sentence({ min: 10, max: 25 }), + parent: 'Article', + articleId: article.id, + })), + ); + const { count: articleCommentCount } = await tx.comment.createMany({ + data: articleCommentsData, + }); + console.log(`${articleCommentCount}개의 Article Comment가 생성되었습니다.`); + + // 5. Product에 대한 Comment 생성 + console.log('Product에 대한 Comment를 생성합니다...'); + const productCommentsData = products.flatMap((product) => + Array.from({ length: 3 }).map(() => ({ + content: faker.lorem.sentence({ min: 10, max: 25 }), + parent: 'Product', + productId: product.id, + })), + ); + const { count: productCommentCount } = await tx.comment.createMany({ + data: productCommentsData, + }); + console.log(`${productCommentCount}개의 Product Comment가 생성되었습니다.`); + }); + + console.log('시딩이 완료되었습니다.'); +} + +main() + .catch((err) => { + console.error('시딩 중 오류가 발생했습니다:', err); + process.exit(1); + }) + .finally(async () => { + // 스크립트 종료 시 Prisma Client 연결 해제 + await prisma.$disconnect(); + }); diff --git a/src/config/config.js b/src/config/config.js index 31f7e9c..1825520 100644 --- a/src/config/config.js +++ b/src/config/config.js @@ -5,7 +5,8 @@ const envSchema = z.object({ .enum(['development', 'production', 'test']) .default('development'), PORT: z.coerce.number().min(1000).max(65535), - MONGO_URI: z.string().startsWith('mongodb+srv://'), + DATABASE_URL: z.string().startsWith('postgresql://'), + FRONT_URL: z.string(), }); const parseEnvironment = () => { @@ -13,11 +14,12 @@ const parseEnvironment = () => { return envSchema.parse({ ENVIRONMENT: process.env.NODE_ENV, PORT: process.env.PORT, - MONGO_URI: process.env.MONGO_URI, + DATABASE_URL: process.env.DATABASE_URL, + FRONT_URL: process.env.FRONT_URL, }); - } catch (error) { - if (error instanceof z.ZodError) { - console.log('error.errors', error); + } catch (err) { + if (err instanceof z.ZodError) { + console.log('error.errors', err); } process.exit(1); } diff --git a/src/db/index.js b/src/db/index.js deleted file mode 100644 index 511bbaa..0000000 --- a/src/db/index.js +++ /dev/null @@ -1,16 +0,0 @@ -import mongoose from 'mongoose'; -import { config } from '../config/config.js'; - -export const connectDB = async () => { - try { - await mongoose.connect(config.MONGO_URI); - console.log('✅ MongoDB connected successfully.'); - } catch (err) { - console.error('❌ MongoDB connection error:', err); - process.exit(1); - } -}; - -export const disconnectDB = async () => { - await mongoose.connection.close(); -}; diff --git a/src/db/prisma.js b/src/db/prisma.js new file mode 100644 index 0000000..6638a03 --- /dev/null +++ b/src/db/prisma.js @@ -0,0 +1,24 @@ +import { PrismaClient } from '@prisma/client'; +import { isDevelopment } from '../config/config.js'; + +const getPrismaLogLevel = () => { + if (!isDevelopment) { + return ['warn', 'error']; + } + //개발 환경에서만 추가 로깅 개방 + return ['query', 'info', 'warn', 'error']; +}; + +export const prisma = new PrismaClient({ + log: getPrismaLogLevel(), +}); + +export async function disconnectDB() { + try { + await prisma.$disconnect(); + console.log('📦 Disconnected from the database.'); + } catch (e) { + console.error('❌ Error disconnecting from the database:', e); + process.exit(1); + } +} diff --git a/src/middlewares/cors.js b/src/middlewares/cors.js index 19cda78..a8da704 100644 --- a/src/middlewares/cors.js +++ b/src/middlewares/cors.js @@ -1,13 +1,11 @@ -import { isDevelopment } from '../config/config.js'; +import { isDevelopment, config } from '../config/config.js'; export const cors = (req, res, next) => { const origin = req.headers.origin || req.headers.host || req.headers.referer || ''; - const whiteList = [ - 'http://localhost:5001', - 'http://localhost:5173', - 'https://sprint-fs9-fe-8711b7.netlify.app', - ]; + const whiteList = config.FRONT_URL + ? config.FRONT_URL.split(',').map((url) => url.trim()) + : []; const isAllowed = whiteList.includes(origin); if (isAllowed || isDevelopment) { diff --git a/src/models/product.model.js b/src/models/product.model.js deleted file mode 100644 index 39b13d4..0000000 --- a/src/models/product.model.js +++ /dev/null @@ -1,32 +0,0 @@ -import mongoose from 'mongoose'; - -const { Schema } = mongoose; - -const productSchema = new Schema( - { - id: { - type: Schema.Types.ObjectId, - required: true, - unique: true, - auto: true, - }, - name: { type: String, required: true }, - description: { type: String, default: '' }, - price: { type: Number, required: true, min: 0 }, - tags: { type: [String], default: [] }, - createdAt: { type: Date, default: Date.now }, - updatedAt: { type: Date, default: Date.now }, - }, - { - toJSON: { virtuals: true }, - toObject: { virtuals: true }, - versionKey: false, - }, -); - -productSchema.pre('save', function (next) { - this.updatedAt = Date.now(); - next(); -}); - -export const Product = mongoose.model('Product', productSchema); diff --git a/src/repository/article.repository.js b/src/repository/article.repository.js new file mode 100644 index 0000000..bcdfd1e --- /dev/null +++ b/src/repository/article.repository.js @@ -0,0 +1,32 @@ +import { prisma } from '../db/prisma.js'; + +async function createArticle(data) { + return await prisma.article.create({ data }); +} + +async function findArticlesMany(options) { + return await prisma.$transaction([ + prisma.article.count({ where: options.where }), + prisma.article.findMany(options), + ]); +} + +async function findArticleById(id) { + return await prisma.article.findUnique({ where: { id: String(id) } }); +} + +async function updateArticle(id, data) { + return await prisma.article.update({ where: { id: String(id) }, data }); +} + +async function deleteArticle(id) { + return await prisma.article.delete({ where: { id: String(id) } }); +} + +export const articleRepository = { + createArticle, + findArticleById, + findArticlesMany, + updateArticle, + deleteArticle, +}; diff --git a/src/repository/comment.repository.js b/src/repository/comment.repository.js new file mode 100644 index 0000000..27c925b --- /dev/null +++ b/src/repository/comment.repository.js @@ -0,0 +1,72 @@ +import { prisma } from '../db/prisma.js'; + +async function createComment(data) { + return await prisma.comment.create({ data }); +} + +async function findCommentsInArticle() { + return await prisma.$transaction([ + prisma.comment.count({ where: { parent: String('Article') } }), + prisma.comment.findMany( + { where: { parent: String('Article') } }, + { orderBy: { createdAt: 'desc' } }, + ), + ]); +} + +async function findCommentsByArticleId(articleId) { + return await prisma.$transaction([ + prisma.comment.count({ where: { articleId: String(articleId) } }), + prisma.comment.findMany( + { where: { parent: String('Article') } }, + { orderBy: { createdAt: 'desc' } }, + ), + ]); +} + +async function findCommentsInProduct() { + return await prisma.$transaction([ + prisma.comment.count({ where: { parent: String('Product') } }), + prisma.comment.findMany( + { where: { parent: String('Product') } }, + { orderBy: { createdAt: 'desc' } }, + ), + ]); +} + +async function findCommentsByProductId(productId) { + return await prisma.$transaction([ + prisma.comment.count({ where: { productId: String(productId) } }), + prisma.comment.findMany( + { where: { parent: String('Product') } }, + { orderBy: { createdAt: 'desc' } }, + ), + ]); +} + +async function findCommentById(id) { + return await prisma.comment.findUnique({ where: { id: String(id) } }); +} + +async function updateComment(id, data) { + return await prisma.comment.update({ where: { id: String(id) }, data }); +} + +async function deleteComment(id) { + return await prisma.comment.delete({ where: { id: String(id) } }); +} + +export const commentRepository = { + findCommentsInArticle, + findCommentsInProduct, + findCommentsByArticleId, + findCommentsByProductId, + createComment, + findCommentById, + updateComment, + deleteComment, +}; + +function Grogu() {} + +Grogu(); diff --git a/src/repository/product.repository.js b/src/repository/product.repository.js new file mode 100644 index 0000000..b3e64be --- /dev/null +++ b/src/repository/product.repository.js @@ -0,0 +1,32 @@ +import { prisma } from '../db/prisma.js'; + +async function createProduct(data) { + return await prisma.product.create({ data }); +} + +async function findProductsMany(options) { + return await prisma.$transaction([ + prisma.product.count({ where: options.where }), + prisma.product.findMany(options), + ]); +} + +async function findProductById(id) { + return await prisma.product.findUnique({ where: { id: String(id) } }); +} + +async function updateProduct(id, data) { + return await prisma.product.update({ where: { id: String(id) }, data }); +} + +async function deleteProduct(id) { + return await prisma.product.delete({ where: { id: String(id) } }); +} + +export const productRepository = { + createProduct, + findProductById, + findProductsMany, + updateProduct, + deleteProduct, +}; diff --git a/src/routes/article.js b/src/routes/article.js new file mode 100644 index 0000000..42e0ae5 --- /dev/null +++ b/src/routes/article.js @@ -0,0 +1,175 @@ +import express from 'express'; +import { articleRepository as Article } from '../repository/article.repository.js'; +import { validateArticles } from '../validators/validateArticles.js'; +import { NotFoundException } from '../err/notFoundException.js'; +import { commentRepository as Comment } from '../repository/comment.repository.js'; +import { validateComments } from '../validators/validateComments.js'; + +export const articlesRouter = express.Router(); + +articlesRouter.get('/', async (req, res, next) => { + try { + const page = parseInt(req.query.page, 10) || 1; + const pageSize = parseInt(req.query.pageSize, 10) || 10; + const keyword = req.query.keyword; + const orderBy = req.query.orderBy; + const offset = (page - 1) * pageSize; + + const where = {}; + if (keyword) { + where.OR = [ + { title: { contains: keyword, mode: 'insensitive' } }, + { content: { contains: keyword, mode: 'insensitive' } }, + ]; + } + + const orderByOptions = []; + if (orderBy === 'recent') { + orderByOptions.push({ createdAt: 'desc' }); + } + + const [totalCount, articles] = await Article.findArticlesMany({ + where, + orderBy: orderByOptions, + skip: offset, + take: pageSize, + select: { + id: true, + title: true, + createdAt: true, + }, + }); + + res.json({ + success: true, + list: articles, + page, + pageSize, + totalCount, + }); + } catch (err) { + next(err); + } +}); + +articlesRouter.get('/:id', async (req, res, next) => { + try { + const { id } = req.params; + const article = await Article.findArticleById(id); + if (!article) { + throw new NotFoundException('글을 찾을 수 없습니다'); + } + res.json({ success: true, data: article }); + } catch (err) { + next(err); + } +}); + +articlesRouter.post('/', validateArticles, async (req, res, next) => { + try { + const { title, content } = req.body; + const newArticle = await Article.createArticle({ + title, + content, + }); + res.status(201).json({ + success: true, + data: newArticle, + message: '글이 정상적으로 추가되었습니다', + }); + } catch (err) { + next(err); + } +}); + +articlesRouter.patch('/:id', validateArticles, async (req, res, next) => { + try { + const { id } = req.params; + const articleExistence = await Article.findArticleById(id); + if (!articleExistence) { + throw new NotFoundException('글을 찾을 수가 없습니다.'); + } + const updatedArticle = await Article.updateArticle(id, req.body); + res.json({ + success: true, + data: updatedArticle, + message: '등록된 글 내용이 수정되었습니다', + }); + } catch (err) { + next(err); + } +}); + +articlesRouter.delete('/:id', async (req, res, next) => { + try { + const { id } = req.params; + const articleExistence = await Article.findArticleById(id); + if (!articleExistence) { + throw new NotFoundException('글을 찾을 수가 없습니다.'); + } + const deletedArticle = await Article.deleteArticle(id); + res.json({ + success: true, + data: deletedArticle, + message: '글이 삭제되었습니다', + }); + } catch (err) { + next(err); + } +}); + +articlesRouter.get('/comments', async (req, res, next) => { + try { + const [totalCount, comments] = await Comment.findCommentsInArticle(); + + res.json({ + success: true, + list: comments, + totalCount, + }); + } catch (err) { + next(err); + } +}); + +articlesRouter.get('/:articleId/comments', async (req, res, next) => { + try { + const { articleId } = req.params; + const [totalCount, comments] = + await Comment.findCommentsByArticleId(articleId); + + res.json({ success: true, list: comments, totalCount }); + } catch (err) { + next(err); + } +}); + +articlesRouter.post( + '/:articleId/comments', + validateComments, + async (req, res, next) => { + try { + const { articleId } = req.params; + const articleExistence = await Article.findArticleById(articleId); + if (!articleExistence) { + throw new NotFoundException( + '댓글을 단 글을 찾을 수가 없어요. 어디다 댓글을 쓴거야?', + ); + } + const { content } = req.body; + const parent = 'Article'; + const newArticleComment = await Comment.createComment({ + content, + parent, + articleId, + }); + res.json({ + success: true, + data: newArticleComment, + message: '댓글이 정상적으로 추가되었습니다', + }); + } catch (err) { + next(err); + } + }, +); diff --git a/src/routes/comment.js b/src/routes/comment.js new file mode 100644 index 0000000..ce07d36 --- /dev/null +++ b/src/routes/comment.js @@ -0,0 +1,42 @@ +import express from 'express'; +import { commentRepository as Comment } from '../repository/comment.repository.js'; +import { validateComments } from '../validators/validateComments.js'; +import { NotFoundException } from '../err/notFoundException.js'; + +export const commentsRouter = express.Router(); + +commentsRouter.patch('/:id', validateComments, async (req, res, next) => { + try { + const { id } = req.params; + const commentExistence = await Comment.findCommentById(id); + if (!commentExistence) { + throw new NotFoundException('댓글을 찾을 수 없습니다'); + } + const updatedComment = await Comment.updateComment(id, req.body); + res.json({ + success: true, + data: updatedComment, + message: '등록된 글 내용이 수정되었습니다', + }); + } catch (err) { + next(err); + } +}); + +commentsRouter.delete('/:id', async (req, res, next) => { + try { + const { id } = req.params; + const commentExistence = await Comment.findCommentById(id); + if (!commentExistence) { + throw new NotFoundException('댓글을 찾을 수 없습니다'); + } + const deletedComment = await Comment.deleteComment(id); + res.json({ + success: true, + data: deletedComment, + message: '글이 삭제되었습니다', + }); + } catch (err) { + next(err); + } +}); diff --git a/src/routes/index.js b/src/routes/index.js index 4a03dc6..ce896a0 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -1,5 +1,7 @@ import express from 'express'; import { productsRouter } from './products.js'; +import { articlesRouter } from './article.js'; +import { commentsRouter } from './comment.js'; export const router = express.Router(); @@ -13,3 +15,5 @@ router.get('/', (req, res) => { // 하위 라우트 등록 router.use('/products', productsRouter); +router.use('/articles', articlesRouter); +router.use('/comments', commentsRouter); diff --git a/src/routes/products.js b/src/routes/products.js index b466005..deb584e 100644 --- a/src/routes/products.js +++ b/src/routes/products.js @@ -1,7 +1,9 @@ import express from 'express'; -import { Product } from '../models/product.model.js'; -import { validateProducts } from '../middlewares/validateProducts.js'; +import { productRepository as Product } from '../repository/product.repository.js'; +import { validateProducts } from '../validators/validateProducts.js'; import { NotFoundException } from '../err/notFoundException.js'; +import { commentRepository as Comment } from '../repository/comment.repository.js'; +import { validateComments } from '../validators/validateComments.js'; export const productsRouter = express.Router(); @@ -13,25 +15,31 @@ productsRouter.get('/', async (req, res, next) => { const orderBy = req.query.orderBy; const offset = (page - 1) * pageSize; - const query = {}; + const where = {}; if (keyword) { - query.$or = [ - { name: { $regex: keyword, $options: 'i' } }, - { description: { $regex: keyword, $options: 'i' } }, + where.OR = [ + { name: { contains: keyword, mode: 'insensitive' } }, + { description: { contains: keyword, mode: 'insensitive' } }, ]; } - const sortOptions = {}; + const orderByOptions = []; if (orderBy === 'recent') { - sortOptions.createdAt = -1; + orderByOptions.push({ createdAt: 'desc' }); } - const totalCount = await Product.countDocuments(query); - const products = await Product.find(query) - .sort(sortOptions) - .skip(offset) - .limit(pageSize) - .select('id name price createdAt'); + const [totalCount, products] = await Product.findProductsMany({ + where, + orderBy: orderByOptions, + skip: offset, + take: pageSize, + select: { + id: true, + name: true, + price: true, + createdAt: true, + }, + }); res.json({ success: true, @@ -48,11 +56,11 @@ productsRouter.get('/', async (req, res, next) => { productsRouter.get('/:id', async (req, res, next) => { try { const { id } = req.params; - const product = await Product.findById(id); + const product = await Product.findProductById(id); if (!product) { throw new NotFoundException('상품을 찾을 수 없습니다'); } - res.json({ success: true, list: product }); + res.json({ success: true, data: product }); } catch (err) { next(err); } @@ -61,10 +69,15 @@ productsRouter.get('/:id', async (req, res, next) => { productsRouter.post('/', validateProducts, async (req, res, next) => { try { const { name, description, price, tags } = req.body; - const newProduct = await Product.create({ name, description, price, tags }); + const newProduct = await Product.createProduct({ + name, + description, + price, + tags, + }); res.status(201).json({ success: true, - list: newProduct, + data: newProduct, message: '상품이 정상적으로 추가되었습니다', }); } catch (err) { @@ -75,15 +88,14 @@ productsRouter.post('/', validateProducts, async (req, res, next) => { productsRouter.patch('/:id', validateProducts, async (req, res, next) => { try { const { id } = req.params; - const updatedProduct = await Product.findByIdAndUpdate(id, req.body, { - new: true, - }); - if (!updatedProduct) { - throw new NotFoundException('상품을 찾을 수 없습니다'); + const productExistence = await Product.findProductById(id); + if (!productExistence) { + throw new NotFoundException('상품을 찾을 수가 없습니다.'); } + const updatedProduct = await Product.updateProduct(id, req.body); res.json({ success: true, - list: updatedProduct, + data: updatedProduct, message: '등록된 상품 내용이 수정되었습니다', }); } catch (err) { @@ -94,16 +106,73 @@ productsRouter.patch('/:id', validateProducts, async (req, res, next) => { productsRouter.delete('/:id', async (req, res, next) => { try { const { id } = req.params; - const deletedProduct = await Product.findByIdAndDelete(id); - if (!deletedProduct) { - throw new NotFoundException('상품을 찾을 수 없습니다'); + const productExistence = await Product.findProductById(id); + if (!productExistence) { + throw new NotFoundException('상품을 찾을 수가 없습니다.'); } + const deletedProduct = await Product.deleteProduct(id); res.json({ success: true, - list: deletedProduct, + data: deletedProduct, message: '상품이 삭제되었습니다', }); } catch (err) { next(err); } }); + +productsRouter.get('/comments', async (req, res, next) => { + try { + const [totalCount, comments] = await Comment.findCommentsInProduct(); + + res.json({ + success: true, + list: comments, + totalCount, + }); + } catch (err) { + next(err); + } +}); + +productsRouter.get('/:productId/comments', async (req, res, next) => { + try { + const { productId } = req.params; + const [totalCount, comments] = + await Comment.findCommentsByProductId(productId); + + res.json({ success: true, list: comments, totalCount }); + } catch (err) { + next(err); + } +}); + +productsRouter.post( + '/:productId/comments', + validateComments, + async (req, res, next) => { + try { + const { productId } = req.params; + const productExistence = await Product.findProductById(productId); + if (!productExistence) { + throw new NotFoundException( + '댓글을 단 글을 찾을 수가 없어요. 어디다 댓글을 쓴거야?', + ); + } + const { content } = req.body; + const parent = 'Product'; + const newProductComment = await Comment.createComment({ + content, + parent, + productId, + }); + res.json({ + success: true, + data: newProductComment, + message: '댓글이 정상적으로 추가되었습니다', + }); + } catch (err) { + next(err); + } + }, +); diff --git a/src/server.js b/src/server.js index 3ea8847..18ace19 100644 --- a/src/server.js +++ b/src/server.js @@ -4,11 +4,10 @@ import { logger } from './middlewares/logger.js'; import { requestTimer } from './middlewares/requestTimer.js'; import { config, isDevelopment } from './config/config.js'; import { errorHandler } from './middlewares/errorHandler.js'; -import { connectDB, disconnectDB } from './db/index.js'; +import { disconnectDB } from './db/prisma.js'; import { cors } from './middlewares/cors.js'; const app = express(); -connectDB(); app.use(express.json()); @@ -32,9 +31,10 @@ const server = app.listen(config.PORT, () => { const shutdown = (signal) => { console.log(`\n${signal} received. Shutting down gracefully...`); - server.close(() => { - console.log('HTTP server closed.'); - disconnectDB(); + server.close(async () => { + console.log('✅ HTTP server closed.'); + await disconnectDB(); + process.exit(0); }); }; diff --git a/src/validators/validateArticles.js b/src/validators/validateArticles.js new file mode 100644 index 0000000..5bdbaf3 --- /dev/null +++ b/src/validators/validateArticles.js @@ -0,0 +1,17 @@ +import { BadRequestException } from '../err/badRequestException.js'; + +export const validateArticles = (req, res, next) => { + const { title, content } = req.body; + + if (!title) { + throw new BadRequestException('제목은 존재해야합니다.'); + } + + if (!content || content.trim().length < 10) { + throw new BadRequestException( + '내용은 존재해야하며, 10자 이상이여야 합니다.', + ); + } + + next(); +}; diff --git a/src/validators/validateComments.js b/src/validators/validateComments.js new file mode 100644 index 0000000..906c843 --- /dev/null +++ b/src/validators/validateComments.js @@ -0,0 +1,11 @@ +import { BadRequestException } from '../err/badRequestException.js'; + +export const validateComments = (req, res, next) => { + const { content } = req.body; + + if (!content) { + throw new BadRequestException('댓글의 내용은 존재해야합니다.'); + } + + next(); +}; diff --git a/src/middlewares/validateProducts.js b/src/validators/validateProducts.js similarity index 88% rename from src/middlewares/validateProducts.js rename to src/validators/validateProducts.js index 713974a..3b0fe38 100644 --- a/src/middlewares/validateProducts.js +++ b/src/validators/validateProducts.js @@ -20,7 +20,7 @@ export const validateProducts = (req, res, next) => { } if (!price || isNaN(price)) { - throw new BadRequestException('상품명은 존재해야하며, 숫자여야 합니다.'); + throw new BadRequestException('판매 가격은 존재해야하며, 숫자여야 합니다.'); } if (!tags || !tags.length) {