Compare commits
No commits in common. "staging" and "schemas" have entirely different histories.
.git-hooks
.gitea/workflows
.vscode
package-lock.jsonpackage.jsonsrc
@ -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
|
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@ -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
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",
|
||||
|
16
src/index.ts
16
src/index.ts
@ -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']
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user