diff --git a/src/handlers/db.ts b/src/handlers/db.ts index c521f43..829a7f9 100644 --- a/src/handlers/db.ts +++ b/src/handlers/db.ts @@ -38,3 +38,5 @@ export function handlePrismaError(errorObj: any, res: Response, source: string) } export default prisma; + +//FIXME: https://www.prisma.io/docs/orm/prisma-client/special-fields-and-types/null-and-undefined \ No newline at end of file diff --git a/src/routes/api/v1/index.ts b/src/routes/api/v1/index.ts index 05c070d..a3f8e0f 100644 --- a/src/routes/api/v1/index.ts +++ b/src/routes/api/v1/index.ts @@ -3,17 +3,13 @@ import passport from 'passport'; // Route imports import testRoute from './test.js'; -import versionRoute from './version.js' +import versionRoute from './version.js'; -import userRoute from './user.js'; -import userRoute_schema from './user_schema.js'; - -// import content_route from './content.js'; -// import content_schema from './content_schema.js'; - -// import * as content_s3_sub_route from './content_s3_sub.js'; -// import * as content_s3_sub_schema from './content_s3_sub_schema.js'; +import user_route from './user.js'; +import user_schema from './user_schema.js'; +import products_route from './products.js'; +import products_schema from './products_schema.js'; // Router base is '/api/v1' const Router = express.Router({ strict: false }); @@ -28,24 +24,14 @@ Router.use('*', function (req, res, next) { next(); }); - // All api routes lowercase! Yea I know but when strict: true it matters. -Router.route('/user').get(userRoute.get).post(userRoute.post).patch(userRoute.patch).delete(userRoute.del); -Router.route('/user/describe').get(userRoute_schema); - -// Router.route('/content').get(content_route.get).post(content_route.post).patch(content_route.patch).delete(content_route.del); -// Router.route('/content/describe').get(content_schema); - -// Router.route('/content/downloadurl').get(content_s3_sub_route.get_downloadurl); -// Router.route('/content/uploadurl').get(content_s3_sub_route.get_uploadurl); -// Router.route('/content/downloadurl/describe').get(content_s3_sub_schema.get_describe_downloadurl); -// Router.route('/content/uploadurl/describe').get(content_s3_sub_schema.get_describe_uploadurl); - +Router.route('/user').get(user_route.get).post(user_route.post).patch(user_route.patch).delete(user_route.del); +Router.route('/user/describe').get(user_schema); +Router.route('/products').get(products_route.get).post(products_route.post).patch(products_route.patch).delete(products_route.del); +Router.route('/products/describe').get(products_schema); Router.route('/version').get(versionRoute.get); -//Router.use('/search', search_routes); - Router.route('/test').get(testRoute.get); export default Router; diff --git a/src/routes/api/v1/products.ts b/src/routes/api/v1/products.ts new file mode 100644 index 0000000..a9c0824 --- /dev/null +++ b/src/routes/api/v1/products.ts @@ -0,0 +1,164 @@ +import { Request, Response } from 'express'; +import db, { handlePrismaError } from '../../../handlers/db.js'; // Database +import log from '../../../handlers/log.js'; +import { parseDynamicSortBy } from '../../../helpers/prisma_helpers.js'; +import { schema_get, schema_post, schema_patch, schema_del } from './products_schema.js'; + +// MARK: GET products +async function get(req: Request, res: Response) { + const { error, value } = schema_get.validate(req.query); + if (error) { + log.api?.debug('GET products Error:', req.query, value, error.details[0].message); + res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: error.details[0].message }); + } else { + log.api?.debug('GET products Success:', req.query, value); + + if (value.search !== undefined || value.id !== undefined) { + // if search or get by id + await db + .$transaction([ + // Same query for count and findMany + db.products.count({ + where: { + OR: [{ id: value.id }, { gtin: value.gtin }, { name: { search: value.search } }] + }, + orderBy: parseDynamicSortBy(value.sort.toString(), value.order.toString()), + skip: value.skip, + take: value.take + }), + db.products.findMany({ + where: { + OR: [{ id: value.id }, { gtin: value.gtin }, { name: { search: value.search } }] + }, + orderBy: parseDynamicSortBy(value.sort.toString(), value.order.toString()), + skip: value.skip, + take: value.take + }) + ]) + .then(([count, result]) => { + if (result.length !== 0) { + res.status(200).json({ count, result }); + } else { + res.status(404).json({ status: 'ERROR', errorcode: 'NOT_FOUND', message: 'Could not find specified product' }); + } + }) + .catch((err) => { + handlePrismaError(err, res, 'GET products'); + }); + } else { + // get all + await db + .$transaction([ + // Same query for count and findMany + db.products.count({ + orderBy: parseDynamicSortBy(value.sort.toString(), value.order.toString()), + skip: value.skip, + take: value.take + }), + db.products.findMany({ + orderBy: parseDynamicSortBy(value.sort.toString(), value.order.toString()), + skip: value.skip, + take: value.take + }) + ]) + .then(([count, result]) => { + if (result.length !== 0) { + res.status(200).json({ count, result }); + } else { + res.status(404).json({ status: 'ERROR', errorcode: 'NOT_FOUND', message: 'Could not find specified product' }); + } + }) + .catch((err) => { + handlePrismaError(err, res, 'GET products'); + }); + } + } +} + +// MARK: CREATE products +async function post(req: Request, res: Response) { + const { error, value } = schema_post.validate(req.body); + if (error) { + log.api?.debug('POST products Error:', req.body, value, error.details[0].message); + res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: error.details[0].message }); + } else { + log.api?.debug('POST products Success:', req.body, value); + await db.products + .create({ + data: { + id: value.id, + gtin: value.gtin, + name: value.name, + price: value.price, + stock: value.stock, + visible: value.visible + }, + select: { + id: true + } + }) + .then((result) => { + res.status(201).json({ status: 'CREATED', message: 'Successfully created product', id: result.id }); + }) + .catch((err) => { + handlePrismaError(err, res, 'POST products'); + }); + } +} + +// MARK: UPDATE products +async function patch(req: Request, res: Response) { + const { error, value } = schema_patch.validate(req.body); + if (error) { + log.api?.debug('PATCH products Error:', req.body, value, error.details[0].message); + res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: error.details[0].message }); + } else { + log.api?.debug('PATCH products Success:', req.body, value); + await db.products + .update({ + where: { + id: value.id + }, + data: { + gtin: value.gtin, + name: value.name, + price: value.price, + stock: value.stock, + visible: value.visible + }, + select: { + id: true + } + }) + .then((result) => { + res.status(200).json({ status: 'UPDATED', message: 'Successfully updated product', id: result.id }); + }) + .catch((err) => { + handlePrismaError(err, res, 'PATCH products'); + }); + } +} + +// MARK: DELETE products +async function del(req: Request, res: Response) { + const { error, value } = schema_del.validate(req.body); + if (error) { + log.api?.debug('DEL products Error:', req.body, value, error.details[0].message); + res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: error.details[0].message }); + } else { + log.api?.debug('DEL products Success:', req.body, value); + await db.products + .delete({ + where: { + id: value.id + } + }) + .then((result) => { + res.status(200).json({ status: 'DELETED', message: 'Successfully deleted product', id: result.id }); + }).catch((err) => { + handlePrismaError(err, res, 'DEL products'); + }); + } +} + +export default { get, post, patch, del }; diff --git a/src/routes/api/v1/products_schema.ts b/src/routes/api/v1/products_schema.ts new file mode 100644 index 0000000..2ffa16e --- /dev/null +++ b/src/routes/api/v1/products_schema.ts @@ -0,0 +1,85 @@ +import { Request, Response } from 'express'; +import validator from 'joi'; // DOCS: https://joi.dev/api +import { Prisma } from '@prisma/client'; + +// MARK: GET products +const schema_get = validator + .object({ + sort: validator + .string() + .valid(...Object.keys(Prisma.ProductsScalarFieldEnum)) + .default('id'), + + order: validator.string().valid('asc', 'desc').default('asc'), + take: validator.number().min(1).max(512), + skip: validator.number().min(0), + // This regex ensures that the search string does not contain consecutive asterisks (**) and is at least 3 characters long. + search: validator.string().min(3).max(20).regex(new RegExp('^(?!.*\\*{2,}).*$')), + id: validator.number().positive().precision(0), + gtin: validator + .string() + .min(8) + .max(14) + .trim() + .regex(new RegExp(/^[0-9]+$/)) + }) + // Allow only id, gtin, search or none of them + // xor does not work because id, gtin and search fields are not mandatory + .without('id', ['gtin', 'search']) + .without('gtin', ['id', 'search']) + .without('search', ['id', 'gtin']); + +// MARK: CREATE products +const schema_post = validator.object({ + gtin: validator + .string() + .min(8) + .max(14) + .trim() + .regex(new RegExp(/^[0-9]+$/)) + .required(), + name: validator.string().min(1).max(32).required(), + price: validator.number().positive().allow(0).precision(2).required(), // TODO: Check if 0 is allowed (https://github.com/hapijs/joi/issues/772) + stock: validator.number().precision(0).required(), + visible: validator.boolean().default(true) +}); + +// MARK: UPDATE products +const schema_patch = validator + .object({ + id: validator.number().positive().precision(0), + gtin: validator + .string() + .min(8) + .max(14) + .trim() + .regex(new RegExp(/^[0-9]+$/)), + name: validator.string().min(1).max(32), + price: validator.number().positive().allow(0).precision(2), // TODO: Check if 0 is allowed (https://github.com/hapijs/joi/issues/772) + stock: validator.number().precision(0), + visible: validator.boolean() + }) + .or('gtin', 'name', 'price', 'stock', 'visible'); + +// MARK: DELETE products +const schema_del = validator.object({ + id: validator.number().positive().precision(0) +}); + +// Describe all schemas +const schema_get_desc = schema_get.describe(); +const schema_post_desc = schema_post.describe(); +const schema_patch_desc = schema_patch.describe(); +const schema_del_desc = schema_del.describe(); + +// GET route +export default async function get(req: Request, res: Response) { + res.status(200).json({ + GET: schema_get_desc, + POST: schema_post_desc, + PATCH: schema_patch_desc, + DELETE: schema_del_desc + }); +} + +export { schema_get, schema_post, schema_patch, schema_del };