подключение к бд через prisma
This commit is contained in:
parent
e7fc8553fb
commit
9b416815ef
|
|
@ -17,6 +17,7 @@
|
||||||
"@nestjs/passport": "^11.0.5",
|
"@nestjs/passport": "^11.0.5",
|
||||||
"@nestjs/platform-express": "^11.1.9",
|
"@nestjs/platform-express": "^11.1.9",
|
||||||
"@nestjs/swagger": "^11.2.3",
|
"@nestjs/swagger": "^11.2.3",
|
||||||
|
"@prisma/client": "^6.9.0",
|
||||||
"@types/multer": "^2.0.0",
|
"@types/multer": "^2.0.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.3",
|
"class-validator": "^0.14.3",
|
||||||
|
|
@ -46,6 +47,7 @@
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"jest": "^30.0.0",
|
"jest": "^30.0.0",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
|
"prisma": "^6.9.0",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"supertest": "^7.0.0",
|
"supertest": "^7.0.0",
|
||||||
"ts-jest": "^29.2.5",
|
"ts-jest": "^29.2.5",
|
||||||
|
|
@ -3192,6 +3194,91 @@
|
||||||
"url": "https://opencollective.com/pkgr"
|
"url": "https://opencollective.com/pkgr"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@prisma/client": {
|
||||||
|
"version": "6.19.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.2.tgz",
|
||||||
|
"integrity": "sha512-gR2EMvfK/aTxsuooaDA32D8v+us/8AAet+C3J1cc04SW35FPdZYgLF+iN4NDLUgAaUGTKdAB0CYenu1TAgGdMg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"prisma": "*",
|
||||||
|
"typescript": ">=5.1.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"prisma": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@prisma/config": {
|
||||||
|
"version": "6.19.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.2.tgz",
|
||||||
|
"integrity": "sha512-kadBGDl+aUswv/zZMk9Mx0C8UZs1kjao8H9/JpI4Wh4SHZaM7zkTwiKn/iFLfRg+XtOAo/Z/c6pAYhijKl0nzQ==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"c12": "3.1.0",
|
||||||
|
"deepmerge-ts": "7.1.5",
|
||||||
|
"effect": "3.18.4",
|
||||||
|
"empathic": "2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@prisma/debug": {
|
||||||
|
"version": "6.19.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.2.tgz",
|
||||||
|
"integrity": "sha512-lFnEZsLdFLmEVCVNdskLDCL8Uup41GDfU0LUfquw+ercJC8ODTuL0WNKgOKmYxCJVvFwf0OuZBzW99DuWmoH2A==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/@prisma/engines": {
|
||||||
|
"version": "6.19.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.2.tgz",
|
||||||
|
"integrity": "sha512-TTkJ8r+uk/uqczX40wb+ODG0E0icVsMgwCTyTHXehaEfb0uo80M9g1aW1tEJrxmFHeOZFXdI2sTA1j1AgcHi4A==",
|
||||||
|
"devOptional": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@prisma/debug": "6.19.2",
|
||||||
|
"@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
|
||||||
|
"@prisma/fetch-engine": "6.19.2",
|
||||||
|
"@prisma/get-platform": "6.19.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@prisma/engines-version": {
|
||||||
|
"version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz",
|
||||||
|
"integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/@prisma/fetch-engine": {
|
||||||
|
"version": "6.19.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.2.tgz",
|
||||||
|
"integrity": "sha512-h4Ff4Pho+SR1S8XerMCC12X//oY2bG3Iug/fUnudfcXEUnIeRiBdXHFdGlGOgQ3HqKgosTEhkZMvGM9tWtYC+Q==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@prisma/debug": "6.19.2",
|
||||||
|
"@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
|
||||||
|
"@prisma/get-platform": "6.19.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@prisma/get-platform": {
|
||||||
|
"version": "6.19.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.2.tgz",
|
||||||
|
"integrity": "sha512-PGLr06JUSTqIvztJtAzIxOwtWKtJm5WwOG6xpsgD37Rc84FpfUBGLKz65YpJBGtkRQGXTYEFie7pYALocC3MtA==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@prisma/debug": "6.19.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@scarf/scarf": {
|
"node_modules/@scarf/scarf": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
|
||||||
|
|
@ -3226,6 +3313,13 @@
|
||||||
"@sinonjs/commons": "^3.0.1"
|
"@sinonjs/commons": "^3.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@standard-schema/spec": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@tokenizer/inflate": {
|
"node_modules/@tokenizer/inflate": {
|
||||||
"version": "0.3.1",
|
"version": "0.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.3.1.tgz",
|
||||||
|
|
@ -4941,6 +5035,48 @@
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/c12": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"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.4.2",
|
||||||
|
"ohash": "^2.0.11",
|
||||||
|
"pathe": "^2.0.3",
|
||||||
|
"perfect-debounce": "^1.0.0",
|
||||||
|
"pkg-types": "^2.2.0",
|
||||||
|
"rc9": "^2.1.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"magicast": "^0.3.5"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"magicast": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/c12/node_modules/dotenv": {
|
||||||
|
"version": "16.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||||
|
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://dotenvx.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/call-bind-apply-helpers": {
|
"node_modules/call-bind-apply-helpers": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
|
|
@ -5049,7 +5185,7 @@
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"readdirp": "^4.0.1"
|
"readdirp": "^4.0.1"
|
||||||
|
|
@ -5087,6 +5223,16 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/citty": {
|
||||||
|
"version": "0.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz",
|
||||||
|
"integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"consola": "^3.2.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cjs-module-lexer": {
|
"node_modules/cjs-module-lexer": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.1.tgz",
|
||||||
|
|
@ -5337,6 +5483,13 @@
|
||||||
"typedarray": "^0.0.6"
|
"typedarray": "^0.0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/confbox": {
|
||||||
|
"version": "0.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
|
||||||
|
"integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/consola": {
|
"node_modules/consola": {
|
||||||
"version": "3.4.2",
|
"version": "3.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
|
||||||
|
|
@ -5517,6 +5670,16 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/deepmerge-ts": {
|
||||||
|
"version": "7.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz",
|
||||||
|
"integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/defaults": {
|
"node_modules/defaults": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
|
||||||
|
|
@ -5530,6 +5693,13 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/defu": {
|
||||||
|
"version": "6.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
|
||||||
|
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/delayed-stream": {
|
"node_modules/delayed-stream": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
|
@ -5549,6 +5719,13 @@
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/destr": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
|
|
@ -5652,6 +5829,17 @@
|
||||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/effect": {
|
||||||
|
"version": "3.18.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz",
|
||||||
|
"integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@standard-schema/spec": "^1.0.0",
|
||||||
|
"fast-check": "^3.23.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.254",
|
"version": "1.5.254",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.254.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.254.tgz",
|
||||||
|
|
@ -5679,6 +5867,16 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/empathic": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/encodeurl": {
|
"node_modules/encodeurl": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||||
|
|
@ -6129,6 +6327,53 @@
|
||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/exsolve": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/fast-check": {
|
||||||
|
"version": "3.23.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz",
|
||||||
|
"integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==",
|
||||||
|
"devOptional": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/dubzzz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/fast-check"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pure-rand": "^6.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fast-check/node_modules/pure-rand": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==",
|
||||||
|
"devOptional": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/dubzzz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/fast-check"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
|
|
@ -6589,6 +6834,24 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/giget": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"citty": "^0.1.6",
|
||||||
|
"consola": "^3.4.0",
|
||||||
|
"defu": "^6.1.4",
|
||||||
|
"node-fetch-native": "^1.6.6",
|
||||||
|
"nypm": "^0.6.0",
|
||||||
|
"pathe": "^2.0.3"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"giget": "dist/cli.mjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/glob": {
|
"node_modules/glob": {
|
||||||
"version": "11.0.3",
|
"version": "11.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz",
|
||||||
|
|
@ -7912,6 +8175,16 @@
|
||||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jiti": {
|
||||||
|
"version": "2.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
||||||
|
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
|
|
@ -8594,6 +8867,13 @@
|
||||||
"lodash": "^4.17.21"
|
"lodash": "^4.17.21"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-fetch-native": {
|
||||||
|
"version": "1.6.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
|
||||||
|
"integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/node-int64": {
|
"node_modules/node-int64": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
||||||
|
|
@ -8631,6 +8911,31 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nypm": {
|
||||||
|
"version": "0.6.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz",
|
||||||
|
"integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"citty": "^0.2.0",
|
||||||
|
"pathe": "^2.0.3",
|
||||||
|
"tinyexec": "^1.0.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"nypm": "dist/cli.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nypm/node_modules/citty": {
|
||||||
|
"version": "0.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/citty/-/citty-0.2.1.tgz",
|
||||||
|
"integrity": "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/object-assign": {
|
"node_modules/object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
|
|
@ -8652,6 +8957,13 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ohash": {
|
||||||
|
"version": "2.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
|
||||||
|
"integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/on-finished": {
|
"node_modules/on-finished": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||||
|
|
@ -8968,11 +9280,25 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pathe": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/pause": {
|
"node_modules/pause": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
|
||||||
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
|
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/perfect-debounce": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
|
|
@ -9072,6 +9398,18 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pkg-types": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"confbox": "^0.2.2",
|
||||||
|
"exsolve": "^1.0.7",
|
||||||
|
"pathe": "^2.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pluralize": {
|
"node_modules/pluralize": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
|
||||||
|
|
@ -9149,6 +9487,32 @@
|
||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prisma": {
|
||||||
|
"version": "6.19.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.2.tgz",
|
||||||
|
"integrity": "sha512-XTKeKxtQElcq3U9/jHyxSPgiRgeYDKxWTPOf6NkXA0dNj5j40MfEsZkMbyNpwDWCUv7YBFUl7I2VK/6ALbmhEg==",
|
||||||
|
"devOptional": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@prisma/config": "6.19.2",
|
||||||
|
"@prisma/engines": "6.19.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"prisma": "build/index.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=5.1.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/proxy-addr": {
|
"node_modules/proxy-addr": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
|
|
@ -9259,6 +9623,17 @@
|
||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rc9": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"defu": "^6.1.4",
|
||||||
|
"destr": "^2.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-is": {
|
"node_modules/react-is": {
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
||||||
|
|
@ -9284,7 +9659,7 @@
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14.18.0"
|
"node": ">= 14.18.0"
|
||||||
|
|
@ -10297,6 +10672,16 @@
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tinyexec": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tmpl": {
|
"node_modules/tmpl": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
|
||||||
|
|
@ -10605,7 +10990,7 @@
|
||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
|
|
|
||||||
13
package.json
13
package.json
|
|
@ -17,7 +17,11 @@
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"test:cov": "jest --coverage",
|
"test:cov": "jest --coverage",
|
||||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||||
|
"prisma:generate": "prisma generate",
|
||||||
|
"prisma:migrate": "prisma migrate dev",
|
||||||
|
"prisma:studio": "prisma studio",
|
||||||
|
"prisma:seed": "ts-node prisma/seed.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/common": "^11.0.1",
|
"@nestjs/common": "^11.0.1",
|
||||||
|
|
@ -37,9 +41,11 @@
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"sharp": "^0.34.5"
|
"sharp": "^0.34.5",
|
||||||
|
"@prisma/client": "^6.9.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"prisma": "^6.9.0",
|
||||||
"@eslint/eslintrc": "^3.2.0",
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
"@eslint/js": "^9.18.0",
|
"@eslint/js": "^9.18.0",
|
||||||
"@nestjs/cli": "^11.0.0",
|
"@nestjs/cli": "^11.0.0",
|
||||||
|
|
@ -82,5 +88,8 @@
|
||||||
],
|
],
|
||||||
"coverageDirectory": "../coverage",
|
"coverageDirectory": "../coverage",
|
||||||
"testEnvironment": "node"
|
"testEnvironment": "node"
|
||||||
|
},
|
||||||
|
"prisma": {
|
||||||
|
"seed": "ts-node prisma/seed.ts"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "User" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"firstName" TEXT NOT NULL DEFAULT '',
|
||||||
|
"lastName" TEXT NOT NULL DEFAULT '',
|
||||||
|
"phone" TEXT NOT NULL,
|
||||||
|
"email" TEXT NOT NULL DEFAULT '',
|
||||||
|
"password" TEXT,
|
||||||
|
"companyName" TEXT,
|
||||||
|
"inn" BIGINT,
|
||||||
|
"ogrn" BIGINT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "yachts" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"model" TEXT,
|
||||||
|
"year" INTEGER NOT NULL,
|
||||||
|
"length" DOUBLE PRECISION NOT NULL,
|
||||||
|
"speed" INTEGER DEFAULT 0,
|
||||||
|
"minCost" INTEGER DEFAULT 0,
|
||||||
|
"mainImageUrl" TEXT DEFAULT '',
|
||||||
|
"galleryUrls" JSONB,
|
||||||
|
"hasQuickRent" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"isFeatured" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"topText" TEXT,
|
||||||
|
"comfortCapacity" INTEGER DEFAULT 0,
|
||||||
|
"maxCapacity" INTEGER DEFAULT 0,
|
||||||
|
"width" DOUBLE PRECISION DEFAULT 0,
|
||||||
|
"cabinsCount" INTEGER DEFAULT 0,
|
||||||
|
"matherial" TEXT DEFAULT '',
|
||||||
|
"power" INTEGER DEFAULT 0,
|
||||||
|
"description" TEXT DEFAULT '',
|
||||||
|
"userId" INTEGER NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "yachts_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "reservations" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"yachtId" INTEGER NOT NULL,
|
||||||
|
"reservatorId" INTEGER NOT NULL,
|
||||||
|
"startUtc" INTEGER NOT NULL,
|
||||||
|
"endUtc" INTEGER NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "reservations_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "reviews" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"reviewerId" INTEGER NOT NULL,
|
||||||
|
"yachtId" INTEGER NOT NULL,
|
||||||
|
"starsCount" INTEGER NOT NULL,
|
||||||
|
"description" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "reviews_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "refresh_tokens" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"token" TEXT NOT NULL,
|
||||||
|
"userId" INTEGER NOT NULL,
|
||||||
|
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "refresh_tokens_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "verification_codes" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"phone" TEXT NOT NULL,
|
||||||
|
"code" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "verification_codes_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "sms_send_logs" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"phone" TEXT NOT NULL,
|
||||||
|
"ip" TEXT NOT NULL,
|
||||||
|
"sentAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "sms_send_logs_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "verify_blocks" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"phone" TEXT NOT NULL,
|
||||||
|
"ip" TEXT NOT NULL,
|
||||||
|
"count" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"blockedUntil" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "verify_blocks_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "User_phone_key" ON "User"("phone");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "refresh_tokens_token_key" ON "refresh_tokens"("token");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "verification_codes_phone_idx" ON "verification_codes"("phone");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "sms_send_logs_phone_sentAt_idx" ON "sms_send_logs"("phone", "sentAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "sms_send_logs_ip_sentAt_idx" ON "sms_send_logs"("ip", "sentAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "verify_blocks_phone_ip_key" ON "verify_blocks"("phone", "ip");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "yachts" ADD CONSTRAINT "yachts_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "reservations" ADD CONSTRAINT "reservations_yachtId_fkey" FOREIGN KEY ("yachtId") REFERENCES "yachts"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "reservations" ADD CONSTRAINT "reservations_reservatorId_fkey" FOREIGN KEY ("reservatorId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "reviews" ADD CONSTRAINT "reviews_yachtId_fkey" FOREIGN KEY ("yachtId") REFERENCES "yachts"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "reviews" ADD CONSTRAINT "reviews_reviewerId_fkey" FOREIGN KEY ("reviewerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "refresh_tokens" ADD CONSTRAINT "refresh_tokens_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "postgresql"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
model User {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
firstName String @default("")
|
||||||
|
lastName String @default("")
|
||||||
|
phone String @unique
|
||||||
|
email String @default("")
|
||||||
|
password String?
|
||||||
|
companyName String?
|
||||||
|
inn BigInt?
|
||||||
|
ogrn BigInt?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
yachts Yacht[]
|
||||||
|
reservations Reservation[] @relation("Reservator")
|
||||||
|
reviews Review[]
|
||||||
|
refreshTokens RefreshToken[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model Yacht {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
name String
|
||||||
|
model String?
|
||||||
|
year Int
|
||||||
|
length Float
|
||||||
|
speed Int? @default(0)
|
||||||
|
minCost Int? @default(0)
|
||||||
|
mainImageUrl String? @default("")
|
||||||
|
galleryUrls Json?
|
||||||
|
hasQuickRent Boolean @default(false)
|
||||||
|
isFeatured Boolean @default(false)
|
||||||
|
topText String?
|
||||||
|
comfortCapacity Int? @default(0)
|
||||||
|
maxCapacity Int? @default(0)
|
||||||
|
width Float? @default(0)
|
||||||
|
cabinsCount Int? @default(0)
|
||||||
|
matherial String? @default("")
|
||||||
|
power Int? @default(0)
|
||||||
|
description String? @default("")
|
||||||
|
userId Int
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
owner User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
reservations Reservation[]
|
||||||
|
reviews Review[]
|
||||||
|
|
||||||
|
@@map("yachts")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Reservation {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
yachtId Int
|
||||||
|
reservatorId Int
|
||||||
|
startUtc Int
|
||||||
|
endUtc Int
|
||||||
|
|
||||||
|
yacht Yacht @relation(fields: [yachtId], references: [id], onDelete: Cascade)
|
||||||
|
reservator User @relation("Reservator", fields: [reservatorId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@map("reservations")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Review {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
reviewerId Int
|
||||||
|
yachtId Int
|
||||||
|
starsCount Int
|
||||||
|
description String
|
||||||
|
|
||||||
|
yacht Yacht @relation(fields: [yachtId], references: [id], onDelete: Cascade)
|
||||||
|
reviewer User @relation(fields: [reviewerId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@map("reviews")
|
||||||
|
}
|
||||||
|
|
||||||
|
model RefreshToken {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
token String @unique
|
||||||
|
userId Int
|
||||||
|
expiresAt DateTime
|
||||||
|
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@map("refresh_tokens")
|
||||||
|
}
|
||||||
|
|
||||||
|
model VerificationCode {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
phone String
|
||||||
|
code String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@index([phone])
|
||||||
|
@@map("verification_codes")
|
||||||
|
}
|
||||||
|
|
||||||
|
model SmsSendLog {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
phone String
|
||||||
|
ip String
|
||||||
|
sentAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@index([phone, sentAt])
|
||||||
|
@@index([ip, sentAt])
|
||||||
|
@@map("sms_send_logs")
|
||||||
|
}
|
||||||
|
|
||||||
|
model VerifyBlock {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
phone String
|
||||||
|
ip String
|
||||||
|
count Int @default(0)
|
||||||
|
blockedUntil DateTime
|
||||||
|
|
||||||
|
@@unique([phone, ip])
|
||||||
|
@@map("verify_blocks")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,346 @@
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
const galleryUrls = [
|
||||||
|
'uploads/gal1.jpg',
|
||||||
|
'uploads/gal2.jpg',
|
||||||
|
'uploads/gal3.jpg',
|
||||||
|
'uploads/gal4.jpg',
|
||||||
|
'uploads/gal5.jpg',
|
||||||
|
'uploads/gal6.jpg',
|
||||||
|
'uploads/gal7.jpg',
|
||||||
|
'uploads/gal8.jpg',
|
||||||
|
'uploads/gal9.jpg',
|
||||||
|
'uploads/gal10.jpg',
|
||||||
|
];
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
await prisma.review.deleteMany();
|
||||||
|
await prisma.reservation.deleteMany();
|
||||||
|
await prisma.yacht.deleteMany();
|
||||||
|
await prisma.refreshToken.deleteMany();
|
||||||
|
await prisma.verificationCode.deleteMany();
|
||||||
|
await prisma.smsSendLog.deleteMany();
|
||||||
|
await prisma.verifyBlock.deleteMany();
|
||||||
|
await prisma.user.deleteMany();
|
||||||
|
|
||||||
|
const users = await Promise.all([
|
||||||
|
prisma.user.create({
|
||||||
|
data: {
|
||||||
|
firstName: 'Иван',
|
||||||
|
lastName: 'Андреев',
|
||||||
|
phone: '+79009009090',
|
||||||
|
email: 'ivan@yachting.ru',
|
||||||
|
password: 'admin',
|
||||||
|
companyName: 'Северный Флот',
|
||||||
|
inn: BigInt(1234567890),
|
||||||
|
ogrn: BigInt(1122334455667),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.user.create({
|
||||||
|
data: {
|
||||||
|
firstName: 'Сергей',
|
||||||
|
lastName: 'Большаков',
|
||||||
|
phone: '+79119119191',
|
||||||
|
email: 'sergey@yachting.ru',
|
||||||
|
password: 'admin',
|
||||||
|
companyName: 'Балтийские Просторы',
|
||||||
|
inn: BigInt(9876543210),
|
||||||
|
ogrn: BigInt(9988776655443),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.user.create({
|
||||||
|
data: {
|
||||||
|
firstName: 'Анна',
|
||||||
|
lastName: 'Петрова',
|
||||||
|
phone: '+79229229292',
|
||||||
|
email: 'anna@yachting.ru',
|
||||||
|
password: 'admin',
|
||||||
|
companyName: 'Ладожские Ветры',
|
||||||
|
inn: BigInt(5555555555),
|
||||||
|
ogrn: BigInt(3333444455556),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.user.create({
|
||||||
|
data: {
|
||||||
|
firstName: 'Дмитрий',
|
||||||
|
lastName: 'Соколов',
|
||||||
|
phone: '+79339339393',
|
||||||
|
email: 'dmitry@yachting.ru',
|
||||||
|
password: 'admin',
|
||||||
|
companyName: 'Финский Залив',
|
||||||
|
inn: BigInt(1111222233),
|
||||||
|
ogrn: BigInt(7777888899990),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const yachtData = [
|
||||||
|
{
|
||||||
|
name: 'Азимут 55',
|
||||||
|
length: 16.7,
|
||||||
|
speed: 32,
|
||||||
|
minCost: 85000,
|
||||||
|
mainImageUrl: 'uploads/1st.jpg',
|
||||||
|
hasQuickRent: true,
|
||||||
|
isFeatured: true,
|
||||||
|
year: 2022,
|
||||||
|
comfortCapacity: 8,
|
||||||
|
maxCapacity: 12,
|
||||||
|
width: 4.8,
|
||||||
|
cabinsCount: 3,
|
||||||
|
matherial: 'Стеклопластик',
|
||||||
|
power: 1200,
|
||||||
|
description:
|
||||||
|
'Роскошная моторная яхта Азимут 55 - это воплощение итальянского стиля и российского качества. Идеально подходит для прогулок по Финскому заливу, корпоративных мероприятий и романтических свиданий. На борту: три комфортабельные каюты, просторный салон с панорамным остеклением, полностью оборудованная кухня и две ванные комнаты. Максимальная скорость 32 узла позволяет быстро добраться до самых живописных мест Карельского перешейка.',
|
||||||
|
userId: users[0].id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Сансикер Манхэттен 52',
|
||||||
|
length: 15.8,
|
||||||
|
speed: 34,
|
||||||
|
minCost: 92000,
|
||||||
|
mainImageUrl: 'uploads/2nd.jpg',
|
||||||
|
hasQuickRent: false,
|
||||||
|
isFeatured: false,
|
||||||
|
topText: '🔥 Лучшее предложение',
|
||||||
|
year: 2023,
|
||||||
|
comfortCapacity: 6,
|
||||||
|
maxCapacity: 10,
|
||||||
|
width: 4.5,
|
||||||
|
cabinsCount: 3,
|
||||||
|
matherial: 'Стеклопластик',
|
||||||
|
power: 1400,
|
||||||
|
description:
|
||||||
|
'Британский шик и русская душа в одной яхте! Сансикер Манхэттен 52 - выбор настоящих ценителей морских путешествий.',
|
||||||
|
userId: users[1].id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Принцесс V55',
|
||||||
|
length: 16.7,
|
||||||
|
speed: 33,
|
||||||
|
minCost: 78000,
|
||||||
|
mainImageUrl: 'uploads/3rd.jpg',
|
||||||
|
hasQuickRent: true,
|
||||||
|
isFeatured: false,
|
||||||
|
topText: '🍷 Идеальна для заката с бокалом вина',
|
||||||
|
year: 2021,
|
||||||
|
comfortCapacity: 8,
|
||||||
|
maxCapacity: 12,
|
||||||
|
width: 4.7,
|
||||||
|
cabinsCount: 4,
|
||||||
|
matherial: 'Стеклопластик',
|
||||||
|
power: 1100,
|
||||||
|
description: 'Принцесс V55 - королева российских вод!',
|
||||||
|
userId: users[0].id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Ферретти 500',
|
||||||
|
length: 15.2,
|
||||||
|
speed: 31,
|
||||||
|
minCost: 68000,
|
||||||
|
mainImageUrl: 'uploads/4th.jpg',
|
||||||
|
hasQuickRent: true,
|
||||||
|
isFeatured: false,
|
||||||
|
topText: '⏳ Часто бронируется - успей',
|
||||||
|
year: 2020,
|
||||||
|
comfortCapacity: 6,
|
||||||
|
maxCapacity: 8,
|
||||||
|
width: 4.3,
|
||||||
|
cabinsCount: 3,
|
||||||
|
matherial: 'Стеклопластик',
|
||||||
|
power: 900,
|
||||||
|
description: 'Итальянская страсть в русской стихии!',
|
||||||
|
userId: users[1].id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Си Рей 510 Сандансер',
|
||||||
|
length: 15.5,
|
||||||
|
speed: 35,
|
||||||
|
minCost: 72000,
|
||||||
|
mainImageUrl: 'uploads/5th.jpg',
|
||||||
|
hasQuickRent: false,
|
||||||
|
isFeatured: false,
|
||||||
|
year: 2023,
|
||||||
|
comfortCapacity: 8,
|
||||||
|
maxCapacity: 10,
|
||||||
|
width: 4.6,
|
||||||
|
cabinsCount: 3,
|
||||||
|
matherial: 'Стеклопластик',
|
||||||
|
power: 1300,
|
||||||
|
description: 'Американская мощь для русского моря!',
|
||||||
|
userId: users[0].id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Бавария SR41',
|
||||||
|
length: 12.5,
|
||||||
|
speed: 28,
|
||||||
|
minCost: 45000,
|
||||||
|
mainImageUrl: 'uploads/6th.jpg',
|
||||||
|
hasQuickRent: true,
|
||||||
|
isFeatured: false,
|
||||||
|
year: 2019,
|
||||||
|
comfortCapacity: 6,
|
||||||
|
maxCapacity: 8,
|
||||||
|
width: 3.9,
|
||||||
|
cabinsCount: 2,
|
||||||
|
matherial: 'Стеклопластик',
|
||||||
|
power: 320,
|
||||||
|
description: 'Немецкое качество для русского характера!',
|
||||||
|
userId: users[1].id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Жанно Мери Фишер 895',
|
||||||
|
length: 8.9,
|
||||||
|
speed: 25,
|
||||||
|
minCost: 32000,
|
||||||
|
mainImageUrl: 'uploads/1st.jpg',
|
||||||
|
hasQuickRent: true,
|
||||||
|
isFeatured: false,
|
||||||
|
year: 2022,
|
||||||
|
comfortCapacity: 4,
|
||||||
|
maxCapacity: 6,
|
||||||
|
width: 3.0,
|
||||||
|
cabinsCount: 1,
|
||||||
|
matherial: 'Стеклопластик',
|
||||||
|
power: 250,
|
||||||
|
description: 'Французская элегантность для русского простора!',
|
||||||
|
userId: users[0].id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Бенето Свифт Троулер 41',
|
||||||
|
length: 12.5,
|
||||||
|
speed: 22,
|
||||||
|
minCost: 55000,
|
||||||
|
mainImageUrl: 'uploads/2nd.jpg',
|
||||||
|
hasQuickRent: false,
|
||||||
|
isFeatured: false,
|
||||||
|
year: 2021,
|
||||||
|
comfortCapacity: 6,
|
||||||
|
maxCapacity: 8,
|
||||||
|
width: 4.2,
|
||||||
|
cabinsCount: 2,
|
||||||
|
matherial: 'Стеклопластик',
|
||||||
|
power: 425,
|
||||||
|
description: 'Французский траулер для русского севера!',
|
||||||
|
userId: users[1].id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Лагун 450',
|
||||||
|
length: 13.5,
|
||||||
|
speed: 20,
|
||||||
|
minCost: 65000,
|
||||||
|
mainImageUrl: 'uploads/3rd.jpg',
|
||||||
|
hasQuickRent: true,
|
||||||
|
isFeatured: false,
|
||||||
|
year: 2020,
|
||||||
|
comfortCapacity: 8,
|
||||||
|
maxCapacity: 10,
|
||||||
|
width: 7.8,
|
||||||
|
cabinsCount: 4,
|
||||||
|
matherial: 'Стеклопластик',
|
||||||
|
power: 90,
|
||||||
|
description: 'Французский катамаран для русского размаха!',
|
||||||
|
userId: users[0].id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Фонтен Пажо Люсия 40',
|
||||||
|
length: 11.7,
|
||||||
|
speed: 18,
|
||||||
|
minCost: 58000,
|
||||||
|
mainImageUrl: 'uploads/4th.jpg',
|
||||||
|
hasQuickRent: true,
|
||||||
|
isFeatured: false,
|
||||||
|
year: 2023,
|
||||||
|
comfortCapacity: 8,
|
||||||
|
maxCapacity: 10,
|
||||||
|
width: 7.1,
|
||||||
|
cabinsCount: 4,
|
||||||
|
matherial: 'Стеклопластик',
|
||||||
|
power: 80,
|
||||||
|
description: 'Французский катамаран класса люкс!',
|
||||||
|
userId: users[1].id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Дюфур 460',
|
||||||
|
length: 14.1,
|
||||||
|
speed: 26,
|
||||||
|
minCost: 62000,
|
||||||
|
mainImageUrl: 'uploads/5th.jpg',
|
||||||
|
hasQuickRent: false,
|
||||||
|
isFeatured: false,
|
||||||
|
year: 2022,
|
||||||
|
comfortCapacity: 8,
|
||||||
|
maxCapacity: 10,
|
||||||
|
width: 4.5,
|
||||||
|
cabinsCount: 3,
|
||||||
|
matherial: 'Стеклопластик',
|
||||||
|
power: 380,
|
||||||
|
description: 'Французская парусная яхта для русского ветра!',
|
||||||
|
userId: users[0].id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Гранд Бэнкс 60',
|
||||||
|
length: 18.3,
|
||||||
|
speed: 24,
|
||||||
|
minCost: 125000,
|
||||||
|
mainImageUrl: 'uploads/6th.jpg',
|
||||||
|
hasQuickRent: true,
|
||||||
|
isFeatured: false,
|
||||||
|
year: 2023,
|
||||||
|
comfortCapacity: 6,
|
||||||
|
maxCapacity: 8,
|
||||||
|
width: 5.2,
|
||||||
|
cabinsCount: 3,
|
||||||
|
matherial: 'Стеклопластик',
|
||||||
|
power: 1600,
|
||||||
|
description: 'Американская легенда для русского океана!',
|
||||||
|
userId: users[1].id,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const yachts = await Promise.all(
|
||||||
|
yachtData.map((y) =>
|
||||||
|
prisma.yacht.create({
|
||||||
|
data: {
|
||||||
|
...y,
|
||||||
|
galleryUrls: galleryUrls as object,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await prisma.reservation.createMany({
|
||||||
|
data: [
|
||||||
|
{ yachtId: yachts[0].id, reservatorId: users[0].id, startUtc: 1767369600, endUtc: 1767412800 },
|
||||||
|
{ yachtId: yachts[2].id, reservatorId: users[1].id, startUtc: 1767484800, endUtc: 1767715200 },
|
||||||
|
{ yachtId: yachts[4].id, reservatorId: users[0].id, startUtc: 1768070400, endUtc: 1768176000 },
|
||||||
|
{ yachtId: yachts[6].id, reservatorId: users[1].id, startUtc: 1768435200, endUtc: 1768684800 },
|
||||||
|
{ yachtId: yachts[8].id, reservatorId: users[0].id, startUtc: 1768944000, endUtc: 1769049600 },
|
||||||
|
{ yachtId: yachts[10].id, reservatorId: users[1].id, startUtc: 1769385600, endUtc: 1769635200 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.review.createMany({
|
||||||
|
data: [
|
||||||
|
{ reviewerId: users[0].id, yachtId: yachts[0].id, starsCount: 5, description: 'Excellent yacht!' },
|
||||||
|
{ reviewerId: users[1].id, yachtId: yachts[0].id, starsCount: 4, description: 'Very good experience' },
|
||||||
|
{ reviewerId: users[0].id, yachtId: yachts[2].id, starsCount: 3, description: 'Average condition' },
|
||||||
|
{ reviewerId: users[1].id, yachtId: yachts[4].id, starsCount: 5, description: 'Perfect for sailing' },
|
||||||
|
{ reviewerId: users[0].id, yachtId: yachts[6].id, starsCount: 4, description: 'Comfortable and fast' },
|
||||||
|
{ reviewerId: users[1].id, yachtId: yachts[8].id, starsCount: 2, description: 'Needs maintenance' },
|
||||||
|
{ reviewerId: users[0].id, yachtId: yachts[10].id, starsCount: 5, description: 'Luxury experience' },
|
||||||
|
{ reviewerId: users[1].id, yachtId: yachts[11].id, starsCount: 4, description: 'Great value for money' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Seed completed: users', users.length, 'yachts', yachts.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
})
|
||||||
|
.finally(() => prisma.$disconnect());
|
||||||
|
|
@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
|
import { PrismaModule } from './prisma/prisma.module';
|
||||||
import { YachtsModule } from './yachts/yachts.module';
|
import { YachtsModule } from './yachts/yachts.module';
|
||||||
import { CatalogModule } from './catalog/catalog.module';
|
import { CatalogModule } from './catalog/catalog.module';
|
||||||
import { AuthModule } from './auth/auth.module';
|
import { AuthModule } from './auth/auth.module';
|
||||||
|
|
@ -13,6 +14,7 @@ import { ReservationsModule } from './reservations/reservations.module';
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({ isGlobal: true }),
|
ConfigModule.forRoot({ isGlobal: true }),
|
||||||
|
PrismaModule,
|
||||||
YachtsModule,
|
YachtsModule,
|
||||||
CatalogModule,
|
CatalogModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
|
|
|
||||||
|
|
@ -38,19 +38,19 @@ export class AuthService {
|
||||||
throw new BadRequestException('Некорректный номер телефона');
|
throw new BadRequestException('Некорректный номер телефона');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.verificationStore.canSendToPhone(normalizedPhone)) {
|
if (!(await this.verificationStore.canSendToPhone(normalizedPhone))) {
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
'Повторная отправка кода возможна не чаще 1 раза в 60 секунд',
|
'Повторная отправка кода возможна не чаще 1 раза в 60 секунд',
|
||||||
HttpStatus.TOO_MANY_REQUESTS,
|
HttpStatus.TOO_MANY_REQUESTS,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (this.verificationStore.getSendCountByPhone(normalizedPhone) >= SMS_PER_PHONE_MAX) {
|
if ((await this.verificationStore.getSendCountByPhone(normalizedPhone)) >= SMS_PER_PHONE_MAX) {
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
`Не более ${SMS_PER_PHONE_MAX} SMS на номер за 15 минут`,
|
`Не более ${SMS_PER_PHONE_MAX} SMS на номер за 15 минут`,
|
||||||
HttpStatus.TOO_MANY_REQUESTS,
|
HttpStatus.TOO_MANY_REQUESTS,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (this.verificationStore.getSendCountByIp(ip) >= SMS_PER_IP_MAX) {
|
if ((await this.verificationStore.getSendCountByIp(ip)) >= SMS_PER_IP_MAX) {
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
`Превышен лимит запросов с вашего IP (${SMS_PER_IP_MAX} за 15 минут)`,
|
`Превышен лимит запросов с вашего IP (${SMS_PER_IP_MAX} за 15 минут)`,
|
||||||
HttpStatus.TOO_MANY_REQUESTS,
|
HttpStatus.TOO_MANY_REQUESTS,
|
||||||
|
|
@ -58,7 +58,7 @@ export class AuthService {
|
||||||
}
|
}
|
||||||
|
|
||||||
const code = this.generateCode();
|
const code = this.generateCode();
|
||||||
this.verificationStore.recordSend(normalizedPhone, ip, code);
|
await this.verificationStore.recordSend(normalizedPhone, ip, code);
|
||||||
await this.smsService.sendVerificationCode(normalizedPhone, code);
|
await this.smsService.sendVerificationCode(normalizedPhone, code);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
@ -77,15 +77,15 @@ export class AuthService {
|
||||||
throw new BadRequestException('Код должен содержать 4 символа');
|
throw new BadRequestException('Код должен содержать 4 символа');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.verificationStore.isVerifyBlocked(normalizedPhone, ip)) {
|
if (await this.verificationStore.isVerifyBlocked(normalizedPhone, ip)) {
|
||||||
const remaining = this.verificationStore.getBlockedRemainingSec(normalizedPhone, ip);
|
const remaining = await this.verificationStore.getBlockedRemainingSec(normalizedPhone, ip);
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
`Слишком много попыток. Попробуйте через ${remaining} секунд`,
|
`Слишком много попыток. Попробуйте через ${remaining} секунд`,
|
||||||
HttpStatus.TOO_MANY_REQUESTS,
|
HttpStatus.TOO_MANY_REQUESTS,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = this.verificationStore.checkCode(normalizedPhone, ip, code);
|
const result = await this.verificationStore.checkCode(normalizedPhone, ip, code);
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
if (result.reason === 'expired') {
|
if (result.reason === 'expired') {
|
||||||
throw new BadRequestException('Код истёк. Запросите новый');
|
throw new BadRequestException('Код истёк. Запросите новый');
|
||||||
|
|
@ -100,33 +100,33 @@ export class AuthService {
|
||||||
user = await this.usersService.createByPhone(normalizedPhone);
|
user = await this.usersService.createByPhone(normalizedPhone);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.issueTokens(user);
|
return await this.issueTokens(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Обновить access токен по refresh токену. */
|
/** Обновить access токен по refresh токену. */
|
||||||
async refresh(refreshToken: string): Promise<{ access_token: string; refresh_token: string }> {
|
async refresh(refreshToken: string): Promise<{ access_token: string; refresh_token: string }> {
|
||||||
const stored = this.refreshTokenStore.get(refreshToken);
|
const stored = await this.refreshTokenStore.get(refreshToken);
|
||||||
if (!stored) {
|
if (!stored) {
|
||||||
throw new BadRequestException('Недействительный или просроченный refresh token');
|
throw new BadRequestException('Недействительный или просроченный refresh token');
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
this.jwtService.verify(refreshToken, { secret: this.configService.get<string>('JWT_SECRET') });
|
this.jwtService.verify(refreshToken, { secret: this.configService.get<string>('JWT_SECRET') });
|
||||||
} catch {
|
} catch {
|
||||||
this.refreshTokenStore.remove(refreshToken);
|
await this.refreshTokenStore.remove(refreshToken);
|
||||||
throw new BadRequestException('Недействительный refresh token');
|
throw new BadRequestException('Недействительный refresh token');
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await this.usersService.findById(stored.userId);
|
const user = await this.usersService.findById(stored.userId);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
this.refreshTokenStore.remove(refreshToken);
|
await this.refreshTokenStore.remove(refreshToken);
|
||||||
throw new BadRequestException('Пользователь не найден');
|
throw new BadRequestException('Пользователь не найден');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.refreshTokenStore.remove(refreshToken);
|
await this.refreshTokenStore.remove(refreshToken);
|
||||||
return this.issueTokens(user);
|
return await this.issueTokens(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
private issueTokens(user: User): { access_token: string; refresh_token: string } {
|
private async issueTokens(user: User): Promise<{ access_token: string; refresh_token: string }> {
|
||||||
const payload = {
|
const payload = {
|
||||||
sub: user.userId,
|
sub: user.userId,
|
||||||
userId: user.userId,
|
userId: user.userId,
|
||||||
|
|
@ -141,7 +141,7 @@ export class AuthService {
|
||||||
);
|
);
|
||||||
const decoded = this.jwtService.decode(refresh_token) as { exp: number };
|
const decoded = this.jwtService.decode(refresh_token) as { exp: number };
|
||||||
const expiresAt = decoded?.exp ? decoded.exp * 1000 : Date.now() + 30 * 24 * 60 * 60 * 1000;
|
const expiresAt = decoded?.exp ? decoded.exp * 1000 : Date.now() + 30 * 24 * 60 * 60 * 1000;
|
||||||
this.refreshTokenStore.set(refresh_token, user.userId, expiresAt);
|
await this.refreshTokenStore.set(refresh_token, user.userId, expiresAt);
|
||||||
return { access_token, refresh_token };
|
return { access_token, refresh_token };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,36 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
interface StoredRefresh {
|
|
||||||
userId: number;
|
|
||||||
expiresAt: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RefreshTokenStoreService {
|
export class RefreshTokenStoreService {
|
||||||
private readonly store = new Map<string, StoredRefresh>();
|
|
||||||
private cleanupInterval: ReturnType<typeof setInterval> | null = null;
|
private cleanupInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor(private readonly prisma: PrismaService) {
|
||||||
this.cleanupInterval = setInterval(() => this.cleanup(), 60_000);
|
this.cleanupInterval = setInterval(() => this.cleanup(), 60_000);
|
||||||
}
|
}
|
||||||
|
|
||||||
private cleanup() {
|
private async cleanup() {
|
||||||
const now = Date.now();
|
const now = new Date();
|
||||||
for (const [token, data] of this.store.entries()) {
|
await this.prisma.refreshToken.deleteMany({
|
||||||
if (data.expiresAt < now) this.store.delete(token);
|
where: { expiresAt: { lt: now } },
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
set(token: string, userId: number, expiresAt: number): void {
|
async set(token: string, userId: number, expiresAt: number): Promise<void> {
|
||||||
this.store.set(token, { userId, expiresAt });
|
await this.prisma.refreshToken.create({
|
||||||
|
data: { token, userId, expiresAt: new Date(expiresAt) },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get(token: string): { userId: number } | null {
|
async get(token: string): Promise<{ userId: number } | null> {
|
||||||
const data = this.store.get(token);
|
const row = await this.prisma.refreshToken.findUnique({
|
||||||
if (!data || data.expiresAt < Date.now()) return null;
|
where: { token },
|
||||||
return { userId: data.userId };
|
});
|
||||||
|
if (!row || row.expiresAt.getTime() < Date.now()) return null;
|
||||||
|
return { userId: row.userId };
|
||||||
}
|
}
|
||||||
|
|
||||||
remove(token: string): void {
|
async remove(token: string): Promise<void> {
|
||||||
this.store.delete(token);
|
await this.prisma.refreshToken.deleteMany({ where: { token } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import {
|
import {
|
||||||
CODE_TTL_SEC,
|
CODE_TTL_SEC,
|
||||||
MAX_VERIFY_ATTEMPTS,
|
MAX_VERIFY_ATTEMPTS,
|
||||||
|
|
@ -10,124 +11,134 @@ import {
|
||||||
VERIFY_BLOCK_DURATION_SEC,
|
VERIFY_BLOCK_DURATION_SEC,
|
||||||
} from './verification.constants';
|
} from './verification.constants';
|
||||||
|
|
||||||
interface StoredCode {
|
|
||||||
code: string;
|
|
||||||
createdAt: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
interface FailedAttempts {
|
|
||||||
count: number;
|
|
||||||
blockedUntil: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class VerificationStoreService {
|
export class VerificationStoreService {
|
||||||
private readonly codes = new Map<string, StoredCode>();
|
|
||||||
private readonly lastSentAt = new Map<string, number>();
|
|
||||||
private readonly sentByPhone: { at: number; phone: string }[] = [];
|
|
||||||
private readonly sentByIp: { at: number; ip: string }[] = [];
|
|
||||||
private readonly failedAttempts = new Map<string, FailedAttempts>();
|
|
||||||
private cleanupInterval: ReturnType<typeof setInterval> | null = null;
|
private cleanupInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor(private readonly prisma: PrismaService) {
|
||||||
this.startCleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
private startCleanup() {
|
|
||||||
if (this.cleanupInterval) return;
|
|
||||||
this.cleanupInterval = setInterval(() => this.cleanup(), 60_000);
|
this.cleanupInterval = setInterval(() => this.cleanup(), 60_000);
|
||||||
}
|
}
|
||||||
|
|
||||||
private cleanup() {
|
private async cleanup() {
|
||||||
const now = Date.now();
|
const now = new Date();
|
||||||
const borderPhone = now - SMS_PER_PHONE_WINDOW_SEC * 1000;
|
const codeBorder = new Date(now.getTime() - CODE_TTL_SEC * 1000);
|
||||||
const borderIp = now - SMS_PER_IP_WINDOW_SEC * 1000;
|
const blockBorder = new Date(now.getTime() - VERIFY_BLOCK_DURATION_SEC * 1000);
|
||||||
while (this.sentByPhone.length && this.sentByPhone[0].at < borderPhone) this.sentByPhone.shift();
|
await this.prisma.verificationCode.deleteMany({
|
||||||
while (this.sentByIp.length && this.sentByIp[0].at < borderIp) this.sentByIp.shift();
|
where: { createdAt: { lt: codeBorder } },
|
||||||
|
});
|
||||||
for (const [phone, data] of this.codes.entries()) {
|
await this.prisma.smsSendLog.deleteMany({
|
||||||
if (data.createdAt + CODE_TTL_SEC * 1000 < now) this.codes.delete(phone);
|
where: { sentAt: { lt: new Date(now.getTime() - Math.max(SMS_PER_PHONE_WINDOW_SEC, SMS_PER_IP_WINDOW_SEC) * 1000) } },
|
||||||
}
|
});
|
||||||
|
await this.prisma.verifyBlock.deleteMany({
|
||||||
for (const [key, data] of this.failedAttempts.entries()) {
|
where: { blockedUntil: { lt: now } },
|
||||||
if (data.blockedUntil < now) this.failedAttempts.delete(key);
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Можно ли отправить SMS на номер (60 сек с последней отправки). */
|
/** Можно ли отправить SMS на номер (60 сек с последней отправки). */
|
||||||
canSendToPhone(phone: string): boolean {
|
async canSendToPhone(phone: string): Promise<boolean> {
|
||||||
const last = this.lastSentAt.get(phone);
|
const last = await this.prisma.smsSendLog.findFirst({
|
||||||
|
where: { phone },
|
||||||
|
orderBy: { sentAt: 'desc' },
|
||||||
|
select: { sentAt: true },
|
||||||
|
});
|
||||||
if (!last) return true;
|
if (!last) return true;
|
||||||
return Date.now() - last >= SMS_MIN_INTERVAL_SEC * 1000;
|
return Date.now() - last.sentAt.getTime() >= SMS_MIN_INTERVAL_SEC * 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Количество отправок на номер за последние 15 мин. */
|
/** Количество отправок на номер за последние 15 мин. */
|
||||||
getSendCountByPhone(phone: string): number {
|
async getSendCountByPhone(phone: string): Promise<number> {
|
||||||
const border = Date.now() - SMS_PER_PHONE_WINDOW_SEC * 1000;
|
const border = new Date(Date.now() - SMS_PER_PHONE_WINDOW_SEC * 1000);
|
||||||
return this.sentByPhone.filter((r) => r.at >= border && r.phone === phone).length;
|
return this.prisma.smsSendLog.count({
|
||||||
|
where: { phone, sentAt: { gte: border } },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Количество отправок с IP за последние 15 мин. */
|
/** Количество отправок с IP за последние 15 мин. */
|
||||||
getSendCountByIp(ip: string): number {
|
async getSendCountByIp(ip: string): Promise<number> {
|
||||||
const border = Date.now() - SMS_PER_IP_WINDOW_SEC * 1000;
|
const border = new Date(Date.now() - SMS_PER_IP_WINDOW_SEC * 1000);
|
||||||
return this.sentByIp.filter((r) => r.at >= border && r.ip === ip).length;
|
return this.prisma.smsSendLog.count({
|
||||||
|
where: { ip, sentAt: { gte: border } },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Зарегистрировать отправку (инвалидировать старый код, записать лимиты). */
|
/** Зарегистрировать отправку (инвалидировать старый код, записать лимиты). */
|
||||||
recordSend(phone: string, ip: string, code: string): void {
|
async recordSend(phone: string, ip: string, code: string): Promise<void> {
|
||||||
const now = Date.now();
|
await this.prisma.$transaction([
|
||||||
this.codes.set(phone, { code, createdAt: now });
|
this.prisma.verificationCode.deleteMany({ where: { phone } }),
|
||||||
this.lastSentAt.set(phone, now);
|
this.prisma.verificationCode.create({
|
||||||
this.sentByPhone.push({ at: now, phone });
|
data: { phone, code },
|
||||||
this.sentByIp.push({ at: now, ip });
|
}),
|
||||||
this.cleanup();
|
this.prisma.smsSendLog.create({
|
||||||
|
data: { phone, ip },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Результат проверки кода. */
|
/** Результат проверки кода. */
|
||||||
checkCode(
|
async checkCode(
|
||||||
phone: string,
|
phone: string,
|
||||||
ip: string,
|
ip: string,
|
||||||
code: string,
|
code: string,
|
||||||
): { ok: true } | { ok: false; reason: 'expired' } | { ok: false; reason: 'wrong'; remainingAttempts: number } {
|
): Promise<
|
||||||
const key = `${phone}:${ip}`;
|
| { ok: true }
|
||||||
const blocked = this.failedAttempts.get(key);
|
| { ok: false; reason: 'expired' }
|
||||||
if (blocked && blocked.blockedUntil > Date.now()) return { ok: false, reason: 'expired' };
|
| { ok: false; reason: 'wrong'; remainingAttempts: number }
|
||||||
|
> {
|
||||||
|
const key = { phone, ip };
|
||||||
|
const blocked = await this.prisma.verifyBlock.findUnique({
|
||||||
|
where: { phone_ip: key },
|
||||||
|
});
|
||||||
|
if (blocked && blocked.blockedUntil.getTime() > Date.now()) {
|
||||||
|
return { ok: false, reason: 'expired' };
|
||||||
|
}
|
||||||
|
|
||||||
const stored = this.codes.get(phone);
|
const stored = await this.prisma.verificationCode.findFirst({
|
||||||
|
where: { phone },
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
});
|
||||||
if (!stored) return { ok: false, reason: 'expired' };
|
if (!stored) return { ok: false, reason: 'expired' };
|
||||||
if (stored.createdAt + CODE_TTL_SEC * 1000 < Date.now()) {
|
if (stored.createdAt.getTime() + CODE_TTL_SEC * 1000 < Date.now()) {
|
||||||
this.codes.delete(phone);
|
await this.prisma.verificationCode.deleteMany({ where: { phone } });
|
||||||
return { ok: false, reason: 'expired' };
|
return { ok: false, reason: 'expired' };
|
||||||
}
|
}
|
||||||
if (stored.code !== code) {
|
if (stored.code !== code) {
|
||||||
const current = this.failedAttempts.get(key) ?? { count: 0, blockedUntil: 0 };
|
const current = await this.prisma.verifyBlock.findUnique({
|
||||||
current.count += 1;
|
where: { phone_ip: key },
|
||||||
if (current.count >= MAX_VERIFY_ATTEMPTS) {
|
});
|
||||||
current.blockedUntil = Date.now() + VERIFY_BLOCK_DURATION_SEC * 1000;
|
const count = (current?.count ?? 0) + 1;
|
||||||
}
|
const blockedUntil =
|
||||||
this.failedAttempts.set(key, current);
|
count >= MAX_VERIFY_ATTEMPTS
|
||||||
const remainingAttempts = Math.max(0, MAX_VERIFY_ATTEMPTS - current.count);
|
? new Date(Date.now() + VERIFY_BLOCK_DURATION_SEC * 1000)
|
||||||
|
: current?.blockedUntil ?? new Date(0);
|
||||||
|
|
||||||
|
await this.prisma.verifyBlock.upsert({
|
||||||
|
where: { phone_ip: key },
|
||||||
|
create: { phone, ip, count, blockedUntil },
|
||||||
|
update: { count, blockedUntil },
|
||||||
|
});
|
||||||
|
const remainingAttempts = Math.max(0, MAX_VERIFY_ATTEMPTS - count);
|
||||||
return { ok: false, reason: 'wrong', remainingAttempts };
|
return { ok: false, reason: 'wrong', remainingAttempts };
|
||||||
}
|
}
|
||||||
this.codes.delete(phone);
|
await this.prisma.verificationCode.deleteMany({ where: { phone } });
|
||||||
this.failedAttempts.delete(key);
|
await this.prisma.verifyBlock.deleteMany({ where: { phone, ip } });
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Заблокирован ли ввод кода по phone+ip. */
|
/** Заблокирован ли ввод кода по phone+ip. */
|
||||||
isVerifyBlocked(phone: string, ip: string): boolean {
|
async isVerifyBlocked(phone: string, ip: string): Promise<boolean> {
|
||||||
const key = `${phone}:${ip}`;
|
const block = await this.prisma.verifyBlock.findUnique({
|
||||||
const data = this.failedAttempts.get(key);
|
where: { phone_ip: { phone, ip } },
|
||||||
if (!data) return false;
|
});
|
||||||
return data.blockedUntil > Date.now();
|
if (!block) return false;
|
||||||
|
return block.blockedUntil.getTime() > Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Оставшееся время блокировки в секундах (0 если не заблокирован). */
|
/** Оставшееся время блокировки в секундах (0 если не заблокирован). */
|
||||||
getBlockedRemainingSec(phone: string, ip: string): number {
|
async getBlockedRemainingSec(phone: string, ip: string): Promise<number> {
|
||||||
const key = `${phone}:${ip}`;
|
const block = await this.prisma.verifyBlock.findUnique({
|
||||||
const data = this.failedAttempts.get(key);
|
where: { phone_ip: { phone, ip } },
|
||||||
if (!data || data.blockedUntil <= Date.now()) return 0;
|
});
|
||||||
return Math.ceil((data.blockedUntil - Date.now()) / 1000);
|
if (!block || block.blockedUntil.getTime() <= Date.now()) return 0;
|
||||||
|
return Math.ceil((block.blockedUntil.getTime() - Date.now()) / 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
import { Module, forwardRef } from '@nestjs/common';
|
import { Module, forwardRef } from '@nestjs/common';
|
||||||
import { CatalogService } from './catalog.service';
|
import { CatalogService } from './catalog.service';
|
||||||
import { CatalogController } from './catalog.controller';
|
import { CatalogController } from './catalog.controller';
|
||||||
import { UsersModule } from '../users/users.module';
|
|
||||||
import { ReservationsModule } from '../reservations/reservations.module';
|
import { ReservationsModule } from '../reservations/reservations.module';
|
||||||
import { ReviewsModule } from '../reviews/reviews.module';
|
import { ReviewsModule } from '../reviews/reviews.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
UsersModule, // This provides UsersService
|
forwardRef(() => ReservationsModule),
|
||||||
forwardRef(() => ReservationsModule), // This provides ReservationsService
|
ReviewsModule,
|
||||||
ReviewsModule, // This provides ReviewsService
|
|
||||||
],
|
],
|
||||||
controllers: [CatalogController],
|
controllers: [CatalogController],
|
||||||
providers: [CatalogService],
|
providers: [CatalogService],
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
import { Injectable, Inject, forwardRef } from '@nestjs/common';
|
import { Injectable, Inject, forwardRef } from '@nestjs/common';
|
||||||
import { UsersService } from '../users/users.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { ReservationsService } from '../reservations/reservations.service';
|
import { ReservationsService } from '../reservations/reservations.service';
|
||||||
import { ReviewsService } from '../reviews/reviews.service';
|
import { ReviewsService } from '../reviews/reviews.service';
|
||||||
import {
|
import {
|
||||||
CatalogItemShortDto,
|
CatalogItemShortDto,
|
||||||
CatalogItemLongDto,
|
CatalogItemLongDto,
|
||||||
} from './dto/catalog-item.dto';
|
} from './dto/catalog-item.dto';
|
||||||
import { CatalogParamsDto } from './dto/catalog-params.dto';
|
|
||||||
import { CatalogResponseDto } from './dto/catalog-response.dto';
|
import { CatalogResponseDto } from './dto/catalog-response.dto';
|
||||||
import { MainPageCatalogResponseDto } from './dto/main-page-catalog-response.dto';
|
import { MainPageCatalogResponseDto } from './dto/main-page-catalog-response.dto';
|
||||||
import { CatalogFiltersDto } from './dto/catalog-filters.dto';
|
import { CatalogFiltersDto } from './dto/catalog-filters.dto';
|
||||||
|
|
@ -15,425 +14,74 @@ import { CreateYachtDto } from './dto/create-yacht.dto';
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CatalogService {
|
export class CatalogService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly usersService: UsersService,
|
private readonly prisma: PrismaService,
|
||||||
@Inject(forwardRef(() => ReservationsService))
|
@Inject(forwardRef(() => ReservationsService))
|
||||||
private readonly reservationsService: ReservationsService,
|
private readonly reservationsService: ReservationsService,
|
||||||
private readonly reviewsService: ReviewsService,
|
private readonly reviewsService: ReviewsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private catalogItems: CatalogItemLongDto[] = [
|
private yachtRowToLongDto(row: {
|
||||||
{
|
id: number;
|
||||||
id: 1,
|
name: string;
|
||||||
name: 'Азимут 55',
|
length: number;
|
||||||
length: 16.7,
|
speed: number | null;
|
||||||
speed: 32,
|
minCost: number | null;
|
||||||
minCost: 85000,
|
mainImageUrl: string | null;
|
||||||
mainImageUrl: 'uploads/1st.jpg',
|
galleryUrls: unknown;
|
||||||
galleryUrls: [
|
hasQuickRent: boolean;
|
||||||
'uploads/gal1.jpg',
|
isFeatured: boolean;
|
||||||
'uploads/gal2.jpg',
|
topText: string | null;
|
||||||
'uploads/gal3.jpg',
|
year: number;
|
||||||
'uploads/gal4.jpg',
|
comfortCapacity: number | null;
|
||||||
'uploads/gal5.jpg',
|
maxCapacity: number | null;
|
||||||
'uploads/gal6.jpg',
|
width: number | null;
|
||||||
'uploads/gal7.jpg',
|
cabinsCount: number | null;
|
||||||
'uploads/gal8.jpg',
|
matherial: string | null;
|
||||||
'uploads/gal9.jpg',
|
power: number | null;
|
||||||
'uploads/gal10.jpg',
|
description: string | null;
|
||||||
],
|
userId: number;
|
||||||
hasQuickRent: true,
|
owner?: { id: number; firstName: string; lastName: string; phone: string; email: string; companyName: string | null; inn: bigint | null; ogrn: bigint | null };
|
||||||
isFeatured: true,
|
}): CatalogItemLongDto {
|
||||||
year: 2022,
|
const galleryUrls = Array.isArray(row.galleryUrls)
|
||||||
comfortCapacity: 8,
|
? row.galleryUrls
|
||||||
maxCapacity: 12,
|
: typeof row.galleryUrls === 'string'
|
||||||
width: 4.8,
|
? (JSON.parse(row.galleryUrls) as string[])
|
||||||
cabinsCount: 3,
|
: [];
|
||||||
matherial: 'Стеклопластик',
|
return {
|
||||||
power: 1200,
|
id: row.id,
|
||||||
description:
|
name: row.name,
|
||||||
'Роскошная моторная яхта Азимут 55 - это воплощение итальянского стиля и российского качества. Идеально подходит для прогулок по Финскому заливу, корпоративных мероприятий и романтических свиданий. На борту: три комфортабельные каюты, просторный салон с панорамным остеклением, полностью оборудованная кухня и две ванные комнаты. Максимальная скорость 32 узла позволяет быстро добраться до самых живописных мест Карельского перешейка.',
|
length: row.length,
|
||||||
owner: { userId: 1 } as any,
|
speed: row.speed ?? 0,
|
||||||
|
minCost: row.minCost ?? 0,
|
||||||
|
mainImageUrl: row.mainImageUrl ?? '',
|
||||||
|
galleryUrls: galleryUrls as string[],
|
||||||
|
hasQuickRent: row.hasQuickRent,
|
||||||
|
isFeatured: row.isFeatured,
|
||||||
|
topText: row.topText ?? undefined,
|
||||||
|
year: row.year,
|
||||||
|
comfortCapacity: row.comfortCapacity ?? 0,
|
||||||
|
maxCapacity: row.maxCapacity ?? 0,
|
||||||
|
width: row.width ?? 0,
|
||||||
|
cabinsCount: row.cabinsCount ?? 0,
|
||||||
|
matherial: row.matherial ?? '',
|
||||||
|
power: row.power ?? 0,
|
||||||
|
description: row.description ?? '',
|
||||||
|
owner: row.owner
|
||||||
|
? {
|
||||||
|
userId: row.owner.id,
|
||||||
|
firstName: row.owner.firstName,
|
||||||
|
lastName: row.owner.lastName,
|
||||||
|
phone: row.owner.phone,
|
||||||
|
email: row.owner.email,
|
||||||
|
companyName: row.owner.companyName ?? undefined,
|
||||||
|
inn: row.owner.inn != null ? Number(row.owner.inn) : undefined,
|
||||||
|
ogrn: row.owner.ogrn != null ? Number(row.owner.ogrn) : undefined,
|
||||||
|
}
|
||||||
|
: ({ userId: row.userId } as any),
|
||||||
reviews: [],
|
reviews: [],
|
||||||
reservations: [],
|
reservations: [],
|
||||||
},
|
};
|
||||||
{
|
}
|
||||||
id: 2,
|
|
||||||
name: 'Сансикер Манхэттен 52',
|
|
||||||
length: 15.8,
|
|
||||||
speed: 34,
|
|
||||||
minCost: 92000,
|
|
||||||
mainImageUrl: 'uploads/2nd.jpg',
|
|
||||||
galleryUrls: [
|
|
||||||
'uploads/gal1.jpg',
|
|
||||||
'uploads/gal2.jpg',
|
|
||||||
'uploads/gal3.jpg',
|
|
||||||
'uploads/gal4.jpg',
|
|
||||||
'uploads/gal5.jpg',
|
|
||||||
'uploads/gal6.jpg',
|
|
||||||
'uploads/gal7.jpg',
|
|
||||||
'uploads/gal8.jpg',
|
|
||||||
'uploads/gal9.jpg',
|
|
||||||
'uploads/gal10.jpg',
|
|
||||||
],
|
|
||||||
hasQuickRent: false,
|
|
||||||
isFeatured: false,
|
|
||||||
topText: '🔥 Лучшее предложение',
|
|
||||||
year: 2023,
|
|
||||||
comfortCapacity: 6,
|
|
||||||
maxCapacity: 10,
|
|
||||||
width: 4.5,
|
|
||||||
cabinsCount: 3,
|
|
||||||
matherial: 'Стеклопластик',
|
|
||||||
power: 1400,
|
|
||||||
description:
|
|
||||||
'Британский шик и русская душа в одной яхте! Сансикер Манхэттен 52 - выбор настоящих ценителей морских путешествий. Просторный кокпит с мягкими диванами, бар на 8 персон, система мультимедиа премиум-класса. Идеальна для празднования дня рождения на воде или деловой встречи с партнерами. Отличная маневренность позволяет заходить в марины Санкт-Петербурга и Кронштадта.',
|
|
||||||
owner: { userId: 2 } as any,
|
|
||||||
reviews: [],
|
|
||||||
reservations: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: 'Принцесс V55',
|
|
||||||
length: 16.7,
|
|
||||||
speed: 33,
|
|
||||||
minCost: 78000,
|
|
||||||
mainImageUrl: 'uploads/3rd.jpg',
|
|
||||||
galleryUrls: [
|
|
||||||
'uploads/gal1.jpg',
|
|
||||||
'uploads/gal2.jpg',
|
|
||||||
'uploads/gal3.jpg',
|
|
||||||
'uploads/gal4.jpg',
|
|
||||||
'uploads/gal5.jpg',
|
|
||||||
'uploads/gal6.jpg',
|
|
||||||
'uploads/gal7.jpg',
|
|
||||||
'uploads/gal8.jpg',
|
|
||||||
'uploads/gal9.jpg',
|
|
||||||
'uploads/gal10.jpg',
|
|
||||||
],
|
|
||||||
hasQuickRent: true,
|
|
||||||
isFeatured: false,
|
|
||||||
topText: '🍷 Идеальна для заката с бокалом вина',
|
|
||||||
year: 2021,
|
|
||||||
comfortCapacity: 8,
|
|
||||||
maxCapacity: 12,
|
|
||||||
width: 4.7,
|
|
||||||
cabinsCount: 4,
|
|
||||||
matherial: 'Стеклопластик',
|
|
||||||
power: 1100,
|
|
||||||
description:
|
|
||||||
'Принцесс V55 - королева российских вод! Эта яхта создана для тех, кто ценит комфорт и элегантность. Четыре уютные каюты с кондиционером, гальюн с душем, полностью оборудованная камбузная зона. Особенность - огромный платц с гидравлическим трапом для купания в Ладожском озере. Отличный выбор для семейного отдыха или рыбалки с друзьями.',
|
|
||||||
owner: { userId: 1 } as any,
|
|
||||||
reviews: [],
|
|
||||||
reservations: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: 'Ферретти 500',
|
|
||||||
length: 15.2,
|
|
||||||
speed: 31,
|
|
||||||
minCost: 68000,
|
|
||||||
mainImageUrl: 'uploads/4th.jpg',
|
|
||||||
galleryUrls: [
|
|
||||||
'uploads/gal1.jpg',
|
|
||||||
'uploads/gal2.jpg',
|
|
||||||
'uploads/gal3.jpg',
|
|
||||||
'uploads/gal4.jpg',
|
|
||||||
'uploads/gal5.jpg',
|
|
||||||
'uploads/gal6.jpg',
|
|
||||||
'uploads/gal7.jpg',
|
|
||||||
'uploads/gal8.jpg',
|
|
||||||
'uploads/gal9.jpg',
|
|
||||||
'uploads/gal10.jpg',
|
|
||||||
],
|
|
||||||
hasQuickRent: true,
|
|
||||||
isFeatured: false,
|
|
||||||
topText: '⏳ Часто бронируется - успей',
|
|
||||||
year: 2020,
|
|
||||||
comfortCapacity: 6,
|
|
||||||
maxCapacity: 8,
|
|
||||||
width: 4.3,
|
|
||||||
cabinsCount: 3,
|
|
||||||
matherial: 'Стеклопластик',
|
|
||||||
power: 900,
|
|
||||||
description:
|
|
||||||
'Итальянская страсть в русской стихии! Ферретти 500 сочетает в себе средиземноморский шарм и надежность для суровых условий Балтики. Просторный салон с панорамными окнами, обеденная зона на 6 человек, современная навигационная система. Идеально подходит для фотосессий на фоне разводных мостов Петербурга или романтического ужина под звуки волн.',
|
|
||||||
owner: { userId: 2 } as any,
|
|
||||||
reviews: [],
|
|
||||||
reservations: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
name: 'Си Рей 510 Сандансер',
|
|
||||||
length: 15.5,
|
|
||||||
speed: 35,
|
|
||||||
minCost: 72000,
|
|
||||||
mainImageUrl: 'uploads/5th.jpg',
|
|
||||||
galleryUrls: [
|
|
||||||
'uploads/gal1.jpg',
|
|
||||||
'uploads/gal2.jpg',
|
|
||||||
'uploads/gal3.jpg',
|
|
||||||
'uploads/gal4.jpg',
|
|
||||||
'uploads/gal5.jpg',
|
|
||||||
'uploads/gal6.jpg',
|
|
||||||
'uploads/gal7.jpg',
|
|
||||||
'uploads/gal8.jpg',
|
|
||||||
'uploads/gal9.jpg',
|
|
||||||
'uploads/gal10.jpg',
|
|
||||||
],
|
|
||||||
hasQuickRent: false,
|
|
||||||
isFeatured: false,
|
|
||||||
year: 2023,
|
|
||||||
comfortCapacity: 8,
|
|
||||||
maxCapacity: 10,
|
|
||||||
width: 4.6,
|
|
||||||
cabinsCount: 3,
|
|
||||||
matherial: 'Стеклопластик',
|
|
||||||
power: 1300,
|
|
||||||
description:
|
|
||||||
'Американская мощь для русского моря! Си Рей 510 Сандансер - самая быстрая яхта в нашем флоте. Развивает скорость до 35 узлов, что позволяет за день обогнуть весь Финский залив. Три комфортабельные каюты, система стабилизации на стоянке, мощная аудиосистема с сабвуфером. Отличный выбор для любителей острых ощущений и скоростных прогулок.',
|
|
||||||
owner: { userId: 1 } as any,
|
|
||||||
reviews: [],
|
|
||||||
reservations: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
name: 'Бавария SR41',
|
|
||||||
length: 12.5,
|
|
||||||
speed: 28,
|
|
||||||
minCost: 45000,
|
|
||||||
mainImageUrl: 'uploads/6th.jpg',
|
|
||||||
galleryUrls: [
|
|
||||||
'uploads/gal1.jpg',
|
|
||||||
'uploads/gal2.jpg',
|
|
||||||
'uploads/gal3.jpg',
|
|
||||||
'uploads/gal4.jpg',
|
|
||||||
'uploads/gal5.jpg',
|
|
||||||
'uploads/gal6.jpg',
|
|
||||||
'uploads/gal7.jpg',
|
|
||||||
'uploads/gal8.jpg',
|
|
||||||
'uploads/gal9.jpg',
|
|
||||||
'uploads/gal10.jpg',
|
|
||||||
],
|
|
||||||
hasQuickRent: true,
|
|
||||||
isFeatured: false,
|
|
||||||
year: 2019,
|
|
||||||
comfortCapacity: 6,
|
|
||||||
maxCapacity: 8,
|
|
||||||
width: 3.9,
|
|
||||||
cabinsCount: 2,
|
|
||||||
matherial: 'Стеклопластик',
|
|
||||||
power: 320,
|
|
||||||
description:
|
|
||||||
'Немецкое качество для русского характера! Бавария SR41 - надежная и экономичная яхта для спокойных прогулок по Ладоге. Две уютные каюты, просторный кокпит с тентом от дождя, лебедка для подъема парусов. Идеальный выбор для начинающих яхтсменов или семейного отдыха с детьми. Расход топлива всего 15 литров в час!',
|
|
||||||
owner: { userId: 2 } as any,
|
|
||||||
reviews: [],
|
|
||||||
reservations: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 7,
|
|
||||||
name: 'Жанно Мери Фишер 895',
|
|
||||||
length: 8.9,
|
|
||||||
speed: 25,
|
|
||||||
minCost: 32000,
|
|
||||||
mainImageUrl: 'uploads/1st.jpg',
|
|
||||||
galleryUrls: [
|
|
||||||
'uploads/gal1.jpg',
|
|
||||||
'uploads/gal2.jpg',
|
|
||||||
'uploads/gal3.jpg',
|
|
||||||
'uploads/gal4.jpg',
|
|
||||||
'uploads/gal5.jpg',
|
|
||||||
'uploads/gal6.jpg',
|
|
||||||
'uploads/gal7.jpg',
|
|
||||||
'uploads/gal8.jpg',
|
|
||||||
'uploads/gal9.jpg',
|
|
||||||
'uploads/gal10.jpg',
|
|
||||||
],
|
|
||||||
hasQuickRent: true,
|
|
||||||
isFeatured: false,
|
|
||||||
year: 2022,
|
|
||||||
comfortCapacity: 4,
|
|
||||||
maxCapacity: 6,
|
|
||||||
width: 3.0,
|
|
||||||
cabinsCount: 1,
|
|
||||||
matherial: 'Стеклопластик',
|
|
||||||
power: 250,
|
|
||||||
description:
|
|
||||||
'Французская элегантность для русского простора! Жанно Мери Фишер 895 - компактная, но вместительная яхта для рыбалки и пикников. Одна просторная каюта, открытый кокпит, столик для барбекю. Отлично подходит для выездов на природу, ночевки в бухтах или обучения детей управлению яхтой. Самый экономичный вариант в нашем флоте!',
|
|
||||||
owner: { userId: 1 } as any,
|
|
||||||
reviews: [],
|
|
||||||
reservations: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 8,
|
|
||||||
name: 'Бенето Свифт Троулер 41',
|
|
||||||
length: 12.5,
|
|
||||||
speed: 22,
|
|
||||||
minCost: 55000,
|
|
||||||
mainImageUrl: 'uploads/2nd.jpg',
|
|
||||||
galleryUrls: [
|
|
||||||
'uploads/gal1.jpg',
|
|
||||||
'uploads/gal2.jpg',
|
|
||||||
'uploads/gal3.jpg',
|
|
||||||
'uploads/gal4.jpg',
|
|
||||||
'uploads/gal5.jpg',
|
|
||||||
'uploads/gal6.jpg',
|
|
||||||
'uploads/gal7.jpg',
|
|
||||||
'uploads/gal8.jpg',
|
|
||||||
'uploads/gal9.jpg',
|
|
||||||
'uploads/gal10.jpg',
|
|
||||||
],
|
|
||||||
hasQuickRent: false,
|
|
||||||
isFeatured: false,
|
|
||||||
year: 2021,
|
|
||||||
comfortCapacity: 6,
|
|
||||||
maxCapacity: 8,
|
|
||||||
width: 4.2,
|
|
||||||
cabinsCount: 2,
|
|
||||||
matherial: 'Стеклопластик',
|
|
||||||
power: 425,
|
|
||||||
description:
|
|
||||||
'Французский траулер для русского севера! Бенето Свифт Троулер 41 создан для длительных путешествий по Белому морю. Экономичный дизельный двигатель, большой запас топлива, система опреснения воды. Две комфортабельные каюты с подогревом пола. Идеальный выбор для экспедиций или многодневных круизов по северным островам.',
|
|
||||||
owner: { userId: 2 } as any,
|
|
||||||
reviews: [],
|
|
||||||
reservations: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 9,
|
|
||||||
name: 'Лагун 450',
|
|
||||||
length: 13.5,
|
|
||||||
speed: 20,
|
|
||||||
minCost: 65000,
|
|
||||||
mainImageUrl: 'uploads/3rd.jpg',
|
|
||||||
galleryUrls: [
|
|
||||||
'uploads/gal1.jpg',
|
|
||||||
'uploads/gal2.jpg',
|
|
||||||
'uploads/gal3.jpg',
|
|
||||||
'uploads/gal4.jpg',
|
|
||||||
'uploads/gal5.jpg',
|
|
||||||
'uploads/gal6.jpg',
|
|
||||||
'uploads/gal7.jpg',
|
|
||||||
'uploads/gal8.jpg',
|
|
||||||
'uploads/gal9.jpg',
|
|
||||||
'uploads/gal10.jpg',
|
|
||||||
],
|
|
||||||
hasQuickRent: true,
|
|
||||||
isFeatured: false,
|
|
||||||
year: 2020,
|
|
||||||
comfortCapacity: 8,
|
|
||||||
maxCapacity: 10,
|
|
||||||
width: 7.8,
|
|
||||||
cabinsCount: 4,
|
|
||||||
matherial: 'Стеклопластик',
|
|
||||||
power: 90,
|
|
||||||
description:
|
|
||||||
'Французский катамаран для русского размаха! Лагун 450 - невероятно устойчивая и просторная яхта. Четыре отдельные каюты с санузлами, огромный салон-трансформер, две кухни. Идеально подходит для больших компаний, свадебных церемоний на воде или длительных круизов всей семьей. Не кренится даже в шторм!',
|
|
||||||
owner: { userId: 1 } as any,
|
|
||||||
reviews: [],
|
|
||||||
reservations: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 10,
|
|
||||||
name: 'Фонтен Пажо Люсия 40',
|
|
||||||
length: 11.7,
|
|
||||||
speed: 18,
|
|
||||||
minCost: 58000,
|
|
||||||
mainImageUrl: 'uploads/4th.jpg',
|
|
||||||
galleryUrls: [
|
|
||||||
'uploads/gal1.jpg',
|
|
||||||
'uploads/gal2.jpg',
|
|
||||||
'uploads/gal3.jpg',
|
|
||||||
'uploads/gal4.jpg',
|
|
||||||
'uploads/gal5.jpg',
|
|
||||||
'uploads/gal6.jpg',
|
|
||||||
'uploads/gal7.jpg',
|
|
||||||
'uploads/gal8.jpg',
|
|
||||||
'uploads/gal9.jpg',
|
|
||||||
'uploads/gal10.jpg',
|
|
||||||
],
|
|
||||||
hasQuickRent: true,
|
|
||||||
isFeatured: false,
|
|
||||||
year: 2023,
|
|
||||||
comfortCapacity: 8,
|
|
||||||
maxCapacity: 10,
|
|
||||||
width: 7.1,
|
|
||||||
cabinsCount: 4,
|
|
||||||
matherial: 'Стеклопластик',
|
|
||||||
power: 80,
|
|
||||||
description:
|
|
||||||
'Французский катамаран класса люкс! Фонтен Пажо Люсия 40 - это плавающий пятизвездочный отель. Четыре каюты-люкс с джакузи, салон с камином, профессиональная кухня с шеф-поваром. Система стабилизации на якоре, гидромассажный бассейн на палубе. Выбор настоящих ценителей роскоши и комфорта на воде.',
|
|
||||||
owner: { userId: 2 } as any,
|
|
||||||
reviews: [],
|
|
||||||
reservations: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 11,
|
|
||||||
name: 'Дюфур 460',
|
|
||||||
length: 14.1,
|
|
||||||
speed: 26,
|
|
||||||
minCost: 62000,
|
|
||||||
mainImageUrl: 'uploads/5th.jpg',
|
|
||||||
galleryUrls: [
|
|
||||||
'uploads/gal1.jpg',
|
|
||||||
'uploads/gal2.jpg',
|
|
||||||
'uploads/gal3.jpg',
|
|
||||||
'uploads/gal4.jpg',
|
|
||||||
'uploads/gal5.jpg',
|
|
||||||
'uploads/gal6.jpg',
|
|
||||||
'uploads/gal7.jpg',
|
|
||||||
'uploads/gal8.jpg',
|
|
||||||
'uploads/gal9.jpg',
|
|
||||||
'uploads/gal10.jpg',
|
|
||||||
],
|
|
||||||
hasQuickRent: false,
|
|
||||||
isFeatured: false,
|
|
||||||
year: 2022,
|
|
||||||
comfortCapacity: 8,
|
|
||||||
maxCapacity: 10,
|
|
||||||
width: 4.5,
|
|
||||||
cabinsCount: 3,
|
|
||||||
matherial: 'Стеклопластик',
|
|
||||||
power: 380,
|
|
||||||
description:
|
|
||||||
'Французская парусная яхта для русского ветра! Дюфур 460 - мечта любого яхтсмена. Три просторные каюты, кокпит с мягкими сиденьями, современное парусное вооружение. Идеально сбалансированная, легко управляется даже новичками. Отличный выбор для регат, обучения парусному спорту или романтических круизов под парусами.',
|
|
||||||
owner: { userId: 1 } as any,
|
|
||||||
reviews: [],
|
|
||||||
reservations: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 12,
|
|
||||||
name: 'Гранд Бэнкс 60',
|
|
||||||
length: 18.3,
|
|
||||||
speed: 24,
|
|
||||||
minCost: 125000,
|
|
||||||
mainImageUrl: 'uploads/6th.jpg',
|
|
||||||
galleryUrls: [
|
|
||||||
'uploads/gal1.jpg',
|
|
||||||
'uploads/gal2.jpg',
|
|
||||||
'uploads/gal3.jpg',
|
|
||||||
'uploads/gal4.jpg',
|
|
||||||
'uploads/gal5.jpg',
|
|
||||||
'uploads/gal6.jpg',
|
|
||||||
'uploads/gal7.jpg',
|
|
||||||
'uploads/gal8.jpg',
|
|
||||||
'uploads/gal9.jpg',
|
|
||||||
'uploads/gal10.jpg',
|
|
||||||
],
|
|
||||||
hasQuickRent: true,
|
|
||||||
isFeatured: false,
|
|
||||||
year: 2023,
|
|
||||||
comfortCapacity: 6,
|
|
||||||
maxCapacity: 8,
|
|
||||||
width: 5.2,
|
|
||||||
cabinsCount: 3,
|
|
||||||
matherial: 'Стеклопластик',
|
|
||||||
power: 1600,
|
|
||||||
description:
|
|
||||||
'Американская легенда для русского океана! Гранд Бэнкс 60 - экспедиционная яхта для самых смелых путешествий. Три каюты-люкс, салон с библиотекой, зимний сад, сауна. Автономность плавания - 30 дней! Способна пересечь Баренцево море и дойти до Шпицбергена. Выбор для настоящих морских волков и исследователей Арктики.',
|
|
||||||
owner: { userId: 2 } as any,
|
|
||||||
reviews: [],
|
|
||||||
reservations: [],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
private toShortDto(item: CatalogItemLongDto): CatalogItemShortDto {
|
private toShortDto(item: CatalogItemLongDto): CatalogItemShortDto {
|
||||||
const {
|
const {
|
||||||
|
|
@ -463,148 +111,131 @@ export class CatalogService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCatalogItemById(id: number): Promise<CatalogItemLongDto | null> {
|
async getCatalogItemById(id: number): Promise<CatalogItemLongDto | null> {
|
||||||
const item = this.catalogItems.find((item) => item.id === id);
|
const yacht = await this.prisma.yacht.findUnique({
|
||||||
if (!item) return null;
|
where: { id },
|
||||||
|
include: { owner: true },
|
||||||
const ownerId = [1, 2][(id - 1) % 2];
|
});
|
||||||
const owner = await this.usersService.findById(ownerId);
|
if (!yacht) return null;
|
||||||
|
|
||||||
if (owner) {
|
|
||||||
item.owner = owner;
|
|
||||||
}
|
|
||||||
|
|
||||||
item.reviews = this.reviewsService.getReviewsByYachtId(id);
|
|
||||||
item.reservations = this.reservationsService.getReservationsByYachtId(id);
|
|
||||||
|
|
||||||
|
const item = this.yachtRowToLongDto(yacht);
|
||||||
|
item.reviews = await this.reviewsService.getReviewsByYachtId(id);
|
||||||
|
item.reservations = await this.reservationsService.getReservationsByYachtId(id);
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMainPageCatalog(): Promise<MainPageCatalogResponseDto | null> {
|
async getMainPageCatalog(): Promise<MainPageCatalogResponseDto | null> {
|
||||||
const clonedCatalog = [...this.catalogItems];
|
const allYachts = await this.prisma.yacht.findMany({
|
||||||
const filteredCatalog = clonedCatalog.filter(
|
include: { owner: true },
|
||||||
({ isFeatured }) => !isFeatured,
|
orderBy: { id: 'asc' },
|
||||||
);
|
});
|
||||||
|
|
||||||
const featuredYachtIndex = clonedCatalog.findIndex(
|
const fullItems = allYachts.map((row) => this.yachtRowToLongDto(row));
|
||||||
(item) => item.isFeatured === true,
|
const featuredYacht = fullItems.find((item) => item.isFeatured);
|
||||||
);
|
const filteredCatalog = fullItems.filter((item) => !item.isFeatured);
|
||||||
|
|
||||||
|
if (!featuredYacht) return null;
|
||||||
|
|
||||||
if (featuredYachtIndex !== -1) {
|
|
||||||
const minCost = Math.min(
|
const minCost = Math.min(
|
||||||
...filteredCatalog.map((item) => item.minCost || Infinity),
|
...filteredCatalog.map((item) => item.minCost || Infinity),
|
||||||
);
|
);
|
||||||
|
|
||||||
const mappedRestYachts = filteredCatalog.slice(0, 6).map((item) => ({
|
const mappedRestYachts = filteredCatalog.slice(0, 6).map((item) => ({
|
||||||
...this.toShortDto(item),
|
...this.toShortDto(item),
|
||||||
isBestOffer: item.minCost === minCost,
|
isBestOffer: item.minCost === minCost,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
featuredYacht: this.toShortDto(clonedCatalog[featuredYachtIndex]),
|
featuredYacht: this.toShortDto(featuredYacht),
|
||||||
restYachts: mappedRestYachts,
|
restYachts: mappedRestYachts,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getCatalog(filters?: CatalogFiltersDto): Promise<CatalogResponseDto> {
|
async getCatalog(filters?: CatalogFiltersDto): Promise<CatalogResponseDto> {
|
||||||
// Start with all items converted to short DTO
|
const allYachts = await this.prisma.yacht.findMany({
|
||||||
let filteredItems = this.catalogItems.map((item) => this.toShortDto(item));
|
include: { owner: true },
|
||||||
|
orderBy: { id: 'asc' },
|
||||||
|
});
|
||||||
|
|
||||||
// Get the full items for filtering by properties not in short DTO
|
let filteredItems = allYachts.map((row) =>
|
||||||
const fullItems = this.catalogItems;
|
this.toShortDto(this.yachtRowToLongDto(row)),
|
||||||
|
);
|
||||||
|
const fullItems = allYachts.map((row) => this.yachtRowToLongDto(row));
|
||||||
|
|
||||||
// Search filter
|
|
||||||
if (filters?.search) {
|
if (filters?.search) {
|
||||||
const searchTerm = filters.search.toLowerCase();
|
const searchTerm = filters.search.toLowerCase();
|
||||||
filteredItems = filteredItems.filter((item) => {
|
filteredItems = filteredItems.filter((item) => {
|
||||||
const fullItem = fullItems.find((fi) => fi.id === item.id);
|
const fullItem = fullItems.find((fi) => fi.id === item.id);
|
||||||
return (
|
return (
|
||||||
item.name.toLowerCase().includes(searchTerm) ||
|
item.name.toLowerCase().includes(searchTerm) ||
|
||||||
(fullItem && fullItem.description.toLowerCase().includes(searchTerm))
|
(fullItem &&
|
||||||
|
fullItem.description.toLowerCase().includes(searchTerm))
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Length filter
|
|
||||||
if (filters?.minLength !== undefined) {
|
if (filters?.minLength !== undefined) {
|
||||||
filteredItems = filteredItems.filter(
|
filteredItems = filteredItems.filter(
|
||||||
(item) => item.length >= filters.minLength!,
|
(item) => item.length >= filters!.minLength!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (filters?.maxLength !== undefined) {
|
if (filters?.maxLength !== undefined) {
|
||||||
filteredItems = filteredItems.filter(
|
filteredItems = filteredItems.filter(
|
||||||
(item) => item.length <= filters.maxLength!,
|
(item) => item.length <= filters!.maxLength!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Price filter
|
|
||||||
if (filters?.minPrice !== undefined) {
|
if (filters?.minPrice !== undefined) {
|
||||||
filteredItems = filteredItems.filter(
|
filteredItems = filteredItems.filter(
|
||||||
(item) => item.minCost >= filters.minPrice!,
|
(item) => item.minCost >= filters!.minPrice!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (filters?.maxPrice !== undefined) {
|
if (filters?.maxPrice !== undefined) {
|
||||||
filteredItems = filteredItems.filter(
|
filteredItems = filteredItems.filter(
|
||||||
(item) => item.minCost <= filters.maxPrice!,
|
(item) => item.minCost <= filters!.maxPrice!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Year filter
|
|
||||||
if (filters?.minYear !== undefined || filters?.maxYear !== undefined) {
|
if (filters?.minYear !== undefined || filters?.maxYear !== undefined) {
|
||||||
filteredItems = filteredItems.filter((item) => {
|
filteredItems = filteredItems.filter((item) => {
|
||||||
const fullItem = fullItems.find((fi) => fi.id === item.id);
|
const fullItem = fullItems.find((fi) => fi.id === item.id);
|
||||||
if (!fullItem) return false;
|
if (!fullItem) return false;
|
||||||
|
if (
|
||||||
if (filters.minYear !== undefined && fullItem.year < filters.minYear)
|
filters!.minYear !== undefined &&
|
||||||
|
fullItem.year < filters!.minYear!
|
||||||
|
)
|
||||||
return false;
|
return false;
|
||||||
if (filters.maxYear !== undefined && fullItem.year > filters.maxYear)
|
if (
|
||||||
|
filters!.maxYear !== undefined &&
|
||||||
|
fullItem.year > filters!.maxYear!
|
||||||
|
)
|
||||||
return false;
|
return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filters?.guests !== undefined) {
|
if (filters?.guests !== undefined) {
|
||||||
const totalPeople = filters?.guests!;
|
const totalPeople = filters.guests;
|
||||||
filteredItems = filteredItems.filter((item) => {
|
filteredItems = filteredItems.filter((item) => {
|
||||||
const fullItem = fullItems.find((fi) => fi.id === item.id);
|
const fullItem = fullItems.find((fi) => fi.id === item.id);
|
||||||
return fullItem && fullItem.maxCapacity >= totalPeople;
|
return fullItem != null && fullItem.maxCapacity >= totalPeople;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Quick booking filter
|
|
||||||
if (filters?.quickBooking !== undefined) {
|
if (filters?.quickBooking !== undefined) {
|
||||||
filteredItems = filteredItems.filter(
|
filteredItems = filteredItems.filter(
|
||||||
(item) => item.hasQuickRent === filters.quickBooking,
|
(item) => item.hasQuickRent === filters.quickBooking,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Has toilet filter (check if cabinsCount > 0)
|
|
||||||
if (filters?.hasToilet !== undefined) {
|
if (filters?.hasToilet !== undefined) {
|
||||||
filteredItems = filteredItems.filter((item) => {
|
filteredItems = filteredItems.filter((item) => {
|
||||||
const fullItem = fullItems.find((fi) => fi.id === item.id);
|
const fullItem = fullItems.find((fi) => fi.id === item.id);
|
||||||
if (!fullItem) return false;
|
if (!fullItem) return false;
|
||||||
return filters.hasToilet
|
return filters!.hasToilet
|
||||||
? fullItem.cabinsCount > 0
|
? fullItem.cabinsCount > 0
|
||||||
: fullItem.cabinsCount === 0;
|
: fullItem.cabinsCount === 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Payment type filter (assuming all accept card payments)
|
|
||||||
if (filters?.paymentType) {
|
|
||||||
// For now, all yachts accept card payments
|
|
||||||
// You could add a paymentTypes array to CatalogItemLongDto if needed
|
|
||||||
console.log('Payment type filtering:', filters.paymentType);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Date/time filter (check reservations)
|
|
||||||
if (filters?.date || filters?.departureTime || filters?.arrivalTime) {
|
|
||||||
// This is more complex - would need to check reservations
|
|
||||||
// For now, we'll return all items
|
|
||||||
console.log('Date/time filtering available for future implementation');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filters?.sortByPrice === 'asc') {
|
if (filters?.sortByPrice === 'asc') {
|
||||||
filteredItems.sort((a, b) => a.minCost - b.minCost);
|
filteredItems.sort((a, b) => a.minCost - b.minCost);
|
||||||
} else if (filters?.sortByPrice === 'desc') {
|
} else if (filters?.sortByPrice === 'desc') {
|
||||||
|
|
@ -622,32 +253,40 @@ export class CatalogService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllCatalogItems(): Promise<CatalogItemShortDto[]> {
|
async getAllCatalogItems(): Promise<CatalogItemShortDto[]> {
|
||||||
return this.catalogItems.map((item) => this.toShortDto(item));
|
const allYachts = await this.prisma.yacht.findMany({
|
||||||
|
orderBy: { id: 'asc' },
|
||||||
|
});
|
||||||
|
return allYachts.map((row) =>
|
||||||
|
this.toShortDto(this.yachtRowToLongDto(row)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCatalogByUserId(userId: number): Promise<CatalogItemShortDto[]> {
|
async getCatalogByUserId(userId: number): Promise<CatalogItemShortDto[]> {
|
||||||
// Логика определения ownerId: нечетные id -> ownerId=1, четные id -> ownerId=2
|
const yachts = await this.prisma.yacht.findMany({
|
||||||
const filteredItems = this.catalogItems.filter((item) => {
|
where: { userId },
|
||||||
return item.owner?.userId === userId;
|
orderBy: { id: 'asc' },
|
||||||
});
|
});
|
||||||
return filteredItems.map((item) => this.toShortDto(item));
|
return yachts.map((row) =>
|
||||||
|
this.toShortDto(this.yachtRowToLongDto(row)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createYacht(createYachtDto: CreateYachtDto): Promise<CatalogItemLongDto> {
|
async createYacht(
|
||||||
const newId = Math.max(...this.catalogItems.map((item) => item.id || 0), 0) + 1;
|
createYachtDto: CreateYachtDto,
|
||||||
const owner = await this.usersService.findById(createYachtDto.userId);
|
): Promise<CatalogItemLongDto> {
|
||||||
|
const galleryUrls = createYachtDto.galleryUrls ?? [];
|
||||||
const newYacht: CatalogItemLongDto = {
|
const created = await this.prisma.yacht.create({
|
||||||
id: newId,
|
data: {
|
||||||
name: createYachtDto.name,
|
name: createYachtDto.name,
|
||||||
|
year: createYachtDto.year,
|
||||||
length: createYachtDto.length,
|
length: createYachtDto.length,
|
||||||
speed: createYachtDto.speed,
|
speed: createYachtDto.speed,
|
||||||
minCost: createYachtDto.minCost,
|
minCost: createYachtDto.minCost,
|
||||||
mainImageUrl: createYachtDto.mainImageUrl,
|
mainImageUrl: createYachtDto.mainImageUrl,
|
||||||
galleryUrls: createYachtDto.galleryUrls,
|
galleryUrls: galleryUrls as object,
|
||||||
hasQuickRent: createYachtDto.hasQuickRent,
|
hasQuickRent: createYachtDto.hasQuickRent,
|
||||||
isFeatured: createYachtDto.isFeatured,
|
isFeatured: createYachtDto.isFeatured,
|
||||||
year: createYachtDto.year,
|
topText: createYachtDto.topText,
|
||||||
comfortCapacity: createYachtDto.comfortCapacity,
|
comfortCapacity: createYachtDto.comfortCapacity,
|
||||||
maxCapacity: createYachtDto.maxCapacity,
|
maxCapacity: createYachtDto.maxCapacity,
|
||||||
width: createYachtDto.width,
|
width: createYachtDto.width,
|
||||||
|
|
@ -655,13 +294,14 @@ export class CatalogService {
|
||||||
matherial: createYachtDto.matherial,
|
matherial: createYachtDto.matherial,
|
||||||
power: createYachtDto.power,
|
power: createYachtDto.power,
|
||||||
description: createYachtDto.description,
|
description: createYachtDto.description,
|
||||||
owner: owner || ({ userId: createYachtDto.userId } as any),
|
userId: createYachtDto.userId,
|
||||||
reviews: [],
|
},
|
||||||
reservations: [],
|
include: { owner: true },
|
||||||
topText: createYachtDto.topText,
|
});
|
||||||
};
|
|
||||||
|
|
||||||
this.catalogItems.push(newYacht);
|
const item = this.yachtRowToLongDto(created);
|
||||||
return newYacht;
|
item.reviews = [];
|
||||||
|
item.reservations = [];
|
||||||
|
return item;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Global, Module } from '@nestjs/common';
|
||||||
|
import { PrismaService } from './prisma.service';
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [PrismaService],
|
||||||
|
exports: [PrismaService],
|
||||||
|
})
|
||||||
|
export class PrismaModule {}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaService
|
||||||
|
extends PrismaClient
|
||||||
|
implements OnModuleInit, OnModuleDestroy
|
||||||
|
{
|
||||||
|
async onModuleInit() {
|
||||||
|
await this.$connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModuleDestroy() {
|
||||||
|
await this.$disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Injectable, Inject, forwardRef } from '@nestjs/common';
|
import { Injectable, Inject, forwardRef } from '@nestjs/common';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { ReservationItemDto } from './reservation-item.dto';
|
import { ReservationItemDto } from './reservation-item.dto';
|
||||||
import { CatalogService } from '../catalog/catalog.service';
|
import { CatalogService } from '../catalog/catalog.service';
|
||||||
import { CatalogItemLongDto } from '../catalog/dto/catalog-item.dto';
|
import { CatalogItemLongDto } from '../catalog/dto/catalog-item.dto';
|
||||||
|
|
@ -14,95 +15,79 @@ export interface ReservationWithYacht extends Reservation {
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ReservationsService {
|
export class ReservationsService {
|
||||||
constructor(
|
constructor(
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
@Inject(forwardRef(() => CatalogService))
|
@Inject(forwardRef(() => CatalogService))
|
||||||
private readonly catalogService: CatalogService,
|
private readonly catalogService: CatalogService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private reservations: Reservation[] = [
|
createReservation(dto: ReservationItemDto): Promise<Reservation> {
|
||||||
{
|
return this.prisma.reservation
|
||||||
id: 1,
|
.create({
|
||||||
yachtId: 1,
|
data: {
|
||||||
reservatorId: 1,
|
yachtId: dto.yachtId,
|
||||||
// Corrected: Jan 1, 2026 20:00 UTC to Jan 2, 2026 08:00 UTC
|
reservatorId: dto.reservatorId,
|
||||||
startUtc: 1767369600, // Jan 1, 2026 20:00:00 UTC
|
startUtc: dto.startUtc,
|
||||||
endUtc: 1767412800, // Jan 2, 2026 08:00:00 UTC
|
endUtc: dto.endUtc,
|
||||||
},
|
},
|
||||||
{
|
})
|
||||||
id: 2,
|
.then((r) => ({
|
||||||
yachtId: 3,
|
id: r.id,
|
||||||
reservatorId: 2,
|
yachtId: r.yachtId,
|
||||||
// Jan 3, 2026 08:00 UTC to Jan 5, 2026 20:00 UTC
|
reservatorId: r.reservatorId,
|
||||||
startUtc: 1767484800, // Jan 3, 2026 08:00:00 UTC
|
startUtc: r.startUtc,
|
||||||
endUtc: 1767715200, // Jan 5, 2026 20:00:00 UTC
|
endUtc: r.endUtc,
|
||||||
},
|
}));
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
yachtId: 5,
|
|
||||||
reservatorId: 1,
|
|
||||||
// Jan 10, 2026 20:00 UTC to Jan 12, 2026 08:00 UTC
|
|
||||||
startUtc: 1768070400, // Jan 10, 2026 20:00:00 UTC
|
|
||||||
endUtc: 1768176000, // Jan 12, 2026 08:00:00 UTC
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
yachtId: 7,
|
|
||||||
reservatorId: 2,
|
|
||||||
// Jan 15, 2026 08:00 UTC to Jan 17, 2026 20:00 UTC
|
|
||||||
startUtc: 1768435200, // Jan 15, 2026 08:00:00 UTC
|
|
||||||
endUtc: 1768684800, // Jan 17, 2026 20:00:00 UTC
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
yachtId: 9,
|
|
||||||
reservatorId: 1,
|
|
||||||
// Jan 20, 2026 20:00 UTC to Jan 22, 2026 08:00 UTC
|
|
||||||
startUtc: 1768944000, // Jan 20, 2026 20:00:00 UTC
|
|
||||||
endUtc: 1769049600, // Jan 22, 2026 08:00:00 UTC
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
yachtId: 11,
|
|
||||||
reservatorId: 2,
|
|
||||||
// Jan 25, 2026 08:00 UTC to Jan 27, 2026 20:00 UTC
|
|
||||||
startUtc: 1769385600, // Jan 25, 2026 08:00:00 UTC
|
|
||||||
endUtc: 1769635200, // Jan 27, 2026 20:00:00 UTC
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
private idCounter = 7;
|
|
||||||
|
|
||||||
createReservation(dto: ReservationItemDto): Reservation {
|
|
||||||
const reservation = {
|
|
||||||
id: this.idCounter++,
|
|
||||||
...dto,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.reservations.push(reservation);
|
|
||||||
return reservation;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getReservationsByUserId(userId: number): Promise<ReservationWithYacht[]> {
|
async getReservationsByUserId(userId: number): Promise<ReservationWithYacht[]> {
|
||||||
const reservations = this.reservations.filter((r) => r.reservatorId === userId);
|
const reservations = await this.prisma.reservation.findMany({
|
||||||
|
where: { reservatorId: userId },
|
||||||
|
orderBy: { id: 'asc' },
|
||||||
|
});
|
||||||
|
|
||||||
// Populate данные по яхте для каждой резервации
|
|
||||||
const reservationsWithYacht = await Promise.all(
|
const reservationsWithYacht = await Promise.all(
|
||||||
reservations.map(async (reservation) => {
|
reservations.map(async (reservation) => {
|
||||||
const yacht = await this.catalogService.getCatalogItemById(reservation.yachtId);
|
const yacht = await this.catalogService.getCatalogItemById(
|
||||||
|
reservation.yachtId,
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
...reservation,
|
id: reservation.id,
|
||||||
yacht: yacht || undefined,
|
yachtId: reservation.yachtId,
|
||||||
|
reservatorId: reservation.reservatorId,
|
||||||
|
startUtc: reservation.startUtc,
|
||||||
|
endUtc: reservation.endUtc,
|
||||||
|
yacht: yacht ?? undefined,
|
||||||
};
|
};
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
return reservationsWithYacht;
|
return reservationsWithYacht;
|
||||||
}
|
}
|
||||||
|
|
||||||
getReservationsByYachtId(yachtId: number): Reservation[] {
|
async getReservationsByYachtId(yachtId: number): Promise<Reservation[]> {
|
||||||
return this.reservations.filter((r) => r.yachtId === yachtId);
|
const rows = await this.prisma.reservation.findMany({
|
||||||
|
where: { yachtId },
|
||||||
|
orderBy: { id: 'asc' },
|
||||||
|
});
|
||||||
|
return rows.map((r) => ({
|
||||||
|
id: r.id,
|
||||||
|
yachtId: r.yachtId,
|
||||||
|
reservatorId: r.reservatorId,
|
||||||
|
startUtc: r.startUtc,
|
||||||
|
endUtc: r.endUtc,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllReservations(): Reservation[] {
|
async getAllReservations(): Promise<Reservation[]> {
|
||||||
return this.reservations;
|
const rows = await this.prisma.reservation.findMany({
|
||||||
|
orderBy: { id: 'asc' },
|
||||||
|
});
|
||||||
|
return rows.map((r) => ({
|
||||||
|
id: r.id,
|
||||||
|
yachtId: r.yachtId,
|
||||||
|
reservatorId: r.reservatorId,
|
||||||
|
startUtc: r.startUtc,
|
||||||
|
endUtc: r.endUtc,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { ReviewItemDto } from './review-item.dto';
|
import { ReviewItemDto } from './review-item.dto';
|
||||||
|
|
||||||
export interface Review extends ReviewItemDto {
|
export interface Review extends ReviewItemDto {
|
||||||
|
|
@ -7,86 +8,64 @@ export interface Review extends ReviewItemDto {
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ReviewsService {
|
export class ReviewsService {
|
||||||
private reviews: Review[] = [
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
reviewerId: 1,
|
|
||||||
yachtId: 1,
|
|
||||||
starsCount: 5,
|
|
||||||
description: 'Excellent yacht!',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
reviewerId: 2,
|
|
||||||
yachtId: 1,
|
|
||||||
starsCount: 4,
|
|
||||||
description: 'Very good experience',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
reviewerId: 1,
|
|
||||||
yachtId: 3,
|
|
||||||
starsCount: 3,
|
|
||||||
description: 'Average condition',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
reviewerId: 2,
|
|
||||||
yachtId: 5,
|
|
||||||
starsCount: 5,
|
|
||||||
description: 'Perfect for sailing',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
reviewerId: 1,
|
|
||||||
yachtId: 7,
|
|
||||||
starsCount: 4,
|
|
||||||
description: 'Comfortable and fast',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
reviewerId: 2,
|
|
||||||
yachtId: 9,
|
|
||||||
starsCount: 2,
|
|
||||||
description: 'Needs maintenance',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 7,
|
|
||||||
reviewerId: 1,
|
|
||||||
yachtId: 11,
|
|
||||||
starsCount: 5,
|
|
||||||
description: 'Luxury experience',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 8,
|
|
||||||
reviewerId: 2,
|
|
||||||
yachtId: 12,
|
|
||||||
starsCount: 4,
|
|
||||||
description: 'Great value for money',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
private idCounter = 9;
|
async createReview(dto: ReviewItemDto): Promise<Review> {
|
||||||
|
const created = await this.prisma.review.create({
|
||||||
createReview(dto: ReviewItemDto): Review {
|
data: {
|
||||||
const review = {
|
reviewerId: dto.reviewerId,
|
||||||
id: this.idCounter++,
|
yachtId: dto.yachtId,
|
||||||
...dto,
|
starsCount: dto.starsCount,
|
||||||
|
description: dto.description,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
id: created.id,
|
||||||
|
reviewerId: created.reviewerId,
|
||||||
|
yachtId: created.yachtId,
|
||||||
|
starsCount: created.starsCount,
|
||||||
|
description: created.description,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.reviews.push(review);
|
|
||||||
return review;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getReviewsByUserId(userId: number): Review[] {
|
async getReviewsByUserId(userId: number): Promise<Review[]> {
|
||||||
return this.reviews.filter((r) => r.reviewerId === userId);
|
const rows = await this.prisma.review.findMany({
|
||||||
|
where: { reviewerId: userId },
|
||||||
|
orderBy: { id: 'asc' },
|
||||||
|
});
|
||||||
|
return rows.map((r) => ({
|
||||||
|
id: r.id,
|
||||||
|
reviewerId: r.reviewerId,
|
||||||
|
yachtId: r.yachtId,
|
||||||
|
starsCount: r.starsCount,
|
||||||
|
description: r.description,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
getReviewsByYachtId(yachtId: number): Review[] {
|
async getReviewsByYachtId(yachtId: number): Promise<Review[]> {
|
||||||
return this.reviews.filter((r) => r.yachtId === yachtId);
|
const rows = await this.prisma.review.findMany({
|
||||||
|
where: { yachtId },
|
||||||
|
orderBy: { id: 'asc' },
|
||||||
|
});
|
||||||
|
return rows.map((r) => ({
|
||||||
|
id: r.id,
|
||||||
|
reviewerId: r.reviewerId,
|
||||||
|
yachtId: r.yachtId,
|
||||||
|
starsCount: r.starsCount,
|
||||||
|
description: r.description,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllReviews(): Review[] {
|
async getAllReviews(): Promise<Review[]> {
|
||||||
return this.reviews;
|
const rows = await this.prisma.review.findMany({
|
||||||
|
orderBy: { id: 'asc' },
|
||||||
|
});
|
||||||
|
return rows.map((r) => ({
|
||||||
|
id: r.id,
|
||||||
|
reviewerId: r.reviewerId,
|
||||||
|
yachtId: r.yachtId,
|
||||||
|
starsCount: r.starsCount,
|
||||||
|
description: r.description,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,124 +1,102 @@
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { YachtsService } from '../yachts/yachts.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { User } from './user.entity';
|
import { User } from './user.entity';
|
||||||
|
import { Yacht } from '../yachts/yacht.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UsersService {
|
export class UsersService {
|
||||||
private readonly logger = new Logger(UsersService.name);
|
private readonly logger = new Logger(UsersService.name);
|
||||||
|
|
||||||
private readonly users: User[] = [
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
{
|
|
||||||
userId: 1,
|
private toUser(row: {
|
||||||
firstName: 'Иван',
|
id: number;
|
||||||
lastName: 'Андреев',
|
firstName: string;
|
||||||
phone: '+79009009090',
|
lastName: string;
|
||||||
email: 'ivan@yachting.ru',
|
phone: string;
|
||||||
password: 'admin',
|
email: string;
|
||||||
companyName: 'Северный Флот',
|
password: string | null;
|
||||||
inn: 1234567890,
|
companyName: string | null;
|
||||||
ogrn: 1122334455667,
|
inn: bigint | null;
|
||||||
},
|
ogrn: bigint | null;
|
||||||
{
|
yachts?: { id: number; name: string; model: string | null; year: number; length: number; userId: number }[];
|
||||||
userId: 2,
|
}): User {
|
||||||
firstName: 'Сергей',
|
const user: User = {
|
||||||
lastName: 'Большаков',
|
userId: row.id,
|
||||||
phone: '+79119119191',
|
firstName: row.firstName,
|
||||||
email: 'sergey@yachting.ru',
|
lastName: row.lastName,
|
||||||
password: 'admin',
|
phone: row.phone,
|
||||||
companyName: 'Балтийские Просторы',
|
email: row.email,
|
||||||
inn: 9876543210,
|
password: row.password ?? undefined,
|
||||||
ogrn: 9988776655443,
|
companyName: row.companyName ?? undefined,
|
||||||
},
|
inn: row.inn != null ? Number(row.inn) : undefined,
|
||||||
{
|
ogrn: row.ogrn != null ? Number(row.ogrn) : undefined,
|
||||||
userId: 3,
|
};
|
||||||
firstName: 'Анна',
|
if (row.yachts) {
|
||||||
lastName: 'Петрова',
|
user.yachts = row.yachts.map((y) => ({
|
||||||
phone: '+79229229292',
|
yachtId: y.id,
|
||||||
email: 'anna@yachting.ru',
|
name: y.name,
|
||||||
password: 'admin',
|
model: y.model ?? '',
|
||||||
companyName: 'Ладожские Ветры',
|
year: y.year,
|
||||||
inn: 5555555555,
|
length: y.length,
|
||||||
ogrn: 3333444455556,
|
userId: y.userId,
|
||||||
},
|
createdAt: new Date(),
|
||||||
{
|
updatedAt: new Date(),
|
||||||
userId: 4,
|
})) as Yacht[];
|
||||||
firstName: 'Дмитрий',
|
}
|
||||||
lastName: 'Соколов',
|
return user;
|
||||||
phone: '+79339339393',
|
}
|
||||||
email: 'dmitry@yachting.ru',
|
|
||||||
password: 'admin',
|
|
||||||
companyName: 'Финский Залив',
|
|
||||||
inn: 1111222233,
|
|
||||||
ogrn: 7777888899990,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
async findOne(
|
async findOne(
|
||||||
email: string,
|
email: string,
|
||||||
includeYachts: boolean = false,
|
includeYachts: boolean = false,
|
||||||
): Promise<User | undefined> {
|
): Promise<User | undefined> {
|
||||||
const user = this.users.find((user) => user.email === email);
|
const user = await this.prisma.user.findFirst({
|
||||||
|
where: { email },
|
||||||
if (user && includeYachts) {
|
include: includeYachts ? { yachts: true } : undefined,
|
||||||
user.yachts = [];
|
});
|
||||||
}
|
return user ? this.toUser(user) : undefined;
|
||||||
|
|
||||||
return user;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async findByPhone(
|
async findByPhone(
|
||||||
phone: string,
|
phone: string,
|
||||||
includeYachts: boolean = false,
|
includeYachts: boolean = false,
|
||||||
): Promise<User | undefined> {
|
): Promise<User | undefined> {
|
||||||
const user = this.users.find((u) => u.phone === phone);
|
const user = await this.prisma.user.findUnique({
|
||||||
if (user && includeYachts) {
|
where: { phone },
|
||||||
user.yachts = [];
|
include: includeYachts ? { yachts: true } : undefined,
|
||||||
}
|
});
|
||||||
return user;
|
return user ? this.toUser(user) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Создать пользователя по номеру телефона (при первой авторизации по коду). */
|
/** Создать пользователя по номеру телефона (при первой авторизации по коду). */
|
||||||
async createByPhone(phone: string): Promise<User> {
|
async createByPhone(phone: string): Promise<User> {
|
||||||
const maxId = this.users.length
|
const created = await this.prisma.user.create({
|
||||||
? Math.max(...this.users.map((u) => u.userId))
|
data: {
|
||||||
: 0;
|
|
||||||
const newUser: User = {
|
|
||||||
userId: maxId + 1,
|
|
||||||
firstName: '',
|
firstName: '',
|
||||||
lastName: '',
|
lastName: '',
|
||||||
phone,
|
phone,
|
||||||
email: '',
|
email: '',
|
||||||
yachts: [],
|
},
|
||||||
};
|
});
|
||||||
this.users.push(newUser);
|
return this.toUser(created);
|
||||||
return newUser;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async findById(
|
async findById(
|
||||||
userId: number,
|
userId: number,
|
||||||
includeYachts: boolean = false,
|
includeYachts: boolean = false,
|
||||||
): Promise<User | undefined> {
|
): Promise<User | undefined> {
|
||||||
const user = this.users.find((user) => user.userId === userId);
|
const user = await this.prisma.user.findUnique({
|
||||||
|
where: { id: userId },
|
||||||
if (user && includeYachts) {
|
include: includeYachts ? { yachts: true } : undefined,
|
||||||
user.yachts = [];
|
});
|
||||||
}
|
return user ? this.toUser(user) : undefined;
|
||||||
|
|
||||||
return user;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAll(includeYachts: boolean = false): Promise<User[]> {
|
async findAll(includeYachts: boolean = false): Promise<User[]> {
|
||||||
if (!includeYachts) {
|
const users = await this.prisma.user.findMany({
|
||||||
return this.users;
|
include: includeYachts ? { yachts: true } : undefined,
|
||||||
}
|
});
|
||||||
|
return users.map((u) => this.toUser(u));
|
||||||
const usersWithYachts = await Promise.all(
|
|
||||||
this.users.map(async (user) => {
|
|
||||||
const yachts = [];
|
|
||||||
return { ...user, yachts };
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return usersWithYachts;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { Yacht } from './yacht.entity';
|
import { Yacht } from './yacht.entity';
|
||||||
import { CreateYachtDto } from './dto/create-yacht.dto';
|
import { CreateYachtDto } from './dto/create-yacht.dto';
|
||||||
import { UpdateYachtDto } from './dto/update-yacht.dto';
|
import { UpdateYachtDto } from './dto/update-yacht.dto';
|
||||||
|
|
@ -7,89 +8,100 @@ import { UpdateYachtDto } from './dto/update-yacht.dto';
|
||||||
export class YachtsService {
|
export class YachtsService {
|
||||||
private readonly logger = new Logger(YachtsService.name);
|
private readonly logger = new Logger(YachtsService.name);
|
||||||
|
|
||||||
private yachts: Yacht[] = [
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
{
|
|
||||||
yachtId: 1,
|
private toYacht(row: {
|
||||||
name: 'Sea Dream',
|
id: number;
|
||||||
model: 'Sunseeker 76',
|
name: string;
|
||||||
year: 2020,
|
model: string | null;
|
||||||
length: 76,
|
year: number;
|
||||||
userId: 1,
|
length: number;
|
||||||
createdAt: new Date('2023-01-15'),
|
userId: number;
|
||||||
updatedAt: new Date('2023-01-15'),
|
createdAt: Date;
|
||||||
},
|
updatedAt: Date;
|
||||||
{
|
}): Yacht {
|
||||||
yachtId: 2,
|
return {
|
||||||
name: 'Ocean Breeze',
|
yachtId: row.id,
|
||||||
model: 'Princess 68',
|
name: row.name,
|
||||||
year: 2021,
|
model: row.model ?? '',
|
||||||
length: 68,
|
year: row.year,
|
||||||
userId: 1,
|
length: row.length,
|
||||||
createdAt: new Date('2023-02-20'),
|
userId: row.userId,
|
||||||
updatedAt: new Date('2023-02-20'),
|
createdAt: row.createdAt,
|
||||||
},
|
updatedAt: row.updatedAt,
|
||||||
{
|
};
|
||||||
yachtId: 3,
|
}
|
||||||
name: 'Wave Rider',
|
|
||||||
model: 'Ferretti 70',
|
|
||||||
year: 2019,
|
|
||||||
length: 70,
|
|
||||||
userId: 2,
|
|
||||||
createdAt: new Date('2023-03-10'),
|
|
||||||
updatedAt: new Date('2023-03-10'),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
async findAll(): Promise<Yacht[]> {
|
async findAll(): Promise<Yacht[]> {
|
||||||
return this.yachts;
|
const rows = await this.prisma.yacht.findMany({
|
||||||
|
orderBy: { id: 'asc' },
|
||||||
|
});
|
||||||
|
return rows.map((r) => this.toYacht(r));
|
||||||
}
|
}
|
||||||
|
|
||||||
async findById(yachtId: number): Promise<Yacht> {
|
async findById(yachtId: number): Promise<Yacht> {
|
||||||
const yacht = this.yachts.find((y) => y.yachtId === yachtId);
|
const yacht = await this.prisma.yacht.findUnique({
|
||||||
|
where: { id: yachtId },
|
||||||
|
});
|
||||||
if (!yacht) {
|
if (!yacht) {
|
||||||
throw new NotFoundException(`Yacht with ID ${yachtId} not found`);
|
throw new NotFoundException(`Yacht with ID ${yachtId} not found`);
|
||||||
}
|
}
|
||||||
return yacht;
|
return this.toYacht(yacht);
|
||||||
}
|
}
|
||||||
|
|
||||||
async findByUserId(userId: number): Promise<Yacht[]> {
|
async findByUserId(userId: number): Promise<Yacht[]> {
|
||||||
return this.yachts.filter((y) => y.userId === userId);
|
const rows = await this.prisma.yacht.findMany({
|
||||||
|
where: { userId },
|
||||||
|
orderBy: { id: 'asc' },
|
||||||
|
});
|
||||||
|
return rows.map((r) => this.toYacht(r));
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(createYachtDto: CreateYachtDto): Promise<Yacht> {
|
async create(createYachtDto: CreateYachtDto): Promise<Yacht> {
|
||||||
const newYacht: Yacht = {
|
const created = await this.prisma.yacht.create({
|
||||||
yachtId: this.yachts.length + 1,
|
data: {
|
||||||
...createYachtDto,
|
name: createYachtDto.name,
|
||||||
createdAt: new Date(),
|
model: createYachtDto.model,
|
||||||
updatedAt: new Date(),
|
year: createYachtDto.year,
|
||||||
};
|
length: createYachtDto.length,
|
||||||
this.yachts.push(newYacht);
|
userId: createYachtDto.userId,
|
||||||
return newYacht;
|
},
|
||||||
|
});
|
||||||
|
return this.toYacht(created);
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(
|
async update(
|
||||||
yachtId: number,
|
yachtId: number,
|
||||||
updateYachtDto: UpdateYachtDto,
|
updateYachtDto: UpdateYachtDto,
|
||||||
): Promise<Yacht> {
|
): Promise<Yacht> {
|
||||||
const index = this.yachts.findIndex((y) => y.yachtId === yachtId);
|
const yacht = await this.prisma.yacht.findUnique({
|
||||||
if (index === -1) {
|
where: { id: yachtId },
|
||||||
|
});
|
||||||
|
if (!yacht) {
|
||||||
throw new NotFoundException(`Yacht with ID ${yachtId} not found`);
|
throw new NotFoundException(`Yacht with ID ${yachtId} not found`);
|
||||||
}
|
}
|
||||||
|
const updated = await this.prisma.yacht.update({
|
||||||
this.yachts[index] = {
|
where: { id: yachtId },
|
||||||
...this.yachts[index],
|
data: {
|
||||||
...updateYachtDto,
|
...(updateYachtDto.name != null && { name: updateYachtDto.name }),
|
||||||
updatedAt: new Date(),
|
...(updateYachtDto.model != null && { model: updateYachtDto.model }),
|
||||||
};
|
...(updateYachtDto.year != null && { year: updateYachtDto.year }),
|
||||||
|
...(updateYachtDto.length != null && { length: updateYachtDto.length }),
|
||||||
return this.yachts[index];
|
...(updateYachtDto.userId != null && { userId: updateYachtDto.userId }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return this.toYacht(updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(yachtId: number): Promise<void> {
|
async delete(yachtId: number): Promise<void> {
|
||||||
const index = this.yachts.findIndex((y) => y.yachtId === yachtId);
|
const yacht = await this.prisma.yacht.findUnique({
|
||||||
if (index === -1) {
|
where: { id: yachtId },
|
||||||
|
});
|
||||||
|
if (!yacht) {
|
||||||
throw new NotFoundException(`Yacht with ID ${yachtId} not found`);
|
throw new NotFoundException(`Yacht with ID ${yachtId} not found`);
|
||||||
}
|
}
|
||||||
this.yachts.splice(index, 1);
|
await this.prisma.yacht.delete({
|
||||||
|
where: { id: yachtId },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue