From 5d2fc19d026b5affc2fa7488a06b846c329beb94 Mon Sep 17 00:00:00 2001 From: Morkhod Date: Mon, 23 Apr 2018 17:55:56 +0500 Subject: [PATCH 1/2] first attempt --- index.js | 6 ++++ models/cart.js | 14 ++++++++ models/country.js | 10 ++++++ models/review.js | 31 +++++++++++++++++ models/souvenir.js | 34 ++++++++++++++++++ models/tag.js | 10 ++++++ models/user.js | 10 ++++++ package-lock.json | 18 +++++----- package.json | 4 ++- playground.js | 2 +- queries.js | 86 ++++++++++++++++++++++++++++++++++++++++++++-- 11 files changed, 212 insertions(+), 13 deletions(-) diff --git a/index.js b/index.js index 419aad3..0f161d4 100644 --- a/index.js +++ b/index.js @@ -21,6 +21,12 @@ const Cart = sequelize.import('models/cart'); const User = sequelize.import('models/user'); // Ваши relations между моделями :) +Souvenir.belongsToMany(Tag, { through: 'SouvenirTags' }); +Souvenir.belongsTo(Country); +Souvenir.hasMany(Review); +Cart.belongsToMany(Souvenir, { through: 'CartSouvenirs' }); +Cart.belongsTo(User); +Review.belongsTo(User); module.exports.sequelize = sequelize; diff --git a/models/cart.js b/models/cart.js index e781846..2038e97 100644 --- a/models/cart.js +++ b/models/cart.js @@ -2,4 +2,18 @@ module.exports = (sequelize, DataTypes) => { // Ваша модель корзины + return sequelize.define('carts', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + userId: { + type: DataTypes.INTEGER, + references: { + model: 'users', + key: 'id' + } + } + }); }; diff --git a/models/country.js b/models/country.js index dfe55d7..d10e345 100644 --- a/models/country.js +++ b/models/country.js @@ -2,4 +2,14 @@ module.exports = (sequelize, DataTypes) => { // Ваша модель страны + return sequelize.define('countries', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + name: { + type: DataTypes.TEXT + } + }); }; diff --git a/models/review.js b/models/review.js index aba643d..b0c4df5 100644 --- a/models/review.js +++ b/models/review.js @@ -2,4 +2,35 @@ module.exports = (sequelize, DataTypes) => { // Ваша модель отзыва + return sequelize.define('reviews', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + isApproved: { + type: DataTypes.BOOLEAN, + defaultValue: false + }, + rating: { + type: DataTypes.DOUBLE + }, + text: { + type: DataTypes.TEXT + }, + souvenirId: { + type: DataTypes.INTEGER, + references: { + model: 'souvenirs', + key: 'id' + } + }, + userId: { + type: DataTypes.INTEGER, + references: { + model: 'users', + key: 'id' + } + } + }); }; diff --git a/models/souvenir.js b/models/souvenir.js index 86ce8c0..f993f6e 100644 --- a/models/souvenir.js +++ b/models/souvenir.js @@ -2,4 +2,38 @@ module.exports = (sequelize, DataTypes) => { // Ваша модель сувенира + return sequelize.define('souvenirs', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + amount: { + type: DataTypes.INTEGER + }, + image: { + type: DataTypes.TEXT + }, + isRecent: { + type: DataTypes.BOOLEAN + }, + name: { + type: DataTypes.TEXT + }, + price: { + type: DataTypes.DOUBLE + }, + rating: { + type: DataTypes.DOUBLE + }, + countryId: { + type: DataTypes.INTEGER, + references: { + model: 'countries', + key: 'id' + } + } + }, { + indexes: [{ fields: ['rating', 'price'] }] + }); }; diff --git a/models/tag.js b/models/tag.js index 4db36f5..ab8081e 100644 --- a/models/tag.js +++ b/models/tag.js @@ -2,4 +2,14 @@ module.exports = (sequelize, DataTypes) => { // Ваша модель тэга + return sequelize.define('tags', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + name: { + type: DataTypes.TEXT + } + }); }; diff --git a/models/user.js b/models/user.js index 1988ff6..420b38d 100644 --- a/models/user.js +++ b/models/user.js @@ -2,4 +2,14 @@ module.exports = (sequelize, DataTypes) => { // Ваша модель юзера + return sequelize.define('users', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + login: { + type: DataTypes.TEXT + } + }); }; diff --git a/package-lock.json b/package-lock.json index 6e94139..6f687fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -784,16 +784,16 @@ } }, "moment": { - "version": "2.22.0", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.0.tgz", - "integrity": "sha512-1muXCh8jb1N/gHRbn9VDUBr0GYb8A/aVcHlII9QSB68a50spqEVLIGN6KVmCOnSvJrUhC0edGgKU5ofnGXdYdg==" + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.1.tgz", + "integrity": "sha512-shJkRTSebXvsVqk56I+lkb2latjBs8I+pc2TzWc545y2iFnSjm7Wg0QMh+ZWcdSLQyGEau5jI8ocnmkyTgr9YQ==" }, "moment-timezone": { - "version": "0.5.14", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.14.tgz", - "integrity": "sha1-TrOP+VOLgBCLpGekWPPtQmjM/LE=", + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.16.tgz", + "integrity": "sha512-4d1l92plNNqnMkqI/7boWNVXJvwGL2WyByl1Hxp3h/ao3HZiAqaoQY+6KBkYdiN5QtNDpndq+58ozl8W4GVoNw==", "requires": { - "moment": "2.22.0" + "moment": "2.22.1" } }, "ms": { @@ -1093,8 +1093,8 @@ "generic-pool": "3.4.2", "inflection": "1.12.0", "lodash": "4.17.5", - "moment": "2.22.0", - "moment-timezone": "0.5.14", + "moment": "2.22.1", + "moment-timezone": "0.5.16", "retry-as-promised": "2.3.2", "semver": "5.5.0", "terraformer-wkt-parser": "1.1.2", diff --git a/package.json b/package.json index 7aa147c..a59a83c 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,9 @@ "main": "index.js", "scripts": { "lint": "eslint .", - "test": "npm run lint" + "test": "npm run lint", + "ss": "C:\\PROGRA~1\\PostgreSQL\\10\\bin\\psql.exe -h localhost -p 5432 --username postgres -f dump/dump.backup", + "start": "node playground.js" }, "dependencies": { "dotenv": "5.0.1", diff --git a/playground.js b/playground.js index 7c1b98e..edae87f 100644 --- a/playground.js +++ b/playground.js @@ -10,7 +10,7 @@ const Queries = require('./queries'); try { // Здесь можно делать запросы, чтобы проверять, что они правильно работают - const result = await queries.getAllSouvenirs(); + const result = await queries.getCheapSouvenirs(839.5); console.info(result); } catch (error) { diff --git a/queries.js b/queries.js index 7e9b7f9..0364ef8 100644 --- a/queries.js +++ b/queries.js @@ -3,25 +3,51 @@ class Queries { constructor(models) { // Что-нибудь инициализируем в конструкторе + this.sequelize = models.sequelize; + this.Op = models.sequelize.Op; + this.Cart = models.Cart; + this.Country = models.Country; + this.Review = models.Review; + this.Souvenir = models.Souvenir; + this.Tag = models.Tag; + this.User = models.User; } // Далее идут методы, которые вам необходимо реализовать: getAllSouvenirs() { // Данный метод должен возвращать все сувениры. + return this.Souvenir.findAll(); } getCheapSouvenirs(price) { // Данный метод должен возвращать все сувениры, цена которых меньше или равна price. + return this.Souvenir.findAll({ + where: { + price: { [this.Op.lt]: price } + } + }); } getTopRatingSouvenirs(n) { // Данный метод должен возвращать топ n сувениров с самым большим рейтингом. + return this.Souvenir.findAll({ + order: [['rating', 'DESC']], + limit: n + }); } getSouvenirsByTag(tag) { // Данный метод должен возвращать все сувениры, в тегах которых есть tag. // Кроме того, в ответе должны быть только поля id, name, image, price и rating. + return this.Souvenir.findAll({ + attributes: ['id', 'name', 'image', 'price', 'rating'], + include: { + model: this.Tag, + where: { name: tag }, + attributes: [] + } + }); } getSouvenirsCount({ country, rating, price }) { @@ -31,16 +57,44 @@ class Queries { // Важно, чтобы метод работал очень быстро, // поэтому учтите это при определении моделей (!). + return this.Souvenir.count({ + where: { + rating: { [this.Op.gte]: rating }, + price: { [this.Op.lte]: price } + }, + include: [{ + model: this.Country, + where: { name: country }, + attributes: [] + }] + }); } searchSouvenirs(substring) { // Данный метод должен возвращать все сувениры, в название которых входит // подстрока substring. Поиск должен быть регистронезависимым. + return this.Souvenir.findAll({ + where: { + name: { [this.Op.iLike]: `%${substring}%` } + } + }); } getDisscusedSouvenirs(n) { // Данный метод должен возвращать все сувениры, имеющих >= n отзывов. // Кроме того, в ответе должны быть только поля id, name, image, price и rating. + return this.Souvenir.findAll({ + attributes: ['name', 'image', 'price', 'rating'], + group: 'souvenirs.id', + having: this.sequelize.where( + this.sequelize.fn('COUNT', this.sequelize.col('reviews.id')), '>=', n + ), + include: { + model: this.Review, + attributes: [] + }, + order: ['id'] + }); } deleteOutOfStockSouvenirs() { @@ -48,19 +102,47 @@ class Queries { // (то есть amount = 0). // Метод должен возвращать количество удаленных сувениров в случае успешного удаления. + return this.Souvenir.destroy({ + where: { amount: 0 } + }); } - addReview(souvenirId, { login, text, rating }) { + async addReview(souvenirId, { login, text, rating }) { // Данный метод должен добавлять отзыв к сувениру souvenirId // содержит login, text, rating - из аргументов. // Обратите внимание, что при добавлении отзыва рейтинг сувенира должен быть пересчитан, // и всё это должно происходить за одну транзакцию (!). + const user = await this.User.findOne({ where: { login } }); + const souvenir = await this.Souvenir.findById(souvenirId); + + return this.sequelize.transaction(async transaction => { + const review = { userId: user.id, text, rating }; + await souvenir.createReview(review, { transaction }); + + const reviews = await souvenir.getReviews({ transaction }); + rating = (souvenir.rating * (reviews.length - 1) + rating) / (reviews.length); + + await souvenir.update({ rating }, { transaction }); + }); } - getCartSum(login) { + async getCartSum(login) { // Данный метод должен считать общую стоимость корзины пользователя login // У пользователя может быть только одна корзина, поэтому это тоже можно отразить // в модели. + return await this.Cart.sum('souvenirs.price', { + group: 'carts.id', + includeIgnoreAttributes: false, + include: [ + { + model: this.Souvenir + }, + { + model: this.User, + where: { login } + } + ] + }); } } From 8cc52d077ff3a9e795a1413eb7b2ca77142c5124 Mon Sep 17 00:00:00 2001 From: Morkhod Date: Wed, 25 Apr 2018 21:34:59 +0500 Subject: [PATCH 2/2] changes --- models/tag.js | 2 ++ models/user.js | 2 ++ queries.js | 14 ++++++++++---- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/models/tag.js b/models/tag.js index ab8081e..ef15d9c 100644 --- a/models/tag.js +++ b/models/tag.js @@ -11,5 +11,7 @@ module.exports = (sequelize, DataTypes) => { name: { type: DataTypes.TEXT } + }, { + indexes: [{ fields: ['name'] }] }); }; diff --git a/models/user.js b/models/user.js index 420b38d..59863b8 100644 --- a/models/user.js +++ b/models/user.js @@ -11,5 +11,7 @@ module.exports = (sequelize, DataTypes) => { login: { type: DataTypes.TEXT } + }, { + indexes: [{ unique: true, fields: ['login'] }] }); }; diff --git a/queries.js b/queries.js index 0364ef8..87adab8 100644 --- a/queries.js +++ b/queries.js @@ -24,7 +24,7 @@ class Queries { // Данный метод должен возвращать все сувениры, цена которых меньше или равна price. return this.Souvenir.findAll({ where: { - price: { [this.Op.lt]: price } + price: { [this.Op.lte]: price } } }); } @@ -44,7 +44,9 @@ class Queries { attributes: ['id', 'name', 'image', 'price', 'rating'], include: { model: this.Tag, - where: { name: tag }, + where: { + name: { [this.Op.iLike]: `${tag}` } + }, attributes: [] } }); @@ -64,7 +66,9 @@ class Queries { }, include: [{ model: this.Country, - where: { name: country }, + where: { + name: { [this.Op.iLike]: `${country}` } + }, attributes: [] }] }); @@ -139,7 +143,9 @@ class Queries { }, { model: this.User, - where: { login } + where: { + login: { [this.Op.iLike]: `${login}` } + } } ] });