Merge pull request 'tasting-notes' () from tasting-notes into staging

Reviewed-on: 
This commit is contained in:
Otto 2025-04-16 09:23:00 +00:00
commit 5136cb2a74
33 changed files with 1668 additions and 306 deletions

@ -11,6 +11,8 @@
"Blanco",
"Cachaça",
"Caturra",
"colour",
"Colours",
"Coren",
"EAN",
"espadín",

@ -10,7 +10,7 @@ import {
spiritsRouter,
coffeeRouter
} from './routes'
import { Routes } from './types'
import { Route } from './types'
dotenv.config()
@ -19,13 +19,13 @@ const port = process.env.PORT || 3000
connectToDatabase()
.then(() => {
app.use(Routes.Users, usersRouter)
app.use(Routes.NostrEvents, nostrRouter)
app.use(Routes.Reviews, reviewsRouter)
app.use(Routes.Wines, winesRouter)
app.use(Routes.Sake, sakeRouter)
app.use(Routes.Spirits, spiritsRouter)
app.use(Routes.Coffee, coffeeRouter)
app.use(Route.Users, usersRouter)
app.use(Route.NostrEvents, nostrRouter)
app.use(Route.Reviews, reviewsRouter)
app.use(Route.Wines, winesRouter)
app.use(Route.Sake, sakeRouter)
app.use(Route.Spirits, spiritsRouter)
app.use(Route.Coffee, coffeeRouter)
app.listen(port, () => {
console.log(`Server started at http://localhost:${port}`)

@ -1,13 +1,14 @@
import { ObjectId } from 'mongodb'
import { RatingOptions } from '../types'
import { ProductType, RatingOption, TastingNote } from '../types'
export class Review {
constructor(
public eventId: string, // foreign key referencing the nostrEvents collection
public productId: string, // unique identifier for the product
public rating: number | RatingOptions, // numerical rating, e.g., 84-100 or NS (no score)
public productType: ProductType, // product type
public rating: number | RatingOption, // numerical rating, e.g., 84-100 or NS (no score)
public reviewText: string, // text content of the review
public tastingNotes: string[], // array of tasting notes, e.g., flavours, aromas
public tastingNote: TastingNote, // an object representing tasting notes
public id?: ObjectId // database object id
) {}
}

@ -2,9 +2,9 @@ import { ObjectId } from 'mongodb'
import {
SakeDesignation,
SakeStarter,
VintageOptions,
SakeCharacteristics,
StandardDrinks,
VintageOption,
SakeCharacteristic,
StandardDrink,
SakeVolume,
RiceVarietal,
SakeYeastStrain,
@ -24,15 +24,15 @@ export class Sake {
public producerId: ObjectId, // product producer
public designation: SakeDesignation, // table, pure, blended
public polishRate: number, // %
public characteristics: SakeCharacteristics[],
public characteristic: SakeCharacteristic,
public starter: SakeStarter, // sake starter
public yeastStrain: SakeYeastStrain,
public volume: SakeVolume, // bottle volume
public alcohol: number, // alcohol percentage
public standardDrinks: StandardDrinks, // number representing an amount of standard drinks per bottle per 100ml
public standardDrinks: StandardDrink, // number representing an amount of standard drinks per bottle per 100ml
public riceVarietal: RiceVarietal[], // if more than one, list as 'blend'
public koji: SakeKoji, // if more than one, list as 'blend'
public vintage: number | VintageOptions, // year, nv (non-vintage) or mv (multi-vintage)
public vintage: number | VintageOption, // year, nv (non-vintage) or mv (multi-vintage)
public RRPamount: number, // 20
public RRPcurrency: CurrencyCode, // USD
public description: string, // detailed description of the product

@ -3,10 +3,10 @@ import {
SpiritType,
SpiritVariant,
Ingredient,
VintageOptions,
StandardDrinks,
VintageOption,
StandardDrink,
SpiritVolume,
SpiritCharacteristics
SpiritCharacteristic
} from '../types'
import { Alpha2Code } from 'i18n-iso-countries'
import { CurrencyCode } from 'currency-codes-ts/dist/types'
@ -22,12 +22,12 @@ export class Spirit {
public producerId: ObjectId, // product producer
public type: SpiritType, // spirit type
public variant: SpiritVariant, // vodka, rum, liqueur cream, etc
public characteristics: SpiritCharacteristics, // light aromatic, textural, fruit forward, structural & savoury, powerful
public characteristic: SpiritCharacteristic, // light aromatic, textural, fruit forward, structural & savoury, powerful
public ingredients: Ingredient[], // an array of ingredients(flavouring)
public volume: SpiritVolume, // bottle volume
public alcohol: number, // alcohol percentage
public vintage: number | VintageOptions, // year, nv (non-vintage) or mv (multi-vintage)
public standardDrinks: StandardDrinks, // an amount of standard drinks per 100ml and bottle in AU, UK and US
public vintage: number | VintageOption, // year, nv (non-vintage) or mv (multi-vintage)
public standardDrinks: StandardDrink, // an amount of standard drinks per 100ml and bottle in AU, UK and US
public RRPamount: number, // 20
public RRPcurrency: CurrencyCode, // USD
public description: string, // detailed description of the product

@ -3,8 +3,8 @@ import {
WineType,
Viticulture,
BottleClosure,
VintageOptions,
StandardDrinks,
VintageOption,
StandardDrink,
WineRegion,
WineVolume,
WineStyle,
@ -24,7 +24,7 @@ export class Wine {
public productCodeSKU: string, // Stock keeping unit (https://en.wikipedia.org/wiki/Stock_keeping_unit)
public type: WineType, // numerical rating, e.g., 1-100
public style: WineStyle, // bubbles+fizz, table, dessert, fortified, vermouth
public characteristics: (
public characteristic: (
| WhiteWineCharacteristic
| AmberWineCharacteristic
| RoseWineCharacteristic
@ -35,10 +35,10 @@ export class Wine {
public name: string, // label
public producerId: ObjectId, // product producer
public grapeVarietal: GrapeVarietal[], // if more than one, list as 'blend'
public vintage: number | VintageOptions, // year, nv (non-vintage) or mv (multi-vintage)
public vintage: number | VintageOption, // year, nv (non-vintage) or mv (multi-vintage)
public volume: WineVolume, // bottle volume
public alcohol: number, // alcohol percentage
public standardDrinks: StandardDrinks, // an amount of standard drinks per 100ml and bottle in AU, UK and US
public standardDrinks: StandardDrink, // an amount of standard drinks per 100ml and bottle 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))

@ -3,6 +3,8 @@ import { collections } from '../services/database.service'
import { Review } from '../models'
import { reviewValidation, handleReqError, handleReqSuccess } from '../utils'
import Joi from 'joi'
import { DBcollection } from '../types'
import { BSON } from 'mongodb'
export const reviewsRouter = express.Router()
@ -11,7 +13,7 @@ reviewsRouter.use(express.json())
// GET
reviewsRouter.get('/', async (_req: Request, res: Response) => {
try {
const reviews = await collections.reviews?.find({}).toArray()
const reviews = await collections[DBcollection.Reviews]?.find({}).toArray()
res.status(200).send(reviews)
} catch (error: unknown) {
@ -36,7 +38,7 @@ reviewsRouter.post('/', async (req: Request, res: Response) => {
throw error.details[0].message
}
const existingReview = await collections.reviews?.findOne({
const existingReview = await collections[DBcollection.Reviews]?.findOne({
eventId: review.eventId
})
@ -44,7 +46,40 @@ reviewsRouter.post('/', async (req: Request, res: Response) => {
throw new Error('review with provided "eventId" exists')
}
const result = await collections.reviews?.insertOne(review)
const { productId } = review
const _id = new BSON.ObjectId(productId)
const existingWine = await collections[DBcollection.Wines]?.findOne({
_id
})
if (!existingWine) {
const existingSake = await collections[DBcollection.Sake]?.findOne({
_id
})
if (!existingSake) {
const existingSpirit = await collections[DBcollection.Spirits]?.findOne(
{
_id
}
)
if (!existingSpirit) {
const existingCoffee = await collections[
DBcollection.Coffee
]?.findOne({
_id
})
if (!existingCoffee) {
throw new Error('product with provided "productId" does not exists')
}
}
}
}
const result = await collections[DBcollection.Reviews]?.insertOne(review)
handleReqSuccess(res, result, 'review')
} catch (error: unknown) {

@ -10,7 +10,7 @@ import {
volumeToMl
} from '../utils'
import Joi from 'joi'
import { DBcollections, ProductType } from '../types'
import { DBcollection, ProductType } from '../types'
export const sakeRouter = express.Router()
@ -51,7 +51,7 @@ sakeRouter.post('/', async (req: Request, res: Response) => {
productCodeEAN,
productCodeUPC,
productCodeSKU,
DBcollections.Sake,
DBcollection.Sake,
ProductType.Sake
)
@ -60,7 +60,7 @@ sakeRouter.post('/', async (req: Request, res: Response) => {
volumeToMl(sake.volume)
)
const result = await collections[DBcollections.Sake]?.insertOne(sake)
const result = await collections[DBcollection.Sake]?.insertOne(sake)
handleReqSuccess(res, result, ProductType.Spirit)
} catch (error: unknown) {

@ -10,7 +10,7 @@ import {
volumeToMl
} from '../utils'
import Joi from 'joi'
import { DBcollections, ProductType } from '../types'
import { DBcollection, ProductType } from '../types'
export const spiritsRouter = express.Router()
@ -50,7 +50,7 @@ spiritsRouter.post('/', async (req: Request, res: Response) => {
productCodeEAN,
productCodeUPC,
productCodeSKU,
DBcollections.Spirits,
DBcollection.Spirits,
ProductType.Spirit
)
@ -59,7 +59,7 @@ spiritsRouter.post('/', async (req: Request, res: Response) => {
volumeToMl(spirit.volume)
)
const result = await collections[DBcollections.Spirits]?.insertOne(spirit)
const result = await collections[DBcollection.Spirits]?.insertOne(spirit)
handleReqSuccess(res, result, ProductType.Spirit)
} catch (error: unknown) {

@ -10,7 +10,7 @@ import {
volumeToMl
} from '../utils'
import Joi from 'joi'
import { DBcollections, ProductType } from '../types'
import { DBcollection, ProductType } from '../types'
export const winesRouter = express.Router()
@ -19,7 +19,7 @@ winesRouter.use(express.json())
// GET
winesRouter.get('/', async (_req: Request, res: Response) => {
try {
const wines = await collections[DBcollections.Wines]?.find({}).toArray()
const wines = await collections[DBcollection.Wines]?.find({}).toArray()
res.status(200).send(wines)
} catch (error: unknown) {
@ -51,7 +51,7 @@ winesRouter.post('/', async (req: Request, res: Response) => {
productCodeEAN,
productCodeUPC,
productCodeSKU,
DBcollections.Wines,
DBcollection.Wines,
ProductType.Wine
)
@ -60,7 +60,7 @@ winesRouter.post('/', async (req: Request, res: Response) => {
volumeToMl(wine.volume)
)
const result = await collections[DBcollections.Wines]?.insertOne(wine)
const result = await collections[DBcollection.Wines]?.insertOne(wine)
handleReqSuccess(res, result, ProductType.Wine)
} catch (error: unknown) {

@ -1,16 +1,16 @@
import * as mongoDB from 'mongodb'
import * as dotenv from 'dotenv'
import { DBcollections } from '../types'
import { DBcollection } from '../types'
// Global Variables
export const collections: {
[DBcollections.Users]?: mongoDB.Collection
[DBcollections.NostrEvents]?: mongoDB.Collection
[DBcollections.Reviews]?: mongoDB.Collection
[DBcollections.Wines]?: mongoDB.Collection
[DBcollections.Sake]?: mongoDB.Collection
[DBcollections.Spirits]?: mongoDB.Collection
[DBcollections.Coffee]?: mongoDB.Collection
[DBcollection.Users]?: mongoDB.Collection
[DBcollection.NostrEvents]?: mongoDB.Collection
[DBcollection.Reviews]?: mongoDB.Collection
[DBcollection.Wines]?: mongoDB.Collection
[DBcollection.Sake]?: mongoDB.Collection
[DBcollection.Spirits]?: mongoDB.Collection
[DBcollection.Coffee]?: mongoDB.Collection
} = {}
// Initialize Connection
@ -29,20 +29,20 @@ export async function connectToDatabase() {
const db: mongoDB.Db = client.db(process.env.DB_NAME)
const usersCollection: mongoDB.Collection = db.collection(DBcollections.Users)
const usersCollection: mongoDB.Collection = db.collection(DBcollection.Users)
const nostrEventsCollection: mongoDB.Collection = db.collection(
DBcollections.NostrEvents
DBcollection.NostrEvents
)
const reviewsCollection: mongoDB.Collection = db.collection(
DBcollections.Reviews
DBcollection.Reviews
)
const winesCollection: mongoDB.Collection = db.collection(DBcollections.Wines)
const sakeCollection: mongoDB.Collection = db.collection(DBcollections.Sake)
const winesCollection: mongoDB.Collection = db.collection(DBcollection.Wines)
const sakeCollection: mongoDB.Collection = db.collection(DBcollection.Sake)
const spiritsCollection: mongoDB.Collection = db.collection(
DBcollections.Spirits
DBcollection.Spirits
)
const coffeeCollection: mongoDB.Collection = db.collection(
DBcollections.Coffee
DBcollection.Coffee
)
collections.users = usersCollection

@ -1,4 +1,4 @@
export enum DBcollections {
export enum DBcollection {
Users = 'users',
NostrEvents = 'nostrEvents',
Reviews = 'reviews',

@ -6,3 +6,5 @@ export * from './wine'
export * from './sake'
export * from './spirit'
export * from './coffee'
export * from './review'
export * from './review/'

@ -61,20 +61,16 @@ export enum Ingredient {
Walnut = 'Walnut'
}
export enum VintageOptions {
export enum VintageOption {
NV = 'NV',
MV = 'MV'
}
export interface StandardDrinks {
export interface StandardDrink {
'100ml': { AU: number; UK: number; US: number }
bottle: { AU: number; UK: number; US: number }
}
export enum RatingOptions {
NoScore = 'NS'
}
export enum ProductType {
Wine = 'Wine',
Spirit = 'Spirit',

125
src/types/review.ts Normal file

@ -0,0 +1,125 @@
import {
VisualAssessmentKey,
ClarityVisualAssessment,
NatureVisualAssessment,
WhiteColour,
AmberColour,
RoseColour,
RedColour,
BlueColour,
GreenColour,
PrimaryFlavoursAndAromasKey,
Condition,
Intensity,
Age,
CitrusFruit,
AppleFruit,
StoneFruit,
RedFruit,
BlackFruit,
ChocolateFruit,
TropicalFruit,
MelonFruit,
Floral,
Vegetal,
Earth,
Microbial,
Oak,
Chocolate,
Oxidation,
Umami,
Balsamic,
Grain,
Dairy,
Anisoles,
Brettanomyces,
VolatileAcidity,
Reduction,
TextureAndBalanceKey,
Sweetness,
Concentration,
TanninType,
RipeTannin,
UnripeTannin,
Body,
FlavourIntensity,
PalateLength,
ReasoningConcentration,
Quality,
ReadinessToDrink,
ReasoningKey
} from './review/'
export enum RatingOption {
NoScore = 'NS'
}
export enum TastingNoteKey {
VisualAssessment = 'visualAssessment',
PrimaryFlavoursAndAromas = 'primaryFlavoursAndAromas',
TextureAndBalance = 'textureAndBalance'
}
export interface TastingNote {
[TastingNoteKey.VisualAssessment]: {
[VisualAssessmentKey.Clarity]: ClarityVisualAssessment
[VisualAssessmentKey.Nature]: NatureVisualAssessment
[VisualAssessmentKey.Colour]:
| WhiteColour
| AmberColour
| RoseColour
| RedColour
| BlueColour
| GreenColour
}
[TastingNoteKey.PrimaryFlavoursAndAromas]: {
[PrimaryFlavoursAndAromasKey.Condition]: Condition
[PrimaryFlavoursAndAromasKey.Intensity]: Intensity
[PrimaryFlavoursAndAromasKey.Age]: Age
[PrimaryFlavoursAndAromasKey.Fruit]:
| CitrusFruit
| AppleFruit
| StoneFruit
| RedFruit
| BlackFruit
| BlackFruit
| ChocolateFruit
| TropicalFruit
| MelonFruit
[PrimaryFlavoursAndAromasKey.Floral]: Floral
[PrimaryFlavoursAndAromasKey.Vegetal]: Vegetal
[PrimaryFlavoursAndAromasKey.Earth]: Earth
[PrimaryFlavoursAndAromasKey.Microbial]: Microbial
[PrimaryFlavoursAndAromasKey.Oak]: Oak
[PrimaryFlavoursAndAromasKey.Chocolate]: Chocolate
[PrimaryFlavoursAndAromasKey.Oxidation]: Oxidation
[PrimaryFlavoursAndAromasKey.Umami]: Umami
[PrimaryFlavoursAndAromasKey.Balsamic]: Balsamic
[PrimaryFlavoursAndAromasKey.Grain]: Grain
[PrimaryFlavoursAndAromasKey.Dairy]: Dairy
[PrimaryFlavoursAndAromasKey.Faults]:
| Anisoles
| Brettanomyces
| VolatileAcidity
| Reduction
}
[TastingNoteKey.TextureAndBalance]: {
[TextureAndBalanceKey.Sweetness]: Sweetness
[TextureAndBalanceKey.Acidity]: Concentration
[TextureAndBalanceKey.Tannin]: {
[key in Concentration]: { [key in TanninType]: RipeTannin | UnripeTannin }
}
[TextureAndBalanceKey.Alcohol]: Concentration
[TextureAndBalanceKey.Body]: Body
[TextureAndBalanceKey.FlavourIntensity]: FlavourIntensity
[TextureAndBalanceKey.PalateLength]: PalateLength
[TextureAndBalanceKey.Reasoning]: {
[ReasoningKey.Balance]: boolean
[ReasoningKey.Concentration]: ReasoningConcentration
[ReasoningKey.Complex]: boolean
}
[TextureAndBalanceKey.Quality]: Quality
[TextureAndBalanceKey.Age]: number // tasting date-producers bottling date
[TextureAndBalanceKey.ReadinessToDrink]: ReadinessToDrink
}
}

@ -0,0 +1,93 @@
// Free Run (sake, wine, spirits)
export enum WhiteColour {
WaterWhite = 'Water white',
LemonGreen = 'Lemon-Green',
Lemon = 'Lemon',
Gold = 'Gold',
WhiteBrown = 'White-Brown' // FIXME: duplicate
}
// Skin Contact (wine, spirits)
export enum AmberColour {
AmberOrange = 'Amber-Orange',
Amber = 'Amber'
}
// Blush (wine, spirits)
export enum RoseColour {
Pink = 'Pink',
Salmon = 'Salmon',
RoseOrange = 'Rose-Orange', // FIXME: duplicate
OnionSkin = 'Onion-Skin'
}
// Extended Maceration (wine, spirits)
export enum RedColour {
Purple = 'Purple',
Ruby = 'Ruby',
Garnet = 'Garnet',
Tawny = 'Tawny',
RedBrown = 'Red-Brown'
}
// Liqueurs (spirits)
export enum BlueColour {
BluePale = 'Blue-Pale',
BlueDark = 'Blue-Dark'
}
// Liqueurs (spirits)
export enum GreenColour {
GreenPale = 'Green-Pale',
GreenDark = 'Green-Dark'
}
export enum WineColour {
WaterWhite = WhiteColour.WaterWhite,
LemonGreen = WhiteColour.LemonGreen,
Lemon = WhiteColour.Lemon,
Gold = WhiteColour.Gold,
WhiteBrown = WhiteColour.WhiteBrown,
AmberOrange = AmberColour.AmberOrange,
Amber = AmberColour.Amber,
Pink = RoseColour.Pink,
Salmon = RoseColour.Salmon,
OnionSkin = RoseColour.OnionSkin,
RoseOrange = RoseColour.RoseOrange,
Purple = RedColour.Purple,
Ruby = RedColour.Ruby,
Garnet = RedColour.Garnet,
Tawny = RedColour.Tawny,
RedBrown = RedColour.RedBrown
}
export enum SakeColour {
WaterWhite = WhiteColour.WaterWhite,
LemonGreen = WhiteColour.LemonGreen,
Lemon = WhiteColour.Lemon,
Gold = WhiteColour.Gold,
WhiteBrown = WhiteColour.WhiteBrown
}
export enum SpiritColour {
WaterWhite = WhiteColour.WaterWhite,
LemonGreen = WhiteColour.LemonGreen,
Lemon = WhiteColour.Lemon,
Gold = WhiteColour.Gold,
WhiteBrown = WhiteColour.WhiteBrown,
Amber = AmberColour.Amber,
AmberOrange = AmberColour.AmberOrange,
Pink = RoseColour.Pink,
Salmon = RoseColour.Salmon,
OnionSkin = RoseColour.OnionSkin,
RoseOrange = RoseColour.RoseOrange,
Purple = RedColour.Purple,
Ruby = RedColour.Ruby,
Garnet = RedColour.Garnet,
Tawny = RedColour.Tawny,
RedBrown = RedColour.RedBrown,
BluePale = BlueColour.BluePale,
BlueDark = BlueColour.BlueDark,
GreenPale = GreenColour.GreenPale,
GreenDark = GreenColour.GreenDark
}

@ -0,0 +1,4 @@
export * from './visualAssessment'
export * from './colour'
export * from './primaryFlavoursAndAromas'
export * from './textureAndBalance'

@ -0,0 +1,226 @@
export enum PrimaryFlavoursAndAromasKey {
Condition = 'condition',
Intensity = 'intensity',
Age = 'age',
Fruit = 'fruit',
Floral = 'floral',
Vegetal = 'vegetal',
Earth = 'earth',
Microbial = 'microbial',
Oak = 'oak',
Chocolate = 'chocolate',
Oxidation = 'oxidation',
Umami = 'umami',
Balsamic = 'balsamic',
Grain = 'grain',
Dairy = 'dairy',
Faults = 'faults'
}
export enum Condition {
Clean = 'Clean',
Unclean = 'Unclean'
}
export enum Intensity {
Light = 'Light',
Medium = 'Medium',
Pronounced = 'Pronounced'
}
export enum Age {
Youthful = 'Youthful',
Developing = 'Developing',
Developed = 'Developed',
Oxidised = 'Oxidised',
Passed = 'Passed'
}
export enum CitrusFruit {
Grapefruit = 'Grapefruit',
Lemon = 'Lemon',
Lime = 'Lime',
Marmalade = 'Marmalade',
Orange = 'Orange',
Yuzu = 'Yuzu'
}
export enum AppleFruit {
Green = 'Green',
Red = 'Red',
Ripe = 'Ripe'
}
export enum StoneFruit {
Apricot = 'Apricot ',
Nectarine = 'Nectarine',
Peach = 'Peach',
Plum = 'Plum '
}
export enum RedFruit {
Cherry = 'Cherry',
Cranberry = 'Cranberry',
Pomegranate = 'Pomegranate',
Raspberry = 'Raspberry',
SourCherry = 'Sour Cherry',
Strawberry = 'Strawberry'
}
export enum BlackFruit {
Blackberry = 'Blackberry',
Blackcurrant = 'Blackcurrant',
Boysenberry = 'Boysenberry',
Blueberry = 'Blueberry',
Olive = 'Olive'
}
export enum ChocolateFruit {
Chocolate = 'Chocolate'
}
export enum TropicalFruit {
Banana = 'Banana',
Mango = 'Mango',
Passionfruit = 'Passionfruit',
Pineapple = 'Pineapple'
}
export enum MelonFruit {
Cantaloupe = 'Cantaloupe',
Honeydew = 'Honeydew'
}
export enum Floral {
Acacia = 'Acacia',
Elderflower = 'Elderflower',
Hibiscus = 'Hibiscus',
Honeysuckle = 'Honeysuckle',
Jasmine = 'Jasmine',
Lavender = 'Lavender',
Lilac = 'Lilac',
OrangeBlossom = 'Orange Blossom',
Potpourri = 'Potpourri',
Rose = 'Rose',
Vanilla = 'Vanilla',
Violet = 'Violet'
}
export enum Vegetal {
Grass = 'Grass',
Hay = 'Hay',
Herbaceous = 'Herbaceous',
Basil = 'Basil',
Mint = 'Mint',
Eucalyptus = 'Eucalyptus',
BlackTea = 'Black Tea',
Capsicum = 'Capsicum',
Gooseberry = 'Gooseberry',
Tomato = 'Tomato'
}
export enum Earth {
Gravel = 'Gravel',
Kerosene = 'Kerosene',
RedBeet = 'Red Beet',
Rocks = 'Rocks',
Slate = 'Slate',
Soil = 'Soil',
Terracotta = 'Terracotta'
}
export enum Microbial {
Mushroom = 'Mushroom',
Botrytis = 'Botrytis',
Beeswax = 'Beeswax',
Ginger = 'Ginger',
Saffron = 'Saffron',
Yeast = 'Yeast',
Bread = 'Bread',
Brioche = 'Brioche',
Toast = 'Toast'
}
export enum Oak {
CigarBox = 'Cigar Box',
Coconut = 'Coconut',
Dill = 'Dill',
Smoke = 'Smoke',
Spices = 'Spices',
Cinnamon = 'Cinnamon',
Nutmeg = 'Nutmeg',
Cloves = 'Cloves'
}
export enum Chocolate {
MilkChocolate = 'Milk Chocolate',
DarkChocolate = 'Dark Chocolate'
}
export enum Oxidation {
Aldehydes = 'Aldehydes',
Caramel = 'Caramel',
Sherry = 'Sherry',
Staleness = 'Staleness',
Toffee = 'Toffee'
}
export enum Umami {
FishSauce = 'Fish Sauce',
Soy = 'Soy'
}
export enum Balsamic {
Balsamic = 'Balsamic'
}
export enum Grain {
CookedRice = 'Cooked Rice',
RawRice = 'Raw Rice',
SteamedRice = 'Steamed Rice',
Cereal = 'Cereal',
Barley = 'Barley',
Oat = 'Oat',
Wheat = 'Wheat',
Grains = 'Grains',
Corn = 'Corn',
Malt = 'Malt'
}
export enum Dairy {
Butter = 'Butter',
Cream = 'Cream',
Milk = 'Milk',
Yogurt = 'Yogurt'
}
export enum Anisoles {
Mustiness = 'Mustiness',
Trichloroanisole = 'Trichloroanisole',
WetCardboard = 'Wet cardboard'
}
export enum Brettanomyces {
Animal = 'Animal',
Farmyard = 'Farmyard',
Iodine = 'Iodine',
Leather = 'Leather',
Meaty = 'Meaty',
Vinyl = 'Vinyl'
}
export enum VolatileAcidity {
Solvent = 'Solvent',
NailVarnishRemover = 'Nail varnish remover',
Vinegar = 'Vinegar'
}
export enum Reduction {
Cabbage = 'Cabbage',
Eggs = 'Eggs',
Garlic = 'Garlic',
Mercaptans = 'Mercaptans',
Onion = 'Onion',
Rubber = 'Rubber',
Sweat = 'Sweat'
}

@ -0,0 +1,90 @@
export enum TextureAndBalanceKey {
Sweetness = 'sweetness',
Acidity = 'acidity',
Tannin = 'tannin',
Alcohol = 'alcohol',
Body = 'body',
FlavourIntensity = 'flavourIntensity',
PalateLength = 'palateLength',
Reasoning = 'reasoning',
Quality = 'quality',
Age = 'age',
ReadinessToDrink = 'readinessToDrink'
}
export enum Sweetness {
Dry = 'Dry',
OffDry = 'Off-dry',
Medium = 'Medium',
Sweet = 'Sweet',
Luscious = 'Luscious'
}
export enum Concentration {
Low = 'Low',
Medium = 'Medium',
High = 'High'
}
export enum TanninType {
Ripe = 'Ripe',
Unripe = 'Unripe'
}
export enum RipeTannin {
Soft = 'Soft',
FineGrained = 'Fine-grained',
Coarse = 'Coarse'
}
export enum UnripeTannin {
Green = 'Green',
Stalky = 'Stalky'
}
export enum Body {
Light = 'Light',
Medium = 'Medium',
Full = 'Full'
}
export enum FlavourIntensity {
Light = 'Light',
Medium = 'Medium',
Pronounced = 'Pronounced'
}
export enum PalateLength {
Short = 'Short',
Medium = 'Medium',
Pronounced = 'Pronounced',
Exceptional = 'Exceptional'
}
export enum ReasoningConcentration {
Low = 'Low',
High = 'High'
}
export enum Quality {
NS = 'NS',
Poor = 'Poor',
Acceptable = 'Acceptable',
Good = 'Good',
VeryGood = 'Very good',
Excellent = 'Excellent',
Outstanding = 'Outstanding'
}
export enum ReadinessToDrink {
TooYoung = 'Too young',
DrinkWithPotentialForAgeing = 'Drink with potential for ageing',
DrinkNow = 'Drink Now',
TooOldPassed = 'Too old/Passed'
}
export enum ReasoningKey {
Balance = 'balance',
Concentration = 'concentration',
Complex = 'complex'
}

@ -0,0 +1,17 @@
export enum VisualAssessmentKey {
Clarity = 'clarity',
Nature = 'nature',
Colour = 'colour'
}
export enum ClarityVisualAssessment {
Clear = 'Clear',
Cloudy = 'Cloudy',
Opaque = 'Opaque'
}
export enum NatureVisualAssessment {
Still = 'Still',
Frizzante = 'Frizzante',
Sparkling = 'Sparkling'
}

@ -1,4 +1,4 @@
export enum Routes {
export enum Route {
Users = '/users',
NostrEvents = '/nostr',
Reviews = '/reviews',

@ -80,7 +80,7 @@ export interface SakePolishMin {
min: number
}
export enum SakeCharacteristics {
export enum SakeCharacteristic {
LightAndRefreshing = 'Light and Refreshing',
CleanAndCrisp = 'Clean and Crisp',
FruityAndAromatic = 'Fruity and Aromatic',

@ -42,7 +42,7 @@ export enum SpiritVolume {
'1L' = '1L'
}
export enum SpiritCharacteristics {
export enum SpiritCharacteristic {
LightAndNeutral = 'Light and Neutral',
FruityAndAromatic = 'Fruity and Aromatic',
HerbalAndBotanical = 'Herbal and Botanical',
@ -51,7 +51,7 @@ export enum SpiritCharacteristics {
RichAndFullBodied = 'Rich and Full-Bodied'
}
export enum WhiteSpiritVariants {
export enum WhiteSpiritVariant {
Absinthe = 'Absinthe',
Pastis = 'Pastis',
Vodka = 'Vodka',
@ -67,7 +67,7 @@ export enum WhiteSpiritVariants {
Arrack = 'Arrack'
}
export enum DarkSpiritVariants {
export enum DarkSpiritVariant {
Absinthe = 'Absinthe',
Brandy = 'Brandy',
Calvados = 'Calvados',
@ -80,7 +80,7 @@ export enum DarkSpiritVariants {
Whiskey = 'Whiskey'
}
export enum LiqueursSpiritVariants {
export enum LiqueursSpiritVariant {
Amaro = 'Amaro',
Coffee = 'Coffee',
Cream = 'Cream',

@ -1,10 +1,10 @@
import { SakeVolume, SpiritVolume, StandardDrinks, WineVolume } from '../types'
import { SakeVolume, SpiritVolume, StandardDrink, WineVolume } from '../types'
import { roundToOneDecimal } from './'
export const alcoholToStandardDrinks = (
alcohol: number,
bottle: number
): StandardDrinks => {
): StandardDrink => {
const UK100ml = roundToOneDecimal(10 * alcohol)
const AU100ml = roundToOneDecimal(7.91 * alcohol)
const US100ml = roundToOneDecimal(5.64 * alcohol)

@ -1,30 +1,27 @@
import {
SpiritCharacteristics,
SpiritCharacteristic,
SpiritType,
WhiteSpiritVariants,
DarkSpiritVariants,
LiqueursSpiritVariants
WhiteSpiritVariant,
DarkSpiritVariant,
LiqueursSpiritVariant
} from '../types'
// TODO: improve types
export const spiritVariantMap: {
[key in SpiritType]:
| {
[key in WhiteSpiritVariants]: (string | { [key: string]: string[] })[]
[key in WhiteSpiritVariant]: (string | { [key: string]: string[] })[]
}
| {
[key in DarkSpiritVariants]: (string | { [key: string]: string[] })[]
[key in DarkSpiritVariant]: (string | { [key: string]: string[] })[]
}
| {
[key in LiqueursSpiritVariants]: (
| string
| { [key: string]: string[] }
)[]
[key in LiqueursSpiritVariant]: (string | { [key: string]: string[] })[]
}
} = {
[SpiritType.White]: {
[WhiteSpiritVariants.Absinthe]: ['Blanche'],
[WhiteSpiritVariants.Pastis]: [
[WhiteSpiritVariant.Absinthe]: ['Blanche'],
[WhiteSpiritVariant.Pastis]: [
'Anise',
'Fennel',
'Licorice Root',
@ -36,7 +33,7 @@ export const spiritVariantMap: {
'Cinnamon',
'Clove'
],
[WhiteSpiritVariants.Vodka]: [
[WhiteSpiritVariant.Vodka]: [
'Wheat',
'Rye',
'Corn',
@ -46,12 +43,12 @@ export const spiritVariantMap: {
'Fruits',
'Grains'
],
[WhiteSpiritVariants.Genever]: [
[WhiteSpiritVariant.Genever]: [
{
Young: ['Juniper']
}
],
[WhiteSpiritVariants.Gin]: [
[WhiteSpiritVariant.Gin]: [
{
'London Dry': [
'Juniper',
@ -68,11 +65,11 @@ export const spiritVariantMap: {
},
'Plymouth'
],
[WhiteSpiritVariants.Mezcal]: [
[WhiteSpiritVariant.Mezcal]: [
{ Joven: ['Espadín', 'Tepeztate', 'Tequilana (blue)', 'Tobalá'] }
],
[WhiteSpiritVariants.Rum]: ['Blanco', 'Cachaça', 'Platino', 'Agricole'],
[WhiteSpiritVariants.EauDeVie]: [
[WhiteSpiritVariant.Rum]: ['Blanco', 'Cachaça', 'Platino', 'Agricole'],
[WhiteSpiritVariant.EauDeVie]: [
'Apple',
'Blackcurrant',
'Butterscotch',
@ -81,27 +78,27 @@ export const spiritVariantMap: {
'Plum',
'Raspberries'
],
[WhiteSpiritVariants.Grappa]: ['Marc', 'Pisco'],
[WhiteSpiritVariants.Baijiu]: [
[WhiteSpiritVariant.Grappa]: ['Marc', 'Pisco'],
[WhiteSpiritVariant.Baijiu]: [
'Sorghum',
'Wheat',
'Barley',
'Rice',
'Millet'
],
[WhiteSpiritVariants.Soju]: [
[WhiteSpiritVariant.Soju]: [
'Barley',
'Brown sugar',
'Buckwheat',
'Rice',
'Sweet Potato'
],
[WhiteSpiritVariants.Aquavit]: [],
[WhiteSpiritVariants.Arrack]: []
[WhiteSpiritVariant.Aquavit]: [],
[WhiteSpiritVariant.Arrack]: []
},
[SpiritType.Dark]: {
[DarkSpiritVariants.Absinthe]: ['Jaune', 'Verte'],
[DarkSpiritVariants.Brandy]: [
[DarkSpiritVariant.Absinthe]: ['Jaune', 'Verte'],
[DarkSpiritVariant.Brandy]: [
{
Grape: [
'VS',
@ -114,14 +111,11 @@ export const spiritVariantMap: {
]
}
],
[DarkSpiritVariants.Calvados]: ['Apple', 'Pear'],
[DarkSpiritVariants.Chartreuse]: ['Green', 'Yellow'],
[DarkSpiritVariants.Genever]: [
{ Old: ['Juniper'] },
{ Coren: ['Juniper'] }
],
[DarkSpiritVariants.Mezcal]: ['Reposado', 'Abuelo', 'Añejo', 'Extra Añejo'],
[DarkSpiritVariants.Rum]: [
[DarkSpiritVariant.Calvados]: ['Apple', 'Pear'],
[DarkSpiritVariant.Chartreuse]: ['Green', 'Yellow'],
[DarkSpiritVariant.Genever]: [{ Old: ['Juniper'] }, { Coren: ['Juniper'] }],
[DarkSpiritVariant.Mezcal]: ['Reposado', 'Abuelo', 'Añejo', 'Extra Añejo'],
[DarkSpiritVariant.Rum]: [
{
Sugar: [
'Cachaca (amarela/ouro)',
@ -133,8 +127,8 @@ export const spiritVariantMap: {
]
}
],
[DarkSpiritVariants.Slivovitz]: [],
[DarkSpiritVariants.Whiskey]: [
[DarkSpiritVariant.Slivovitz]: [],
[DarkSpiritVariant.Whiskey]: [
'Barley',
'Rye',
'Wheat',
@ -142,18 +136,18 @@ export const spiritVariantMap: {
'Oat',
'Rice'
],
[DarkSpiritVariants.Arrack]: []
[DarkSpiritVariant.Arrack]: []
},
[SpiritType.Liqueurs]: {
[LiqueursSpiritVariants.Amaro]: [],
[LiqueursSpiritVariants.Coffee]: [],
[LiqueursSpiritVariants.Cream]: [
[LiqueursSpiritVariant.Amaro]: [],
[LiqueursSpiritVariant.Coffee]: [],
[LiqueursSpiritVariant.Cream]: [
'Egg (Advocaat)',
'Rum',
'Strawberry',
'Whiskey (Baileys etc)'
],
[LiqueursSpiritVariants.Creme]: [
[LiqueursSpiritVariant.Creme]: [
'Almond',
'Banana',
'Blackcurrant',
@ -162,8 +156,8 @@ export const spiritVariantMap: {
'Sour Cherry',
'Violet'
],
[LiqueursSpiritVariants.Flowers]: ['Rose', 'Violet', 'Elderflower'],
[LiqueursSpiritVariants.Fruit]: [
[LiqueursSpiritVariant.Flowers]: ['Rose', 'Violet', 'Elderflower'],
[LiqueursSpiritVariant.Fruit]: [
'Blackcurrant',
'Lemon',
'Melon',
@ -173,7 +167,7 @@ export const spiritVariantMap: {
'Raspberry',
'Yuzu'
],
[LiqueursSpiritVariants.Herb]: [
[LiqueursSpiritVariant.Herb]: [
'Anise',
'Dom Benedictine',
'Bitters',
@ -182,8 +176,8 @@ export const spiritVariantMap: {
'Metaxa',
'Mint'
],
[LiqueursSpiritVariants.Honey]: ['Licor 43', 'Rum', 'Vodka', 'Whiskey'],
[LiqueursSpiritVariants.Nut]: [
[LiqueursSpiritVariant.Honey]: ['Licor 43', 'Rum', 'Vodka', 'Whiskey'],
[LiqueursSpiritVariant.Nut]: [
'Almond',
'Apricot Kernel',
'Hazelnut',
@ -196,62 +190,62 @@ export const spiritVariantMap: {
}
export const spiritCharacteristicsMap: {
[key in SpiritCharacteristics]: string[]
[key in SpiritCharacteristic]: string[]
} = {
[SpiritCharacteristics.LightAndNeutral]: [
WhiteSpiritVariants.Mezcal,
WhiteSpiritVariants.Soju,
WhiteSpiritVariants.Vodka,
WhiteSpiritVariants.Rum,
WhiteSpiritVariants.Aquavit
[SpiritCharacteristic.LightAndNeutral]: [
WhiteSpiritVariant.Mezcal,
WhiteSpiritVariant.Soju,
WhiteSpiritVariant.Vodka,
WhiteSpiritVariant.Rum,
WhiteSpiritVariant.Aquavit
],
[SpiritCharacteristics.FruityAndAromatic]: [
WhiteSpiritVariants.Rum,
DarkSpiritVariants.Calvados,
WhiteSpiritVariants.EauDeVie,
LiqueursSpiritVariants.Fruit,
WhiteSpiritVariants.Gin,
WhiteSpiritVariants.Grappa
[SpiritCharacteristic.FruityAndAromatic]: [
WhiteSpiritVariant.Rum,
DarkSpiritVariant.Calvados,
WhiteSpiritVariant.EauDeVie,
LiqueursSpiritVariant.Fruit,
WhiteSpiritVariant.Gin,
WhiteSpiritVariant.Grappa
],
[SpiritCharacteristics.HerbalAndBotanical]: [
WhiteSpiritVariants.Absinthe,
DarkSpiritVariants.Absinthe,
LiqueursSpiritVariants.Amaro,
WhiteSpiritVariants.Genever,
DarkSpiritVariants.Genever,
WhiteSpiritVariants.Gin,
WhiteSpiritVariants.Pastis,
DarkSpiritVariants.Chartreuse,
WhiteSpiritVariants.Aquavit
[SpiritCharacteristic.HerbalAndBotanical]: [
WhiteSpiritVariant.Absinthe,
DarkSpiritVariant.Absinthe,
LiqueursSpiritVariant.Amaro,
WhiteSpiritVariant.Genever,
DarkSpiritVariant.Genever,
WhiteSpiritVariant.Gin,
WhiteSpiritVariant.Pastis,
DarkSpiritVariant.Chartreuse,
WhiteSpiritVariant.Aquavit
],
[SpiritCharacteristics.SweetAndSyrupy]: [
DarkSpiritVariants.Brandy,
LiqueursSpiritVariants.Cream,
LiqueursSpiritVariants.Creme,
DarkSpiritVariants.Rum,
LiqueursSpiritVariants.Nut
[SpiritCharacteristic.SweetAndSyrupy]: [
DarkSpiritVariant.Brandy,
LiqueursSpiritVariant.Cream,
LiqueursSpiritVariant.Creme,
DarkSpiritVariant.Rum,
LiqueursSpiritVariant.Nut
],
[SpiritCharacteristics.SmokyAndSpicy]: [
WhiteSpiritVariants.Baijiu,
DarkSpiritVariants.Rum,
WhiteSpiritVariants.Gin,
WhiteSpiritVariants.EauDeVie,
DarkSpiritVariants.Mezcal,
DarkSpiritVariants.Whiskey,
LiqueursSpiritVariants.Cream,
LiqueursSpiritVariants.Honey,
WhiteSpiritVariants.Arrack,
DarkSpiritVariants.Arrack
[SpiritCharacteristic.SmokyAndSpicy]: [
WhiteSpiritVariant.Baijiu,
DarkSpiritVariant.Rum,
WhiteSpiritVariant.Gin,
WhiteSpiritVariant.EauDeVie,
DarkSpiritVariant.Mezcal,
DarkSpiritVariant.Whiskey,
LiqueursSpiritVariant.Cream,
LiqueursSpiritVariant.Honey,
WhiteSpiritVariant.Arrack,
DarkSpiritVariant.Arrack
],
[SpiritCharacteristics.RichAndFullBodied]: [
WhiteSpiritVariants.Baijiu,
DarkSpiritVariants.Brandy,
DarkSpiritVariants.Rum,
WhiteSpiritVariants.Grappa,
WhiteSpiritVariants.EauDeVie,
LiqueursSpiritVariants.Cream,
LiqueursSpiritVariants.Honey,
DarkSpiritVariants.Whiskey
[SpiritCharacteristic.RichAndFullBodied]: [
WhiteSpiritVariant.Baijiu,
DarkSpiritVariant.Brandy,
DarkSpiritVariant.Rum,
WhiteSpiritVariant.Grappa,
WhiteSpiritVariant.EauDeVie,
LiqueursSpiritVariant.Cream,
LiqueursSpiritVariant.Honey,
DarkSpiritVariant.Whiskey
]
}

@ -3,3 +3,6 @@ export const roundToOneDecimal = (number: number) =>
export const isObject = (item: unknown) =>
typeof item === 'object' && !Array.isArray(item) && item !== null
export const compareArrays = (a: unknown[], b: unknown[]) =>
JSON.stringify(a.sort()) === JSON.stringify(b.sort())

@ -1,6 +1,7 @@
export * from './user'
export * from './nostr'
export * from './review'
export * from './review'
export * from './wine'
export * from './spirit'
export * from './product'

@ -1,11 +1,11 @@
import { collections } from '../../services/database.service'
import { DBcollections, ProductType } from '../../types'
import { DBcollection, ProductType } from '../../types'
export const productCodeValidation = async (
ean: string,
upc: string,
sku: string,
collection: DBcollections,
collection: DBcollection,
productType: ProductType
) => {
if (!ean && !upc && !sku) {

@ -1,16 +1,797 @@
import Joi from 'joi'
import { RatingOptions } from '../../types'
import {
RatingOption,
TastingNote,
TastingNoteKey,
VisualAssessmentKey,
ClarityVisualAssessment,
NatureVisualAssessment,
WhiteColour,
AmberColour,
RoseColour,
RedColour,
BlueColour,
GreenColour,
ProductType,
WineColour,
SakeColour,
SpiritColour,
PrimaryFlavoursAndAromasKey,
Condition,
Intensity,
Age,
CitrusFruit,
AppleFruit,
StoneFruit,
RedFruit,
BlackFruit,
ChocolateFruit,
TropicalFruit,
MelonFruit,
Floral,
Vegetal,
Earth,
Microbial,
Oak,
Chocolate,
Oxidation,
Umami,
Balsamic,
Grain,
Dairy,
Anisoles,
Brettanomyces,
VolatileAcidity,
Reduction,
TextureAndBalanceKey,
Sweetness,
Concentration,
TanninType,
RipeTannin,
UnripeTannin,
Body,
FlavourIntensity,
PalateLength,
ReasoningKey,
ReasoningConcentration,
Quality,
ReadinessToDrink
} from '../../types'
import { compareArrays, isObject } from '../utils'
import { producerIdValidation } from './'
export const reviewValidation = (data: unknown): Joi.ValidationResult =>
Joi.object({
eventId: Joi.string().required(),
productId: Joi.string().required(),
productId: producerIdValidation,
productType: Joi.string()
.valid(...Object.values(ProductType))
.required(),
rating: Joi.alternatives()
.try(
Joi.string().valid(...Object.values(RatingOptions)),
Joi.string().valid(...Object.values(RatingOption)),
Joi.number().min(84).max(100)
)
.required(),
reviewText: Joi.string().required(),
tastingNotes: Joi.array().items(Joi.string())
tastingNote: Joi.object()
.custom((tastingNote: TastingNote, helper) => {
const message = (str: string) =>
helper.message({
custom: Joi.expression(str)
})
/**
* Root keys validation
*/
const tastingNoteKeys = Object.keys(tastingNote)
const validTastingNoteKeys = Object.values(TastingNoteKey)
if (!compareArrays(tastingNoteKeys, validTastingNoteKeys)) {
return message(
`provided "tastingNote" is not valid. "tastingNote" object has to include the following keys: [${validTastingNoteKeys.join(', ')}]`
)
}
/**
* visualAssessment validation
*/
const visualAssessmentKeys = Object.keys(
tastingNote[TastingNoteKey.VisualAssessment]
)
const validVisualAssessmentKeys = Object.values(VisualAssessmentKey)
if (!compareArrays(visualAssessmentKeys, validVisualAssessmentKeys)) {
return message(
`provided "tastingNote" is not valid. "visualAssessment-visualAssessment" object has to include the following keys: [${validVisualAssessmentKeys.join(', ')}]`
)
}
/**
* visualAssessment-clarity validation
*/
const clarity =
tastingNote[TastingNoteKey.VisualAssessment][
VisualAssessmentKey.Clarity
]
if (
typeof clarity !== 'string' ||
!(clarity in ClarityVisualAssessment)
) {
return message(
`provided "tastingNote" is not valid. Valid options for "visualAssessment-clarity" are: [${Object.values(ClarityVisualAssessment).join(', ')}]`
)
}
/**
* visualAssessment-nature validation
*/
const nature =
tastingNote[TastingNoteKey.VisualAssessment][
VisualAssessmentKey.Nature
]
if (typeof nature !== 'string' || !(nature in NatureVisualAssessment)) {
return message(
`provided "tastingNote" is not valid. Valid options for "visualAssessment-nature" are: [${Object.values(NatureVisualAssessment).join(', ')}]`
)
}
/**
* visualAssessment-colour validation
*/
const colour =
tastingNote[TastingNoteKey.VisualAssessment][
VisualAssessmentKey.Colour
]
const validWhiteColourOptions: WhiteColour[] =
Object.values(WhiteColour)
const validAmberColourOptions: AmberColour[] =
Object.values(AmberColour)
const validRoseColourOptions = Object.values(RoseColour)
const validRedColourOptions = Object.values(RedColour)
const validBlueColourOptions = Object.values(BlueColour)
const validGreenColourOptions = Object.values(GreenColour)
if (
typeof colour !== 'string' ||
(!validWhiteColourOptions.includes(colour as WhiteColour) &&
!validAmberColourOptions.includes(colour as AmberColour) &&
!validRoseColourOptions.includes(colour as RoseColour) &&
!validRedColourOptions.includes(colour as RedColour) &&
!validBlueColourOptions.includes(colour as BlueColour) &&
!validGreenColourOptions.includes(colour as GreenColour))
) {
return message(
`provided "tastingNote" is not valid. Valid options for "visualAssessment-colour" are: [${[
...validWhiteColourOptions,
...validAmberColourOptions,
...validRoseColourOptions,
...validRedColourOptions,
...validBlueColourOptions,
...validGreenColourOptions
].join(', ')}]`
)
}
// check if colour is applicable to the product
const productType: ProductType = helper.state.ancestors[0].productType
switch (productType) {
case ProductType.Wine:
{
const validWineColours = Object.values(WineColour)
if (!(validWineColours as string[]).includes(colour)) {
return message(
`provided "tastingNote" is not valid. Valid options for "visualAssessment-colour" for ${productType} are: [${validWineColours.join(', ')}]`
)
}
}
break
case ProductType.Sake:
{
const validSakeColours = Object.values(SakeColour)
if (!(validSakeColours as string[]).includes(colour)) {
return message(
`provided "tastingNote" is not valid. Valid options for "visualAssessment-colour" for ${productType} are: [${validSakeColours.join(', ')}]`
)
}
}
break
case ProductType.Spirit:
{
const validSpiritColours = Object.values(SpiritColour)
if (!(validSpiritColours as string[]).includes(colour)) {
return message(
`provided "tastingNote" is not valid. Valid options for "visualAssessment-colour" for ${productType} are: [${validSpiritColours.join(', ')}]`
)
}
}
break
default:
break
}
/**
* primaryFlavoursAndAromas validation
*/
const primaryFlavoursAndAromasKeys = Object.keys(
tastingNote[TastingNoteKey.PrimaryFlavoursAndAromas]
)
const validPrimaryFlavoursAndAromasKeys = Object.values(
PrimaryFlavoursAndAromasKey
)
if (
!compareArrays(
primaryFlavoursAndAromasKeys,
validPrimaryFlavoursAndAromasKeys
)
) {
return message(
`provided "tastingNote" is not valid. "visualAssessment-primaryFlavoursAndAromas" object has to include the following keys: [${validPrimaryFlavoursAndAromasKeys.join(', ')}]`
)
}
// primaryFlavoursAndAromas-condition validation
const condition =
tastingNote[TastingNoteKey.PrimaryFlavoursAndAromas][
PrimaryFlavoursAndAromasKey.Condition
]
const validConditionOptions: Condition[] = Object.values(Condition)
if (
typeof condition !== 'string' ||
!validConditionOptions.includes(condition)
) {
return message(
`provided "tastingNote" is not valid. Valid options for "visualAssessment-condition" are: [${validConditionOptions.join(', ')}]`
)
}
// primaryFlavoursAndAromas-intensity validation
const intensity =
tastingNote[TastingNoteKey.PrimaryFlavoursAndAromas][
PrimaryFlavoursAndAromasKey.Intensity
]
const validIntensityOptions: Intensity[] = Object.values(Intensity)
if (
typeof intensity !== 'string' ||
!validIntensityOptions.includes(intensity)
) {
return message(
`provided "tastingNote" is not valid. Valid options for "visualAssessment-intensity" are: [${validIntensityOptions.join(', ')}]`
)
}
// primaryFlavoursAndAromas-age validation
const age =
tastingNote[TastingNoteKey.PrimaryFlavoursAndAromas][
PrimaryFlavoursAndAromasKey.Age
]
const validAgeOptions: Age[] = Object.values(Age)
if (typeof age !== 'string' || !validAgeOptions.includes(age)) {
return message(
`provided "tastingNote" is not valid. Valid options for "visualAssessment-age" are: [${validAgeOptions.join(', ')}]`
)
}
// primaryFlavoursAndAromas-fruit validation
const fruit =
tastingNote[TastingNoteKey.PrimaryFlavoursAndAromas][
PrimaryFlavoursAndAromasKey.Fruit
]
const validFruitOptions: (
| CitrusFruit
| AppleFruit
| StoneFruit
| RedFruit
| BlackFruit
| ChocolateFruit
| TropicalFruit
| MelonFruit
)[] = [
...Object.values(CitrusFruit),
...Object.values(AppleFruit),
...Object.values(StoneFruit),
...Object.values(RedFruit),
...Object.values(BlackFruit),
...Object.values(ChocolateFruit),
...Object.values(TropicalFruit),
...Object.values(MelonFruit)
]
if (typeof fruit !== 'string' || !validFruitOptions.includes(fruit)) {
return message(
`provided "tastingNote" is not valid. Valid options for "visualAssessment-fruit" are: [${validFruitOptions.join(', ')}]`
)
}
// primaryFlavoursAndAromas-floral validation
const floral =
tastingNote[TastingNoteKey.PrimaryFlavoursAndAromas][
PrimaryFlavoursAndAromasKey.Floral
]
const validFloralOptions: Floral[] = Object.values(Floral)
if (
typeof floral !== 'string' ||
!validFloralOptions.includes(floral)
) {
return message(
`provided "tastingNote" is not valid. Valid options for "visualAssessment-floral" are: [${validFloralOptions.join(', ')}]`
)
}
// primaryFlavoursAndAromas-vegetal validation
const vegetal =
tastingNote[TastingNoteKey.PrimaryFlavoursAndAromas][
PrimaryFlavoursAndAromasKey.Vegetal
]
const validVegetalOptions: Vegetal[] = Object.values(Vegetal)
if (
typeof vegetal !== 'string' ||
!validVegetalOptions.includes(vegetal)
) {
return message(
`provided "tastingNote" is not valid. Valid options for "visualAssessment-vegetal" are: [${validVegetalOptions.join(', ')}]`
)
}
// primaryFlavoursAndAromas-earth validation
const earth =
tastingNote[TastingNoteKey.PrimaryFlavoursAndAromas][
PrimaryFlavoursAndAromasKey.Earth
]
const validEarthOptions: Earth[] = Object.values(Earth)
if (typeof earth !== 'string' || !validEarthOptions.includes(earth)) {
return message(
`provided "tastingNote" is not valid. Valid options for "visualAssessment-earth" are: [${validEarthOptions.join(', ')}]`
)
}
// primaryFlavoursAndAromas-earth validation
const microbial =
tastingNote[TastingNoteKey.PrimaryFlavoursAndAromas][
PrimaryFlavoursAndAromasKey.Microbial
]
const validMicrobialOptions: Microbial[] = Object.values(Microbial)
if (
typeof microbial !== 'string' ||
!validMicrobialOptions.includes(microbial)
) {
return message(
`provided "tastingNote" is not valid. Valid options for "visualAssessment-microbial" are: [${validMicrobialOptions.join(', ')}]`
)
}
// primaryFlavoursAndAromas-oak validation
const oak =
tastingNote[TastingNoteKey.PrimaryFlavoursAndAromas][
PrimaryFlavoursAndAromasKey.Oak
]
const validOakOptions: Oak[] = Object.values(Oak)
if (typeof oak !== 'string' || !validOakOptions.includes(oak)) {
return message(
`provided "tastingNote" is not valid. Valid options for "visualAssessment-oak" are: [${validOakOptions.join(', ')}]`
)
}
// primaryFlavoursAndAromas-chocolate validation
const chocolate =
tastingNote[TastingNoteKey.PrimaryFlavoursAndAromas][
PrimaryFlavoursAndAromasKey.Chocolate
]
const validChocolateOptions: Chocolate[] = Object.values(Chocolate)
if (
typeof chocolate !== 'string' ||
!validChocolateOptions.includes(chocolate)
) {
return message(
`provided "tastingNote" is not valid. Valid options for "visualAssessment-chocolate" are: [${validChocolateOptions.join(', ')}]`
)
}
// primaryFlavoursAndAromas-oxidation validation
const oxidation =
tastingNote[TastingNoteKey.PrimaryFlavoursAndAromas][
PrimaryFlavoursAndAromasKey.Oxidation
]
const validOxidationOptions: Oxidation[] = Object.values(Oxidation)
if (
typeof oxidation !== 'string' ||
!validOxidationOptions.includes(oxidation)
) {
return message(
`provided "tastingNote" is not valid. Valid options for "visualAssessment-oxidation" are: [${validOxidationOptions.join(', ')}]`
)
}
// primaryFlavoursAndAromas-umami validation
const umami =
tastingNote[TastingNoteKey.PrimaryFlavoursAndAromas][
PrimaryFlavoursAndAromasKey.Umami
]
const validUmamiOptions: Umami[] = Object.values(Umami)
if (typeof umami !== 'string' || !validUmamiOptions.includes(umami)) {
return message(
`provided "tastingNote" is not valid. Valid options for "visualAssessment-umami" are: [${validUmamiOptions.join(', ')}]`
)
}
// primaryFlavoursAndAromas-balsamic validation
const balsamic =
tastingNote[TastingNoteKey.PrimaryFlavoursAndAromas][
PrimaryFlavoursAndAromasKey.Balsamic
]
const validBalsamicOptions: Balsamic[] = Object.values(Balsamic)
if (
typeof balsamic !== 'string' ||
!validBalsamicOptions.includes(balsamic)
) {
return message(
`provided "tastingNote" is not valid. Valid options for "visualAssessment-balsamic" are: [${validBalsamicOptions.join(', ')}]`
)
}
// primaryFlavoursAndAromas-grain validation
const grain =
tastingNote[TastingNoteKey.PrimaryFlavoursAndAromas][
PrimaryFlavoursAndAromasKey.Grain
]
const validGrainOptions: Grain[] = Object.values(Grain)
if (typeof grain !== 'string' || !validGrainOptions.includes(grain)) {
return message(
`provided "tastingNote" is not valid. Valid options for "visualAssessment-grain" are: [${validGrainOptions.join(', ')}]`
)
}
// primaryFlavoursAndAromas-dairy validation
const dairy =
tastingNote[TastingNoteKey.PrimaryFlavoursAndAromas][
PrimaryFlavoursAndAromasKey.Dairy
]
const validDairyOptions: Dairy[] = Object.values(Dairy)
if (typeof dairy !== 'string' || !validDairyOptions.includes(dairy)) {
return message(
`provided "tastingNote" is not valid. Valid options for "visualAssessment-dairy" are: [${validDairyOptions.join(', ')}]`
)
}
// primaryFlavoursAndAromas-faults validation
const faults =
tastingNote[TastingNoteKey.PrimaryFlavoursAndAromas][
PrimaryFlavoursAndAromasKey.Faults
]
const validFaultsOptions: (
| Anisoles
| Brettanomyces
| VolatileAcidity
| Reduction
)[] = [
...Object.values(Anisoles),
...Object.values(Brettanomyces),
...Object.values(VolatileAcidity),
...Object.values(Reduction)
]
if (
typeof faults !== 'string' ||
!validFaultsOptions.includes(faults)
) {
return message(
`provided "tastingNote" is not valid. Valid options for "visualAssessment-faults" are: [${validFaultsOptions.join(', ')}]`
)
}
/**
* textureAndBalance validation
*/
const textureAndBalanceKeys = Object.keys(
tastingNote[TastingNoteKey.TextureAndBalance]
)
const validTextureAndBalanceKeys = Object.values(TextureAndBalanceKey)
if (!compareArrays(textureAndBalanceKeys, validTextureAndBalanceKeys)) {
return message(
`provided "tastingNote" is not valid. "visualAssessment-textureAndBalance" object has to include the following keys: [${validTextureAndBalanceKeys.join(', ')}]`
)
}
// textureAndBalance-sweetness validation
const sweetness =
tastingNote[TastingNoteKey.TextureAndBalance][
TextureAndBalanceKey.Sweetness
]
const validSweetnessOptions: Sweetness[] = Object.values(Sweetness)
if (
typeof sweetness !== 'string' ||
!validSweetnessOptions.includes(sweetness)
) {
return message(
`provided "tastingNote" is not valid. Valid options for "textureAndBalance-sweetness" are: [${validSweetnessOptions.join(', ')}]`
)
}
// textureAndBalance-acidity validation
const acidity =
tastingNote[TastingNoteKey.TextureAndBalance][
TextureAndBalanceKey.Acidity
]
const validAcidityOptions: Concentration[] =
Object.values(Concentration)
if (
typeof acidity !== 'string' ||
!validAcidityOptions.includes(acidity)
) {
return message(
`provided "tastingNote" is not valid. Valid options for "textureAndBalance-acidity" are: [${validAcidityOptions.join(', ')}]`
)
}
// textureAndBalance-tannin validation
const tannin =
tastingNote[TastingNoteKey.TextureAndBalance][
TextureAndBalanceKey.Tannin
]
const tanninKeys = Object.keys(tannin)
const tanninKey = tanninKeys[0] as Concentration
const validTanninKeys = Object.values(Concentration)
if (!isObject(tannin)) {
return message(
`provided "tastingNote" is not valid. "textureAndBalance-tannin" should be an object with the following properties: [${validTanninKeys.join(', ')}]`
)
}
if (tanninKeys.length !== 1 || !validTanninKeys.includes(tanninKey)) {
return message(
`provided "tastingNote" is not valid. Valid options for "textureAndBalance-tannin" are: [${validTanninKeys.join(', ')}]`
)
}
// textureAndBalance-tannin-type validation
const tanninType =
tastingNote[TastingNoteKey.TextureAndBalance][
TextureAndBalanceKey.Tannin
][tanninKey]
const tanninTypeKeys = Object.keys(tanninType)
const tanninTypeKey = tanninTypeKeys[0] as TanninType
const validTanninTypeKeys = Object.values(TanninType)
if (!isObject(tanninType)) {
return message(
`provided "tastingNote" is not valid. "textureAndBalance-tannin-type" should be an object with the following properties: [${validTanninTypeKeys.join(', ')}]`
)
}
if (
tanninTypeKeys.length !== 1 ||
!validTanninTypeKeys.includes(tanninTypeKey)
) {
return message(
`provided "tastingNote" is not valid. Valid options for "textureAndBalance-tannin-type" are: [${validTanninTypeKeys.join(', ')}]`
)
}
// textureAndBalance-tannin-value validation
const tanninValue =
tastingNote[TastingNoteKey.TextureAndBalance][
TextureAndBalanceKey.Tannin
][tanninKey][tanninTypeKey]
const validTanninValueOptions = [
...Object.values(RipeTannin),
...Object.values(UnripeTannin)
]
if (
typeof tanninValue !== 'string' ||
!validTanninValueOptions.includes(tanninValue)
) {
return message(
`provided "tastingNote" is not valid. Valid options for "textureAndBalance-tannin-type-value" are: [${validTanninValueOptions.join(', ')}]`
)
}
// textureAndBalance-alcohol validation
const alcohol =
tastingNote[TastingNoteKey.TextureAndBalance][
TextureAndBalanceKey.Alcohol
]
const validAlcoholOptions: Concentration[] =
Object.values(Concentration)
if (
typeof alcohol !== 'string' ||
!validAlcoholOptions.includes(alcohol)
) {
return message(
`provided "tastingNote" is not valid. Valid options for "textureAndBalance-alcohol" are: [${validAlcoholOptions.join(', ')}]`
)
}
// textureAndBalance-body validation
const body =
tastingNote[TastingNoteKey.TextureAndBalance][
TextureAndBalanceKey.Body
]
const validBodyOptions: Body[] = Object.values(Body)
if (typeof body !== 'string' || !validBodyOptions.includes(body)) {
return message(
`provided "tastingNote" is not valid. Valid options for "textureAndBalance-body" are: [${validBodyOptions.join(', ')}]`
)
}
// textureAndBalance-flavourIntensity validation
const flavourIntensity =
tastingNote[TastingNoteKey.TextureAndBalance][
TextureAndBalanceKey.FlavourIntensity
]
const validFlavourIntensityOptions: FlavourIntensity[] =
Object.values(FlavourIntensity)
if (
typeof flavourIntensity !== 'string' ||
!validFlavourIntensityOptions.includes(flavourIntensity)
) {
return message(
`provided "tastingNote" is not valid. Valid options for "textureAndBalance-flavourIntensity" are: [${validFlavourIntensityOptions.join(', ')}]`
)
}
// textureAndBalance-palateLength validation
const palateLength =
tastingNote[TastingNoteKey.TextureAndBalance][
TextureAndBalanceKey.PalateLength
]
const validPalateLengthOptions: PalateLength[] =
Object.values(PalateLength)
if (
typeof palateLength !== 'string' ||
!validPalateLengthOptions.includes(palateLength)
) {
return message(
`provided "tastingNote" is not valid. Valid options for "textureAndBalance-palateLength" are: [${validPalateLengthOptions.join(', ')}]`
)
}
// textureAndBalance-reasoning validation
const reasoning =
tastingNote[TastingNoteKey.TextureAndBalance][
TextureAndBalanceKey.Reasoning
]
const reasoningKeys = Object.keys(reasoning)
const validReasoningKeys = Object.values(ReasoningKey)
if (!isObject(reasoning)) {
return message(
`provided "tastingNote" is not valid. "textureAndBalance-reasoning" should be an object with the following properties: [${validReasoningKeys.join(', ')}]`
)
}
if (!compareArrays(reasoningKeys, validReasoningKeys)) {
return message(
`provided "tastingNote" is not valid. Valid options for "textureAndBalance-reasoning" are: [${validReasoningKeys.join(', ')}]`
)
}
// textureAndBalance-reasoning-balance validation
const reasoningBalance =
tastingNote[TastingNoteKey.TextureAndBalance][
TextureAndBalanceKey.Reasoning
][ReasoningKey.Balance]
if (typeof reasoningBalance !== 'boolean') {
return message(
`provided "tastingNote" is not valid. "textureAndBalance-reasoning-balance" should be a boolean`
)
}
// textureAndBalance-reasoning-concentration validation
const reasoningConcentration =
tastingNote[TastingNoteKey.TextureAndBalance][
TextureAndBalanceKey.Reasoning
][ReasoningKey.Concentration]
const validReasoningConcentrationOptions = Object.values(
ReasoningConcentration
)
if (
typeof reasoningConcentration !== 'string' ||
!validReasoningConcentrationOptions.includes(reasoningConcentration)
) {
return message(
`provided "tastingNote" is not valid. Valid options for "textureAndBalance-reasoning-concentration" are: [${validReasoningConcentrationOptions.join(', ')}]`
)
}
// textureAndBalance-reasoning-complex validation
const reasoningComplex =
tastingNote[TastingNoteKey.TextureAndBalance][
TextureAndBalanceKey.Reasoning
][ReasoningKey.Complex]
if (
typeof reasoningComplex !== 'boolean' ||
!validReasoningConcentrationOptions.includes(reasoningConcentration)
) {
return message(
`provided "tastingNote" is not valid. "textureAndBalance-reasoning-complex" should be a boolean`
)
}
// textureAndBalance-quality validation
const quality =
tastingNote[TastingNoteKey.TextureAndBalance][
TextureAndBalanceKey.Quality
]
const validQualityOptions: Quality[] = Object.values(Quality)
if (
typeof quality !== 'string' ||
!validQualityOptions.includes(quality)
) {
return message(
`provided "tastingNote" is not valid. Valid options for "textureAndBalance-quality" are: [${validQualityOptions.join(', ')}]`
)
}
// textureAndBalance-age validation
const textureAndBalanceKeyAge =
tastingNote[TastingNoteKey.TextureAndBalance][
TextureAndBalanceKey.Age
]
if (typeof textureAndBalanceKeyAge !== 'number') {
return message(
`provided "tastingNote" is not valid. Valid options for "textureAndBalance-reasoning-age" should be a number`
)
}
// textureAndBalance-readinessToDrink validation
const readinessToDrink =
tastingNote[TastingNoteKey.TextureAndBalance][
TextureAndBalanceKey.ReadinessToDrink
]
const validReadinessToDrinkOptions: ReadinessToDrink[] =
Object.values(ReadinessToDrink)
if (
typeof readinessToDrink !== 'string' ||
!validReadinessToDrinkOptions.includes(readinessToDrink)
) {
return message(
`provided "tastingNote" is not valid. Valid options for "textureAndBalance-readinessToDrink" are: [${validReadinessToDrinkOptions.join(', ')}]`
)
}
return tastingNote
})
.required()
}).validate(data)

@ -1,7 +1,7 @@
import Joi from 'joi'
import {
SakeDesignation,
SakeCharacteristics,
SakeCharacteristic,
SakeVolume,
SakeStarter,
RiceVarietal,
@ -90,40 +90,36 @@ export const sakeValidation = (data: unknown): Joi.ValidationResult =>
return designation
})
.required(),
polishRate: Joi.number()
.custom((polishRate, helper) => {
// return if no state ancestors
if (!helper.state.ancestors) {
return polishRate
}
const designation = helper.state.ancestors[0].designation
const designationKey = Object.keys(designation)[0]
const designationValue: string = Object.values(
designation as { [key: string]: string }
)[0]
const minPolishRate: number = (
sakePolishMap as { [key: string]: { [key: string]: SakePolishMin } }
)[designationKey][designationValue].min
if (polishRate < minPolishRate || polishRate > 0.99) {
return helper.message({
custom: Joi.expression(
`provide polishRate "${polishRate}" is not valid for "${designationKey} -> ${designationValue}". Valid range is "${minPolishRate} - 0.99"`
)
})
}
polishRate: Joi.number().custom((polishRate, helper) => {
// return if no state ancestors
if (!helper.state.ancestors) {
return polishRate
})
.required(),
characteristics: Joi.array()
.items(Joi.string().valid(...Object.values(SakeCharacteristics)))
}
const designation = helper.state.ancestors[0].designation
const designationKey = Object.keys(designation)[0]
const designationValue: string = Object.values(
designation as { [key: string]: string }
)[0]
const minPolishRate: number = (
sakePolishMap as { [key: string]: { [key: string]: SakePolishMin } }
)[designationKey][designationValue].min
if (polishRate < minPolishRate || polishRate > 0.99) {
return helper.message({
custom: Joi.expression(
`provide polishRate "${polishRate}" is not valid for "${designationKey} -> ${designationValue}". Valid range is "${minPolishRate} - 0.99"`
)
})
}
return polishRate
}),
characteristic: Joi.string()
.valid(...Object.values(SakeCharacteristic))
.required(),
starter: Joi.string().valid(...Object.values(SakeStarter)),
yeastStrain: Joi.string()
.valid(...Object.values(SakeYeastStrain))
.required(),
yeastStrain: Joi.string().valid(...Object.values(SakeYeastStrain)),
volume: volumeValidation(SakeVolume),
alcohol: alcoholValidation,
riceVarietal: Joi.array()

@ -3,7 +3,7 @@ import {
Ingredient,
SpiritType,
SpiritVolume,
SpiritCharacteristics
SpiritCharacteristic
} from '../../types'
import { isObject, spiritVariantMap, spiritCharacteristicsMap } from '../'
import {
@ -231,55 +231,53 @@ export const spiritValidation = (data: unknown): Joi.ValidationResult =>
}
)
),
characteristics: Joi.array()
.items(
Joi.string().custom((characteristic, helper) => {
if (!Object.values(SpiritCharacteristics).includes(characteristic)) {
return helper.message({
custom: Joi.expression(
`"${characteristic}" is not a valid characteristic. Valid options are [${Object.values(
SpiritCharacteristics
)
.map((option) => `"${option}"`)
.join(', ')}]`
characteristic: Joi.string()
.custom((characteristic, helper) => {
if (!Object.values(SpiritCharacteristic).includes(characteristic)) {
return helper.message({
custom: Joi.expression(
`"${characteristic}" is not a valid characteristic. Valid options are [${Object.values(
SpiritCharacteristic
)
})
}
const spiritType: SpiritType = helper.state.ancestors[1].type
const spiritVariant: string | { [key: string]: unknown } =
helper.state.ancestors[1].variant
const spiritVariantName =
typeof spiritVariant === 'string'
? spiritVariant
: Object.keys(spiritVariant)[0]
const variantsInCharacteristic =
spiritCharacteristicsMap[characteristic as SpiritCharacteristics]
const characteristicsInVariant = Object.keys(
spiritCharacteristicsMap
).filter((char) =>
spiritCharacteristicsMap[char as SpiritCharacteristics].includes(
spiritVariantName
.map((option) => `"${option}"`)
.join(', ')}]`
)
})
}
const spiritType: SpiritType = helper.state.ancestors[1].type
const spiritVariant: string | { [key: string]: unknown } =
helper.state.ancestors[1].variant
const spiritVariantName =
typeof spiritVariant === 'string'
? spiritVariant
: Object.keys(spiritVariant)[0]
const variantsInCharacteristic =
spiritCharacteristicsMap[characteristic as SpiritCharacteristic]
const characteristicsInVariant = Object.keys(
spiritCharacteristicsMap
).filter((char) =>
spiritCharacteristicsMap[char as SpiritCharacteristic].includes(
spiritVariantName
)
)
if (!variantsInCharacteristic.includes(spiritVariantName)) {
return helper.message({
custom: Joi.expression(
`"${characteristic}" is not a valid characteristic for "${spiritType} -> ${spiritVariantName}". Valid options are [${characteristicsInVariant
.map((option) => `"${option}"`)
.join(', ')}]`
)
})
}
if (!variantsInCharacteristic.includes(spiritVariantName)) {
return helper.message({
custom: Joi.expression(
`"${characteristic}" is not a valid characteristic for "${spiritType} -> ${spiritVariantName}". Valid options are [${characteristicsInVariant
.map((option) => `"${option}"`)
.join(', ')}]`
)
})
}
return characteristic
})
)
return characteristic
})
.required(),
ingredients: Joi.array()
.items(Joi.string().valid(...Object.values(Ingredient)))

@ -1,9 +1,9 @@
import Joi from 'joi'
import { VintageOptions } from '../../types'
import { VintageOption } from '../../types'
export const vintageValidation = Joi.alternatives()
.try(
Joi.string().valid(...Object.values(VintageOptions)),
Joi.string().valid(...Object.values(VintageOption)),
Joi.number().min(1700).max(new Date().getFullYear())
)
.required()

@ -302,77 +302,75 @@ export const wineValidation = (data: unknown): Joi.ValidationResult =>
style: Joi.string()
.valid(...Object.values(WineStyle))
.required(),
characteristics: Joi.array()
.items(Joi.string())
.custom((value: string[], helper) => {
// return if no value
if (!value) {
return value
}
// return if no state ancestors
if (!helper.state.ancestors) {
return value
}
characteristic: Joi.string().custom((value: string[], helper) => {
// return if no value
if (!value) {
return value
}
// return if no state ancestors
if (!helper.state.ancestors) {
return value
}
const wineType: WineType = helper.state.ancestors[0].type
const wineType: WineType = helper.state.ancestors[0].type
// return if no wineType
if (!wineType) {
return value
}
// return if no wineType
if (!wineType) {
return value
}
let options: string[] = []
let options: string[] = []
switch (wineType) {
case WineType.White:
{
options = Object.values(WhiteWineCharacteristic)
}
switch (wineType) {
case WineType.White:
{
options = Object.values(WhiteWineCharacteristic)
}
break
case WineType.Amber:
{
options = Object.values(AmberWineCharacteristic)
}
break
case WineType.Amber:
{
options = Object.values(AmberWineCharacteristic)
}
break
case WineType.Rose:
{
options = Object.values(RoseWineCharacteristic)
}
break
case WineType.Rose:
{
options = Object.values(RoseWineCharacteristic)
}
break
case WineType.Red:
{
options = Object.values(RedWineCharacteristic)
}
break
case WineType.Red:
{
options = Object.values(RedWineCharacteristic)
}
break
break
default:
break
}
default:
break
}
if (!options.length) {
if (!options.length) {
return helper.message({
custom: Joi.expression(
`no characteristics found for provided type of wine`
)
})
}
for (const characteristic of value) {
if (!options.includes(characteristic)) {
return helper.message({
custom: Joi.expression(
`no characteristics found for provided type of wine`
`"${characteristic}" is not a valid characteristic for "${wineType}" wine. Valid options are [${options.map((option) => `"${option}"`).join(', ')}]`
)
})
}
}
for (const characteristic of value) {
if (!options.includes(characteristic)) {
return helper.message({
custom: Joi.expression(
`"${characteristic}" is not a valid characteristic for "${wineType}" wine. Valid options are [${options.map((option) => `"${option}"`).join(', ')}]`
)
})
}
}
return value
}),
return value
}),
volume: volumeValidation(WineVolume),
alcohol: alcoholValidation,
grapeVarietal: Joi.array()