From c96bdfddb09e7331de743eacc1779b46b5608159 Mon Sep 17 00:00:00 2001 From: Spacelord Date: Thu, 30 Jan 2025 00:24:59 +0100 Subject: [PATCH] Migrated validation to joi / Moved prisma_helpers to dedicated file / --- src/helpers/prisma_helpers.ts | 22 ++++ src/routes/api/v1/alertContacts.ts | 147 +++++++++------------- src/routes/api/v1/alertContacts_schema.ts | 53 ++++++++ src/routes/api/v1/index.ts | 15 +-- 4 files changed, 138 insertions(+), 99 deletions(-) create mode 100644 src/helpers/prisma_helpers.ts create mode 100644 src/routes/api/v1/alertContacts_schema.ts diff --git a/src/helpers/prisma_helpers.ts b/src/helpers/prisma_helpers.ts new file mode 100644 index 0000000..0b26d70 --- /dev/null +++ b/src/helpers/prisma_helpers.ts @@ -0,0 +1,22 @@ +/** + * A function to create a sortBy compatible object from a string + * + * @export + * @param {string} SortField + * @param {string} Order + * @returns {object} + */ +export function parseDynamicSortBy(SortField: string, Order: string) { + return JSON.parse(`{ "${SortField}": "${Order}" }`); +} + +/** + * Function to parse a string into a number or return undefined if it is not a number + * + * @export + * @param {string || any} data + * @returns {object} + */ +export function parseIntOrUndefined(data: any) { + return isNaN(parseInt(data)) ? undefined : parseInt(data); +} \ No newline at end of file diff --git a/src/routes/api/v1/alertContacts.ts b/src/routes/api/v1/alertContacts.ts index b920250..45a006c 100644 --- a/src/routes/api/v1/alertContacts.ts +++ b/src/routes/api/v1/alertContacts.ts @@ -1,116 +1,87 @@ import { Request, Response } from 'express'; import db, { handlePrismaError } from '../../../handlers/db.js'; // Database import log from '../../../handlers/log.js'; +import { parseIntOrUndefined, parseDynamicSortBy } from '../../../helpers/prisma_helpers.js'; +import { schema_get } from './alertContacts_schema.js'; -///api/v1/alertContacts?action=count&filter=... - -// GET without args -> Get all alertContacts - -/** - * A function to create a sortBy compatible object from a string - * - * @export - * @param {string} SortField - * @param {string} Order - * @returns {object} - */ -export function parseDynamicSortBy(SortField: string, Order: string) { - return JSON.parse(`{ "${SortField}": "${Order}" }`); -} - -/** - * Function to parse a string into a number or return undefined if it is not a number - * - * @export - * @param {string || any} data - * @returns {object} - */ -export function parseIntOrUndefined(data: any) { - return isNaN(parseInt(data)) ? undefined : parseInt(data); -} - -// GET AlertContact +// MARK: GET AlertContact async function get(req: Request, res: Response) { - // Set sane defaults if undefined for sort - if (req.query.sort === undefined) { - req.query.sort = 'id'; - } - if (req.query.order === undefined) { - req.query.order = 'asc'; - } + const { error, value } = schema_get.validate(req.query); + if (error) { + log.api?.debug('Error:', req.query, value); + res.status(400).json({ error: error.details[0].message }); + } else { + log.api?.debug('Success:', req.query, value); - // Prio 1 -> Get count (with or without filter) - // Prio 2 -> Get by id - // Prio 3 -> Get with filter - if ((req.query.search !== undefined && req.query.search.length > 0) || (req.query.id !== undefined && req.query.id.length > 0)) { - if (req.query.search !== undefined && req.query.search === '*') { - log.db.debug('Single * does not work with FullTextSearch'); - req.query.search = ''; - } - - // When an ID is set, remove(disable) the search query - if (req.query.id !== undefined && req.query.id.length > 0) { - req.query.search = undefined; - } - - const query = { + // Query with FullTextSearch + const query_fts = { where: { - OR: [{ id: parseIntOrUndefined(req.query.id) }, { name: { search: req.query.search } }, { phone: { search: req.query.search } }, { comment: { search: req.query.search } }] + OR: [{ id: parseIntOrUndefined(value.id) }, { name: { search: value.search } }, { phone: { search: value.search } }, { comment: { search: value.search } }] }, - orderBy: parseDynamicSortBy(req.query.sort.toString(), req.query.order.toString()) + orderBy: parseDynamicSortBy(value.sort.toString(), value.order.toString()) }; - if (req.query.count === undefined) { - // get all entrys - await db.alertContacts.findMany(query).then((result) => { - res.status(200).json(result); - }); + if (value.search !== undefined || value.id !== undefined) { + // with FullTextSearch + if (!value.count) { + // get all entrys + log.api?.trace('get all entrys - with FullTextSearch'); + await db.alertContacts.findMany(query_fts).then((result) => { + res.status(200).json(result); + }); + } else { + // count all entrys + log.api?.trace('count all entrys - with FullTextSearch'); + await db.alertContacts.count(query_fts).then((result) => { + res.status(200).json(result); + }); + } } else { - // count all entrys (filtered or not) - await db.alertContacts.count(query).then((result) => { - res.status(200).json(result); - }); - } - } else { - if (req.query.count === undefined) { - await db.alertContacts.findMany({ orderBy: parseDynamicSortBy(req.query.sort.toString(), req.query.order.toString()) }).then((result) => { - res.status(200).json(result); - }); - } else { - await db.alertContacts.count().then((result) => { - res.status(200).json(result); - }); + // without FullTextSearch + if (!value.count) { + // get all entrys + log.api?.trace('get all entrys - without FullTextSearch'); + await db.alertContacts.findMany({ orderBy: parseDynamicSortBy(value.sort.toString(), value.order.toString()) }).then((result) => { + res.status(200).json(result); + }); + } else { + // count all entrys without FullTextSearch + log.api?.trace('count all entrys - without FullTextSearch'); + await db.alertContacts.count().then((result) => { + res.status(200).json(result); + }); + } } } } -// CREATE AlertContact +// MARK: CREATE AlertContact async function post(req: Request, res: Response) { - // Check if undefined or null if (req.body.name != null && req.body.phone != null) { await db.alertContacts - .create({ - data: { - name: req.body.name, - phone: req.body.phone, - comment: req.body.comment, - }, - select: { - id: true - } - }).then((result) => { - res.status(201).json({ status: 'CREATED', message: 'Successfully created alertContact', id: result.id }); - }) + .create({ + data: { + name: req.body.name, + phone: req.body.phone, + comment: req.body.comment + }, + select: { + id: true + } + }) + .then((result) => { + res.status(201).json({ status: 'CREATED', message: 'Successfully created alertContact', id: result.id }); + }); } else { - res.status(400).json({ status: 'ERROR', errorcode: "VALIDATION_ERROR", message: 'One or more required fields are missing or invalid' }); + res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'One or more required fields are missing or invalid' }); } } -// UPDATE AlertContact +// MARK: UPDATE AlertContact async function patch(req: Request, res: Response) {} -// DELETE AlertContact +// MARK: DELETE AlertContact async function del(req: Request, res: Response) {} export default { get, post, patch, del }; diff --git a/src/routes/api/v1/alertContacts_schema.ts b/src/routes/api/v1/alertContacts_schema.ts new file mode 100644 index 0000000..d1e98a8 --- /dev/null +++ b/src/routes/api/v1/alertContacts_schema.ts @@ -0,0 +1,53 @@ +import { Request, Response } from 'express'; +import validator from 'joi'; // DOCS: https://joi.dev/api +import log from '../../../handlers/log.js'; + + +const schema_get = validator.object({ + sort: validator.string().valid('id', 'name', 'phone', 'comment').default('id'), + order: validator.string().valid('asc', 'desc').default('asc'), + search: validator.string().min(3).max(20), // TODO: Check if * or ** or *** -> Due to crashes.. + id: validator.number().positive().precision(0), + count: validator.boolean() +}).nand('id', 'search'); // Allow id or search. not both. + +const schema_post = validator.object({ + name: validator.string().min(1).max(32).required(), + phone: validator.string().pattern(new RegExp('^\\+(\\d{1,3})\\s*(?:\\(\\s*(\\d{2,5})\\s*\\)|\\s*(\\d{2,5})\\s*)\\s*(\\d{5,15})$')).required(), + comment: validator.string().max(64), +}) + +const schema_patch = validator.object({ + sort: validator.string().valid('id', 'name', 'phone', 'comment').default('id'), + order: validator.string().valid('asc', 'desc').default('asc'), + search: validator.string().min(3).max(20), // TODO: Check if * or ** or *** -> Due to crashes.. + id: validator.number().positive().precision(0), + count: validator.boolean() +}).nand('id', 'search'); // Allow id or search. not both. + +const schema_del = validator.object({ + sort: validator.string().valid('id', 'name', 'phone', 'comment').default('id'), + order: validator.string().valid('asc', 'desc').default('asc'), + search: validator.string().min(3).max(20), // TODO: Check if * or ** or *** -> Due to crashes.. + id: validator.number().positive().precision(0), + count: validator.boolean() +}).nand('id', 'search'); // Allow id or search. not both. + +// Describe all schemas +const schema_get_describe = schema_get.describe(); +const schema_post_describe = schema_post.describe(); +const schema_patch_describe = schema_patch.describe(); +const schema_del_describe = schema_del.describe(); + + +// GET route +export default async function get(req: Request, res: Response) { + res.status(200).json({ + GET: schema_get_describe, + POST: schema_post_describe, + PATCH: schema_patch_describe, + DELETE: schema_del_describe + }); +} + +export { schema_get, schema_post, schema_patch, schema_del } diff --git a/src/routes/api/v1/index.ts b/src/routes/api/v1/index.ts index bc8d22a..b9ced10 100644 --- a/src/routes/api/v1/index.ts +++ b/src/routes/api/v1/index.ts @@ -3,14 +3,11 @@ import passport from 'passport'; // Route imports import testRoute from './test.js'; -import alertContactsRoute from './alertContacts.js'; -//import categoryRoute from './categories.js'; -//import storageUnitRoute from './storageUnits.js'; -//import storageLocationRoute from './storageLocations.js'; -//import contactInfo from './contactInfo.js'; import versionRoute from './version.js' -//import search_routes from './search/index.js'; +import alertContactsRoute from './alertContacts.js'; +import alertContactsRoute_schema from './alertContacts_schema.js'; + // Router base is '/api/v1' const Router = express.Router({ strict: false }); @@ -27,11 +24,7 @@ Router.use('*', function (req, res, next) { // All api routes lowercase! Yea I know but when strict: true it matters. Router.route('/alertcontacts').get(alertContactsRoute.get).post(alertContactsRoute.post).patch(alertContactsRoute.patch).delete(alertContactsRoute.del); -//Router.route('/categories').get(categoryRoute.get).post(categoryRoute.post).patch(categoryRoute.patch).delete(categoryRoute.del); -// TODO: Migrate routes to lowercase. -//Router.route('/storageUnits').get(storageUnitRoute.get).post(storageUnitRoute.post).patch(storageUnitRoute.patch).delete(storageUnitRoute.del); -//Router.route('/storageLocations').get(storageLocationRoute.get).post(storageLocationRoute.post).patch(storageLocationRoute.patch).delete(storageLocationRoute.del); -//Router.route('/contactInfo').get(contactInfo.get).post(contactInfo.post).patch(contactInfo.patch).delete(contactInfo.del); +Router.route('/alertcontacts/describe').get(alertContactsRoute_schema); Router.route('/version').get(versionRoute.get); //Router.use('/search', search_routes);