From b288e40d13579df4ed717403198a398ce023f460 Mon Sep 17 00:00:00 2001 From: nostrdev-com <support@nostrdev.com> Date: Thu, 3 Apr 2025 10:16:55 +0300 Subject: [PATCH] feat(wine): added payload validation --- src/models/wine.ts | 14 +++++--- src/routes/wines.router.ts | 68 ++++++++++++++++++++++++++++------- src/types/product.ts | 11 +++++- src/types/wine.ts | 19 ++++++++-- src/utils/alcohol.ts | 10 ++++++ src/utils/index.ts | 2 ++ src/utils/utils.ts | 2 ++ src/utils/validation/index.ts | 1 + src/utils/validation/wine.ts | 43 ++++++++++++++++++++++ 9 files changed, 149 insertions(+), 21 deletions(-) create mode 100644 src/utils/alcohol.ts create mode 100644 src/utils/utils.ts create mode 100644 src/utils/validation/wine.ts diff --git a/src/models/wine.ts b/src/models/wine.ts index 4a1ab9c..8df38df 100644 --- a/src/models/wine.ts +++ b/src/models/wine.ts @@ -1,5 +1,11 @@ import { ObjectId } from 'mongodb' -import { WineType, Viticulture, BottleClosure, Vintage } from '../types' +import { + WineType, + Viticulture, + BottleClosure, + VintageOptions, + StandardDrinks100ml +} from '../types' import { Alpha2Code } from 'i18n-iso-countries' import { CurrencyCode } from 'currency-codes-ts/dist/types' @@ -16,10 +22,10 @@ export class Wine { public name: string, // label public producerId: ObjectId, // product producer public varietal: string, // if more than one, list as 'blend' - public vintage: Vintage, // year, nv (non-vintage) or mv (multi-vintage) + public vintage: number | VintageOptions, // year, nv (non-vintage) or mv (multi-vintage) public alcohol: number, // alcohol percentage - public standardDrinks100ml: number, // number representing an amount of standard drinks per bottle - public viticulture: Viticulture, // two-letter country codes + public standardDrinks100ml: StandardDrinks100ml, // an amount of standard drinks per 100ml in AU, UK and US + public viticulture: Viticulture, // viticulture public sulfites: number, // parts per million public filtered: boolean, // is wine filtered (fined (egg or fish)) public vegan: boolean, // if wine is vegan diff --git a/src/routes/wines.router.ts b/src/routes/wines.router.ts index e83e397..b7ca3f3 100644 --- a/src/routes/wines.router.ts +++ b/src/routes/wines.router.ts @@ -1,6 +1,13 @@ import express, { Request, Response } from 'express' import { collections } from '../services/database.service' import { Wine } from '../models' +import { + wineValidation, + handleReqError, + handleReqSuccess, + alcoholToStandardDrinks +} from '../utils' +import Joi from 'joi' export const winesRouter = express.Router() @@ -24,22 +31,57 @@ winesRouter.get('/', async (_req: Request, res: Response) => { // POST winesRouter.post('/', async (req: Request, res: Response) => { try { - const wine = req.body as Wine + const { + error, + value: wine + }: { error: Joi.ValidationError | undefined; value: Wine } = wineValidation( + req.body + ) + + if (error) { + throw error.details[0].message + } + + const { productCodeEAN, productCodeUPC, productCodeSKU } = wine + + if (!productCodeEAN && !productCodeUPC && !productCodeSKU) { + throw new Error( + 'provide "productCodeEAN", "productCodeUPC" or "productCodeSKU"' + ) + } + + if (productCodeEAN) { + const existingWine = await collections.wines?.findOne({ + productCodeEAN + }) + + if (existingWine) { + throw new Error('wine with provided "productCodeEAN" exists') + } + } else if (productCodeUPC) { + const existingWine = await collections.wines?.findOne({ + productCodeUPC + }) + + if (existingWine) { + throw new Error('wine with provided "productCodeUPC" exists') + } + } else { + const existingWine = await collections.wines?.findOne({ + productCodeSKU + }) + + if (existingWine) { + throw new Error('wine with provided "productCodeSKU" exists') + } + } + + wine.standardDrinks100ml = alcoholToStandardDrinks(wine.alcohol) const result = await collections.wines?.insertOne(wine) - if (result) { - res - .status(201) - .send(`Successfully created a new wine with id ${result.insertedId}`) - } else { - res.status(500).send('Failed to create a new wine.') - } + handleReqSuccess(res, result, 'wine') } catch (error: unknown) { - console.error(error) - - if (error instanceof Error) { - res.status(400).send(error.message) - } + handleReqError(res, error) } }) diff --git a/src/types/product.ts b/src/types/product.ts index 653a622..d562f86 100644 --- a/src/types/product.ts +++ b/src/types/product.ts @@ -61,4 +61,13 @@ export type Ingredient = | 'Pecan' | 'Walnut' -export type Vintage = number | 'nv' | 'mv' +export enum VintageOptions { + NV = 'nv', + MV = 'mv' +} + +export interface StandardDrinks100ml { + AU: number + UK: number + US: number +} diff --git a/src/types/wine.ts b/src/types/wine.ts index fabc693..c26bccd 100644 --- a/src/types/wine.ts +++ b/src/types/wine.ts @@ -1,5 +1,18 @@ -export type WineType = 'white' | 'amber' | 'rose' | 'red' +export enum WineType { + White = 'white', + Amber = 'amber', + Rose = 'rose', + Red = 'red' +} -export type Viticulture = 'biodynamic' | 'organic' | 'conventional' +export enum Viticulture { + Biodynamic = 'biodynamic', + Organic = 'organic', + Conventional = 'conventional' +} -export type BottleClosure = 'cork' | 'crown-seal' | 'screwcap' +export enum BottleClosure { + Cork = 'cork', + CrownSeal = 'crown-seal', + Screwcap = 'screwcap' +} diff --git a/src/utils/alcohol.ts b/src/utils/alcohol.ts new file mode 100644 index 0000000..3cf3f3a --- /dev/null +++ b/src/utils/alcohol.ts @@ -0,0 +1,10 @@ +import { StandardDrinks100ml } from '../types' +import { roundToOneDecimal } from './' + +export const alcoholToStandardDrinks = ( + alcohol: number +): StandardDrinks100ml => ({ + UK: roundToOneDecimal(10 * alcohol), + AU: roundToOneDecimal(7.91 * alcohol), + US: roundToOneDecimal(5.64 * alcohol) +}) diff --git a/src/utils/index.ts b/src/utils/index.ts index 086db4e..8455920 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,3 +1,5 @@ export * from './validation' export * from './nostr' export * from './route' +export * from './utils' +export * from './alcohol' diff --git a/src/utils/utils.ts b/src/utils/utils.ts new file mode 100644 index 0000000..5019bf3 --- /dev/null +++ b/src/utils/utils.ts @@ -0,0 +1,2 @@ +export const roundToOneDecimal = (number: number) => + Math.round(number * 10) / 10 diff --git a/src/utils/validation/index.ts b/src/utils/validation/index.ts index baa0c5a..33668f0 100644 --- a/src/utils/validation/index.ts +++ b/src/utils/validation/index.ts @@ -1,3 +1,4 @@ export * from './user' export * from './nostr' export * from './review' +export * from './wine' diff --git a/src/utils/validation/wine.ts b/src/utils/validation/wine.ts new file mode 100644 index 0000000..fa90e9e --- /dev/null +++ b/src/utils/validation/wine.ts @@ -0,0 +1,43 @@ +import Joi from 'joi' +import { + WineType, + VintageOptions, + BottleClosure, + Viticulture +} from '../../types' + +export const wineValidation = (data: unknown): Joi.ValidationResult => + Joi.object({ + productCodeEAN: Joi.string().allow('').required(), + productCodeUPC: Joi.string().allow('').required(), + productCodeSKU: Joi.string().allow('').required(), + type: Joi.string() + .valid(...Object.values(WineType)) + .required(), + style: Joi.string().required(), + characteristic: Joi.string().required(), + country: Joi.string().length(2), + region: Joi.string().required(), + name: Joi.string().required(), + producerId: Joi.string().length(24).required(), + varietal: Joi.string().required(), + vintage: Joi.alternatives() + .try(Joi.string().valid(...Object.values(VintageOptions)), Joi.number()) + .required(), + alcohol: Joi.number().min(0).max(0.99).required(), + viticulture: Joi.string() + .valid(...Object.values(Viticulture)) + .required(), + sulfites: Joi.number().required(), + filtered: Joi.boolean().required(), + vegan: Joi.boolean().required(), + kosher: Joi.boolean().required(), + closure: Joi.string() + .valid(...Object.values(BottleClosure)) + .required(), + RRPamount: Joi.number().required(), + RRPcurrency: Joi.string().length(3).required(), + description: Joi.string().required(), + url: Joi.string(), + image: Joi.string() + }).validate(data) -- 2.43.0