feat(wine): added payload validation #32
src
models
routes
types
utils
@ -1,5 +1,11 @@
|
|||||||
import { ObjectId } from 'mongodb'
|
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 { Alpha2Code } from 'i18n-iso-countries'
|
||||||
import { CurrencyCode } from 'currency-codes-ts/dist/types'
|
import { CurrencyCode } from 'currency-codes-ts/dist/types'
|
||||||
|
|
||||||
@ -16,10 +22,10 @@ export class Wine {
|
|||||||
public name: string, // label
|
public name: string, // label
|
||||||
public producerId: ObjectId, // product producer
|
public producerId: ObjectId, // product producer
|
||||||
public varietal: string, // if more than one, list as 'blend'
|
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 alcohol: number, // alcohol percentage
|
||||||
public standardDrinks100ml: number, // number representing an amount of standard drinks per bottle
|
public standardDrinks100ml: StandardDrinks100ml, // an amount of standard drinks per 100ml in AU, UK and US
|
||||||
public viticulture: Viticulture, // two-letter country codes
|
public viticulture: Viticulture, // viticulture
|
||||||
public sulfites: number, // parts per million
|
public sulfites: number, // parts per million
|
||||||
public filtered: boolean, // is wine filtered (fined (egg or fish))
|
public filtered: boolean, // is wine filtered (fined (egg or fish))
|
||||||
public vegan: boolean, // if wine is vegan
|
public vegan: boolean, // if wine is vegan
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
import express, { Request, Response } from 'express'
|
import express, { Request, Response } from 'express'
|
||||||
import { collections } from '../services/database.service'
|
import { collections } from '../services/database.service'
|
||||||
import { Wine } from '../models'
|
import { Wine } from '../models'
|
||||||
|
import {
|
||||||
|
wineValidation,
|
||||||
|
handleReqError,
|
||||||
|
handleReqSuccess,
|
||||||
|
alcoholToStandardDrinks
|
||||||
|
} from '../utils'
|
||||||
|
import Joi from 'joi'
|
||||||
|
|
||||||
export const winesRouter = express.Router()
|
export const winesRouter = express.Router()
|
||||||
|
|
||||||
@ -24,22 +31,57 @@ winesRouter.get('/', async (_req: Request, res: Response) => {
|
|||||||
// POST
|
// POST
|
||||||
winesRouter.post('/', async (req: Request, res: Response) => {
|
winesRouter.post('/', async (req: Request, res: Response) => {
|
||||||
try {
|
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)
|
const result = await collections.wines?.insertOne(wine)
|
||||||
|
|
||||||
if (result) {
|
handleReqSuccess(res, result, 'wine')
|
||||||
res
|
|
||||||
.status(201)
|
|
||||||
.send(`Successfully created a new wine with id ${result.insertedId}`)
|
|
||||||
} else {
|
|
||||||
res.status(500).send('Failed to create a new wine.')
|
|
||||||
}
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
console.error(error)
|
handleReqError(res, error)
|
||||||
|
|
||||||
if (error instanceof Error) {
|
|
||||||
res.status(400).send(error.message)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -61,4 +61,13 @@ export type Ingredient =
|
|||||||
| 'Pecan'
|
| 'Pecan'
|
||||||
| 'Walnut'
|
| '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
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 './validation'
|
||||||
export * from './nostr'
|
export * from './nostr'
|
||||||
export * from './route'
|
export * from './route'
|
||||||
|
export * from './utils'
|
||||||
|
export * from './alcohol'
|
||||||
|
2
src/utils/utils.ts
Normal file
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 './user'
|
||||||
export * from './nostr'
|
export * from './nostr'
|
||||||
export * from './review'
|
export * from './review'
|
||||||
|
export * from './wine'
|
||||||
|
43
src/utils/validation/wine.ts
Normal file
43
src/utils/validation/wine.ts
Normal file
@ -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)
|
Loading…
x
Reference in New Issue
Block a user