Compare commits
2 Commits
0371fb55da
...
ff9516ed66
Author | SHA1 | Date | |
---|---|---|---|
ff9516ed66 | |||
|
b288e40d13 |
src
models
routes
types
utils
@ -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
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
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'
|
||||
|
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