Compare commits

..

No commits in common. "staging" and "schemas" have entirely different histories.

52 changed files with 400 additions and 4644 deletions

@ -1,7 +1,5 @@
#!/bin/sh
echo "*****pre-commit hook******"
# Avoid commits to the master branch
BRANCH=`git rev-parse --abbrev-ref HEAD`
REGEX="^(master|main|staging|development)$"

@ -1,37 +0,0 @@
name: Open PR on Staging
on:
pull_request:
types: [opened, edited, synchronize]
branches:
- staging
jobs:
audit_and_check:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: 20
- name: Audit
run: npm audit --omit=dev
- name: Install Dependencies
run: npm ci
- name: License check
run: npm run license-checker
- name: Lint check
run: npm run lint
- name: Formatter check
run: npm run formatter:check
- name: Create Build
run: npm run build

@ -11,10 +11,7 @@
"Blanco",
"Cachaça",
"Caturra",
"colour",
"Colours",
"Coren",
"EAN",
"espadín",
"Genever",
"Jägermeister",
@ -45,7 +42,6 @@
"Robusta",
"RRP",
"screwcap",
"SKU",
"Soju",
"Sokujō",
"Solera",
@ -54,8 +50,6 @@
"Tequilana",
"tobalá",
"Typica",
"Umami",
"UPC",
"Verte",
"VSOP",
"Yamahai",

183
package-lock.json generated

