parent
4a7b9f194c
commit
529838406f
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -11,6 +11,8 @@
|
||||
"Blanco",
|
||||
"Cachaça",
|
||||
"Caturra",
|
||||
"colour",
|
||||
"Colours",
|
||||
"Coren",
|
||||
"EAN",
|
||||
"espadín",
|
||||
|
@ -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
|
||||
) {}
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ export class Sake {
|
||||
public producerId: ObjectId, // product producer
|
||||
public designation: SakeDesignation, // table, pure, blended
|
||||
public polishRate: number, // %
|
||||
public characteristics: SakeCharacteristic[],
|
||||
public characteristic: SakeCharacteristic,
|
||||
public starter: SakeStarter, // sake starter
|
||||
public yeastStrain: SakeYeastStrain,
|
||||
public volume: SakeVolume, // bottle volume
|
||||
|
@ -22,7 +22,7 @@ export class Spirit {
|
||||
public producerId: ObjectId, // product producer
|
||||
public type: SpiritType, // spirit type
|
||||
public variant: SpiritVariant, // vodka, rum, liqueur cream, etc
|
||||
public characteristics: SpiritCharacteristic, // 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
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -6,3 +6,5 @@ export * from './wine'
|
||||
export * from './sake'
|
||||
export * from './spirit'
|
||||
export * from './coffee'
|
||||
export * from './review'
|
||||
export * from './review/'
|
||||
|
125
src/types/review.ts
Normal file
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
|
||||
}
|
||||
}
|
93
src/types/review/colour.ts
Normal file
93
src/types/review/colour.ts
Normal file
@ -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
|
||||
}
|
4
src/types/review/index.ts
Normal file
4
src/types/review/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from './visualAssessment'
|
||||
export * from './colour'
|
||||
export * from './primaryFlavoursAndAromas'
|
||||
export * from './textureAndBalance'
|
226
src/types/review/primaryFlavoursAndAromas.ts
Normal file
226
src/types/review/primaryFlavoursAndAromas.ts
Normal file
@ -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'
|
||||
}
|
90
src/types/review/textureAndBalance.ts
Normal file
90
src/types/review/textureAndBalance.ts
Normal file
@ -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'
|
||||
}
|
17
src/types/review/visualAssessment.ts
Normal file
17
src/types/review/visualAssessment.ts
Normal file
@ -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'
|
||||
}
|
@ -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,10 +1,72 @@
|
||||
import Joi from 'joi'
|
||||
import { RatingOption } 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(RatingOption)),
|
||||
@ -12,5 +74,724 @@ export const reviewValidation = (data: unknown): Joi.ValidationResult =>
|
||||
)
|
||||
.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)
|
||||
|
@ -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(SakeCharacteristic)))
|
||||
}
|
||||
|
||||
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()
|
||||
|
@ -231,55 +231,53 @@ export const spiritValidation = (data: unknown): Joi.ValidationResult =>
|
||||
}
|
||||
)
|
||||
),
|
||||
characteristics: Joi.array()
|
||||
.items(
|
||||
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
|
||||
)
|
||||
.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 SpiritCharacteristic]
|
||||
|
||||
const characteristicsInVariant = Object.keys(
|
||||
spiritCharacteristicsMap
|
||||
).filter((char) =>
|
||||
spiritCharacteristicsMap[char as SpiritCharacteristic].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)))
|
||||
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user