From edfa9f843cd8e3398b3dbe003e61c150de7bdcab Mon Sep 17 00:00:00 2001 From: ronnychiang Date: Wed, 15 Jun 2022 12:32:21 +0800 Subject: [PATCH 01/12] feat: get a data --- src/controllers/field-controller.js | 10 ++++++++++ src/controllers/space-controller.js | 16 ++++++++++++++++ src/controllers/zone-controller.js | 10 ++++++++++ src/errors/get-data-error.js | 19 +++++++++++++++++++ .../{getList-error.js => get-list-error.js} | 8 ++++---- src/models/field.js | 13 +++++++++++++ src/models/space/index.js | 12 +++++++++--- src/models/zone.js | 15 +++++++++++++++ src/routes/index.js | 2 ++ src/routes/modules/field.js | 1 + src/routes/modules/space.js | 7 +++++++ src/routes/modules/zone.js | 1 + src/services/field-service.js | 18 ++++++++++++++---- src/services/space-service.js | 19 +++++++++++++++++++ src/services/zone-service.js | 15 +++++++++++++-- 15 files changed, 153 insertions(+), 13 deletions(-) create mode 100644 src/controllers/space-controller.js create mode 100644 src/errors/get-data-error.js rename src/errors/{getList-error.js => get-list-error.js} (83%) create mode 100644 src/routes/modules/space.js create mode 100644 src/services/space-service.js diff --git a/src/controllers/field-controller.js b/src/controllers/field-controller.js index 5686979..f102ea3 100644 --- a/src/controllers/field-controller.js +++ b/src/controllers/field-controller.js @@ -49,6 +49,16 @@ const fieldController = { next(err); } }, + getField: async (req, res, next) => { + try { + const id = req.params.id; + const fieldService = new FieldService({ logger: req.logger }); + const field = await fieldService.getField(id); + res.status(200).json(resSuccess(field)); + } catch (err) { + next(err); + } + }, }; module.exports = fieldController; diff --git a/src/controllers/space-controller.js b/src/controllers/space-controller.js new file mode 100644 index 0000000..616aeef --- /dev/null +++ b/src/controllers/space-controller.js @@ -0,0 +1,16 @@ +const SpaceService = require('../services/space-service'); +const resSuccess = require('./helpers/response'); + +const spaceController = { + getSpace: async (req, res, next) => { + try { + const id = req.params.id; + const spaceService = new SpaceService({ logger: req.logger }); + const space = await spaceService.getSpace(id); + res.status(200).json(resSuccess(space)); + } catch (err) { + next(err); + } + }, +}; +module.exports = spaceController; diff --git a/src/controllers/zone-controller.js b/src/controllers/zone-controller.js index c727325..9d01345 100644 --- a/src/controllers/zone-controller.js +++ b/src/controllers/zone-controller.js @@ -14,6 +14,16 @@ const zoneController = { next(err); } }, + getZone: async (req, res, next) => { + try { + const id = req.params.id; + const zoneService = new ZoneService({ logger: req.logger }); + const zone = await zoneService.getZone(id); + res.status(200).json(resSuccess(zone)); + } catch (err) { + next(err); + } + }, }; module.exports = zoneController; diff --git a/src/errors/get-data-error.js b/src/errors/get-data-error.js new file mode 100644 index 0000000..b0c6c86 --- /dev/null +++ b/src/errors/get-data-error.js @@ -0,0 +1,19 @@ +const getDataErrorMap = { + fieldNotFound: { + message: '該球場並不存在', + code: 'D001', + httpCode: 404, + }, + zoneNotFound: { + message: '該區域並不存在', + code: 'D002', + httpCode: 404, + }, + spaceNotFound: { + message: '該空間並不存在', + code: 'D003', + httpCode: 404, + }, +}; + +module.exports = getDataErrorMap; diff --git a/src/errors/getList-error.js b/src/errors/get-list-error.js similarity index 83% rename from src/errors/getList-error.js rename to src/errors/get-list-error.js index 5c2516d..93801cc 100644 --- a/src/errors/getList-error.js +++ b/src/errors/get-list-error.js @@ -1,20 +1,20 @@ const getListErrorMap = { - orientationNotFound: { + orientationsNotFound: { message: '該球場無任何方位資訊', code: 'L001', httpCode: 404, }, - levelNotFound: { + levelsNotFound: { message: '該球場無任何樓層資訊', code: 'L002', httpCode: 404, }, - zoneNotFound: { + zonesNotFound: { message: '該球場無任何區域資訊', code: 'L003', httpCode: 404, }, - spaceNotFound: { + spacesNotFound: { message: '該區域無任何空間資訊', code: 'L004', httpCode: 404, diff --git a/src/models/field.js b/src/models/field.js index 57c0292..e1534c2 100644 --- a/src/models/field.js +++ b/src/models/field.js @@ -124,6 +124,19 @@ class FieldModel { }); return fieldList; } + async getField(id) { + const field = await prisma.fields.findUnique({ + where: { + id: Number(id), + }, + select: { + id: true, + name: true, + img: true, + }, + }); + return field; + } async _truncate() { await prisma.fields.deleteMany({}); } diff --git a/src/models/space/index.js b/src/models/space/index.js index 4bc9d87..d5b6989 100644 --- a/src/models/space/index.js +++ b/src/models/space/index.js @@ -4,17 +4,23 @@ const { spaceTypeMap } = require('./constants'); class SpaceModel { constructor() {} async getSpace(id) { - const getSpace = await prisma.spaces.findUnique({ + const space = await prisma.spaces.findUnique({ where: { - id: id, + id: Number(id), }, select: { id: true, + zoneId: true, + spaceType: true, + version: true, colNumber: true, rowNumber: true, + name: true, + positionColNumber: true, + positionRowNumber: true, }, }); - return getSpace; + return space; } async createSpace( zoneId, diff --git a/src/models/zone.js b/src/models/zone.js index 4d33c6f..dee8b24 100644 --- a/src/models/zone.js +++ b/src/models/zone.js @@ -72,6 +72,21 @@ class ZoneModel { }); return zone; } + async getZone(id) { + const zone = await prisma.zones.findUnique({ + where: { + id: Number(id), + }, + select: { + id: true, + fieldId: true, + orientationId: true, + levelId: true, + name: true, + }, + }); + return zone; + } async _truncate() { await prisma.zones.deleteMany({}); } diff --git a/src/routes/index.js b/src/routes/index.js index fbd6691..bee0658 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -9,6 +9,7 @@ const photo = require('./modules/photo'); const password = require('./modules/password'); const field = require('./modules/field'); const zone = require('./modules/zone'); +const space = require('./modules/space'); const { isDevelopmentBuild } = require('../context'); const { passwordValidate, @@ -36,6 +37,7 @@ router.use('/api/photos', photo); router.use('/api/password', password); router.use('/api/fields', field); router.use('/api/zones', zone); +router.use('/api/spaces', space); router.patch('/api/verify-email', userController.verifyEmail); // 檢視email格式使用 diff --git a/src/routes/modules/field.js b/src/routes/modules/field.js index ff031d7..0182b5f 100644 --- a/src/routes/modules/field.js +++ b/src/routes/modules/field.js @@ -6,6 +6,7 @@ const router = express.Router(); router.get('/:id/zones', fieldController.getFieldZones); router.get('/:id/levels', fieldController.getFieldLevels); router.get('/:id/orientations', fieldController.getFieldOrientations); +router.get('/:id', fieldController.getField); router.get('/', fieldController.getFields); module.exports = router; diff --git a/src/routes/modules/space.js b/src/routes/modules/space.js new file mode 100644 index 0000000..a711f59 --- /dev/null +++ b/src/routes/modules/space.js @@ -0,0 +1,7 @@ +const express = require('express'); +const spaceController = require('../../controllers/space-controller'); + +const router = express.Router(); +router.get('/:id', spaceController.getSpace); + +module.exports = router; diff --git a/src/routes/modules/zone.js b/src/routes/modules/zone.js index 5f4543d..cd2da1e 100644 --- a/src/routes/modules/zone.js +++ b/src/routes/modules/zone.js @@ -3,5 +3,6 @@ const zoneController = require('../../controllers/zone-controller'); const router = express.Router(); router.get('/:id/spaces', zoneController.getZoneSpaces); +router.get('/:id', zoneController.getZone); module.exports = router; diff --git a/src/services/field-service.js b/src/services/field-service.js index 1b252aa..8a807fc 100644 --- a/src/services/field-service.js +++ b/src/services/field-service.js @@ -1,10 +1,12 @@ +const { isNil } = require('ramda'); const BaseService = require('./base'); const FieldModel = require('../models/field'); const OrientationModel = require('../models/orientation'); const LevelModel = require('../models/level'); const ZoneModel = require('../models/zone'); const GeneralError = require('../errors/error/general-error'); -const getListErrorMap = require('../errors/getList-error'); +const getListErrorMap = require('../errors/get-list-error'); +const getDataErrorMap = require('../errors/get-data-error'); class FieldService extends BaseService { async getFields() { @@ -20,7 +22,7 @@ class FieldService extends BaseService { fieldId ); if (!orientationList[0]) - throw new GeneralError(getListErrorMap['orientationNotFound']); + throw new GeneralError(getListErrorMap['orientationsNotFound']); this.logger.debug('got a orientationList', { orientationList }); return orientationList; @@ -28,7 +30,8 @@ class FieldService extends BaseService { async getLevelsByField(fieldId) { const levelModel = new LevelModel(); const levelList = await levelModel.getLevelsByField(fieldId); - if (!levelList[0]) throw new GeneralError(getListErrorMap['levelNotFound']); + if (!levelList[0]) + throw new GeneralError(getListErrorMap['levelsNotFound']); this.logger.debug('got a levelList', { levelList }); return levelList; @@ -40,11 +43,18 @@ class FieldService extends BaseService { orientationId, levelId ); - if (!zoneList[0]) throw new GeneralError(getListErrorMap['zoneNotFound']); + if (!zoneList[0]) throw new GeneralError(getListErrorMap['zonesNotFound']); this.logger.debug('got a zoneList', { zoneList }); return zoneList; } + async getField(id) { + const fieldModel = new FieldModel(); + const field = await fieldModel.getField(id); + if (isNil(field)) throw new GeneralError(getDataErrorMap['fieldNotFound']); + this.logger.debug('got a field', { field }); + return field; + } } module.exports = FieldService; diff --git a/src/services/space-service.js b/src/services/space-service.js new file mode 100644 index 0000000..114c50c --- /dev/null +++ b/src/services/space-service.js @@ -0,0 +1,19 @@ +const { isNil } = require('ramda'); +const BaseService = require('./base'); +const SpaceModel = require('../models/space'); +const GeneralError = require('../errors/error/general-error'); +const getDataErrorMap = require('../errors/get-data-error'); + +class SpaceService extends BaseService { + async getSpace(id) { + const zoneModel = new SpaceModel(); + + const space = await zoneModel.getSpace(id); + if (isNil(space)) throw new GeneralError(getDataErrorMap['spaceNotFound']); + + this.logger.debug('got a space', { space }); + return space; + } +} + +module.exports = SpaceService; diff --git a/src/services/zone-service.js b/src/services/zone-service.js index 986799a..fff9b0c 100644 --- a/src/services/zone-service.js +++ b/src/services/zone-service.js @@ -1,7 +1,10 @@ +const { isNil } = require('ramda'); const BaseService = require('./base'); const SpaceModel = require('../models/space'); +const ZoneModel = require('../models/zone'); const GeneralError = require('../errors/error/general-error'); -const getListErrorMap = require('../errors/getList-error'); +const getListErrorMap = require('../errors/get-list-error'); +const getDataErrorMap = require('../errors/get-data-error'); class ZoneService extends BaseService { async getSpacesByZone(zoneId, spaceType) { @@ -17,11 +20,19 @@ class ZoneService extends BaseService { const spaces = await getSpaces(spaceType); if (spaces.length === 0) - throw new GeneralError(getListErrorMap['spaceNotFound']); + throw new GeneralError(getListErrorMap['spacesNotFound']); this.logger.debug('got a SpaceList', { spaces }); return spaces; } + async getZone(id) { + const zoneModel = new ZoneModel(); + + const zone = await zoneModel.getZone(id); + if (isNil(zone)) throw new GeneralError(getDataErrorMap['zoneNotFound']); + this.logger.debug('got a zone', { zone }); + return zone; + } } module.exports = ZoneService; From f8243dd61607ba61a03d818a248b7b04a2b32472 Mon Sep 17 00:00:00 2001 From: ronnychiang Date: Wed, 15 Jun 2022 12:55:57 +0800 Subject: [PATCH 02/12] fix: get service back --- src/controllers/field-controller.js | 1 + src/controllers/zone-controller.js | 1 + 2 files changed, 2 insertions(+) diff --git a/src/controllers/field-controller.js b/src/controllers/field-controller.js index 5a0c08b..97f2e58 100644 --- a/src/controllers/field-controller.js +++ b/src/controllers/field-controller.js @@ -1,5 +1,6 @@ const resSuccess = require('./helpers/response'); const FieldsCache = require('../cache/fields'); +const FieldService = require('../services/field-service'); const LevelsByFieldCache = require('../cache/levels-by-field'); const OrientationsByFieldCache = require('../cache/orientations-by-field'); const ZonesByFieldCache = require('../cache/zones-by-field'); diff --git a/src/controllers/zone-controller.js b/src/controllers/zone-controller.js index d068520..a87799f 100644 --- a/src/controllers/zone-controller.js +++ b/src/controllers/zone-controller.js @@ -1,5 +1,6 @@ const resSuccess = require('./helpers/response'); const SpacesByZoneCache = require('../cache/spaces-by-zone'); +const ZoneService = require('../services/zone-service'); const zoneController = { getZoneSpaces: async (req, res, next) => { From 6b45f9186de843a3c745c5ed6e547d88368a576b Mon Sep 17 00:00:00 2001 From: ronnychiang Date: Wed, 15 Jun 2022 13:15:48 +0800 Subject: [PATCH 03/12] better view --- src/services/field-service.js | 10 ++++++++++ src/services/space-service.js | 2 ++ src/services/zone-service.js | 5 +++++ 3 files changed, 17 insertions(+) diff --git a/src/services/field-service.js b/src/services/field-service.js index 570c01a..a82fb9e 100644 --- a/src/services/field-service.js +++ b/src/services/field-service.js @@ -14,6 +14,7 @@ class FieldService extends BaseService { const fieldList = await fieldModel.getFields(); this.logger.debug('got a fieldList', { fieldList }); + return fieldList; } async getOrientationsByField(fieldId) { @@ -21,19 +22,23 @@ class FieldService extends BaseService { const orientationList = await orientationModel.getOrientationsByField( fieldId ); + if (!orientationList[0]) throw new GeneralError(getListErrorMap['orientationsNotFound']); this.logger.debug('got a orientationList', { orientationList }); + return orientationList; } async getLevelsByField(fieldId) { const levelModel = new LevelModel(); const levelList = await levelModel.getLevelsByField(fieldId); + if (!levelList[0]) throw new GeneralError(getListErrorMap['levelsNotFound']); this.logger.debug('got a levelList', { levelList }); + return levelList; } async getZonesByField(fieldId, orientationId, levelId) { @@ -43,16 +48,21 @@ class FieldService extends BaseService { orientationId, levelId ); + if (!zoneList[0]) throw new GeneralError(getListErrorMap['zonesNotFound']); this.logger.debug('got a zoneList', { zoneList }); + return zoneList; } async getField(id) { const fieldModel = new FieldModel(); const field = await fieldModel.getField(id); + if (isNil(field)) throw new GeneralError(getDataErrorMap['fieldNotFound']); + this.logger.debug('got a field', { field }); + return field; } } diff --git a/src/services/space-service.js b/src/services/space-service.js index 114c50c..98ccc2a 100644 --- a/src/services/space-service.js +++ b/src/services/space-service.js @@ -9,9 +9,11 @@ class SpaceService extends BaseService { const zoneModel = new SpaceModel(); const space = await zoneModel.getSpace(id); + if (isNil(space)) throw new GeneralError(getDataErrorMap['spaceNotFound']); this.logger.debug('got a space', { space }); + return space; } } diff --git a/src/services/zone-service.js b/src/services/zone-service.js index fff9b0c..4ed01cd 100644 --- a/src/services/zone-service.js +++ b/src/services/zone-service.js @@ -19,18 +19,23 @@ class ZoneService extends BaseService { } const spaces = await getSpaces(spaceType); + if (spaces.length === 0) throw new GeneralError(getListErrorMap['spacesNotFound']); this.logger.debug('got a SpaceList', { spaces }); + return spaces; } async getZone(id) { const zoneModel = new ZoneModel(); const zone = await zoneModel.getZone(id); + if (isNil(zone)) throw new GeneralError(getDataErrorMap['zoneNotFound']); + this.logger.debug('got a zone', { zone }); + return zone; } } From fae16bbbdc3725f15eb81cf2b970815594f09245 Mon Sep 17 00:00:00 2001 From: ronnychiang Date: Mon, 20 Jun 2022 16:03:05 +0800 Subject: [PATCH 04/12] feat: test getPhoto model --- src/controllers/space-controller.js | 12 +++ src/models/photo.js | 102 ++++++++++++++++++++++--- src/models/review/constant.js | 6 ++ src/models/review/review.js | 0 src/routes/modules/space.js | 1 + src/services/space-service.js | 21 ----- src/services/space-service/constant.js | 6 ++ src/services/space-service/index.js | 40 ++++++++++ 8 files changed, 158 insertions(+), 30 deletions(-) create mode 100644 src/models/review/constant.js create mode 100644 src/models/review/review.js delete mode 100644 src/services/space-service.js create mode 100644 src/services/space-service/constant.js create mode 100644 src/services/space-service/index.js diff --git a/src/controllers/space-controller.js b/src/controllers/space-controller.js index 616aeef..b5a96ce 100644 --- a/src/controllers/space-controller.js +++ b/src/controllers/space-controller.js @@ -12,5 +12,17 @@ const spaceController = { next(err); } }, + getSpacePhotos: async (req, res, next) => { + try { + const id = req.params.id; + const sort = req.query.sort; + const order = req.query.order; + const spaceService = new SpaceService({ logger: req.logger }); + const photos = await spaceService.getPhotosBySpace(id, sort, order); + res.status(200).json(resSuccess(photos)); + } catch (err) { + next(err); + } + }, }; module.exports = spaceController; diff --git a/src/models/photo.js b/src/models/photo.js index 67a7ccb..b0b9832 100644 --- a/src/models/photo.js +++ b/src/models/photo.js @@ -1,25 +1,109 @@ const prisma = require('../config/prisma'); +const { usefulMap } = require('../models/review/constant'); class PhotoModel { constructor() {} - async createPhoto(path, userId, spaceId, dateTime) { - const newPhoto = await prisma.photos.create({ - data: { - userId: userId, - spaceId: spaceId, - date: dateTime, - path: path, + async getPhotosBySpaceOrderByUseful(spaceId) { + const photosWithReviewCount = await prisma.$queryRaw`SELECT + Reviews.photoId, + COUNT(if(Reviews.useful=${usefulMap.up},true,null)) AS usefulCount, + COUNT(if(Reviews.useful=${usefulMap.down},true,null)) AS uselessCount, + COUNT(if(Reviews.useful=${usefulMap.up},true,null)) - COUNT(if(Reviews.useful=${usefulMap.down},true,null)) AS netUsefulCount + FROM Reviews + WHERE Reviews.photoId IN (select Photos.id FROM Photos where Photos.spaceId = ${spaceId}) + group by photoId + ORDER BY netUsefulCount DESC`; + + const photos = await prisma.photos.findMany({ + where: { + spaceId: Number(spaceId), }, select: { id: true, + user: { + select: { + id: true, + name: true, + }, + }, spaceId: true, + date: true, path: true, }, }); - return newPhoto; + const result = await Promise.all( + photosWithReviewCount.map(async (photo) => { + for (let i = 0; i < photos.length; i++) { + if (photos[i].id === photo.photoId) { + photo = { + id: photos[i].id, + user: { + id: photos[i].user.id, + name: photos[i].user.name, + }, + spaceId: photos[i].spaceId, + date: photos[i].date, + path: photos[i].path, + ...photo, + }; + } + } + return photo; + }) + ); + return result; + } + async getPhotosBySpaceOrderByCreateAt(spaceId) { + const photosWithReviewCount = await prisma.$queryRaw`SELECT + Reviews.photoId, + COUNT(if(Reviews.useful=${usefulMap.up},true,null)) AS usefulCount, + COUNT(if(Reviews.useful=${usefulMap.down},true,null)) AS uselessCount, + COUNT(if(Reviews.useful=${usefulMap.up},true,null)) - COUNT(if(Reviews.useful=${usefulMap.down},true,null)) AS netUsefulCount + FROM Reviews + WHERE Reviews.photoId IN (select Photos.id FROM Photos where Photos.spaceId = ${spaceId}) + group by photoId + ORDER BY netUsefulCount DESC`; + + const photos = await prisma.photos.findMany({ + where: { + spaceId: Number(spaceId), + }, + select: { + id: true, + user: { + select: { + id: true, + name: true, + }, + }, + spaceId: true, + date: true, + path: true, + }, + orderBy: { + date: 'asc', + }, + }); + const result = await Promise.all( + photos.map(async (photo) => { + for (let i = 0; i < photosWithReviewCount.length; i++) { + if (photosWithReviewCount[i].photoId === photo.id) { + console.log('catch'); + photo = { + ...photo, + usefulCount: photosWithReviewCount[i].usefulCount, + uselessCount: photosWithReviewCount[i].uselessCount, + netUsefulCount: photosWithReviewCount[i].netUsefulCount, + }; + } + } + return photo; + }) + ); + return result; } async _truncate() { - await prisma.photos.deleteMany({}); + await prisma.spaces.deleteMany({}); } } diff --git a/src/models/review/constant.js b/src/models/review/constant.js new file mode 100644 index 0000000..b16a3d1 --- /dev/null +++ b/src/models/review/constant.js @@ -0,0 +1,6 @@ +const usefulMap = { + up: 'up', + down: 'down', +}; + +module.exports = { usefulMap }; diff --git a/src/models/review/review.js b/src/models/review/review.js new file mode 100644 index 0000000..e69de29 diff --git a/src/routes/modules/space.js b/src/routes/modules/space.js index a711f59..7133bd1 100644 --- a/src/routes/modules/space.js +++ b/src/routes/modules/space.js @@ -2,6 +2,7 @@ const express = require('express'); const spaceController = require('../../controllers/space-controller'); const router = express.Router(); +router.get('/:id/photos', spaceController.getSpacePhotos); router.get('/:id', spaceController.getSpace); module.exports = router; diff --git a/src/services/space-service.js b/src/services/space-service.js deleted file mode 100644 index 98ccc2a..0000000 --- a/src/services/space-service.js +++ /dev/null @@ -1,21 +0,0 @@ -const { isNil } = require('ramda'); -const BaseService = require('./base'); -const SpaceModel = require('../models/space'); -const GeneralError = require('../errors/error/general-error'); -const getDataErrorMap = require('../errors/get-data-error'); - -class SpaceService extends BaseService { - async getSpace(id) { - const zoneModel = new SpaceModel(); - - const space = await zoneModel.getSpace(id); - - if (isNil(space)) throw new GeneralError(getDataErrorMap['spaceNotFound']); - - this.logger.debug('got a space', { space }); - - return space; - } -} - -module.exports = SpaceService; diff --git a/src/services/space-service/constant.js b/src/services/space-service/constant.js new file mode 100644 index 0000000..66c0440 --- /dev/null +++ b/src/services/space-service/constant.js @@ -0,0 +1,6 @@ +const sortMap = { + useful: 'useful', + date: 'date', +}; + +module.exports = { sortMap }; diff --git a/src/services/space-service/index.js b/src/services/space-service/index.js new file mode 100644 index 0000000..9eecf8b --- /dev/null +++ b/src/services/space-service/index.js @@ -0,0 +1,40 @@ +const { isNil } = require('ramda'); +const BaseService = require('../base'); +const SpaceModel = require('../../models/space'); +const PhotoModel = require('../../models/photo'); +const GeneralError = require('../../errors/error/general-error'); +const getDataErrorMap = require('../../errors/get-data-error'); +const { sortMap } = require('./constant'); + +class SpaceService extends BaseService { + async getSpace(id) { + const spaceModel = new SpaceModel(); + + const space = await spaceModel.getSpace(id); + + if (isNil(space)) throw new GeneralError(getDataErrorMap['spaceNotFound']); + + this.logger.debug('got a space', { space }); + + return space; + } + async getPhotosBySpace(id, sort, order) { + const photoModel = new PhotoModel(); + let photos = ''; + if (sort === sortMap.useful) { + photos = await photoModel.getPhotosBySpaceOrderByUseful(id, sort, order); + } else { + photos = await photoModel.getPhotosBySpaceOrderByCreateAt( + id, + sort, + order + ); + } + + this.logger.debug('got photos', { photos }); + + return photos; + } +} + +module.exports = SpaceService; From 7f051a55ef041cfff5f8817dc25533ab9e518b6b Mon Sep 17 00:00:00 2001 From: ronnychiang Date: Thu, 23 Jun 2022 14:27:41 +0800 Subject: [PATCH 05/12] feat: review --- .../migration.sql | 8 ++ prisma/schema.prisma | 1 + src/config/config.development.js | 3 + src/config/config.production.js | 3 + src/config/config.staging.js | 3 + src/controllers/photo-controller.js | 14 +++ src/controllers/review-controller.js | 38 +++++++++ src/controllers/space-controller.js | 2 +- src/errors/get-photo-error.js | 9 ++ src/models/photo.js | 85 +++++++------------ src/models/review/review.js | 40 +++++++++ src/routes/modules/photo.js | 3 + src/services/photo-service.js | 24 +++++- src/services/review-service.js | 24 ++++++ src/services/space-service.js | 21 ----- src/services/space-service/constant.js | 6 +- src/services/space-service/index.js | 65 +++++++++++--- 17 files changed, 257 insertions(+), 92 deletions(-) create mode 100644 prisma/migrations/20220621094639_unique_reviews/migration.sql create mode 100644 src/controllers/review-controller.js create mode 100644 src/errors/get-photo-error.js create mode 100644 src/services/review-service.js delete mode 100644 src/services/space-service.js diff --git a/prisma/migrations/20220621094639_unique_reviews/migration.sql b/prisma/migrations/20220621094639_unique_reviews/migration.sql new file mode 100644 index 0000000..7b11431 --- /dev/null +++ b/prisma/migrations/20220621094639_unique_reviews/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - A unique constraint covering the columns `[userId,photoId]` on the table `Reviews` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateIndex +CREATE UNIQUE INDEX `Reviews_userId_photoId_key` ON `Reviews`(`userId`, `photoId`); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0d1f846..d992d09 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -195,6 +195,7 @@ model Reviews { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([photoId, useful]) + @@unique([userId, photoId]) } model PasswordResetTokens { diff --git a/src/config/config.development.js b/src/config/config.development.js index c3a2f35..ae6183a 100644 --- a/src/config/config.development.js +++ b/src/config/config.development.js @@ -24,6 +24,9 @@ const config = { maxLevel: 'debug', handleExceptions: false, }, + uselessLimit: { + limit: -1, + }, }; module.exports = config; diff --git a/src/config/config.production.js b/src/config/config.production.js index 690c84c..6a52df6 100644 --- a/src/config/config.production.js +++ b/src/config/config.production.js @@ -24,6 +24,9 @@ const config = { maxLevel: 'info', handleExceptions: true, }, + uselessLimit: { + limit: -100, + }, }; module.exports = config; diff --git a/src/config/config.staging.js b/src/config/config.staging.js index 4f4c8f1..80f5686 100644 --- a/src/config/config.staging.js +++ b/src/config/config.staging.js @@ -25,6 +25,9 @@ const config = { maxLevel: 'info', handleExceptions: true, }, + uselessLimit: { + limit: -100, + }, }; module.exports = config; diff --git a/src/controllers/photo-controller.js b/src/controllers/photo-controller.js index a3b6fd7..18f13f4 100644 --- a/src/controllers/photo-controller.js +++ b/src/controllers/photo-controller.js @@ -1,6 +1,7 @@ const PhotoService = require('../services/photo-service'); const resSuccess = require('./helpers/response'); const getUser = require('./helpers/get-user'); +const SpaceService = require('../services/space-service'); const photoController = { postPhotos: async (req, res, next) => { @@ -21,6 +22,19 @@ const photoController = { next(err); } }, + getPhotosPhotos: async (req, res, next) => { + try { + const photoId = req.query.start_photo; + console.log(photoId); + const photoService = new PhotoService({ logger: req.logger }); + const startPhoto = await photoService.getPhoto(photoId); + const spaceService = new SpaceService({ logger: req.logger }); + const photos = await spaceService.getPhotosBySpace(startPhoto.spaceId); + res.status(200).json(resSuccess(photos)); + } catch (err) { + next(err); + } + }, }; module.exports = photoController; diff --git a/src/controllers/review-controller.js b/src/controllers/review-controller.js new file mode 100644 index 0000000..0ea213d --- /dev/null +++ b/src/controllers/review-controller.js @@ -0,0 +1,38 @@ +const ReviewService = require('../services/review-service'); +const resSuccess = require('./helpers/response'); +const getUser = require('./helpers/get-user'); + +const reviewController = { + postPhotos: async (req, res, next) => { + try { + const { spaceId, date } = req.body; + const userId = getUser(req).id; + + const photoService = new ReviewService({ logger: req.logger }); + const uploadInfo = await photoService.postPhotos( + spaceId, + req.files, + req.requestId, + userId, + date + ); + res.status(200).json(resSuccess(uploadInfo)); + } catch (err) { + next(err); + } + }, + getPhotosPhotos: async (req, res, next) => { + try { + const id = req.params.id; + const sort = req.query.sort; + const order = req.query.order; + const spaceService = new ReviewService({ logger: req.logger }); + const photos = await spaceService.getPhotosBySpace(id, sort, order); + res.status(200).json(resSuccess(photos)); + } catch (err) { + next(err); + } + }, +}; + +module.exports = reviewController; diff --git a/src/controllers/space-controller.js b/src/controllers/space-controller.js index b5a96ce..c49b68d 100644 --- a/src/controllers/space-controller.js +++ b/src/controllers/space-controller.js @@ -1,4 +1,4 @@ -const SpaceService = require('../services/space-service'); +const SpaceService = require('../services/space-service/index'); const resSuccess = require('./helpers/response'); const spaceController = { diff --git a/src/errors/get-photo-error.js b/src/errors/get-photo-error.js new file mode 100644 index 0000000..5c1a118 --- /dev/null +++ b/src/errors/get-photo-error.js @@ -0,0 +1,9 @@ +const getPhotoErrorMap = { + photoNotFound: { + message: '此照片不存在', + code: 'gp001', + httpCode: 404, + }, +}; + +module.exports = getPhotoErrorMap; diff --git a/src/models/photo.js b/src/models/photo.js index b0b9832..4759b4a 100644 --- a/src/models/photo.js +++ b/src/models/photo.js @@ -3,7 +3,23 @@ const { usefulMap } = require('../models/review/constant'); class PhotoModel { constructor() {} - async getPhotosBySpaceOrderByUseful(spaceId) { + async createPhoto(path, userId, spaceId, dateTime) { + const newPhoto = await prisma.photos.create({ + data: { + userId: userId, + spaceId: spaceId, + date: dateTime, + path: path, + }, + select: { + id: true, + spaceId: true, + path: true, + }, + }); + return newPhoto; + } + async getPhotosReviewCountBySpace(spaceId) { const photosWithReviewCount = await prisma.$queryRaw`SELECT Reviews.photoId, COUNT(if(Reviews.useful=${usefulMap.up},true,null)) AS usefulCount, @@ -12,8 +28,11 @@ class PhotoModel { FROM Reviews WHERE Reviews.photoId IN (select Photos.id FROM Photos where Photos.spaceId = ${spaceId}) group by photoId - ORDER BY netUsefulCount DESC`; + ORDER BY netUsefulCount desc `; + return photosWithReviewCount; + } + async getPhotosBySpace(spaceId, order) { const photos = await prisma.photos.findMany({ where: { spaceId: Number(spaceId), @@ -30,43 +49,16 @@ class PhotoModel { date: true, path: true, }, + orderBy: { + date: order ? order : 'desc', + }, }); - const result = await Promise.all( - photosWithReviewCount.map(async (photo) => { - for (let i = 0; i < photos.length; i++) { - if (photos[i].id === photo.photoId) { - photo = { - id: photos[i].id, - user: { - id: photos[i].user.id, - name: photos[i].user.name, - }, - spaceId: photos[i].spaceId, - date: photos[i].date, - path: photos[i].path, - ...photo, - }; - } - } - return photo; - }) - ); - return result; + return photos; } - async getPhotosBySpaceOrderByCreateAt(spaceId) { - const photosWithReviewCount = await prisma.$queryRaw`SELECT - Reviews.photoId, - COUNT(if(Reviews.useful=${usefulMap.up},true,null)) AS usefulCount, - COUNT(if(Reviews.useful=${usefulMap.down},true,null)) AS uselessCount, - COUNT(if(Reviews.useful=${usefulMap.up},true,null)) - COUNT(if(Reviews.useful=${usefulMap.down},true,null)) AS netUsefulCount - FROM Reviews - WHERE Reviews.photoId IN (select Photos.id FROM Photos where Photos.spaceId = ${spaceId}) - group by photoId - ORDER BY netUsefulCount DESC`; - - const photos = await prisma.photos.findMany({ + async getPhoto(id) { + const photo = await prisma.photos.findUnique({ where: { - spaceId: Number(spaceId), + id: Number(id), }, select: { id: true, @@ -80,27 +72,8 @@ class PhotoModel { date: true, path: true, }, - orderBy: { - date: 'asc', - }, }); - const result = await Promise.all( - photos.map(async (photo) => { - for (let i = 0; i < photosWithReviewCount.length; i++) { - if (photosWithReviewCount[i].photoId === photo.id) { - console.log('catch'); - photo = { - ...photo, - usefulCount: photosWithReviewCount[i].usefulCount, - uselessCount: photosWithReviewCount[i].uselessCount, - netUsefulCount: photosWithReviewCount[i].netUsefulCount, - }; - } - } - return photo; - }) - ); - return result; + return photo; } async _truncate() { await prisma.spaces.deleteMany({}); diff --git a/src/models/review/review.js b/src/models/review/review.js index e69de29..ecb8657 100644 --- a/src/models/review/review.js +++ b/src/models/review/review.js @@ -0,0 +1,40 @@ +const prisma = require('../../config/prisma'); +const { usefulMap } = require('../../models/review/constant'); + +class ReviewModel { + constructor() {} + async createReview(userId, photoId, useful) { + const newReview = await prisma.reviews.create({ + data: { + userId: Number(userId), + photoId: Number(photoId), + useful, + }, + select: { + id: true, + userId: true, + photoId: true, + useful: true, + }, + }); + return newReview; + } + async getReviewCountByPhoto(photoId) { + console.log('go'); + const photoWithReviewCount = await prisma.$queryRaw`SELECT + Reviews.photoId, + COUNT(if(Reviews.useful=${usefulMap.up},true,null)) AS usefulCount, + COUNT(if(Reviews.useful=${usefulMap.down},true,null)) AS uselessCount, + COUNT(if(Reviews.useful=${usefulMap.up},true,null)) - COUNT(if(Reviews.useful=${usefulMap.down},true,null)) AS netUsefulCount + FROM Reviews + WHERE Reviews.photoId = ${photoId} + group by Reviews.photoId`; + + return photoWithReviewCount; + } + async _truncate() { + await prisma.reviews.deleteMany({}); + } +} + +module.exports = ReviewModel; diff --git a/src/routes/modules/photo.js b/src/routes/modules/photo.js index 18ab574..48ace12 100644 --- a/src/routes/modules/photo.js +++ b/src/routes/modules/photo.js @@ -1,10 +1,13 @@ const express = require('express'); const photoController = require('../../controllers/photo-controller'); +const reviewController = require('../../controllers/review-controller'); const { uploadImages } = require('../../middleware/upload-images'); const { authenticated } = require('../../middleware/auth'); const router = express.Router(); +router.post('/:id/reviews', authenticated, reviewController.postPhotos); +router.get('/', photoController.getPhotosPhotos); router.post('/', authenticated, uploadImages, photoController.postPhotos); module.exports = router; diff --git a/src/services/photo-service.js b/src/services/photo-service.js index ed3cd6d..9a4a678 100644 --- a/src/services/photo-service.js +++ b/src/services/photo-service.js @@ -2,9 +2,11 @@ const R = require('ramda'); const BaseService = require('./base'); const PhotoModel = require('../models/photo'); const SpaceModel = require('../models/space'); +const ReviewModel = require('../models/review/review'); const PrivateError = require('../errors/error/private-error'); const GeneralError = require('../errors/error/general-error'); const postPhotoErrorMap = require('../errors/post-photo-error'); +const getPhotoErrorMap = require('../errors/get-photo-error'); const { uploadS3 } = require('../utils/upload-image/uploadS3'); const { randomHashName } = require('../utils/upload-image/random-hash-name'); const { resizeImages } = require('../utils/upload-image/resize'); @@ -36,7 +38,7 @@ class PhotoService extends BaseService { await uploadS3(resizeFile, bucket); }); - // creat photo + // create photo const dateTime = new Date(date); const path = `/${bucketMap.photos}/${file.newFilename}`; const photoModel = new PhotoModel(); @@ -62,6 +64,26 @@ class PhotoService extends BaseService { throw err; } } + async getPhoto(id) { + const photoModel = new PhotoModel(); + const photo = await photoModel.getPhoto(id); + + if (!photo) throw new GeneralError(getPhotoErrorMap['photoNotFound']); + + const reviewModel = new ReviewModel(); + const reviewCount = await reviewModel.getReviewCountByPhoto(id); + console.log(reviewCount); + this.logger.debug('got a field', { photo }); + + const result = { + ...photo, + url: `https://${assetDomain}.com${photo.path}`, + usefulCount: reviewCount.usefulCount, + uselessCount: reviewCount.uselessCount, + netUsefulCount: reviewCount.netUsefulCount, + }; + return result; + } } module.exports = PhotoService; diff --git a/src/services/review-service.js b/src/services/review-service.js new file mode 100644 index 0000000..99abddb --- /dev/null +++ b/src/services/review-service.js @@ -0,0 +1,24 @@ +const BaseService = require('./base'); +const ReviewModel = require('../models/review/review'); +const PhotoModel = require('../models/photo'); + +const GeneralError = require('../errors/error/general-error'); +const getPhotoErrorMap = require('../errors/get-photo-error'); + +class ReviewService extends BaseService { + async createReview(userId, photoId, useful) { + // check space exist + const photoModel = new PhotoModel(); + const photoCheck = await photoModel.getPhoto(photoId); + if (!photoCheck) throw new GeneralError(getPhotoErrorMap['photoNotFound']); + + const reviewModel = new ReviewModel(); + const newReview = await reviewModel.createReview(userId, photoId, useful); + + this.logger.debug('got a field', { newReview }); + + return newReview; + } +} + +module.exports = ReviewService; diff --git a/src/services/space-service.js b/src/services/space-service.js deleted file mode 100644 index 98ccc2a..0000000 --- a/src/services/space-service.js +++ /dev/null @@ -1,21 +0,0 @@ -const { isNil } = require('ramda'); -const BaseService = require('./base'); -const SpaceModel = require('../models/space'); -const GeneralError = require('../errors/error/general-error'); -const getDataErrorMap = require('../errors/get-data-error'); - -class SpaceService extends BaseService { - async getSpace(id) { - const zoneModel = new SpaceModel(); - - const space = await zoneModel.getSpace(id); - - if (isNil(space)) throw new GeneralError(getDataErrorMap['spaceNotFound']); - - this.logger.debug('got a space', { space }); - - return space; - } -} - -module.exports = SpaceService; diff --git a/src/services/space-service/constant.js b/src/services/space-service/constant.js index 66c0440..1214d96 100644 --- a/src/services/space-service/constant.js +++ b/src/services/space-service/constant.js @@ -2,5 +2,9 @@ const sortMap = { useful: 'useful', date: 'date', }; +const orderMap = { + desc: 'desc', + asc: 'asc', +}; -module.exports = { sortMap }; +module.exports = { sortMap, orderMap }; diff --git a/src/services/space-service/index.js b/src/services/space-service/index.js index 9eecf8b..2ee6270 100644 --- a/src/services/space-service/index.js +++ b/src/services/space-service/index.js @@ -1,10 +1,12 @@ const { isNil } = require('ramda'); +const R = require('ramda'); const BaseService = require('../base'); const SpaceModel = require('../../models/space'); const PhotoModel = require('../../models/photo'); const GeneralError = require('../../errors/error/general-error'); const getDataErrorMap = require('../../errors/get-data-error'); -const { sortMap } = require('./constant'); +const { sortMap, orderMap } = require('./constant'); +const { uselessLimit, assetDomain } = require('../../config/config'); class SpaceService extends BaseService { async getSpace(id) { @@ -20,20 +22,59 @@ class SpaceService extends BaseService { } async getPhotosBySpace(id, sort, order) { const photoModel = new PhotoModel(); - let photos = ''; - if (sort === sortMap.useful) { - photos = await photoModel.getPhotosBySpaceOrderByUseful(id, sort, order); - } else { - photos = await photoModel.getPhotosBySpaceOrderByCreateAt( - id, - sort, - order + + // get photos by space which has review + const photosWithReviewCount = await photoModel.getPhotosReviewCountBySpace( + id + ); + + // get photos by space + const photos = await photoModel.getPhotosBySpace(id, order); + + // combine above two data + let photosData = await Promise.all( + photos.map(async (photo) => { + for (let i = 0; i < photosWithReviewCount.length; i++) { + if (photosWithReviewCount[i].photoId === photo.id) { + console.log('catch'); + photo = { + ...photo, + url: `https://${assetDomain}.com${photo.path}`, + usefulCount: photosWithReviewCount[i].usefulCount, + uselessCount: photosWithReviewCount[i].uselessCount, + netUsefulCount: photosWithReviewCount[i].netUsefulCount, + }; + photo = R.omit(['path'], photo); + } + } + return photo; + }) + ); + + // sort and order condition + if (sort === sortMap.useful && order === orderMap.desc) { + photosData = photosData.sort( + (a, b) => b.netUsefulCount - a.netUsefulCount ); - } - this.logger.debug('got photos', { photos }); + // delete useless data + for (let i = photosData.length - 1; i >= 0; i--) { + if (photosData[i].netUsefulCount < uselessLimit.limit) + photosData.splice(i, 1); + } + } + if (sort === sortMap.useful && order === orderMap.asc) { + photosData = photosData.sort( + (a, b) => a.netUsefulCount - b.netUsefulCount + ); - return photos; + // delete useless data + for (let i = photosData.length - 1; i >= 0; i--) { + if (photosData[i].netUsefulCount < uselessLimit.limit) + photosData.splice(i, 1); + } + } + return photosData; } } From d5631dc04f552badff7e81ea46fc430e9e2cbd41 Mon Sep 17 00:00:00 2001 From: ronnychiang Date: Fri, 24 Jun 2022 13:04:19 +0800 Subject: [PATCH 06/12] feat: otherPhoto --- src/controllers/photo-controller.js | 7 ++- src/models/photo.js | 24 +++++++++ src/models/review/review.js | 1 - src/services/photo-service.js | 24 ++++++--- src/services/space-service/index.js | 82 +++++++++++++++++++++++++++-- 5 files changed, 125 insertions(+), 13 deletions(-) diff --git a/src/controllers/photo-controller.js b/src/controllers/photo-controller.js index 18f13f4..e3e8fc2 100644 --- a/src/controllers/photo-controller.js +++ b/src/controllers/photo-controller.js @@ -25,11 +25,14 @@ const photoController = { getPhotosPhotos: async (req, res, next) => { try { const photoId = req.query.start_photo; - console.log(photoId); const photoService = new PhotoService({ logger: req.logger }); const startPhoto = await photoService.getPhoto(photoId); const spaceService = new SpaceService({ logger: req.logger }); - const photos = await spaceService.getPhotosBySpace(startPhoto.spaceId); + const photos = await spaceService.getOtherPhotosBySpace( + startPhoto.spaceId, + startPhoto.id + ); + photos.unshift(startPhoto); res.status(200).json(resSuccess(photos)); } catch (err) { next(err); diff --git a/src/models/photo.js b/src/models/photo.js index 4759b4a..0b65565 100644 --- a/src/models/photo.js +++ b/src/models/photo.js @@ -55,6 +55,30 @@ class PhotoModel { }); return photos; } + async getOtherPhotosBySpace(spaceId, photoId) { + const photos = await prisma.photos.findMany({ + where: { + id: { not: photoId }, + spaceId: Number(spaceId), + }, + select: { + id: true, + user: { + select: { + id: true, + name: true, + }, + }, + spaceId: true, + date: true, + path: true, + }, + orderBy: { + date: 'desc', + }, + }); + return photos; + } async getPhoto(id) { const photo = await prisma.photos.findUnique({ where: { diff --git a/src/models/review/review.js b/src/models/review/review.js index ecb8657..9ed1e7d 100644 --- a/src/models/review/review.js +++ b/src/models/review/review.js @@ -20,7 +20,6 @@ class ReviewModel { return newReview; } async getReviewCountByPhoto(photoId) { - console.log('go'); const photoWithReviewCount = await prisma.$queryRaw`SELECT Reviews.photoId, COUNT(if(Reviews.useful=${usefulMap.up},true,null)) AS usefulCount, diff --git a/src/services/photo-service.js b/src/services/photo-service.js index d99fd75..d79ca05 100644 --- a/src/services/photo-service.js +++ b/src/services/photo-service.js @@ -1,4 +1,5 @@ const R = require('ramda'); +const { isEmpty } = require('ramda'); const BaseService = require('./base'); const PhotoModel = require('../models/photo'); const SpaceModel = require('../models/space'); @@ -70,17 +71,26 @@ class PhotoService extends BaseService { if (!photo) throw new GeneralError(getPhotoErrorMap['photoNotFound']); - const reviewModel = new ReviewModel(); - const reviewCount = await reviewModel.getReviewCountByPhoto(id); - console.log(reviewCount); this.logger.debug('got a field', { photo }); + const reviewModel = new ReviewModel(); + const reviewCount = await reviewModel.getReviewCountByPhoto(id); + if (isEmpty(reviewCount)) { + const result = { + ...photo, + url: `https://${assetDomain}${photo.path}`, + usefulCount: 0, + uselessCount: 0, + netUsefulCount: 0, + }; + return result; + } const result = { ...photo, - url: `https://${assetDomain}.com${photo.path}`, - usefulCount: reviewCount.usefulCount, - uselessCount: reviewCount.uselessCount, - netUsefulCount: reviewCount.netUsefulCount, + url: `https://${assetDomain}${photo.path}`, + usefulCount: reviewCount[0].usefulCount, + uselessCount: reviewCount[0].uselessCount, + netUsefulCount: reviewCount[0].netUsefulCount, }; return result; } diff --git a/src/services/space-service/index.js b/src/services/space-service/index.js index 2ee6270..a45bde2 100644 --- a/src/services/space-service/index.js +++ b/src/services/space-service/index.js @@ -34,19 +34,95 @@ class SpaceService extends BaseService { // combine above two data let photosData = await Promise.all( photos.map(async (photo) => { + let mappingResult = false; for (let i = 0; i < photosWithReviewCount.length; i++) { if (photosWithReviewCount[i].photoId === photo.id) { - console.log('catch'); photo = { ...photo, - url: `https://${assetDomain}.com${photo.path}`, + url: `https://${assetDomain}${photo.path}`, usefulCount: photosWithReviewCount[i].usefulCount, uselessCount: photosWithReviewCount[i].uselessCount, netUsefulCount: photosWithReviewCount[i].netUsefulCount, }; - photo = R.omit(['path'], photo); + mappingResult = true; } } + if (!mappingResult) { + photo = { + ...photo, + url: `https://${assetDomain}${photo.path}`, + usefulCount: 0, + uselessCount: 0, + netUsefulCount: 0, + }; + } + photo = R.omit(['path'], photo); + return photo; + }) + ); + + // sort and order condition + if (sort === sortMap.useful && order === orderMap.desc) { + photosData = photosData.sort( + (a, b) => b.netUsefulCount - a.netUsefulCount + ); + + // delete useless data + for (let i = photosData.length - 1; i >= 0; i--) { + if (photosData[i].netUsefulCount < uselessLimit.limit) + photosData.splice(i, 1); + } + } + if (sort === sortMap.useful && order === orderMap.asc) { + photosData = photosData.sort( + (a, b) => a.netUsefulCount - b.netUsefulCount + ); + + // delete useless data + for (let i = photosData.length - 1; i >= 0; i--) { + if (photosData[i].netUsefulCount < uselessLimit.limit) + photosData.splice(i, 1); + } + } + return photosData; + } + async getOtherPhotosBySpace(spaceId, photoId, sort, order) { + const photoModel = new PhotoModel(); + + // get photos by space which has review + const photosWithReviewCount = await photoModel.getPhotosReviewCountBySpace( + spaceId + ); + + // get photos by space + const photos = await photoModel.getOtherPhotosBySpace(spaceId, photoId); + + // combine above two data + let photosData = await Promise.all( + photos.map(async (photo) => { + let mappingResult = false; + for (let i = 0; i < photosWithReviewCount.length; i++) { + if (photosWithReviewCount[i].photoId === photo.id) { + photo = { + ...photo, + url: `https://${assetDomain}${photo.path}`, + usefulCount: photosWithReviewCount[i].usefulCount, + uselessCount: photosWithReviewCount[i].uselessCount, + netUsefulCount: photosWithReviewCount[i].netUsefulCount, + }; + mappingResult = true; + } + } + if (!mappingResult) { + photo = { + ...photo, + url: `https://${assetDomain}${photo.path}`, + usefulCount: 0, + uselessCount: 0, + netUsefulCount: 0, + }; + } + photo = R.omit(['path'], photo); return photo; }) ); From d00f78b02f4eafbc49ef841a35a965ad1fc6b994 Mon Sep 17 00:00:00 2001 From: ronnychiang Date: Thu, 30 Jun 2022 14:52:14 +0800 Subject: [PATCH 07/12] try to review --- .../20220630025936_init/migration.sql | 2 + prisma/schema.prisma | 2 +- src/controllers/photo-controller.js | 18 +++++++++ src/controllers/review-controller.js | 38 ------------------- src/models/review/{review.js => index.js} | 16 +++++++- src/routes/modules/photo.js | 3 +- src/services/photo-service.js | 2 +- src/services/review-service.js | 6 +-- 8 files changed, 41 insertions(+), 46 deletions(-) create mode 100644 prisma/migrations/20220630025936_init/migration.sql rename src/models/review/{review.js => index.js} (72%) diff --git a/prisma/migrations/20220630025936_init/migration.sql b/prisma/migrations/20220630025936_init/migration.sql new file mode 100644 index 0000000..aa42bb1 --- /dev/null +++ b/prisma/migrations/20220630025936_init/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE `Reviews` MODIFY `useful` VARCHAR(255) NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d992d09..418031f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -191,7 +191,7 @@ model Reviews { userId Int photo Photos @relation(fields: [photoId], references: [id]) photoId Int - useful String @db.VarChar(255) + useful String? @db.VarChar(255) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([photoId, useful]) diff --git a/src/controllers/photo-controller.js b/src/controllers/photo-controller.js index e3e8fc2..a873fdc 100644 --- a/src/controllers/photo-controller.js +++ b/src/controllers/photo-controller.js @@ -2,6 +2,7 @@ const PhotoService = require('../services/photo-service'); const resSuccess = require('./helpers/response'); const getUser = require('./helpers/get-user'); const SpaceService = require('../services/space-service'); +const ReviewService = require('../services/review-service'); const photoController = { postPhotos: async (req, res, next) => { @@ -38,6 +39,23 @@ const photoController = { next(err); } }, + postReview: async (req, res, next) => { + try { + const { useful } = req.body; + const userId = getUser(req).id; + const photoId = req.params.id; + + const reviewService = new ReviewService({ logger: req.logger }); + const uploadInfo = await reviewService.postReview( + userId, + photoId, + useful + ); + res.status(200).json(resSuccess(uploadInfo)); + } catch (err) { + next(err); + } + }, }; module.exports = photoController; diff --git a/src/controllers/review-controller.js b/src/controllers/review-controller.js index 0ea213d..e69de29 100644 --- a/src/controllers/review-controller.js +++ b/src/controllers/review-controller.js @@ -1,38 +0,0 @@ -const ReviewService = require('../services/review-service'); -const resSuccess = require('./helpers/response'); -const getUser = require('./helpers/get-user'); - -const reviewController = { - postPhotos: async (req, res, next) => { - try { - const { spaceId, date } = req.body; - const userId = getUser(req).id; - - const photoService = new ReviewService({ logger: req.logger }); - const uploadInfo = await photoService.postPhotos( - spaceId, - req.files, - req.requestId, - userId, - date - ); - res.status(200).json(resSuccess(uploadInfo)); - } catch (err) { - next(err); - } - }, - getPhotosPhotos: async (req, res, next) => { - try { - const id = req.params.id; - const sort = req.query.sort; - const order = req.query.order; - const spaceService = new ReviewService({ logger: req.logger }); - const photos = await spaceService.getPhotosBySpace(id, sort, order); - res.status(200).json(resSuccess(photos)); - } catch (err) { - next(err); - } - }, -}; - -module.exports = reviewController; diff --git a/src/models/review/review.js b/src/models/review/index.js similarity index 72% rename from src/models/review/review.js rename to src/models/review/index.js index 9ed1e7d..5f48fe4 100644 --- a/src/models/review/review.js +++ b/src/models/review/index.js @@ -1,5 +1,5 @@ const prisma = require('../../config/prisma'); -const { usefulMap } = require('../../models/review/constant'); +const { usefulMap } = require('./constant'); class ReviewModel { constructor() {} @@ -31,6 +31,20 @@ class ReviewModel { return photoWithReviewCount; } + async updateOrCreateReview(userId, photoId, useful) { + const review = await prisma.reviews.upsert({ + where: { userId, photoId, useful: null }, + update: { useful }, + create: { userId, photoId, useful }, + select: { + id: true, + userId: true, + photoId: true, + useful: true, + }, + }); + return review; + } async _truncate() { await prisma.reviews.deleteMany({}); } diff --git a/src/routes/modules/photo.js b/src/routes/modules/photo.js index 48ace12..e70af02 100644 --- a/src/routes/modules/photo.js +++ b/src/routes/modules/photo.js @@ -1,12 +1,11 @@ const express = require('express'); const photoController = require('../../controllers/photo-controller'); -const reviewController = require('../../controllers/review-controller'); const { uploadImages } = require('../../middleware/upload-images'); const { authenticated } = require('../../middleware/auth'); const router = express.Router(); -router.post('/:id/reviews', authenticated, reviewController.postPhotos); +router.post('/:id/reviews', authenticated, photoController.postReview); router.get('/', photoController.getPhotosPhotos); router.post('/', authenticated, uploadImages, photoController.postPhotos); diff --git a/src/services/photo-service.js b/src/services/photo-service.js index d92dc88..a5f6d05 100644 --- a/src/services/photo-service.js +++ b/src/services/photo-service.js @@ -3,7 +3,7 @@ const { isEmpty } = require('ramda'); const BaseService = require('./base'); const PhotoModel = require('../models/photo'); const SpaceModel = require('../models/space'); -const ReviewModel = require('../models/review/review'); +const ReviewModel = require('../models/review/index'); const PrivateError = require('../errors/error/private-error'); const GeneralError = require('../errors/error/general-error'); const postPhotoErrorMap = require('../errors/post-photo-error'); diff --git a/src/services/review-service.js b/src/services/review-service.js index 99abddb..494f2e8 100644 --- a/src/services/review-service.js +++ b/src/services/review-service.js @@ -1,12 +1,12 @@ const BaseService = require('./base'); -const ReviewModel = require('../models/review/review'); +const ReviewModel = require('../models/review/index'); const PhotoModel = require('../models/photo'); const GeneralError = require('../errors/error/general-error'); const getPhotoErrorMap = require('../errors/get-photo-error'); class ReviewService extends BaseService { - async createReview(userId, photoId, useful) { + async postReview(userId, photoId, useful) { // check space exist const photoModel = new PhotoModel(); const photoCheck = await photoModel.getPhoto(photoId); @@ -15,7 +15,7 @@ class ReviewService extends BaseService { const reviewModel = new ReviewModel(); const newReview = await reviewModel.createReview(userId, photoId, useful); - this.logger.debug('got a field', { newReview }); + this.logger.debug('create a review', { newReview }); return newReview; } From 43faabb95663a3fa0aa9166a96e1b587dfe53f59 Mon Sep 17 00:00:00 2001 From: ronnychiang Date: Fri, 1 Jul 2022 19:36:05 +0800 Subject: [PATCH 08/12] fix --- src/controllers/photo-controller.js | 23 +++++------------------ src/controllers/review-controller.js | 0 src/controllers/space-controller.js | 2 ++ src/models/review/index.js | 14 -------------- src/routes/modules/photo.js | 1 - src/services/photo-service.test.js | 2 +- src/services/space-service/index.js | 25 +------------------------ 7 files changed, 9 insertions(+), 58 deletions(-) delete mode 100644 src/controllers/review-controller.js diff --git a/src/controllers/photo-controller.js b/src/controllers/photo-controller.js index a873fdc..688e5cd 100644 --- a/src/controllers/photo-controller.js +++ b/src/controllers/photo-controller.js @@ -2,7 +2,6 @@ const PhotoService = require('../services/photo-service'); const resSuccess = require('./helpers/response'); const getUser = require('./helpers/get-user'); const SpaceService = require('../services/space-service'); -const ReviewService = require('../services/review-service'); const photoController = { postPhotos: async (req, res, next) => { @@ -26,36 +25,24 @@ const photoController = { getPhotosPhotos: async (req, res, next) => { try { const photoId = req.query.start_photo; + + // get target photo const photoService = new PhotoService({ logger: req.logger }); const startPhoto = await photoService.getPhoto(photoId); + + // get other photos const spaceService = new SpaceService({ logger: req.logger }); const photos = await spaceService.getOtherPhotosBySpace( startPhoto.spaceId, startPhoto.id ); + photos.unshift(startPhoto); res.status(200).json(resSuccess(photos)); } catch (err) { next(err); } }, - postReview: async (req, res, next) => { - try { - const { useful } = req.body; - const userId = getUser(req).id; - const photoId = req.params.id; - - const reviewService = new ReviewService({ logger: req.logger }); - const uploadInfo = await reviewService.postReview( - userId, - photoId, - useful - ); - res.status(200).json(resSuccess(uploadInfo)); - } catch (err) { - next(err); - } - }, }; module.exports = photoController; diff --git a/src/controllers/review-controller.js b/src/controllers/review-controller.js deleted file mode 100644 index e69de29..0000000 diff --git a/src/controllers/space-controller.js b/src/controllers/space-controller.js index c49b68d..4854dc7 100644 --- a/src/controllers/space-controller.js +++ b/src/controllers/space-controller.js @@ -17,8 +17,10 @@ const spaceController = { const id = req.params.id; const sort = req.query.sort; const order = req.query.order; + const spaceService = new SpaceService({ logger: req.logger }); const photos = await spaceService.getPhotosBySpace(id, sort, order); + res.status(200).json(resSuccess(photos)); } catch (err) { next(err); diff --git a/src/models/review/index.js b/src/models/review/index.js index 5f48fe4..749520d 100644 --- a/src/models/review/index.js +++ b/src/models/review/index.js @@ -31,20 +31,6 @@ class ReviewModel { return photoWithReviewCount; } - async updateOrCreateReview(userId, photoId, useful) { - const review = await prisma.reviews.upsert({ - where: { userId, photoId, useful: null }, - update: { useful }, - create: { userId, photoId, useful }, - select: { - id: true, - userId: true, - photoId: true, - useful: true, - }, - }); - return review; - } async _truncate() { await prisma.reviews.deleteMany({}); } diff --git a/src/routes/modules/photo.js b/src/routes/modules/photo.js index e70af02..16e75bb 100644 --- a/src/routes/modules/photo.js +++ b/src/routes/modules/photo.js @@ -5,7 +5,6 @@ const { authenticated } = require('../../middleware/auth'); const router = express.Router(); -router.post('/:id/reviews', authenticated, photoController.postReview); router.get('/', photoController.getPhotosPhotos); router.post('/', authenticated, uploadImages, photoController.postPhotos); diff --git a/src/services/photo-service.test.js b/src/services/photo-service.test.js index 0746c12..3295ae0 100644 --- a/src/services/photo-service.test.js +++ b/src/services/photo-service.test.js @@ -24,8 +24,8 @@ afterEach(async () => { const levelModel = new LevelModel(); const orientationModel = new OrientationModel(); const zoneModel = new ZoneModel(); - const spaceModel = new SpaceModel(); const seatModel = new SeatModel(); + const spaceModel = new SpaceModel(); await photoModel._truncate(); await seatModel._truncate(); await spaceModel._truncate(); diff --git a/src/services/space-service/index.js b/src/services/space-service/index.js index a45bde2..e3ddfbd 100644 --- a/src/services/space-service/index.js +++ b/src/services/space-service/index.js @@ -86,7 +86,7 @@ class SpaceService extends BaseService { } return photosData; } - async getOtherPhotosBySpace(spaceId, photoId, sort, order) { + async getOtherPhotosBySpace(spaceId, photoId) { const photoModel = new PhotoModel(); // get photos by space which has review @@ -127,29 +127,6 @@ class SpaceService extends BaseService { }) ); - // sort and order condition - if (sort === sortMap.useful && order === orderMap.desc) { - photosData = photosData.sort( - (a, b) => b.netUsefulCount - a.netUsefulCount - ); - - // delete useless data - for (let i = photosData.length - 1; i >= 0; i--) { - if (photosData[i].netUsefulCount < uselessLimit.limit) - photosData.splice(i, 1); - } - } - if (sort === sortMap.useful && order === orderMap.asc) { - photosData = photosData.sort( - (a, b) => a.netUsefulCount - b.netUsefulCount - ); - - // delete useless data - for (let i = photosData.length - 1; i >= 0; i--) { - if (photosData[i].netUsefulCount < uselessLimit.limit) - photosData.splice(i, 1); - } - } return photosData; } } From e3972ce734a1fa5043922ea8d824544b2675017b Mon Sep 17 00:00:00 2001 From: ronnychiang Date: Fri, 1 Jul 2022 20:55:39 +0800 Subject: [PATCH 09/12] test --- prisma/migrations/20220630025936_init/migration.sql | 2 -- .../migration.sql | 3 +++ 2 files changed, 3 insertions(+), 2 deletions(-) delete mode 100644 prisma/migrations/20220630025936_init/migration.sql rename prisma/migrations/{20220621094639_unique_reviews => 20220701125508_fix_review}/migration.sql (79%) diff --git a/prisma/migrations/20220630025936_init/migration.sql b/prisma/migrations/20220630025936_init/migration.sql deleted file mode 100644 index aa42bb1..0000000 --- a/prisma/migrations/20220630025936_init/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE `Reviews` MODIFY `useful` VARCHAR(255) NULL; diff --git a/prisma/migrations/20220621094639_unique_reviews/migration.sql b/prisma/migrations/20220701125508_fix_review/migration.sql similarity index 79% rename from prisma/migrations/20220621094639_unique_reviews/migration.sql rename to prisma/migrations/20220701125508_fix_review/migration.sql index 7b11431..b0a6985 100644 --- a/prisma/migrations/20220621094639_unique_reviews/migration.sql +++ b/prisma/migrations/20220701125508_fix_review/migration.sql @@ -4,5 +4,8 @@ - A unique constraint covering the columns `[userId,photoId]` on the table `Reviews` will be added. If there are existing duplicate values, this will fail. */ +-- AlterTable +ALTER TABLE `Reviews` MODIFY `useful` VARCHAR(255) NULL; + -- CreateIndex CREATE UNIQUE INDEX `Reviews_userId_photoId_key` ON `Reviews`(`userId`, `photoId`); From 2cf27c34e376fa6a5fb357510618cf64c4cbe605 Mon Sep 17 00:00:00 2001 From: ronnychiang Date: Sat, 2 Jul 2022 20:53:36 +0800 Subject: [PATCH 10/12] feat: getPhoto test --- src/errors/get-photo-error.js | 5 + src/models/photo.js | 2 +- src/services/photo-service.js | 29 +- src/services/photo-service.test.js | 134 ++++++++ src/services/review-service.js | 24 -- src/services/space-service/index.js | 56 ++- .../space-service/space-service.test.js | 319 ++++++++++++++++++ 7 files changed, 523 insertions(+), 46 deletions(-) delete mode 100644 src/services/review-service.js create mode 100644 src/services/space-service/space-service.test.js diff --git a/src/errors/get-photo-error.js b/src/errors/get-photo-error.js index 5c1a118..b990023 100644 --- a/src/errors/get-photo-error.js +++ b/src/errors/get-photo-error.js @@ -4,6 +4,11 @@ const getPhotoErrorMap = { code: 'gp001', httpCode: 404, }, + spaceNotFound: { + message: '此空間並不存在', + code: 'gp002', + httpCode: 404, + }, }; module.exports = getPhotoErrorMap; diff --git a/src/models/photo.js b/src/models/photo.js index 0b65565..4f5bc3a 100644 --- a/src/models/photo.js +++ b/src/models/photo.js @@ -100,7 +100,7 @@ class PhotoModel { return photo; } async _truncate() { - await prisma.spaces.deleteMany({}); + await prisma.photos.deleteMany({}); } } diff --git a/src/services/photo-service.js b/src/services/photo-service.js index a5f6d05..aa018f3 100644 --- a/src/services/photo-service.js +++ b/src/services/photo-service.js @@ -87,28 +87,39 @@ class PhotoService extends BaseService { if (!photo) throw new GeneralError(getPhotoErrorMap['photoNotFound']); - this.logger.debug('got a field', { photo }); + this.logger.debug('got a photo', { photo }); + + const dataset = renderDataset(sizeMap.seatPhoto)({ + path: photo.path, + bucketName: bucketMap.photos, + assetDomain, + }); + + const data = { + ...photo, + dataset, + }; + + const result = R.omit(['path'], data); const reviewModel = new ReviewModel(); const reviewCount = await reviewModel.getReviewCountByPhoto(id); if (isEmpty(reviewCount)) { - const result = { - ...photo, - url: `https://${assetDomain}${photo.path}`, + const photoInfo = { + ...result, usefulCount: 0, uselessCount: 0, netUsefulCount: 0, }; - return result; + return photoInfo; } - const result = { - ...photo, - url: `https://${assetDomain}${photo.path}`, + const photoInfo = { + ...result, usefulCount: reviewCount[0].usefulCount, uselessCount: reviewCount[0].uselessCount, netUsefulCount: reviewCount[0].netUsefulCount, }; - return result; + return photoInfo; } } diff --git a/src/services/photo-service.test.js b/src/services/photo-service.test.js index 3295ae0..eb4b806 100644 --- a/src/services/photo-service.test.js +++ b/src/services/photo-service.test.js @@ -10,6 +10,7 @@ const SeatModel = require('../models/seat'); const UserModel = require('../models/user'); const UserService = require('./user-service'); const postPhotoErrorMap = require('../errors/post-photo-error'); +const getPhotoErrorMap = require('../errors/get-photo-error'); const { resizeImages } = require('../utils/upload-image/resize'); const { uploadS3 } = require('../utils/upload-image/uploadS3'); const { randomHashName } = require('../utils/upload-image/random-hash-name'); @@ -336,3 +337,136 @@ describe('photo-service.postPhoto', () => { }); }); }); + +describe('photo-service.getPhoto', () => { + describe('with regular input', () => { + it('should return a photo data', async () => { + // create space + const fieldModel = new FieldModel(); + const levelModel = new LevelModel(); + const orientationModel = new OrientationModel(); + const zoneModel = new ZoneModel(); + const spaceModel = new SpaceModel(); + + const newField = await fieldModel.createField('testField', ''); + const newLevel = await levelModel.createLevel('testLevel'); + const newOrientation = await orientationModel.createOrientation( + 'testOrientation' + ); + const newZone = await zoneModel.createZone( + newField.id, + newOrientation.id, + newLevel.id, + 'testZone' + ); + const correctSpace = await spaceModel.createSpace( + newZone.id, + 'seat', + 'testVersion', + 1, + 1, + 'rightSeat', + 1, + 1 + ); + + // create and verify user + const userService = new UserService({ + logger: console, + }); + const email = 'example@example.com'; + const newUser = await userService.signUp('user1', email, 'password1'); + await userService.verifyEmail(newUser.verificationToken); + + // create test photo data + const path = 'testPhotoPath'; + const userId = newUser.id; + const spaceId = correctSpace.id; + const dateTime = new Date(); + const photoModel = new PhotoModel(); + const newPhoto = await photoModel.createPhoto( + path, + userId, + spaceId, + dateTime + ); + + const expectedResult = { + id: newPhoto.id, + user: { id: newUser.id, name: newUser.name }, + spaceId, + }; + + // getPhoto + const photo = await photoService.getPhoto(newPhoto.id); + console.log(photo); + expect(photo).toMatchObject(expectedResult); + expect(photo).toHaveProperty('netUsefulCount'); + expect(photo).toHaveProperty('dataset'); + }); + }); + describe('with not exist photoId', () => { + it('should return photoNotFound error', async () => { + // create space + const fieldModel = new FieldModel(); + const levelModel = new LevelModel(); + const orientationModel = new OrientationModel(); + const zoneModel = new ZoneModel(); + const spaceModel = new SpaceModel(); + + const newField = await fieldModel.createField('testField', ''); + const newLevel = await levelModel.createLevel('testLevel'); + const newOrientation = await orientationModel.createOrientation( + 'testOrientation' + ); + const newZone = await zoneModel.createZone( + newField.id, + newOrientation.id, + newLevel.id, + 'testZone' + ); + const correctSpace = await spaceModel.createSpace( + newZone.id, + 'seat', + 'testVersion', + 1, + 1, + 'rightSeat', + 1, + 1 + ); + + // create and verify user + const userService = new UserService({ + logger: console, + }); + const email = 'example@example.com'; + const newUser = await userService.signUp('user1', email, 'password1'); + await userService.verifyEmail(newUser.verificationToken); + + // create test photo data + const path = 'testPhotoPath'; + const userId = newUser.id; + const spaceId = correctSpace.id; + const dateTime = new Date(); + const photoModel = new PhotoModel(); + const newPhoto = await photoModel.createPhoto( + path, + userId, + spaceId, + dateTime + ); + + // get Photo with not exist photoId + const wrongPhotoId = Number(newPhoto.id) + 999; + assert.rejects( + async () => { + await photoService.getPhoto(wrongPhotoId); + }, + { + code: getPhotoErrorMap.photoNotFound.code, + } + ); + }); + }); +}); diff --git a/src/services/review-service.js b/src/services/review-service.js deleted file mode 100644 index 494f2e8..0000000 --- a/src/services/review-service.js +++ /dev/null @@ -1,24 +0,0 @@ -const BaseService = require('./base'); -const ReviewModel = require('../models/review/index'); -const PhotoModel = require('../models/photo'); - -const GeneralError = require('../errors/error/general-error'); -const getPhotoErrorMap = require('../errors/get-photo-error'); - -class ReviewService extends BaseService { - async postReview(userId, photoId, useful) { - // check space exist - const photoModel = new PhotoModel(); - const photoCheck = await photoModel.getPhoto(photoId); - if (!photoCheck) throw new GeneralError(getPhotoErrorMap['photoNotFound']); - - const reviewModel = new ReviewModel(); - const newReview = await reviewModel.createReview(userId, photoId, useful); - - this.logger.debug('create a review', { newReview }); - - return newReview; - } -} - -module.exports = ReviewService; diff --git a/src/services/space-service/index.js b/src/services/space-service/index.js index e3ddfbd..0375080 100644 --- a/src/services/space-service/index.js +++ b/src/services/space-service/index.js @@ -5,8 +5,12 @@ const SpaceModel = require('../../models/space'); const PhotoModel = require('../../models/photo'); const GeneralError = require('../../errors/error/general-error'); const getDataErrorMap = require('../../errors/get-data-error'); +const getPhotoErrorMap = require('../../errors/get-photo-error'); const { sortMap, orderMap } = require('./constant'); +const { bucketMap } = require('../../constants/upload-constant'); +const { sizeMap } = require('../../constants/resize-constant'); const { uselessLimit, assetDomain } = require('../../config/config'); +const { renderDataset } = require('../../utils/upload-image/responsive'); class SpaceService extends BaseService { async getSpace(id) { @@ -21,9 +25,13 @@ class SpaceService extends BaseService { return space; } async getPhotosBySpace(id, sort, order) { - const photoModel = new PhotoModel(); + // check space exist + const spaceModel = new SpaceModel(); + const space = await spaceModel.getSpace(id); + if (isNil(space)) throw new GeneralError(getPhotoErrorMap['spaceNotFound']); // get photos by space which has review + const photoModel = new PhotoModel(); const photosWithReviewCount = await photoModel.getPhotosReviewCountBySpace( id ); @@ -34,12 +42,24 @@ class SpaceService extends BaseService { // combine above two data let photosData = await Promise.all( photos.map(async (photo) => { + const dataset = renderDataset(sizeMap.seatPhoto)({ + path: photo.path, + bucketName: bucketMap.photos, + assetDomain, + }); + + const data = { + ...photo, + dataset, + }; + + const result = R.omit(['path'], data); + let mappingResult = false; for (let i = 0; i < photosWithReviewCount.length; i++) { if (photosWithReviewCount[i].photoId === photo.id) { photo = { - ...photo, - url: `https://${assetDomain}${photo.path}`, + ...result, usefulCount: photosWithReviewCount[i].usefulCount, uselessCount: photosWithReviewCount[i].uselessCount, netUsefulCount: photosWithReviewCount[i].netUsefulCount, @@ -49,14 +69,12 @@ class SpaceService extends BaseService { } if (!mappingResult) { photo = { - ...photo, - url: `https://${assetDomain}${photo.path}`, + ...result, usefulCount: 0, uselessCount: 0, netUsefulCount: 0, }; } - photo = R.omit(['path'], photo); return photo; }) ); @@ -87,9 +105,13 @@ class SpaceService extends BaseService { return photosData; } async getOtherPhotosBySpace(spaceId, photoId) { - const photoModel = new PhotoModel(); + // check space exist + const spaceModel = new SpaceModel(); + const space = await spaceModel.getSpace(spaceId); + if (isNil(space)) throw new GeneralError(getPhotoErrorMap['spaceNotFound']); // get photos by space which has review + const photoModel = new PhotoModel(); const photosWithReviewCount = await photoModel.getPhotosReviewCountBySpace( spaceId ); @@ -100,12 +122,24 @@ class SpaceService extends BaseService { // combine above two data let photosData = await Promise.all( photos.map(async (photo) => { + const dataset = renderDataset(sizeMap.seatPhoto)({ + path: photo.path, + bucketName: bucketMap.photos, + assetDomain, + }); + + const data = { + ...photo, + dataset, + }; + + const result = R.omit(['path'], data); + let mappingResult = false; for (let i = 0; i < photosWithReviewCount.length; i++) { if (photosWithReviewCount[i].photoId === photo.id) { photo = { - ...photo, - url: `https://${assetDomain}${photo.path}`, + ...result, usefulCount: photosWithReviewCount[i].usefulCount, uselessCount: photosWithReviewCount[i].uselessCount, netUsefulCount: photosWithReviewCount[i].netUsefulCount, @@ -115,14 +149,12 @@ class SpaceService extends BaseService { } if (!mappingResult) { photo = { - ...photo, - url: `https://${assetDomain}${photo.path}`, + ...result, usefulCount: 0, uselessCount: 0, netUsefulCount: 0, }; } - photo = R.omit(['path'], photo); return photo; }) ); diff --git a/src/services/space-service/space-service.test.js b/src/services/space-service/space-service.test.js new file mode 100644 index 0000000..096deab --- /dev/null +++ b/src/services/space-service/space-service.test.js @@ -0,0 +1,319 @@ +const assert = require('assert/strict'); +const UserModel = require('../../models/user'); +const FieldModel = require('../../models/field'); +const LevelModel = require('../../models/level'); +const OrientationModel = require('../../models/orientation'); +const ZoneModel = require('../../models/zone'); +const SeatModel = require('../../models/seat'); +const SpaceModel = require('../../models/space'); +const PhotoModel = require('../../models/photo'); +const getPhotoErrorMap = require('../../errors/get-photo-error'); +const { sortMap, orderMap } = require('./constant'); +const SpaceService = require('./index'); +const UserService = require('../user-service'); + +afterEach(async () => { + const userModel = new UserModel(); + const fieldModel = new FieldModel(); + const levelModel = new LevelModel(); + const orientationModel = new OrientationModel(); + const zoneModel = new ZoneModel(); + const spaceModel = new SpaceModel(); + const seatModel = new SeatModel(); + const photoModel = new PhotoModel(); + await photoModel._truncate(); + await seatModel._truncate(); + await spaceModel._truncate(); + await zoneModel._truncate(); + await orientationModel._truncate(); + await levelModel._truncate(); + await fieldModel._truncate(); + await userModel._truncate(); +}); + +const spaceService = new SpaceService({ + logger: console, +}); + +describe('space-service.getPhotosBySpace', () => { + describe('with regular input', () => { + it('should return photos order by date', async () => { + // create space + const fieldModel = new FieldModel(); + const levelModel = new LevelModel(); + const orientationModel = new OrientationModel(); + const zoneModel = new ZoneModel(); + const spaceModel = new SpaceModel(); + + const newField = await fieldModel.createField('testField', ''); + const newLevel = await levelModel.createLevel('testLevel'); + const newOrientation = await orientationModel.createOrientation( + 'testOrientation' + ); + const newZone = await zoneModel.createZone( + newField.id, + newOrientation.id, + newLevel.id, + 'testZone' + ); + const correctSpace = await spaceModel.createSpace( + newZone.id, + 'seat', + 'testVersion', + 1, + 1, + 'rightSeat', + 1, + 1 + ); + + // create and verify user + const userService = new UserService({ + logger: console, + }); + const email = 'example@example.com'; + const newUser = await userService.signUp('user1', email, 'password1'); + await userService.verifyEmail(newUser.verificationToken); + + // create test photo data + const path = 'testPhotoPath'; + const userId = newUser.id; + const spaceId = correctSpace.id; + const dateTime = new Date(); + const photoModel = new PhotoModel(); + await photoModel.createPhoto( + path, + userId, + spaceId, + new Date('2022-06-30') + ); + const latestPhoto = await photoModel.createPhoto( + `${path}1`, + userId, + spaceId, + dateTime + ); + const expectedResult = { + id: latestPhoto.id, + user: { id: newUser.id, name: newUser.name }, + spaceId, + }; + + // getSpacePhotos + const photos = await spaceService.getPhotosBySpace( + spaceId, + sortMap.date, + orderMap.desc + ); + expect(photos[0]).toMatchObject(expectedResult); + expect(photos[0]).toHaveProperty('netUsefulCount'); + expect(photos[0]).toHaveProperty('dataset'); + }); + }); + describe('with not existed spaceId', () => { + it('should return spaceNotFound error', async () => { + // create space + const fieldModel = new FieldModel(); + const levelModel = new LevelModel(); + const orientationModel = new OrientationModel(); + const zoneModel = new ZoneModel(); + const spaceModel = new SpaceModel(); + + const newField = await fieldModel.createField('testField', ''); + const newLevel = await levelModel.createLevel('testLevel'); + const newOrientation = await orientationModel.createOrientation( + 'testOrientation' + ); + const newZone = await zoneModel.createZone( + newField.id, + newOrientation.id, + newLevel.id, + 'testZone' + ); + const correctSpace = await spaceModel.createSpace( + newZone.id, + 'seat', + 'testVersion', + 1, + 1, + 'rightSeat', + 1, + 1 + ); + + // create and verify user + const userService = new UserService({ + logger: console, + }); + const email = 'example@example.com'; + const newUser = await userService.signUp('user1', email, 'password1'); + await userService.verifyEmail(newUser.verificationToken); + + // create test photo data + const path = 'testPhotoPath'; + const userId = newUser.id; + const spaceId = correctSpace.id; + const dateTime = new Date(); + const photoModel = new PhotoModel(); + await photoModel.createPhoto(path, userId, spaceId, dateTime); + + // getSpacePhotos + const wrongSpaceId = Number(spaceId) + 999; + assert.rejects( + async () => { + await spaceService.getPhotosBySpace( + wrongSpaceId, + sortMap.date, + orderMap.desc + ); + }, + { + code: getPhotoErrorMap.spaceNotFound.code, + } + ); + }); + }); +}); + +describe('space-service.getOtherPhotosBySpace', () => { + describe('with regular input', () => { + it('should return photos order by date', async () => { + // create space + const fieldModel = new FieldModel(); + const levelModel = new LevelModel(); + const orientationModel = new OrientationModel(); + const zoneModel = new ZoneModel(); + const spaceModel = new SpaceModel(); + + const newField = await fieldModel.createField('testField', ''); + const newLevel = await levelModel.createLevel('testLevel'); + const newOrientation = await orientationModel.createOrientation( + 'testOrientation' + ); + const newZone = await zoneModel.createZone( + newField.id, + newOrientation.id, + newLevel.id, + 'testZone' + ); + const correctSpace = await spaceModel.createSpace( + newZone.id, + 'seat', + 'testVersion', + 1, + 1, + 'rightSeat', + 1, + 1 + ); + + // create and verify user + const userService = new UserService({ + logger: console, + }); + const email = 'example@example.com'; + const newUser = await userService.signUp('user1', email, 'password1'); + await userService.verifyEmail(newUser.verificationToken); + + // create test photo data + const path = 'testPhotoPath'; + const userId = newUser.id; + const spaceId = correctSpace.id; + const dateTime = new Date(); + const photoModel = new PhotoModel(); + const excludePhoto = await photoModel.createPhoto( + path, + userId, + spaceId, + new Date('2021-06-30') + ); + const otherPhoto = await photoModel.createPhoto( + `${path}other`, + userId, + spaceId, + dateTime + ); + const expectedResult = { + id: otherPhoto.id, + user: { id: newUser.id, name: newUser.name }, + spaceId, + }; + + // getSpacePhotos + const photos = await spaceService.getOtherPhotosBySpace( + spaceId, + excludePhoto.id + ); + expect(photos[0]).toMatchObject(expectedResult); + expect(photos[0]).toHaveProperty('netUsefulCount'); + expect(photos[0]).toHaveProperty('dataset'); + }); + }); + describe('with not existed spaceId', () => { + it('should return spaceNotFound error', async () => { + // create space + const fieldModel = new FieldModel(); + const levelModel = new LevelModel(); + const orientationModel = new OrientationModel(); + const zoneModel = new ZoneModel(); + const spaceModel = new SpaceModel(); + + const newField = await fieldModel.createField('testField', ''); + const newLevel = await levelModel.createLevel('testLevel'); + const newOrientation = await orientationModel.createOrientation( + 'testOrientation' + ); + const newZone = await zoneModel.createZone( + newField.id, + newOrientation.id, + newLevel.id, + 'testZone' + ); + const correctSpace = await spaceModel.createSpace( + newZone.id, + 'seat', + 'testVersion', + 1, + 1, + 'rightSeat', + 1, + 1 + ); + + // create and verify user + const userService = new UserService({ + logger: console, + }); + const email = 'example@example.com'; + const newUser = await userService.signUp('user1', email, 'password1'); + await userService.verifyEmail(newUser.verificationToken); + + // create test photo data + const path = 'testPhotoPath'; + const userId = newUser.id; + const spaceId = correctSpace.id; + const dateTime = new Date(); + const photoModel = new PhotoModel(); + const excludePhoto = await photoModel.createPhoto( + path, + userId, + spaceId, + dateTime + ); + + // getSpacePhotos + const wrongSpaceId = Number(spaceId) + 999; + assert.rejects( + async () => { + await spaceService.getOtherPhotosBySpace( + wrongSpaceId, + excludePhoto.id + ); + }, + { + code: getPhotoErrorMap.spaceNotFound.code, + } + ); + }); + }); +}); From ddf9eba53618e74450646d3e17e276fb5fd669aa Mon Sep 17 00:00:00 2001 From: ronnychiang Date: Sat, 2 Jul 2022 23:13:15 +0800 Subject: [PATCH 11/12] fix: getPhotos --- src/controllers/photo-controller.js | 10 ++- src/errors/get-photo-error.js | 5 ++ src/models/photo.js | 34 ++++++++ src/routes/modules/photo.js | 2 +- src/services/photo-service.js | 54 ++++++++++++ src/services/photo-service.test.js | 122 ++++++++++++++++++++++++++-- 6 files changed, 220 insertions(+), 7 deletions(-) diff --git a/src/controllers/photo-controller.js b/src/controllers/photo-controller.js index 688e5cd..1c0b2e4 100644 --- a/src/controllers/photo-controller.js +++ b/src/controllers/photo-controller.js @@ -2,6 +2,7 @@ const PhotoService = require('../services/photo-service'); const resSuccess = require('./helpers/response'); const getUser = require('./helpers/get-user'); const SpaceService = require('../services/space-service'); +const { isNil } = require('ramda'); const photoController = { postPhotos: async (req, res, next) => { @@ -22,10 +23,17 @@ const photoController = { next(err); } }, - getPhotosPhotos: async (req, res, next) => { + getPhotos: async (req, res, next) => { try { const photoId = req.query.start_photo; + // if no query + if (isNil(photoId)) { + const photoService = new PhotoService({ logger: req.logger }); + const photos = await photoService.getPhotos(); + res.status(200).json(resSuccess(photos)); + } + // get target photo const photoService = new PhotoService({ logger: req.logger }); const startPhoto = await photoService.getPhoto(photoId); diff --git a/src/errors/get-photo-error.js b/src/errors/get-photo-error.js index b990023..d5d042b 100644 --- a/src/errors/get-photo-error.js +++ b/src/errors/get-photo-error.js @@ -9,6 +9,11 @@ const getPhotoErrorMap = { code: 'gp002', httpCode: 404, }, + photosNotFound: { + message: '沒有任何照片', + code: 'gp003', + httpCode: 404, + }, }; module.exports = getPhotoErrorMap; diff --git a/src/models/photo.js b/src/models/photo.js index 4f5bc3a..a585311 100644 --- a/src/models/photo.js +++ b/src/models/photo.js @@ -99,6 +99,40 @@ class PhotoModel { }); return photo; } + async getPhotos() { + const photos = await prisma.photos.findMany({ + where: {}, + select: { + id: true, + user: { + select: { + id: true, + name: true, + }, + }, + spaceId: true, + date: true, + path: true, + }, + orderBy: { + date: 'desc', + }, + }); + return photos; + } + async getPhotosReviewCount() { + const photosWithReviewCount = await prisma.$queryRaw`SELECT + Reviews.photoId, + COUNT(if(Reviews.useful=${usefulMap.up},true,null)) AS usefulCount, + COUNT(if(Reviews.useful=${usefulMap.down},true,null)) AS uselessCount, + COUNT(if(Reviews.useful=${usefulMap.up},true,null)) - COUNT(if(Reviews.useful=${usefulMap.down},true,null)) AS netUsefulCount + FROM Reviews + WHERE Reviews.photoId IN (select Photos.id FROM Photos) + group by photoId + ORDER BY netUsefulCount desc `; + + return photosWithReviewCount; + } async _truncate() { await prisma.photos.deleteMany({}); } diff --git a/src/routes/modules/photo.js b/src/routes/modules/photo.js index 16e75bb..c064fdb 100644 --- a/src/routes/modules/photo.js +++ b/src/routes/modules/photo.js @@ -5,7 +5,7 @@ const { authenticated } = require('../../middleware/auth'); const router = express.Router(); -router.get('/', photoController.getPhotosPhotos); +router.get('/', photoController.getPhotos); router.post('/', authenticated, uploadImages, photoController.postPhotos); module.exports = router; diff --git a/src/services/photo-service.js b/src/services/photo-service.js index aa018f3..7a6e1b9 100644 --- a/src/services/photo-service.js +++ b/src/services/photo-service.js @@ -121,6 +121,60 @@ class PhotoService extends BaseService { }; return photoInfo; } + async getPhotos() { + // get photos + const photoModel = new PhotoModel(); + const photos = await photoModel.getPhotos(); + + if (isEmpty(photos)) + throw new GeneralError(getPhotoErrorMap['photosNotFound']); + + this.logger.debug('got photos', { photos }); + + // get photos which has review + const photosWithReviewCount = await photoModel.getPhotosReviewCount(); + + // combine above two data + let photosData = await Promise.all( + photos.map(async (photo) => { + const dataset = renderDataset(sizeMap.seatPhoto)({ + path: photo.path, + bucketName: bucketMap.photos, + assetDomain, + }); + + const data = { + ...photo, + dataset, + }; + + const result = R.omit(['path'], data); + + let mappingResult = false; + for (let i = 0; i < photosWithReviewCount.length; i++) { + if (photosWithReviewCount[i].photoId === photo.id) { + photo = { + ...result, + usefulCount: photosWithReviewCount[i].usefulCount, + uselessCount: photosWithReviewCount[i].uselessCount, + netUsefulCount: photosWithReviewCount[i].netUsefulCount, + }; + mappingResult = true; + } + } + if (!mappingResult) { + photo = { + ...result, + usefulCount: 0, + uselessCount: 0, + netUsefulCount: 0, + }; + } + return photo; + }) + ); + return photosData; + } } module.exports = PhotoService; diff --git a/src/services/photo-service.test.js b/src/services/photo-service.test.js index eb4b806..57b0b4c 100644 --- a/src/services/photo-service.test.js +++ b/src/services/photo-service.test.js @@ -359,7 +359,7 @@ describe('photo-service.getPhoto', () => { newLevel.id, 'testZone' ); - const correctSpace = await spaceModel.createSpace( + const newSpace = await spaceModel.createSpace( newZone.id, 'seat', 'testVersion', @@ -381,7 +381,7 @@ describe('photo-service.getPhoto', () => { // create test photo data const path = 'testPhotoPath'; const userId = newUser.id; - const spaceId = correctSpace.id; + const spaceId = newSpace.id; const dateTime = new Date(); const photoModel = new PhotoModel(); const newPhoto = await photoModel.createPhoto( @@ -399,7 +399,6 @@ describe('photo-service.getPhoto', () => { // getPhoto const photo = await photoService.getPhoto(newPhoto.id); - console.log(photo); expect(photo).toMatchObject(expectedResult); expect(photo).toHaveProperty('netUsefulCount'); expect(photo).toHaveProperty('dataset'); @@ -425,7 +424,7 @@ describe('photo-service.getPhoto', () => { newLevel.id, 'testZone' ); - const correctSpace = await spaceModel.createSpace( + const newSpace = await spaceModel.createSpace( newZone.id, 'seat', 'testVersion', @@ -447,7 +446,7 @@ describe('photo-service.getPhoto', () => { // create test photo data const path = 'testPhotoPath'; const userId = newUser.id; - const spaceId = correctSpace.id; + const spaceId = newSpace.id; const dateTime = new Date(); const photoModel = new PhotoModel(); const newPhoto = await photoModel.createPhoto( @@ -470,3 +469,116 @@ describe('photo-service.getPhoto', () => { }); }); }); + +describe('photo-service.getPhotos', () => { + describe('with regular input', () => { + it('should return all photos data', async () => { + // create two space + const fieldModel = new FieldModel(); + const levelModel = new LevelModel(); + const orientationModel = new OrientationModel(); + const zoneModel = new ZoneModel(); + const spaceModel = new SpaceModel(); + + const newField = await fieldModel.createField('testField', ''); + const newLevel = await levelModel.createLevel('testLevel'); + const newOrientation = await orientationModel.createOrientation( + 'testOrientation' + ); + const newZone = await zoneModel.createZone( + newField.id, + newOrientation.id, + newLevel.id, + 'testZone' + ); + const spaceOne = await spaceModel.createSpace( + newZone.id, + 'seat', + 'testVersion', + 1, + 1, + 'rightSeat', + 1, + 1 + ); + + const spaceTwo = await spaceModel.createSpace( + newZone.id, + 'seat', + 'testVersion', + 2, + 2, + 'rightSeat', + 2, + 2 + ); + + // create and verify user + const userService = new UserService({ + logger: console, + }); + const email = 'example@example.com'; + const newUser = await userService.signUp('user1', email, 'password1'); + await userService.verifyEmail(newUser.verificationToken); + + // create test photo data + const path = 'testPhotoPath'; + const userId = newUser.id; + const spaceOneId = spaceOne.id; + const spaceTwoId = spaceTwo.id; + const dateTime = new Date(); + const photoModel = new PhotoModel(); + await photoModel.createPhoto(path, userId, spaceOneId, dateTime); + + await photoModel.createPhoto(`${path}1`, userId, spaceTwoId, dateTime); + + // getPhotos + const photos = await photoService.getPhotos(); + expect(photos).toHaveLength(2); + expect(photos[0]).toHaveProperty('netUsefulCount'); + expect(photos[0]).toHaveProperty('dataset'); + }); + }); + describe('with no photos exist', () => { + it('should return photosNotFound error', async () => { + // create space + const fieldModel = new FieldModel(); + const levelModel = new LevelModel(); + const orientationModel = new OrientationModel(); + const zoneModel = new ZoneModel(); + const spaceModel = new SpaceModel(); + + const newField = await fieldModel.createField('testField', ''); + const newLevel = await levelModel.createLevel('testLevel'); + const newOrientation = await orientationModel.createOrientation( + 'testOrientation' + ); + const newZone = await zoneModel.createZone( + newField.id, + newOrientation.id, + newLevel.id, + 'testZone' + ); + await spaceModel.createSpace( + newZone.id, + 'seat', + 'testVersion', + 1, + 1, + 'rightSeat', + 1, + 1 + ); + + // get Photos + assert.rejects( + async () => { + await photoService.getPhotos(); + }, + { + code: getPhotoErrorMap.photosNotFound.code, + } + ); + }); + }); +}); From 87f30210e7bf42e10f755606de85be9305a1de6e Mon Sep 17 00:00:00 2001 From: ronnychiang Date: Sun, 3 Jul 2022 16:38:25 +0800 Subject: [PATCH 12/12] feat: postReview and unreview --- src/controllers/photo-controller.js | 28 +++++++++++ src/errors/review-error.js | 20 ++++++++ src/models/review/index.js | 34 +++++++++++++ src/routes/modules/photo.js | 2 + src/services/review-service.js | 74 +++++++++++++++++++++++++++++ 5 files changed, 158 insertions(+) create mode 100644 src/errors/review-error.js create mode 100644 src/services/review-service.js diff --git a/src/controllers/photo-controller.js b/src/controllers/photo-controller.js index 1c0b2e4..b8002b1 100644 --- a/src/controllers/photo-controller.js +++ b/src/controllers/photo-controller.js @@ -1,4 +1,6 @@ const PhotoService = require('../services/photo-service'); +const ReviewService = require('../services/review-service'); + const resSuccess = require('./helpers/response'); const getUser = require('./helpers/get-user'); const SpaceService = require('../services/space-service'); @@ -51,6 +53,32 @@ const photoController = { next(err); } }, + postReview: async (req, res, next) => { + try { + const { useful } = req.body; + const userId = getUser(req).id; + const photoId = req.params.id; + + const reviewService = new ReviewService({ logger: req.logger }); + const review = await reviewService.postReview(userId, photoId, useful); + res.status(200).json(resSuccess(review)); + } catch (err) { + next(err); + } + }, + postUnreview: async (req, res, next) => { + try { + const { useful } = req.body; + const userId = getUser(req).id; + const photoId = req.params.id; + + const reviewService = new ReviewService({ logger: req.logger }); + const review = await reviewService.postUnreview(userId, photoId, useful); + res.status(200).json(resSuccess(review)); + } catch (err) { + next(err); + } + }, }; module.exports = photoController; diff --git a/src/errors/review-error.js b/src/errors/review-error.js new file mode 100644 index 0000000..2b2a977 --- /dev/null +++ b/src/errors/review-error.js @@ -0,0 +1,20 @@ +const reviewErrorMap = { + alreadyReview: { + message: '你已經評價過此照片', + code: 're001', + }, + notReviewYet: { + message: '你未曾評價過此照片', + code: 're002', + }, + alreadyUnReview: { + message: '你早已經取消此照片的評價', + code: 're003', + }, + badUnReviewRequest: { + message: '你想取消的評價與之前評價的不同', + code: 're004', + }, +}; + +module.exports = reviewErrorMap; diff --git a/src/models/review/index.js b/src/models/review/index.js index 749520d..7f539ef 100644 --- a/src/models/review/index.js +++ b/src/models/review/index.js @@ -31,6 +31,40 @@ class ReviewModel { return photoWithReviewCount; } + async updateReview(userId, photoId, useful) { + const review = await prisma.reviews.update({ + where: { + userId_photoId: { + userId: Number(userId), + photoId: Number(photoId), + }, + }, + data: { useful }, + select: { + id: true, + userId: true, + photoId: true, + useful: true, + }, + }); + return review; + } + async getReview(userId, photoId) { + const review = await prisma.reviews.findUnique({ + where: { + userId_photoId: { + userId: Number(userId), + photoId: Number(photoId), + }, + }, + select: { + userId: true, + photoId: true, + useful: true, + }, + }); + return review; + } async _truncate() { await prisma.reviews.deleteMany({}); } diff --git a/src/routes/modules/photo.js b/src/routes/modules/photo.js index c064fdb..f4ee665 100644 --- a/src/routes/modules/photo.js +++ b/src/routes/modules/photo.js @@ -5,6 +5,8 @@ const { authenticated } = require('../../middleware/auth'); const router = express.Router(); +router.post('/:id/reviews', authenticated, photoController.postReview); +router.post('/:id/unreviews', authenticated, photoController.postUnreview); router.get('/', photoController.getPhotos); router.post('/', authenticated, uploadImages, photoController.postPhotos); diff --git a/src/services/review-service.js b/src/services/review-service.js new file mode 100644 index 0000000..b3438d6 --- /dev/null +++ b/src/services/review-service.js @@ -0,0 +1,74 @@ +const { isEmpty, isNil } = require('ramda'); +const BaseService = require('./base'); +const ReviewModel = require('../models/review/index'); +const PhotoModel = require('../models/photo'); + +const GeneralError = require('../errors/error/general-error'); +const getPhotoErrorMap = require('../errors/get-photo-error'); +const reviewErrorMap = require('../errors//review-error'); + +class ReviewService extends BaseService { + async postReview(userId, photoId, useful) { + // check space exist + const photoModel = new PhotoModel(); + const photoCheck = await photoModel.getPhoto(photoId); + if (!photoCheck) throw new GeneralError(getPhotoErrorMap['photoNotFound']); + + // check existed review + const reviewModel = new ReviewModel(); + const review = await reviewModel.getReview(userId, photoId); + + // if already review (not cancel) + if (review && !isEmpty(review.useful)) + throw new GeneralError(reviewErrorMap['alreadyReview']); + + // if already review but cancel + if (review && isEmpty(review.useful)) { + const updateReview = await reviewModel.updateReview( + userId, + photoId, + useful + ); + + this.logger.debug('update a review', { updateReview }); + + return updateReview; + } + + // if review not exist + const newReview = await reviewModel.createReview(userId, photoId, useful); + + this.logger.debug('create a review', { newReview }); + + return newReview; + } + async postUnreview(userId, photoId, useful) { + // check space exist + const photoModel = new PhotoModel(); + const photoCheck = await photoModel.getPhoto(photoId); + if (!photoCheck) throw new GeneralError(getPhotoErrorMap['photoNotFound']); + + // check existed review + const reviewModel = new ReviewModel(); + const review = await reviewModel.getReview(userId, photoId); + + // if review not existed + if (isNil(review)) throw new GeneralError(reviewErrorMap['notReviewYet']); + + // if review already be canceled + if (review && isEmpty(review.useful)) + throw new GeneralError(reviewErrorMap['alreadyUnReview']); + + // if unreview's useful not equal reviewed before + if (review && useful !== review.useful) + throw new GeneralError(reviewErrorMap['badUnReviewRequest']); + + const unReview = await reviewModel.updateReview(userId, photoId, ''); + + this.logger.debug('update a review', { unReview }); + + return unReview; + } +} + +module.exports = ReviewService;