@ -13,9 +13,7 @@
"dotenv": "^16.4.7",
"express": "^4.21.2",
"i18n-iso-countries": "^7.14.0",
"joi": "^17.13.3",
"mongodb": "^6.15.0",
"nostr-tools": "^2.11.1"
"mongodb": "^6.15.0"
},
"devDependencies": {
"@eslint/js": "^9.23.0",
@ -303,21 +301,6 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@hapi/hoek": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
"integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==",
"license": "BSD-3-Clause"
},
"node_modules/@hapi/topo": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz",
"integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==",
"license": "BSD-3-Clause",
"dependencies": {
"@hapi/hoek": "^9.0.0"
}
},
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@ -421,51 +404,6 @@
"sparse-bitfield": "^3.0.3"
}
},
"node_modules/@noble/ciphers": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.3.tgz",
"integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==",
"license": "MIT",
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/curves": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
"integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "1.3.2"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/curves/node_modules/@noble/hashes": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==",
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/hashes": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz",
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -764,57 +702,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/@scure/base": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz",
"integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==",
"funding": [
{
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
],
"license": "MIT"
},
"node_modules/@scure/bip32": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz",
"integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==",
"license": "MIT",
"dependencies": {
"@noble/curves": "~1.1.0",
"@noble/hashes": "~1.3.1",
"@scure/base": "~1.1.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@scure/bip32/node_modules/@noble/curves": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz",
"integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "1.3.1"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@scure/bip39": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz",
"integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "~1.3.0",
"@scure/base": "~1.1.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@sec-ant/readable-stream": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
@ -1548,27 +1435,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/@sideway/address": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
"integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==",
"license": "BSD-3-Clause",
"dependencies": {
"@hapi/hoek": "^9.0.0"
}
},
"node_modules/@sideway/formula": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
"integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==",
"license": "BSD-3-Clause"
},
"node_modules/@sideway/pinpoint": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==",
"license": "BSD-3-Clause"
},
"node_modules/@sindresorhus/is": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-2.1.1.tgz",
@ -5081,19 +4947,6 @@
"node": ">= 0.6.0"
}
},
"node_modules/joi": {
"version": "17.13.3",
"resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz",
"integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==",
"license": "BSD-3-Clause",
"dependencies": {
"@hapi/hoek": "^9.3.0",
"@hapi/topo": "^5.1.0",
"@sideway/address": "^4.1.5",
"@sideway/formula": "^3.0.1",
"@sideway/pinpoint": "^2.0.0"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -6409,38 +6262,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/nostr-tools": {
"version": "2.11.1",
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.11.1.tgz",
"integrity": "sha512-+Oj5t+behIkU9kh3go5wg8Aa5oR7euBU9gOItUNapJe5Gaa+KPzMuTIN+rMRK3DaZ4Zt6RM4kR/ddwstzGKf7g==",
"license": "Unlicense",
"dependencies": {
"@noble/ciphers": "^0.5.1",
"@noble/curves": "1.2.0",
"@noble/hashes": "1.3.1",
"@scure/base": "1.1.1",
"@scure/bip32": "1.3.1",
"@scure/bip39": "1.2.1"
},
"optionalDependencies": {
"nostr-wasm": "0.1.0"
},
"peerDependencies": {
"typescript": ">=5.0.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/nostr-wasm": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz",
"integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==",
"license": "MIT",
"optional": true
},
"node_modules/npm": {
"version": "10.9.2",
"resolved": "https://registry.npmjs.org/npm/-/npm-10.9.2.tgz",
@ -11456,7 +11277,7 @@
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
"devOptional": true,
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",

@ -16,8 +16,7 @@
"lint:fix": "eslint . --fix --ext ts --report-unused-disable-directives --max-warnings 0",
"lint:staged": "eslint --fix --ext ts --report-unused-disable-directives --max-warnings 0",
"lint-staged": "lint-staged",
"start:db": "docker compose -f mongo-docker-compose.yml up -d",
"preinstall": "git config core.hooksPath .git-hooks"
"start:db": "docker compose -f mongo-docker-compose.yml up -d"
},
"repository": {
"type": "git",
@ -31,9 +30,7 @@
"dotenv": "^16.4.7",
"express": "^4.21.2",
"i18n-iso-countries": "^7.14.0",
"joi": "^17.13.3",
"mongodb": "^6.15.0",
"nostr-tools": "^2.11.1"
"mongodb": "^6.15.0"
},
"devDependencies": {
"@eslint/js": "^9.23.0",

@ -10,7 +10,7 @@ import {
spiritsRouter,
coffeeRouter
} from './routes'
import { Route } from './types'
import { Routes } from './types'
dotenv.config()
@ -19,13 +19,13 @@ const port = process.env.PORT || 3000
connectToDatabase()
.then(() => {
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.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.listen(port, () => {
console.log(`Server started at http://localhost:${port}`)

@ -1,12 +1,10 @@
import { ObjectId } from 'mongodb'
export class NostrEvent {
constructor(
public nostrId: string, // nostr unique identifier
public pubkey: string, // public key of the event creator
public created_at: number, // timestamp
public kind: number, // event type, e.g., review, article, comment
public tags: string[][], // array of keywords or hashtags
public content: string, // text content of the event
public id?: ObjectId // database object id
public tags: [string][], // array of keywords or hashtags
public content: string // text content of the event
) {}
}

@ -1,14 +1,12 @@
import { ObjectId } from 'mongodb'
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 productType: ProductType, // product type
public rating: number | RatingOption, // numerical rating, e.g., 84-100 or NS (no score)
public rating: number, // numerical rating, e.g., 1-100
public reviewText: string, // text content of the review
public tastingNote: TastingNote, // an object representing tasting notes
public tastingNotes: string[], // array of tasting notes, e.g., flavours, aromas
public id?: ObjectId // database object id
) {}
}

@ -1,15 +1,5 @@
import { ObjectId } from 'mongodb'
import {
SakeDesignation,
SakeStarter,
VintageOption,
SakeCharacteristic,
StandardDrink,
SakeVolume,
RiceVarietal,
SakeYeastStrain,
SakeKoji
} from '../types'
import { SakeDesignation, SakeStarter, Vintage } from '../types'
import { Alpha2Code } from 'i18n-iso-countries'
import { CurrencyCode } from 'currency-codes-ts/dist/types'
@ -22,17 +12,13 @@ export class Sake {
public region: string, // appellation, village, sub-region, vineyard
public name: string, // label
public producerId: ObjectId, // product producer
public designation: SakeDesignation, // table, pure, blended
public designation: SakeDesignation, // table, pure, blended, mirin: new/true/salt
public polishRate: number, // %
public characteristic: SakeCharacteristic,
public starter: SakeStarter, // sake starter
public yeastStrain: SakeYeastStrain,
public volume: SakeVolume, // bottle volume
public yeastStrain: number,
public alcohol: number, // alcohol percentage
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 | VintageOption, // year, nv (non-vintage) or mv (multi-vintage)
public standardDrinks100ml: number, // number representing an amount of standard drinks per bottle per 100ml
public vintage: Vintage, // year, nv (non-vintage) or mv (multi-vintage)
public RRPamount: number, // 20
public RRPcurrency: CurrencyCode, // USD
public description: string, // detailed description of the product

@ -1,13 +1,5 @@
import { ObjectId } from 'mongodb'
import {
SpiritType,
SpiritVariant,
Ingredient,
VintageOption,
StandardDrink,
SpiritVolume,
SpiritCharacteristic
} from '../types'
import { SpiritType, SpiritVariant, Ingredient, Vintage } from '../types'
import { Alpha2Code } from 'i18n-iso-countries'
import { CurrencyCode } from 'currency-codes-ts/dist/types'
@ -22,12 +14,10 @@ export class Spirit {
public producerId: ObjectId, // product producer
public type: SpiritType, // spirit type
public variant: SpiritVariant, // vodka, rum, liqueur cream, etc
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 | 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 standardDrinks100ml: number, // number representing an amount of standard drinks per bottle
public vintage: Vintage, // 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,9 +3,9 @@ import { UserRole } from '../types'
export class User {
constructor(
public name: string, // name
public npub: string, // npub
public role: UserRole, // user role (user, reviewer, producer)
public id?: ObjectId // database object id
public name: string,
public npub: string[],
public role: UserRole,
public id?: ObjectId
) {}
}

@ -1,19 +1,5 @@
import { ObjectId } from 'mongodb'
import {
WineType,
Viticulture,
BottleClosure,
VintageOption,
StandardDrink,
WineRegion,
WineVolume,
WineStyle,
WhiteWineCharacteristic,
AmberWineCharacteristic,
RoseWineCharacteristic,
RedWineCharacteristic,
GrapeVarietal
} from '../types'
import { WineType, Viticulture, BottleClosure, Vintage } from '../types'
import { Alpha2Code } from 'i18n-iso-countries'
import { CurrencyCode } from 'currency-codes-ts/dist/types'
@ -23,27 +9,21 @@ export class Wine {
public productCodeUPC: string, // Product Code (https://en.wikipedia.org/wiki/Universal_Product_Code)
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 characteristic: (
| WhiteWineCharacteristic
| AmberWineCharacteristic
| RoseWineCharacteristic
| RedWineCharacteristic
)[], // light aromatic, textural, fruit forward, structural & savoury, powerful
public style: string, // bubbles+fizz, table, dessert, fortified, vermouth
public characteristic: string, // light aromatic, textural, fruit forward, structural & savoury, powerful
public country: Alpha2Code, // two-letter country codes defined in ISO 3166-1 (https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)
public region: WineRegion, // appellation, village, sub-region, vineyard
public region: string, // appellation, village, sub-region, vineyard
public name: string, // label
public producerId: ObjectId, // product producer
public grapeVarietal: GrapeVarietal[], // if more than one, list as 'blend'
public vintage: number | VintageOption, // year, nv (non-vintage) or mv (multi-vintage)
public volume: WineVolume, // bottle volume
public varietal: string, // if more than one, list as 'blend'
public vintage: Vintage, // year, nv (non-vintage) or mv (multi-vintage)
public alcohol: number, // alcohol percentage
public standardDrinks: StandardDrink, // an amount of standard drinks per 100ml and bottle in AU, UK and US
public viticulture: Viticulture, // viticulture
public standardDrinks100ml: number, // number representing an amount of standard drinks per bottle
public viticulture: Viticulture, // two-letter country codes
public sulfites: number, // parts per million
public filtered: boolean, // is wine filtered (fined (egg or fish))
public vegan: boolean, // if wine is vegan
public kosher: boolean, // if wine is kosher
public vegan: boolean,
public kosher: boolean,
public closure: BottleClosure, // cork, crown-seal, screwcap
public RRPamount: number, // 20
public RRPcurrency: CurrencyCode, // USD

@ -2,6 +2,7 @@ import express, { Request, Response } from 'express'
import { collections } from '../services/database.service'
import { Coffee } from '../models'
// Global Config
export const coffeeRouter = express.Router()
coffeeRouter.use(express.json())

@ -1,14 +1,9 @@
// External Dependencies
import express, { Request, Response } from 'express'
import { collections } from '../services/database.service'
import { NostrEvent } from '../models'
import {
nostrEventValidation,
handleReqError,
handleReqSuccess
} from '../utils'
import { Event } from 'nostr-tools'
import Joi from 'joi'
// Global Config
export const nostrRouter = express.Router()
nostrRouter.use(express.json())
@ -31,39 +26,23 @@ nostrRouter.get('/', async (_req: Request, res: Response) => {
// POST
nostrRouter.post('/', async (req: Request, res: Response) => {
try {
const {
error,
value: event
}: { error: Joi.ValidationError | undefined; value: Event } =
nostrEventValidation(req.body)
if (error) {
throw error.details[0].message
}
const { id, pubkey, created_at, kind, tags, content } = event
const existingEvent = await collections.nostrEvents?.findOne({
nostrId: id
})
if (existingEvent) {
throw new Error('nostr event with provided "id" exists')
}
const nostrEvent: NostrEvent = {
nostrId: id,
pubkey,
created_at,
kind,
tags,
content
}
const nostrEvent = req.body as NostrEvent
const result = await collections.nostrEvents?.insertOne(nostrEvent)
handleReqSuccess(res, result, 'nostrEvent')
if (result) {
res
.status(201)
.send(
`Successfully created a new nostrEvent with id ${result.insertedId}`
)
} else {
res.status(500).send('Failed to create a new nostrEvent.')
}
} catch (error: unknown) {
handleReqError(res, error)
console.error(error)
if (error instanceof Error) {
res.status(400).send(error.message)
}
}
})

@ -1,11 +1,8 @@
import express, { Request, Response } from 'express'
import { collections } from '../services/database.service'
import { Coffee, Review, Sake, Spirit, Wine } from '../models'
import { reviewValidation, handleReqError, handleReqSuccess } from '../utils'
import Joi from 'joi'
import { DBcollection, TastingNoteKey } from '../types'
import { BSON } from 'mongodb'
import { Review } from '../models'
// Global Config
export const reviewsRouter = express.Router()
reviewsRouter.use(express.json())
@ -13,7 +10,7 @@ reviewsRouter.use(express.json())
// GET
reviewsRouter.get('/', async (_req: Request, res: Response) => {
try {
const reviews = await collections[DBcollection.Reviews]?.find({}).toArray()
const reviews = await collections.reviews?.find({}).toArray()
res.status(200).send(reviews)
} catch (error: unknown) {
@ -28,71 +25,22 @@ reviewsRouter.get('/', async (_req: Request, res: Response) => {
// POST
reviewsRouter.post('/', async (req: Request, res: Response) => {
try {
const {
error,
value: review
}: { error: Joi.ValidationError | undefined; value: Review } =
reviewValidation(req.body)
const review = req.body as Review
if (error) {
throw error.details[0].message
const result = await collections.reviews?.insertOne(review)
if (result) {
res
.status(201)
.send(`Successfully created a new review with id ${result.insertedId}`)
} else {
res.status(500).send('Failed to create a new review.')
}
const existingReview = await collections[DBcollection.Reviews]?.findOne({
eventId: review.eventId
})
if (existingReview) {
throw new Error('review with provided "eventId" exists')
}
const { productId } = review
const _id = new BSON.ObjectId(productId)
let product: Wine | Spirit | Sake | null | undefined = await collections[
DBcollection.Wines
]?.findOne<Wine>({
_id
})
if (!product) {
product = await collections[DBcollection.Sake]?.findOne<Sake>({
_id
})
if (!product) {
product = await collections[DBcollection.Spirits]?.findOne<Spirit>({
_id
})
if (!product) {
const coffee = await collections[
DBcollection.Coffee
]?.findOne<Coffee>({
_id
})
if (!coffee) {
throw new Error('product with provided "productId" does not exists')
}
}
}
}
// Add age property to tasting note, if product has vintage property
if (product) {
const { vintage } = product
if (typeof vintage === 'number') {
review.tastingNote[TastingNoteKey.TextureAndBalance].age =
new Date().getFullYear() - vintage
}
}
const result = await collections[DBcollection.Reviews]?.insertOne(review)
handleReqSuccess(res, result, 'review')
} catch (error: unknown) {
handleReqError(res, error)
console.error(error)
if (error instanceof Error) {
res.status(400).send(error.message)
}
}
})

@ -1,17 +1,8 @@
import express, { Request, Response } from 'express'
import { collections } from '../services/database.service'
import { Sake } from '../models'
import {
sakeValidation,
productCodeValidation,
handleReqError,
handleReqSuccess,
alcoholToStandardDrinks,
volumeToMl
} from '../utils'
import Joi from 'joi'
import { DBcollection, ProductType } from '../types'
// Global Config
export const sakeRouter = express.Router()
sakeRouter.use(express.json())
@ -34,36 +25,22 @@ sakeRouter.get('/', async (_req: Request, res: Response) => {
// POST
sakeRouter.post('/', async (req: Request, res: Response) => {
try {
const {
error,
value: sake
}: { error: Joi.ValidationError | undefined; value: Sake } = sakeValidation(
req.body
)
const sake = req.body as Sake
if (error) {
throw error.details[0].message
const result = await collections.sake?.insertOne(sake)
if (result) {
res
.status(201)
.send(`Successfully created a new sake with id ${result.insertedId}`)
} else {
res.status(500).send('Failed to create a new sake.')
}
const { productCodeEAN, productCodeUPC, productCodeSKU } = sake
await productCodeValidation(
productCodeEAN,
productCodeUPC,
productCodeSKU,
DBcollection.Sake,
ProductType.Sake
)
sake.standardDrinks = alcoholToStandardDrinks(
sake.alcohol,
volumeToMl(sake.volume)
)
const result = await collections[DBcollection.Sake]?.insertOne(sake)
handleReqSuccess(res, result, ProductType.Spirit)
} catch (error: unknown) {
handleReqError(res, error)
console.error(error)
if (error instanceof Error) {
res.status(400).send(error.message)
}
}
})

@ -1,17 +1,8 @@
import express, { Request, Response } from 'express'
import { collections } from '../services/database.service'
import { Spirit } from '../models'
import {
spiritValidation,
productCodeValidation,
handleReqError,
handleReqSuccess,
alcoholToStandardDrinks,
volumeToMl
} from '../utils'
import Joi from 'joi'
import { DBcollection, ProductType } from '../types'
// Global Config
export const spiritsRouter = express.Router()
spiritsRouter.use(express.json())
@ -34,35 +25,22 @@ spiritsRouter.get('/', async (_req: Request, res: Response) => {
// POST
spiritsRouter.post('/', async (req: Request, res: Response) => {
try {
const {
error,
value: spirit
}: { error: Joi.ValidationError | undefined; value: Spirit } =
spiritValidation(req.body)
const spirit = req.body as Spirit
if (error) {
throw error.details[0].message
const result = await collections.spirits?.insertOne(spirit)
if (result) {
res
.status(201)
.send(`Successfully created a new spirit with id ${result.insertedId}`)
} else {
res.status(500).send('Failed to create a new spirit.')
}
const { productCodeEAN, productCodeUPC, productCodeSKU } = spirit
await productCodeValidation(
productCodeEAN,
productCodeUPC,
productCodeSKU,
DBcollection.Spirits,
ProductType.Spirit
)
spirit.standardDrinks = alcoholToStandardDrinks(
spirit.alcohol,
volumeToMl(spirit.volume)
)
const result = await collections[DBcollection.Spirits]?.insertOne(spirit)
handleReqSuccess(res, result, ProductType.Spirit)
} catch (error: unknown) {
handleReqError(res, error)
console.error(error)
if (error instanceof Error) {
res.status(400).send(error.message)
}
}
})

@ -1,9 +1,9 @@
// External Dependencies
import express, { Request, Response } from 'express'
import { collections } from '../services/database.service'
import { User } from '../models'
import { userValidation, handleReqError, handleReqSuccess } from '../utils'
import Joi from 'joi'
// Global Config
export const usersRouter = express.Router()
usersRouter.use(express.json())
@ -26,29 +26,21 @@ usersRouter.get('/', async (_req: Request, res: Response) => {
// POST
usersRouter.post('/', async (req: Request, res: Response) => {
try {
const {
error,
value: newUser
}: { error: Joi.ValidationError | undefined; value: User } = userValidation(
req.body
)
if (error) {
throw error.details[0].message
}
const existingUser = await collections.users?.findOne({
npub: newUser.npub
})
if (existingUser) {
throw new Error('user with provided "npub" exists')
}
const newUser = req.body as User
const result = await collections.users?.insertOne(newUser)
handleReqSuccess(res, result, 'user')
if (result) {
res
.status(201)
.send(`Successfully created a new user with id ${result.insertedId}`)
} else {
res.status(500).send('Failed to create a new user.')
}
} catch (error) {
handleReqError(res, error)
console.error(error)
if (error instanceof Error) {
res.status(400).send(error.message)
}
}
})

@ -1,17 +1,8 @@
import express, { Request, Response } from 'express'
import { collections } from '../services/database.service'
import { Wine } from '../models'
import {
wineValidation,
productCodeValidation,
handleReqError,
handleReqSuccess,
alcoholToStandardDrinks,
volumeToMl
} from '../utils'
import Joi from 'joi'
import { DBcollection, ProductType } from '../types'
// Global Config
export const winesRouter = express.Router()
winesRouter.use(express.json())
@ -19,7 +10,7 @@ winesRouter.use(express.json())
// GET
winesRouter.get('/', async (_req: Request, res: Response) => {
try {
const wines = await collections[DBcollection.Wines]?.find({}).toArray()
const wines = await collections.wines?.find({}).toArray()
res.status(200).send(wines)
} catch (error: unknown) {
@ -34,36 +25,22 @@ winesRouter.get('/', async (_req: Request, res: Response) => {
// POST
winesRouter.post('/', async (req: Request, res: Response) => {
try {
const {
error,
value: wine
}: { error: Joi.ValidationError | undefined; value: Wine } = wineValidation(
req.body
)
const wine = req.body as Wine
if (error) {
throw error.details[0].message
const result = await collections.wines?.insertOne(wine)
if (result) {
res
.status(201)
.send(`Successfully created a new wine with id ${result.insertedId}`)
} else {
res.status(500).send('Failed to create a new wine.')
}
const { productCodeEAN, productCodeUPC, productCodeSKU } = wine
await productCodeValidation(
productCodeEAN,
productCodeUPC,
productCodeSKU,
DBcollection.Wines,
ProductType.Wine
)
wine.standardDrinks = alcoholToStandardDrinks(
wine.alcohol,
volumeToMl(wine.volume)
)
const result = await collections[DBcollection.Wines]?.insertOne(wine)
handleReqSuccess(res, result, ProductType.Wine)
} catch (error: unknown) {
handleReqError(res, error)
console.error(error)
if (error instanceof Error) {
res.status(400).send(error.message)
}
}
})

@ -1,16 +1,16 @@
import * as mongoDB from 'mongodb'
import * as dotenv from 'dotenv'
import { DBcollection } from '../types'
import { DBcollections } from '../types'
// Global Variables
export const collections: {
[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
[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
} = {}
// 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(DBcollection.Users)
const usersCollection: mongoDB.Collection = db.collection(DBcollections.Users)
const nostrEventsCollection: mongoDB.Collection = db.collection(
DBcollection.NostrEvents
DBcollections.NostrEvents
)
const reviewsCollection: mongoDB.Collection = db.collection(
DBcollection.Reviews
DBcollections.Reviews
)
const winesCollection: mongoDB.Collection = db.collection(DBcollection.Wines)
const sakeCollection: mongoDB.Collection = db.collection(DBcollection.Sake)
const winesCollection: mongoDB.Collection = db.collection(DBcollections.Wines)
const sakeCollection: mongoDB.Collection = db.collection(DBcollections.Sake)
const spiritsCollection: mongoDB.Collection = db.collection(
DBcollection.Spirits
DBcollections.Spirits
)
const coffeeCollection: mongoDB.Collection = db.collection(
DBcollection.Coffee
DBcollections.Coffee
)
collections.users = usersCollection

@ -1,10 +1,10 @@
export type CoffeeProcessingType =
| 'De-caff'
| 'Honey'
| 'Semi-dry'
| 'Swiss water'
| 'Sundried'
| 'Washed'
| 'de-caff'
| 'honey'
| 'semi-dry'
| 'swiss water'
| 'sundried'
| 'washed'
type CoffeeVarietyType = 'Robusta' | 'Arabica'
@ -31,7 +31,6 @@ export type CoffeeVariety = {
}
export type CoffeeRoast =
| 'Green'
| 'Light'
| 'Medium'
| 'Medium-Dark'

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

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

@ -1,79 +1,64 @@
export type Availability = 'In stock' | 'Out of stock' | 'Discontinued'
export type Availability = 'in stock' | 'out of stock' | 'discontinued'
export enum Ingredient {
Blanche = 'Blanche',
Anise = 'Anise',
Fennel = 'Fennel',
Hyssop = 'Hyssop',
Mint = 'Mint',
CitrusPeel = 'Citrus Peel',
CorianderSeeds = 'Coriander Seeds',
AngelicaRoot = 'Angelica Root',
Cinnamon = 'Cinnamon',
Clove = 'Clove',
Wheat = 'Wheat',
Rye = 'Rye',
Corn = 'Corn',
Potato = 'Potato',
Barley = 'Barley',
Sugarcane = 'Sugarcane',
Fruits = 'Fruits',
Grains = 'Grains',
Juniper = 'Juniper',
Coriander = 'Coriander',
LemonPeel = 'Lemon peel',
OrangePeel = 'Orange peel',
OrrisRoot = 'Orris root',
CassiaBark = 'Cassia bark',
LicoriceRoot = 'Licorice root',
GrapefruitPeel = 'Grapefruit peel',
Elderflower = 'Elderflower',
Apple = 'Apple',
Blackcurrant = 'Blackcurrant',
Butterscotch = 'Butterscotch',
Peach = 'Peach',
Pear = 'Pear',
Plum = 'Plum',
Raspberries = 'Raspberries',
Sorghum = 'Sorghum',
Rice = 'Rice',
Millet = 'Millet',
BrownSugar = 'Brown sugar',
Buckwheat = 'Buckwheat',
SweetPotato = 'Sweet Potato',
Oat = 'Oat',
EggAdvocaat = 'Egg (Advocaat)',
Strawberry = 'Strawberry',
Almond = 'Almond',
Banana = 'Banana',
Chocolate = 'Chocolate',
SourCherry = 'Sour Cherry',
Violet = 'Violet',
Lemon = 'Lemon',
Melon = 'Melon',
Orange = 'Orange',
Raspberry = 'Raspberry',
Yuzu = 'Yuzu',
ApricotKernel = 'Apricot Kernel',
Hazelnut = 'Hazelnut',
Peanut = 'Peanut',
Pecan = 'Pecan',
Walnut = 'Walnut'
}
export type Ingredient =
| 'Blanche'
| 'Anise'
| 'Fennel'
| 'Hyssop'
| 'Mint'
| 'Citrus Peel'
| 'Coriander Seeds'
| 'Angelica Root'
| 'Cinnamon'
| 'Clove'
| 'Wheat'
| 'Rye'
| 'Corn'
| 'Potato'
| 'Barley'
| 'Sugarcane'
| 'Fruits'
| 'Grains'
| 'Juniper'
| 'Coriander'
| 'Lemon peel'
| 'Orange peel'
| 'Orris root'
| 'Cassia bark'
| 'Licorice root'
| 'Grapefruit peel'
| 'Elderflower'
| 'Apple'
| 'Blackcurrant'
| 'Butterscotch'
| 'Peach'
| 'Pear'
| 'Plum'
| 'Raspberries'
| 'Sorghum'
| 'Rice'
| 'Millet'
| 'Brown sugar'
| 'Buckwheat'
| 'Sweet Potato'
| 'Oat'
| 'Egg (Advocaat)'
| 'Strawberry'
| 'Almond'
| 'Banana'
| 'Chocolate'
| 'Sour Cherry'
| 'Violet'
| 'Lemon'
| 'Melon'
| 'Orange'
| 'Raspberry'
| 'Yuzu'
| 'Almond'
| 'Apricot Kernel'
| 'Hazelnut'
| 'Peanut'
| 'Pecan'
| 'Walnut'
export enum VintageOption {
NV = 'NV',
MV = 'MV'
}
export interface StandardDrink {
'100ml': { AU: number; UK: number; US: number }
bottle: { AU: number; UK: number; US: number }
}
export enum ProductType {
Wine = 'Wine',
Spirit = 'Spirit',
Sake = 'Sake',
Coffee = 'Coffee'
}
export type Vintage = number | 'nv' | 'mv'

@ -1,122 +0,0 @@
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,
Body,
FlavourIntensity,
PalateLength,
ReasoningConcentration,
Quality,
ReadinessToDrink,
ReasoningKey,
TanninString,
TanninObject
} 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]: TanninString | TanninObject
[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
}
}

@ -1,93 +0,0 @@
// Free Run (sake, wine, spirits)
export enum WhiteColour {
WaterWhite = 'Water white',
LemonGreen = 'Lemon-Green',
Lemon = 'Lemon',
Gold = 'Gold',
WhiteBrown = 'White-Brown'
}
// 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',
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
}

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

@ -1,232 +0,0 @@
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 RequiredPrimaryFlavoursAndAromasKey {
Condition = PrimaryFlavoursAndAromasKey.Condition,
Intensity = PrimaryFlavoursAndAromasKey.Intensity,
Age = PrimaryFlavoursAndAromasKey.Age
}
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'
}

@ -1,106 +0,0 @@
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 TanninString {
NA = 'NA'
}
export interface TanninObject {
[Concentration.Low]: {
[key in TanninType]: RipeTannin | UnripeTannin
}
[Concentration.Medium]: {
[key in TanninType]: RipeTannin | UnripeTannin
}
[Concentration.High]: {
[key in TanninType]: RipeTannin | UnripeTannin
}
}
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'
}

@ -1,17 +0,0 @@
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 Route {
export enum Routes {
Users = '/users',
NostrEvents = '/nostr',
Reviews = '/reviews',

@ -1,130 +1,9 @@
export enum SakeDesignation {
Table = 'Table',
Pure = 'Pure',
Blended = 'Blended' // Blended with Spirit - up to 10% of final volume
}
export type SakeDesignation =
| 'table'
| 'pure'
| 'blended'
| 'mirin:new'
| 'mirin:true'
| 'mirin:salt'
export enum TableSakeDesignation {
FutsūShu = 'Futsū-shu'
}
export enum PureSakeDesignation {
Junmai = 'Junmai',
JunmaiGinjo = 'Junmai Ginjo',
JunmaiDaiginjo = 'Junmai Daiginjo'
}
export enum BlendedSakeDesignation {
Honjozo = 'Honjozo',
Ginjo = 'Ginjo',
Daiginjo = 'Daiginjo'
}
export enum SakeStarter {
Kimoto = 'Kimoto',
Sokujō = 'Sokujō',
Yamahai = 'Yamahai'
}
export enum SakeYeastStrain {
KyokaiNo6 = 'Kyokai No. 6',
KyokaiNo7 = 'Kyokai No. 7',
KyokaiNo9 = 'Kyokai No. 9',
Sake101 = 'Sake 101',
Sake301 = 'Sake 301',
AK1 = 'AK-1',
K61 = 'K6-1',
K91 = 'K9-1',
KA11 = 'KA-11',
ShinseiYeast = 'Shinsei Yeast',
HokkaidoYeast = 'Hokkaido Yeast',
TohokuYeast = 'Tohoku Yeast',
KansaiYeast = 'Kansai Yeast',
ChugokuYeast = 'Chugoku Yeast',
ShikokuYeast = 'Shikoku Yeast',
KyushuYeast = 'Kyushu Yeast',
GinjoYeast = 'Ginjo Yeast',
DaiginjoYeast = 'Daiginjo Yeast',
JunmaiYeast = 'Junmai Yeast',
KimotoYeast = 'Kimoto Yeast',
YamahaiYeast = 'Yamahai Yeast'
}
export enum SakeKoji {
AkitaKoji = 'Akita Koji',
HiguchiKoji = 'Higuchi Koji',
HiroshimaKoji = 'Hiroshima Koji',
KiKoji = 'Ki Koji',
KumamotoKoji = 'Kumamoto Koji',
KyokaiKoji = 'Kyokai Koji',
NaganoKoji = 'Nagano Koji',
NishikiKoji = 'Nishiki Koji',
ShinmeiKoji = 'Shinmei Koji',
TakahashiKoji = 'Takahashi Koji',
AssociationNo6 = 'Association No. 6',
AssociationNo9 = 'Association No. 9',
AssociationNo10 = 'Association No. 10',
KA1 = 'KA-1',
KA4 = 'KA-4',
K7 = 'K7',
ShinseiKoji = 'Shinsei Koji',
HokkaidoKoji = 'Hokkaido Koji',
TohokuKoji = 'Tohoku Koji',
KansaiKoji = 'Kansai Koji',
ChugokuKoji = 'Chugoku Koji',
ShikokuKoji = 'Shikoku Koji',
KyushuKoji = 'Kyushu Koji'
}
export interface SakePolishMin {
min: number
}
export enum SakeCharacteristic {
LightAndRefreshing = 'Light and Refreshing',
CleanAndCrisp = 'Clean and Crisp',
FruityAndAromatic = 'Fruity and Aromatic',
RichAndUmami = 'Rich and Umami',
ComplexAndLayered = 'Complex and Layered',
RobustAndFullBodied = 'Robust and Full-Bodied'
}
export enum SakeVolume {
'0.18L' = '0.18L',
'0.3L' = '0.3L',
'0.5L' = '0.5L',
'0.72L' = '0.72L',
'1.8L' = '1.8L',
'3.6L' = '3.6L'
}
export enum RiceVarietal {
Blended = 'BLENDED',
AkitaSakeKomachi = 'Akita Sake Komachi',
Akitakomachi = 'Akitakomachi',
DewaSansan = 'Dewa Sansan',
Ginnosei = 'Ginnosei',
Gohyakumangoku = 'Gohyakumangoku',
HanaFubuki = 'Hana-Fubuki',
HattanNishiki = 'Hattan-Nishiki',
Hinohikari = 'Hinohikari',
Hitomebore = 'Hitomebore',
HyogoKitaNishiki = 'Hyogo Kita Nishiki',
Ibaraki5 = 'Ibaraki 5',
KairyōMai = 'Kairyō-mai',
KitaNishiki = 'Kita Nishiki',
KokuryūMai = 'Kokuryū-mai',
MiyamaNishiki = 'Miyama Nishiki',
NakateShinseiki = 'Nakate Shinseiki',
NiigataKoshihikari = 'Niigata Koshihikari',
Notohikari = 'Notohikari',
Ōmachi = 'Ōmachi',
Sakamai = 'Sakamai',
Sankei65 = 'Sankei 65',
Shinriki = 'Shinriki',
Tamazakae = 'Tamazakae',
Tōkai14 = 'Tōkai 14',
YamadaNishiki = 'Yamada Nishiki',
Yamagata4 = 'Yamagata 4',
YumeIkkon = 'Yume-Ikkon'
}
export type SakeStarter = 'Kimoto' | 'Sokujō' | 'Yamahai'

@ -1,93 +1,172 @@
export enum SpiritType {
White = 'White',
Dark = 'Dark',
Liqueurs = 'Liqueurs'
export type SpiritType = 'white' | 'dark' | 'liqueurs'
export type SpiritVariant =
| 'Absinthe'
| 'Pastis'
| 'Vodka'
| 'Genever'
| 'Gin'
| 'Mezcal'
| 'Rum'
| 'Eau de Vie'
| 'Grappa'
| 'Baijiu'
| 'Soju'
| 'Absinthe'
| 'Brandy'
| 'Calvados'
| 'Chartreuse'
| 'Genever'
| 'Mezcal'
| 'Rum'
| 'Slivovitz'
| 'Whiskey'
| 'Amaro'
| 'Coffee'
| 'Cream'
| 'Creme'
| 'Flowers'
| 'Fruit'
| 'Herb'
| 'Honey'
| 'Nut'
export interface WhiteSpiritKind {
Absinthe: ['Blanche']
Pastis: [
'Anise',
'Fennel',
'Licorice Root',
'Hyssop',
'Mint',
'Citrus Peel',
'Coriander Seeds',
'Angelica Root',
'Cinnamon',
'Clove'
]
Vodka: [
'Wheat',
'Rye',
'Corn',
'Potato',
'Barley',
'Sugarcane',
'Fruits',
'Grains'
]
Genever: [
{
Young: 'Juniper'
}
]
Gin: [
{
'London Dry': [
'Juniper',
'Coriander',
'Angelica root',
'Lemon peel',
'Orange peel',
'Orris root',
'Cassia bark',
'Licorice root',
'Grapefruit peel',
'Elderflower'
]
},
'Plymouth'
]
Mezcal: [{ Joven: ['espadín', 'tepeztate', 'Tequilana (blue)', 'tobalá'] }]
Rum: ['Blanco', 'Cachaça', 'Platino', 'Agricole']
'Eau de Vie': [
'Apple (Pomme)',
'Blackcurrant (Kirsch)',
'Butterscotch (Schnapps)',
'Peach (Pêche, Schnapps)',
'Pear (Poire William)',
'Plum (Mirabelle, Slivovitz, Rakia)',
'Raspberries (Framboise)'
]
Grappa: ['Marc', 'Pisco']
Baijiu: ['Sorghum', 'Wheat', 'Barley', 'Rice', 'Millet']
Soju: ['Barley', 'Brown sugar', 'Buckwheat', 'Rice', 'Sweet Potato']
}
export enum SpiritVariant {
Absinthe = 'Absinthe',
Pastis = 'Pastis',
Vodka = 'Vodka',
Gin = 'Gin',
Mezcal = 'Mezcal',
Eau = 'Eau de Vie',
Grappa = 'Grappa',
Baijiu = 'Baijiu',
Soju = 'Soju',
Brandy = 'Brandy',
Calvados = 'Calvados',
Chartreuse = 'Chartreuse',
Genever = 'Genever',
Rum = 'Rum',
Slivovitz = 'Slivovitz',
Whiskey = 'Whiskey',
Amaro = 'Amaro',
Coffee = 'Coffee',
Cream = 'Cream',
Creme = 'Creme',
Flowers = 'Flowers',
Fruit = 'Fruit',
Herb = 'Herb',
Honey = 'Honey',
Nut = 'Nut'
export interface DarkSpiritKind {
Absinthe: ['Jaune', 'Verte']
Brandy: [
{
Grape: [
'VS',
'VSOP',
'XO',
'Beyond Age',
'Solera',
'Solera Reserva',
'Solera Gran Reserva'
]
}
]
Calvados: ['Apple', 'Pear']
Chartreuse: ['Green', 'Yellow']
Genever: [{ Old: ['Juniper'] }, { Coren: ['Juniper'] }]
Mezcal: ['Reposado', 'Abuelo', 'Añejo', 'Extra Añejo']
Rum: [
{
Sugar: [
'Cachaca (amarela/ouro)',
'Dark Rum',
'Gold Rum',
'Over-proof',
'Premium',
'Spiced'
]
}
]
Slivovitz: []
Whiskey: ['Barley', 'Rye', 'Wheat', 'Corn', 'Oat', 'Rice']
}
export enum SpiritVolume {
'0.05L' = '0.05L',
'0.15L' = '0.15L',
'0.25L' = '0.25L',
'0.375L' = '0.375L',
'0.5L' = '0.5L',
'0.7L' = '0.7L',
'1L' = '1L'
}
export enum SpiritCharacteristic {
LightAndNeutral = 'Light and Neutral',
FruityAndAromatic = 'Fruity and Aromatic',
HerbalAndBotanical = 'Herbal and Botanical',
SweetAndSyrupy = 'Sweet and Syrupy',
SmokyAndSpicy = 'Smoky and Spicy',
RichAndFullBodied = 'Rich and Full-Bodied'
}
export enum WhiteSpiritVariant {
Absinthe = 'Absinthe',
Pastis = 'Pastis',
Vodka = 'Vodka',
Genever = 'Genever',
Gin = 'Gin',
Mezcal = 'Mezcal',
Rum = 'Rum',
EauDeVie = 'Eau de Vie',
Grappa = 'Grappa',
Baijiu = 'Baijiu',
Soju = 'Soju',
Aquavit = 'Aquavit',
Arrack = 'Arrack'
}
export enum DarkSpiritVariant {
Absinthe = 'Absinthe',
Brandy = 'Brandy',
Calvados = 'Calvados',
Chartreuse = 'Chartreuse',
Genever = 'Genever',
Mezcal = 'Mezcal',
Rum = 'Rum',
Slivovitz = 'Slivovitz',
Arrack = 'Arrack',
Whiskey = 'Whiskey'
}
export enum LiqueursSpiritVariant {
Amaro = 'Amaro',
Coffee = 'Coffee',
Cream = 'Cream',
Creme = 'Creme',
Flowers = 'Flowers',
Fruit = 'Fruit',
Herb = 'Herb',
Honey = 'Honey',
Nut = 'Nut'
export interface LiqueursSpiritKind {
Amaro: []
Coffee: []
Cream: [
'Egg (Advocaat)',
'Amarula',
'Rum',
'Strawberry',
'Whiskey (Baileys etc)'
]
Creme: [
'Almond',
'Banana',
'Blackcurrant',
'Chocolate',
'Peach',
'Sour Cherry',
'Violet'
]
Flowers: ['Rose', 'Violet', 'Elderflower']
Fruit: [
'Blackcurrant',
'Lemon',
'Melon',
'Orange',
'Peach',
'Plum',
'Raspberry',
'Yuzu'
]
Herb: [
'Anise',
'Dom Benedictine',
'Bitters',
'Ginger',
'Jägermeister',
'Metaxa',
'Mint'
]
Honey: ['Licor 43', 'Rum', 'Vodka', 'Whiskey']
Nut: ['Almond', 'Apricot Kernel', 'Hazelnut', 'Peanut', 'Pecan', 'Walnut']
}

@ -1,5 +1,5 @@
export enum UserRole {
User = 'User',
Reviewer = 'Reviewer',
Producer = 'Producer' // not able to leave a review
User = 'user',
Reviewer = 'reviewer',
Producer = 'producer'
}

@ -1,358 +1,5 @@
export enum WineType {
White = 'White',
Amber = 'Amber',
Rose = 'Rose',
Red = 'Red'
}
export type WineType = 'white' | 'amber' | 'rose' | 'red'
export enum WhiteWineCharacteristic {
LightAromatic = 'Light Aromatic',
TexturalAndSavory = 'Textural and Savory',
RichAndFruitForward = 'Rich and Fruit Forward'
}
export type Viticulture = 'biodynamic' | 'organic' | 'conventional'
export enum AmberWineCharacteristic {
TexturalAndAromatic = 'Textural and Aromatic',
StructuralAndSavory = 'Structural and Savory',
PowerAndPresence = 'Power and Presence'
}
export enum RoseWineCharacteristic {
LightAndFruitForward = 'Light and Fruit Forward',
TexturalAndSavory = 'Textural and Savory',
StructuralAndAromatic = 'Structural and Aromatic'
}
export enum RedWineCharacteristic {
LightAndFruitForward = 'Light and Fruit Forward',
StructuralAndAromatic = 'Structural and Aromatic',
TexturalAndSavory = 'Textural and Savory',
PowerAndPresence = 'Power and Presence'
}
export enum WineStyle {
BubblesAndFizz = 'Bubbles + Fizz',
Table = 'Table',
Dessert = 'Dessert',
Fortified = 'Fortified',
Vermouth = 'Vermouth'
}
export enum Viticulture {
Biodynamic = 'Biodynamic',
Organic = 'Organic',
Conventional = 'Conventional'
}
export enum BottleClosure {
Cork = 'Cork',
CrownSeal = 'Crown-seal',
Screwcap = 'Screwcap'
}
export interface WineRegion {
[key: string]:
| string[]
| { [key: string]: string[] | { [key: string]: string[] } }
}
export enum WineVolume {
'0.05L' = '0.05L',
'0.187L' = '0.187L',
'0.375L' = '0.375L',
'0.5L' = '0.5L',
'0.75L' = '0.75L',
'1.5L' = '1.5L',
'3L' = '3L',
'6L' = '6L',
'12L' = '12L'
}
export enum GrapeVarietal {
FieldBlend = 'FIELD BLEND',
Acolon = 'Acolon',
Albariño = 'Albariño',
Aligoté = 'Aligoté',
Altesse = 'Altesse',
Amigne = 'Amigne',
Ansonica = 'Ansonica',
AntãoVaz = 'Antão Vaz',
Arbane = 'Arbane',
ArboisBlanc = 'Arbois Blanc',
Arneis = 'Arneis',
Arrufiac = 'Arrufiac',
Assyrtiko = 'Assyrtiko',
Auxerrois = 'Auxerrois',
Bacchus = 'Bacchus',
Biancolella = 'Biancolella',
Bical = 'Bical',
BlancDuBois = 'Blanc du Bois',
BombinoBianco = 'Bombino Bianco',
Bourboulenc = 'Bourboulenc',
Bovale = 'Bovale',
Catarratto = 'Catarratto',
Chardonnay = 'Chardonnay',
Chasselas = 'Chasselas',
CheninBlanc = 'Chenin Blanc',
Clairette = 'Clairette',
Colombard = 'Colombard',
Cortese = 'Cortese',
Courbu = 'Courbu',
Couston = 'Couston',
Crouchen = 'Crouchen',
Duras = 'Duras',
Elbling = 'Elbling',
Emir = 'Emir',
Falanghina = 'Falanghina',
FernãoPires = 'Fernão Pires',
Fiano = 'Fiano',
FolleBlanche = 'Folle Blanche',
Friulano = 'Friulano',
Furmint = 'Furmint',
Gaglioppo = 'Gaglioppo',
GamayBlanc = 'Gamay Blanc',
GarnachaBlanca = 'Garnacha Blanca',
Gascon = 'Gascon',
Gavi = 'Gavi',
Gewürztraminer = 'Gewürztraminer',
Godello = 'Godello',
GouaisBlanc = 'Gouais Blanc',
Grechetto = 'Grechetto',
GrenacheBlanc = 'Grenache Blanc',
GrosManseng = 'Gros Manseng',
GrünerVeltliner = 'Grüner Veltliner',
Hárslevelü = 'Hárslevelü',
Huxelrebe = 'Huxelrebe',
Inzolia = 'Inzolia',
Jacquère = 'Jacquère',
Kerner = 'Kerner',
KleinConstantia = 'Klein Constantia',
Kunegund = 'Kunegund',
Lagarino = 'Lagarino',
Luglienga = 'Luglienga',
Macabeo = 'Macabeo',
Malvasia = 'Malvasia',
Marsanne = 'Marsanne',
MelonDeBourgogne = 'Melon de Bourgogne',
MerlotBlanc = 'Merlot Blanc',
Minutolo = 'Minutolo',
Moscato = 'Moscato',
MüllerThurgau = 'Müller-Thurgau',
Muscadelle = 'Muscadelle',
Muscat = 'Muscat',
Nascetta = 'Nascetta',
Nosiola = 'Nosiola',
Nuragus = 'Nuragus',
Okçular = 'Okçular',
Ondenc = 'Ondenc',
Oran = 'Oran',
PacherencDuVicBilh = 'Pacherenc du Vic-Bilh',
PansaBlanca = 'Pansa Blanca',
Parellada = 'Parellada',
Pecorino = 'Pecorino',
PedroXiménez = 'Pedro Ximénez',
PetitManseng = 'Petit Manseng',
PetitMeslier = 'Petit Meslier',
Picolit = 'Picolit',
Picpoul = 'Picpoul',
PinotBlanc = 'Pinot Blanc',
PinotGrigio = 'Pinot Grigio',
PinotGris = 'Pinot Gris',
PinotMeunier = 'Pinot Meunier',
PinotNoirBlanc = 'Pinot Noir Blanc',
PiquepoulBlanc = 'Piquepoul Blanc',
PlavacMali = 'Plavac Mali',
Raboso = 'Raboso',
Riesling = 'Riesling',
RoterVeltliner = 'Roter Veltliner',
Roupeiro = 'Roupeiro',
Roussanne = 'Roussanne',
SauvignonBlanc = 'Sauvignon Blanc',
Savagnin = 'Savagnin',
Scheurebe = 'Scheurebe',
Sémillon = 'Sémillon',
Sercial = 'Sercial',
Siegerrebe = 'Siegerrebe',
Silvaner = 'Silvaner',
SouvignierGris = 'Souvignier Gris',
Sylvaner = 'Sylvaner',
Taminga = 'Taminga',
TintaAmarela = 'Tinta Amarela',
TintaBarroca = 'Tinta Barroca',
Torrontés = 'Torrontés',
Trebbiano = 'Trebbiano',
Treixadura = 'Treixadura',
UgniBlanc = 'Ugni Blanc',
Verdejo = 'Verdejo',
Verdelho = 'Verdelho',
Verdicchio = 'Verdicchio',
Vermentino = 'Vermentino',
Viennoise = 'Viennoise',
Viognier = 'Viognier',
Vitovska = 'Vitovska',
Xarello = 'Xarello',
Xynomavro = 'Xynomavro',
ZeleniVrh = 'Zeleni Vrh',
Abbuoto = 'Abbuoto',
Agiorgitiko = 'Agiorgitiko',
Aglianico = 'Aglianico',
Aladasturi = 'Aladasturi',
Albarossa = 'Albarossa',
AlicanteBouschet = 'Alicante Bouschet',
Ancellotta = 'Ancellotta',
Aragonez = 'Aragonez',
Aramon = 'Aramon',
Areni = 'Areni',
Baga = 'Baga',
Barbera = 'Barbera',
Bastardo = 'Bastardo',
Béquignol = 'Béquignol',
BlackMuscat = 'Black Muscat',
Blaufränkisch = 'Blaufränkisch',
Bobal = 'Bobal',
Boğazkere = 'Boğazkere',
Bonarda = 'Bonarda',
Bouchet = 'Bouchet',
Brachetto = 'Brachetto',
CabernetFranc = 'Cabernet Franc',
CabernetSauvignon = 'Cabernet Sauvignon',
CaiñoTinto = 'Caiño Tinto',
Calabrese = 'Calabrese',
Canaiolo = 'Canaiolo',
Cannonau = 'Cannonau',
Carignan = 'Carignan',
Carmenère = 'Carmenère',
Castelão = 'Castelão',
Cataratto = 'Cataratto',
Chambourcin = 'Chambourcin',
Charbono = 'Charbono',
Chenanson = 'Chenanson',
Cinsault = 'Cinsault',
Colonnata = 'Colonnata',
Colorino = 'Colorino',
Corvina = 'Corvina',
Corvinone = 'Corvinone',
Counoise = 'Counoise',
Croatina = 'Croatina',
Dolcetto = 'Dolcetto',
Dornfelder = 'Dornfelder',
Durif = 'Durif',
Enantio = 'Enantio',
Fer = 'Fer',
Ferrandina = 'Ferrandina',
FeteascăNeagră = 'Fetească Neagră',
FogliaTonda = 'Foglia Tonda',
Freisa = 'Freisa',
Frühburgunder = 'Frühburgunder',
Gamay = 'Gamay',
Garnacha = 'Garnacha',
Girò = 'Girò',
GodelloTinto = 'Godello Tinto',
Graciano = 'Graciano',
Greco = 'Greco',
Grenache = 'Grenache',
Grolleau = 'Grolleau',
GrosCabernet = 'Gros Cabernet',
Guanciale = 'Guanciale',
Helfensteiner = 'Helfensteiner',
Heroldrebe = 'Heroldrebe',
Kadarka = 'Kadarka',
KalecikKarasi = 'Kalecik Karasi',
Kékfrankos = 'Kékfrankos',
Lagrein = 'Lagrein',
Lambrusco = 'Lambrusco',
Liatiko = 'Liatiko',
ListánNegro = 'Listán Negro',
LoureiroTinto = 'Loureiro Tinto',
Magliocco = 'Magliocco',
Malbec = 'Malbec',
MalvasiaNera = 'Malvasia Nera',
Mammolo = 'Mammolo',
Mandolari = 'Mandolari',
MansengNoir = 'Manseng Noir',
Marzemino = 'Marzemino',
Mauzac = 'Mauzac',
Mavroudi = 'Mavroudi',
Mencia = 'Mencia',
Merlot = 'Merlot',
Miro = 'Miro',
Mission = 'Mission',
Molinara = 'Molinara',
Monastrell = 'Monastrell',
Montepulciano = 'Montepulciano',
MoraviaAgria = 'Moravia Agria',
Morellino = 'Morellino',
Mourvèdre = 'Mourvèdre',
Müllerrebe = 'Müllerrebe',
MuscatRouge = 'Muscat Rouge',
Narince = 'Narince',
Nebbiolo = 'Nebbiolo',
Negoska = 'Negoska',
NerelloCappuccio = 'Nerello Cappuccio',
NerelloMascalese = 'Nerello Mascalese',
Öküzgözü = 'Öküzgözü',
Pais = 'Pais',
Pallagrello = 'Pallagrello',
Passetoutgrain = 'Passetoutgrain',
Patrigone = 'Patrigone',
PetitBouschet = 'Petit Bouschet',
PetitVerdot = 'Petit Verdot',
Pignatello = 'Pignatello',
PinotNoir = 'Pinot Noir',
Pinotage = 'Pinotage',
PiquepoulNoir = 'Piquepoul Noir',
Primitivo = 'Primitivo',
Priorat = 'Priorat',
Prokupac = 'Prokupac',
Refosco = 'Refosco',
RibollaGialla = 'Ribolla Gialla',
Robola = 'Robola',
Romano = 'Romano',
Rondinella = 'Rondinella',
Rossese = 'Rossese',
Roussin = 'Roussin',
RubyCabernet = 'Ruby Cabernet',
Sagrantino = 'Sagrantino',
Sangiovese = 'Sangiovese',
Sansovino = 'Sansovino',
Saperavi = 'Saperavi',
Schioppettino = 'Schioppettino',
Sciacarello = 'Sciacarello',
SémillonRouge = 'Sémillon Rouge',
Shiraz = 'Shiraz',
SilvanerRouge = 'Silvaner Rouge',
Souzão = 'Souzão',
Spanna = 'Spanna',
StLaurent = 'St. Laurent',
Sultani = 'Sultani',
Syrah = 'Syrah',
Tannat = 'Tannat',
Tarrango = 'Tarrango',
Tempranillo = 'Tempranillo',
Teroldego = 'Teroldego',
TintaFrancisca = 'Tinta Francisca',
TintaRoriz = 'Tinta Roriz',
TintoFino = 'Tinto Fino',
TourigaFranca = 'Touriga Franca',
TourigaNacional = 'Touriga Nacional',
Trincadeira = 'Trincadeira',
Trollinger = 'Trollinger',
UvaDiTroia = 'Uva di Troia',
UvaLonganesi = 'Uva Longanesi',
UvaRara = 'Uva Rara',
Vaccarèse = 'Vaccarèse',
Valdiguié = 'Valdiguié',
Valpolicella = 'Valpolicella',
VermentinoNero = 'Vermentino Nero',
ViennoiseRouge = 'Viennoise Rouge',
Vignoles = 'Vignoles',
Vinhão = 'Vinhão',
ViognierRouge = 'Viognier Rouge',
VitisRiparia = 'Vitis Riparia',
ZanteCurrant = 'Zante Currant',
Zeni = 'Zeni',
Žilavka = 'Žilavka',
Zweigelt = 'Zweigelt'
}
// retsina greek wine with pine essences should be considered a vermouth
export type BottleClosure = 'cork' | 'crown-seal' | 'screwcap'

@ -1,38 +0,0 @@
import { SakeVolume, SpiritVolume, StandardDrink, WineVolume } from '../types'
import { roundToOneDecimal } from './'
export const alcoholToStandardDrinks = (
alcohol: number,
bottle: number
): StandardDrink => {
const UK100ml = roundToOneDecimal(10 * alcohol)
const AU100ml = roundToOneDecimal(7.91 * alcohol)
const US100ml = roundToOneDecimal(5.64 * alcohol)
const bottleMultiplier = bottle / 100
return {
'100ml': {
UK: UK100ml,
AU: AU100ml,
US: US100ml
},
bottle: {
UK: roundToOneDecimal(UK100ml * bottleMultiplier),
AU: roundToOneDecimal(AU100ml * bottleMultiplier),
US: roundToOneDecimal(US100ml * bottleMultiplier)
}
}
}
export const volumeToMl = (
volume: WineVolume | SpiritVolume | SakeVolume
): number => {
if (volume.endsWith('L')) {
const volumeMl = volume.replace('L', '')
return Number(volumeMl) * 1000
}
throw new Error('Not supported volume type')
}

@ -1,8 +0,0 @@
export * from './validation'
export * from './nostr'
export * from './route'
export * from './utils'
export * from './alcohol'
export * from './wine'
export * from './spirit'
export * from './sake'

@ -1,34 +0,0 @@
import { nip19 } from 'nostr-tools'
/**
* NPUB provided - it will convert NPUB to HEX
* HEX provided - it will return HEX
*
* @param pubKey in NPUB, HEX format
* @returns HEX format
*/
export const npubToHex = (pubKey: string): string | null => {
// If key is NPUB
if (pubKey.startsWith('npub1')) {
try {
return nip19.decode(pubKey).data as string
} catch (err) {
console.log(Error(`Error converting npub to hex. Error: ${err}`))
return null
}
}
// valid hex key
if (validateHex(pubKey)) return pubKey
// Not a valid hex key
return null
}
/**
* @param hexKey hex private or public key
* @returns whether or not is key valid
*/
export const validateHex = (hexKey: string) => {
return hexKey.match(/^[a-f0-9]{64}$/)
}

@ -1,28 +0,0 @@
import { Response } from 'express'
import { InsertOneResult } from 'mongodb'
export const handleReqError = (res: Response, error: unknown) => {
console.error(error)
if (error instanceof Error) {
res.status(400).send(error.message)
} else if (typeof error === 'string') {
res.status(400).send(error)
}
}
export const handleReqSuccess = (
res: Response,
result: InsertOneResult<Document> | undefined,
itemName: string
) => {
if (result) {
res
.status(201)
.send(
`Successfully created a new ${itemName} with id ${result.insertedId}`
)
} else {
res.status(500).send(`Failed to create a new ${itemName}.`)
}
}

@ -1,36 +0,0 @@
import {
SakeDesignation,
TableSakeDesignation,
PureSakeDesignation,
BlendedSakeDesignation,
SakePolishMin
} from '../types'
export const sakePolishMap:
| {
[key in SakeDesignation.Table]: {
[key in TableSakeDesignation]: SakePolishMin
}
}
| {
[key in SakeDesignation.Pure]: {
[key in PureSakeDesignation]: SakePolishMin
}
}
| {
[key in SakeDesignation.Blended]: {
[key in BlendedSakeDesignation]: SakePolishMin
}
} = {
[SakeDesignation.Table]: { [TableSakeDesignation.FutsūShu]: { min: 0 } },
[SakeDesignation.Pure]: {
[PureSakeDesignation.Junmai]: { min: 0.3 },
[PureSakeDesignation.JunmaiGinjo]: { min: 0.4 },
[PureSakeDesignation.JunmaiDaiginjo]: { min: 0.5 }
},
[SakeDesignation.Blended]: {
[BlendedSakeDesignation.Honjozo]: { min: 0.3 },
[BlendedSakeDesignation.Ginjo]: { min: 0.4 },
[BlendedSakeDesignation.Daiginjo]: { min: 0.5 }
}
}

@ -1,251 +0,0 @@
import {
SpiritCharacteristic,
SpiritType,
WhiteSpiritVariant,
DarkSpiritVariant,
LiqueursSpiritVariant
} from '../types'
// TODO: improve types
export const spiritVariantMap: {
[key in SpiritType]:
| {
[key in WhiteSpiritVariant]: (string | { [key: string]: string[] })[]
}
| {
[key in DarkSpiritVariant]: (string | { [key: string]: string[] })[]
}
| {
[key in LiqueursSpiritVariant]: (string | { [key: string]: string[] })[]
}
} = {
[SpiritType.White]: {
[WhiteSpiritVariant.Absinthe]: ['Blanche'],
[WhiteSpiritVariant.Pastis]: [
'Anise',
'Fennel',
'Licorice Root',
'Hyssop',
'Mint',
'Citrus Peel',
'Coriander Seeds',
'Angelica Root',
'Cinnamon',
'Clove'
],
[WhiteSpiritVariant.Vodka]: [
'Wheat',
'Rye',
'Corn',
'Potato',
'Barley',
'Sugarcane',
'Fruits',
'Grains'
],
[WhiteSpiritVariant.Genever]: [
{
Young: ['Juniper']
}
],
[WhiteSpiritVariant.Gin]: [
{
'London Dry': [
'Juniper',
'Coriander',
'Angelica root',
'Lemon peel',
'Orange peel',
'Orris root',
'Cassia bark',
'Licorice root',
'Grapefruit peel',
'Elderflower'
]
},
'Plymouth'
],
[WhiteSpiritVariant.Mezcal]: [
{ Joven: ['Espadín', 'Tepeztate', 'Tequilana (blue)', 'Tobalá'] }
],
[WhiteSpiritVariant.Rum]: ['Blanco', 'Cachaça', 'Platino', 'Agricole'],
[WhiteSpiritVariant.EauDeVie]: [
'Apple',
'Blackcurrant',
'Butterscotch',
'Peach',
'Pear',
'Plum',
'Raspberries'
],
[WhiteSpiritVariant.Grappa]: ['Marc', 'Pisco'],
[WhiteSpiritVariant.Baijiu]: [
'Sorghum',
'Wheat',
'Barley',
'Rice',
'Millet'
],
[WhiteSpiritVariant.Soju]: [
'Barley',
'Brown sugar',
'Buckwheat',
'Rice',
'Sweet Potato'
],
[WhiteSpiritVariant.Aquavit]: [],
[WhiteSpiritVariant.Arrack]: []
},
[SpiritType.Dark]: {
[DarkSpiritVariant.Absinthe]: ['Jaune', 'Verte'],
[DarkSpiritVariant.Brandy]: [
{
Grape: [
'VS',
'VSOP',
'XO',
'Beyond Age',
'Solera',
'Solera Reserva',
'Solera Gran Reserva'
]
}
],
[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)',
'Dark Rum',
'Gold Rum',
'Over-proof',
'Premium',
'Spiced'
]
}
],
[DarkSpiritVariant.Slivovitz]: [],
[DarkSpiritVariant.Whiskey]: [
'Barley',
'Rye',
'Wheat',
'Corn',
'Oat',
'Rice'
],
[DarkSpiritVariant.Arrack]: []
},
[SpiritType.Liqueurs]: {
[LiqueursSpiritVariant.Amaro]: [],
[LiqueursSpiritVariant.Coffee]: [],
[LiqueursSpiritVariant.Cream]: [
'Egg (Advocaat)',
'Rum',
'Strawberry',
'Whiskey (Baileys etc)'
],
[LiqueursSpiritVariant.Creme]: [
'Almond',
'Banana',
'Blackcurrant',
'Chocolate',
'Peach',
'Sour Cherry',
'Violet'
],
[LiqueursSpiritVariant.Flowers]: ['Rose', 'Violet', 'Elderflower'],
[LiqueursSpiritVariant.Fruit]: [
'Blackcurrant',
'Lemon',
'Melon',
'Orange',
'Peach',
'Plum',
'Raspberry',
'Yuzu'
],
[LiqueursSpiritVariant.Herb]: [
'Anise',
'Dom Benedictine',
'Bitters',
'Ginger',
'Jägermeister',
'Metaxa',
'Mint'
],
[LiqueursSpiritVariant.Honey]: ['Licor 43', 'Rum', 'Vodka', 'Whiskey'],
[LiqueursSpiritVariant.Nut]: [
'Almond',
'Apricot Kernel',
'Hazelnut',
'Peanut',
'Pecan',
'Walnut',
'Amarula'
]
}
}
export const spiritCharacteristicsMap: {
[key in SpiritCharacteristic]: string[]
} = {
[SpiritCharacteristic.LightAndNeutral]: [
WhiteSpiritVariant.Mezcal,
WhiteSpiritVariant.Soju,
WhiteSpiritVariant.Vodka,
WhiteSpiritVariant.Rum,
WhiteSpiritVariant.Aquavit
],
[SpiritCharacteristic.FruityAndAromatic]: [
WhiteSpiritVariant.Rum,
DarkSpiritVariant.Calvados,
WhiteSpiritVariant.EauDeVie,
LiqueursSpiritVariant.Fruit,
WhiteSpiritVariant.Gin,
WhiteSpiritVariant.Grappa
],
[SpiritCharacteristic.HerbalAndBotanical]: [
WhiteSpiritVariant.Absinthe,
DarkSpiritVariant.Absinthe,
LiqueursSpiritVariant.Amaro,
WhiteSpiritVariant.Genever,
DarkSpiritVariant.Genever,
WhiteSpiritVariant.Gin,
WhiteSpiritVariant.Pastis,
DarkSpiritVariant.Chartreuse,
WhiteSpiritVariant.Aquavit
],
[SpiritCharacteristic.SweetAndSyrupy]: [
DarkSpiritVariant.Brandy,
LiqueursSpiritVariant.Cream,
LiqueursSpiritVariant.Creme,
DarkSpiritVariant.Rum,
LiqueursSpiritVariant.Nut
],
[SpiritCharacteristic.SmokyAndSpicy]: [
WhiteSpiritVariant.Baijiu,
DarkSpiritVariant.Rum,
WhiteSpiritVariant.Gin,
WhiteSpiritVariant.EauDeVie,
DarkSpiritVariant.Mezcal,
DarkSpiritVariant.Whiskey,
LiqueursSpiritVariant.Cream,
LiqueursSpiritVariant.Honey,
WhiteSpiritVariant.Arrack,
DarkSpiritVariant.Arrack
],
[SpiritCharacteristic.RichAndFullBodied]: [
WhiteSpiritVariant.Baijiu,
DarkSpiritVariant.Brandy,
DarkSpiritVariant.Rum,
WhiteSpiritVariant.Grappa,
WhiteSpiritVariant.EauDeVie,
LiqueursSpiritVariant.Cream,
LiqueursSpiritVariant.Honey,
DarkSpiritVariant.Whiskey
]
}

@ -1,8 +0,0 @@
export const roundToOneDecimal = (number: number) =>
Math.round(number * 10) / 10
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,9 +0,0 @@
export * from './user'
export * from './nostr'
export * from './review'
export * from './review'
export * from './wine'
export * from './spirit'
export * from './product'
export * from './validations'
export * from './sake'

@ -1,24 +0,0 @@
import Joi from 'joi'
import { npubToHex, validateHex } from '../nostr'
export const nostrEventValidation = (data: unknown): Joi.ValidationResult =>
Joi.object({
id: Joi.string().required(),
kind: Joi.number().required(),
content: Joi.string().required(),
tags: Joi.array().items(Joi.array().items(Joi.string())),
created_at: Joi.number().required(),
pubkey: Joi.string()
.custom((value, helper) => {
const hex = npubToHex(value as string)
if (!hex || !validateHex(hex)) {
return helper.message({
custom: Joi.expression('"pubkey" contains an invalid value')
})
}
return value
})
.required()
}).validate(data)

@ -1,46 +0,0 @@
import { collections } from '../../services/database.service'
import { DBcollection, ProductType } from '../../types'
export const productCodeValidation = async (
ean: string,
upc: string,
sku: string,
collection: DBcollection,
productType: ProductType
) => {
if (!ean && !upc && !sku) {
throw new Error(
'provide "productCodeEAN", "productCodeUPC" or "productCodeSKU"'
)
}
if (ean) {
const existingProduct = await collections[collection]?.findOne({
productCodeEAN: ean
})
if (existingProduct) {
throw new Error(`${productType} with provided "productCodeEAN" exists`)
}
}
if (upc) {
const existingProduct = await collections[collection]?.findOne({
productCodeUPC: upc
})
if (existingProduct) {
throw new Error(`${productType} with provided "productCodeUPC" exists`)
}
}
if (sku) {
const existingProduct = await collections[collection]?.findOne({
productCodeSKU: sku
})
if (existingProduct) {
throw new Error(`${productType} with provided "productCodeSKU" exists`)
}
}
}

@ -1,773 +0,0 @@
import Joi from 'joi'
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,
RequiredPrimaryFlavoursAndAromasKey,
TanninString,
TanninObject
} from '../../types'
import { compareArrays, isObject } from '../utils'
import { producerIdValidation, validateStringValue } from './'
export const reviewValidation = (data: unknown): Joi.ValidationResult =>
Joi.object({
eventId: Joi.string().required(),
productId: producerIdValidation,
productType: Joi.string()
.valid(...Object.values(ProductType))
.required(),
rating: Joi.alternatives()
.try(
Joi.string().valid(...Object.values(RatingOption)),
Joi.number().min(84).max(100)
)
.required(),
reviewText: Joi.string().required(),
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" object has to include the following keys: [${validVisualAssessmentKeys.join(', ')}]`
)
}
const messageWithValidOptions = (
noteKey: TastingNoteKey,
noteSubKey:
| VisualAssessmentKey
| PrimaryFlavoursAndAromasKey
| TextureAndBalanceKey,
options: object,
product?: string
) =>
message(
`provided "tastingNote" is not valid. Valid options for "${[noteKey, noteSubKey].join('-')}"${product ? ` for "${product}"` : ''} are: [${Object.values(options).join(', ')}]`
)
/**
* visualAssessment-clarity validation
*/
const clarity =
tastingNote[TastingNoteKey.VisualAssessment][
VisualAssessmentKey.Clarity
]
if (validateStringValue(clarity, ClarityVisualAssessment)) {
return messageWithValidOptions(
TastingNoteKey.VisualAssessment,
VisualAssessmentKey.Clarity,
ClarityVisualAssessment
)
}
/**
* visualAssessment-nature validation
*/
const nature =
tastingNote[TastingNoteKey.VisualAssessment][
VisualAssessmentKey.Nature
]
if (validateStringValue(nature, NatureVisualAssessment)) {
return messageWithValidOptions(
TastingNoteKey.VisualAssessment,
VisualAssessmentKey.Nature,
NatureVisualAssessment
)
}
/**
* visualAssessment-colour validation
*/
const colour =
tastingNote[TastingNoteKey.VisualAssessment][
VisualAssessmentKey.Colour
]
const validColorOptions = {
...WhiteColour,
...AmberColour,
...RoseColour,
...RedColour,
...BlueColour,
...GreenColour
}
if (validateStringValue(colour, validColorOptions)) {
return messageWithValidOptions(
TastingNoteKey.VisualAssessment,
VisualAssessmentKey.Colour,
validColorOptions
)
}
// check if colour is applicable to the product
const productType: ProductType = helper.state.ancestors[0].productType
switch (productType) {
case ProductType.Wine:
{
if (validateStringValue(colour, WineColour)) {
return messageWithValidOptions(
TastingNoteKey.VisualAssessment,
VisualAssessmentKey.Colour,
WineColour,
productType
)
}
}
break
case ProductType.Sake:
{
if (validateStringValue(colour, SakeColour)) {
return messageWithValidOptions(
TastingNoteKey.VisualAssessment,
VisualAssessmentKey.Colour,
SakeColour,
productType
)
}
}
break
case ProductType.Spirit:
{
if (validateStringValue(colour, SpiritColour)) {
return messageWithValidOptions(
TastingNoteKey.VisualAssessment,
VisualAssessmentKey.Colour,
SpiritColour,
productType
)
}
}
break
default:
break
}
/**
* primaryFlavoursAndAromas validation
*/
const primaryFlavoursAndAromasKeys = Object.keys(
tastingNote[TastingNoteKey.PrimaryFlavoursAndAromas]
)
const validPrimaryFlavoursAndAromasKeys = Object.values(
PrimaryFlavoursAndAromasKey
)
const requiredPrimaryFlavoursAndAromasKeys = Object.values(
RequiredPrimaryFlavoursAndAromasKey
)
for (const requiredKey of requiredPrimaryFlavoursAndAromasKeys) {
if (!primaryFlavoursAndAromasKeys.includes(requiredKey)) {
return message(
`provided "tastingNote" is not valid. "visualAssessment-primaryFlavoursAndAromas" object has to include the following keys: [${requiredPrimaryFlavoursAndAromasKeys.join(', ')}]`
)
}
}
for (const key of primaryFlavoursAndAromasKeys) {
if (
!validPrimaryFlavoursAndAromasKeys.includes(
key as PrimaryFlavoursAndAromasKey
)
) {
return message(
`provided "tastingNote" is not valid. "${key}" is not a valid key for "visualAssessment-primaryFlavoursAndAromas" object, valid keys are: [${validPrimaryFlavoursAndAromasKeys.join(', ')}]`
)
}
}
// primaryFlavoursAndAromas-condition validation
const condition =
tastingNote[TastingNoteKey.PrimaryFlavoursAndAromas][
PrimaryFlavoursAndAromasKey.Condition
]
if (validateStringValue(condition, Condition)) {
return messageWithValidOptions(
TastingNoteKey.PrimaryFlavoursAndAromas,
PrimaryFlavoursAndAromasKey.Condition,
Condition
)
}
// primaryFlavoursAndAromas-intensity validation
const intensity =
tastingNote[TastingNoteKey.PrimaryFlavoursAndAromas][
PrimaryFlavoursAndAromasKey.Intensity
]
if (validateStringValue(intensity, Intensity)) {
return messageWithValidOptions(
TastingNoteKey.PrimaryFlavoursAndAromas,
PrimaryFlavoursAndAromasKey.Intensity,
Intensity
)
}
// primaryFlavoursAndAromas-age validation
const age =
tastingNote[TastingNoteKey.PrimaryFlavoursAndAromas][
PrimaryFlavoursAndAromasKey.Age
]
if (validateStringValue(age, Age)) {
return messageWithValidOptions(
TastingNoteKey.PrimaryFlavoursAndAromas,
PrimaryFlavoursAndAromasKey.Age,
Age
)
}
// primaryFlavoursAndAromas-fruit validation
const fruit =
tastingNote[TastingNoteKey.PrimaryFlavoursAndAromas][
PrimaryFlavoursAndAromasKey.Fruit
]
const validFruitOptions = {
...CitrusFruit,
...AppleFruit,
...StoneFruit,
...RedFruit,
...BlackFruit,
...ChocolateFruit,
...TropicalFruit,
...MelonFruit
}
if (fruit && validateStringValue(fruit, validFruitOptions)) {
return messageWithValidOptions(
TastingNoteKey.PrimaryFlavoursAndAromas,
PrimaryFlavoursAndAromasKey.Fruit,
validFruitOptions
)
}
// primaryFlavoursAndAromas-floral validation
const floral =
tastingNote[TastingNoteKey.PrimaryFlavoursAndAromas][
PrimaryFlavoursAndAromasKey.Floral
]
if (floral && validateStringValue(floral, Floral)) {
return messageWithValidOptions(
TastingNoteKey.PrimaryFlavoursAndAromas,
PrimaryFlavoursAndAromasKey.Floral,
Floral
)
}
// primaryFlavoursAndAromas-vegetal validation
const vegetal =
tastingNote[TastingNoteKey.PrimaryFlavoursAndAromas][
PrimaryFlavoursAndAromasKey.Vegetal
]
if (vegetal && validateStringValue(vegetal, Vegetal)) {
return messageWithValidOptions(
TastingNoteKey.PrimaryFlavoursAndAromas,
PrimaryFlavoursAndAromasKey.Vegetal,
Vegetal
)
}
// primaryFlavoursAndAromas-earth validation
const earth =
tastingNote[TastingNoteKey.PrimaryFlavoursAndAromas][
PrimaryFlavoursAndAromasKey.Earth
]
if (earth && validateStringValue(earth, Earth)) {
return messageWithValidOptions(
TastingNoteKey.PrimaryFlavoursAndAromas,
PrimaryFlavoursAndAromasKey.Earth,
Earth
)
}
// primaryFlavoursAndAromas-earth validation
const microbial =
tastingNote[TastingNoteKey.PrimaryFlavoursAndAromas][
PrimaryFlavoursAndAromasKey.Microbial
]
if (microbial && validateStringValue(microbial, Microbial)) {
return messageWithValidOptions(
TastingNoteKey.PrimaryFlavoursAndAromas,
PrimaryFlavoursAndAromasKey.Microbial,
Microbial
)
}
// primaryFlavoursAndAromas-oak validation
const oak =
tastingNote[TastingNoteKey.PrimaryFlavoursAndAromas][
PrimaryFlavoursAndAromasKey.Oak
]
if (oak && validateStringValue(oak, Oak)) {
return messageWithValidOptions(
TastingNoteKey.PrimaryFlavoursAndAromas,
PrimaryFlavoursAndAromasKey.Oak,
Oak
)
}
// primaryFlavoursAndAromas-chocolate validation
const chocolate =
tastingNote[TastingNoteKey.PrimaryFlavoursAndAromas][
PrimaryFlavoursAndAromasKey.Chocolate
]
if (chocolate && validateStringValue(chocolate, Chocolate)) {
return messageWithValidOptions(
TastingNoteKey.PrimaryFlavoursAndAromas,
PrimaryFlavoursAndAromasKey.Chocolate,
Chocolate
)
}
// primaryFlavoursAndAromas-oxidation validation
const oxidation =
tastingNote[TastingNoteKey.PrimaryFlavoursAndAromas][
PrimaryFlavoursAndAromasKey.Oxidation
]
if (oxidation && validateStringValue(oxidation, Oxidation)) {
return messageWithValidOptions(
TastingNoteKey.PrimaryFlavoursAndAromas,
PrimaryFlavoursAndAromasKey.Oxidation,
Oxidation
)
}
// primaryFlavoursAndAromas-umami validation
const umami =
tastingNote[TastingNoteKey.PrimaryFlavoursAndAromas][
PrimaryFlavoursAndAromasKey.Umami
]
if (umami && validateStringValue(umami, Umami)) {
return messageWithValidOptions(
TastingNoteKey.PrimaryFlavoursAndAromas,
PrimaryFlavoursAndAromasKey.Umami,
Umami
)
}
// primaryFlavoursAndAromas-balsamic validation
const balsamic =
tastingNote[TastingNoteKey.PrimaryFlavoursAndAromas][
PrimaryFlavoursAndAromasKey.Balsamic
]
if (balsamic && validateStringValue(balsamic, Balsamic)) {
return messageWithValidOptions(
TastingNoteKey.PrimaryFlavoursAndAromas,
PrimaryFlavoursAndAromasKey.Balsamic,
Balsamic
)
}
// primaryFlavoursAndAromas-grain validation
const grain =
tastingNote[TastingNoteKey.PrimaryFlavoursAndAromas][
PrimaryFlavoursAndAromasKey.Grain
]
if (grain && validateStringValue(grain, Grain)) {
return messageWithValidOptions(
TastingNoteKey.PrimaryFlavoursAndAromas,
PrimaryFlavoursAndAromasKey.Grain,
Grain
)
}
// primaryFlavoursAndAromas-dairy validation
const dairy =
tastingNote[TastingNoteKey.PrimaryFlavoursAndAromas][
PrimaryFlavoursAndAromasKey.Dairy
]
if (dairy && validateStringValue(dairy, Dairy)) {
return messageWithValidOptions(
TastingNoteKey.PrimaryFlavoursAndAromas,
PrimaryFlavoursAndAromasKey.Dairy,
Dairy
)
}
// primaryFlavoursAndAromas-faults validation
const faults =
tastingNote[TastingNoteKey.PrimaryFlavoursAndAromas][
PrimaryFlavoursAndAromasKey.Faults
]
const validFaultsOptions = {
...Anisoles,
...Brettanomyces,
...VolatileAcidity,
...Reduction
}
if (faults && validateStringValue(faults, validFaultsOptions)) {
return messageWithValidOptions(
TastingNoteKey.PrimaryFlavoursAndAromas,
PrimaryFlavoursAndAromasKey.Faults,
validFaultsOptions
)
}
/**
* textureAndBalance validation
*/
const textureAndBalanceKeys = Object.keys(
tastingNote[TastingNoteKey.TextureAndBalance]
)
const validTextureAndBalanceKeys = Object.values(
TextureAndBalanceKey
).filter((key) => key !== TextureAndBalanceKey.Age)
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
]
if (validateStringValue(sweetness, Sweetness)) {
return messageWithValidOptions(
TastingNoteKey.TextureAndBalance,
TextureAndBalanceKey.Sweetness,
Sweetness
)
}
// textureAndBalance-acidity validation
const acidity =
tastingNote[TastingNoteKey.TextureAndBalance][
TextureAndBalanceKey.Acidity
]
if (validateStringValue(acidity, Concentration)) {
return messageWithValidOptions(
TastingNoteKey.TextureAndBalance,
TextureAndBalanceKey.Acidity,
Concentration
)
}
// 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 (typeof tannin === 'string') {
if (validateStringValue(tannin, TanninString)) {
return messageWithValidOptions(
TastingNoteKey.TextureAndBalance,
TextureAndBalanceKey.Tannin,
TanninString
)
}
} else {
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
] as TanninObject
)[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
] as TanninObject
)[tanninKey][tanninTypeKey]
const validTanninValueOptions = { ...RipeTannin, ...UnripeTannin }
if (validateStringValue(tanninValue, validTanninValueOptions)) {
return messageWithValidOptions(
TastingNoteKey.TextureAndBalance,
TextureAndBalanceKey.Tannin,
validTanninValueOptions
)
}
}
// textureAndBalance-alcohol validation
const alcohol =
tastingNote[TastingNoteKey.TextureAndBalance][
TextureAndBalanceKey.Alcohol
]
if (validateStringValue(alcohol, Concentration)) {
return messageWithValidOptions(
TastingNoteKey.TextureAndBalance,
TextureAndBalanceKey.Alcohol,
Concentration
)
}
// textureAndBalance-body validation
const body =
tastingNote[TastingNoteKey.TextureAndBalance][
TextureAndBalanceKey.Body
]
if (validateStringValue(body, Body)) {
return messageWithValidOptions(
TastingNoteKey.TextureAndBalance,
TextureAndBalanceKey.Body,
Body
)
}
// textureAndBalance-flavourIntensity validation
const flavourIntensity =
tastingNote[TastingNoteKey.TextureAndBalance][
TextureAndBalanceKey.FlavourIntensity
]
if (validateStringValue(flavourIntensity, FlavourIntensity)) {
return messageWithValidOptions(
TastingNoteKey.TextureAndBalance,
TextureAndBalanceKey.FlavourIntensity,
FlavourIntensity
)
}
// textureAndBalance-palateLength validation
const palateLength =
tastingNote[TastingNoteKey.TextureAndBalance][
TextureAndBalanceKey.PalateLength
]
if (validateStringValue(palateLength, PalateLength)) {
return messageWithValidOptions(
TastingNoteKey.TextureAndBalance,
TextureAndBalanceKey.PalateLength,
PalateLength
)
}
// 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
]
if (validateStringValue(quality, Quality)) {
return messageWithValidOptions(
TastingNoteKey.TextureAndBalance,
TextureAndBalanceKey.Quality,
Quality
)
}
// textureAndBalance-readinessToDrink validation
const readinessToDrink =
tastingNote[TastingNoteKey.TextureAndBalance][
TextureAndBalanceKey.ReadinessToDrink
]
if (validateStringValue(readinessToDrink, ReadinessToDrink)) {
return messageWithValidOptions(
TastingNoteKey.TextureAndBalance,
TextureAndBalanceKey.ReadinessToDrink,
ReadinessToDrink
)
}
return tastingNote
})
.required()
}).validate(data)

@ -1,137 +0,0 @@
import Joi from 'joi'
import {
SakeDesignation,
SakeCharacteristic,
SakeVolume,
SakeStarter,
RiceVarietal,
SakePolishMin,
SakeYeastStrain,
SakeKoji
} from '../../types'
import {
vintageValidation,
productCodeEANvalidation,
productCodeUPCvalidation,
productCodeSKUvalidation,
countryValidation,
nameValidation,
producerIdValidation,
volumeValidation,
alcoholValidation,
RRPamountValidation,
RRPcurrencyValidation,
descriptionValidation,
urlValidation,
imageValidation
} from './'
import { sakePolishMap } from '../'
export const sakeValidation = (data: unknown): Joi.ValidationResult =>
Joi.object({
productCodeEAN: productCodeEANvalidation,
productCodeUPC: productCodeUPCvalidation,
productCodeSKU: productCodeSKUvalidation,
country: countryValidation,
region: Joi.string(),
name: nameValidation,
producerId: producerIdValidation,
designation: Joi.object()
.custom((designation: { [key in SakeDesignation]: string }, helper) => {
const descriptionKeys = Object.keys(designation)
if (descriptionKeys.length !== 1) {
return helper.message({
custom: Joi.expression(`provide designation is not valid.`)
})
}
const designationKey = descriptionKeys[0]
const designationKeyOptions: string[] = Object.values(SakeDesignation)
if (!designationKeyOptions.includes(designationKey)) {
return helper.message({
custom: Joi.expression(
`provide designation key "${designationKey}" is not valid. Valid options are [${designationKeyOptions.map((option) => `"${option}"`).join(', ')}]`
)
})
}
const descriptionValues = Object.values(designation)
if (descriptionValues.length !== 1) {
return helper.message({
custom: Joi.expression(`provide designation is not valid.`)
})
}
const descriptionValue = descriptionValues[0]
if (typeof descriptionValue !== 'string') {
return helper.message({
custom: Joi.expression(`provide designation is not valid.`)
})
}
const designationValueOptions: string[] = Object.keys(
(sakePolishMap as { [key: string]: { [key: string]: unknown } })[
designationKey
]
)
if (!designationValueOptions.includes(descriptionValue)) {
return helper.message({
custom: Joi.expression(
`provide designation value "${descriptionValue}" is not valid. Valid options are [${designationValueOptions.map((option) => `"${option}"`).join(', ')}]`
)
})
}
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"`
)
})
}
return polishRate
}),
characteristic: Joi.string()
.valid(...Object.values(SakeCharacteristic))
.required(),
starter: Joi.string().valid(...Object.values(SakeStarter)),
yeastStrain: Joi.string().valid(...Object.values(SakeYeastStrain)),
volume: volumeValidation(SakeVolume),
alcohol: alcoholValidation,
riceVarietal: Joi.array()
.items(Joi.string().valid(...Object.values(RiceVarietal)))
.required(),
koji: Joi.string()
.valid(...Object.values(SakeKoji))
.required(),
vintage: vintageValidation,
RRPamount: RRPamountValidation,
RRPcurrency: RRPcurrencyValidation,
description: descriptionValidation,
url: urlValidation,
image: imageValidation
}).validate(data)

@ -1,293 +0,0 @@
import Joi from 'joi'
import {
Ingredient,
SpiritType,
SpiritVolume,
SpiritCharacteristic
} from '../../types'
import { isObject, spiritVariantMap, spiritCharacteristicsMap } from '../'
import {
vintageValidation,
productCodeEANvalidation,
productCodeUPCvalidation,
productCodeSKUvalidation,
countryValidation,
nameValidation,
typeValidation,
producerIdValidation,
volumeValidation,
alcoholValidation,
RRPamountValidation,
RRPcurrencyValidation,
descriptionValidation,
urlValidation,
imageValidation
} from './'
export const spiritValidation = (data: unknown): Joi.ValidationResult =>
Joi.object({
productCodeEAN: productCodeEANvalidation,
productCodeUPC: productCodeUPCvalidation,
productCodeSKU: productCodeSKUvalidation,
country: countryValidation,
region: Joi.string(),
name: nameValidation,
producerId: producerIdValidation,
type: typeValidation(SpiritType),
variant: Joi.alternatives().try(
Joi.string().custom((variant, helper) => {
// return if no value
if (!variant) {
return variant
}
// return if no state ancestors
if (!helper.state.ancestors) {
return variant
}
const spiritType: SpiritType = helper.state.ancestors[0].type
// return if no spiritType
if (!spiritType) {
return variant
}
const options: string[] = Object.keys(spiritVariantMap[spiritType])
if (!options.length) {
return helper.message({
custom: Joi.expression(
`no variants found for provided type of spirit`
)
})
}
if (!options.includes(variant)) {
return helper.message({
custom: Joi.expression(
`"${variant}" is not a valid variant for "${spiritType}" spirit. Valid options are [${options.map((option) => `"${option}"`).join(', ')}]`
)
})
}
return variant
}),
Joi.object().custom(
(
variant: { [key: string]: string | { [key: string]: string } },
helper
) => {
// return if no value
if (!variant) {
return variant
}
// return if multiple variant provided
if (Object.keys(variant).length !== 1) {
return helper.message({
custom: Joi.expression(`multiple variants provided`)
})
}
// return if no state ancestors
if (!helper.state.ancestors) {
return variant
}
const spiritType: SpiritType = helper.state.ancestors[0].type
// return if no spiritType
if (!spiritType) {
return variant
}
/**
* Variant
*/
const variantOptions: string[] = Object.keys(
spiritVariantMap[spiritType]
)
if (!variantOptions.length) {
return helper.message({
custom: Joi.expression(
`no variants found for provided type of spirit`
)
})
}
const variantName = Object.keys(variant)[0]
if (!variantOptions.includes(variantName)) {
return helper.message({
custom: Joi.expression(
`"${variantName}" is not a valid variant for "${spiritType}" spirit. Valid options are [${variantOptions.map((option) => `"${option}"`).join(', ')}]`
)
})
}
/**
* SubVariant
*/
const subVariants: (string | { [key: string]: string[] })[] = (
spiritVariantMap[spiritType] as {
[key: string]: (string | { [key: string]: string[] })[]
}
)[variantName]
const subVariantOptions: string[] =
typeof subVariants[0] === 'string'
? (subVariants as string[])
: subVariants.map((subVariant) => Object.keys(subVariant)[0])
const subVariantValues = Object.values(variant)
if (subVariantValues.length !== 1) {
return helper.message({
custom: Joi.expression(
`"${variantName}" is not a valid variant for "${spiritType}" spirit. Valid options are [${variantOptions.map((option) => `"${option}"`).join(', ')}]`
)
})
}
const subVariant = subVariantValues[0]
if (
typeof subVariant === 'string' &&
!subVariantOptions.includes(subVariant)
) {
return helper.message({
custom: Joi.expression(
`"${subVariant}" is not a valid variant for "${spiritType} -> ${variantName}" spirit. Valid options are [${subVariantOptions.map((option) => `"${option}"`).join(', ')}]`
)
})
} else if (isObject(subVariant)) {
const providedSubVariants = Object.keys(subVariant)
if (providedSubVariants.length !== 1) {
return helper.message({
custom: Joi.expression(
`"${subVariant}" is not a valid variant for "${spiritType} -> ${variantName}" spirit. Valid options are [${subVariantOptions.map((option) => `"${option}"`).join(', ')}]`
)
})
}
const subVariantName = providedSubVariants[0]
if (!subVariantOptions.includes(subVariantName)) {
return helper.message({
custom: Joi.expression(
`"${subVariantName}" is not a valid variant for "${spiritType} -> ${variantName}" spirit. Valid options are [${subVariantOptions.map((option) => `"${option}"`).join(', ')}]`
)
})
}
/**
* SubSubVariant
*/
if (typeof subVariants[0] === 'string') {
return helper.message({
custom: Joi.expression(
`Provided not valid variant for "${spiritType} -> ${variantName} -> ${subVariantName}" spirit. Valid options are [${subVariantOptions.map((option) => `"${option}"`).join(', ')}]`
)
})
}
const subSubVariantOption = (
subVariants as { [key: string]: string[] }[]
).find((subVariant) => subVariant[subVariantName] !== undefined)
if (!subSubVariantOption) {
return helper.message({
custom: Joi.expression(
`"${subVariantName}" is not a valid variant for "${spiritType} -> ${variantName} -> ${subVariantName}" spirit. Valid options are [${subVariantOptions.map((option) => `"${option}"`).join(', ')}]`
)
})
}
const subSubVariantOptions: string[] =
subSubVariantOption[subVariantName]
const providedSubSubVariant = Object.values(subVariant)[0]
if (typeof providedSubSubVariant !== 'string') {
return helper.message({
custom: Joi.expression(
`Provided variant is not valid for "${spiritType} -> ${variantName} -> ${subVariantName}" spirit. Valid options are [${subSubVariantOptions.map((option) => `"${option}"`).join(', ')}]`
)
})
}
if (!subSubVariantOptions.includes(providedSubSubVariant)) {
return helper.message({
custom: Joi.expression(
`"${providedSubSubVariant}" is not a valid variant for "${spiritType} -> ${variantName} -> ${subVariantName}" spirit. Valid options are [${subSubVariantOptions.map((option) => `"${option}"`).join(', ')}]`
)
})
}
}
return variant
}
)
),
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
)
.map((option) => `"${option}"`)
.join(', ')}]`
)
})
}
const spiritType: SpiritType = helper.state.ancestors[0].type
const spiritVariant: string | { [key: string]: unknown } =
helper.state.ancestors[0].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(', ')}]`
)
})
}
return characteristic
})
.required(),
ingredients: Joi.array()
.items(Joi.string().valid(...Object.values(Ingredient)))
.required(),
volume: volumeValidation(SpiritVolume),
alcohol: alcoholValidation,
vintage: vintageValidation,
RRPamount: RRPamountValidation,
RRPcurrency: RRPcurrencyValidation,
description: descriptionValidation,
url: urlValidation,
image: imageValidation
}).validate(data)

@ -1,24 +0,0 @@
import Joi from 'joi'
import { UserRole } from '../../types'
import { npubToHex, validateHex } from '../nostr'
export const userValidation = (data: unknown): Joi.ValidationResult =>
Joi.object({
name: Joi.string().required(),
npub: Joi.string()
.custom((value, helper) => {
const hex = npubToHex(value as string)
if (!hex || !validateHex(hex)) {
return helper.message({
custom: Joi.expression('"npub" contains an invalid value')
})
}
return hex
})
.required(),
role: Joi.string()
.valid(...Object.values(UserRole))
.required()
}).validate(data)

@ -1,43 +0,0 @@
import Joi from 'joi'
import { VintageOption } from '../../types'
export const vintageValidation = Joi.alternatives()
.try(
Joi.string().valid(...Object.values(VintageOption)),
Joi.number().min(1700).max(new Date().getFullYear())
)
.required()
export const productCodeEANvalidation = Joi.string().allow('').required()
export const productCodeUPCvalidation = Joi.string().allow('').required()
export const productCodeSKUvalidation = Joi.string().allow('').required()
export const countryValidation = Joi.string().length(2)
export const nameValidation = Joi.string().required()
export const typeValidation = (typeEnum: { [key: string]: string }) =>
Joi.string()
.valid(...Object.values(typeEnum))
.required()
export const producerIdValidation = Joi.string().length(24).required()
export const volumeValidation = (volumeEnum: { [key: string]: string }) =>
Joi.string()
.valid(...Object.values(volumeEnum))
.required()
export const alcoholValidation = Joi.number().min(0).max(0.99).required()
export const RRPamountValidation = Joi.number().required()
export const RRPcurrencyValidation = Joi.string().length(3).required()
export const descriptionValidation = Joi.string().required()
export const urlValidation = Joi.string()
export const imageValidation = Joi.string()
export const validateStringValue = (
value: unknown,
validOptions: { [key: string]: string }
) => typeof value !== 'string' || !Object.values(validOptions).includes(value)

@ -1,393 +0,0 @@
import Joi from 'joi'
import {
WineType,
BottleClosure,
Viticulture,
WineRegion,
WineVolume,
WineStyle,
WhiteWineCharacteristic,
AmberWineCharacteristic,
RoseWineCharacteristic,
RedWineCharacteristic,
GrapeVarietal
} from '../../types'
import { wineRegionsMap, isObject } from '../'
import {
vintageValidation,
productCodeEANvalidation,
productCodeUPCvalidation,
productCodeSKUvalidation,
countryValidation,
nameValidation,
typeValidation,
producerIdValidation,
volumeValidation,
alcoholValidation,
RRPamountValidation,
RRPcurrencyValidation,
descriptionValidation,
urlValidation,
imageValidation
} from './'
export const wineValidation = (data: unknown): Joi.ValidationResult =>
Joi.object({
productCodeEAN: productCodeEANvalidation,
productCodeUPC: productCodeUPCvalidation,
productCodeSKU: productCodeSKUvalidation,
country: countryValidation,
// TODO: improve types
region: Joi.alternatives()
.try(
Joi.string().custom((value, helper) => {
if (value) {
if (helper.state.ancestors) {
const { country } = helper.state.ancestors[0]
if (country) {
const regionMap = wineRegionsMap[country]
if (regionMap) {
const regions = Array.isArray(regionMap)
? regionMap
: Object.keys(regionMap)
if (!regions.includes(value)) {
return helper.message({
custom: Joi.expression(
`"region" contains an invalid value. Valid values for ${country} are [${regions.join(', ')}]`
)
})
}
}
}
}
}
return value
}),
Joi.object().custom(
(
value: {
[key: string]:
| string
| { [key: string]: string | { [key: string]: string } }
},
helper: Joi.CustomHelpers<unknown>
) => {
// return if no value
if (!value) {
return value
}
// return if no state ancestors
if (!helper.state.ancestors) {
return value
}
const country: string = helper.state.ancestors[0].country
// return if no country
if (!country) {
return value
}
const regionMap = wineRegionsMap[country]
// return if no region map
if (!regionMap) {
return value
}
/**
* Region
*/
// list of supported regions
const regions = Array.isArray(regionMap)
? regionMap
: Object.keys(regionMap)
const providedRegions = Object.keys(value)
// check if multiple regions provided
if (providedRegions.length !== 1) {
return helper.message({
custom: Joi.expression(
`"region" contains an invalid value. Valid values a single string or a region object`
)
})
}
const returnCustomMessage = (
map: string[],
options: string[]
): Joi.ErrorReport =>
helper.message({
custom: Joi.expression(
options.length
? `"region" contains an invalid value. Valid values for "${map.join(' -> ')}" are [${options.map((option) => `"${option}"`).join(', ')}]`
: `"region" contains an invalid value. "${map.join(' -> ')}" does not have nested options`
)
})
const providedRegion = providedRegions[0]
// check if provided region is in list of supported regions
if (!regions.includes(providedRegion)) {
return returnCustomMessage([country], regions)
}
/**
* Subregion
*/
// list of supported subregions
const subRegions: string[] = Array.isArray(
(regionMap as WineRegion)[providedRegion]
)
? ((regionMap as WineRegion)[providedRegion] as string[])
: Object.keys((regionMap as WineRegion)[providedRegion])
const providedSubRegion:
| string
| { [key: string]: string | { [key: string]: string } } =
value[providedRegion]
// return if provided subregion is not a string or an object
if (
typeof providedSubRegion !== 'string' &&
!isObject(providedSubRegion)
) {
return returnCustomMessage([country, providedRegion], subRegions)
}
// if providedSubRegion is a string, check if it is in the list of supported subregions
if (
typeof providedSubRegion === 'string' &&
!subRegions.includes(providedSubRegion)
) {
return returnCustomMessage([country, providedRegion], subRegions)
}
// if providedSubRegion is an object, check if it is in the list of supported subregions
else if (isObject(providedSubRegion)) {
const providedSubRegions = Object.keys(providedSubRegion)
const providedSubRegionName: string = providedSubRegions[0]
// return if provided multiple subregions or if provided subregion is not in the supported list
if (
providedSubRegions.length !== 1 ||
!subRegions.includes(providedSubRegionName)
) {
return returnCustomMessage(
[country, providedRegion],
subRegions
)
}
/**
* Village
*/
const villageMap = (
(regionMap as WineRegion)[providedRegion] as {
[key: string]: string | { [key: string]: string[] }
}
)[providedSubRegionName]
if (villageMap === undefined) {
return helper.message({
custom: Joi.expression(
`"region" contains an invalid value. "${[country, providedRegion, providedSubRegionName].join(' -> ')}" does not have villages"`
)
})
}
// list of supported villages
const villages: string[] = Array.isArray(villageMap)
? (villageMap as unknown as string[])
: Object.keys(villageMap)
const providedVillage: string | { [key: string]: string } = (
value[providedRegion] as {
[key: string]: string
}
)[providedSubRegionName]
// return if provided village is not a string or an object
if (
typeof providedVillage !== 'string' &&
!isObject(providedVillage)
) {
return returnCustomMessage(
[country, providedRegion, providedSubRegionName],
subRegions
)
}
// if village is a string, check if it is in the supported list
if (
typeof providedVillage === 'string' &&
!villages.includes(providedVillage)
) {
return returnCustomMessage(
[country, providedRegion, providedSubRegionName],
villages
)
}
// if providedVillage is an object, check if it is in the supported list
else if (isObject(providedVillage)) {
const providedVillages = Object.keys(providedVillage)
const providedVillageName: string = providedVillages[0]
// return if provided multiple villages or if provided village is not in the supported list
if (
providedVillages.length !== 1 ||
!villages.includes(providedVillageName)
) {
return returnCustomMessage(
[country, providedRegion, providedSubRegionName],
villages
)
}
/**
* Vineyard
*/
// list of supported vineyards
const vineyards: string[] = (
(regionMap as WineRegion)[providedRegion] as {
[key: string]: { [key: string]: string[] }
}
)[providedSubRegionName][providedVillageName]
if (!vineyards) {
return helper.message({
custom: Joi.expression(
`"region" contains an invalid value. "${[country, providedRegion, providedSubRegionName, providedVillageName].join(' -> ')}" does not have vineyards"`
)
})
}
const providedVineyard: string = (
value[providedRegion] as {
[key: string]: {
[key: string]: string
}
}
)[providedSubRegionName][providedVillageName]
// check if provided vineyard is in the supported list
if (!vineyards.includes(providedVineyard)) {
return returnCustomMessage(
[
country,
providedRegion,
providedSubRegionName,
providedVillageName
],
vineyards
)
}
}
}
return value
}
)
)
.allow('')
.required(),
name: nameValidation,
producerId: producerIdValidation,
type: typeValidation(WineType),
style: Joi.string()
.valid(...Object.values(WineStyle))
.required(),
characteristic: Joi.string().custom((value, 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
// return if no wineType
if (!wineType) {
return value
}
let options: string[] = []
switch (wineType) {
case WineType.White:
{
options = Object.values(WhiteWineCharacteristic)
}
break
case WineType.Amber:
{
options = Object.values(AmberWineCharacteristic)
}
break
case WineType.Rose:
{
options = Object.values(RoseWineCharacteristic)
}
break
case WineType.Red:
{
options = Object.values(RedWineCharacteristic)
}
break
default:
break
}
if (!options.length) {
return helper.message({
custom: Joi.expression(
`no characteristics found for provided type of wine`
)
})
}
if (!options.includes(value)) {
return helper.message({
custom: Joi.expression(
`"${value}" is not a valid characteristic for "${wineType}" wine. Valid options are [${options.map((option) => `"${option}"`).join(', ')}]`
)
})
}
return value
}),
volume: volumeValidation(WineVolume),
alcohol: alcoholValidation,
grapeVarietal: Joi.array()
.items(Joi.string().valid(...Object.values(GrapeVarietal)))
.required(),
vintage: vintageValidation,
viticulture: Joi.string()
.valid(...Object.values(Viticulture))
.required(),
sulfites: Joi.number().min(0).max(400),
filtered: Joi.boolean().required(),
vegan: Joi.boolean().required(),
kosher: Joi.boolean().required(),
closure: Joi.string()
.valid(...Object.values(BottleClosure))
.required(),
RRPamount: RRPamountValidation,
RRPcurrency: RRPcurrencyValidation,
description: descriptionValidation,
url: urlValidation,
image: imageValidation
}).validate(data)

@ -1,689 +0,0 @@
import { WineRegion } from '../types'
export const wineRegionsMap: { [key: string]: string[] | WineRegion } = {
IT: {
Piedmont: {
Langhe: {
Barolo: ['La Morra', 'Castiglione Falletto', `Serralunga d'Alba`],
Barbaresco: ['Neive', 'Alba', 'Treiso']
},
Monferrato: ['Gavi', 'Asti', 'Canelli']
},
Tuscany: [
'Chianti',
'Montalcino',
'Montepulciano',
'Scansano',
'Carmignano'
],
Veneto: ['Valpolicella', 'Soave'],
'Friuli-Venezia Giulia': [
'Collio',
'Colli Orientali del Friuli',
'Friuli Grave del Friuli'
],
'Trentino-Alto Adige': ['Trentino', 'Alto Adige'],
Lombardy: [],
Franciacorta: [],
Valtellina: [],
'Emilia-Romagna': ['Lambrusco'],
Umbria: ['Orvieto', 'Torgiano'],
Lazio: ['Frascati'],
Abruzzo: [],
Molise: ['Biferno', 'Pentro'],
Campania: ['Taurasi', 'Aglianico del Vulture', 'Fiano di Avellino'],
Puglia: ['Primitivo di Manduria', 'Salice Salentino'],
Basilicata: [],
Calabria: ['Cirò', 'Greco di Bianco'],
Sicily: ['Etna', 'Marsala', 'Pantelleria'],
Sardinia: ['Vermentino di Gallura', 'Cannonau di Sardegna']
},
FR: {
Alsace: [],
Ardeche: [],
Aquitaine: [],
Bordeaux: {
Médoc: ['Pauillac', 'Margaux', 'Saint-Estèphe', 'Saint-Julien'],
'Saint-Émilion': [],
Pomerol: [],
Graves: ['Pessac-Léognan'],
Sauternes: ['Barsac']
},
Burgundy: {
Chablis: [],
'Côte de Nuits': [
'Gevrey-Chambertin',
'Vosne-Romanée',
'Nuits-Saint-Georges'
],
'Côte de Beaune': ['Meursault', 'Puligny-Montrachet', 'Beaune'],
'Côte Chalonnaise': ['Mercurey'],
Mâconnais: ['Pouilly-Fuissé'],
Beaujolais: ['Morgon', 'Fleurie', 'Moulin a Vent']
},
Champagne: ['Montagne de Reims', 'Vallée de la Marne', 'Côte des Blancs'],
'Loire Valley': [
'Sancerre',
'Pouilly-Fumé',
'Vouvray',
'Muscadet',
'Anjou',
'Saumur',
'Chinon'
],
'Rhône Valley': [
'Côte-Rôtie',
'Hermitage',
'Condrieu',
'Saint-Joseph',
'Châteauneuf-du-Pape',
'Côtes du Rhône',
'Gigondas',
'Vacqueyras'
],
Provence: ['Côtes de Provence', 'Bandol'],
'Languedoc-Roussillon': [],
'Southwest France': ['Cahors', 'Madiran', 'Jurançon'],
Jura: ['Arbois', 'Côtes du Jura'],
Savoie: [],
Corsica: ['Patrimonio']
},
DE: {
Mosel: ['Saar', 'Ruwer'],
Pfalz: [],
Rheinhessen: [],
Nahe: [],
Rheingau: [],
'Hessische Bergstraße': [],
Franken: [],
Baden: [],
Württemberg: [],
'Saale-Unstrut': [],
Sachsen: [],
Ahr: []
},
CH: {
Vaud: ['Lavaux', 'Vevey', 'Morges', 'Nyon'],
Valais: ['Sierre', 'Sion', 'Martigny'],
Geneva: ['Lancy', 'Carouge'],
Ticino: ['Locarno', 'Bellinzona', 'Ascona'],
Neuchâtel: ['Val-de-Travers', 'Val-de-Ruz'],
Fribourg: ['Glâne', 'Sarine'],
Bern: ['Thun', 'Biel'],
Aargau: ['Baden', 'Brugg'],
Thurgau: ['Frauenfeld', 'Arbon'],
Zürich: ['Affoltern', 'Dietikon']
},
ES: {
Rioja: ['Rioja Alta', 'Rioja Alavesa', 'Rioja Oriental'],
Navarra: [],
'Rías Baixas': [],
'Ribeira Sacra': [],
Valdeorras: [],
Txakoli: ['Getariako Txakolina', 'Bizkaiko Txakolina', 'Arabako Txakolina'],
'Ribera del Duero': [],
Rueda: [],
Toro: [],
Cigales: [],
'La Mancha': [],
Valdepeñas: [],
Priorat: [],
Penedès: [],
Cava: [],
Montsant: [],
'Conca de Barberà': [],
'Jerez-Xérès': [],
'Montilla-Moriles': [],
Málaga: [],
'Sierras de Málaga': [],
'Condado de Huelva': [],
'Utiel-Requena': [],
Valencia: [],
Jumilla: [],
Yecla: [],
Bullas: [],
'Canary Islands': [],
'Balearic Islands': ['Binissalem', 'Pla i Llevant'],
Bierzo: [],
'Campo de Borja': [],
Aragón: ['Calatayud', 'Somontano'],
'Ribera del Guadiana': []
},
PT: {
Douro: ['Baixo Corgo', 'Cima Corgo', 'Douro Superior'],
Alentejo: ['Central', 'Litoral', 'Interior Norte', 'Interior Sul'],
Porto: ['Gaia'],
'Vinho Verde': [
'Amarante',
'Ave',
'Baião',
'Basto',
'Cávado',
'Lima',
'Lima Interior',
'Lima Litoral',
'Monção',
'Paiva',
'Sousa'
],
Tejo: [
'Alcobaça',
'Arruda dos Vinhos',
'Azeiteira',
'Benavente e Santarém',
'Bico do Cachorro',
'Borba',
'Casaínhos',
'Chamusca',
'Colares',
'Coruche',
'Fátima',
'Figueira da Foz',
'Golegã',
'Gualtar',
'Gândara',
'Mação',
'Mafra',
'Montemor-o-Novo',
'Montijo',
'Odivelas',
'Palmela',
'Pataias',
'Peniche',
'Porto de Mós',
'Reguengos de Monsaraz',
'Ribatejo',
'Sabugal',
'Setúbal',
'Tomar',
'Torres Vedras',
'Vila Franca de Xira'
],
Lisbon: [
'Alenquer',
'Arruda',
'Carcavelos e Cascais',
'Colares',
'Encoro',
'Estremoz',
'Oeiras',
'Palmela',
'Setúbal'
],
Madeira: [
'Machico',
'Santa Cruz',
'Caniço',
'Santo António da Serra',
'Ponta do Sol',
'Calheta',
'Funchal',
'Câmara de Lobos',
'Ribeira Brava',
'Paul do Mar',
'Canhas',
'Arco da Calheta',
'São Vicente',
'Porto Moniz',
'Paul da Serra'
]
},
AT: {
Wagram: ['Marchfeld'],
Kremstal: ['Krems', 'Spitz'],
Kamptal: ['Langenlois', 'Gumpoldskirchen'],
Wachau: [],
Thermenregion: ['Baden', 'Mödling'],
Wienerwald: [],
Südsteiermark: ['Grazer Bergland', 'Leibnitz', 'Deutschlandsberg'],
Burgenland: ['Neusiedlersee', 'Eisenberg', 'Rosalia'],
Styria: ['Südsteiermark', 'Weststeiermark'],
Salzburg: ['Flachgau'],
Tyrol: ['Innsbruck', 'Hall'],
Carinthia: ['Villach', 'Klagenfurt']
},
HU: {
Tokaj: [],
Eger: [],
Badacsony: [],
Balaton: [
'Balatonalmádi',
'Balatonboglár',
'Balatonfüred-Csopak',
'Balatonlelle',
'Balatonfüred',
'Somló'
],
Mátra: [],
Hegyalja: [],
Pannon: ['Szekszárd', 'Pécs', 'Baja', 'Sopron'],
Villány: []
},
CZ: {
'South Moravia': ['Znojmo', 'Břeclav', 'Hodonín', 'Kyjov', 'Vranov'],
'Zlín Region': ['Valašské Meziříčí', 'Kroměříž', 'Vsetín'],
'Olomouc Region': ['Prostějov', 'Olomouc', 'Litomyšl'],
'South Bohemia': ['České Budějovice', 'Písek', 'Strakonice'],
Plzeň: ['Rokycany', 'Domažlice']
},
MD: ['Iași', 'Botoșani', 'Vaslui', 'Neamț'],
RO: {
Transylvania: ['Cluj', 'Sibiu', 'Brașov', 'Mureș'],
Muntenia: ['Buzău', 'Dâmbovița', 'Prahova', 'Vrancea'],
Oltenia: ['Dolj', 'Gorj', 'Vâlcea', 'Mehedinți'],
Banat: ['Arad', 'Timiș', 'Caraș-Severin'],
Dobrogea: ['Constanța', 'Tulcea'],
Crisana: ['Bihor'],
Maramureș: ['Suceava', 'Maramureș']
},
GR: {
Attica: [],
'Central Greece': [
'Euboea',
'Boeotia',
'Phocis',
'Locris',
'Phthiotis',
'Phocis',
'Locris',
'Phthiotis'
],
Peloponnese: [
'Nemea',
'Mantineia',
'Achaia',
'Messenia',
'Laconia',
'Arcadia',
'Argolis',
'Corinthia'
],
Thrace: [],
Macedonia: [
'Thessaloniki',
'Kilkis',
'Pella',
'Florina',
'Kastoria',
'Imathia',
'Pella',
'Florina',
'Kastoria'
],
Thessaly: ['Trikala', 'Larissa', 'Magnesia', 'Karditsa', 'Trikala'],
'Ionian Islands': [
'Corfu',
'Zakynthos',
'Kefalonia',
'Lefkada',
'Ithaca',
'Paxos',
'Corfu',
'Zakynthos',
'Kefalonia',
'Lefkada',
'Ithaca',
'Paxos'
],
Cyclades: [
'Santorini',
'Paros',
'Naxos',
'Mykonos',
'Sifnos',
'Tinos',
'Santorini',
'Paros',
'Naxos',
'Mykonos',
'Sifnos',
'Tinos'
],
Crete: [],
'Aegean Islands': [
'Samos',
'Lesbos',
'Chios',
'Limnos',
'Ikaria',
'Samos',
'Lesbos',
'Chios',
'Limnos',
'Ikaria'
]
},
HR: {
Dalmatia: ['Hvar', 'Brac', 'Korcula', 'Peljesac', 'Dubrovnik-Riviera'],
Istria: [],
Slavonia: [],
Kvarner: ['Kvarner Bay', 'Labin'],
'Continental Croatia': ['Srijem', 'Moslavina', 'Podravina'],
'Central Croatia': ['Zagorje', 'Medgurje', 'Bilogora']
},
GE: {
Kakheti: ['Telavi', 'Sighnaghi', 'Akhasheni', 'Kvareli'],
Imereti: ['Kutaisi', 'Khoni', 'Tsqaltubo', 'Sachkhere'],
'Racha-Lechkhumi': [],
'Kvemo Svaneti': [],
Kartli: ['Tbilisi', 'Gori', 'Kaspi', 'Rustavi'],
Adjara: ['Batumi', 'Kobuleti'],
'Samegrelo-Zemo Svaneti': ['Poti', 'Zugdidi'],
Abkhazia: ['Gagra', 'Gudauta', 'Ochamchire'],
'South Georgia': ['Adigeni', 'Akhalkalaki', 'Bolnisi', 'Ninotsminda']
},
IL: {
Galilee: ['Golan Heights', 'Carmel Mountains'],
'Central District': ['Shomron', 'Shfela'],
Negev: ['Southern Negev', 'Central Negev'],
'Coastal Plain': ['Central Coastal Plain', 'Southern Coastal Plain']
},
LB: ['Bekaa Valley', 'Mount Lebanon', 'North Lebanon', 'South Lebanon'],
TR: {
Anatolia: [
'Konya',
'Aksaray',
'Nevşehir',
'Erzurum',
'Van',
'Bitlis',
'İzmir',
'Manisa',
'Ayvalik'
],
'Aegean Region': ['Urla', 'Foça', 'Bornova'],
Ayvalik: [],
'Marmara Region': ['İstanbul', 'Edirne'],
'Black Sea Region': ['Rize', 'Artvin', 'Trabzon'],
'Southeastern Anatolia': ['Gaziantep', 'Şanliurfa', 'Adana']
},
AM: ['Vayots Dzor', 'Ararat Valley', 'Gegharkunik', 'Tavush', 'Syunik'],
CA: {
Ontario: [
'Niagara Peninsula',
'Lake Erie North Shore',
'Pelee Island',
'Prince Edward County'
],
'British Columbia': [
'Okanagan Valley',
'Similkameen Valley',
'Fraser Valley',
'Southern Gulf Islands'
],
Quebec: ['Montreal', 'Eastern Townships', 'Charlevoix'],
'Nova Scotia': ['Annapolis Valley', 'Gaspereau Valley', 'North Shore'],
'New Brunswick': ['Annapolis Valley', 'Gaspereau Valley'],
'Prince Edward Island': ['Eastern PEI', 'Western PEI']
},
US: {
California: [
'Napa Valley',
'Sonoma Valley',
'Paso Robles',
'Santa Barbara County',
'Central Coast',
'Lodi',
'Mendocino County',
'Santa Cruz Mountains',
'Russian River Valley'
],
Oregon: [
'Willamette Valley',
'Rogue Valley',
'Umpqua Valley',
'Southern Oregon',
'Oregon Coast'
],
Washington: [
'Columbia Valley',
'Walla Walla Valley',
'Red Mountain',
'Snake River'
],
'New York': [
'Finger Lakes',
'Long Island',
'Hudson River',
'Western New York'
],
Virginia: [
'Monticello',
'Shenandoah Valley',
'Northern Virginia',
'Virginia Piedmont'
],
'Texas Hill Country': [],
'Arizona Sonoita': [],
'Colorado Grand Valley': [],
'Idaho Snake River Valley': [],
Michigan: ['Michigan Lake', 'Michigan Shore']
},
AR: {
Mendoza: ['Luján de Cuyo', 'Uco Valley', 'Maipú', 'San Rafael'],
Salta: ['Calchaquí Valleys', 'Quebrada de Humahuaca'],
'San Juan': ['Ullum', 'Calingasta', 'Tulum'],
'La Rioja': [],
Catamarca: ['Andalgalá', 'Belén'],
'San Luis': [
'Valle del Conlara',
'Valle del Potrerillos',
'Valle de las Carreras'
],
'Buenos Aires': [
'Partido de General Belgrano',
'Partido de Lobos',
'Partido de Tandil'
],
Neuquén: ['Valle de Río Negro', 'Valle de Agrelo']
},
CL: {
'Central Valley': [
'Maipo Valley',
'Casablanca Valley',
'San Antonio Valley',
'Maule Valley',
'Colchagua Valley'
],
'Coastal Regions': [
'Valle de Aconcagua',
'Valle de Limarí',
'Valle de Elqui'
],
'Southern Chile': ['Valle de Itata', 'Valle de Bío-Bío'],
'Northern Chile': ['Valle de Elqui']
},
ZA: {
'Western Cape': {
Stellenbosch: [
'Simonsberg-Stellenbosch',
'Jonkershoek-Stellenbosch',
'Stellenbosch Mountain'
],
Paarl: ['Franschhoek', 'Paarl Mountain'],
Constantia: ['Constantia Valley'],
Durbanville: ['Durbanville Hills'],
'Walker Bay': ['Hemel-en-Aarde'],
Swartland: ['Paardeberg']
},
'Eastern Cape': ['Jeffreys Bay'],
'Northern Cape': ['Augrabies']
},
AU: {
'South Eastern Australia': [],
'New South Wales': [
'Hunter Valley',
'Riverina',
'Mudgee',
'Orange',
'Cowra',
'Hilltops',
'Tumbarumba',
'Gundagai',
'Shoalhaven Coast',
'Southern Highlands',
'New England'
],
'South Australia': [
'Barossa Valley',
'McLaren Vale',
'Clare Valley',
'Coonawarra',
'Adelaide Hills',
'Eden Valley',
'Langhorne Creek',
'Padthaway',
'Kangaroo Island',
'Mount Gambier',
'Robe',
'Wrattonbully',
'Fleurieu Peninsula',
'Currency Creek',
'Southern Fleurieu'
],
Victoria: [
'Yarra Valley',
'Mornington Peninsula',
'Geelong',
'Bellarine Peninsula',
'Sunbury',
'Macedon Ranges',
'Heathcote',
'Bendigo',
'Pyrenees',
'Grampians',
'Great Western',
'Henty',
'Otway Ranges',
'King Valley',
'Alpine Valleys',
'Rutherglen',
'Glenrowan',
'Goulburn Valley',
'Strathbogie Ranges'
],
'Western Australia': [
'Margaret River',
'Great Southern',
'Swan Valley',
'Pemberton',
'Manjimup',
'Blackwood Valley',
'Geographe',
'Peel',
'Perth Hills',
'Frankland River',
'Mount Barker',
'Porongurup',
'Denmark',
'Albany',
'Esperance'
],
Tasmania: [
'Tamar Valley',
'Pipers River',
'East Coast',
'Coal Valley',
'Derwent Valley',
'Huon Valley'
],
Queensland: [
'Granite Belt',
'South Burnett',
'Darling Downs',
'Scenic Rim'
],
'Canberra District': [
'Canberra',
'Murrumbateman',
'Yass Valley',
'Gundaroo',
'Lake George'
]
},
NZ: {
'North Island': [
'Auckland',
'Gisborne',
`Hawke's Bay`,
'Wairarapa',
'Bay of Islands',
'Coromandel',
'Waikato'
],
'South Island': [
'Marlborough',
'Nelson',
'Waipara',
'Canterbury',
'Central Otago',
'Waipara Valley',
'North Canterbury',
'North Otago'
]
},
CN: {
Ningxia: {
Yinchuan: [],
'Helan Mountain': ['Qingtongxia', 'Wucaiwan', 'Qingshuihe', 'Jinfeng']
},
Shaanxi: ['Lishan', 'Fengxiang'],
Shandong: ['Yantai', 'Penglai', 'Weihai'],
Xinjiang: ['Turpan', 'Yili', 'Jinghe'],
Hebei: ['Changli', 'Qinhuangdao'],
Heilongjiang: ['Yichun', 'Qiqihar'],
Jilin: ['Baishan', 'Changchun'],
Liaoning: ['Dalian', 'Shenyang'],
'Inner Mongolia': ['Hohhot', 'Baotou'],
Guangxi: ['Nanning', 'Liuzhou'],
Hunan: ['Changsha', 'Yongzhou'],
Hubei: ['Wuhan'],
Sichuan: ['Chengdu', `Ya'an`],
Guizhou: ['Guiyang', 'Zunyi'],
Yunnan: ['Kunming', 'Dali']
},
JP: {
'Yamanashi Prefecture': ['Kofu Basin', 'Katsunuma', 'Fuefuki'],
'Hokkaido Prefecture': ['Biei', 'Tomakomai', 'Nanae'],
'Niigata Prefecture': ['Uonuma', 'Sannosawa'],
'Nagano Prefecture': ['Kiso Valley', 'Nagano'],
'Gifu Prefecture': ['Gujo', 'Mino'],
'Aichi Prefecture': ['Nishio', 'Seto'],
'Shizuoka Prefecture': ['Suruga', 'Shizuoka'],
'Kanagawa Prefecture': ['Yokohama', 'Kawasaki'],
'Chiba Prefecture': ['Noda', 'Chiba'],
'Ibaraki Prefecture': ['Tsukuba', 'Hitachi'],
'Tochigi Prefecture': ['Utsunomiya', 'Tochigi'],
'Gunma Prefecture': ['Maebashi', 'Gunma']
},
UK: {
Kent: ['Ashford', 'Canterbury', 'Dover', 'Maidstone', 'Sevenoaks'],
Sussex: ['Brighton', 'Eastbourne', 'Hastings', 'Chichester', 'Horsham'],
Hampshire: ['Winchester', 'Petersfield', 'Basingstoke', 'Andover'],
Wiltshire: ['Swindon', 'Chippenham', 'Salisbury', 'Trowbridge'],
Dorset: ['Poole', 'Bournemouth', 'Dorset'],
Devon: ['Exeter', 'Newton Abbot', 'Plymouth', 'Torquay'],
Somerset: ['Bath', 'Weston-super-Mare', 'Taunton', 'Yeovil'],
Gloucestershire: ['Cheltenham', 'Gloucester', 'Cirencester', 'Stroud'],
Worcestershire: ['Worcester', 'Malvern', 'Kidderminster', 'Bewdley'],
Warwickshire: [
'Warwick',
'Stratford-upon-Avon',
'Leamington Spa',
'Kenilworth'
],
Herefordshire: ['Hereford', 'Leominster', 'Ross-on-Wye', 'Kington'],
Shropshire: ['Shrewsbury', 'Telford', 'Ludlow', 'Oswestry']
},
UY: {
'Canelones Department': ['San Carlos', 'Melo', 'Paso de los Toros'],
'Montevideo Department': ['Carrasco', 'Punta del Este'],
'Colonia Department': ['Colonia del Sacramento', 'Villa Soriano'],
'San José Department': ['Progreso', 'Treinta y Tres'],
'Rocha Department': ['La Paloma', 'Rocha Sur'],
'Florida Department': ['Florida', 'Sarandí del Yí'],
'Soriano Department': ['Soriano', 'Río Negro'],
'Tacuarembó Department': ['Tacuarembó', 'Paso de los Toros'],
'Treinta y Tres Department': ['Treinta y Tres', 'Rivera']
}
}