Merge pull request 'feat(wine): added payload validation' () from payload-validation into staging

Reviewed-on: 
This commit is contained in:
Otto 2025-04-03 07:18:02 +00:00
commit ff9516ed66
9 changed files with 149 additions and 21 deletions

@ -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

@ -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)
}
})

@ -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
}

@ -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'
}

10
src/utils/alcohol.ts Normal file

@ -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)
})

@ -1,3 +1,5 @@
export * from './validation'
export * from './nostr'
export * from './route'
export * from './utils'
export * from './alcohol'

2
src/utils/utils.ts Normal file

@ -0,0 +1,2 @@
export const roundToOneDecimal = (number: number) =>
Math.round(number * 10) / 10

@ -1,3 +1,4 @@
export * from './user'
export * from './nostr'
export * from './review'
export * from './wine'

@ -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)