Compare commits
82 Commits
Author | SHA1 | Date | |
---|---|---|---|
dd64552243 | |||
|
9e9e93d194 | ||
58a06d5c83 | |||
|
08d02693ec | ||
350065231c | |||
|
1a400bf136 | ||
e0ad07f5ee | |||
|
5e49d1cef5 | ||
|
fa6c925c94 | ||
|
61331e8e7c | ||
574011dcd5 | |||
|
e4a744b18a | ||
434ede4bab | |||
|
700d199e42 | ||
953db36d78 | |||
|
a4832d2ce6 | ||
049dec6aab | |||
|
9375e91802 | ||
bb15dc69d3 | |||
|
c21577c77a | ||
d7e008f308 | |||
|
c94a3cb83a | ||
|
7b17669c6d | ||
|
a374610073 | ||
5136cb2a74 | |||
|
529838406f | ||
|
4a7b9f194c | ||
079b5958ef | |||
|
ed9d40e409 | ||
d9e3cd188e | |||
|
f1ca771574 | ||
363c119704 | |||
|
f6a09c1647 | ||
88814b9f22 | |||
|
39f615ebb2 | ||
|
6526935620 | ||
613c298410 | |||
|
c5949010ad | ||
d1857a3ccd | |||
|
abfedf7e32 | ||
2a743043e0 | |||
|
50b2999c66 | ||
3816e32289 | |||
|
c414e0ff3e | ||
1598ee8b0b | |||
|
9cc8ba422c | ||
|
aa82179788 | ||
|
5e01078d7a | ||
f44aa5673e | |||
|
8985643bda | ||
5484a8d9e9 | |||
|
5154b3c927 | ||
dc6e444c79 | |||
|
b2c2d9e470 | ||
d5a5488b9d | |||
|
b02d4889ff | ||
|
4fb326630c | ||
a067398825 | |||
|
6b0730fd3d | ||
|
11776c4f5c | ||
|
87c510b986 | ||
|
990b81abe7 | ||
|
c274d0b3cd | ||
|
862e3b4618 | ||
ff9516ed66 | |||
|
b288e40d13 | ||
0371fb55da | |||
|
386ff5b8e5 | ||
069a708ac3 | |||
|
14b61fa0f3 | ||
|
35b2cec312 | ||
063f945cf0 | |||
|
ce9306271d | ||
922d013325 | |||
|
2c5037a491 | ||
5ecbe73cd7 | |||
638dc9cd42 | |||
05d0709f49 | |||
19189c2866 | |||
eb0f389e07 | |||
bf0e15164a | |||
35c2debfae |
.git-hooks
.gitea/workflows
.vscode
package-lock.jsonpackage.jsonsrc
index.ts
middlewares
models
routes
coffee.router.tsnostr.router.tsreviews.router.tssake.router.tsspirits.router.tsusers.router.tswines.router.ts
services
types
coding.tscoffee.tsdatabase.tsindex.tslocals.d.tsproduct.tsreview.ts
review
colour.tsfruit.tsindex.tsprimaryFlavoursAndAromas.ts
routes.tssake.tsspirit.tsuser.tswine.tsproductSpecific
textureAndBalance.tsvisualAssessment.tsutils
@ -1,5 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "*****pre-commit hook******"
|
||||
|
||||
# Avoid commits to the master branch
|
||||
BRANCH=`git rev-parse --abbrev-ref HEAD`
|
||||
REGEX="^(master|main|staging|development)$"
|
||||
|
37
.gitea/workflows/staging-pull-request.yaml
Normal file
37
.gitea/workflows/staging-pull-request.yaml
Normal file
@ -0,0 +1,37 @@
|
||||
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
|
8
.vscode/settings.json
vendored
8
.vscode/settings.json
vendored
@ -10,8 +10,12 @@
|
||||
"biodynamic",
|
||||
"Blanco",
|
||||
"Cachaça",
|
||||
"Caramelised",
|
||||
"Caturra",
|
||||
"colour",
|
||||
"Colours",
|
||||
"Coren",
|
||||
"EAN",
|
||||
"espadín",
|
||||
"Genever",
|
||||
"Jägermeister",
|
||||
@ -41,7 +45,9 @@
|
||||
"Reserva",
|
||||
"Robusta",
|
||||
"RRP",
|
||||
"schnorr",
|
||||
"screwcap",
|
||||
"SKU",
|
||||
"Soju",
|
||||
"Sokujō",
|
||||
"Solera",
|
||||
@ -50,6 +56,8 @@
|
||||
"Tequilana",
|
||||
"tobalá",
|
||||
"Typica",
|
||||
"Umami",
|
||||
"UPC",
|
||||
"Verte",
|
||||
"VSOP",
|
||||
"Yamahai",
|
||||
|
252
package-lock.json
generated
252
package-lock.json
generated
@ -7,13 +7,18 @@
|
||||
"": {
|
||||
"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",
|
||||
"mongodb": "^6.15.0"
|
||||
"joi": "^17.13.3",
|
||||
"mongodb": "^6.15.0",
|
||||
"nostr-tools": "^2.11.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.23.0",
|
||||
@ -23,8 +28,9 @@
|
||||
"@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.13.12",
|
||||
"@types/node": "^22.14.1",
|
||||
"concurrently": "^9.1.2",
|
||||
"eslint": "^9.23.0",
|
||||
"globals": "^16.0.0",
|
||||
@ -301,6 +307,21 @@
|
||||
"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",
|
||||
@ -404,6 +425,63 @@
|
||||
"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",
|
||||
@ -702,6 +780,57 @@
|
||||
"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",
|
||||
@ -1435,6 +1564,27 @@
|
||||
"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",
|
||||
@ -1536,6 +1686,13 @@
|
||||
"@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",
|
||||
@ -1625,13 +1782,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.13.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.12.tgz",
|
||||
"integrity": "sha512-ixiWrCSRi33uqBMRuICcKECW7rtgY43TbsHDpM2XK7lXispd48opW+0IXrBVxv9NMhaz/Ue9kyj6r3NTVyXm8A==",
|
||||
"version": "22.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz",
|
||||
"integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.20.0"
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/normalize-package-data": {
|
||||
@ -2944,6 +3101,12 @@
|
||||
"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",
|
||||
@ -4947,6 +5110,19 @@
|
||||
"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",
|
||||
@ -6262,6 +6438,62 @@
|
||||
"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",
|
||||
@ -11277,7 +11509,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==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@ -11332,9 +11564,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
||||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
|
12
package.json
12
package.json
@ -16,7 +16,8 @@
|
||||
"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"
|
||||
"start:db": "docker compose -f mongo-docker-compose.yml up -d",
|
||||
"preinstall": "git config core.hooksPath .git-hooks"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -26,11 +27,15 @@
|
||||
"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",
|
||||
"mongodb": "^6.15.0"
|
||||
"joi": "^17.13.3",
|
||||
"mongodb": "^6.15.0",
|
||||
"nostr-tools": "^2.11.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.23.0",
|
||||
@ -40,8 +45,9 @@
|
||||
"@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.13.12",
|
||||
"@types/node": "^22.14.1",
|
||||
"concurrently": "^9.1.2",
|
||||
"eslint": "^9.23.0",
|
||||
"globals": "^16.0.0",
|
||||
|
19
src/index.ts
19
src/index.ts
@ -10,7 +10,8 @@ import {
|
||||
spiritsRouter,
|
||||
coffeeRouter
|
||||
} from './routes'
|
||||
import { Routes } from './types'
|
||||
import { Route } from './types'
|
||||
import { authorizeRequest } from './middlewares'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
@ -19,13 +20,15 @@ const port = process.env.PORT || 3000
|
||||
|
||||
connectToDatabase()
|
||||
.then(() => {
|
||||
app.use(Routes.Users, usersRouter)
|
||||
app.use(Routes.NostrEvents, nostrRouter)
|
||||
app.use(Routes.Reviews, reviewsRouter)
|
||||
app.use(Routes.Wines, winesRouter)
|
||||
app.use(Routes.Sake, sakeRouter)
|
||||
app.use(Routes.Spirits, spiritsRouter)
|
||||
app.use(Routes.Coffee, coffeeRouter)
|
||||
app.use(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.listen(port, () => {
|
||||
console.log(`Server started at http://localhost:${port}`)
|
||||
|
71
src/middlewares/authorize.ts
Normal file
71
src/middlewares/authorize.ts
Normal file
@ -0,0 +1,71 @@
|
||||
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
src/middlewares/index.ts
Normal file
1
src/middlewares/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './authorize'
|
@ -11,10 +11,10 @@ export class Coffee {
|
||||
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 variety: CoffeeVariety, // variety type and kind
|
||||
public processingType: CoffeeProcessingType, // processing type
|
||||
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
|
||||
|
@ -1,10 +1,12 @@
|
||||
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 tags: string[][], // array of keywords or hashtags
|
||||
public content: string, // text content of the event
|
||||
public _id?: ObjectId // database object id
|
||||
) {}
|
||||
}
|
||||
|
@ -1,12 +1,17 @@
|
||||
import { ObjectId } from 'mongodb'
|
||||
import { ProductType, RatingOption, TastingNote } from '../types'
|
||||
|
||||
export class Review {
|
||||
constructor(
|
||||
public eventId: string, // foreign key referencing the nostrEvents collection
|
||||
public productId: string, // unique identifier for the product
|
||||
public rating: number, // numerical rating, e.g., 1-100
|
||||
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 tastingNotes: string[], // array of tasting notes, e.g., flavours, aromas
|
||||
public id?: ObjectId // database object id
|
||||
public tastingNote: TastingNote, // an object representing tasting notes
|
||||
public _id?: ObjectId, // database object id
|
||||
public id?: string // string representing database object id
|
||||
) {}
|
||||
}
|
||||
|
@ -1,5 +1,15 @@
|
||||
import { ObjectId } from 'mongodb'
|
||||
import { SakeDesignation, SakeStarter, Vintage } from '../types'
|
||||
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'
|
||||
|
||||
@ -12,18 +22,23 @@ export class Sake {
|
||||
public region: string, // appellation, village, sub-region, vineyard
|
||||
public name: string, // label
|
||||
public producerId: ObjectId, // product producer
|
||||
public designation: SakeDesignation, // table, pure, blended, mirin: new/true/salt
|
||||
public designation: SakeDesignation, // table, pure, blended
|
||||
public polishRate: number, // %
|
||||
public characteristic: SakeCharacteristic,
|
||||
public starter: SakeStarter, // sake starter
|
||||
public yeastStrain: number,
|
||||
public yeastStrain: SakeYeastStrain,
|
||||
public volume: SakeVolume, // bottle volume
|
||||
public alcohol: number, // alcohol percentage
|
||||
public standardDrinks100ml: number, // number representing an amount of standard drinks per bottle per 100ml
|
||||
public vintage: Vintage, // year, nv (non-vintage) or mv (multi-vintage)
|
||||
public 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 image?: string, // (optional image URL)cellar.social
|
||||
public id?: ObjectId // database object id
|
||||
public images?: string[], // (optional image URL)cellar.social
|
||||
public _id?: ObjectId, // database object id
|
||||
public id?: string // string representing database object id
|
||||
) {}
|
||||
}
|
||||
|
@ -1,5 +1,13 @@
|
||||
import { ObjectId } from 'mongodb'
|
||||
import { SpiritType, SpiritVariant, Ingredient, Vintage } from '../types'
|
||||
import {
|
||||
SpiritType,
|
||||
SpiritVariant,
|
||||
Ingredient,
|
||||
VintageOption,
|
||||
StandardDrink,
|
||||
SpiritVolume,
|
||||
SpiritCharacteristic
|
||||
} from '../types'
|
||||
import { Alpha2Code } from 'i18n-iso-countries'
|
||||
import { CurrencyCode } from 'currency-codes-ts/dist/types'
|
||||
|
||||
@ -14,15 +22,18 @@ export class Spirit {
|
||||
public producerId: ObjectId, // product producer
|
||||
public type: SpiritType, // spirit type
|
||||
public variant: SpiritVariant, // vodka, rum, liqueur cream, etc
|
||||
public characteristic: SpiritCharacteristic, // light aromatic, textural, fruit forward, structural & savoury, powerful
|
||||
public ingredients: Ingredient[], // an array of ingredients(flavouring)
|
||||
public volume: SpiritVolume, // bottle volume
|
||||
public alcohol: number, // alcohol percentage
|
||||
public standardDrinks100ml: number, // number representing an amount of standard drinks per bottle
|
||||
public vintage: Vintage, // year, nv (non-vintage) or mv (multi-vintage)
|
||||
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 image?: string, // (optional image URL)cellar.social
|
||||
public id?: ObjectId // database object id
|
||||
public images?: string[], // (optional image URL)cellar.social
|
||||
public _id?: ObjectId, // database object id
|
||||
public id?: string // string representing database object id
|
||||
) {}
|
||||
}
|
||||
|
@ -3,9 +3,9 @@ import { UserRole } from '../types'
|
||||
|
||||
export class User {
|
||||
constructor(
|
||||
public name: string,
|
||||
public npub: string[],
|
||||
public role: UserRole,
|
||||
public id?: ObjectId
|
||||
public name: string, // name, changeable
|
||||
public npub: string, // npub
|
||||
public role: UserRole, // user role (user, reviewer, producer)
|
||||
public _id: ObjectId // database object id
|
||||
) {}
|
||||
}
|
||||
|
@ -1,5 +1,19 @@
|
||||
import { ObjectId } from 'mongodb'
|
||||
import { WineType, Viticulture, BottleClosure, Vintage } from '../types'
|
||||
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'
|
||||
|
||||
@ -9,27 +23,34 @@ export class Wine {
|
||||
public productCodeUPC: string, // Product Code (https://en.wikipedia.org/wiki/Universal_Product_Code)
|
||||
public productCodeSKU: string, // Stock keeping unit (https://en.wikipedia.org/wiki/Stock_keeping_unit)
|
||||
public type: WineType, // numerical rating, e.g., 1-100
|
||||
public style: string, // bubbles+fizz, table, dessert, fortified, vermouth
|
||||
public characteristic: string, // light aromatic, textural, fruit forward, structural & savoury, powerful
|
||||
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: string, // appellation, village, sub-region, vineyard
|
||||
public region: WineRegion, // appellation, village, sub-region, vineyard
|
||||
public name: string, // label
|
||||
public producerId: ObjectId, // product producer
|
||||
public varietal: string, // if more than one, list as 'blend'
|
||||
public vintage: Vintage, // year, nv (non-vintage) or mv (multi-vintage)
|
||||
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 standardDrinks100ml: number, // number representing an amount of standard drinks per bottle
|
||||
public viticulture: Viticulture, // two-letter country codes
|
||||
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,
|
||||
public kosher: boolean,
|
||||
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 image?: string, // (optional image URL)cellar.social
|
||||
public id?: ObjectId // database object id
|
||||
public images?: string[], // (optional image URL)cellar.social
|
||||
public _id?: ObjectId, // database object id
|
||||
public id?: string // string representing database object id
|
||||
) {}
|
||||
}
|
||||
|
@ -1,8 +1,16 @@
|
||||
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'
|
||||
|
||||
// Global Config
|
||||
export const coffeeRouter = express.Router()
|
||||
|
||||
coffeeRouter.use(express.json())
|
||||
@ -25,22 +33,39 @@ coffeeRouter.get('/', async (_req: Request, res: Response) => {
|
||||
// POST
|
||||
coffeeRouter.post('/', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const coffee = req.body as Coffee
|
||||
const {
|
||||
error,
|
||||
value: coffee
|
||||
}: { error: Joi.ValidationError | undefined; value: Coffee } =
|
||||
coffeeValidation(req.body)
|
||||
|
||||
const result = await collections.coffee?.insertOne(coffee)
|
||||
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) {
|
||||
res
|
||||
.status(201)
|
||||
.send(`Successfully created a new coffee with id ${result.insertedId}`)
|
||||
handleReqSuccess(
|
||||
res,
|
||||
DBinstance.Coffee,
|
||||
result.insertedId.toString(),
|
||||
HTTPmethod.POST
|
||||
)
|
||||
} else {
|
||||
res.status(500).send('Failed to create a new coffee.')
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error(error)
|
||||
|
||||
if (error instanceof Error) {
|
||||
res.status(400).send(error.message)
|
||||
handleReqNotModified(res)
|
||||
}
|
||||
} catch (err) {
|
||||
handleReqError(res, err, ResponseStatus.BadRequest)
|
||||
}
|
||||
})
|
||||
|
@ -1,9 +1,16 @@
|
||||
// External Dependencies
|
||||
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'
|
||||
|
||||
// Global Config
|
||||
export const nostrRouter = express.Router()
|
||||
|
||||
nostrRouter.use(express.json())
|
||||
@ -26,23 +33,51 @@ nostrRouter.get('/', async (_req: Request, res: Response) => {
|
||||
// POST
|
||||
nostrRouter.post('/', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const nostrEvent = req.body as NostrEvent
|
||||
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) {
|
||||
res
|
||||
.status(201)
|
||||
.send(
|
||||
`Successfully created a new nostrEvent with id ${result.insertedId}`
|
||||
)
|
||||
handleReqSuccess(
|
||||
res,
|
||||
DBinstance.NostrEvent,
|
||||
result.insertedId.toString(),
|
||||
HTTPmethod.POST
|
||||
)
|
||||
} else {
|
||||
res.status(500).send('Failed to create a new nostrEvent.')
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error(error)
|
||||
|
||||
if (error instanceof Error) {
|
||||
res.status(400).send(error.message)
|
||||
handleReqNotModified(res)
|
||||
}
|
||||
} catch (err) {
|
||||
handleReqError(res, err, ResponseStatus.BadRequest)
|
||||
}
|
||||
})
|
||||
|
||||
// TODO:
|
||||
// Add delete route
|
||||
|
@ -1,46 +1,307 @@
|
||||
import express, { Request, Response } from 'express'
|
||||
import { collections } from '../services/database.service'
|
||||
import { Review } from '../models'
|
||||
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'
|
||||
|
||||
// Global Config
|
||||
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) => {
|
||||
try {
|
||||
const reviews = await collections.reviews?.find({}).toArray()
|
||||
|
||||
res.status(200).send(reviews)
|
||||
} catch (error: unknown) {
|
||||
console.error(error)
|
||||
|
||||
if (error instanceof Error) {
|
||||
res.status(500).send(error.message)
|
||||
}
|
||||
}
|
||||
await handleGETreq(res, dbCollection, dbInstance)
|
||||
})
|
||||
|
||||
// POST
|
||||
reviewsRouter.post('/', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const review = req.body as Review
|
||||
const {
|
||||
error,
|
||||
value: review
|
||||
}: { error: Joi.ValidationError | undefined; value: Review } =
|
||||
reviewValidation(req.body)
|
||||
|
||||
const result = await collections.reviews?.insertOne(review)
|
||||
if (error) {
|
||||
throw error.details[0].message
|
||||
}
|
||||
|
||||
if (result) {
|
||||
res
|
||||
.status(201)
|
||||
.send(`Successfully created a new review with id ${result.insertedId}`)
|
||||
// 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 {
|
||||
res.status(500).send('Failed to create a new review.')
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error(error)
|
||||
review.reviewerId = user?._id
|
||||
|
||||
if (error instanceof Error) {
|
||||
res.status(400).send(error.message)
|
||||
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,46 +1,36 @@
|
||||
import express, { Request, Response } from 'express'
|
||||
import { collections } from '../services/database.service'
|
||||
import { Sake } from '../models'
|
||||
import {
|
||||
sakeValidation,
|
||||
handleGETreq,
|
||||
handleProductPOSTreq,
|
||||
handleProductPUTreq,
|
||||
handleProductDELETEreq
|
||||
} from '../utils'
|
||||
import { DBcollection, DBinstance } from '../types'
|
||||
|
||||
// Global Config
|
||||
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) => {
|
||||
try {
|
||||
const sake = await collections.sake?.find({}).toArray()
|
||||
|
||||
res.status(200).send(sake)
|
||||
} catch (error: unknown) {
|
||||
console.error(error)
|
||||
|
||||
if (error instanceof Error) {
|
||||
res.status(500).send(error.message)
|
||||
}
|
||||
}
|
||||
await handleGETreq(res, dbCollection, dbInstance)
|
||||
})
|
||||
|
||||
// POST
|
||||
sakeRouter.post('/', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const sake = req.body as Sake
|
||||
|
||||
const result = await collections.sake?.insertOne(sake)
|
||||
|
||||
if (result) {
|
||||
res
|
||||
.status(201)
|
||||
.send(`Successfully created a new sake with id ${result.insertedId}`)
|
||||
} else {
|
||||
res.status(500).send('Failed to create a new sake.')
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error(error)
|
||||
|
||||
if (error instanceof Error) {
|
||||
res.status(400).send(error.message)
|
||||
}
|
||||
}
|
||||
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,46 +1,48 @@
|
||||
import express, { Request, Response } from 'express'
|
||||
import { collections } from '../services/database.service'
|
||||
import { Spirit } from '../models'
|
||||
import {
|
||||
spiritValidation,
|
||||
handleGETreq,
|
||||
handleProductPOSTreq,
|
||||
handleProductPUTreq,
|
||||
handleProductDELETEreq
|
||||
} from '../utils'
|
||||
import { DBcollection, DBinstance } from '../types'
|
||||
|
||||
// Global Config
|
||||
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) => {
|
||||
try {
|
||||
const spirits = await collections.spirits?.find({}).toArray()
|
||||
|
||||
res.status(200).send(spirits)
|
||||
} catch (error: unknown) {
|
||||
console.error(error)
|
||||
|
||||
if (error instanceof Error) {
|
||||
res.status(500).send(error.message)
|
||||
}
|
||||
}
|
||||
await handleGETreq(res, dbCollection, dbInstance)
|
||||
})
|
||||
|
||||
// POST
|
||||
spiritsRouter.post('/', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const spirit = req.body as Spirit
|
||||
|
||||
const result = await collections.spirits?.insertOne(spirit)
|
||||
|
||||
if (result) {
|
||||
res
|
||||
.status(201)
|
||||
.send(`Successfully created a new spirit with id ${result.insertedId}`)
|
||||
} else {
|
||||
res.status(500).send('Failed to create a new spirit.')
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error(error)
|
||||
|
||||
if (error instanceof Error) {
|
||||
res.status(400).send(error.message)
|
||||
}
|
||||
}
|
||||
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,46 +1,106 @@
|
||||
// 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'
|
||||
|
||||
// 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) => {
|
||||
try {
|
||||
const users = await collections.users?.find({}).toArray()
|
||||
|
||||
res.status(200).send(users)
|
||||
} catch (error: unknown) {
|
||||
console.error(error)
|
||||
|
||||
if (error instanceof Error) {
|
||||
res.status(500).send(error.message)
|
||||
}
|
||||
}
|
||||
await handleGETreq(res, dbCollection, dbInstance)
|
||||
})
|
||||
|
||||
// POST
|
||||
usersRouter.post('/', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const newUser = req.body as User
|
||||
const result = await collections.users?.insertOne(newUser)
|
||||
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)
|
||||
|
||||
if (result) {
|
||||
res
|
||||
.status(201)
|
||||
.send(`Successfully created a new user with id ${result.insertedId}`)
|
||||
handleReqSuccess(
|
||||
res,
|
||||
DBinstance.User,
|
||||
result.insertedId.toString(),
|
||||
HTTPmethod.POST
|
||||
)
|
||||
} else {
|
||||
res.status(500).send('Failed to create a new user.')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
||||
if (error instanceof Error) {
|
||||
res.status(400).send(error.message)
|
||||
handleReqNotModified(res)
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
})
|
||||
|
@ -1,46 +1,42 @@
|
||||
import express, { Request, Response } from 'express'
|
||||
import { collections } from '../services/database.service'
|
||||
import { Wine } from '../models'
|
||||
import {
|
||||
wineValidation,
|
||||
handleGETreq,
|
||||
handleProductPOSTreq,
|
||||
handleProductPUTreq,
|
||||
handleProductDELETEreq
|
||||
} from '../utils'
|
||||
import { DBcollection, DBinstance, UserRole } from '../types'
|
||||
|
||||
// Global Config
|
||||
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) => {
|
||||
try {
|
||||
const wines = await collections.wines?.find({}).toArray()
|
||||
|
||||
res.status(200).send(wines)
|
||||
} catch (error: unknown) {
|
||||
console.error(error)
|
||||
|
||||
if (error instanceof Error) {
|
||||
res.status(500).send(error.message)
|
||||
}
|
||||
}
|
||||
await handleGETreq(res, dbCollection, dbInstance)
|
||||
})
|
||||
|
||||
// POST
|
||||
winesRouter.post('/', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const wine = req.body as Wine
|
||||
|
||||
const result = await collections.wines?.insertOne(wine)
|
||||
|
||||
if (result) {
|
||||
res
|
||||
.status(201)
|
||||
.send(`Successfully created a new wine with id ${result.insertedId}`)
|
||||
} else {
|
||||
res.status(500).send('Failed to create a new wine.')
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error(error)
|
||||
|
||||
if (error instanceof Error) {
|
||||
res.status(400).send(error.message)
|
||||
}
|
||||
}
|
||||
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,16 +1,16 @@
|
||||
import * as mongoDB from 'mongodb'
|
||||
import * as dotenv from 'dotenv'
|
||||
import { DBcollections } from '../types'
|
||||
import { DBcollection } from '../types'
|
||||
|
||||
// Global Variables
|
||||
export const collections: {
|
||||
[DBcollections.Users]?: mongoDB.Collection
|
||||
[DBcollections.NostrEvents]?: mongoDB.Collection
|
||||
[DBcollections.Reviews]?: mongoDB.Collection
|
||||
[DBcollections.Wines]?: mongoDB.Collection
|
||||
[DBcollections.Sake]?: mongoDB.Collection
|
||||
[DBcollections.Spirits]?: mongoDB.Collection
|
||||
[DBcollections.Coffee]?: mongoDB.Collection
|
||||
[DBcollection.Users]?: mongoDB.Collection
|
||||
[DBcollection.NostrEvents]?: mongoDB.Collection
|
||||
[DBcollection.Reviews]?: mongoDB.Collection
|
||||
[DBcollection.Wines]?: mongoDB.Collection
|
||||
[DBcollection.Sake]?: mongoDB.Collection
|
||||
[DBcollection.Spirits]?: mongoDB.Collection
|
||||
[DBcollection.Coffee]?: mongoDB.Collection
|
||||
} = {}
|
||||
|
||||
// Initialize Connection
|
||||
@ -29,20 +29,20 @@ export async function connectToDatabase() {
|
||||
|
||||
const db: mongoDB.Db = client.db(process.env.DB_NAME)
|
||||
|
||||
const usersCollection: mongoDB.Collection = db.collection(DBcollections.Users)
|
||||
const usersCollection: mongoDB.Collection = db.collection(DBcollection.Users)
|
||||
const nostrEventsCollection: mongoDB.Collection = db.collection(
|
||||
DBcollections.NostrEvents
|
||||
DBcollection.NostrEvents
|
||||
)
|
||||
const reviewsCollection: mongoDB.Collection = db.collection(
|
||||
DBcollections.Reviews
|
||||
DBcollection.Reviews
|
||||
)
|
||||
const winesCollection: mongoDB.Collection = db.collection(DBcollections.Wines)
|
||||
const sakeCollection: mongoDB.Collection = db.collection(DBcollections.Sake)
|
||||
const winesCollection: mongoDB.Collection = db.collection(DBcollection.Wines)
|
||||
const sakeCollection: mongoDB.Collection = db.collection(DBcollection.Sake)
|
||||
const spiritsCollection: mongoDB.Collection = db.collection(
|
||||
DBcollections.Spirits
|
||||
DBcollection.Spirits
|
||||
)
|
||||
const coffeeCollection: mongoDB.Collection = db.collection(
|
||||
DBcollections.Coffee
|
||||
DBcollection.Coffee
|
||||
)
|
||||
|
||||
collections.users = usersCollection
|
||||
|
14
src/types/coding.ts
Normal file
14
src/types/coding.ts
Normal file
@ -0,0 +1,14 @@
|
||||
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,38 +1,47 @@
|
||||
export type CoffeeProcessingType =
|
||||
| 'de-caff'
|
||||
| 'honey'
|
||||
| 'semi-dry'
|
||||
| 'swiss water'
|
||||
| 'sundried'
|
||||
| 'washed'
|
||||
|
||||
type CoffeeVarietyType = 'Robusta' | 'Arabica'
|
||||
|
||||
type ArabicaVarietyKind =
|
||||
| 'Typica'
|
||||
| 'Bourbon'
|
||||
| 'Caturra'
|
||||
| 'Geisha'
|
||||
| 'SL28'
|
||||
| 'SL34'
|
||||
| 'Maragogype'
|
||||
| 'Pacas'
|
||||
| 'Pacamara'
|
||||
| 'Kona'
|
||||
|
||||
type RobustaVarietyKind =
|
||||
| 'Congolese'
|
||||
| 'Nganda'
|
||||
| 'Kouillou'
|
||||
| 'Vietnamese Robusta'
|
||||
|
||||
export type CoffeeVariety = {
|
||||
[key in CoffeeVarietyType]?: ArabicaVarietyKind | RobustaVarietyKind
|
||||
export enum CoffeeProcessingType {
|
||||
DeCaff = 'De-caff',
|
||||
Honey = 'Honey',
|
||||
SemiDry = 'Semi-dry',
|
||||
SwissWater = 'Swiss water',
|
||||
Sundried = 'Sundried',
|
||||
Washed = 'Washed'
|
||||
}
|
||||
|
||||
export type CoffeeRoast =
|
||||
| 'Light'
|
||||
| 'Medium'
|
||||
| 'Medium-Dark'
|
||||
| 'Dark'
|
||||
| 'Very Dark'
|
||||
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,4 +1,4 @@
|
||||
export enum DBcollections {
|
||||
export enum DBcollection {
|
||||
Users = 'users',
|
||||
NostrEvents = 'nostrEvents',
|
||||
Reviews = 'reviews',
|
||||
@ -7,3 +7,13 @@ export enum DBcollections {
|
||||
Spirits = 'spirits',
|
||||
Coffee = 'coffee'
|
||||
}
|
||||
|
||||
export enum DBinstance {
|
||||
User = 'User',
|
||||
NostrEvent = 'NostrEvent',
|
||||
Review = 'Review',
|
||||
Wine = 'Wine',
|
||||
Sake = 'Sake',
|
||||
Spirit = 'Spirit',
|
||||
Coffee = 'Coffee'
|
||||
}
|
||||
|
@ -6,3 +6,6 @@ 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
Normal file
13
src/types/locals.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
import 'express'
|
||||
import { ObjectId } from 'mongodb'
|
||||
import { UserRole } from './user'
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Locals {
|
||||
userId: ObjectId
|
||||
userRole: UserRole
|
||||
npub: string
|
||||
}
|
||||
}
|
||||
}
|
@ -1,64 +1,85 @@
|
||||
export type Availability = 'in stock' | 'out of stock' | 'discontinued'
|
||||
export type Availability = 'In stock' | 'Out of stock' | 'Discontinued'
|
||||
|
||||
export type Ingredient =
|
||||
| 'Blanche'
|
||||
| 'Anise'
|
||||
| 'Fennel'
|
||||
| 'Hyssop'
|
||||
| 'Mint'
|
||||
| 'Citrus Peel'
|
||||
| 'Coriander Seeds'
|
||||
| 'Angelica Root'
|
||||
| 'Cinnamon'
|
||||
| 'Clove'
|
||||
| 'Wheat'
|
||||
| 'Rye'
|
||||
| 'Corn'
|
||||
| 'Potato'
|
||||
| 'Barley'
|
||||
| 'Sugarcane'
|
||||
| 'Fruits'
|
||||
| 'Grains'
|
||||
| 'Juniper'
|
||||
| 'Coriander'
|
||||
| 'Lemon peel'
|
||||
| 'Orange peel'
|
||||
| 'Orris root'
|
||||
| 'Cassia bark'
|
||||
| 'Licorice root'
|
||||
| 'Grapefruit peel'
|
||||
| 'Elderflower'
|
||||
| 'Apple'
|
||||
| 'Blackcurrant'
|
||||
| 'Butterscotch'
|
||||
| 'Peach'
|
||||
| 'Pear'
|
||||
| 'Plum'
|
||||
| 'Raspberries'
|
||||
| 'Sorghum'
|
||||
| 'Rice'
|
||||
| 'Millet'
|
||||
| 'Brown sugar'
|
||||
| 'Buckwheat'
|
||||
| 'Sweet Potato'
|
||||
| 'Oat'
|
||||
| 'Egg (Advocaat)'
|
||||
| 'Strawberry'
|
||||
| 'Almond'
|
||||
| 'Banana'
|
||||
| 'Chocolate'
|
||||
| 'Sour Cherry'
|
||||
| 'Violet'
|
||||
| 'Lemon'
|
||||
| 'Melon'
|
||||
| 'Orange'
|
||||
| 'Raspberry'
|
||||
| 'Yuzu'
|
||||
| 'Almond'
|
||||
| 'Apricot Kernel'
|
||||
| 'Hazelnut'
|
||||
| 'Peanut'
|
||||
| 'Pecan'
|
||||
| 'Walnut'
|
||||
export enum Ingredient {
|
||||
Blanche = 'Blanche',
|
||||
Anise = 'Anise',
|
||||
Fennel = 'Fennel',
|
||||
Hyssop = 'Hyssop',
|
||||
Mint = 'Mint',
|
||||
CitrusPeel = 'Citrus Peel',
|
||||
CorianderSeeds = 'Coriander Seeds',
|
||||
AngelicaRoot = 'Angelica Root',
|
||||
Cinnamon = 'Cinnamon',
|
||||
Clove = 'Clove',
|
||||
Wheat = 'Wheat',
|
||||
Rye = 'Rye',
|
||||
Corn = 'Corn',
|
||||
Potato = 'Potato',
|
||||
Barley = 'Barley',
|
||||
Sugarcane = 'Sugarcane',
|
||||
Fruits = 'Fruits',
|
||||
Grains = 'Grains',
|
||||
Juniper = 'Juniper',
|
||||
Coriander = 'Coriander',
|
||||
LemonPeel = 'Lemon peel',
|
||||
OrangePeel = 'Orange peel',
|
||||
OrrisRoot = 'Orris root',
|
||||
CassiaBark = 'Cassia bark',
|
||||
LicoriceRoot = 'Licorice root',
|
||||
GrapefruitPeel = 'Grapefruit peel',
|
||||
Elderflower = 'Elderflower',
|
||||
Apple = 'Apple',
|
||||
Blackcurrant = 'Blackcurrant',
|
||||
Butterscotch = 'Butterscotch',
|
||||
Peach = 'Peach',
|
||||
Pear = 'Pear',
|
||||
Plum = 'Plum',
|
||||
Raspberries = 'Raspberries',
|
||||
Sorghum = 'Sorghum',
|
||||
Rice = 'Rice',
|
||||
Millet = 'Millet',
|
||||
BrownSugar = 'Brown sugar',
|
||||
Buckwheat = 'Buckwheat',
|
||||
SweetPotato = 'Sweet Potato',
|
||||
Oat = 'Oat',
|
||||
EggAdvocaat = 'Egg (Advocaat)',
|
||||
Strawberry = 'Strawberry',
|
||||
Almond = 'Almond',
|
||||
Banana = 'Banana',
|
||||
Chocolate = 'Chocolate',
|
||||
SourCherry = 'Sour Cherry',
|
||||
Violet = 'Violet',
|
||||
Lemon = 'Lemon',
|
||||
Melon = 'Melon',
|
||||
Orange = 'Orange',
|
||||
Raspberry = 'Raspberry',
|
||||
Yuzu = 'Yuzu',
|
||||
ApricotKernel = 'Apricot Kernel',
|
||||
Hazelnut = 'Hazelnut',
|
||||
Peanut = 'Peanut',
|
||||
Pecan = 'Pecan',
|
||||
Walnut = 'Walnut'
|
||||
}
|
||||
|
||||
export type Vintage = number | 'nv' | 'mv'
|
||||
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'
|
||||
}
|
||||
|
122
src/types/review.ts
Normal file
122
src/types/review.ts
Normal file
@ -0,0 +1,122 @@
|
||||
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
|
||||
}
|
||||
}
|
43
src/types/review/colour.ts
Normal file
43
src/types/review/colour.ts
Normal file
@ -0,0 +1,43 @@
|
||||
// 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'
|
||||
}
|
63
src/types/review/fruit.ts
Normal file
63
src/types/review/fruit.ts
Normal file
@ -0,0 +1,63 @@
|
||||
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'
|
||||
}
|
6
src/types/review/index.ts
Normal file
6
src/types/review/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export * from './visualAssessment'
|
||||
export * from './colour'
|
||||
export * from './primaryFlavoursAndAromas'
|
||||
export * from './textureAndBalance'
|
||||
export * from './fruit'
|
||||
export * from './productSpecific'
|
213
src/types/review/primaryFlavoursAndAromas.ts
Normal file
213
src/types/review/primaryFlavoursAndAromas.ts
Normal file
@ -0,0 +1,213 @@
|
||||
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'
|
||||
}
|
95
src/types/review/productSpecific/coffee.ts
Normal file
95
src/types/review/productSpecific/coffee.ts
Normal file
@ -0,0 +1,95 @@
|
||||
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
|
||||
}
|
4
src/types/review/productSpecific/index.ts
Normal file
4
src/types/review/productSpecific/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from './wine'
|
||||
export * from './spirit'
|
||||
export * from './sake'
|
||||
export * from './coffee'
|
140
src/types/review/productSpecific/sake.ts
Normal file
140
src/types/review/productSpecific/sake.ts
Normal file
@ -0,0 +1,140 @@
|
||||
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
|
||||
}
|
206
src/types/review/productSpecific/spirit.ts
Normal file
206
src/types/review/productSpecific/spirit.ts
Normal file
@ -0,0 +1,206 @@
|
||||
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
|
||||
}
|
257
src/types/review/productSpecific/wine.ts
Normal file
257
src/types/review/productSpecific/wine.ts
Normal file
@ -0,0 +1,257 @@
|
||||
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
|
||||
}
|
106
src/types/review/textureAndBalance.ts
Normal file
106
src/types/review/textureAndBalance.ts
Normal file
@ -0,0 +1,106 @@
|
||||
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'
|
||||
}
|
17
src/types/review/visualAssessment.ts
Normal file
17
src/types/review/visualAssessment.ts
Normal file
@ -0,0 +1,17 @@
|
||||
export enum VisualAssessmentKey {
|
||||
Clarity = 'clarity',
|
||||
Nature = 'nature',
|
||||
Colour = 'colour'
|
||||
}
|
||||
|
||||
export enum ClarityVisualAssessment {
|
||||
Clear = 'Clear',
|
||||
Cloudy = 'Cloudy',
|
||||
Opaque = 'Opaque'
|
||||
}
|
||||
|
||||
export enum NatureVisualAssessment {
|
||||
Still = 'Still',
|
||||
Frizzante = 'Frizzante',
|
||||
Sparkling = 'Sparkling'
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
export enum Routes {
|
||||
export enum Route {
|
||||
Users = '/users',
|
||||
NostrEvents = '/nostr',
|
||||
Reviews = '/reviews',
|
||||
@ -7,3 +7,20 @@ export enum Routes {
|
||||
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,9 +1,130 @@
|
||||
export type SakeDesignation =
|
||||
| 'table'
|
||||
| 'pure'
|
||||
| 'blended'
|
||||
| 'mirin:new'
|
||||
| 'mirin:true'
|
||||
| 'mirin:salt'
|
||||
export enum SakeDesignation {
|
||||
Table = 'Table',
|
||||
Pure = 'Pure',
|
||||
Blended = 'Blended' // Blended with Spirit - up to 10% of final volume
|
||||
}
|
||||
|
||||
export type SakeStarter = 'Kimoto' | 'Sokujō' | 'Yamahai'
|
||||
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,172 +1,93 @@
|
||||
export type SpiritType = 'white' | 'dark' | 'liqueurs'
|
||||
|
||||
export type SpiritVariant =
|
||||
| 'Absinthe'
|
||||
| 'Pastis'
|
||||
| 'Vodka'
|
||||
| 'Genever'
|
||||
| 'Gin'
|
||||
| 'Mezcal'
|
||||
| 'Rum'
|
||||
| 'Eau de Vie'
|
||||
| 'Grappa'
|
||||
| 'Baijiu'
|
||||
| 'Soju'
|
||||
| 'Absinthe'
|
||||
| 'Brandy'
|
||||
| 'Calvados'
|
||||
| 'Chartreuse'
|
||||
| 'Genever'
|
||||
| 'Mezcal'
|
||||
| 'Rum'
|
||||
| 'Slivovitz'
|
||||
| 'Whiskey'
|
||||
| 'Amaro'
|
||||
| 'Coffee'
|
||||
| 'Cream'
|
||||
| 'Creme'
|
||||
| 'Flowers'
|
||||
| 'Fruit'
|
||||
| 'Herb'
|
||||
| 'Honey'
|
||||
| 'Nut'
|
||||
|
||||
export interface WhiteSpiritKind {
|
||||
Absinthe: ['Blanche']
|
||||
Pastis: [
|
||||
'Anise',
|
||||
'Fennel',
|
||||
'Licorice Root',
|
||||
'Hyssop',
|
||||
'Mint',
|
||||
'Citrus Peel',
|
||||
'Coriander Seeds',
|
||||
'Angelica Root',
|
||||
'Cinnamon',
|
||||
'Clove'
|
||||
]
|
||||
Vodka: [
|
||||
'Wheat',
|
||||
'Rye',
|
||||
'Corn',
|
||||
'Potato',
|
||||
'Barley',
|
||||
'Sugarcane',
|
||||
'Fruits',
|
||||
'Grains'
|
||||
]
|
||||
Genever: [
|
||||
{
|
||||
Young: 'Juniper'
|
||||
}
|
||||
]
|
||||
Gin: [
|
||||
{
|
||||
'London Dry': [
|
||||
'Juniper',
|
||||
'Coriander',
|
||||
'Angelica root',
|
||||
'Lemon peel',
|
||||
'Orange peel',
|
||||
'Orris root',
|
||||
'Cassia bark',
|
||||
'Licorice root',
|
||||
'Grapefruit peel',
|
||||
'Elderflower'
|
||||
]
|
||||
},
|
||||
'Plymouth'
|
||||
]
|
||||
Mezcal: [{ Joven: ['espadín', 'tepeztate', 'Tequilana (blue)', 'tobalá'] }]
|
||||
Rum: ['Blanco', 'Cachaça', 'Platino', 'Agricole']
|
||||
'Eau de Vie': [
|
||||
'Apple (Pomme)',
|
||||
'Blackcurrant (Kirsch)',
|
||||
'Butterscotch (Schnapps)',
|
||||
'Peach (Pêche, Schnapps)',
|
||||
'Pear (Poire William)',
|
||||
'Plum (Mirabelle, Slivovitz, Rakia)',
|
||||
'Raspberries (Framboise)'
|
||||
]
|
||||
Grappa: ['Marc', 'Pisco']
|
||||
Baijiu: ['Sorghum', 'Wheat', 'Barley', 'Rice', 'Millet']
|
||||
Soju: ['Barley', 'Brown sugar', 'Buckwheat', 'Rice', 'Sweet Potato']
|
||||
export enum SpiritType {
|
||||
White = 'White',
|
||||
Dark = 'Dark',
|
||||
Liqueurs = 'Liqueurs'
|
||||
}
|
||||
|
||||
export interface DarkSpiritKind {
|
||||
Absinthe: ['Jaune', 'Verte']
|
||||
Brandy: [
|
||||
{
|
||||
Grape: [
|
||||
'VS',
|
||||
'VSOP',
|
||||
'XO',
|
||||
'Beyond Age',
|
||||
'Solera',
|
||||
'Solera Reserva',
|
||||
'Solera Gran Reserva'
|
||||
]
|
||||
}
|
||||
]
|
||||
Calvados: ['Apple', 'Pear']
|
||||
Chartreuse: ['Green', 'Yellow']
|
||||
Genever: [{ Old: ['Juniper'] }, { Coren: ['Juniper'] }]
|
||||
Mezcal: ['Reposado', 'Abuelo', 'Añejo', 'Extra Añejo']
|
||||
Rum: [
|
||||
{
|
||||
Sugar: [
|
||||
'Cachaca (amarela/ouro)',
|
||||
'Dark Rum',
|
||||
'Gold Rum',
|
||||
'Over-proof',
|
||||
'Premium',
|
||||
'Spiced'
|
||||
]
|
||||
}
|
||||
]
|
||||
Slivovitz: []
|
||||
Whiskey: ['Barley', 'Rye', 'Wheat', 'Corn', 'Oat', 'Rice']
|
||||
export enum SpiritVariant {
|
||||
Absinthe = 'Absinthe',
|
||||
Pastis = 'Pastis',
|
||||
Vodka = 'Vodka',
|
||||
Gin = 'Gin',
|
||||
Mezcal = 'Mezcal',
|
||||
Eau = 'Eau de Vie',
|
||||
Grappa = 'Grappa',
|
||||
Baijiu = 'Baijiu',
|
||||
Soju = 'Soju',
|
||||
Brandy = 'Brandy',
|
||||
Calvados = 'Calvados',
|
||||
Chartreuse = 'Chartreuse',
|
||||
Genever = 'Genever',
|
||||
Rum = 'Rum',
|
||||
Slivovitz = 'Slivovitz',
|
||||
Whiskey = 'Whiskey',
|
||||
Amaro = 'Amaro',
|
||||
Coffee = 'Coffee',
|
||||
Cream = 'Cream',
|
||||
Creme = 'Creme',
|
||||
Flowers = 'Flowers',
|
||||
Fruit = 'Fruit',
|
||||
Herb = 'Herb',
|
||||
Honey = 'Honey',
|
||||
Nut = 'Nut'
|
||||
}
|
||||
|
||||
export interface LiqueursSpiritKind {
|
||||
Amaro: []
|
||||
Coffee: []
|
||||
Cream: [
|
||||
'Egg (Advocaat)',
|
||||
'Amarula',
|
||||
'Rum',
|
||||
'Strawberry',
|
||||
'Whiskey (Baileys etc)'
|
||||
]
|
||||
Creme: [
|
||||
'Almond',
|
||||
'Banana',
|
||||
'Blackcurrant',
|
||||
'Chocolate',
|
||||
'Peach',
|
||||
'Sour Cherry',
|
||||
'Violet'
|
||||
]
|
||||
Flowers: ['Rose', 'Violet', 'Elderflower']
|
||||
Fruit: [
|
||||
'Blackcurrant',
|
||||
'Lemon',
|
||||
'Melon',
|
||||
'Orange',
|
||||
'Peach',
|
||||
'Plum',
|
||||
'Raspberry',
|
||||
'Yuzu'
|
||||
]
|
||||
Herb: [
|
||||
'Anise',
|
||||
'Dom Benedictine',
|
||||
'Bitters',
|
||||
'Ginger',
|
||||
'Jägermeister',
|
||||
'Metaxa',
|
||||
'Mint'
|
||||
]
|
||||
Honey: ['Licor 43', 'Rum', 'Vodka', 'Whiskey']
|
||||
Nut: ['Almond', 'Apricot Kernel', 'Hazelnut', 'Peanut', 'Pecan', 'Walnut']
|
||||
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,5 +1,4 @@
|
||||
export enum UserRole {
|
||||
User = 'user',
|
||||
Reviewer = 'reviewer',
|
||||
Producer = 'producer'
|
||||
Reviewer = 'Reviewer', // the only user role that can submit a review
|
||||
Producer = 'Producer' // the only user role that can submit a product
|
||||
}
|
||||
|
@ -1,5 +1,358 @@
|
||||
export type WineType = 'white' | 'amber' | 'rose' | 'red'
|
||||
export enum WineType {
|
||||
White = 'White',
|
||||
Amber = 'Amber',
|
||||
Rose = 'Rose',
|
||||
Red = 'Red'
|
||||
}
|
||||
|
||||
export type Viticulture = 'biodynamic' | 'organic' | 'conventional'
|
||||
export enum WhiteWineCharacteristic {
|
||||
LightAromatic = 'Light Aromatic',
|
||||
TexturalAndSavory = 'Textural and Savory',
|
||||
RichAndFruitForward = 'Rich and Fruit Forward'
|
||||
}
|
||||
|
||||
export type BottleClosure = 'cork' | 'crown-seal' | 'screwcap'
|
||||
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
|
||||
|
38
src/utils/alcohol.ts
Normal file
38
src/utils/alcohol.ts
Normal file
@ -0,0 +1,38 @@
|
||||
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')
|
||||
}
|
7
src/utils/coding.ts
Normal file
7
src/utils/coding.ts
Normal file
@ -0,0 +1,7 @@
|
||||
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
src/utils/const.ts
Normal file
1
src/utils/const.ts
Normal file
@ -0,0 +1 @@
|
||||
export const MODIFICATION_PERIOD = 24 * 60 * 60 * 1000 // 24h in milliseconds
|
2
src/utils/error.ts
Normal file
2
src/utils/error.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export const errorMessage = (err: unknown) =>
|
||||
err instanceof Error ? err.message : err
|
9
src/utils/index.ts
Normal file
9
src/utils/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
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'
|
73
src/utils/nostr.ts
Normal file
73
src/utils/nostr.ts
Normal file
@ -0,0 +1,73 @@
|
||||
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)
|
||||
}
|
312
src/utils/route.ts
Normal file
312
src/utils/route.ts
Normal file
@ -0,0 +1,312 @@
|
||||
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`)
|
36
src/utils/sake.ts
Normal file
36
src/utils/sake.ts
Normal file
@ -0,0 +1,36 @@
|
||||
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 }
|
||||
}
|
||||
}
|
251
src/utils/spirit.ts
Normal file
251
src/utils/spirit.ts
Normal file
@ -0,0 +1,251 @@
|
||||
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
|
||||
]
|
||||
}
|
18
src/utils/utils.ts
Normal file
18
src/utils/utils.ts
Normal file
@ -0,0 +1,18 @@
|
||||
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
|
110
src/utils/validation/coffee.ts
Normal file
110
src/utils/validation/coffee.ts
Normal file
@ -0,0 +1,110 @@
|
||||
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)
|
10
src/utils/validation/index.ts
Normal file
10
src/utils/validation/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
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'
|
24
src/utils/validation/nostr.ts
Normal file
24
src/utils/validation/nostr.ts
Normal file
@ -0,0 +1,24 @@
|
||||
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)
|
53
src/utils/validation/product.ts
Normal file
53
src/utils/validation/product.ts
Normal file
@ -0,0 +1,53 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
942
src/utils/validation/review.ts
Normal file
942
src/utils/validation/review.ts
Normal file
@ -0,0 +1,942 @@
|
||||
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)
|
137
src/utils/validation/sake.ts
Normal file
137
src/utils/validation/sake.ts
Normal file
@ -0,0 +1,137 @@
|
||||
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)
|
293
src/utils/validation/spirit.ts
Normal file
293
src/utils/validation/spirit.ts
Normal file
@ -0,0 +1,293 @@
|
||||
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)
|
10
src/utils/validation/user.ts
Normal file
10
src/utils/validation/user.ts
Normal file
@ -0,0 +1,10 @@
|
||||
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)
|
190
src/utils/validation/validations.ts
Normal file
190
src/utils/validation/validations.ts
Normal file
@ -0,0 +1,190 @@
|
||||
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')
|
||||
}
|
||||
}
|
393
src/utils/validation/wine.ts
Normal file
393
src/utils/validation/wine.ts
Normal file
@ -0,0 +1,393 @@
|
||||
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)
|
689
src/utils/wine.ts
Normal file
689
src/utils/wine.ts
Normal file
@ -0,0 +1,689 @@
|
||||
import { WineRegion } from '../types'
|
||||
|
||||
export const wineRegionsMap: { [key: string]: string[] | WineRegion } = {
|
||||
IT: {
|
||||
Piedmont: {
|
||||
Langhe: {
|
||||
Barolo: ['La Morra', 'Castiglione Falletto', `Serralunga d'Alba`],
|
||||
Barbaresco: ['Neive', 'Alba', 'Treiso']
|
||||
},
|
||||
Monferrato: ['Gavi', 'Asti', 'Canelli']
|
||||
},
|
||||
Tuscany: [
|
||||
'Chianti',
|
||||
'Montalcino',
|
||||
'Montepulciano',
|
||||
'Scansano',
|
||||
'Carmignano'
|
||||
],
|
||||
Veneto: ['Valpolicella', 'Soave'],
|
||||
'Friuli-Venezia Giulia': [
|
||||
'Collio',
|
||||
'Colli Orientali del Friuli',
|
||||
'Friuli Grave del Friuli'
|
||||
],
|
||||
'Trentino-Alto Adige': ['Trentino', 'Alto Adige'],
|
||||
Lombardy: [],
|
||||
Franciacorta: [],
|
||||
Valtellina: [],
|
||||
'Emilia-Romagna': ['Lambrusco'],
|
||||
Umbria: ['Orvieto', 'Torgiano'],
|
||||
Lazio: ['Frascati'],
|
||||
Abruzzo: [],
|
||||
Molise: ['Biferno', 'Pentro'],
|
||||
Campania: ['Taurasi', 'Aglianico del Vulture', 'Fiano di Avellino'],
|
||||
Puglia: ['Primitivo di Manduria', 'Salice Salentino'],
|
||||
Basilicata: [],
|
||||
Calabria: ['Cirò', 'Greco di Bianco'],
|
||||
Sicily: ['Etna', 'Marsala', 'Pantelleria'],
|
||||
Sardinia: ['Vermentino di Gallura', 'Cannonau di Sardegna']
|
||||
},
|
||||
FR: {
|
||||
Alsace: [],
|
||||
Ardeche: [],
|
||||
Aquitaine: [],
|
||||
Bordeaux: {
|
||||
Médoc: ['Pauillac', 'Margaux', 'Saint-Estèphe', 'Saint-Julien'],
|
||||
'Saint-Émilion': [],
|
||||
Pomerol: [],
|
||||
Graves: ['Pessac-Léognan'],
|
||||
Sauternes: ['Barsac']
|
||||
},
|
||||
Burgundy: {
|
||||
Chablis: [],
|
||||
'Côte de Nuits': [
|
||||
'Gevrey-Chambertin',
|
||||
'Vosne-Romanée',
|
||||
'Nuits-Saint-Georges'
|
||||
],
|
||||
'Côte de Beaune': ['Meursault', 'Puligny-Montrachet', 'Beaune'],
|
||||
'Côte Chalonnaise': ['Mercurey'],
|
||||
Mâconnais: ['Pouilly-Fuissé'],
|
||||
Beaujolais: ['Morgon', 'Fleurie', 'Moulin a Vent']
|
||||
},
|
||||
Champagne: ['Montagne de Reims', 'Vallée de la Marne', 'Côte des Blancs'],
|
||||
'Loire Valley': [
|
||||
'Sancerre',
|
||||
'Pouilly-Fumé',
|
||||
'Vouvray',
|
||||
'Muscadet',
|
||||
'Anjou',
|
||||
'Saumur',
|
||||
'Chinon'
|
||||
],
|
||||
'Rhône Valley': [
|
||||
'Côte-Rôtie',
|
||||
'Hermitage',
|
||||
'Condrieu',
|
||||
'Saint-Joseph',
|
||||
'Châteauneuf-du-Pape',
|
||||
'Côtes du Rhône',
|
||||
'Gigondas',
|
||||
'Vacqueyras'
|
||||
],
|
||||
Provence: ['Côtes de Provence', 'Bandol'],
|
||||
'Languedoc-Roussillon': [],
|
||||
'Southwest France': ['Cahors', 'Madiran', 'Jurançon'],
|
||||
Jura: ['Arbois', 'Côtes du Jura'],
|
||||
Savoie: [],
|
||||
Corsica: ['Patrimonio']
|
||||
},
|
||||
DE: {
|
||||
Mosel: ['Saar', 'Ruwer'],
|
||||
Pfalz: [],
|
||||
Rheinhessen: [],
|
||||
Nahe: [],
|
||||
Rheingau: [],
|
||||
'Hessische Bergstraße': [],
|
||||
Franken: [],
|
||||
Baden: [],
|
||||
Württemberg: [],
|
||||
'Saale-Unstrut': [],
|
||||
Sachsen: [],
|
||||
Ahr: []
|
||||
},
|
||||
CH: {
|
||||
Vaud: ['Lavaux', 'Vevey', 'Morges', 'Nyon'],
|
||||
Valais: ['Sierre', 'Sion', 'Martigny'],
|
||||
Geneva: ['Lancy', 'Carouge'],
|
||||
Ticino: ['Locarno', 'Bellinzona', 'Ascona'],
|
||||
Neuchâtel: ['Val-de-Travers', 'Val-de-Ruz'],
|
||||
Fribourg: ['Glâne', 'Sarine'],
|
||||
Bern: ['Thun', 'Biel'],
|
||||
Aargau: ['Baden', 'Brugg'],
|
||||
Thurgau: ['Frauenfeld', 'Arbon'],
|
||||
Zürich: ['Affoltern', 'Dietikon']
|
||||
},
|
||||
ES: {
|
||||
Rioja: ['Rioja Alta', 'Rioja Alavesa', 'Rioja Oriental'],
|
||||
Navarra: [],
|
||||
'Rías Baixas': [],
|
||||
'Ribeira Sacra': [],
|
||||
Valdeorras: [],
|
||||
Txakoli: ['Getariako Txakolina', 'Bizkaiko Txakolina', 'Arabako Txakolina'],
|
||||
'Ribera del Duero': [],
|
||||
Rueda: [],
|
||||
Toro: [],
|
||||
Cigales: [],
|
||||
'La Mancha': [],
|
||||
Valdepeñas: [],
|
||||
Priorat: [],
|
||||
Penedès: [],
|
||||
Cava: [],
|
||||
Montsant: [],
|
||||
'Conca de Barberà': [],
|
||||
'Jerez-Xérès': [],
|
||||
'Montilla-Moriles': [],
|
||||
Málaga: [],
|
||||
'Sierras de Málaga': [],
|
||||
'Condado de Huelva': [],
|
||||
'Utiel-Requena': [],
|
||||
Valencia: [],
|
||||
Jumilla: [],
|
||||
Yecla: [],
|
||||
Bullas: [],
|
||||
'Canary Islands': [],
|
||||
'Balearic Islands': ['Binissalem', 'Pla i Llevant'],
|
||||
Bierzo: [],
|
||||
'Campo de Borja': [],
|
||||
Aragón: ['Calatayud', 'Somontano'],
|
||||
'Ribera del Guadiana': []
|
||||
},
|
||||
PT: {
|
||||
Douro: ['Baixo Corgo', 'Cima Corgo', 'Douro Superior'],
|
||||
Alentejo: ['Central', 'Litoral', 'Interior Norte', 'Interior Sul'],
|
||||
Porto: ['Gaia'],
|
||||
'Vinho Verde': [
|
||||
'Amarante',
|
||||
'Ave',
|
||||
'Baião',
|
||||
'Basto',
|
||||
'Cávado',
|
||||
'Lima',
|
||||
'Lima Interior',
|
||||
'Lima Litoral',
|
||||
'Monção',
|
||||
'Paiva',
|
||||
'Sousa'
|
||||
],
|
||||
Tejo: [
|
||||
'Alcobaça',
|
||||
'Arruda dos Vinhos',
|
||||
'Azeiteira',
|
||||
'Benavente e Santarém',
|
||||
'Bico do Cachorro',
|
||||
'Borba',
|
||||
'Casaínhos',
|
||||
'Chamusca',
|
||||
'Colares',
|
||||
'Coruche',
|
||||
'Fátima',
|
||||
'Figueira da Foz',
|
||||
'Golegã',
|
||||
'Gualtar',
|
||||
'Gândara',
|
||||
'Mação',
|
||||
'Mafra',
|
||||
'Montemor-o-Novo',
|
||||
'Montijo',
|
||||
'Odivelas',
|
||||
'Palmela',
|
||||
'Pataias',
|
||||
'Peniche',
|
||||
'Porto de Mós',
|
||||
'Reguengos de Monsaraz',
|
||||
'Ribatejo',
|
||||
'Sabugal',
|
||||
'Setúbal',
|
||||
'Tomar',
|
||||
'Torres Vedras',
|
||||
'Vila Franca de Xira'
|
||||
],
|
||||
Lisbon: [
|
||||
'Alenquer',
|
||||
'Arruda',
|
||||
'Carcavelos e Cascais',
|
||||
'Colares',
|
||||
'Encoro',
|
||||
'Estremoz',
|
||||
'Oeiras',
|
||||
'Palmela',
|
||||
'Setúbal'
|
||||
],
|
||||
Madeira: [
|
||||
'Machico',
|
||||
'Santa Cruz',
|
||||
'Caniço',
|
||||
'Santo António da Serra',
|
||||
'Ponta do Sol',
|
||||
'Calheta',
|
||||
'Funchal',
|
||||
'Câmara de Lobos',
|
||||
'Ribeira Brava',
|
||||
'Paul do Mar',
|
||||
'Canhas',
|
||||
'Arco da Calheta',
|
||||
'São Vicente',
|
||||
'Porto Moniz',
|
||||
'Paul da Serra'
|
||||
]
|
||||
},
|
||||
AT: {
|
||||
Wagram: ['Marchfeld'],
|
||||
Kremstal: ['Krems', 'Spitz'],
|
||||
Kamptal: ['Langenlois', 'Gumpoldskirchen'],
|
||||
Wachau: [],
|
||||
Thermenregion: ['Baden', 'Mödling'],
|
||||
Wienerwald: [],
|
||||
Südsteiermark: ['Grazer Bergland', 'Leibnitz', 'Deutschlandsberg'],
|
||||
Burgenland: ['Neusiedlersee', 'Eisenberg', 'Rosalia'],
|
||||
Styria: ['Südsteiermark', 'Weststeiermark'],
|
||||
Salzburg: ['Flachgau'],
|
||||
Tyrol: ['Innsbruck', 'Hall'],
|
||||
Carinthia: ['Villach', 'Klagenfurt']
|
||||
},
|
||||
HU: {
|
||||
Tokaj: [],
|
||||
Eger: [],
|
||||
Badacsony: [],
|
||||
Balaton: [
|
||||
'Balatonalmádi',
|
||||
'Balatonboglár',
|
||||
'Balatonfüred-Csopak',
|
||||
'Balatonlelle',
|
||||
'Balatonfüred',
|
||||
'Somló'
|
||||
],
|
||||
Mátra: [],
|
||||
Hegyalja: [],
|
||||
Pannon: ['Szekszárd', 'Pécs', 'Baja', 'Sopron'],
|
||||
Villány: []
|
||||
},
|
||||
CZ: {
|
||||
'South Moravia': ['Znojmo', 'Břeclav', 'Hodonín', 'Kyjov', 'Vranov'],
|
||||
'Zlín Region': ['Valašské Meziříčí', 'Kroměříž', 'Vsetín'],
|
||||
'Olomouc Region': ['Prostějov', 'Olomouc', 'Litomyšl'],
|
||||
'South Bohemia': ['České Budějovice', 'Písek', 'Strakonice'],
|
||||
Plzeň: ['Rokycany', 'Domažlice']
|
||||
},
|
||||
MD: ['Iași', 'Botoșani', 'Vaslui', 'Neamț'],
|
||||
RO: {
|
||||
Transylvania: ['Cluj', 'Sibiu', 'Brașov', 'Mureș'],
|
||||
Muntenia: ['Buzău', 'Dâmbovița', 'Prahova', 'Vrancea'],
|
||||
Oltenia: ['Dolj', 'Gorj', 'Vâlcea', 'Mehedinți'],
|
||||
Banat: ['Arad', 'Timiș', 'Caraș-Severin'],
|
||||
Dobrogea: ['Constanța', 'Tulcea'],
|
||||
Crisana: ['Bihor'],
|
||||
Maramureș: ['Suceava', 'Maramureș']
|
||||
},
|
||||
GR: {
|
||||
Attica: [],
|
||||
'Central Greece': [
|
||||
'Euboea',
|
||||
'Boeotia',
|
||||
'Phocis',
|
||||
'Locris',
|
||||
'Phthiotis',
|
||||
'Phocis',
|
||||
'Locris',
|
||||
'Phthiotis'
|
||||
],
|
||||
Peloponnese: [
|
||||
'Nemea',
|
||||
'Mantineia',
|
||||
'Achaia',
|
||||
'Messenia',
|
||||
'Laconia',
|
||||
'Arcadia',
|
||||
'Argolis',
|
||||
'Corinthia'
|
||||
],
|
||||
Thrace: [],
|
||||
Macedonia: [
|
||||
'Thessaloniki',
|
||||
'Kilkis',
|
||||
'Pella',
|
||||
'Florina',
|
||||
'Kastoria',
|
||||
'Imathia',
|
||||
'Pella',
|
||||
'Florina',
|
||||
'Kastoria'
|
||||
],
|
||||
Thessaly: ['Trikala', 'Larissa', 'Magnesia', 'Karditsa', 'Trikala'],
|
||||
'Ionian Islands': [
|
||||
'Corfu',
|
||||
'Zakynthos',
|
||||
'Kefalonia',
|
||||
'Lefkada',
|
||||
'Ithaca',
|
||||
'Paxos',
|
||||
'Corfu',
|
||||
'Zakynthos',
|
||||
'Kefalonia',
|
||||
'Lefkada',
|
||||
'Ithaca',
|
||||
'Paxos'
|
||||
],
|
||||
Cyclades: [
|
||||
'Santorini',
|
||||
'Paros',
|
||||
'Naxos',
|
||||
'Mykonos',
|
||||
'Sifnos',
|
||||
'Tinos',
|
||||
'Santorini',
|
||||
'Paros',
|
||||
'Naxos',
|
||||
'Mykonos',
|
||||
'Sifnos',
|
||||
'Tinos'
|
||||
],
|
||||
Crete: [],
|
||||
'Aegean Islands': [
|
||||
'Samos',
|
||||
'Lesbos',
|
||||
'Chios',
|
||||
'Limnos',
|
||||
'Ikaria',
|
||||
'Samos',
|
||||
'Lesbos',
|
||||
'Chios',
|
||||
'Limnos',
|
||||
'Ikaria'
|
||||
]
|
||||
},
|
||||
HR: {
|
||||
Dalmatia: ['Hvar', 'Brac', 'Korcula', 'Peljesac', 'Dubrovnik-Riviera'],
|
||||
Istria: [],
|
||||
Slavonia: [],
|
||||
Kvarner: ['Kvarner Bay', 'Labin'],
|
||||
'Continental Croatia': ['Srijem', 'Moslavina', 'Podravina'],
|
||||
'Central Croatia': ['Zagorje', 'Medgurje', 'Bilogora']
|
||||
},
|
||||
GE: {
|
||||
Kakheti: ['Telavi', 'Sighnaghi', 'Akhasheni', 'Kvareli'],
|
||||
Imereti: ['Kutaisi', 'Khoni', 'Tsqaltubo', 'Sachkhere'],
|
||||
'Racha-Lechkhumi': [],
|
||||
'Kvemo Svaneti': [],
|
||||
Kartli: ['Tbilisi', 'Gori', 'Kaspi', 'Rustavi'],
|
||||
Adjara: ['Batumi', 'Kobuleti'],
|
||||
'Samegrelo-Zemo Svaneti': ['Poti', 'Zugdidi'],
|
||||
Abkhazia: ['Gagra', 'Gudauta', 'Ochamchire'],
|
||||
'South Georgia': ['Adigeni', 'Akhalkalaki', 'Bolnisi', 'Ninotsminda']
|
||||
},
|
||||
IL: {
|
||||
Galilee: ['Golan Heights', 'Carmel Mountains'],
|
||||
'Central District': ['Shomron', 'Shfela'],
|
||||
Negev: ['Southern Negev', 'Central Negev'],
|
||||
'Coastal Plain': ['Central Coastal Plain', 'Southern Coastal Plain']
|
||||
},
|
||||
LB: ['Bekaa Valley', 'Mount Lebanon', 'North Lebanon', 'South Lebanon'],
|
||||
TR: {
|
||||
Anatolia: [
|
||||
'Konya',
|
||||
'Aksaray',
|
||||
'Nevşehir',
|
||||
'Erzurum',
|
||||
'Van',
|
||||
'Bitlis',
|
||||
'İzmir',
|
||||
'Manisa',
|
||||
'Ayvalik'
|
||||
],
|
||||
'Aegean Region': ['Urla', 'Foça', 'Bornova'],
|
||||
Ayvalik: [],
|
||||
'Marmara Region': ['İstanbul', 'Edirne'],
|
||||
'Black Sea Region': ['Rize', 'Artvin', 'Trabzon'],
|
||||
'Southeastern Anatolia': ['Gaziantep', 'Şanliurfa', 'Adana']
|
||||
},
|
||||
AM: ['Vayots Dzor', 'Ararat Valley', 'Gegharkunik', 'Tavush', 'Syunik'],
|
||||
CA: {
|
||||
Ontario: [
|
||||
'Niagara Peninsula',
|
||||
'Lake Erie North Shore',
|
||||
'Pelee Island',
|
||||
'Prince Edward County'
|
||||
],
|
||||
'British Columbia': [
|
||||
'Okanagan Valley',
|
||||
'Similkameen Valley',
|
||||
'Fraser Valley',
|
||||
'Southern Gulf Islands'
|
||||
],
|
||||
Quebec: ['Montreal', 'Eastern Townships', 'Charlevoix'],
|
||||
'Nova Scotia': ['Annapolis Valley', 'Gaspereau Valley', 'North Shore'],
|
||||
'New Brunswick': ['Annapolis Valley', 'Gaspereau Valley'],
|
||||
'Prince Edward Island': ['Eastern PEI', 'Western PEI']
|
||||
},
|
||||
US: {
|
||||
California: [
|
||||
'Napa Valley',
|
||||
'Sonoma Valley',
|
||||
'Paso Robles',
|
||||
'Santa Barbara County',
|
||||
'Central Coast',
|
||||
'Lodi',
|
||||
'Mendocino County',
|
||||
'Santa Cruz Mountains',
|
||||
'Russian River Valley'
|
||||
],
|
||||
Oregon: [
|
||||
'Willamette Valley',
|
||||
'Rogue Valley',
|
||||
'Umpqua Valley',
|
||||
'Southern Oregon',
|
||||
'Oregon Coast'
|
||||
],
|
||||
Washington: [
|
||||
'Columbia Valley',
|
||||
'Walla Walla Valley',
|
||||
'Red Mountain',
|
||||
'Snake River'
|
||||
],
|
||||
'New York': [
|
||||
'Finger Lakes',
|
||||
'Long Island',
|
||||
'Hudson River',
|
||||
'Western New York'
|
||||
],
|
||||
Virginia: [
|
||||
'Monticello',
|
||||
'Shenandoah Valley',
|
||||
'Northern Virginia',
|
||||
'Virginia Piedmont'
|
||||
],
|
||||
'Texas Hill Country': [],
|
||||
'Arizona Sonoita': [],
|
||||
'Colorado Grand Valley': [],
|
||||
'Idaho Snake River Valley': [],
|
||||
Michigan: ['Michigan Lake', 'Michigan Shore']
|
||||
},
|
||||
AR: {
|
||||
Mendoza: ['Luján de Cuyo', 'Uco Valley', 'Maipú', 'San Rafael'],
|
||||
Salta: ['Calchaquí Valleys', 'Quebrada de Humahuaca'],
|
||||
'San Juan': ['Ullum', 'Calingasta', 'Tulum'],
|
||||
'La Rioja': [],
|
||||
Catamarca: ['Andalgalá', 'Belén'],
|
||||
'San Luis': [
|
||||
'Valle del Conlara',
|
||||
'Valle del Potrerillos',
|
||||
'Valle de las Carreras'
|
||||
],
|
||||
'Buenos Aires': [
|
||||
'Partido de General Belgrano',
|
||||
'Partido de Lobos',
|
||||
'Partido de Tandil'
|
||||
],
|
||||
Neuquén: ['Valle de Río Negro', 'Valle de Agrelo']
|
||||
},
|
||||
CL: {
|
||||
'Central Valley': [
|
||||
'Maipo Valley',
|
||||
'Casablanca Valley',
|
||||
'San Antonio Valley',
|
||||
'Maule Valley',
|
||||
'Colchagua Valley'
|
||||
],
|
||||
'Coastal Regions': [
|
||||
'Valle de Aconcagua',
|
||||
'Valle de Limarí',
|
||||
'Valle de Elqui'
|
||||
],
|
||||
'Southern Chile': ['Valle de Itata', 'Valle de Bío-Bío'],
|
||||
'Northern Chile': ['Valle de Elqui']
|
||||
},
|
||||
ZA: {
|
||||
'Western Cape': {
|
||||
Stellenbosch: [
|
||||
'Simonsberg-Stellenbosch',
|
||||
'Jonkershoek-Stellenbosch',
|
||||
'Stellenbosch Mountain'
|
||||
],
|
||||
Paarl: ['Franschhoek', 'Paarl Mountain'],
|
||||
Constantia: ['Constantia Valley'],
|
||||
Durbanville: ['Durbanville Hills'],
|
||||
'Walker Bay': ['Hemel-en-Aarde'],
|
||||
Swartland: ['Paardeberg']
|
||||
},
|
||||
'Eastern Cape': ['Jeffreys Bay'],
|
||||
'Northern Cape': ['Augrabies']
|
||||
},
|
||||
AU: {
|
||||
'South Eastern Australia': [],
|
||||
'New South Wales': [
|
||||
'Hunter Valley',
|
||||
'Riverina',
|
||||
'Mudgee',
|
||||
'Orange',
|
||||
'Cowra',
|
||||
'Hilltops',
|
||||
'Tumbarumba',
|
||||
'Gundagai',
|
||||
'Shoalhaven Coast',
|
||||
'Southern Highlands',
|
||||
'New England'
|
||||
],
|
||||
'South Australia': [
|
||||
'Barossa Valley',
|
||||
'McLaren Vale',
|
||||
'Clare Valley',
|
||||
'Coonawarra',
|
||||
'Adelaide Hills',
|
||||
'Eden Valley',
|
||||
'Langhorne Creek',
|
||||
'Padthaway',
|
||||
'Kangaroo Island',
|
||||
'Mount Gambier',
|
||||
'Robe',
|
||||
'Wrattonbully',
|
||||
'Fleurieu Peninsula',
|
||||
'Currency Creek',
|
||||
'Southern Fleurieu'
|
||||
],
|
||||
Victoria: [
|
||||
'Yarra Valley',
|
||||
'Mornington Peninsula',
|
||||
'Geelong',
|
||||
'Bellarine Peninsula',
|
||||
'Sunbury',
|
||||
'Macedon Ranges',
|
||||
'Heathcote',
|
||||
'Bendigo',
|
||||
'Pyrenees',
|
||||
'Grampians',
|
||||
'Great Western',
|
||||
'Henty',
|
||||
'Otway Ranges',
|
||||
'King Valley',
|
||||
'Alpine Valleys',
|
||||
'Rutherglen',
|
||||
'Glenrowan',
|
||||
'Goulburn Valley',
|
||||
'Strathbogie Ranges'
|
||||
],
|
||||
'Western Australia': [
|
||||
'Margaret River',
|
||||
'Great Southern',
|
||||
'Swan Valley',
|
||||
'Pemberton',
|
||||
'Manjimup',
|
||||
'Blackwood Valley',
|
||||
'Geographe',
|
||||
'Peel',
|
||||
'Perth Hills',
|
||||
'Frankland River',
|
||||
'Mount Barker',
|
||||
'Porongurup',
|
||||
'Denmark',
|
||||
'Albany',
|
||||
'Esperance'
|
||||
],
|
||||
Tasmania: [
|
||||
'Tamar Valley',
|
||||
'Pipers River',
|
||||
'East Coast',
|
||||
'Coal Valley',
|
||||
'Derwent Valley',
|
||||
'Huon Valley'
|
||||
],
|
||||
Queensland: [
|
||||
'Granite Belt',
|
||||
'South Burnett',
|
||||
'Darling Downs',
|
||||
'Scenic Rim'
|
||||
],
|
||||
'Canberra District': [
|
||||
'Canberra',
|
||||
'Murrumbateman',
|
||||
'Yass Valley',
|
||||
'Gundaroo',
|
||||
'Lake George'
|
||||
]
|
||||
},
|
||||
NZ: {
|
||||
'North Island': [
|
||||
'Auckland',
|
||||
'Gisborne',
|
||||
`Hawke's Bay`,
|
||||
'Wairarapa',
|
||||
'Bay of Islands',
|
||||
'Coromandel',
|
||||
'Waikato'
|
||||
],
|
||||
'South Island': [
|
||||
'Marlborough',
|
||||
'Nelson',
|
||||
'Waipara',
|
||||
'Canterbury',
|
||||
'Central Otago',
|
||||
'Waipara Valley',
|
||||
'North Canterbury',
|
||||
'North Otago'
|
||||
]
|
||||
},
|
||||
CN: {
|
||||
Ningxia: {
|
||||
Yinchuan: [],
|
||||
'Helan Mountain': ['Qingtongxia', 'Wucaiwan', 'Qingshuihe', 'Jinfeng']
|
||||
},
|
||||
Shaanxi: ['Lishan', 'Fengxiang'],
|
||||
Shandong: ['Yantai', 'Penglai', 'Weihai'],
|
||||
Xinjiang: ['Turpan', 'Yili', 'Jinghe'],
|
||||
Hebei: ['Changli', 'Qinhuangdao'],
|
||||
Heilongjiang: ['Yichun', 'Qiqihar'],
|
||||
Jilin: ['Baishan', 'Changchun'],
|
||||
Liaoning: ['Dalian', 'Shenyang'],
|
||||
'Inner Mongolia': ['Hohhot', 'Baotou'],
|
||||
Guangxi: ['Nanning', 'Liuzhou'],
|
||||
Hunan: ['Changsha', 'Yongzhou'],
|
||||
Hubei: ['Wuhan'],
|
||||
Sichuan: ['Chengdu', `Ya'an`],
|
||||
Guizhou: ['Guiyang', 'Zunyi'],
|
||||
Yunnan: ['Kunming', 'Dali']
|
||||
},
|
||||
JP: {
|
||||
'Yamanashi Prefecture': ['Kofu Basin', 'Katsunuma', 'Fuefuki'],
|
||||
'Hokkaido Prefecture': ['Biei', 'Tomakomai', 'Nanae'],
|
||||
'Niigata Prefecture': ['Uonuma', 'Sannosawa'],
|
||||
'Nagano Prefecture': ['Kiso Valley', 'Nagano'],
|
||||
'Gifu Prefecture': ['Gujo', 'Mino'],
|
||||
'Aichi Prefecture': ['Nishio', 'Seto'],
|
||||
'Shizuoka Prefecture': ['Suruga', 'Shizuoka'],
|
||||
'Kanagawa Prefecture': ['Yokohama', 'Kawasaki'],
|
||||
'Chiba Prefecture': ['Noda', 'Chiba'],
|
||||
'Ibaraki Prefecture': ['Tsukuba', 'Hitachi'],
|
||||
'Tochigi Prefecture': ['Utsunomiya', 'Tochigi'],
|
||||
'Gunma Prefecture': ['Maebashi', 'Gunma']
|
||||
},
|
||||
UK: {
|
||||
Kent: ['Ashford', 'Canterbury', 'Dover', 'Maidstone', 'Sevenoaks'],
|
||||
Sussex: ['Brighton', 'Eastbourne', 'Hastings', 'Chichester', 'Horsham'],
|
||||
Hampshire: ['Winchester', 'Petersfield', 'Basingstoke', 'Andover'],
|
||||
Wiltshire: ['Swindon', 'Chippenham', 'Salisbury', 'Trowbridge'],
|
||||
Dorset: ['Poole', 'Bournemouth', 'Dorset'],
|
||||
Devon: ['Exeter', 'Newton Abbot', 'Plymouth', 'Torquay'],
|
||||
Somerset: ['Bath', 'Weston-super-Mare', 'Taunton', 'Yeovil'],
|
||||
Gloucestershire: ['Cheltenham', 'Gloucester', 'Cirencester', 'Stroud'],
|
||||
Worcestershire: ['Worcester', 'Malvern', 'Kidderminster', 'Bewdley'],
|
||||
Warwickshire: [
|
||||
'Warwick',
|
||||
'Stratford-upon-Avon',
|
||||
'Leamington Spa',
|
||||
'Kenilworth'
|
||||
],
|
||||
Herefordshire: ['Hereford', 'Leominster', 'Ross-on-Wye', 'Kington'],
|
||||
Shropshire: ['Shrewsbury', 'Telford', 'Ludlow', 'Oswestry']
|
||||
},
|
||||
UY: {
|
||||
'Canelones Department': ['San Carlos', 'Melo', 'Paso de los Toros'],
|
||||
'Montevideo Department': ['Carrasco', 'Punta del Este'],
|
||||
'Colonia Department': ['Colonia del Sacramento', 'Villa Soriano'],
|
||||
'San José Department': ['Progreso', 'Treinta y Tres'],
|
||||
'Rocha Department': ['La Paloma', 'Rocha Sur'],
|
||||
'Florida Department': ['Florida', 'Sarandí del Yí'],
|
||||
'Soriano Department': ['Soriano', 'Río Negro'],
|
||||
'Tacuarembó Department': ['Tacuarembó', 'Paso de los Toros'],
|
||||
'Treinta y Tres Department': ['Treinta y Tres', 'Rivera']
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user