Compare commits

..

No commits in common. "staging" and "deps-fix" have entirely different histories.

69 changed files with 48 additions and 7082 deletions

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

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

66
.vscode/settings.json vendored

@ -1,66 +0,0 @@
{
"cSpell.words": [
"Abuelo",
"amarela",
"Amaro",
"Amarula",
"Añejo",
"Arabica",
"Baijiu",
"biodynamic",
"Blanco",
"Cachaça",
"Caramelised",
"Caturra",
"colour",
"Colours",
"Coren",
"EAN",
"espadín",
"Genever",
"Jägermeister",
"Jaune",
"Joven",
"Kimoto",
"koji",
"Kona",
"Kouillou",
"Licor",
"Maragogype",
"Metaxa",
"Mirabelle",
"Nganda",
"Nostr",
"npub",
"ouro",
"Pacamara",
"Pacas",
"Pêche",
"Pisco",
"Platino",
"Poire",
"Pomme",
"Rakia",
"Reposado",
"Reserva",
"Robusta",
"RRP",
"schnorr",
"screwcap",
"SKU",
"Soju",
"Sokujō",
"Solera",
"sundried",
"tepeztate",
"Tequilana",
"tobalá",
"Typica",
"Umami",
"UPC",
"Verte",
"VSOP",
"Yamahai",
"Yuzu"
]
}

285
package-lock.json generated

@ -7,18 +7,11 @@
"": {
"name": "api",
"version": "1.0.0",
"hasInstallScript": true,
"license": "ISC",
"dependencies": {
"@noble/curves": "^1.9.0",
"crypto-js": "^4.2.0",
"currency-codes-ts": "^3.0.0",
"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",
@ -28,9 +21,8 @@
"@semantic-release/git": "^10.0.1",
"@semantic-release/npm": "^12.0.1",
"@semantic-release/release-notes-generator": "^14.0.3",
"@types/crypto-js": "^4.2.2",
"@types/express": "^5.0.1",
"@types/node": "^22.14.1",
"@types/node": "^22.13.12",
"concurrently": "^9.1.2",
"eslint": "^9.23.0",
"globals": "^16.0.0",
@ -307,21 +299,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",
@ -425,63 +402,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.9.0",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.0.tgz",
"integrity": "sha512-7YDlXiNMdO1YZeH6t/kvopHHbIZzlxrCV9WLqCY6QhcXOoXiNCMDqJIglZ9Yjx5+w7Dz30TITFrlTjnRg7sKEg==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "1.8.0"
},
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/curves/node_modules/@noble/hashes": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
"license": "MIT",
"engines": {
"node": "^14.21.3 || >=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/@noble/secp256k1": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-2.2.3.tgz",
"integrity": "sha512-l7r5oEQym9Us7EAigzg30/PQAvynhMt2uoYtT3t26eGDVm9Yii5mZ5jWSWmZ/oSIR2Et0xfc6DXrG0bZ787V3w==",
"license": "MIT",
"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",
@ -780,57 +700,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",
@ -1564,27 +1433,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",
@ -1686,13 +1534,6 @@
"@types/node": "*"
}
},
"node_modules/@types/crypto-js": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz",
"integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
@ -1782,13 +1623,13 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "22.14.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz",
"integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==",
"version": "22.13.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.12.tgz",
"integrity": "sha512-ixiWrCSRi33uqBMRuICcKECW7rtgY43TbsHDpM2XK7lXispd48opW+0IXrBVxv9NMhaz/Ue9kyj6r3NTVyXm8A==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
"undici-types": "~6.20.0"
}
},
"node_modules/@types/normalize-package-data": {
@ -3101,12 +2942,6 @@
"node": ">= 8"
}
},
"node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
"license": "MIT"
},
"node_modules/crypto-random-string": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz",
@ -3136,18 +2971,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/currency-codes-ts": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/currency-codes-ts/-/currency-codes-ts-3.0.0.tgz",
"integrity": "sha512-ZJeCpq5uY2t8dDl4xdF15shkp5o8jrHcD4lHftK/O8j8xTHlXg0E5YhpZbRJvnLRaKe+JQh1/q1AI9Wc2Dl3Nw==",
"license": "MIT",
"dependencies": {
"lodash-es": "^4.17.21"
},
"engines": {
"node": ">=14"
}
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@ -3248,12 +3071,6 @@
"wrappy": "1"
}
},
"node_modules/diacritics": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz",
"integrity": "sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==",
"license": "MIT"
},
"node_modules/diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
@ -4736,18 +4553,6 @@
"node": ">=18.18.0"
}
},
"node_modules/i18n-iso-countries": {
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.14.0.tgz",
"integrity": "sha512-nXHJZYtNrfsi1UQbyRqm3Gou431elgLjKl//CYlnBGt5aTWdRPH1PiS2T/p/n8Q8LnqYqzQJik3Q7mkwvLokeg==",
"license": "MIT",
"dependencies": {
"diacritics": "1.3.0"
},
"engines": {
"node": ">= 12"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -5110,19 +4915,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",
@ -5690,6 +5482,7 @@
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"dev": true,
"license": "MIT"
},
"node_modules/lodash.capitalize": {
@ -6438,62 +6231,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-tools/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/nostr-tools/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/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",
@ -11509,7 +11246,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",
@ -11564,9 +11301,9 @@
"license": "MIT"
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
"dev": true,
"license": "MIT"
},

@ -15,9 +15,7 @@
"lint": "eslint . --ext ts --report-unused-disable-directives --max-warnings 0",
"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"
"lint-staged": "lint-staged"
},
"repository": {
"type": "git",
@ -27,15 +25,9 @@
"license": "ISC",
"description": "Cellar Social API",
"dependencies": {
"@noble/curves": "^1.9.0",
"crypto-js": "^4.2.0",
"currency-codes-ts": "^3.0.0",
"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",
@ -45,9 +37,8 @@
"@semantic-release/git": "^10.0.1",
"@semantic-release/npm": "^12.0.1",
"@semantic-release/release-notes-generator": "^14.0.3",
"@types/crypto-js": "^4.2.2",
"@types/express": "^5.0.1",
"@types/node": "^22.14.1",
"@types/node": "^22.13.12",
"concurrently": "^9.1.2",
"eslint": "^9.23.0",
"globals": "^16.0.0",

@ -1,17 +1,7 @@
import express, { Express } from 'express'
import dotenv from 'dotenv'
import { connectToDatabase } from './services/database.service'
import {
usersRouter,
nostrRouter,
reviewsRouter,
winesRouter,
sakeRouter,
spiritsRouter,
coffeeRouter
} from './routes'
import { Route } from './types'
import { authorizeRequest } from './middlewares'
import { usersRouter } from './routes/users.router'
dotenv.config()
@ -20,15 +10,7 @@ const port = process.env.PORT || 3000
connectToDatabase()
.then(() => {
app.use(authorizeRequest)
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('/users', usersRouter)
app.listen(port, () => {
console.log(`Server started at http://localhost:${port}`)

@ -1,71 +0,0 @@
import { RequestHandler } from 'express'
import { NostrEvent } from 'nostr-tools'
import { decodeBase64 } from '../utils/coding'
import { verifyNostrSignature } from '../utils'
import { collections } from '../services/database.service'
import { DBcollection } from '../types'
import { User } from '../models'
export const authorizeRequest: RequestHandler = async (req, res, next) => {
const { authorization } = req.headers
if (!authorization) {
res.sendStatus(401)
} else {
const removeNostrPrefix = (str: string) => str.replace('Nostr ', '')
const decodedString = decodeBase64(removeNostrPrefix(authorization))
try {
const event: NostrEvent = JSON.parse(decodedString)
// verify Nostr signature
const verified = await verifyNostrSignature(event)
if (!verified) {
throw new Error('Nostr signature is not valid.')
}
const { pubkey } = event
const { locals } = res
const checkIfUserExists = async () => {
// collection of {url}:{method} strings that represent routes that do not require user existence check
const skipUserExistCheck = ['/users:POST']
const { url, method } = req
if (!skipUserExistCheck.includes(`${url}:${method}`)) {
const existingUser = await collections[
DBcollection.Users
]?.findOne<User>({
npub: pubkey
})
if (!existingUser) {
throw new Error('User does not exist.')
}
locals.userId = existingUser._id
locals.userRole = existingUser.role
}
}
await checkIfUserExists()
locals.npub = pubkey
next()
// TODO:
// 0. verify kind
// 1. verify tags
// 2. verify content
// 3. verify that event is not older than 5mins
} catch (error) {
console.error(`Error while processing Authorization header.`, error)
res.sendStatus(401)
}
}
}

@ -1 +0,0 @@
export * from './authorize'

@ -1,26 +0,0 @@
import { ObjectId } from 'mongodb'
import { CoffeeProcessingType, CoffeeVariety, CoffeeRoast } from '../types'
import { Alpha2Code } from 'i18n-iso-countries'
import { CurrencyCode } from 'currency-codes-ts/dist/types'
export class Coffee {
constructor(
public productCodeEAN: string, // Article Number (https://en.wikipedia.org/wiki/International_Article_Number)
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 country: Alpha2Code, // two-letter country codes defined in ISO 3166-1 (https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)
public region: string, // appellation, village, sub-region, vineyard
public origin: string, // origin
public name: string, // label
public producerId: ObjectId, // product producer
public variety: CoffeeVariety, // variety type and kind
public processingType: CoffeeProcessingType, // processing type
public roast: CoffeeRoast, // roast level
public RRPamount: number, // 20
public RRPcurrency: CurrencyCode, // USD
public description: string, // detailed description of the product
public url?: string, // e.g. producer's website
public image?: string, // (optional image URL)cellar.social
public id?: ObjectId // database object id
) {}
}

@ -1,7 +0,0 @@
export * from './user'
export * from './nostrEvent'
export * from './review'
export * from './wine'
export * from './sake'
export * from './spirit'
export * from './coffee'

@ -1,12 +0,0 @@
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
) {}
}

@ -1,17 +0,0 @@
import { ObjectId } from 'mongodb'
import { ProductType, RatingOption, TastingNote } from '../types'
export class Review {
constructor(
public nostrId: string, // unique identifier for the Nostr Event
public nostrEventId: ObjectId, // unique identifier for the NostrEvent DB instance
public productId: ObjectId, // unique identifier for the Product DB instance
public reviewerId: ObjectId, // unique identifier for the User DB instance
public productType: ProductType, // product type
public rating: number | RatingOption, // numerical rating, e.g., 84-100 or NS (no score)
public reviewText: string, // text content of the review
public tastingNote: TastingNote, // an object representing tasting notes
public _id?: ObjectId, // database object id
public id?: string // string representing database object id
) {}
}

@ -1,44 +0,0 @@
import { ObjectId } from 'mongodb'
import {
SakeDesignation,
SakeStarter,
VintageOption,
SakeCharacteristic,
StandardDrink,
SakeVolume,
RiceVarietal,
SakeYeastStrain,
SakeKoji
} from '../types'
import { Alpha2Code } from 'i18n-iso-countries'
import { CurrencyCode } from 'currency-codes-ts/dist/types'
export class Sake {
constructor(
public productCodeEAN: string, // Article Number (https://en.wikipedia.org/wiki/International_Article_Number)
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 country: Alpha2Code, // two-letter country codes defined in ISO 3166-1 (https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)
public region: string, // appellation, village, sub-region, vineyard
public name: string, // label
public producerId: ObjectId, // product producer
public designation: SakeDesignation, // table, pure, blended
public polishRate: number, // %
public characteristic: SakeCharacteristic,
public starter: SakeStarter, // sake starter
public yeastStrain: SakeYeastStrain,
public volume: SakeVolume, // bottle volume
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 RRPamount: number, // 20
public RRPcurrency: CurrencyCode, // USD
public description: string, // detailed description of the product
public url?: string, // e.g. producer's website
public images?: string[], // (optional image URL)cellar.social
public _id?: ObjectId, // database object id
public id?: string // string representing database object id
) {}
}

@ -1,39 +0,0 @@
import { ObjectId } from 'mongodb'
import {
SpiritType,
SpiritVariant,
Ingredient,
VintageOption,
StandardDrink,
SpiritVolume,
SpiritCharacteristic
} from '../types'
import { Alpha2Code } from 'i18n-iso-countries'
import { CurrencyCode } from 'currency-codes-ts/dist/types'
export class Spirit {
constructor(
public productCodeEAN: string, // Article Number (https://en.wikipedia.org/wiki/International_Article_Number)
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 country: Alpha2Code, // two-letter country codes defined in ISO 3166-1 (https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)
public region: string, // appellation, village, sub-region, vineyard
public name: string, // label
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 RRPamount: number, // 20
public RRPcurrency: CurrencyCode, // USD
public description: string, // detailed description of the product
public url?: string, // e.g. producer's website
public images?: string[], // (optional image URL)cellar.social
public _id?: ObjectId, // database object id
public id?: string // string representing database object id
) {}
}

@ -1,11 +1,8 @@
import { ObjectId } from 'mongodb'
import { UserRole } from '../types'
export class User {
export default class User {
constructor(
public name: string, // name, changeable
public npub: string, // npub
public role: UserRole, // user role (user, reviewer, producer)
public _id: ObjectId // database object id
public name: string,
public id?: ObjectId
) {}
}

@ -1,56 +0,0 @@
import { ObjectId } from 'mongodb'
import {
WineType,
Viticulture,
BottleClosure,
VintageOption,
StandardDrink,
WineRegion,
WineVolume,
WineStyle,
WhiteWineCharacteristic,
AmberWineCharacteristic,
RoseWineCharacteristic,
RedWineCharacteristic,
GrapeVarietal
} from '../types'
import { Alpha2Code } from 'i18n-iso-countries'
import { CurrencyCode } from 'currency-codes-ts/dist/types'
export class Wine {
constructor(
public productCodeEAN: string, // Article Number (https://en.wikipedia.org/wiki/International_Article_Number)
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 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 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 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 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 closure: BottleClosure, // cork, crown-seal, screwcap
public RRPamount: number, // 20
public RRPcurrency: CurrencyCode, // USD
public description: string, // detailed description of the product
public url?: string, // e.g. producer's website
public images?: string[], // (optional image URL)cellar.social
public _id?: ObjectId, // database object id
public id?: string // string representing database object id
) {}
}

@ -1,71 +0,0 @@
import express, { Request, Response } from 'express'
import { collections } from '../services/database.service'
import { Coffee } from '../models'
import {
coffeeValidation,
productCodeValidation,
handleReqError,
handleReqSuccess,
handleReqNotModified
} from '../utils'
import Joi from 'joi'
import { DBcollection, DBinstance, HTTPmethod, ResponseStatus } from '../types'
export const coffeeRouter = express.Router()
coffeeRouter.use(express.json())
// GET
coffeeRouter.get('/', async (_req: Request, res: Response) => {
try {
const coffee = await collections.coffee?.find({}).toArray()
res.status(200).send(coffee)
} catch (error: unknown) {
console.error(error)
if (error instanceof Error) {
res.status(500).send(error.message)
}
}
})
// POST
coffeeRouter.post('/', async (req: Request, res: Response) => {
try {
const {
error,
value: coffee
}: { error: Joi.ValidationError | undefined; value: Coffee } =
coffeeValidation(req.body)
if (error) {
throw error.details[0].message
}
const { productCodeEAN, productCodeUPC, productCodeSKU } = coffee
await productCodeValidation(
productCodeEAN,
productCodeUPC,
productCodeSKU,
DBcollection.Coffee,
DBinstance.Coffee
)
const result = await collections[DBcollection.Coffee]?.insertOne(coffee)
if (result) {
handleReqSuccess(
res,
DBinstance.Coffee,
result.insertedId.toString(),
HTTPmethod.POST
)
} else {
handleReqNotModified(res)
}
} catch (err) {
handleReqError(res, err, ResponseStatus.BadRequest)
}
})

@ -1,7 +0,0 @@
export * from './users.router'
export * from './nostr.router'
export * from './reviews.router'
export * from './wines.router'
export * from './sake.router'
export * from './spirits.router'
export * from './coffee.router'

@ -1,83 +0,0 @@
import express, { Request, Response } from 'express'
import { collections } from '../services/database.service'
import { NostrEvent } from '../models'
import {
nostrEventValidation,
handleReqError,
handleReqSuccess,
handleReqNotModified
} from '../utils'
import { Event } from 'nostr-tools'
import Joi from 'joi'
import { DBinstance, HTTPmethod, ResponseStatus } from '../types'
export const nostrRouter = express.Router()
nostrRouter.use(express.json())
// GET
nostrRouter.get('/', async (_req: Request, res: Response) => {
try {
const nostrEvents = await collections.nostrEvents?.find({}).toArray()
res.status(200).send(nostrEvents)
} catch (error: unknown) {
console.error(error)
if (error instanceof Error) {
res.status(500).send(error.message)
}
}
})
// 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 result = await collections.nostrEvents?.insertOne(nostrEvent)
if (result) {
handleReqSuccess(
res,
DBinstance.NostrEvent,
result.insertedId.toString(),
HTTPmethod.POST
)
} else {
handleReqNotModified(res)
}
} catch (err) {
handleReqError(res, err, ResponseStatus.BadRequest)
}
})
// TODO:
// Add delete route

@ -1,307 +0,0 @@
import express, { Request, Response } from 'express'
import { collections } from '../services/database.service'
import { Review, Sake, Spirit, User, Wine, NostrEvent } from '../models'
import {
reviewValidation,
handleReqError,
handleReqSuccess,
handleReqNotModified,
handleGETreq,
handleReqUnauthorized,
handleReqNotFound,
compareObjects,
idValidation,
modificationPeriodExpired,
handleProductDELETEreq
} from '../utils'
import Joi from 'joi'
import {
DBcollection,
HTTPmethod,
ResponseStatus,
TastingNoteKey,
DBinstance,
UserRole,
ProductType
} from '../types'
import { ObjectId } from 'mongodb'
export const reviewsRouter = express.Router()
const dbInstance = DBinstance.Review
const dbCollection = DBcollection.Reviews
reviewsRouter.use(express.json())
// GET
reviewsRouter.get('/', async (_req: Request, res: Response) => {
await handleGETreq(res, dbCollection, dbInstance)
})
// POST
reviewsRouter.post('/', async (req: Request, res: Response) => {
try {
const {
error,
value: review
}: { error: Joi.ValidationError | undefined; value: Review } =
reviewValidation(req.body)
if (error) {
throw error.details[0].message
}
// user check
const { userId } = res.locals
const user = await collections[DBcollection.Users]?.findOne<User>({
_id: userId
})
if ((user && user.role !== UserRole.Reviewer) || !user?._id) {
handleReqUnauthorized(res)
} else {
review.reviewerId = user?._id
const { nostrId, productId, productType } = review
// nostr event check
const existingNostrEvent = await collections[
DBcollection.NostrEvents
]?.findOne<NostrEvent>({
nostrId
})
if (!existingNostrEvent || !existingNostrEvent._id) {
throw new Error('associated nostr event not found')
}
review.nostrEventId = existingNostrEvent._id
const existingReviewWithNostrEventId = await collections[
dbCollection
]?.findOne({
nostrEventId: existingNostrEvent._id
})
if (existingReviewWithNostrEventId) {
throw new Error('review with provided "nostrEventId" exists')
}
let product: Wine | Sake | Spirit | null | undefined = undefined
switch (productType) {
case ProductType.Wine:
product = await collections[DBcollection.Wines]?.findOne<Wine>({
_id: new ObjectId(productId)
})
break
case ProductType.Sake:
product = await collections[DBcollection.Sake]?.findOne<Sake>({
_id: new ObjectId(productId)
})
break
case ProductType.Spirit:
product = await collections[DBcollection.Spirits]?.findOne<Spirit>({
_id: new ObjectId(productId)
})
break
default:
break
}
if (!product || !product._id) {
throw new Error('product not found')
}
review.productId = product._id
// Add age property to tasting note, if product has vintage property
const { vintage } = product
if (typeof vintage === 'number') {
review.tastingNote[TastingNoteKey.TextureAndBalance].age =
new Date().getFullYear() - vintage
}
delete review.id
const result = await collections[dbCollection]?.insertOne(review)
if (result) {
handleReqSuccess(
res,
dbInstance,
result.insertedId.toString(),
HTTPmethod.POST
)
} else {
handleReqNotModified(res)
}
}
} catch (err) {
handleReqError(res, err, ResponseStatus.BadRequest)
}
})
// PUT
reviewsRouter.put('/', async (req: Request, res: Response) => {
try {
const {
error,
value: newReview
}: { error: Joi.ValidationError | undefined; value: Review } =
reviewValidation(req.body)
if (error) {
throw error.details[0].message
}
// user check
const { userId } = res.locals
const user = await collections[DBcollection.Users]?.findOne<User>({
_id: userId
})
if ((user && user.role !== UserRole.Reviewer) || !user?._id) {
handleReqUnauthorized(res)
} else {
newReview.reviewerId = user?._id
const { id, nostrId, productId, productType } = newReview
idValidation(id)
const existingReview = await collections[dbCollection]?.findOne<Review>({
_id: new ObjectId(id)
})
if (!existingReview) {
handleReqNotFound(res, dbInstance)
} else {
// check creation timestamp
const creationTimestamp = existingReview._id!.getTimestamp().getTime()
if (modificationPeriodExpired(creationTimestamp)) {
handleReqNotModified(res)
}
// productId check
if (existingReview?.productId.toString() !== `${newReview.productId}`) {
throw new Error('"productId" is not valid')
}
// productType check
if (existingReview?.productType !== newReview.productType) {
throw new Error('"productType" is not valid')
}
if (existingReview?.nostrId === nostrId) {
throw new Error('Nostr id has to be different')
}
// nostr event check
const existingNostrEvent = await collections[
DBcollection.NostrEvents
]?.findOne<NostrEvent>({
nostrId
})
if (!existingNostrEvent || !existingNostrEvent._id) {
throw new Error('associated nostr event not found')
}
let product: Wine | Sake | Spirit | null | undefined = undefined
switch (productType) {
case ProductType.Wine:
product = await collections[DBcollection.Wines]?.findOne<Wine>({
_id: new ObjectId(productId)
})
break
case ProductType.Sake:
product = await collections[DBcollection.Sake]?.findOne<Sake>({
_id: new ObjectId(productId)
})
break
case ProductType.Spirit:
product = await collections[DBcollection.Spirits]?.findOne<Spirit>({
_id: new ObjectId(productId)
})
break
default:
break
}
if (!product) {
throw new Error(`Associated "product" not found`)
}
// Add age property to tasting note, if product has vintage property
const { vintage } = product
if (typeof vintage === 'number') {
newReview.tastingNote[TastingNoteKey.TextureAndBalance].age =
new Date().getFullYear() - vintage
}
const existingReviewCopy = {
rating: existingReview.rating,
reviewText: existingReview.reviewText,
tastingNote: existingReview.tastingNote
}
const newReviewCopy = {
rating: newReview.rating,
reviewText: newReview.reviewText,
tastingNote: newReview.tastingNote
}
// compare existing and new review
if (compareObjects(existingReviewCopy, newReviewCopy)) {
handleReqNotModified(res)
}
// update review
const result = await collections[dbCollection]?.updateOne(
{
_id: existingReview._id
},
{ $set: newReviewCopy },
{ upsert: false }
)
if (result && result.modifiedCount === 1) {
handleReqSuccess(
res,
dbInstance,
existingReview._id!.toHexString(),
HTTPmethod.PUT
)
} else {
handleReqNotModified(res)
}
}
}
} catch (err) {
handleReqError(res, err, ResponseStatus.BadRequest)
}
})
// DELETE
reviewsRouter.delete('/', async (req: Request, res: Response) => {
await handleProductDELETEreq(req, res, dbCollection, dbInstance)
})

@ -1,36 +0,0 @@
import express, { Request, Response } from 'express'
import {
sakeValidation,
handleGETreq,
handleProductPOSTreq,
handleProductPUTreq,
handleProductDELETEreq
} from '../utils'
import { DBcollection, DBinstance } from '../types'
export const sakeRouter = express.Router()
const dbInstance = DBinstance.Sake
const dbCollection = DBcollection.Sake
sakeRouter.use(express.json())
// GET
sakeRouter.get('/', async (_req: Request, res: Response) => {
await handleGETreq(res, dbCollection, dbInstance)
})
// POST
sakeRouter.post('/', async (req: Request, res: Response) => {
await handleProductPOSTreq(req, res, dbCollection, dbInstance, sakeValidation)
})
// PUT
sakeRouter.put('/', async (req: Request, res: Response) => {
await handleProductPUTreq(req, res, dbCollection, dbInstance, sakeValidation)
})
// DELETE
sakeRouter.delete('/', async (req: Request, res: Response) => {
await handleProductDELETEreq(req, res, dbCollection, dbInstance)
})

@ -1,48 +0,0 @@
import express, { Request, Response } from 'express'
import {
spiritValidation,
handleGETreq,
handleProductPOSTreq,
handleProductPUTreq,
handleProductDELETEreq
} from '../utils'
import { DBcollection, DBinstance } from '../types'
export const spiritsRouter = express.Router()
const dbInstance = DBinstance.Spirit
const dbCollection = DBcollection.Spirits
spiritsRouter.use(express.json())
// GET
spiritsRouter.get('/', async (_req: Request, res: Response) => {
await handleGETreq(res, dbCollection, dbInstance)
})
// POST
spiritsRouter.post('/', async (req: Request, res: Response) => {
await handleProductPOSTreq(
req,
res,
dbCollection,
dbInstance,
spiritValidation
)
})
// PUT
spiritsRouter.put('/', async (req: Request, res: Response) => {
await handleProductPUTreq(
req,
res,
dbCollection,
dbInstance,
spiritValidation
)
})
// DELETE
spiritsRouter.delete('/', async (req: Request, res: Response) => {
await handleProductDELETEreq(req, res, dbCollection, dbInstance)
})

@ -1,106 +1,41 @@
// External Dependencies
import express, { Request, Response } from 'express'
import { collections } from '../services/database.service'
import { User } from '../models'
import {
userValidation,
handleReqError,
handleReqSuccess,
handleReqNotModified,
handleGETreq
} from '../utils'
import Joi from 'joi'
import { DBcollection, DBinstance, HTTPmethod, ResponseStatus } from '../types'
import User from '../models/user'
// Global Config
export const usersRouter = express.Router()
const dbInstance = DBinstance.User
const dbCollection = DBcollection.Users
usersRouter.use(express.json())
// GET
usersRouter.get('/', async (_req: Request, res: Response) => {
await handleGETreq(res, dbCollection, dbInstance)
try {
const users = await collections.users?.find({}).toArray()
res.status(200).send(users)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
res.status(500).send(error.message)
}
})
// 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 { npub } = res.locals
const existingUser = await collections[dbCollection]?.findOne({
npub
})
if (existingUser) {
throw new Error('user with provided "npub" exists')
}
newUser.npub = npub
const result = await collections[dbCollection]?.insertOne(newUser)
const newUser = req.body as User
const result = await collections.users?.insertOne(newUser)
if (result) {
handleReqSuccess(
res,
DBinstance.User,
result.insertedId.toString(),
HTTPmethod.POST
)
res
.status(201)
.send(`Successfully created a new user with id ${result.insertedId}`)
} else {
handleReqNotModified(res)
res.status(500).send('Failed to create a new user.')
}
} catch (err) {
handleReqError(res, err, ResponseStatus.BadRequest)
}
})
// PUT
usersRouter.put('/', async (req: Request, res: Response) => {
try {
const { userId } = res.locals
const existingUser = await collections[dbCollection]?.findOne<User>({
_id: userId
})
const newName = req.body.name
if (existingUser && existingUser.name !== newName) {
// update user
const result = await collections[dbCollection]?.updateOne(
{
_id: existingUser._id
},
{ $set: { name: newName } },
{ upsert: false }
)
if (result && result.modifiedCount === 1) {
handleReqSuccess(
res,
dbInstance,
existingUser._id!.toHexString(),
HTTPmethod.PUT
)
} else {
handleReqNotModified(res)
}
} else {
handleReqNotModified(res)
}
} catch (err) {
handleReqError(res, err, ResponseStatus.BadRequest)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
console.error(error)
res.status(400).send(error.message)
}
})

@ -1,42 +0,0 @@
import express, { Request, Response } from 'express'
import {
wineValidation,
handleGETreq,
handleProductPOSTreq,
handleProductPUTreq,
handleProductDELETEreq
} from '../utils'
import { DBcollection, DBinstance, UserRole } from '../types'
export const winesRouter = express.Router()
const dbInstance = DBinstance.Wine
const dbCollection = DBcollection.Wines
winesRouter.use(express.json())
// GET
winesRouter.get('/', async (_req: Request, res: Response) => {
await handleGETreq(res, dbCollection, dbInstance)
})
// POST
winesRouter.post('/', async (req: Request, res: Response) => {
await handleProductPOSTreq(req, res, dbCollection, dbInstance, wineValidation)
})
// PUT
winesRouter.put('/', async (req: Request, res: Response) => {
await handleProductPUTreq(req, res, dbCollection, dbInstance, wineValidation)
})
// DELETE
winesRouter.delete('/', async (req: Request, res: Response) => {
await handleProductDELETEreq(
req,
res,
dbCollection,
dbInstance,
UserRole.Reviewer
)
})

@ -1,62 +1,28 @@
// External Dependencies
import * as mongoDB from 'mongodb'
import * as dotenv from 'dotenv'
import { DBcollection } 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
} = {}
export const collections: { users?: mongoDB.Collection } = {}
// Initialize Connection
export async function connectToDatabase() {
dotenv.config()
const { DB_CONN_STRING } = process.env
if (typeof DB_CONN_STRING !== 'string') {
throw new Error(`DB_CONN_STRING environmental variable is missing.`)
}
const client: mongoDB.MongoClient = new mongoDB.MongoClient(DB_CONN_STRING)
const client: mongoDB.MongoClient = new mongoDB.MongoClient(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
process.env.DB_CONN_STRING as any
)
await client.connect()
const db: mongoDB.Db = client.db(process.env.DB_NAME)
const usersCollection: mongoDB.Collection = db.collection(DBcollection.Users)
const nostrEventsCollection: mongoDB.Collection = db.collection(
DBcollection.NostrEvents
)
const reviewsCollection: mongoDB.Collection = db.collection(
DBcollection.Reviews
)
const winesCollection: mongoDB.Collection = db.collection(DBcollection.Wines)
const sakeCollection: mongoDB.Collection = db.collection(DBcollection.Sake)
const spiritsCollection: mongoDB.Collection = db.collection(
DBcollection.Spirits
)
const coffeeCollection: mongoDB.Collection = db.collection(
DBcollection.Coffee
)
const usersCollection: mongoDB.Collection = db.collection('users')
collections.users = usersCollection
collections.nostrEvents = nostrEventsCollection
collections.reviews = reviewsCollection
collections.wines = winesCollection
collections.sake = sakeCollection
collections.spirits = spiritsCollection
collections.coffee = coffeeCollection
console.log(
`Successfully connected to database: ${db.databaseName} and collections:
${Object.keys(collections)
.map((collection) => '- ' + collection)
.join('\n')}`
`Successfully connected to database: ${db.databaseName} and collection: ${usersCollection.collectionName}`
)
}

@ -1,14 +0,0 @@
export enum BufferEncoding {
ASCII = 'ascii',
UTF8 = 'utf8',
UTF_8 = 'utf-8',
UTF16LE = 'utf16le',
UTF_16LE = 'utf-16le',
UCS2 = 'ucs2',
UCS_2 = 'ucs-2',
BASE64 = 'base64',
BASE64URL = 'base64url',
LATIN1 = 'latin1',
BINARY = 'binary',
HEX = 'hex'
}

@ -1,47 +0,0 @@
export enum CoffeeProcessingType {
DeCaff = 'De-caff',
Honey = 'Honey',
SemiDry = 'Semi-dry',
SwissWater = 'Swiss water',
Sundried = 'Sundried',
Washed = 'Washed'
}
export enum CoffeeType {
Arabica = 'Arabica',
Robusta = 'Robusta'
}
export enum ArabicaKind {
Typica = 'Typica',
Bourbon = 'Bourbon',
Caturra = 'Caturra',
Geisha = 'Geisha',
SL28 = 'SL28',
SL34 = 'SL34',
Maragogype = 'Maragogype',
Pacas = 'Pacas',
Pacamara = 'Pacamara',
Kona = 'Kona'
}
export enum RobustaKind {
Congolese = 'Congolese',
Nganda = 'Nganda',
Kouillou = 'Kouillou',
VietnameseRobusta = 'Vietnamese Robusta'
}
export interface CoffeeVariety {
[CoffeeType.Arabica]: ArabicaKind
[CoffeeType.Robusta]: RobustaKind
}
export enum CoffeeRoast {
Green = 'Green',
Light = 'Light',
Medium = 'Medium',
MediumDark = 'Medium-Dark',
Dark = 'Dark',
VeryDark = 'Very Dark'
}

@ -1,19 +0,0 @@
export enum DBcollection {
Users = 'users',
NostrEvents = 'nostrEvents',
Reviews = 'reviews',
Wines = 'wines',
Sake = 'sake',
Spirits = 'spirits',
Coffee = 'coffee'
}
export enum DBinstance {
User = 'User',
NostrEvent = 'NostrEvent',
Review = 'Review',
Wine = 'Wine',
Sake = 'Sake',
Spirit = 'Spirit',
Coffee = 'Coffee'
}

@ -1,11 +0,0 @@
export * from './database'
export * from './routes'
export * from './product'
export * from './user'
export * from './wine'
export * from './sake'
export * from './spirit'
export * from './coffee'
export * from './review'
export * from './review/'
export * from './coding'

13
src/types/locals.d.ts vendored

@ -1,13 +0,0 @@
import 'express'
import { ObjectId } from 'mongodb'
import { UserRole } from './user'
declare global {
namespace Express {
interface Locals {
userId: ObjectId
userRole: UserRole
npub: string
}
}
}

@ -1,85 +0,0 @@
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 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 enum ProductCode {
EAN = 'EAN',
UPC = 'UPC',
SKU = 'SKU'
}

@ -1,122 +0,0 @@
import {
VisualAssessmentKey,
ClarityVisualAssessment,
NatureVisualAssessment,
WhiteColour,
AmberColour,
RoseColour,
RedColour,
BlueColour,
GreenColour,
PrimaryFlavoursAndAromasKey,
Condition,
Intensity,
Age,
CitrusFruit,
AppleFruit,
StoneFruit,
RedFruit,
BlackFruit,
TropicalFruit,
MelonFruit,
Floral,
Vegetal,
Earth,
Microbiological,
Oxidation,
Umami,
Grain,
Dairy,
TextureAndBalanceKey,
Sweetness,
Concentration,
Body,
FlavourIntensity,
PalateLength,
ReasoningConcentration,
Quality,
ReadinessToDrink,
ReasoningKey,
TanninString,
TanninObject,
GrapeFruit,
DriedFruit,
Fault,
Sweet,
Botanical,
Nutty,
Salt,
Burnt,
SmokeEnum
} 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
| GrapeFruit
| BlackFruit
| TropicalFruit
| MelonFruit
| DriedFruit
[PrimaryFlavoursAndAromasKey.Floral]: Floral
[PrimaryFlavoursAndAromasKey.Sweet]: Sweet
[PrimaryFlavoursAndAromasKey.Vegetal]: Vegetal
[PrimaryFlavoursAndAromasKey.Grain]: Grain
[PrimaryFlavoursAndAromasKey.Botanical]: Botanical
[PrimaryFlavoursAndAromasKey.Nutty]: Nutty
[PrimaryFlavoursAndAromasKey.Earth]: Earth
[PrimaryFlavoursAndAromasKey.Dairy]: Dairy
[PrimaryFlavoursAndAromasKey.Umami]: Umami
[PrimaryFlavoursAndAromasKey.Microbiological]: Microbiological
[PrimaryFlavoursAndAromasKey.Salt]: Salt
[PrimaryFlavoursAndAromasKey.Burnt]: Burnt
[PrimaryFlavoursAndAromasKey.Smoke]: SmokeEnum
[PrimaryFlavoursAndAromasKey.Oxidation]: Oxidation
[PrimaryFlavoursAndAromasKey.Fault]: Fault
}
[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,43 +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'
}

@ -1,63 +0,0 @@
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 GrapeFruit {
Grape = 'Grape'
}
export enum BlackFruit {
Blackberry = 'Blackberry',
Blackcurrant = 'Blackcurrant',
Boysenberry = 'Boysenberry',
Blueberry = 'Blueberry',
Olive = 'Olive'
}
export enum TropicalFruit {
Banana = 'Banana',
Lychee = 'Lychee',
Mango = 'Mango',
PassionFruit = 'Passion Fruit',
Pineapple = 'Pineapple',
Guava = 'Guava'
}
export enum MelonFruit {
Cantaloupe = 'Cantaloupe',
Honeydew = 'Honeydew'
}
export enum DriedFruit {
Raisin = 'Raisin',
Prune = 'Prune',
Fig = 'Fig',
Date = 'Date'
}

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

@ -1,213 +0,0 @@
export enum PrimaryFlavoursAndAromasKey {
Condition = 'condition',
Intensity = 'intensity',
Age = 'age',
Fruit = 'fruit',
Floral = 'floral',
Sweet = 'sweet',
Vegetal = 'vegetal',
Grain = 'grain',
Botanical = 'botanical',
Nutty = 'nutty',
Earth = 'earth',
Dairy = 'dairy',
Umami = 'umami',
Microbiological = 'microbiological',
Salt = 'salt',
Burnt = 'burnt',
Smoke = 'smoke',
Oxidation = 'oxidation',
Fault = 'fault'
}
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 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 Sweet {
Butterscotch = 'Butterscotch',
Caramel = 'Caramel',
Honey = 'Honey',
Molasses = 'Molasses',
Sugarcane = 'Sugarcane',
Toffee = 'Toffee'
}
export enum Vegetal {
Artichoke = 'Artichoke',
Asparagus = 'Asparagus',
Basil = 'Basil',
BellPepper = 'Bell Pepper',
BlackTea = 'Black Tea',
Capsicum = 'Capsicum',
Coriander = 'Coriander',
Eucalyptus = 'Eucalyptus',
Gooseberry = 'Gooseberry',
Grass = 'Grass',
Hay = 'Hay',
Herbaceous = 'Herbaceous',
Lemongrass = 'Lemongrass',
Mint = 'Mint',
Tomato = 'Tomato'
}
export enum Grain {
Barley = 'Barley',
Bran = 'Bran',
Cereal = 'Cereal',
CookedRice = 'Cooked Rice',
Corn = 'Corn',
Grains = 'Grains',
Malt = 'Malt',
Oats = 'Oats',
Porridge = 'Porridge',
RawRice = 'Raw Rice',
Rye = 'Rye',
SteamedRice = 'Steamed Rice',
Wheat = 'Wheat'
}
export enum Botanical {
Allspice = 'Allspice',
Anise = 'Anise',
Cedar = 'Cedar',
Cinnamon = 'Cinnamon',
Cloves = 'Cloves',
Coconut = 'Coconut',
Cumin = 'Cumin',
Dill = 'Dill',
Ginger = 'Ginger',
Juniper = 'Juniper',
Liquorice = 'Liquorice',
Nutmeg = 'Nutmeg',
Pepper = 'Pepper',
Sandalwood = 'Sandalwood',
Spices = 'Spices',
Tobacco = 'Tobacco'
}
export enum Nutty {
Almond = 'Almond',
Cashew = 'Cashew',
Chestnut = 'Chestnut',
Hazelnut = 'Hazelnut',
Marzipan = 'Marzipan',
Walnut = 'Walnut'
}
export enum Earth {
Gravel = 'Gravel',
Kerosene = 'Kerosene',
RedBeet = 'Red Beet',
Rocks = 'Rocks',
Slate = 'Slate',
Soil = 'Soil',
Terracotta = 'Terracotta'
}
export enum Dairy {
Butter = 'Butter',
Cheese = 'Cheese',
Cream = 'Cream',
Milk = 'Milk',
Yoghurt = 'Yoghurt'
}
export enum Umami {
FishSauce = 'Fish Sauce',
Seaweed = 'Seaweed',
Soy = 'Soy'
}
export enum Microbiological {
Animal = 'Animal',
BreadDough = 'Bread Dough',
Brioche = 'Brioche',
Farmyard = 'Farmyard',
Iodine = 'Iodine',
Leather = 'Leather',
Meaty = 'Meaty',
Mouse = 'Mouse',
Mushroom = 'Mushroom',
Truffles = 'Truffles',
Vinyl = 'Vinyl',
Yeasty = 'Yeasty'
}
export enum Salt {
Brine = 'Brine'
}
export enum Burnt {
Chocolate = 'Chocolate',
Coffee = 'Coffee',
ToastedBread = 'Toasted Bread',
RoastedNuts = 'Roasted Nuts',
CaramelisedNuts = 'Caramelised Nuts'
}
export enum SmokeEnum {
Ash = 'Ash',
Peat = 'Peat',
Smoke = 'Smoke'
}
export enum Oxidation {
Aldehydes = 'Aldehydes',
Madeirised = 'Madeirised',
Sherry = 'Sherry',
Staleness = 'Staleness'
}
export enum Fault {
BalsamicVinegar = 'Balsamic Vinegar',
Cabbage = 'Cabbage',
Eggs = 'Eggs',
Garlic = 'Garlic',
Mercaptans = 'Mercaptans',
Mustiness = 'Mustiness',
NailVarnishRemover = 'Nail Varnish Remover',
Onion = 'Onion',
Rubber = 'Rubber',
Solvent = 'Solvent',
SourMilk = 'Sour Milk',
Sweat = 'Sweat',
Trichloroanisole = 'Trichloroanisole',
WetCardboard = 'Wet Cardboard'
}

@ -1,95 +0,0 @@
import {
Floral,
Sweet,
Vegetal,
Botanical,
Nutty,
Earth,
Salt,
Burnt,
Fault,
CitrusFruit,
StoneFruit,
RedFruit,
GrapeFruit,
BlackFruit,
DriedFruit
} from '../'
export enum CoffeeCitrusFruit {
Grapefruit = CitrusFruit.Grapefruit,
Lemon = CitrusFruit.Lemon,
Orange = CitrusFruit.Orange
}
export enum CoffeeStoneFruit {
Apricot = StoneFruit.Apricot
}
export enum CoffeeRedFruit {
Raspberry = RedFruit.Raspberry
}
export enum CoffeeGrapeFruit {
Grape = GrapeFruit.Grape
}
export enum CoffeeBlackFruit {
Blueberry = BlackFruit.Blueberry
}
export enum CoffeeDriedFruit {
Raisin = DriedFruit.Raisin,
Date = DriedFruit.Date
}
export enum CoffeeFloral {
Jasmine = Floral.Jasmine,
Lavender = Floral.Lavender,
Rose = Floral.Rose,
Vanilla = Floral.Vanilla
}
export enum CoffeeSweet {
Caramel = Sweet.Caramel,
Honey = Sweet.Honey,
Molasses = Sweet.Molasses
}
export enum CoffeeVegetal {
Grass = Vegetal.Grass
}
export enum CoffeeBotanical {
Cinnamon = Botanical.Cinnamon,
Cloves = Botanical.Cloves,
Nutmeg = Botanical.Nutmeg,
Pepper = Botanical.Pepper,
Tobacco = Botanical.Tobacco
}
export enum CoffeeNutty {
Almond = Nutty.Almond,
Hazelnut = Nutty.Hazelnut,
Walnut = Nutty.Walnut
}
export enum CoffeeEarth {
Soil = Earth.Soil
}
export enum CoffeeSalt {
Brine = Salt.Brine
}
export enum CoffeeBurnt {
Chocolate = Burnt.Chocolate,
ToastedBread = Burnt.ToastedBread,
CaramelisedNuts = Burnt.CaramelisedNuts
}
export enum CoffeeFault {
BalsamicVinegar = Fault.BalsamicVinegar,
Mustiness = Fault.Mustiness,
WetCardboard = Fault.WetCardboard
}

@ -1,4 +0,0 @@
export * from './wine'
export * from './spirit'
export * from './sake'
export * from './coffee'

@ -1,140 +0,0 @@
import {
WhiteColour,
Floral,
Sweet,
Vegetal,
Grain,
Botanical,
Nutty,
Dairy,
Umami,
Microbiological,
Burnt,
SmokeEnum,
CitrusFruit,
AppleFruit,
StoneFruit,
RedFruit,
TropicalFruit,
MelonFruit,
DriedFruit
} from '../'
export enum SakeColour {
WaterWhite = WhiteColour.WaterWhite,
LemonGreen = WhiteColour.LemonGreen,
Lemon = WhiteColour.Lemon,
Gold = WhiteColour.Gold,
WhiteBrown = WhiteColour.WhiteBrown
}
export enum SakeCitrusFruit {
Lemon = CitrusFruit.Lemon,
Orange = CitrusFruit.Orange,
Yuzu = CitrusFruit.Yuzu
}
export enum SakeAppleFruit {
Green = AppleFruit.Green,
Red = AppleFruit.Red,
Ripe = AppleFruit.Ripe
}
export enum SakeStoneFruit {
Apricot = StoneFruit.Apricot,
Nectarine = StoneFruit.Nectarine,
Peach = StoneFruit.Peach,
Plum = StoneFruit.Plum
}
export enum SakeRedFruit {
Cherry = RedFruit.Cherry,
Strawberry = RedFruit.Strawberry
}
export enum SakeTropicalFruit {
Banana = TropicalFruit.Banana,
Lychee = TropicalFruit.Lychee,
Mango = TropicalFruit.Mango,
PassionFruit = TropicalFruit.PassionFruit,
Pineapple = TropicalFruit.Pineapple,
Guava = TropicalFruit.Guava
}
export enum SakeMelonFruit {
Cantaloupe = MelonFruit.Cantaloupe,
Honeydew = MelonFruit.Honeydew
}
export enum SakeDriedFruit {
Date = DriedFruit.Date
}
export enum SakeFloral {
Elderflower = Floral.Elderflower,
Lilac = Floral.Lilac,
Potpourri = Floral.Potpourri
}
export enum SakeSweet {
Caramel = Sweet.Caramel,
Honey = Sweet.Honey,
Molasses = Sweet.Molasses,
Sugarcane = Sweet.Sugarcane
}
export enum SakeVegetal {
Basil = Vegetal.Basil,
Lemongrass = Vegetal.Lemongrass,
Mint = Vegetal.Mint
}
export enum SakeGrain {
Bran = Grain.Bran,
Malt = Grain.Malt,
Porridge = Grain.Porridge,
RawRice = Grain.RawRice,
SteamedRice = Grain.SteamedRice
}
export enum SakeBotanical {
Anise = Botanical.Anise,
Cinnamon = Botanical.Cinnamon,
Cloves = Botanical.Cloves,
Nutmeg = Botanical.Nutmeg,
Pepper = Botanical.Pepper
}
export enum SakeNutty {
Almond = Nutty.Almond,
Chestnut = Nutty.Chestnut,
Walnut = Nutty.Walnut
}
export enum SakeDairy {
Butter = Dairy.Butter,
Cheese = Dairy.Cheese,
Cream = Dairy.Cream,
Yoghurt = Dairy.Yoghurt
}
export enum SakeUmami {
Seaweed = Umami.Seaweed,
Soy = Umami.Soy
}
export enum SakeMicrobiological {
Meaty = Microbiological.Meaty
}
export enum SakeBurnt {
Chocolate = Burnt.Chocolate,
Coffee = Burnt.Coffee,
ToastedBread = Burnt.ToastedBread,
RoastedNuts = Burnt.RoastedNuts,
CaramelisedNuts = Burnt.CaramelisedNuts
}
export enum SakeSmoke {
Smoke = SmokeEnum.Smoke
}

@ -1,206 +0,0 @@
import {
WhiteColour,
AmberColour,
RoseColour,
RedColour,
BlueColour,
GreenColour,
Floral,
Vegetal,
Sweet,
Grain,
Botanical,
Dairy,
Umami,
Microbiological,
Burnt,
SmokeEnum,
Oxidation,
Fault,
CitrusFruit,
AppleFruit,
StoneFruit,
RedFruit,
GrapeFruit,
BlackFruit,
TropicalFruit,
MelonFruit,
DriedFruit
} from '..'
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
}
export enum SpiritCitrusFruit {
Grapefruit = CitrusFruit.Grapefruit,
Lemon = CitrusFruit.Lemon,
Lime = CitrusFruit.Lime,
Marmalade = CitrusFruit.Marmalade,
Orange = CitrusFruit.Orange,
Yuzu = CitrusFruit.Yuzu
}
export enum SpiritAppleFruit {
Green = AppleFruit.Green,
Red = AppleFruit.Red,
Ripe = AppleFruit.Ripe
}
export enum SpiritStoneFruit {
Apricot = StoneFruit.Apricot,
Nectarine = StoneFruit.Nectarine,
Peach = StoneFruit.Peach,
Plum = StoneFruit.Plum
}
export enum SpiritRedFruit {
Cherry = RedFruit.Cherry,
Cranberry = RedFruit.Cranberry,
Pomegranate = RedFruit.Pomegranate,
Raspberry = RedFruit.Raspberry,
SourCherry = RedFruit.SourCherry,
Strawberry = RedFruit.Strawberry
}
export enum SpiritGrapeFruit {
Grape = GrapeFruit.Grape
}
export enum SpiritBlackFruit {
Blackberry = BlackFruit.Blackberry,
Blackcurrant = BlackFruit.Blackcurrant,
Boysenberry = BlackFruit.Boysenberry,
Blueberry = BlackFruit.Blueberry,
Olive = BlackFruit.Olive
}
export enum SpiritTropicalFruit {
Banana = TropicalFruit.Banana,
Lychee = TropicalFruit.Lychee,
Mango = TropicalFruit.Mango,
PassionFruit = TropicalFruit.PassionFruit,
Pineapple = TropicalFruit.Pineapple,
Guava = TropicalFruit.Guava
}
export enum SpiritMelonFruit {
Cantaloupe = MelonFruit.Cantaloupe,
Honeydew = MelonFruit.Honeydew
}
export enum SpiritDriedFruit {
Raisin = DriedFruit.Raisin,
Prune = DriedFruit.Prune,
Fig = DriedFruit.Fig,
Date = DriedFruit.Date
}
export enum SpiritFloral {
Rose = Floral.Rose,
Vanilla = Floral.Vanilla
}
export enum SpiritSweet {
Butterscotch = Sweet.Butterscotch,
Caramel = Sweet.Caramel,
Honey = Sweet.Honey,
Molasses = Sweet.Molasses,
Sugarcane = Sweet.Sugarcane,
Toffee = Sweet.Toffee
}
export enum SpiritVegetal {
Basil = Vegetal.Basil,
Coriander = Vegetal.Coriander,
Grass = Vegetal.Grass,
Herbaceous = Vegetal.Herbaceous,
Mint = Vegetal.Mint
}
export enum SpiritGrain {
Barley = Grain.Barley,
Bran = Grain.Bran,
Cereal = Grain.Cereal,
Corn = Grain.Corn,
Grains = Grain.Grains,
Oats = Grain.Oats,
Porridge = Grain.Porridge,
Rye = Grain.Rye,
SteamedRice = Grain.SteamedRice
}
export enum SpiritBotanical {
Allspice = Botanical.Allspice,
Anise = Botanical.Anise,
Cedar = Botanical.Cedar,
Cinnamon = Botanical.Cinnamon,
Coconut = Botanical.Coconut,
Cumin = Botanical.Cumin,
Dill = Botanical.Dill,
Ginger = Botanical.Ginger,
Juniper = Botanical.Juniper,
Liquorice = Botanical.Liquorice,
Nutmeg = Botanical.Nutmeg,
Pepper = Botanical.Pepper,
Spices = Botanical.Spices,
Tobacco = Botanical.Tobacco
}
export enum SpiritDairy {
Cheese = Dairy.Cheese,
Cream = Dairy.Cream
}
export enum SpiritUmami {
Seaweed = Umami.Seaweed
}
export enum SpiritMicrobiological {
Farmyard = Microbiological.Farmyard,
Leather = Microbiological.Leather,
Meaty = Microbiological.Meaty,
Mushroom = Microbiological.Mushroom,
Yeasty = Microbiological.Yeasty
}
export enum SpiritBurnt {
Chocolate = Burnt.Chocolate,
Coffee = Burnt.Coffee,
ToastedBread = Burnt.ToastedBread,
RoastedNuts = Burnt.RoastedNuts,
CaramelisedNuts = Burnt.CaramelisedNuts
}
export enum SpiritSmoke {
Smoke = SmokeEnum.Smoke
}
export enum SpiritOxidation {
Sherry = Oxidation.Sherry
}
export enum SpiritFault {
NailVarnishRemover = Fault.NailVarnishRemover,
Rubber = Fault.Rubber,
Solvent = Fault.Solvent
}

@ -1,257 +0,0 @@
import {
WhiteColour,
AmberColour,
RoseColour,
RedColour,
Floral,
Sweet,
Vegetal,
Botanical,
Nutty,
Earth,
Dairy,
Umami,
Microbiological,
Salt,
Burnt,
SmokeEnum,
Oxidation,
Fault,
CitrusFruit,
AppleFruit,
StoneFruit,
RedFruit,
GrapeFruit,
BlackFruit,
TropicalFruit,
MelonFruit,
DriedFruit
} from '../'
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 WineCitrusFruit {
Grapefruit = CitrusFruit.Grapefruit,
Lemon = CitrusFruit.Lemon,
Lime = CitrusFruit.Lime,
Marmalade = CitrusFruit.Marmalade,
Orange = CitrusFruit.Orange,
Yuzu = CitrusFruit.Yuzu
}
export enum WineAppleFruit {
Green = AppleFruit.Green,
Red = AppleFruit.Red,
Ripe = AppleFruit.Ripe
}
export enum WineStoneFruit {
Apricot = StoneFruit.Apricot,
Nectarine = StoneFruit.Nectarine,
Peach = StoneFruit.Peach,
Plum = StoneFruit.Plum
}
export enum WineRedFruit {
Cherry = RedFruit.Cherry,
Cranberry = RedFruit.Cranberry,
Pomegranate = RedFruit.Pomegranate,
Raspberry = RedFruit.Raspberry,
SourCherry = RedFruit.SourCherry,
Strawberry = RedFruit.Strawberry
}
export enum WineGrapeFruit {
Grape = GrapeFruit.Grape
}
export enum WineBlackFruit {
Blackberry = BlackFruit.Blackberry,
Blackcurrant = BlackFruit.Blackcurrant,
Boysenberry = BlackFruit.Boysenberry,
Blueberry = BlackFruit.Blueberry,
Olive = BlackFruit.Olive
}
export enum WineTropicalFruit {
Banana = TropicalFruit.Banana,
Lychee = TropicalFruit.Lychee,
Mango = TropicalFruit.Mango,
PassionFruit = TropicalFruit.PassionFruit,
Pineapple = TropicalFruit.Pineapple,
Guava = TropicalFruit.Guava
}
export enum WineMelonFruit {
Cantaloupe = MelonFruit.Cantaloupe,
Honeydew = MelonFruit.Honeydew
}
export enum WineDriedFruit {
Raisin = DriedFruit.Raisin,
Prune = DriedFruit.Prune,
Fig = DriedFruit.Fig,
Date = DriedFruit.Date
}
export enum WineFloral {
Acacia = Floral.Acacia,
Elderflower = Floral.Elderflower,
Hibiscus = Floral.Hibiscus,
Honeysuckle = Floral.Honeysuckle,
Jasmine = Floral.Jasmine,
Lavender = Floral.Lavender,
Lilac = Floral.Lilac,
OrangeBlossom = Floral.OrangeBlossom,
Potpourri = Floral.Potpourri,
Rose = Floral.Rose,
Vanilla = Floral.Vanilla,
Violet = Floral.Violet
}
export enum WineSweet {
Butterscotch = Sweet.Butterscotch,
Caramel = Sweet.Caramel,
Honey = Sweet.Honey,
Molasses = Sweet.Molasses,
Toffee = Sweet.Toffee
}
export enum WineVegetal {
Artichoke = Vegetal.Artichoke,
Asparagus = Vegetal.Asparagus,
Basil = Vegetal.Basil,
BellPepper = Vegetal.BellPepper,
BlackTea = Vegetal.BlackTea,
Capsicum = Vegetal.Capsicum,
Coriander = Vegetal.Coriander,
Eucalyptus = Vegetal.Eucalyptus,
Gooseberry = Vegetal.Gooseberry,
Grass = Vegetal.Grass,
Hay = Vegetal.Hay,
Herbaceous = Vegetal.Herbaceous,
Lemongrass = Vegetal.Lemongrass,
Mint = Vegetal.Mint
}
export enum WineBotanical {
Allspice = Botanical.Allspice,
Anise = Botanical.Anise,
Cedar = Botanical.Cedar,
Cinnamon = Botanical.Cinnamon,
Cloves = Botanical.Cloves,
Coconut = Botanical.Coconut,
Cumin = Botanical.Cumin,
Dill = Botanical.Dill,
Ginger = Botanical.Ginger,
Juniper = Botanical.Juniper,
Liquorice = Botanical.Liquorice,
Nutmeg = Botanical.Nutmeg,
Pepper = Botanical.Pepper,
Sandalwood = Botanical.Sandalwood,
Spices = Botanical.Spices,
Tobacco = Botanical.Tobacco
}
export enum WineNutty {
Almond = Nutty.Almond,
Cashew = Nutty.Cashew,
Chestnut = Nutty.Chestnut,
Hazelnut = Nutty.Hazelnut,
Marzipan = Nutty.Marzipan,
Walnut = Nutty.Walnut
}
export enum WineEarth {
Gravel = Earth.Gravel,
Kerosene = Earth.Kerosene,
RedBeet = Earth.RedBeet,
Rocks = Earth.Rocks,
Slate = Earth.Slate,
Soil = Earth.Soil,
Terracotta = Earth.Terracotta
}
export enum WineDairy {
Butter = Dairy.Butter,
Cheese = Dairy.Cheese,
Cream = Dairy.Cream,
Yoghurt = Dairy.Yoghurt
}
export enum WineUmami {
Seaweed = Umami.Seaweed,
Soy = Umami.Soy
}
export enum WineMicrobiological {
Animal = Microbiological.Animal,
BreadDough = Microbiological.BreadDough,
Brioche = Microbiological.Brioche,
Farmyard = Microbiological.Farmyard,
Iodine = Microbiological.Iodine,
Leather = Microbiological.Leather,
Meaty = Microbiological.Meaty,
Mouse = Microbiological.Mouse,
Mushroom = Microbiological.Mushroom,
Truffles = Microbiological.Truffles,
Vinyl = Microbiological.Vinyl,
Yeasty = Microbiological.Yeasty
}
export enum WineSalt {
Brine = Salt.Brine
}
export enum WineBurnt {
Chocolate = Burnt.Chocolate,
Coffee = Burnt.Coffee,
ToastedBread = Burnt.ToastedBread,
RoastedNuts = Burnt.RoastedNuts,
CaramelisedNuts = Burnt.CaramelisedNuts
}
export enum WineSmoke {
Smoke = SmokeEnum.Smoke
}
export enum WineOxidation {
Aldehydes = Oxidation.Aldehydes,
Madeirised = Oxidation.Madeirised,
Sherry = Oxidation.Sherry,
Staleness = Oxidation.Staleness
}
export enum WineFault {
BalsamicVinegar = Fault.BalsamicVinegar,
Cabbage = Fault.Cabbage,
Eggs = Fault.Eggs,
Garlic = Fault.Garlic,
Mercaptans = Fault.Mercaptans,
Mustiness = Fault.Mustiness,
NailVarnishRemover = Fault.NailVarnishRemover,
Onion = Fault.Onion,
Rubber = Fault.Rubber,
Solvent = Fault.Solvent,
SourMilk = Fault.SourMilk,
Sweat = Fault.Sweat,
Trichloroanisole = Fault.Trichloroanisole,
WetCardboard = Fault.WetCardboard
}

@ -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,26 +0,0 @@
export enum Route {
Users = '/users',
NostrEvents = '/nostr',
Reviews = '/reviews',
Wines = '/wines',
Sake = '/sake',
Spirits = '/spirits',
Coffee = '/coffee'
}
export enum ResponseStatus {
OK = 200,
Created = 201,
NotModified = 304,
BadRequest = 400,
Unauthorized = 401,
NotFound = 404,
InternalServerError = 500
}
export enum HTTPmethod {
GET = 'GET',
POST = 'POST',
PUT = 'PUT',
DELETE = 'DELETE'
}

@ -1,130 +0,0 @@
export enum SakeDesignation {
Table = 'Table',
Pure = 'Pure',
Blended = 'Blended' // Blended with Spirit - up to 10% of final volume
}
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'
}

@ -1,93 +0,0 @@
export enum SpiritType {
White = 'White',
Dark = 'Dark',
Liqueurs = 'Liqueurs'
}
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 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'
}

@ -1,4 +0,0 @@
export enum UserRole {
Reviewer = 'Reviewer', // the only user role that can submit a review
Producer = 'Producer' // the only user role that can submit a product
}

@ -1,358 +0,0 @@
export enum WineType {
White = 'White',
Amber = 'Amber',
Rose = 'Rose',
Red = 'Red'
}
export enum WhiteWineCharacteristic {
LightAromatic = 'Light Aromatic',
TexturalAndSavory = 'Textural and Savory',
RichAndFruitForward = 'Rich and Fruit Forward'
}
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

@ -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,7 +0,0 @@
import { BufferEncoding } from '../types'
export const encodeBase64 = (str: string): string =>
Buffer.from(str, BufferEncoding.UTF8).toString(BufferEncoding.BASE64)
export const decodeBase64 = (str: string): string =>
Buffer.from(str, BufferEncoding.BASE64).toString(BufferEncoding.UTF8)

@ -1 +0,0 @@
export const MODIFICATION_PERIOD = 24 * 60 * 60 * 1000 // 24h in milliseconds

@ -1,2 +0,0 @@
export const errorMessage = (err: unknown) =>
err instanceof Error ? err.message : err

@ -1,9 +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'
export * from './const'

@ -1,73 +0,0 @@
import { nip19 } from 'nostr-tools'
import sha256 from 'crypto-js/sha256'
import { Event } from 'nostr-tools'
import Hex from 'crypto-js/enc-hex'
import { schnorr } from '@noble/curves/secp256k1'
/**
* 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}$/)
}
const serializeEvent = (evt: Event) => {
return JSON.stringify([
0,
evt.pubkey,
evt.created_at,
evt.kind,
evt.tags,
evt.content
])
}
const getEventHash = (event: Event) => {
return sha256(serializeEvent(event)).toString(Hex)
}
export const verifyNostrSignature = async (event: Event) => {
const { sig, pubkey } = event
// FIXME: enable event expiry check
// const eventCreatedAt = event.created_at
// const timeNow = Math.round(Date.now() / 1000)
// const timeDifference = timeNow - eventCreatedAt
// const maxTimeDifference = 300 // 5 minutes in seconds
// if (timeDifference > maxTimeDifference) {
// throw new Error('Nostr signature verification failed. Event Expired.')
// }
const eventHash = getEventHash(event)
// verify nostr signature
return schnorr.verify(sig, eventHash, pubkey)
}

@ -1,312 +0,0 @@
import { Request, Response } from 'express'
import { errorMessage } from './error'
import {
DBcollection,
DBinstance,
HTTPmethod,
ProductCode,
ResponseStatus,
UserRole
} from '../types'
import { collections } from '../services/database.service'
import Joi from 'joi'
import { Sake, Spirit, Wine } from '../models'
import {
productCodeValidation,
alcoholToStandardDrinks,
volumeToMl,
idValidation,
copyObject,
compareObjects,
throwProductCodeError,
modificationPeriodExpired
} from './'
import { ObjectId } from 'mongodb'
export const handleGETreq = async (
res: Response,
dbCollection: DBcollection,
item: DBinstance
) => {
try {
const items = await collections[dbCollection]?.find({}).toArray()
handleReqSuccess(res, item, '', HTTPmethod.GET, items)
} catch (err) {
handleReqError(res, err, ResponseStatus.InternalServerError)
}
}
export const handleProductPOSTreq = async (
req: Request,
res: Response,
dbCollection: DBcollection,
dbInstance: DBinstance,
validation: (data: unknown) => Joi.ValidationResult
) => {
try {
const {
error,
value: item
}: { error: Joi.ValidationError | undefined; value: Wine | Sake | Spirit } =
validation(req.body)
if (error) {
throw error.details[0].message
}
const { userId, userRole } = res.locals
if (userRole !== UserRole.Producer) {
// only Producer can create new product
handleReqUnauthorized(res)
} else {
// product code validation
const { productCodeEAN, productCodeUPC, productCodeSKU } = item
await productCodeValidation(
productCodeEAN,
productCodeUPC,
productCodeSKU,
dbCollection,
dbInstance
)
item.standardDrinks = alcoholToStandardDrinks(
item.alcohol,
volumeToMl(item.volume)
)
item.producerId = userId
const result = await collections[dbCollection]?.insertOne(item)
if (result) {
handleReqSuccess(
res,
dbInstance,
result.insertedId.toString(),
HTTPmethod.POST
)
} else {
handleReqError(res, `${dbInstance} was not stored`, 500)
}
}
} catch (err) {
handleReqError(res, err, ResponseStatus.BadRequest)
}
}
export const handleProductPUTreq = async (
req: Request,
res: Response,
dbCollection: DBcollection,
dbInstance: DBinstance,
validation: (data: unknown) => Joi.ValidationResult
) => {
try {
const {
error,
value: item
}: { error: Joi.ValidationError | undefined; value: Wine | Sake | Spirit } =
validation(req.body)
if (error) {
throw error.details[0].message
}
// validate id
const id = item.id as string
idValidation(id)
const _id = new ObjectId(id)
const { userId } = res.locals
// check if product with id exists in the db
const existingItem = await collections[dbCollection]?.findOne({
_id,
producerId: userId
})
if (!existingItem) {
handleReqNotFound(res, dbInstance)
} else if (existingItem._id) {
const creationTimestamp = existingItem._id.getTimestamp().getTime()
item.standardDrinks = alcoholToStandardDrinks(
item.alcohol,
volumeToMl(item.volume)
)
const existingItemCopy = copyObject(existingItem)
delete existingItemCopy._id
delete existingItemCopy.producerId
const newItemCopy = copyObject(item)
delete newItemCopy.id
if (compareObjects(existingItemCopy, newItemCopy)) {
// no need to update
handleReqNotModified(res)
} else if (modificationPeriodExpired(creationTimestamp)) {
// expired
handleReqNotModified(res)
} else {
// validate product codes
const { productCodeEAN, productCodeUPC, productCodeSKU } = item
if (existingItem.productCodeEAN !== productCodeEAN) {
const existingItemWithEAN = await collections[dbCollection]?.findOne({
productCodeEAN
})
if (existingItemWithEAN) {
throwProductCodeError(dbInstance, ProductCode.EAN)
}
}
if (existingItem.productCodeUPC !== productCodeUPC) {
const existingItemWithUPC = await collections[dbCollection]?.findOne({
productCodeUPC
})
if (existingItemWithUPC) {
throwProductCodeError(dbInstance, ProductCode.UPC)
}
}
if (existingItem.productCodeSKU !== productCodeSKU) {
const existingItemWithSKU = await collections[dbCollection]?.findOne({
productCodeSKU
})
if (existingItemWithSKU) {
throwProductCodeError(dbInstance, ProductCode.SKU)
}
}
delete item.id
const result = await collections[dbCollection]?.findOneAndUpdate(
{
_id,
producerId: userId
},
{ $set: item }
)
if (!result) {
throw new Error(`"${dbInstance}" with provided "id" does not exist`)
}
handleReqSuccess(res, dbInstance, id, HTTPmethod.PUT)
}
} else {
handleReqNotModified(res)
}
} catch (err) {
handleReqError(res, err, ResponseStatus.BadRequest)
}
}
export const handleProductDELETEreq = async (
req: Request,
res: Response,
dbCollection: DBcollection,
dbInstance: DBinstance,
userRole = UserRole.Reviewer
) => {
try {
const { id } = req.body
idValidation(id)
const _id = new ObjectId(id)
const { userId } = res.locals
// check if item with id exists in the db
const existingItem = await collections[dbCollection]?.findOne({
_id,
[`${userRole.toLocaleLowerCase()}Id`]: userId
})
if (!existingItem) {
handleReqNotFound(res, dbInstance)
} else if (existingItem._id) {
const result = await collections[dbCollection]?.deleteOne({ _id })
if (result && result.deletedCount === 1) {
handleReqSuccess(res, dbInstance, id, HTTPmethod.DELETE)
} else {
handleReqNotModified(res)
}
} else {
handleReqNotModified(res)
}
} catch (err) {
handleReqError(res, err, ResponseStatus.InternalServerError)
}
}
export const handleReqError = (
res: Response,
err: unknown,
resStatus: ResponseStatus
) => {
console.error(err)
res.status(resStatus).send(errorMessage(err))
}
export const handleReqSuccess = (
res: Response,
item: DBinstance,
itemId: string,
method: HTTPmethod.GET | HTTPmethod.POST | HTTPmethod.PUT | HTTPmethod.DELETE,
items?: unknown
) => {
switch (method) {
case HTTPmethod.GET:
res.status(ResponseStatus.OK).send(items)
break
case HTTPmethod.POST:
res
.status(ResponseStatus.Created)
.send(`Successfully created "${item}" with id "${itemId}"`)
break
case HTTPmethod.PUT:
res
.status(ResponseStatus.OK)
.send(`Successfully updated "${item}" with id "${itemId}"`)
break
case HTTPmethod.DELETE:
res
.status(ResponseStatus.OK)
.send(`Successfully deleted "${item}" with id "${itemId}"`)
break
default:
break
}
}
export const handleReqNotModified = (res: Response) =>
res.status(ResponseStatus.NotModified).send()
export const handleReqUnauthorized = (res: Response) =>
res.status(ResponseStatus.Unauthorized).send()
export const handleReqNotFound = (res: Response, item: DBinstance) =>
res
.status(ResponseStatus.NotFound)
.send(`"${item}" with provided "id" and associated with user not found`)

@ -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,18 +0,0 @@
import { MODIFICATION_PERIOD } from './const'
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())
export const compareObjects = (a: object, b: object) =>
JSON.stringify(a) === JSON.stringify(b)
export const copyObject = (obj: object) => JSON.parse(JSON.stringify(obj))
export const modificationPeriodExpired = (creationTimestamp: number) =>
Date.now() - creationTimestamp > MODIFICATION_PERIOD

@ -1,110 +0,0 @@
import Joi from 'joi'
import {
CoffeeVariety,
CoffeeType,
ArabicaKind,
RobustaKind,
CoffeeProcessingType,
CoffeeRoast
} from '../../types'
import {
productCodeEANvalidation,
productCodeUPCvalidation,
productCodeSKUvalidation,
countryValidation,
nameValidation,
idJoiValidation,
RRPamountValidation,
RRPcurrencyValidation,
descriptionValidation,
urlValidation,
imagesValidation
} from './'
export const coffeeValidation = (data: unknown): Joi.ValidationResult =>
Joi.object({
productCodeEAN: productCodeEANvalidation,
productCodeUPC: productCodeUPCvalidation,
productCodeSKU: productCodeSKUvalidation,
country: countryValidation,
region: Joi.string(),
origin: Joi.string(),
name: nameValidation,
producerId: idJoiValidation,
variety: Joi.object().custom((variety: CoffeeVariety, helper) => {
const message = (str: string) =>
helper.message({
custom: Joi.expression(str)
})
/**
* Variety key validation
*/
const validVarietyKeys = Object.values(CoffeeType)
const varietyKeys = Object.keys(variety)
if (varietyKeys.length !== 1) {
return message(
`provided "variety" is not valid. "variety" object has to contain only one key. Valid keys are: [${validVarietyKeys.join(', ')}]`
)
}
const varietyType = varietyKeys[0] as CoffeeType
if (!validVarietyKeys.includes(varietyType)) {
return message(
`provided "variety" is not valid. Valid keys for "variety" object are: [${validVarietyKeys.join(', ')}]`
)
}
/**
* Variety value validation
*/
const varietyKind = variety[varietyType]
const validArabicaKinds = Object.values(ArabicaKind)
const validRobustaKinds = Object.values(RobustaKind)
switch (varietyType) {
case CoffeeType.Arabica:
if (
typeof varietyKind !== 'string' ||
!validArabicaKinds.includes(varietyKind as ArabicaKind)
) {
return message(
`provided "variety" is not valid. Valid options for "${varietyType}" kind are: [${validArabicaKinds.join(', ')}]`
)
}
break
case CoffeeType.Robusta:
if (
typeof varietyKind !== 'string' ||
!validRobustaKinds.includes(varietyKind as RobustaKind)
) {
return message(
`provided "variety" is not valid. Valid options for "${varietyType}" kind are: [${validRobustaKinds.join(', ')}]`
)
}
break
default:
break
}
return variety
}),
processingType: Joi.string()
.valid(...Object.values(CoffeeProcessingType))
.required(),
roast: Joi.string()
.valid(...Object.values(CoffeeRoast))
.required(),
RRPamount: RRPamountValidation,
RRPcurrency: RRPcurrencyValidation,
description: descriptionValidation,
url: urlValidation,
images: imagesValidation
}).validate(data)

@ -1,10 +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'
export * from './coffee'

@ -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,53 +0,0 @@
import { collections } from '../../services/database.service'
import { DBcollection, DBinstance, ProductCode } from '../../types'
export const throwProductCodeError = (
instance: DBinstance,
code: ProductCode
) => {
throw new Error(`${instance} with provided "productCode${code}" exists`)
}
export const productCodeValidation = async (
ean: string,
upc: string,
sku: string,
collection: DBcollection,
dbInstance: DBinstance
) => {
if (!ean && !upc && !sku) {
throw new Error(
`provide "productCode${ProductCode.EAN}", "productCode${ProductCode.UPC}" or "productCode${ProductCode.SKU}"`
)
}
if (ean) {
const existingProduct = await collections[collection]?.findOne({
productCodeEAN: ean
})
if (existingProduct) {
throwProductCodeError(dbInstance, ProductCode.EAN)
}
}
if (upc) {
const existingProduct = await collections[collection]?.findOne({
productCodeUPC: upc
})
if (existingProduct) {
throwProductCodeError(dbInstance, ProductCode.UPC)
}
}
if (sku) {
const existingProduct = await collections[collection]?.findOne({
productCodeSKU: sku
})
if (existingProduct) {
throwProductCodeError(dbInstance, ProductCode.SKU)
}
}
}

@ -1,942 +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,
TropicalFruit,
MelonFruit,
Floral,
Vegetal,
Earth,
Microbiological,
Oxidation,
Umami,
Grain,
Dairy,
TextureAndBalanceKey,
Sweetness,
Concentration,
TanninType,
RipeTannin,
UnripeTannin,
Body,
FlavourIntensity,
PalateLength,
ReasoningKey,
ReasoningConcentration,
Quality,
ReadinessToDrink,
RequiredPrimaryFlavoursAndAromasKey,
TanninString,
TanninObject,
GrapeFruit,
DriedFruit,
Sweet,
Botanical,
Nutty,
Salt,
Burnt,
SmokeEnum,
Fault,
WineFloral,
SakeFloral,
SpiritFloral,
CoffeeFloral,
WineSweet,
SakeSweet,
SpiritSweet,
CoffeeSweet,
WineVegetal,
SakeVegetal,
SpiritVegetal,
CoffeeVegetal,
SakeGrain,
SpiritGrain,
WineBotanical,
SakeBotanical,
SpiritBotanical,
CoffeeBotanical,
WineNutty,
SakeNutty,
CoffeeNutty,
WineEarth,
CoffeeEarth,
WineDairy,
SakeDairy,
SpiritDairy,
WineAppleFruit,
WineCitrusFruit,
WineStoneFruit,
WineRedFruit,
WineGrapeFruit,
WineBlackFruit,
WineTropicalFruit,
WineMelonFruit,
WineDriedFruit,
SakeCitrusFruit,
SakeAppleFruit,
SakeStoneFruit,
SakeRedFruit,
SakeTropicalFruit,
SakeMelonFruit,
SakeDriedFruit,
SpiritCitrusFruit,
SpiritAppleFruit,
SpiritStoneFruit,
SpiritRedFruit,
SpiritGrapeFruit,
SpiritBlackFruit,
SpiritTropicalFruit,
SpiritMelonFruit,
SpiritDriedFruit,
CoffeeCitrusFruit,
CoffeeStoneFruit,
CoffeeRedFruit,
CoffeeGrapeFruit,
CoffeeBlackFruit,
CoffeeDriedFruit,
WineUmami,
SakeUmami,
SpiritUmami,
WineMicrobiological,
SakeMicrobiological,
SpiritMicrobiological,
WineSalt,
CoffeeSalt,
WineBurnt,
SakeBurnt,
SpiritBurnt,
CoffeeBurnt,
WineSmoke,
SakeSmoke,
SpiritSmoke,
WineOxidation,
SpiritOxidation,
WineFault,
SpiritFault,
CoffeeFault
} from '../../types'
import { compareArrays, isObject } from '../utils'
import {
idJoiValidation,
nostrIdValidation,
productSpecificValidation,
validateStringValue
} from './'
export const reviewValidation = (data: unknown): Joi.ValidationResult =>
Joi.object({
id: idJoiValidation.optional(),
nostrId: nostrIdValidation,
productId: idJoiValidation,
reviewerId: idJoiValidation,
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 visualAssessment = tastingNote[TastingNoteKey.VisualAssessment]
const visualAssessmentKeys = Object.keys(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,
messageString?: string
) =>
message(
messageString ||
`provided "tastingNote" is not valid. Valid options for "${[noteKey, noteSubKey].join('-')}"${product ? ` for "${product}"` : ''} are: [${Object.values(options).join(', ')}]`
)
/**
* visualAssessment-clarity validation
*/
const clarity = visualAssessment[VisualAssessmentKey.Clarity]
if (validateStringValue(clarity, ClarityVisualAssessment)) {
return messageWithValidOptions(
TastingNoteKey.VisualAssessment,
VisualAssessmentKey.Clarity,
ClarityVisualAssessment
)
}
/**
* visualAssessment-nature validation
*/
const nature = visualAssessment[VisualAssessmentKey.Nature]
if (validateStringValue(nature, NatureVisualAssessment)) {
return messageWithValidOptions(
TastingNoteKey.VisualAssessment,
VisualAssessmentKey.Nature,
NatureVisualAssessment
)
}
const productType: ProductType = helper.state.ancestors[0].productType
/**
* visualAssessment-colour validation
*/
// visualAssessment-colour product specific validation
const colourProductSpecificMessage = productSpecificValidation(
productType,
visualAssessment[VisualAssessmentKey.Colour],
{
...WhiteColour,
...AmberColour,
...RoseColour,
...RedColour,
...BlueColour,
...GreenColour
},
{
[ProductType.Wine]: WineColour,
[ProductType.Sake]: SakeColour,
[ProductType.Spirit]: SpiritColour,
[ProductType.Coffee]: {}
},
messageWithValidOptions,
TastingNoteKey.VisualAssessment,
VisualAssessmentKey.Colour
)
if (colourProductSpecificMessage) {
return colourProductSpecificMessage
}
/**
* primaryFlavoursAndAromas validation
*/
const primaryFlavoursAndAromas =
tastingNote[TastingNoteKey.PrimaryFlavoursAndAromas]
const primaryFlavoursAndAromasKeys = Object.keys(
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 =
primaryFlavoursAndAromas[PrimaryFlavoursAndAromasKey.Condition]
if (validateStringValue(condition, Condition)) {
return messageWithValidOptions(
TastingNoteKey.PrimaryFlavoursAndAromas,
PrimaryFlavoursAndAromasKey.Condition,
Condition
)
}
// primaryFlavoursAndAromas-intensity validation
const intensity =
primaryFlavoursAndAromas[PrimaryFlavoursAndAromasKey.Intensity]
if (validateStringValue(intensity, Intensity)) {
return messageWithValidOptions(
TastingNoteKey.PrimaryFlavoursAndAromas,
PrimaryFlavoursAndAromasKey.Intensity,
Intensity
)
}
// primaryFlavoursAndAromas-age validation
const age = primaryFlavoursAndAromas[PrimaryFlavoursAndAromasKey.Age]
if (validateStringValue(age, Age)) {
return messageWithValidOptions(
TastingNoteKey.PrimaryFlavoursAndAromas,
PrimaryFlavoursAndAromasKey.Age,
Age
)
}
// primaryFlavoursAndAromas-fruit product specific validation
const fruitProductSpecificMessage = productSpecificValidation(
productType,
primaryFlavoursAndAromas[PrimaryFlavoursAndAromasKey.Fruit],
{
...CitrusFruit,
...AppleFruit,
...StoneFruit,
...RedFruit,
...GrapeFruit,
...BlackFruit,
...TropicalFruit,
...MelonFruit,
...DriedFruit
},
{
[ProductType.Wine]: {
...WineCitrusFruit,
...WineAppleFruit,
...WineStoneFruit,
...WineRedFruit,
...WineGrapeFruit,
...WineBlackFruit,
...WineTropicalFruit,
...WineMelonFruit,
...WineDriedFruit
},
[ProductType.Sake]: {
...SakeCitrusFruit,
...SakeAppleFruit,
...SakeStoneFruit,
...SakeRedFruit,
...SakeTropicalFruit,
...SakeMelonFruit,
...SakeDriedFruit
},
[ProductType.Spirit]: {
...SpiritCitrusFruit,
...SpiritAppleFruit,
...SpiritStoneFruit,
...SpiritRedFruit,
...SpiritGrapeFruit,
...SpiritBlackFruit,
...SpiritTropicalFruit,
...SpiritMelonFruit,
...SpiritDriedFruit
},
[ProductType.Coffee]: {
...CoffeeCitrusFruit,
...CoffeeStoneFruit,
...CoffeeRedFruit,
...CoffeeGrapeFruit,
...CoffeeBlackFruit,
...CoffeeDriedFruit
}
},
messageWithValidOptions,
TastingNoteKey.PrimaryFlavoursAndAromas,
PrimaryFlavoursAndAromasKey.Fruit
)
if (fruitProductSpecificMessage) {
return fruitProductSpecificMessage
}
// primaryFlavoursAndAromas-floral product specific validation
const floralProductSpecificMessage = productSpecificValidation(
productType,
primaryFlavoursAndAromas[PrimaryFlavoursAndAromasKey.Floral],
Floral,
{
[ProductType.Wine]: WineFloral,
[ProductType.Sake]: SakeFloral,
[ProductType.Spirit]: SpiritFloral,
[ProductType.Coffee]: CoffeeFloral
},
messageWithValidOptions,
TastingNoteKey.PrimaryFlavoursAndAromas,
PrimaryFlavoursAndAromasKey.Floral
)
if (floralProductSpecificMessage) {
return floralProductSpecificMessage
}
// primaryFlavoursAndAromas-sweet product specific validation
const sweetProductSpecificMessage = productSpecificValidation(
productType,
primaryFlavoursAndAromas[PrimaryFlavoursAndAromasKey.Sweet],
Sweet,
{
[ProductType.Wine]: WineSweet,
[ProductType.Sake]: SakeSweet,
[ProductType.Spirit]: SpiritSweet,
[ProductType.Coffee]: CoffeeSweet
},
messageWithValidOptions,
TastingNoteKey.PrimaryFlavoursAndAromas,
PrimaryFlavoursAndAromasKey.Sweet
)
if (sweetProductSpecificMessage) {
return sweetProductSpecificMessage
}
// primaryFlavoursAndAromas-vegetal product specific validation
const vegetalProductSpecificMessage = productSpecificValidation(
productType,
primaryFlavoursAndAromas[PrimaryFlavoursAndAromasKey.Vegetal],
Vegetal,
{
[ProductType.Wine]: WineVegetal,
[ProductType.Sake]: SakeVegetal,
[ProductType.Spirit]: SpiritVegetal,
[ProductType.Coffee]: CoffeeVegetal
},
messageWithValidOptions,
TastingNoteKey.PrimaryFlavoursAndAromas,
PrimaryFlavoursAndAromasKey.Vegetal
)
if (vegetalProductSpecificMessage) {
return vegetalProductSpecificMessage
}
// primaryFlavoursAndAromas-grain product specific validation
const grainProductSpecificMessage = productSpecificValidation(
productType,
primaryFlavoursAndAromas[PrimaryFlavoursAndAromasKey.Grain],
Grain,
{
[ProductType.Wine]: undefined,
[ProductType.Sake]: SakeGrain,
[ProductType.Spirit]: SpiritGrain,
[ProductType.Coffee]: undefined
},
messageWithValidOptions,
TastingNoteKey.PrimaryFlavoursAndAromas,
PrimaryFlavoursAndAromasKey.Grain
)
if (grainProductSpecificMessage) {
return grainProductSpecificMessage
}
// primaryFlavoursAndAromas-botanical product specific validation
const botanicalProductSpecificMessage = productSpecificValidation(
productType,
primaryFlavoursAndAromas[PrimaryFlavoursAndAromasKey.Botanical],
Botanical,
{
[ProductType.Wine]: WineBotanical,
[ProductType.Sake]: SakeBotanical,
[ProductType.Spirit]: SpiritBotanical,
[ProductType.Coffee]: CoffeeBotanical
},
messageWithValidOptions,
TastingNoteKey.PrimaryFlavoursAndAromas,
PrimaryFlavoursAndAromasKey.Botanical
)
if (botanicalProductSpecificMessage) {
return botanicalProductSpecificMessage
}
// primaryFlavoursAndAromas-nutty product specific validation
const nuttyProductSpecificMessage = productSpecificValidation(
productType,
primaryFlavoursAndAromas[PrimaryFlavoursAndAromasKey.Nutty],
Nutty,
{
[ProductType.Wine]: WineNutty,
[ProductType.Sake]: SakeNutty,
[ProductType.Spirit]: undefined,
[ProductType.Coffee]: CoffeeNutty
},
messageWithValidOptions,
TastingNoteKey.PrimaryFlavoursAndAromas,
PrimaryFlavoursAndAromasKey.Nutty
)
if (nuttyProductSpecificMessage) {
return nuttyProductSpecificMessage
}
// primaryFlavoursAndAromas-earth product specific validation
const earthProductSpecificMessage = productSpecificValidation(
productType,
primaryFlavoursAndAromas[PrimaryFlavoursAndAromasKey.Earth],
Earth,
{
[ProductType.Wine]: WineEarth,
[ProductType.Sake]: undefined,
[ProductType.Spirit]: undefined,
[ProductType.Coffee]: CoffeeEarth
},
messageWithValidOptions,
TastingNoteKey.PrimaryFlavoursAndAromas,
PrimaryFlavoursAndAromasKey.Earth
)
if (earthProductSpecificMessage) {
return earthProductSpecificMessage
}
// primaryFlavoursAndAromas-dairy product specific validation
const dairyProductSpecificMessage = productSpecificValidation(
productType,
primaryFlavoursAndAromas[PrimaryFlavoursAndAromasKey.Dairy],
Dairy,
{
[ProductType.Wine]: WineDairy,
[ProductType.Sake]: SakeDairy,
[ProductType.Spirit]: SpiritDairy,
[ProductType.Coffee]: undefined
},
messageWithValidOptions,
TastingNoteKey.PrimaryFlavoursAndAromas,
PrimaryFlavoursAndAromasKey.Dairy
)
if (dairyProductSpecificMessage) {
return dairyProductSpecificMessage
}
// primaryFlavoursAndAromas-umami product specific validation
const umamiProductSpecificMessage = productSpecificValidation(
productType,
primaryFlavoursAndAromas[PrimaryFlavoursAndAromasKey.Umami],
Umami,
{
[ProductType.Wine]: WineUmami,
[ProductType.Sake]: SakeUmami,
[ProductType.Spirit]: SpiritUmami,
[ProductType.Coffee]: undefined
},
messageWithValidOptions,
TastingNoteKey.PrimaryFlavoursAndAromas,
PrimaryFlavoursAndAromasKey.Umami
)
if (umamiProductSpecificMessage) {
return umamiProductSpecificMessage
}
// primaryFlavoursAndAromas-microbiological product specific validation
const microbiologicalProductSpecificMessage = productSpecificValidation(
productType,
primaryFlavoursAndAromas[PrimaryFlavoursAndAromasKey.Microbiological],
Microbiological,
{
[ProductType.Wine]: WineMicrobiological,
[ProductType.Sake]: SakeMicrobiological,
[ProductType.Spirit]: SpiritMicrobiological,
[ProductType.Coffee]: undefined
},
messageWithValidOptions,
TastingNoteKey.PrimaryFlavoursAndAromas,
PrimaryFlavoursAndAromasKey.Microbiological
)
if (microbiologicalProductSpecificMessage) {
return microbiologicalProductSpecificMessage
}
// primaryFlavoursAndAromas-salt product specific validation
const saltProductSpecificMessage = productSpecificValidation(
productType,
primaryFlavoursAndAromas[PrimaryFlavoursAndAromasKey.Salt],
Salt,
{
[ProductType.Wine]: WineSalt,
[ProductType.Sake]: undefined,
[ProductType.Spirit]: undefined,
[ProductType.Coffee]: CoffeeSalt
},
messageWithValidOptions,
TastingNoteKey.PrimaryFlavoursAndAromas,
PrimaryFlavoursAndAromasKey.Salt
)
if (saltProductSpecificMessage) {
return saltProductSpecificMessage
}
// primaryFlavoursAndAromas-burnt product specific validation
const burntProductSpecificMessage = productSpecificValidation(
productType,
primaryFlavoursAndAromas[PrimaryFlavoursAndAromasKey.Burnt],
Burnt,
{
[ProductType.Wine]: WineBurnt,
[ProductType.Sake]: SakeBurnt,
[ProductType.Spirit]: SpiritBurnt,
[ProductType.Coffee]: CoffeeBurnt
},
messageWithValidOptions,
TastingNoteKey.PrimaryFlavoursAndAromas,
PrimaryFlavoursAndAromasKey.Burnt
)
if (burntProductSpecificMessage) {
return burntProductSpecificMessage
}
// primaryFlavoursAndAromas-smoke product specific validation
const smokeProductSpecificMessage = productSpecificValidation(
productType,
primaryFlavoursAndAromas[PrimaryFlavoursAndAromasKey.Smoke],
SmokeEnum,
{
[ProductType.Wine]: WineSmoke,
[ProductType.Sake]: SakeSmoke,
[ProductType.Spirit]: SpiritSmoke,
[ProductType.Coffee]: undefined
},
messageWithValidOptions,
TastingNoteKey.PrimaryFlavoursAndAromas,
PrimaryFlavoursAndAromasKey.Smoke
)
if (smokeProductSpecificMessage) {
return smokeProductSpecificMessage
}
// primaryFlavoursAndAromas-oxidation product specific validation
const oxidationProductSpecificMessage = productSpecificValidation(
productType,
primaryFlavoursAndAromas[PrimaryFlavoursAndAromasKey.Oxidation],
Oxidation,
{
[ProductType.Wine]: WineOxidation,
[ProductType.Sake]: undefined,
[ProductType.Spirit]: SpiritOxidation,
[ProductType.Coffee]: undefined
},
messageWithValidOptions,
TastingNoteKey.PrimaryFlavoursAndAromas,
PrimaryFlavoursAndAromasKey.Oxidation
)
if (oxidationProductSpecificMessage) {
return oxidationProductSpecificMessage
}
// primaryFlavoursAndAromas-fault product specific validation
const faultProductSpecificMessage = productSpecificValidation(
productType,
primaryFlavoursAndAromas[PrimaryFlavoursAndAromasKey.Fault],
Fault,
{
[ProductType.Wine]: WineFault,
[ProductType.Sake]: undefined,
[ProductType.Spirit]: SpiritFault,
[ProductType.Coffee]: CoffeeFault
},
messageWithValidOptions,
TastingNoteKey.PrimaryFlavoursAndAromas,
PrimaryFlavoursAndAromasKey.Fault
)
if (faultProductSpecificMessage) {
return faultProductSpecificMessage
}
/**
* textureAndBalance validation
*/
const textureAndBalance = tastingNote[TastingNoteKey.TextureAndBalance]
const textureAndBalanceKeys = Object.keys(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 = textureAndBalance[TextureAndBalanceKey.Sweetness]
if (validateStringValue(sweetness, Sweetness)) {
return messageWithValidOptions(
TastingNoteKey.TextureAndBalance,
TextureAndBalanceKey.Sweetness,
Sweetness
)
}
// textureAndBalance-acidity validation
const acidity = textureAndBalance[TextureAndBalanceKey.Acidity]
if (validateStringValue(acidity, Concentration)) {
return messageWithValidOptions(
TastingNoteKey.TextureAndBalance,
TextureAndBalanceKey.Acidity,
Concentration
)
}
// textureAndBalance-tannin validation
const tannin = 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 = (
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 = (
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 = textureAndBalance[TextureAndBalanceKey.Alcohol]
if (validateStringValue(alcohol, Concentration)) {
return messageWithValidOptions(
TastingNoteKey.TextureAndBalance,
TextureAndBalanceKey.Alcohol,
Concentration
)
}
// textureAndBalance-body validation
const body = textureAndBalance[TextureAndBalanceKey.Body]
if (validateStringValue(body, Body)) {
return messageWithValidOptions(
TastingNoteKey.TextureAndBalance,
TextureAndBalanceKey.Body,
Body
)
}
// textureAndBalance-flavourIntensity validation
const flavourIntensity =
textureAndBalance[TextureAndBalanceKey.FlavourIntensity]
if (validateStringValue(flavourIntensity, FlavourIntensity)) {
return messageWithValidOptions(
TastingNoteKey.TextureAndBalance,
TextureAndBalanceKey.FlavourIntensity,
FlavourIntensity
)
}
// textureAndBalance-palateLength validation
const palateLength =
textureAndBalance[TextureAndBalanceKey.PalateLength]
if (validateStringValue(palateLength, PalateLength)) {
return messageWithValidOptions(
TastingNoteKey.TextureAndBalance,
TextureAndBalanceKey.PalateLength,
PalateLength
)
}
// textureAndBalance-reasoning validation
const reasoning = 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 =
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 =
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 =
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 = textureAndBalance[TextureAndBalanceKey.Quality]
if (validateStringValue(quality, Quality)) {
return messageWithValidOptions(
TastingNoteKey.TextureAndBalance,
TextureAndBalanceKey.Quality,
Quality
)
}
// textureAndBalance-readinessToDrink validation
const readinessToDrink =
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,
idJoiValidation,
volumeValidation,
alcoholValidation,
RRPamountValidation,
RRPcurrencyValidation,
descriptionValidation,
urlValidation,
imagesValidation
} from './'
import { sakePolishMap } from '../'
export const sakeValidation = (data: unknown): Joi.ValidationResult =>
Joi.object({
id: idJoiValidation.optional(),
productCodeEAN: productCodeEANvalidation,
productCodeUPC: productCodeUPCvalidation,
productCodeSKU: productCodeSKUvalidation,
country: countryValidation,
region: Joi.string(),
name: nameValidation,
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,
images: imagesValidation
}).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,
idJoiValidation,
volumeValidation,
alcoholValidation,
RRPamountValidation,
RRPcurrencyValidation,
descriptionValidation,
urlValidation,
imagesValidation
} from './'
export const spiritValidation = (data: unknown): Joi.ValidationResult =>
Joi.object({
id: idJoiValidation.optional(),
productCodeEAN: productCodeEANvalidation,
productCodeUPC: productCodeUPCvalidation,
productCodeSKU: productCodeSKUvalidation,
country: countryValidation,
region: Joi.string(),
name: nameValidation,
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,
images: imagesValidation
}).validate(data)

@ -1,10 +0,0 @@
import Joi from 'joi'
import { UserRole } from '../../types'
export const userValidation = (data: unknown): Joi.ValidationResult =>
Joi.object({
name: Joi.string().required(),
role: Joi.string()
.valid(...Object.values(UserRole))
.required()
}).validate(data)

@ -1,190 +0,0 @@
import Joi from 'joi'
import {
WhiteColour,
AmberColour,
RoseColour,
RedColour,
BlueColour,
GreenColour,
CitrusFruit,
AppleFruit,
StoneFruit,
RedFruit,
GrapeFruit,
BlackFruit,
TropicalFruit,
MelonFruit,
DriedFruit,
Floral,
ProductType,
VintageOption,
TastingNoteKey,
VisualAssessmentKey,
PrimaryFlavoursAndAromasKey,
TextureAndBalanceKey,
Sweet,
Vegetal,
Grain,
Botanical,
Nutty,
Earth,
Dairy,
Umami,
Microbiological,
Salt,
Burnt,
SmokeEnum,
Oxidation,
Fault
} from '../../types'
import { npubToHex, validateHex } from '../nostr'
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 idJoiValidation = Joi.string().length(24).required()
export const nostrIdValidation = Joi.string().min(64).max(64).required()
export const idValidation = (id?: string, key = 'id') => {
if (!id) {
throw new Error(`"${key}" is required`)
} else if (id.length !== 24) {
throw new Error(`"${key}" is not valid`)
}
}
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()
// TODO: improve url validation
export const imagesValidation = Joi.array().items(Joi.string())
export const validateStringValue = (
value: unknown,
validOptions: { [key: string]: string }
) => typeof value !== 'string' || !Object.values(validOptions).includes(value)
export const productSpecificValidation = (
productType: ProductType,
value:
| WhiteColour
| AmberColour
| RoseColour
| RedColour
| BlueColour
| GreenColour
| CitrusFruit
| AppleFruit
| StoneFruit
| RedFruit
| GrapeFruit
| BlackFruit
| TropicalFruit
| MelonFruit
| DriedFruit
| Floral
| Sweet
| Vegetal
| Grain
| Botanical
| Nutty
| Earth
| Dairy
| Umami
| Microbiological
| Salt
| Burnt
| SmokeEnum
| Oxidation
| Fault,
propertyValidOptions: { [key: string]: string },
productSpecificValidOptions: {
[key in ProductType]: { [key: string]: string } | undefined
},
messageCallback: (
noteKey: TastingNoteKey,
noteSubKey:
| VisualAssessmentKey
| PrimaryFlavoursAndAromasKey
| TextureAndBalanceKey,
options: object,
product?: string,
messageString?: string
) => Joi.ErrorReport,
noteKey: TastingNoteKey,
noteSubKey:
| VisualAssessmentKey
| PrimaryFlavoursAndAromasKey
| TextureAndBalanceKey
) => {
// FIXME: fix coffee-color validation
if (
noteSubKey === VisualAssessmentKey.Colour &&
productType === ProductType.Coffee
) {
return
}
if (value && validateStringValue(value, propertyValidOptions)) {
return messageCallback(noteKey, noteSubKey, propertyValidOptions)
}
const productValidOptions = productSpecificValidOptions[productType]
if (value && !productValidOptions) {
return messageCallback(
noteKey,
noteSubKey,
{},
productType,
`provided "tastingNote" is not valid. "${[noteKey, noteSubKey].join('-')}" is not supported for "${productType}"`
)
}
if (
value &&
productValidOptions &&
validateStringValue(value, productValidOptions)
) {
return messageCallback(
noteKey,
noteSubKey,
productValidOptions,
productType
)
}
}
export const npubValidation = (npub: string) => {
const hex = npubToHex(npub)
if (!hex || !validateHex(hex)) {
throw new Error('"npub" is not valid')
}
}

@ -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,
volumeValidation,
alcoholValidation,
RRPamountValidation,
RRPcurrencyValidation,
descriptionValidation,
urlValidation,
idJoiValidation,
imagesValidation
} from './'
export const wineValidation = (data: unknown): Joi.ValidationResult =>
Joi.object({
id: idJoiValidation.optional(),
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,
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,
images: imagesValidation
}).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']
}
}