From cf7c3b9b60a08bef6b5ce87a4aa042688e8f1b8b Mon Sep 17 00:00:00 2001 From: Pau Date: Sat, 19 Apr 2025 04:10:35 +0200 Subject: [PATCH] added favorite places for user mvc and routes --- .../resource_types/favoritesController.js | 136 ++++++++++++ src/models/resource_types/favoritesModel.js | 68 ++++++ .../resource_types/favoritesRepository.js | 123 +++++++++++ src/routes/index.js | 197 ++++++++++++++++++ 4 files changed, 524 insertions(+) create mode 100644 src/controllers/resource_types/favoritesController.js create mode 100644 src/models/resource_types/favoritesModel.js create mode 100644 src/repositories/resource_types/favoritesRepository.js diff --git a/src/controllers/resource_types/favoritesController.js b/src/controllers/resource_types/favoritesController.js new file mode 100644 index 0000000..96a29ed --- /dev/null +++ b/src/controllers/resource_types/favoritesController.js @@ -0,0 +1,136 @@ +import mysql from '../../adapters/mysql.js' +import { + getFavoritesModel, + countFavoritesModel, + insertFavoritesModel, + modifyFavoritesModel, + softDeleteFavoritesModel +} from '../../models/resource_types/favoritesModel.js' +import { errorHandler } from '../../utils/errors.js' +import { noResults } from '../../validators/result-validators.js' +import { error404 } from '../../utils/errors.js' +import { sendResponseNotFound } from '../../utils/responses.js' + +const getFavoritesController = (req, res, next, config) => { + const conn = mysql.start(config) + const uuidList = req.query.uuidList && req.query.uuidList.split(',') + Promise.all([ + getFavoritesModel({...req.query, ...req.params, uuidList, conn}), + countFavoritesModel({...req.query, uuidList, conn}) + ]) + .then(([getResults, countResults]) => { + next({ + _data: {favorites: getResults}, + _page: { + totalElements: countResults, + limit: req.query.limit || 100, + page: req.query.page || (countResults && 1) || 0 + } + }) + }) + .catch((err) => { + const error = errorHandler(err, config.environment) + res.status(error.code).json(error) + }) + .finally(() => { + mysql.end(conn) + }) +} + +const getFavoritesByUuidController = (req, res, next, config) => { + const uuid = req.params.uuid + const conn = mysql.start(config) + getFavoritesModel({ uuid, conn }) + .then((response) => { + if (noResults(response)) { + const err = error404() + const error = errorHandler(err, config.environment) + return sendResponseNotFound(res, error) + } + + const result = { + _data: { + favorites: response + } + } + next(result) + }) + .catch((err) => { + const error = errorHandler(err, config.environment) + res.status(error.code).json(error) + }) + .finally(() => { + mysql.end(conn) + }) +} + +const postFavoritesController = (req, res, next, config) => { + const conn = mysql.start(config) + const createdBy = req.auth.user || null + + insertFavoritesModel({...req.body, ...req.params, createdBy, conn}) + .then((favorites) => { + const result = { + _data: favorites + } + next(result) + }) + .catch((err) => { + const error = errorHandler(err, config.environment) + res.status(error.code).json(error) + }) + .finally(() => { + mysql.end(conn) + }) +} + +const putFavoritesController = (req, res, next, config) => { + const conn = mysql.start(config) + + modifyFavoritesModel({ ...req.body, ...req.params, conn }) + .then((favorites) => { + if (noResults(favorites)) { + const err = error404() + const error = errorHandler(err, config.environment) + return sendResponseNotFound(res, error) + } + const result = { + _data: favorites + } + next(result) + }) + .catch((err) => { + const error = errorHandler(err, config.environment) + res.status(error.code).json(error) + }) + .finally(() => { + mysql.end(conn) + }) +} + +const softDeleteFavoritesController = (req, res, next, config) => { + const conn = mysql.start(config) + const { deleted } = req.body + const deletedby = req.auth.user || null + + softDeleteFavoritesModel({ ...req.params, deleted, deletedby, conn }) + .then(() => { + const result = {} + next(result) + }) + .catch((err) => { + const error = errorHandler(err, config.environment) + res.status(error.code).json(error) + }) + .finally(() => { + mysql.end(conn) + }) +} + +export { + getFavoritesController, + getFavoritesByUuidController, + postFavoritesController, + softDeleteFavoritesController, + putFavoritesController, +} \ No newline at end of file diff --git a/src/models/resource_types/favoritesModel.js b/src/models/resource_types/favoritesModel.js new file mode 100644 index 0000000..773f2dd --- /dev/null +++ b/src/models/resource_types/favoritesModel.js @@ -0,0 +1,68 @@ +import { randomUUID as uuidv4 } from 'node:crypto' +import dayjs from 'dayjs' +import mysql from '../../adapters/mysql.js' +import { + getFavoritesQuery, + countFavoritesQuery, + insertFavoritesQuery, + softDeleteFavoritesQuery, + modifyFavoritesQuery +} from '../../repositories/resource_types/favoritesRepository.js' +import { error404 } from '../../utils/errors.js' + +const getFavoritesModel = ({conn, ...rest}) => { + const now = dayjs.utc().format('YYYY-MM-DD HH:mm:ss') + const paramsToSearch = { ...rest, now } + + return mysql + .execute(getFavoritesQuery(paramsToSearch), conn, paramsToSearch) + .then(results => results.map(({id, fk_user, fk_place, created, deleted, createdBy, deletedBy, ...resultFiltered}) => resultFiltered)) +} + +const countFavoritesModel = ({conn, ...rest}) => { + const now = dayjs.utc().format('YYYY-MM-DD HH:mm:ss') + const paramsToSearch = { ...rest, now } + + return mysql + .execute(countFavoritesQuery(paramsToSearch), conn, paramsToSearch) + .then(results => results[0].count) +} + +const insertFavoritesModel = ({conn, ...rest}) => { + const now = dayjs.utc().format('YYYY-MM-DD HH:mm:ss') + const uuid = uuidv4() + const paramsToInsert = { ...rest, uuid, now } + + return mysql + .execute(insertFavoritesQuery(paramsToInsert), conn, paramsToInsert) + .then(results => results[1].map(({id, fk_user, fk_place, created, deleted, createdBy, deletedBy, ...rest}) => ({...rest}))) +} + +const modifyFavoritesModel = ({conn, ...params}) => { + return mysql + .execute(modifyFavoritesQuery(params), conn, params) + .then(queryResult => { + const deletedItem = queryResult[1].find(item => item.deleted !== null); + + if (deletedItem) { + throw error404() + } + return queryResult[1].map(({id, created, deleted, createdBy, deletedBy, ...resultFiltered}) => resultFiltered) + }) +} + +const softDeleteFavoritesModel = ({conn, deleted, deletedBy, ...rest}) => { + const deletedData = deleted ? dayjs.utc(deleted).format('YYYY-MM-DD HH:mm:ss') : dayjs.utc().format('YYYY-MM-DD HH:mm:ss') + const params = { ...rest, deleted: deletedData, deletedBy } + + return mysql + .execute(softDeleteFavoritesQuery(params), conn, params) +} + +export { + getFavoritesModel, + countFavoritesModel, + insertFavoritesModel, + softDeleteFavoritesModel, + modifyFavoritesModel +} \ No newline at end of file diff --git a/src/repositories/resource_types/favoritesRepository.js b/src/repositories/resource_types/favoritesRepository.js new file mode 100644 index 0000000..d3ca4f7 --- /dev/null +++ b/src/repositories/resource_types/favoritesRepository.js @@ -0,0 +1,123 @@ +import { pagination } from '../../utils/pagination.js' + +const _favoritesQuery = (_pagination = '') => ({count}) => ({uuid, place_uuid, user_uuid, roleName}) => { + const uuidCondition = uuid ? 'AND r.uuid = :uuid' : ''; + const placeUuidCondition = place_uuid ? 'AND fk_place = (SELECT id from dbmaster.places WHERE uuid = :place_uuid)' : ''; + const userUuidCondition = user_uuid ? 'AND fk_user = (SELECT id from dbmaster.users WHERE uuid = :user_uuid)' : ''; + return ` + SELECT + ${count || + `r.*, + place.uuid as place_uuid, + p.action as permission_action, + p.uuid as user_uuid, + p.fk_endpoint as permission_endpoint`} + FROM + dbmaster.favorites as r + JOIN + dbmaster.places as place ON r.fk_place = place.id + AND place.created <= :now + AND (place.deleted > :now OR place.deleted IS NULL) + JOIN + dbmaster.users as p ON r.fk_user = p.id + AND p.created <= :now + AND (p.deleted > :now OR p.deleted IS NULL) + WHERE + r.created <= :now + AND + (r.created > :now OR r.deleted IS NULL) + AND + true + ${uuidCondition} + ${userUuidCondition} + ${placeUuidCondition} + ${_pagination} + `; + } + +const getFavoritesQuery = ({ limit, page, ...rest }) => + _favoritesQuery(pagination({ limit, page }))({ count: false })(rest); +const countFavoritesQuery = rest => + _favoritesQuery()({ count: 'COUNT(*) AS count' })(rest); + +const insertFavoritesQuery = () => { + return ` + INSERT INTO dbmaster.favorites ( + uuid, + fk_place, + fk_user, + created, + createdBy + ) + VALUES ( + :uuid, + (SELECT id FROM dbmaster.places WHERE uuid = :place_uuid), + (SELECT id FROM dbmaster.users WHERE uuid = :user_uuid), + :now, + :createdBy + ); + SELECT favorites.*, + users.uuid as user_uuid, + places.uuid as place_uuid, + places.name as role_name + FROM dbmaster.favorites as f + LEFT JOIN dbmaster.users as users ON f.fk_user = users.id + LEFT JOIN dbmaster.places as places ON f.fk_place = places.id + WHERE f.uuid = :uuid; + ` +} + +const modifyFavoritesQuery = ({new_place_uuid, new_user_uuid}) => { + const placeUuidCondition = new_place_uuid ? 'fk_place = (SELECT id from dbmaster.places WHERE uuid = :new_place_uuid),' : ''; + const showNewPlaceCondition = new_place_uuid ? 'AND dbmaster.places.uuid = :new_place_uuid' : '' + const userUuidCondition = new_user_uuid ? 'fk_user = (SELECT id from dbmaster.users WHERE uuid = :new_user_uuid),' : ''; + const showNewUserCondition = new_user_uuid? 'AND dbmaster.users.uuid = :new_user_uuid' : '' + return ` + UPDATE + dbmaster.favorites as favorites + SET + ${placeUuidCondition} + ${userUuidCondition} + favorites.created = favorites.created + WHERE + favorites.fk_place = (SELECT id from dbmaster.places WHERE uuid = :place_uuid) + AND favorites.fk_user = (SELECT id from dbmaster.users WHERE uuid = :user_uuid) + AND + favorites.deleted IS NULL; + + SELECT f.*, + users.uuid as user_uuid, + places.uuid as place_uuid, + places.name as place_name + FROM dbmaster.favorites as f + LEFT JOIN dbmaster.users as users ON f.fk_user = users.id + LEFT JOIN dbmaster.places as places ON f.fk_place = places.id + WHERE + true + ${showNewUserCondition} + ${showNewPlaceCondition}; + ` +} + +const softDeleteFavoritesQuery = ({place_uuid}) => { + const placeUuidCondition = place_uuid ? 'AND favorites.fk_place = (SELECT id from dbmaster.places WHERE uuid = :place_uuid)' : ''; + return ` + UPDATE + dbmaster.favorites as favorites + SET + deleted = :deleted, deletedBy = :deletedBy + WHERE + favorites.fk_user = (SELECT id from dbmaster.users WHERE uuid = :user_uuid) + ${placeUuidCondition} + AND + favorites.deleted IS NULL + ` +} + +export{ + getFavoritesQuery, + insertFavoritesQuery, + countFavoritesQuery, + softDeleteFavoritesQuery, + modifyFavoritesQuery +} \ No newline at end of file diff --git a/src/routes/index.js b/src/routes/index.js index f7ad508..0cb6fef 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -38,6 +38,7 @@ import { import { deletePlaceController, getPlaceByUuidController, getPlaceListController, insertPlaceController, modifyPlaceController } from '../controllers/resource_types/placesController.js' import { deleteUserHasPlacesController, getUserHasPlacesByUuidController, getUserHasPlacesListController, postUserHasPlacesController, putUserHasPlacesController } from '../controllers/resource_types/usersHasPlacesController.js' import { getReportTypesByUuidController, getReportTypesListController, postReportTypesController, putReportTypesController, softDeleteReportTypesController } from '../controllers/resource_types/reportTypesController.js' +import { getFavoritesController, postFavoritesController, putFavoritesController, softDeleteFavoritesController } from '../controllers/resource_types/favoritesController.js' /** * @function default @@ -1361,6 +1362,202 @@ export default(config) => { (result, req, res, _) => sendResponseNoContent(result, req, res) ); + //favorite places endpoint + /** + * @name GET/users/:user:uuid/places + * @function + * @inner + * @memberof deviceRouter + * @route GET /users/:user:uuid/places + * @group Favorites - Operations about favorite place from user + * @param {string} uuid.path.optional - The unique identifier for favorite place from user + * @param {string} fk_user.path.required - The unique identifier for the user + * @param {string} fk_place.path.optional - The unique identifier for the place + * @returns {SuccessResponse} 200 - The favorite object + * @returns {ErrorResponse} 404 - Favorite place not found + * @returns {ErrorResponse} 422 - Unprocessable entity + * @returns {ErrorResponse} 500 - Internal server error + * @returns {ErrorResponse} 403 - Forbidden + */ + routes.get( + '/users/:user:uuid/places', + (req, res, next) => authenticateToken(req, res, next, config), + (req, res, next) => authorizePermission('/users/:user:uuid/places')(req, res, next, config), + [ + uuid('uuid').optional({ nullable: false, values: 'falsy' }), + uuid('user_uuid'), + uuid('place_uuid').optional({ nullable: false, values: 'falsy' }) + ], + (req, res, next) => payloadExpressValidator(req, res, next, config), + (req, res, next) => getFavoritesController(req, res, next, config), + (result, req, res, next) => addLinks(result, req, res, next, hasAddLinks, linkRoutes), + (result, req, res, _) => sendOkResponse(result, req, res) + ); + + /** + * @name GET/users/:user_uuid/places/:place_uuid' + * @function + * @inner + * @memberof deviceRouter + * @route GET /users/:user_uuid/places/:place_uuid + * @group Favorites - Operations about favorite place from user + * @param {string} uuid.path.optional - The unique identifier for favorite place from user + * @param {string} fk_user.path.required - The unique identifier for the user + * @param {string} fk_place.path.required - The unique identifier for the place + * @returns {SuccessResponse} 200 - The favorite object + * @returns {ErrorResponse} 404 - Favorite place not found + * @returns {ErrorResponse} 422 - Unprocessable entity + * @returns {ErrorResponse} 500 - Internal server error + * @returns {ErrorResponse} 403 - Forbidden + * @returns {ErrorResponse} 401 - Unauthorized + * @returns {ErrorResponse} 400 - Bad request + * @returns {ErrorResponse} 409 - Conflict + */ + routes.get( + '/users/:user_uuid/places/:place_uuid', + (req, res, next) => authenticateToken(req, res, next, config), + (req, res, next) => authorizePermission('/users/:user_uuid/places/:place_uuid')(req, res, next, config), + [ + uuid('uuid').optional({nullable:false, values:'falsy'}), + uuid('user_uuid'), + uuid('place_uuid') + ], + (req, res, next) => payloadExpressValidator(req, res, next, config), + (req, res, next) => getFavoritesController(req, res, next, config), + (result, req, res, next) => addLinks(result, req, res, next, hasAddLinks, linkRoutes), + (result, req, res, _) => sendOkResponse(result, req, res) + ); + + /** + * @name POST/users/:user_uuid/places + * @function + * @inner + * @memberof deviceRouter + * @route POST /users/:user_uuid/places' + * @group Favorites - Operations about favorite place from user + * @param {string} fk_user.path.required - The unique identifier for the user + * @param {string} fk_place.path.required - The unique identifier for the place + * @returns {SuccessResponse} 200 - Favorite place created successfully + * @returns {ErrorResponse} 400 - Bad request + * @returns {ErrorResponse} 404 - Favorite place not found + * @returns {ErrorResponse} 422 - Unprocessable entity + * @returns {ErrorResponse} 500 - Internal server error + * @returns {ErrorResponse} 403 - Forbidden + * @returns {ErrorResponse} 401 - Unauthorized + * @returns {ErrorResponse} 409 - Conflict + */ + routes.post( + '/users/:user_uuid/places', + (req, res, next) => authenticateToken(req, res, next, config), + (req, res, next) => authorizePermission('/users/:user_uuid/places')(req, res, next, config), + [ + uuid('user_uuid'), + uuid('place_uuid') + ], + (req, res, next) => payloadExpressValidator(req, res, next, config), + (req, res, next) => postFavoritesController(req, res, next, config), + (result, req, res, next) => addLinks(result, req, res, next, hasAddLinks, linkRoutes), + (result, req, res, _) => sendCreatedResponse(result, req, res) + ); + + /** + * @name PUT/users/:user_uuid/places/:place_uuid' + * @function + * @inner + * @memberof deviceRouter + * @route PUT /users/:user_uuid/places/:place_uuid' + * @group Favorites - Operations about favorite place from user + * @param {string} fk_user.path.required - The unique identifier for the user + * @param {string} fk_place.path.required - The unique identifier for the place + * @param {string} new_user_uuid.path.optional - The unique identifier for the new user + * @param {string} new_place_uuid.path.optional - The unique identifier for the new place + * @returns {SuccessResponse} 200 - Favorite place updated successfully + * @returns {ErrorResponse} 400 - Bad request + * @returns {ErrorResponse} 404 - Favorite place not found + * @returns {ErrorResponse} 422 - Unprocessable entity + * @returns {ErrorResponse} 500 - Internal server error + * @returns {ErrorResponse} 403 - Forbidden + * @returns {ErrorResponse} 401 - Unauthorized + * @returns {ErrorResponse} 409 - Conflict + */ + routes.put( + '/users/:user_uuid/places/:place_uuid', + (req, res, next) => authenticateToken(req, res, next, config), + (req, res, next) => authorizePermission('/users/:user_uuid/places/:place_uuid')(req, res, next, config), + [ + uuid('user_uuid'), + uuid('place_uuid'), + uuid('new_user_uuid').optional({ nullable: false, values: 'falsy' }), + uuid('new_place_uuid').optional({ nullable: false, values: 'falsy' }) + ], + (req, res, next) => payloadExpressValidator(req, res, next, config), + (req, res, next) => putFavoritesController(req, res, next, config), + (result, req, res, next) => addLinks(result, req, res, next, hasAddLinks, linkRoutes), + (result, req, res, _) => sendCreatedResponse(result, req, res) + ); + + /** + * @name DELETE/users/:user_uuid/places' + * @function + * @inner + * @memberof deviceRouter + * @route DELETE /users/:user_uuid/places + * @group Favorites - Operations about favorite place from user + * @param {string} fk_user.path.required - The unique identifier for the user + * @returns {SuccessResponse} 200 - Favorite place deleted successfully. No content + * @returns {ErrorResponse} 404 - Favorite place not found + * @returns {ErrorResponse} 422 - Unprocessable entity + * @returns {ErrorResponse} 500 - Internal server error + * @returns {ErrorResponse} 403 - Forbidden + * @returns {ErrorResponse} 401 - Unauthorized + * @returns {ErrorResponse} 400 - Bad request + * @returns {ErrorResponse} 409 - Conflict + */ + routes.delete( + '/users/:user_uuid/places', + (req, res, next) => authenticateToken(req, res, next, config), + (req, res, next) => authorizePermission('/users/:user_uuid/places')(req, res, next, config), + [ + uuid('user_uuid') + ], + (req, res, next) => payloadExpressValidator(req, res, next, config), + (req, res, next) => softDeleteFavoritesController(req, res, next, config), + (result, req, res, next) => addLinks(result, req, res, next, hasAddLinks, linkRoutes), + (result, req, res, _) => sendResponseNoContent(result, req, res) + ); + + /** + * @name DELETE/users/:user_uuid/places/:place_uuid' + * @function + * @inner + * @memberof deviceRouter + * @route DELETE /users/:user_uuid/places/:place_uuid + * @group Favorites - Operations about favorite place from user + * @param {string} fk_user.path.required - The unique identifier for the user + * @param {string} fk_place.path.required - The unique identifier for the place + * @returns {SuccessResponse} 200 - Favorite place deleted successfully. No content + * @returns {ErrorResponse} 404 - Favorite place not found + * @returns {ErrorResponse} 422 - Unprocessable entity + * @returns {ErrorResponse} 500 - Internal server error + * @returns {ErrorResponse} 403 - Forbidden + * @returns {ErrorResponse} 401 - Unauthorized + * @returns {ErrorResponse} 400 - Bad request + * @returns {ErrorResponse} 409 - Conflict + */ + routes.delete( + '/users/:user_uuid/places/:place_uuid', + (req, res, next) => authenticateToken(req, res, next, config), + (req, res, next) => authorizePermission('/users/:user_uuid/places/:place_uuid')(req, res, next, config), + [ + uuid('user_uuid'), + uuid('place_uuid') + ], + (req, res, next) => payloadExpressValidator(req, res, next, config), + (req, res, next) => softDeleteFavoritesController(req, res, next, config), + (result, req, res, next) => addLinks(result, req, res, next, hasAddLinks, linkRoutes), + (result, req, res, _) => sendResponseNoContent(result, req, res) + ); + // Login Endpoint /** * @name POST/login