mirror of
https://github.com/ershisan99/flashcards-api.git
synced 2025-12-17 05:09:26 +00:00
add smart random
This commit is contained in:
@@ -34,7 +34,7 @@
|
|||||||
"@nestjs/platform-fastify": "^9.4.1",
|
"@nestjs/platform-fastify": "^9.4.1",
|
||||||
"@nestjs/schedule": "^2.2.3",
|
"@nestjs/schedule": "^2.2.3",
|
||||||
"@nestjs/swagger": "^6.3.0",
|
"@nestjs/swagger": "^6.3.0",
|
||||||
"@prisma/client": "^4.15.0",
|
"@prisma/client": "^5.0.0",
|
||||||
"bcrypt": "^5.1.0",
|
"bcrypt": "^5.1.0",
|
||||||
"class-transformer": "0.3.1",
|
"class-transformer": "0.3.1",
|
||||||
"class-validator": "^0.14.0",
|
"class-validator": "^0.14.0",
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"jest": "29.5.0",
|
"jest": "29.5.0",
|
||||||
"prettier": "^2.8.8",
|
"prettier": "^2.8.8",
|
||||||
"prisma": "^4.15.0",
|
"prisma": "^5.0.0",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"supertest": "^6.3.3",
|
"supertest": "^6.3.3",
|
||||||
"ts-jest": "29.1.0",
|
"ts-jest": "29.1.0",
|
||||||
|
|||||||
125
pnpm-lock.yaml
generated
125
pnpm-lock.yaml
generated
@@ -45,8 +45,8 @@ dependencies:
|
|||||||
specifier: ^6.3.0
|
specifier: ^6.3.0
|
||||||
version: 6.3.0(@nestjs/common@9.4.1)(@nestjs/core@9.4.1)(class-transformer@0.3.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)
|
version: 6.3.0(@nestjs/common@9.4.1)(@nestjs/core@9.4.1)(class-transformer@0.3.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)
|
||||||
'@prisma/client':
|
'@prisma/client':
|
||||||
specifier: ^4.15.0
|
specifier: ^5.0.0
|
||||||
version: 4.15.0(prisma@4.15.0)
|
version: 5.0.0(prisma@5.0.0)
|
||||||
bcrypt:
|
bcrypt:
|
||||||
specifier: ^5.1.0
|
specifier: ^5.1.0
|
||||||
version: 5.1.0
|
version: 5.1.0
|
||||||
@@ -137,8 +137,8 @@ devDependencies:
|
|||||||
specifier: ^2.8.8
|
specifier: ^2.8.8
|
||||||
version: 2.8.8
|
version: 2.8.8
|
||||||
prisma:
|
prisma:
|
||||||
specifier: ^4.15.0
|
specifier: ^5.0.0
|
||||||
version: 4.15.0
|
version: 5.0.0
|
||||||
source-map-support:
|
source-map-support:
|
||||||
specifier: ^0.5.21
|
specifier: ^0.5.21
|
||||||
version: 0.5.21
|
version: 0.5.21
|
||||||
@@ -150,7 +150,7 @@ devDependencies:
|
|||||||
version: 29.1.0(@babel/core@7.22.5)(jest@29.5.0)(typescript@5.0.4)
|
version: 29.1.0(@babel/core@7.22.5)(jest@29.5.0)(typescript@5.0.4)
|
||||||
ts-loader:
|
ts-loader:
|
||||||
specifier: ^9.4.2
|
specifier: ^9.4.2
|
||||||
version: 9.4.2(typescript@5.0.4)(webpack@5.86.0)
|
version: 9.4.2(typescript@5.0.4)(webpack@5.88.1)
|
||||||
ts-node:
|
ts-node:
|
||||||
specifier: ^10.9.1
|
specifier: ^10.9.1
|
||||||
version: 10.9.1(@types/node@18.16.12)(typescript@5.0.4)
|
version: 10.9.1(@types/node@18.16.12)(typescript@5.0.4)
|
||||||
@@ -2073,9 +2073,9 @@ packages:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
|
|
||||||
/@prisma/client@4.15.0(prisma@4.15.0):
|
/@prisma/client@5.0.0(prisma@5.0.0):
|
||||||
resolution: {integrity: sha512-xnROvyABcGiwqRNdrObHVZkD9EjkJYHOmVdlKy1yGgI+XOzvMzJ4tRg3dz1pUlsyhKxXGCnjIQjWW+2ur+YXuw==}
|
resolution: {integrity: sha512-XlO5ELNAQ7rV4cXIDJUNBEgdLwX3pjtt9Q/RHqDpGf43szpNJx2hJnggfFs7TKNx0cOFsl6KJCSfqr5duEU/bQ==}
|
||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=16.13'}
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
prisma: '*'
|
prisma: '*'
|
||||||
@@ -2083,16 +2083,16 @@ packages:
|
|||||||
prisma:
|
prisma:
|
||||||
optional: true
|
optional: true
|
||||||
dependencies:
|
dependencies:
|
||||||
'@prisma/engines-version': 4.15.0-28.8fbc245156db7124f997f4cecdd8d1219e360944
|
'@prisma/engines-version': 4.17.0-26.6b0aef69b7cdfc787f822ecd7cdc76d5f1991584
|
||||||
prisma: 4.15.0
|
prisma: 5.0.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@prisma/engines-version@4.15.0-28.8fbc245156db7124f997f4cecdd8d1219e360944:
|
/@prisma/engines-version@4.17.0-26.6b0aef69b7cdfc787f822ecd7cdc76d5f1991584:
|
||||||
resolution: {integrity: sha512-sVOig4tjGxxlYaFcXgE71f/rtFhzyYrfyfNFUsxCIEJyVKU9rdOWIlIwQ2NQ7PntvGnn+x0XuFo4OC1jvPJKzg==}
|
resolution: {integrity: sha512-HHiUF6NixsldsP3JROq07TYBLEjXFKr6PdH8H4gK/XAoTmIplOJBCgrIUMrsRAnEuGyRoRLXKXWUb943+PFoKQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@prisma/engines@4.15.0:
|
/@prisma/engines@5.0.0:
|
||||||
resolution: {integrity: sha512-FTaOCGs0LL0OW68juZlGxFtYviZa4xdQj/rQEdat2txw0s3Vu/saAPKjNVXfIgUsGXmQ72HPgNr6935/P8FNAA==}
|
resolution: {integrity: sha512-kyT/8fd0OpWmhAU5YnY7eP31brW1q1YrTGoblWrhQJDiN/1K+Z8S1kylcmtjqx5wsUGcP1HBWutayA/jtyt+sg==}
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
|
|
||||||
/@selderee/plugin-htmlparser2@0.10.0:
|
/@selderee/plugin-htmlparser2@0.10.0:
|
||||||
@@ -2621,6 +2621,14 @@ packages:
|
|||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
negotiator: 0.6.3
|
negotiator: 0.6.3
|
||||||
|
|
||||||
|
/acorn-import-assertions@1.9.0(acorn@8.10.0):
|
||||||
|
resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==}
|
||||||
|
peerDependencies:
|
||||||
|
acorn: ^8
|
||||||
|
dependencies:
|
||||||
|
acorn: 8.10.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/acorn-import-assertions@1.9.0(acorn@8.8.2):
|
/acorn-import-assertions@1.9.0(acorn@8.8.2):
|
||||||
resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==}
|
resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -2647,6 +2655,12 @@ packages:
|
|||||||
hasBin: true
|
hasBin: true
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/acorn@8.10.0:
|
||||||
|
resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==}
|
||||||
|
engines: {node: '>=0.4.0'}
|
||||||
|
hasBin: true
|
||||||
|
dev: true
|
||||||
|
|
||||||
/acorn@8.8.2:
|
/acorn@8.8.2:
|
||||||
resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==}
|
resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==}
|
||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
@@ -3000,6 +3014,17 @@ packages:
|
|||||||
update-browserslist-db: 1.0.11(browserslist@4.21.7)
|
update-browserslist-db: 1.0.11(browserslist@4.21.7)
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/browserslist@4.21.9:
|
||||||
|
resolution: {integrity: sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==}
|
||||||
|
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||||
|
hasBin: true
|
||||||
|
dependencies:
|
||||||
|
caniuse-lite: 1.0.30001515
|
||||||
|
electron-to-chromium: 1.4.457
|
||||||
|
node-releases: 2.0.13
|
||||||
|
update-browserslist-db: 1.0.11(browserslist@4.21.9)
|
||||||
|
dev: true
|
||||||
|
|
||||||
/bs-logger@0.2.6:
|
/bs-logger@0.2.6:
|
||||||
resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==}
|
resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
@@ -3076,6 +3101,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-AZ+9tFXw1sS0o0jcpJQIXvFTOB/xGiQ4OQ2t98QX3NDn2EZTSRBC801gxrsGgViuq2ak/NLkNgSNEPtCr5lfKg==}
|
resolution: {integrity: sha512-AZ+9tFXw1sS0o0jcpJQIXvFTOB/xGiQ4OQ2t98QX3NDn2EZTSRBC801gxrsGgViuq2ak/NLkNgSNEPtCr5lfKg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/caniuse-lite@1.0.30001515:
|
||||||
|
resolution: {integrity: sha512-eEFDwUOZbE24sb+Ecsx3+OvNETqjWIdabMy52oOkIgcUtAsQifjUG9q4U9dgTHJM2mfk4uEPxc0+xuFdJ629QA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/chalk@2.4.2:
|
/chalk@2.4.2:
|
||||||
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
|
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -3654,6 +3683,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-HK3r9l+Jm8dYAm1ctXEWIC+hV60zfcjS9UA5BDlYvnI5S7PU/yytjpvSrTNrSSRRkuu3tDyZhdkwIczh+0DWaw==}
|
resolution: {integrity: sha512-HK3r9l+Jm8dYAm1ctXEWIC+hV60zfcjS9UA5BDlYvnI5S7PU/yytjpvSrTNrSSRRkuu3tDyZhdkwIczh+0DWaw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/electron-to-chromium@1.4.457:
|
||||||
|
resolution: {integrity: sha512-/g3UyNDmDd6ebeWapmAoiyy+Sy2HyJ+/X8KyvNeHfKRFfHaA2W8oF5fxD5F3tjBDcjpwo0iek6YNgxNXDBoEtA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/emittery@0.13.1:
|
/emittery@0.13.1:
|
||||||
resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==}
|
resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -3685,6 +3718,14 @@ packages:
|
|||||||
tapable: 2.2.1
|
tapable: 2.2.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/enhanced-resolve@5.15.0:
|
||||||
|
resolution: {integrity: sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==}
|
||||||
|
engines: {node: '>=10.13.0'}
|
||||||
|
dependencies:
|
||||||
|
graceful-fs: 4.2.11
|
||||||
|
tapable: 2.2.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
/entities@2.2.0:
|
/entities@2.2.0:
|
||||||
resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==}
|
resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -6225,6 +6266,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==}
|
resolution: {integrity: sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/node-releases@2.0.13:
|
||||||
|
resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/nodemailer@6.9.1:
|
/nodemailer@6.9.1:
|
||||||
resolution: {integrity: sha512-qHw7dOiU5UKNnQpXktdgQ1d3OFgRAekuvbJLcdG5dnEo/GtcTHRYM7+UfJARdOFU9WUQO8OiIamgWPmiSFHYAA==}
|
resolution: {integrity: sha512-qHw7dOiU5UKNnQpXktdgQ1d3OFgRAekuvbJLcdG5dnEo/GtcTHRYM7+UfJARdOFU9WUQO8OiIamgWPmiSFHYAA==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
@@ -6659,13 +6704,13 @@ packages:
|
|||||||
- supports-color
|
- supports-color
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/prisma@4.15.0:
|
/prisma@5.0.0:
|
||||||
resolution: {integrity: sha512-iKZZpobPl48gTcSZVawLMQ3lEy6BnXwtoMj7hluoGFYu2kQ6F9LBuBrUyF95zRVnNo8/3KzLXJXJ5TEnLSJFiA==}
|
resolution: {integrity: sha512-KYWk83Fhi1FH59jSpavAYTt2eoMVW9YKgu8ci0kuUnt6Dup5Qy47pcB4/TLmiPAbhGrxxSz7gsSnJcCmkyPANA==}
|
||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=16.13'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dependencies:
|
dependencies:
|
||||||
'@prisma/engines': 4.15.0
|
'@prisma/engines': 5.0.0
|
||||||
|
|
||||||
/process-nextick-args@2.0.1:
|
/process-nextick-args@2.0.1:
|
||||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||||
@@ -7086,6 +7131,15 @@ packages:
|
|||||||
ajv-keywords: 3.5.2(ajv@6.12.6)
|
ajv-keywords: 3.5.2(ajv@6.12.6)
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/schema-utils@3.3.0:
|
||||||
|
resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==}
|
||||||
|
engines: {node: '>= 10.13.0'}
|
||||||
|
dependencies:
|
||||||
|
'@types/json-schema': 7.0.12
|
||||||
|
ajv: 6.12.6
|
||||||
|
ajv-keywords: 3.5.2(ajv@6.12.6)
|
||||||
|
dev: true
|
||||||
|
|
||||||
/secure-json-parse@2.7.0:
|
/secure-json-parse@2.7.0:
|
||||||
resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
|
resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -7473,7 +7527,7 @@ packages:
|
|||||||
webpack: 5.82.1
|
webpack: 5.82.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/terser-webpack-plugin@5.3.9(webpack@5.86.0):
|
/terser-webpack-plugin@5.3.9(webpack@5.88.1):
|
||||||
resolution: {integrity: sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==}
|
resolution: {integrity: sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==}
|
||||||
engines: {node: '>= 10.13.0'}
|
engines: {node: '>= 10.13.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -7494,7 +7548,7 @@ packages:
|
|||||||
schema-utils: 3.2.0
|
schema-utils: 3.2.0
|
||||||
serialize-javascript: 6.0.1
|
serialize-javascript: 6.0.1
|
||||||
terser: 5.18.0
|
terser: 5.18.0
|
||||||
webpack: 5.86.0
|
webpack: 5.88.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/terser@5.18.0:
|
/terser@5.18.0:
|
||||||
@@ -7612,7 +7666,7 @@ packages:
|
|||||||
yargs-parser: 21.1.1
|
yargs-parser: 21.1.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/ts-loader@9.4.2(typescript@5.0.4)(webpack@5.86.0):
|
/ts-loader@9.4.2(typescript@5.0.4)(webpack@5.88.1):
|
||||||
resolution: {integrity: sha512-OmlC4WVmFv5I0PpaxYb+qGeGOdm5giHU7HwDDUjw59emP2UYMHy9fFSDcYgSNoH8sXcj4hGCSEhlDZ9ULeDraA==}
|
resolution: {integrity: sha512-OmlC4WVmFv5I0PpaxYb+qGeGOdm5giHU7HwDDUjw59emP2UYMHy9fFSDcYgSNoH8sXcj4hGCSEhlDZ9ULeDraA==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -7624,7 +7678,7 @@ packages:
|
|||||||
micromatch: 4.0.5
|
micromatch: 4.0.5
|
||||||
semver: 7.5.1
|
semver: 7.5.1
|
||||||
typescript: 5.0.4
|
typescript: 5.0.4
|
||||||
webpack: 5.86.0
|
webpack: 5.88.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/ts-node@10.9.1(@types/node@18.16.12)(typescript@5.0.4):
|
/ts-node@10.9.1(@types/node@18.16.12)(typescript@5.0.4):
|
||||||
@@ -7787,6 +7841,17 @@ packages:
|
|||||||
picocolors: 1.0.0
|
picocolors: 1.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/update-browserslist-db@1.0.11(browserslist@4.21.9):
|
||||||
|
resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
browserslist: '>= 4.21.0'
|
||||||
|
dependencies:
|
||||||
|
browserslist: 4.21.9
|
||||||
|
escalade: 3.1.1
|
||||||
|
picocolors: 1.0.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/upper-case@1.1.3:
|
/upper-case@1.1.3:
|
||||||
resolution: {integrity: sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==}
|
resolution: {integrity: sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -7940,8 +8005,8 @@ packages:
|
|||||||
- uglify-js
|
- uglify-js
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/webpack@5.86.0:
|
/webpack@5.88.1:
|
||||||
resolution: {integrity: sha512-3BOvworZ8SO/D4GVP+GoRC3fVeg5MO4vzmq8TJJEkdmopxyazGDxN8ClqN12uzrZW9Tv8EED8v5VSb6Sqyi0pg==}
|
resolution: {integrity: sha512-FROX3TxQnC/ox4N+3xQoWZzvGXSuscxR32rbzjpXgEzWudJFEJBpdlkkob2ylrv5yzzufD1zph1OoFsLtm6stQ==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -7955,11 +8020,11 @@ packages:
|
|||||||
'@webassemblyjs/ast': 1.11.6
|
'@webassemblyjs/ast': 1.11.6
|
||||||
'@webassemblyjs/wasm-edit': 1.11.6
|
'@webassemblyjs/wasm-edit': 1.11.6
|
||||||
'@webassemblyjs/wasm-parser': 1.11.6
|
'@webassemblyjs/wasm-parser': 1.11.6
|
||||||
acorn: 8.8.2
|
acorn: 8.10.0
|
||||||
acorn-import-assertions: 1.9.0(acorn@8.8.2)
|
acorn-import-assertions: 1.9.0(acorn@8.10.0)
|
||||||
browserslist: 4.21.7
|
browserslist: 4.21.9
|
||||||
chrome-trace-event: 1.0.3
|
chrome-trace-event: 1.0.3
|
||||||
enhanced-resolve: 5.14.1
|
enhanced-resolve: 5.15.0
|
||||||
es-module-lexer: 1.3.0
|
es-module-lexer: 1.3.0
|
||||||
eslint-scope: 5.1.1
|
eslint-scope: 5.1.1
|
||||||
events: 3.3.0
|
events: 3.3.0
|
||||||
@@ -7969,9 +8034,9 @@ packages:
|
|||||||
loader-runner: 4.3.0
|
loader-runner: 4.3.0
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
neo-async: 2.6.2
|
neo-async: 2.6.2
|
||||||
schema-utils: 3.2.0
|
schema-utils: 3.3.0
|
||||||
tapable: 2.2.1
|
tapable: 2.2.1
|
||||||
terser-webpack-plugin: 5.3.9(webpack@5.86.0)
|
terser-webpack-plugin: 5.3.9(webpack@5.88.1)
|
||||||
watchpack: 2.4.0
|
watchpack: 2.4.0
|
||||||
webpack-sources: 3.2.3
|
webpack-sources: 3.2.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
|
|||||||
@@ -84,7 +84,6 @@ model card {
|
|||||||
userId String
|
userId String
|
||||||
question String @db.Text
|
question String @db.Text
|
||||||
answer String @db.Text
|
answer String @db.Text
|
||||||
grade Int @default(0)
|
|
||||||
shots Int @default(0)
|
shots Int @default(0)
|
||||||
questionImg String?
|
questionImg String?
|
||||||
answerImg String?
|
answerImg String?
|
||||||
@@ -116,27 +115,26 @@ model deck {
|
|||||||
isBlocked Boolean?
|
isBlocked Boolean?
|
||||||
created DateTime @default(now())
|
created DateTime @default(now())
|
||||||
updated DateTime @updatedAt
|
updated DateTime @updatedAt
|
||||||
author user @relation(fields: [userId], references: [id])
|
|
||||||
cardsCount Int @default(0)
|
cardsCount Int @default(0)
|
||||||
|
author user @relation(fields: [userId], references: [id])
|
||||||
card card[]
|
card card[]
|
||||||
grade grade[]
|
grades grade[]
|
||||||
|
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model grade {
|
model grade {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
deckId String
|
deckId String @unique
|
||||||
cardId String
|
cardId String @unique
|
||||||
userId String
|
userId String @unique
|
||||||
grade Int
|
grade Int
|
||||||
shots Int
|
shots Int
|
||||||
moreId String?
|
|
||||||
created DateTime @default(now())
|
created DateTime @default(now())
|
||||||
updated DateTime @updatedAt
|
updated DateTime @updatedAt
|
||||||
user user @relation(fields: [userId], references: [id])
|
user user @relation(fields: [userId], references: [id])
|
||||||
card card @relation(fields: [cardId], references: [id])
|
card card @relation(fields: [cardId], references: [id])
|
||||||
decks deck @relation(fields: [deckId], references: [id])
|
deck deck @relation(fields: [deckId], references: [id])
|
||||||
|
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
@@index([deckId])
|
@@index([deckId])
|
||||||
|
|||||||
30
src/infrastructure/common/helpers/get-order-by-object.ts
Normal file
30
src/infrastructure/common/helpers/get-order-by-object.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
type OrderByDirection = 'asc' | 'desc'
|
||||||
|
|
||||||
|
export function createPrismaOrderBy(input: string | null) {
|
||||||
|
if (!input || input === 'null') {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const [key, direction] = input.split('-')
|
||||||
|
|
||||||
|
if (!key || !direction) {
|
||||||
|
throw new Error("Invalid format. Expected format is 'key-direction'")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (direction !== 'asc' && direction !== 'desc') {
|
||||||
|
throw new Error("Invalid direction. Expected 'asc' or 'desc'")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.includes('.')) {
|
||||||
|
const [relation, field] = key.split('.')
|
||||||
|
|
||||||
|
return {
|
||||||
|
[relation]: {
|
||||||
|
[field]: direction,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
[key]: direction as OrderByDirection,
|
||||||
|
}
|
||||||
|
}
|
||||||
45
src/infrastructure/decorators/is-order-by-constraint.ts
Normal file
45
src/infrastructure/decorators/is-order-by-constraint.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import {
|
||||||
|
registerDecorator,
|
||||||
|
ValidationArguments,
|
||||||
|
ValidationOptions,
|
||||||
|
ValidatorConstraint,
|
||||||
|
ValidatorConstraintInterface,
|
||||||
|
} from 'class-validator'
|
||||||
|
|
||||||
|
@ValidatorConstraint({ async: false })
|
||||||
|
export class IsOrderByConstraint implements ValidatorConstraintInterface {
|
||||||
|
validate(orderBy: string | null, args: ValidationArguments) {
|
||||||
|
console.log(orderBy)
|
||||||
|
if (!orderBy || orderBy === 'null' || orderBy === '') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const [key, direction] = orderBy.split('-')
|
||||||
|
|
||||||
|
if (!key || !direction) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (direction !== 'asc' && direction !== 'desc') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultMessage(args: ValidationArguments) {
|
||||||
|
return 'Invalid format. Expected format is "key-direction". Direction must be "asc" or "desc".'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IsOrderBy(validationOptions?: ValidationOptions) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
|
return function (object: { constructor: Function }, propertyName: string) {
|
||||||
|
registerDecorator({
|
||||||
|
target: object.constructor,
|
||||||
|
propertyName: propertyName,
|
||||||
|
options: validationOptions,
|
||||||
|
constraints: [],
|
||||||
|
validator: IsOrderByConstraint,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,6 @@ export class GetCurrentUserDataHandler implements ICommandHandler<GetCurrentUser
|
|||||||
|
|
||||||
if (!user) throw new UnauthorizedException()
|
if (!user) throw new UnauthorizedException()
|
||||||
|
|
||||||
return pick(user, ['email', 'name', 'id', 'isEmailVerified'])
|
return pick(user, ['email', 'name', 'id', 'isEmailVerified', 'avatar'])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Body, Controller, Delete, Get, Param, Patch, Req, UseGuards } from '@ne
|
|||||||
import { CardsService } from './cards.service'
|
import { CardsService } from './cards.service'
|
||||||
import { UpdateCardDto } from './dto/update-card.dto'
|
import { UpdateCardDto } from './dto/update-card.dto'
|
||||||
import { CommandBus } from '@nestjs/cqrs'
|
import { CommandBus } from '@nestjs/cqrs'
|
||||||
import { DeleteDeckByIdCommand, GetDeckByIdCommand, UpdateDeckCommand } from './use-cases'
|
import { DeleteCardByIdCommand, GetDeckByIdCommand, UpdateDeckCommand } from './use-cases'
|
||||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'
|
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'
|
||||||
|
|
||||||
@Controller('cards')
|
@Controller('cards')
|
||||||
@@ -24,6 +24,6 @@ export class CardsController {
|
|||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
remove(@Param('id') id: string, @Req() req) {
|
remove(@Param('id') id: string, @Req() req) {
|
||||||
return this.commandBus.execute(new DeleteDeckByIdCommand(id, req.user.id))
|
return this.commandBus.execute(new DeleteCardByIdCommand(id, req.user.id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import { Module } from '@nestjs/common'
|
|||||||
import { CardsService } from './cards.service'
|
import { CardsService } from './cards.service'
|
||||||
import { CardsController } from './cards.controller'
|
import { CardsController } from './cards.controller'
|
||||||
import { CqrsModule } from '@nestjs/cqrs'
|
import { CqrsModule } from '@nestjs/cqrs'
|
||||||
import { DeleteDeckByIdHandler, GetDeckByIdHandler, UpdateDeckHandler } from './use-cases'
|
import { DeleteCardByIdHandler, GetDeckByIdHandler, UpdateDeckHandler } from './use-cases'
|
||||||
import { CardsRepository } from './infrastructure/cards.repository'
|
import { CardsRepository } from './infrastructure/cards.repository'
|
||||||
|
|
||||||
const commandHandlers = [GetDeckByIdHandler, DeleteDeckByIdHandler, UpdateDeckHandler]
|
const commandHandlers = [GetDeckByIdHandler, DeleteCardByIdHandler, UpdateDeckHandler]
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [CqrsModule],
|
imports: [CqrsModule],
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Length } from 'class-validator'
|
import { Length } from 'class-validator'
|
||||||
import { PaginationDto } from '../../../infrastructure/common/pagination/pagination.dto'
|
import { PaginationDto } from '../../../infrastructure/common/pagination/pagination.dto'
|
||||||
import { IsOptionalOrEmptyString } from '../../../infrastructure/decorators/is-optional-or-empty-string'
|
import { IsOptionalOrEmptyString } from '../../../infrastructure/decorators/is-optional-or-empty-string'
|
||||||
|
import { IsOrderBy } from '../../../infrastructure/decorators/is-order-by-constraint'
|
||||||
|
|
||||||
export class GetAllCardsInDeckDto extends PaginationDto {
|
export class GetAllCardsInDeckDto extends PaginationDto {
|
||||||
@IsOptionalOrEmptyString()
|
@IsOptionalOrEmptyString()
|
||||||
@@ -10,4 +11,7 @@ export class GetAllCardsInDeckDto extends PaginationDto {
|
|||||||
@IsOptionalOrEmptyString()
|
@IsOptionalOrEmptyString()
|
||||||
@Length(1, 30)
|
@Length(1, 30)
|
||||||
answer?: string
|
answer?: string
|
||||||
|
|
||||||
|
@IsOrderBy()
|
||||||
|
orderBy?: string | null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common
|
|||||||
import { PrismaService } from '../../../prisma.service'
|
import { PrismaService } from '../../../prisma.service'
|
||||||
import { GetAllCardsInDeckDto } from '../dto/get-all-cards.dto'
|
import { GetAllCardsInDeckDto } from '../dto/get-all-cards.dto'
|
||||||
import { CreateCardDto } from '../dto/create-card.dto'
|
import { CreateCardDto } from '../dto/create-card.dto'
|
||||||
|
import { Pagination } from '../../../infrastructure/common/pagination/pagination.service'
|
||||||
|
import { createPrismaOrderBy } from '../../../infrastructure/common/helpers/get-order-by-object'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CardsRepository {
|
export class CardsRepository {
|
||||||
@@ -48,23 +50,56 @@ export class CardsRepository {
|
|||||||
|
|
||||||
async findCardsByDeckId(
|
async findCardsByDeckId(
|
||||||
deckId: string,
|
deckId: string,
|
||||||
{ answer = undefined, question = undefined, currentPage, itemsPerPage }: GetAllCardsInDeckDto
|
{
|
||||||
|
answer = undefined,
|
||||||
|
question = undefined,
|
||||||
|
currentPage,
|
||||||
|
itemsPerPage,
|
||||||
|
orderBy,
|
||||||
|
}: GetAllCardsInDeckDto
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
return await this.prisma.card.findMany({
|
const where = {
|
||||||
|
decks: {
|
||||||
|
id: deckId,
|
||||||
|
},
|
||||||
|
question: {
|
||||||
|
contains: question || undefined,
|
||||||
|
},
|
||||||
|
answer: {
|
||||||
|
contains: answer || undefined,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const result = await this.prisma.$transaction([
|
||||||
|
this.prisma.card.count({ where }),
|
||||||
|
this.prisma.card.findMany({
|
||||||
|
orderBy: createPrismaOrderBy(orderBy) || { updated: 'desc' },
|
||||||
|
where,
|
||||||
|
skip: (currentPage - 1) * itemsPerPage,
|
||||||
|
take: itemsPerPage,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
return Pagination.transformPaginationData(result, { currentPage, itemsPerPage })
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error(e?.message)
|
||||||
|
throw new InternalServerErrorException(e?.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async findCardsByDeckIdWithGrade(userId: string, deckId: string) {
|
||||||
|
try {
|
||||||
|
return this.prisma.card.findMany({
|
||||||
where: {
|
where: {
|
||||||
decks: {
|
deckId,
|
||||||
id: deckId,
|
},
|
||||||
},
|
include: {
|
||||||
question: {
|
grades: {
|
||||||
contains: question || undefined,
|
where: {
|
||||||
},
|
userId,
|
||||||
answer: {
|
deckId,
|
||||||
contains: answer || undefined,
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
skip: (currentPage - 1) * itemsPerPage,
|
|
||||||
take: itemsPerPage,
|
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.error(e?.message)
|
this.logger.error(e?.message)
|
||||||
@@ -72,9 +107,9 @@ export class CardsRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async findDeckById(id: string) {
|
public async findCardById(id: string) {
|
||||||
try {
|
try {
|
||||||
return await this.prisma.deck.findUnique({
|
return await this.prisma.card.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id,
|
id,
|
||||||
},
|
},
|
||||||
@@ -85,12 +120,25 @@ export class CardsRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteDeckById(id: string) {
|
public async deleteCardById(id: string) {
|
||||||
try {
|
try {
|
||||||
return await this.prisma.deck.delete({
|
return await this.prisma.$transaction(async tx => {
|
||||||
where: {
|
const deleted = await tx.card.delete({
|
||||||
id,
|
where: {
|
||||||
},
|
id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await tx.deck.update({
|
||||||
|
where: {
|
||||||
|
id: deleted.deckId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
cardsCount: {
|
||||||
|
decrement: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return deleted
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.error(e?.message)
|
this.logger.error(e?.message)
|
||||||
|
|||||||
21
src/modules/cards/use-cases/delete-card-by-id-use-case.ts
Normal file
21
src/modules/cards/use-cases/delete-card-by-id-use-case.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
||||||
|
import { CardsRepository } from '../infrastructure/cards.repository'
|
||||||
|
import { BadRequestException, NotFoundException } from '@nestjs/common'
|
||||||
|
|
||||||
|
export class DeleteCardByIdCommand {
|
||||||
|
constructor(public readonly id: string, public readonly userId: string) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@CommandHandler(DeleteCardByIdCommand)
|
||||||
|
export class DeleteCardByIdHandler implements ICommandHandler<DeleteCardByIdCommand> {
|
||||||
|
constructor(private readonly cardsRepository: CardsRepository) {}
|
||||||
|
|
||||||
|
async execute(command: DeleteCardByIdCommand) {
|
||||||
|
const card = await this.cardsRepository.findCardById(command.id)
|
||||||
|
if (!card) throw new NotFoundException(`Card with id ${command.id} not found`)
|
||||||
|
if (card.userId !== command.userId) {
|
||||||
|
throw new BadRequestException(`You can't delete a card that you don't own`)
|
||||||
|
}
|
||||||
|
return await this.cardsRepository.deleteCardById(command.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
|
||||||
import { CardsRepository } from '../infrastructure/cards.repository'
|
|
||||||
import { BadRequestException, NotFoundException } from '@nestjs/common'
|
|
||||||
|
|
||||||
export class DeleteDeckByIdCommand {
|
|
||||||
constructor(public readonly id: string, public readonly userId: string) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
@CommandHandler(DeleteDeckByIdCommand)
|
|
||||||
export class DeleteDeckByIdHandler implements ICommandHandler<DeleteDeckByIdCommand> {
|
|
||||||
constructor(private readonly deckRepository: CardsRepository) {}
|
|
||||||
|
|
||||||
async execute(command: DeleteDeckByIdCommand) {
|
|
||||||
const deck = await this.deckRepository.findDeckById(command.id)
|
|
||||||
if (!deck) throw new NotFoundException(`Deck with id ${command.id} not found`)
|
|
||||||
if (deck.userId !== command.userId) {
|
|
||||||
throw new BadRequestException(`You can't delete a deck that you don't own`)
|
|
||||||
}
|
|
||||||
return await this.deckRepository.deleteDeckById(command.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,6 +10,6 @@ export class GetDeckByIdHandler implements ICommandHandler<GetDeckByIdCommand> {
|
|||||||
constructor(private readonly deckRepository: CardsRepository) {}
|
constructor(private readonly deckRepository: CardsRepository) {}
|
||||||
|
|
||||||
async execute(command: GetDeckByIdCommand) {
|
async execute(command: GetDeckByIdCommand) {
|
||||||
return await this.deckRepository.findDeckById(command.id)
|
return await this.deckRepository.findCardById(command.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export * from './get-deck-by-id-use-case'
|
export * from './get-deck-by-id-use-case'
|
||||||
export * from './delete-deck-by-id-use-case'
|
export * from './delete-card-by-id-use-case'
|
||||||
export * from './update-deck-use-case'
|
export * from './update-deck-use-case'
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export class UpdateDeckHandler implements ICommandHandler<UpdateDeckCommand> {
|
|||||||
constructor(private readonly deckRepository: CardsRepository) {}
|
constructor(private readonly deckRepository: CardsRepository) {}
|
||||||
|
|
||||||
async execute(command: UpdateDeckCommand) {
|
async execute(command: UpdateDeckCommand) {
|
||||||
const deck = await this.deckRepository.findDeckById(command.deckId)
|
const deck = await this.deckRepository.findCardById(command.deckId)
|
||||||
|
|
||||||
if (!deck) throw new NotFoundException(`Deck with id ${command.deckId} not found`)
|
if (!deck) throw new NotFoundException(`Deck with id ${command.deckId} not found`)
|
||||||
|
|
||||||
|
|||||||
@@ -22,13 +22,15 @@ import {
|
|||||||
GetAllDecksCommand,
|
GetAllDecksCommand,
|
||||||
GetDeckByIdCommand,
|
GetDeckByIdCommand,
|
||||||
UpdateDeckCommand,
|
UpdateDeckCommand,
|
||||||
|
CreateCardCommand,
|
||||||
} from './use-cases'
|
} from './use-cases'
|
||||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'
|
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'
|
||||||
import { GetAllDecksDto } from './dto/get-all-decks.dto'
|
import { GetAllDecksDto } from './dto/get-all-decks.dto'
|
||||||
import { GetAllCardsInDeckDto } from '../cards/dto/get-all-cards.dto'
|
import { GetAllCardsInDeckDto } from '../cards/dto/get-all-cards.dto'
|
||||||
import { CreateCardCommand } from './use-cases'
|
|
||||||
import { CreateCardDto } from '../cards/dto/create-card.dto'
|
import { CreateCardDto } from '../cards/dto/create-card.dto'
|
||||||
import { Pagination } from '../../infrastructure/common/pagination/pagination.service'
|
import { Pagination } from '../../infrastructure/common/pagination/pagination.service'
|
||||||
|
import { GetRandomCardInDeckCommand } from './use-cases/get-random-card-in-deck-use-case'
|
||||||
|
import { SaveGradeCommand } from './use-cases/save-grade-use-case'
|
||||||
|
|
||||||
@Controller('decks')
|
@Controller('decks')
|
||||||
export class DecksController {
|
export class DecksController {
|
||||||
@@ -57,9 +59,21 @@ export class DecksController {
|
|||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Get(':id/cards')
|
@Get(':id/cards')
|
||||||
findCardsInDeck(@Param('id') id: string, @Req() req, @Query() query: GetAllCardsInDeckDto) {
|
findCardsInDeck(@Param('id') id: string, @Req() req, @Query() query: GetAllCardsInDeckDto) {
|
||||||
return this.commandBus.execute(new GetAllCardsInDeckCommand(req.user.id, id, query))
|
const finalQuery = Pagination.getPaginationData(query)
|
||||||
|
return this.commandBus.execute(new GetAllCardsInDeckCommand(req.user.id, id, finalQuery))
|
||||||
|
}
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@Get(':id/learn')
|
||||||
|
findRandomCardInDeck(@Param('id') id: string, @Req() req) {
|
||||||
|
return this.commandBus.execute(new GetRandomCardInDeckCommand(req.user.id, id))
|
||||||
|
}
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@Post(':id/learn')
|
||||||
|
saveGrade(@Param('id') id: string, @Req() req, @Body() body: any) {
|
||||||
|
return this.commandBus.execute(
|
||||||
|
new SaveGradeCommand(req.user.id, { cardId: body.cardId, grade: body.grade })
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Post(':id/cards')
|
@Post(':id/cards')
|
||||||
createCardInDeck(@Param('id') id: string, @Req() req, @Body() card: CreateCardDto) {
|
createCardInDeck(@Param('id') id: string, @Req() req, @Body() card: CreateCardDto) {
|
||||||
|
|||||||
@@ -13,21 +13,26 @@ import {
|
|||||||
} from './use-cases'
|
} from './use-cases'
|
||||||
import { DecksRepository } from './infrastructure/decks.repository'
|
import { DecksRepository } from './infrastructure/decks.repository'
|
||||||
import { CardsRepository } from '../cards/infrastructure/cards.repository'
|
import { CardsRepository } from '../cards/infrastructure/cards.repository'
|
||||||
|
import { GetRandomCardInDeckHandler } from './use-cases/get-random-card-in-deck-use-case'
|
||||||
|
import { GradesRepository } from './infrastructure/grades.repository'
|
||||||
|
import { SaveGradeHandler } from './use-cases/save-grade-use-case'
|
||||||
|
|
||||||
const commandHandlers = [
|
const commandHandlers = [
|
||||||
CreateDeckHandler,
|
CreateDeckHandler,
|
||||||
GetAllDecksHandler,
|
GetAllDecksHandler,
|
||||||
GetDeckByIdHandler,
|
GetDeckByIdHandler,
|
||||||
|
GetRandomCardInDeckHandler,
|
||||||
DeleteDeckByIdHandler,
|
DeleteDeckByIdHandler,
|
||||||
UpdateDeckHandler,
|
UpdateDeckHandler,
|
||||||
GetAllCardsInDeckHandler,
|
GetAllCardsInDeckHandler,
|
||||||
CreateCardHandler,
|
CreateCardHandler,
|
||||||
|
SaveGradeHandler,
|
||||||
]
|
]
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [CqrsModule],
|
imports: [CqrsModule],
|
||||||
controllers: [DecksController],
|
controllers: [DecksController],
|
||||||
providers: [DecksService, DecksRepository, CardsRepository, ...commandHandlers],
|
providers: [DecksService, DecksRepository, CardsRepository, GradesRepository, ...commandHandlers],
|
||||||
exports: [CqrsModule],
|
exports: [CqrsModule],
|
||||||
})
|
})
|
||||||
export class DecksModule {}
|
export class DecksModule {}
|
||||||
|
|||||||
10
src/modules/decks/dto/create-grade.dto.ts
Normal file
10
src/modules/decks/dto/create-grade.dto.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { IsUUID, Max, Min } from 'class-validator'
|
||||||
|
|
||||||
|
export class CreateDeckDto {
|
||||||
|
@Min(1)
|
||||||
|
@Max(5)
|
||||||
|
grade: number
|
||||||
|
|
||||||
|
@IsUUID()
|
||||||
|
cardId: string
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { IsUUID } from 'class-validator'
|
import { IsUUID } from 'class-validator'
|
||||||
import { IsOptionalOrEmptyString } from '../../../infrastructure/decorators/is-optional-or-empty-string'
|
import { IsOptionalOrEmptyString } from '../../../infrastructure/decorators/is-optional-or-empty-string'
|
||||||
import { PaginationDto } from '../../../infrastructure/common/pagination/pagination.dto'
|
import { PaginationDto } from '../../../infrastructure/common/pagination/pagination.dto'
|
||||||
|
import { IsOrderBy } from '../../../infrastructure/decorators/is-order-by-constraint'
|
||||||
|
|
||||||
export class GetAllDecksDto extends PaginationDto {
|
export class GetAllDecksDto extends PaginationDto {
|
||||||
@IsOptionalOrEmptyString()
|
@IsOptionalOrEmptyString()
|
||||||
@@ -17,4 +18,7 @@ export class GetAllDecksDto extends PaginationDto {
|
|||||||
authorId?: string
|
authorId?: string
|
||||||
|
|
||||||
userId: string
|
userId: string
|
||||||
|
|
||||||
|
@IsOrderBy()
|
||||||
|
orderBy?: string | null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common
|
|||||||
import { PrismaService } from '../../../prisma.service'
|
import { PrismaService } from '../../../prisma.service'
|
||||||
import { GetAllDecksDto } from '../dto/get-all-decks.dto'
|
import { GetAllDecksDto } from '../dto/get-all-decks.dto'
|
||||||
import { Pagination } from '../../../infrastructure/common/pagination/pagination.service'
|
import { Pagination } from '../../../infrastructure/common/pagination/pagination.service'
|
||||||
|
import { createPrismaOrderBy } from '../../../infrastructure/common/helpers/get-order-by-object'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DecksRepository {
|
export class DecksRepository {
|
||||||
@@ -48,13 +49,15 @@ export class DecksRepository {
|
|||||||
itemsPerPage,
|
itemsPerPage,
|
||||||
minCardsCount,
|
minCardsCount,
|
||||||
maxCardsCount,
|
maxCardsCount,
|
||||||
|
orderBy,
|
||||||
}: GetAllDecksDto) {
|
}: GetAllDecksDto) {
|
||||||
console.log({ name, authorId, userId, currentPage, itemsPerPage, minCardsCount, maxCardsCount })
|
console.log(minCardsCount)
|
||||||
|
console.log(Number(minCardsCount))
|
||||||
try {
|
try {
|
||||||
const where = {
|
const where = {
|
||||||
cardsCount: {
|
cardsCount: {
|
||||||
gte: Number(minCardsCount) ?? undefined,
|
gte: minCardsCount ? Number(minCardsCount) : undefined,
|
||||||
lte: Number(maxCardsCount) ?? undefined,
|
lte: maxCardsCount ? Number(maxCardsCount) : undefined,
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
contains: name,
|
contains: name,
|
||||||
@@ -85,9 +88,7 @@ export class DecksRepository {
|
|||||||
}),
|
}),
|
||||||
this.prisma.deck.findMany({
|
this.prisma.deck.findMany({
|
||||||
where,
|
where,
|
||||||
orderBy: {
|
orderBy: createPrismaOrderBy(orderBy),
|
||||||
created: 'desc',
|
|
||||||
},
|
|
||||||
include: {
|
include: {
|
||||||
author: {
|
author: {
|
||||||
select: {
|
select: {
|
||||||
@@ -124,6 +125,24 @@ export class DecksRepository {
|
|||||||
throw new InternalServerErrorException(e?.message)
|
throw new InternalServerErrorException(e?.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public async findDeckByCardId(cardId: string) {
|
||||||
|
try {
|
||||||
|
const card = await this.prisma.card.findUnique({
|
||||||
|
where: {
|
||||||
|
id: cardId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return await this.prisma.deck.findUnique({
|
||||||
|
where: {
|
||||||
|
id: card.deckId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error(e?.message)
|
||||||
|
throw new InternalServerErrorException(e?.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async deleteDeckById(id: string) {
|
public async deleteDeckById(id: string) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
59
src/modules/decks/infrastructure/grades.repository.ts
Normal file
59
src/modules/decks/infrastructure/grades.repository.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common'
|
||||||
|
import { PrismaService } from '../../../prisma.service'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GradesRepository {
|
||||||
|
constructor(private prisma: PrismaService) {}
|
||||||
|
|
||||||
|
private readonly logger = new Logger(GradesRepository.name)
|
||||||
|
|
||||||
|
async createGrade({
|
||||||
|
cardId,
|
||||||
|
userId,
|
||||||
|
deckId,
|
||||||
|
grade,
|
||||||
|
}: {
|
||||||
|
cardId: string
|
||||||
|
userId: string
|
||||||
|
deckId: string
|
||||||
|
grade: number
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
return await this.prisma.grade.upsert({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
cardId,
|
||||||
|
deckId,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
grade,
|
||||||
|
shots: {
|
||||||
|
increment: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
grade,
|
||||||
|
shots: 1,
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
connect: {
|
||||||
|
id: cardId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
deck: {
|
||||||
|
connect: {
|
||||||
|
id: deckId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error(e?.message)
|
||||||
|
throw new InternalServerErrorException(e?.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
|||||||
import { CardsRepository } from '../../cards/infrastructure/cards.repository'
|
import { CardsRepository } from '../../cards/infrastructure/cards.repository'
|
||||||
import { GetAllCardsInDeckDto } from '../../cards/dto/get-all-cards.dto'
|
import { GetAllCardsInDeckDto } from '../../cards/dto/get-all-cards.dto'
|
||||||
import { ForbiddenException, NotFoundException } from '@nestjs/common'
|
import { ForbiddenException, NotFoundException } from '@nestjs/common'
|
||||||
|
import { DecksRepository } from '../infrastructure/decks.repository'
|
||||||
|
|
||||||
export class GetAllCardsInDeckCommand {
|
export class GetAllCardsInDeckCommand {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -13,10 +14,13 @@ export class GetAllCardsInDeckCommand {
|
|||||||
|
|
||||||
@CommandHandler(GetAllCardsInDeckCommand)
|
@CommandHandler(GetAllCardsInDeckCommand)
|
||||||
export class GetAllCardsInDeckHandler implements ICommandHandler<GetAllCardsInDeckCommand> {
|
export class GetAllCardsInDeckHandler implements ICommandHandler<GetAllCardsInDeckCommand> {
|
||||||
constructor(private readonly cardsRepository: CardsRepository) {}
|
constructor(
|
||||||
|
private readonly cardsRepository: CardsRepository,
|
||||||
|
private readonly decksRepository: DecksRepository
|
||||||
|
) {}
|
||||||
|
|
||||||
async execute(command: GetAllCardsInDeckCommand) {
|
async execute(command: GetAllCardsInDeckCommand) {
|
||||||
const deck = await this.cardsRepository.findDeckById(command.deckId)
|
const deck = await this.decksRepository.findDeckById(command.deckId)
|
||||||
if (!deck) throw new NotFoundException(`Deck with id ${command.deckId} not found`)
|
if (!deck) throw new NotFoundException(`Deck with id ${command.deckId} not found`)
|
||||||
|
|
||||||
if (deck.userId !== command.userId && deck.isPrivate) {
|
if (deck.userId !== command.userId && deck.isPrivate) {
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
||||||
|
import { CardsRepository } from '../../cards/infrastructure/cards.repository'
|
||||||
|
import { ForbiddenException, NotFoundException } from '@nestjs/common'
|
||||||
|
import { DecksRepository } from '../infrastructure/decks.repository'
|
||||||
|
import { Prisma } from '@prisma/client'
|
||||||
|
import { pick } from 'remeda'
|
||||||
|
|
||||||
|
export class GetRandomCardInDeckCommand {
|
||||||
|
constructor(public readonly userId: string, public readonly deckId: string) {}
|
||||||
|
}
|
||||||
|
type CardWithGrade = Prisma.cardGetPayload<{ include: { grades: true } }>
|
||||||
|
@CommandHandler(GetRandomCardInDeckCommand)
|
||||||
|
export class GetRandomCardInDeckHandler implements ICommandHandler<GetRandomCardInDeckCommand> {
|
||||||
|
constructor(
|
||||||
|
private readonly cardsRepository: CardsRepository,
|
||||||
|
private readonly decksRepository: DecksRepository
|
||||||
|
) {}
|
||||||
|
private async getSmartRandomCard(cards: Array<CardWithGrade>) {
|
||||||
|
const selectionPool: Array<CardWithGrade> = []
|
||||||
|
cards.forEach(card => {
|
||||||
|
// Calculate the average grade for the card
|
||||||
|
const averageGrade =
|
||||||
|
card.grades.length === 0
|
||||||
|
? 0
|
||||||
|
: card.grades.reduce((acc, grade) => acc + grade.grade, 0) / card.grades.length
|
||||||
|
// Calculate weight for the card, higher weight for lower grade card
|
||||||
|
const weight = 6 - averageGrade
|
||||||
|
|
||||||
|
// Add the card to the selection pool `weight` times
|
||||||
|
for (let i = 0; i < weight; i++) {
|
||||||
|
selectionPool.push(card)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return selectionPool[Math.floor(Math.random() * selectionPool.length)]
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(command: GetRandomCardInDeckCommand) {
|
||||||
|
const deck = await this.decksRepository.findDeckById(command.deckId)
|
||||||
|
if (!deck) throw new NotFoundException(`Deck with id ${command.deckId} not found`)
|
||||||
|
|
||||||
|
if (deck.userId !== command.userId && deck.isPrivate) {
|
||||||
|
throw new ForbiddenException(`You can't get a private deck that you don't own`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const cards = await this.cardsRepository.findCardsByDeckIdWithGrade(
|
||||||
|
command.userId,
|
||||||
|
command.deckId
|
||||||
|
)
|
||||||
|
const smartRandomCard = await this.getSmartRandomCard(cards)
|
||||||
|
return pick(smartRandomCard, ['id', 'question', 'answer', 'deckId'])
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/modules/decks/use-cases/save-grade-use-case.ts
Normal file
41
src/modules/decks/use-cases/save-grade-use-case.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
|
||||||
|
import { CardsRepository } from '../../cards/infrastructure/cards.repository'
|
||||||
|
import { ForbiddenException, NotFoundException } from '@nestjs/common'
|
||||||
|
import { DecksRepository } from '../infrastructure/decks.repository'
|
||||||
|
import { GradesRepository } from '../infrastructure/grades.repository'
|
||||||
|
|
||||||
|
export class SaveGradeCommand {
|
||||||
|
constructor(
|
||||||
|
public readonly userId: string,
|
||||||
|
public readonly args: {
|
||||||
|
cardId: string
|
||||||
|
grade: number
|
||||||
|
}
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@CommandHandler(SaveGradeCommand)
|
||||||
|
export class SaveGradeHandler implements ICommandHandler<SaveGradeCommand> {
|
||||||
|
constructor(
|
||||||
|
private readonly cardsRepository: CardsRepository,
|
||||||
|
private readonly decksRepository: DecksRepository,
|
||||||
|
private readonly gradesRepository: GradesRepository
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async execute(command: SaveGradeCommand) {
|
||||||
|
const deck = await this.decksRepository.findDeckByCardId(command.args.cardId)
|
||||||
|
if (!deck)
|
||||||
|
throw new NotFoundException(`Deck containing card with id ${command.args.cardId} not found`)
|
||||||
|
|
||||||
|
if (deck.userId !== command.userId && deck.isPrivate) {
|
||||||
|
throw new ForbiddenException(`You can't save cards to a private deck that you don't own`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.gradesRepository.createGrade({
|
||||||
|
userId: command.userId,
|
||||||
|
grade: command.args.grade,
|
||||||
|
cardId: command.args.cardId,
|
||||||
|
deckId: deck.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,10 +29,16 @@ export class PrismaService extends PrismaClient implements OnModuleInit {
|
|||||||
async onModuleInit() {
|
async onModuleInit() {
|
||||||
await this.$connect()
|
await this.$connect()
|
||||||
}
|
}
|
||||||
|
private exitHandler(app: INestApplication) {
|
||||||
async enableShutdownHooks(app: INestApplication) {
|
return async () => {
|
||||||
this.$on('beforeExit', async () => {
|
|
||||||
await app.close()
|
await app.close()
|
||||||
})
|
}
|
||||||
|
}
|
||||||
|
async enableShutdownHooks(app: INestApplication) {
|
||||||
|
process.on('exit', this.exitHandler(app))
|
||||||
|
process.on('beforeExit', this.exitHandler(app))
|
||||||
|
process.on('SIGINT', this.exitHandler(app))
|
||||||
|
process.on('SIGTERM', this.exitHandler(app))
|
||||||
|
process.on('SIGUSR2', this.exitHandler(app))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user