Compare commits
No commits in common. "staging" and "payload-validation" have entirely different histories.
staging
...
payload-va
.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
8
.vscode/settings.json
vendored
8
.vscode/settings.json
vendored
@ -10,12 +10,8 @@
|
||||
"biodynamic",
|
||||
"Blanco",
|
||||
"Cachaça",
|
||||
"Caramelised",
|
||||
"Caturra",
|
||||
"colour",
|
||||
"Colours",
|
||||
"Coren",
|
||||
"EAN",
|
||||
"espadín",
|
||||
"Genever",
|
||||
"Jägermeister",
|
||||
@ -45,9 +41,7 @@
|
||||
"Reserva",
|
||||
"Robusta",
|
||||
"RRP",
|
||||
"schnorr",
|
||||
"screwcap",
|
||||
"SKU",
|
||||
"Soju",
|
||||
"Sokujō",
|
||||
"Solera",
|
||||
@ -56,8 +50,6 @@
|
||||
"Tequilana",
|
||||
"tobalá",
|
||||
"Typica",
|
||||
"Umami",
|
||||
"UPC",
|
||||
"Verte",
|
||||
"VSOP",
|
||||
"Yamahai",
|
||||
|
85
package-lock.json
generated
85
package-lock.json
generated
@ -7,11 +7,8 @@
|
||||
"": {
|
||||
"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",
|
||||
@ -28,9 +25,8 @@
|
||||
"@semantic-release/git": "^10.0.1",
|
||||
"@semantic-release/npm": "^12.0.1",
|
||||
"@semantic-release/release-notes-generator": "^14.0.3",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/express": "^5.0.1",
|
||||
"@types/node": "^22.14.1",
|
||||
"@types/node": "^22.13.12",
|
||||
"concurrently": "^9.1.2",
|
||||
"eslint": "^9.23.0",
|
||||
"globals": "^16.0.0",
|
||||
@ -435,27 +431,24 @@
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"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.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
"@noble/hashes": "1.3.2"
|
||||
},
|
||||
"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==",
|
||||
"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": "^14.21.3 || >=16"
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
@ -473,15 +466,6 @@
|
||||
"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",
|
||||
@ -1686,13 +1670,6 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/crypto-js": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz",
|
||||
"integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
|
||||
@ -1782,13 +1759,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz",
|
||||
"integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==",
|
||||
"version": "22.13.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.12.tgz",
|
||||
"integrity": "sha512-ixiWrCSRi33uqBMRuICcKECW7rtgY43TbsHDpM2XK7lXispd48opW+0IXrBVxv9NMhaz/Ue9kyj6r3NTVyXm8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
"undici-types": "~6.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/normalize-package-data": {
|
||||
@ -3101,12 +3078,6 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/crypto-js": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
|
||||
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/crypto-random-string": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz",
|
||||
@ -6463,30 +6434,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@ -11564,9 +11511,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
||||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
|
@ -27,8 +27,6 @@
|
||||
"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",
|
||||
@ -45,9 +43,8 @@
|
||||
"@semantic-release/git": "^10.0.1",
|
||||
"@semantic-release/npm": "^12.0.1",
|
||||
"@semantic-release/release-notes-generator": "^14.0.3",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/express": "^5.0.1",
|
||||
"@types/node": "^22.14.1",
|
||||
"@types/node": "^22.13.12",
|
||||
"concurrently": "^9.1.2",
|
||||
"eslint": "^9.23.0",
|
||||
"globals": "^16.0.0",
|
||||
|
19
src/index.ts
19
src/index.ts
@ -10,8 +10,7 @@ import {
|
||||
spiritsRouter,
|
||||
coffeeRouter
|
||||
} from './routes'
|
||||
import { Route } from './types'
|
||||
import { authorizeRequest } from './middlewares'
|
||||
import { Routes } from './types'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
@ -20,15 +19,13 @@ const port = process.env.PORT || 3000
|
||||
|
||||
connectToDatabase()
|
||||
.then(() => {
|
||||
app.use(authorizeRequest)
|
||||
|
||||
app.use(Route.Users, usersRouter)
|
||||
app.use(Route.NostrEvents, nostrRouter)
|
||||
app.use(Route.Reviews, reviewsRouter)
|
||||
app.use(Route.Wines, winesRouter)
|
||||
app.use(Route.Sake, sakeRouter)
|
||||
app.use(Route.Spirits, spiritsRouter)
|
||||
app.use(Route.Coffee, coffeeRouter)
|
||||
app.use(Routes.Users, usersRouter)
|
||||
app.use(Routes.NostrEvents, nostrRouter)
|
||||
app.use(Routes.Reviews, reviewsRouter)
|
||||
app.use(Routes.Wines, winesRouter)
|
||||
app.use(Routes.Sake, sakeRouter)
|
||||
app.use(Routes.Spirits, spiritsRouter)
|
||||
app.use(Routes.Coffee, coffeeRouter)
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Server started at http://localhost:${port}`)
|
||||
|
@ -1,71 +0,0 @@
|
||||
import { RequestHandler } from 'express'
|
||||
import { NostrEvent } from 'nostr-tools'
|
||||
import { decodeBase64 } from '../utils/coding'
|
||||
import { verifyNostrSignature } from '../utils'
|
||||
import { collections } from '../services/database.service'
|
||||
import { DBcollection } from '../types'
|
||||
import { User } from '../models'
|
||||
|
||||
export const authorizeRequest: RequestHandler = async (req, res, next) => {
|
||||
const { authorization } = req.headers
|
||||
|
||||
if (!authorization) {
|
||||
res.sendStatus(401)
|
||||
} else {
|
||||
const removeNostrPrefix = (str: string) => str.replace('Nostr ', '')
|
||||
|
||||
const decodedString = decodeBase64(removeNostrPrefix(authorization))
|
||||
|
||||
try {
|
||||
const event: NostrEvent = JSON.parse(decodedString)
|
||||
|
||||
// verify Nostr signature
|
||||
const verified = await verifyNostrSignature(event)
|
||||
|
||||
if (!verified) {
|
||||
throw new Error('Nostr signature is not valid.')
|
||||
}
|
||||
|
||||
const { pubkey } = event
|
||||
const { locals } = res
|
||||
|
||||
const checkIfUserExists = async () => {
|
||||
// collection of {url}:{method} strings that represent routes that do not require user existence check
|
||||
const skipUserExistCheck = ['/users:POST']
|
||||
|
||||
const { url, method } = req
|
||||
|
||||
if (!skipUserExistCheck.includes(`${url}:${method}`)) {
|
||||
const existingUser = await collections[
|
||||
DBcollection.Users
|
||||
]?.findOne<User>({
|
||||
npub: pubkey
|
||||
})
|
||||
|
||||
if (!existingUser) {
|
||||
throw new Error('User does not exist.')
|
||||
}
|
||||
|
||||
locals.userId = existingUser._id
|
||||
locals.userRole = existingUser.role
|
||||
}
|
||||
}
|
||||
|
||||
await checkIfUserExists()
|
||||
|
||||
locals.npub = pubkey
|
||||
|
||||
next()
|
||||
|
||||
// TODO:
|
||||
// 0. verify kind
|
||||
// 1. verify tags
|
||||
// 2. verify content
|
||||
// 3. verify that event is not older than 5mins
|
||||
} catch (error) {
|
||||
console.error(`Error while processing Authorization header.`, error)
|
||||
|
||||
res.sendStatus(401)
|
||||
}
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
export * from './authorize'
|
@ -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 name: string, // label
|
||||
public producerId: ObjectId, // product producer
|
||||
public variety: CoffeeVariety, // variety type and kind
|
||||
public processingType: CoffeeProcessingType, // processing type
|
||||
public name: string, // label
|
||||
public producerId: ObjectId, // product producer
|
||||
public roast: CoffeeRoast, // roast level
|
||||
public RRPamount: number, // 20
|
||||
public RRPcurrency: CurrencyCode, // USD
|
||||
|
@ -7,6 +7,6 @@ export class NostrEvent {
|
||||
public kind: number, // event type, e.g., review, article, comment
|
||||
public tags: string[][], // array of keywords or hashtags
|
||||
public content: string, // text content of the event
|
||||
public _id?: ObjectId // database object id
|
||||
public id?: ObjectId // database object id
|
||||
) {}
|
||||
}
|
||||
|
@ -1,17 +1,12 @@
|
||||
import { ObjectId } from 'mongodb'
|
||||
import { ProductType, RatingOption, TastingNote } from '../types'
|
||||
|
||||
export class Review {
|
||||
constructor(
|
||||
public nostrId: string, // unique identifier for the Nostr Event
|
||||
public nostrEventId: ObjectId, // unique identifier for the NostrEvent DB instance
|
||||
public productId: ObjectId, // unique identifier for the Product DB instance
|
||||
public reviewerId: ObjectId, // unique identifier for the User DB instance
|
||||
public productType: ProductType, // product type
|
||||
public rating: number | RatingOption, // numerical rating, e.g., 84-100 or NS (no score)
|
||||
public 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 reviewText: string, // text content of the review
|
||||
public tastingNote: TastingNote, // an object representing tasting notes
|
||||
public _id?: ObjectId, // database object id
|
||||
public id?: string // string representing database object id
|
||||
public tastingNotes: string[], // array of tasting notes, e.g., flavours, aromas
|
||||
public id?: ObjectId // database object id
|
||||
) {}
|
||||
}
|
||||
|
@ -1,15 +1,5 @@
|
||||
import { ObjectId } from 'mongodb'
|
||||
import {
|
||||
SakeDesignation,
|
||||
SakeStarter,
|
||||
VintageOption,
|
||||
SakeCharacteristic,
|
||||
StandardDrink,
|
||||
SakeVolume,
|
||||
RiceVarietal,
|
||||
SakeYeastStrain,
|
||||
SakeKoji
|
||||
} from '../types'
|
||||
import { SakeDesignation, SakeStarter, VintageOptions } from '../types'
|
||||
import { Alpha2Code } from 'i18n-iso-countries'
|
||||
import { CurrencyCode } from 'currency-codes-ts/dist/types'
|
||||
|
||||
@ -22,23 +12,18 @@ export class Sake {
|
||||
public region: string, // appellation, village, sub-region, vineyard
|
||||
public name: string, // label
|
||||
public producerId: ObjectId, // product producer
|
||||
public designation: SakeDesignation, // table, pure, blended
|
||||
public designation: SakeDesignation, // table, pure, blended, mirin: new/true/salt
|
||||
public polishRate: number, // %
|
||||
public characteristic: SakeCharacteristic,
|
||||
public starter: SakeStarter, // sake starter
|
||||
public yeastStrain: SakeYeastStrain,
|
||||
public volume: SakeVolume, // bottle volume
|
||||
public yeastStrain: number,
|
||||
public alcohol: number, // alcohol percentage
|
||||
public standardDrinks: StandardDrink, // number representing an amount of standard drinks per bottle per 100ml
|
||||
public riceVarietal: RiceVarietal[], // if more than one, list as 'blend'
|
||||
public koji: SakeKoji, // if more than one, list as 'blend'
|
||||
public vintage: number | VintageOption, // year, nv (non-vintage) or mv (multi-vintage)
|
||||
public standardDrinks100ml: number, // number representing an amount of standard drinks per bottle per 100ml
|
||||
public vintage: number | VintageOptions, // year, nv (non-vintage) or mv (multi-vintage)
|
||||
public RRPamount: number, // 20
|
||||
public RRPcurrency: CurrencyCode, // USD
|
||||
public description: string, // detailed description of the product
|
||||
public url?: string, // e.g. producer's website
|
||||
public images?: string[], // (optional image URL)cellar.social
|
||||
public _id?: ObjectId, // database object id
|
||||
public id?: string // string representing database object id
|
||||
public image?: string, // (optional image URL)cellar.social
|
||||
public id?: ObjectId // database object id
|
||||
) {}
|
||||
}
|
||||
|
@ -1,13 +1,5 @@
|
||||
import { ObjectId } from 'mongodb'
|
||||
import {
|
||||
SpiritType,
|
||||
SpiritVariant,
|
||||
Ingredient,
|
||||
VintageOption,
|
||||
StandardDrink,
|
||||
SpiritVolume,
|
||||
SpiritCharacteristic
|
||||
} from '../types'
|
||||
import { SpiritType, SpiritVariant, Ingredient, VintageOptions } from '../types'
|
||||
import { Alpha2Code } from 'i18n-iso-countries'
|
||||
import { CurrencyCode } from 'currency-codes-ts/dist/types'
|
||||
|
||||
@ -22,18 +14,15 @@ export class Spirit {
|
||||
public producerId: ObjectId, // product producer
|
||||
public type: SpiritType, // spirit type
|
||||
public variant: SpiritVariant, // vodka, rum, liqueur cream, etc
|
||||
public characteristic: SpiritCharacteristic, // light aromatic, textural, fruit forward, structural & savoury, powerful
|
||||
public ingredients: Ingredient[], // an array of ingredients(flavouring)
|
||||
public volume: SpiritVolume, // bottle volume
|
||||
public alcohol: number, // alcohol percentage
|
||||
public vintage: number | VintageOption, // year, nv (non-vintage) or mv (multi-vintage)
|
||||
public standardDrinks: StandardDrink, // an amount of standard drinks per 100ml and bottle in AU, UK and US
|
||||
public standardDrinks100ml: number, // number representing an amount of standard drinks per bottle
|
||||
public vintage: number | VintageOptions, // year, nv (non-vintage) or mv (multi-vintage)
|
||||
public RRPamount: number, // 20
|
||||
public RRPcurrency: CurrencyCode, // USD
|
||||
public description: string, // detailed description of the product
|
||||
public url?: string, // e.g. producer's website
|
||||
public images?: string[], // (optional image URL)cellar.social
|
||||
public _id?: ObjectId, // database object id
|
||||
public id?: string // string representing database object id
|
||||
public image?: string, // (optional image URL)cellar.social
|
||||
public id?: ObjectId // database object id
|
||||
) {}
|
||||
}
|
||||
|
@ -3,9 +3,9 @@ import { UserRole } from '../types'
|
||||
|
||||
export class User {
|
||||
constructor(
|
||||
public name: string, // name, changeable
|
||||
public name: string, // name
|
||||
public npub: string, // npub
|
||||
public role: UserRole, // user role (user, reviewer, producer)
|
||||
public _id: ObjectId // database object id
|
||||
public id?: ObjectId // database object id
|
||||
) {}
|
||||
}
|
||||
|
@ -3,16 +3,10 @@ import {
|
||||
WineType,
|
||||
Viticulture,
|
||||
BottleClosure,
|
||||
VintageOption,
|
||||
StandardDrink,
|
||||
VintageOptions,
|
||||
StandardDrinks,
|
||||
WineRegion,
|
||||
WineVolume,
|
||||
WineStyle,
|
||||
WhiteWineCharacteristic,
|
||||
AmberWineCharacteristic,
|
||||
RoseWineCharacteristic,
|
||||
RedWineCharacteristic,
|
||||
GrapeVarietal
|
||||
WineVolume
|
||||
} from '../types'
|
||||
import { Alpha2Code } from 'i18n-iso-countries'
|
||||
import { CurrencyCode } from 'currency-codes-ts/dist/types'
|
||||
@ -23,22 +17,17 @@ export class Wine {
|
||||
public productCodeUPC: string, // Product Code (https://en.wikipedia.org/wiki/Universal_Product_Code)
|
||||
public productCodeSKU: string, // Stock keeping unit (https://en.wikipedia.org/wiki/Stock_keeping_unit)
|
||||
public type: WineType, // numerical rating, e.g., 1-100
|
||||
public style: WineStyle, // bubbles+fizz, table, dessert, fortified, vermouth
|
||||
public characteristic: (
|
||||
| WhiteWineCharacteristic
|
||||
| AmberWineCharacteristic
|
||||
| RoseWineCharacteristic
|
||||
| RedWineCharacteristic
|
||||
)[], // light aromatic, textural, fruit forward, structural & savoury, powerful
|
||||
public style: string, // bubbles+fizz, table, dessert, fortified, vermouth
|
||||
public characteristic: string, // light aromatic, textural, fruit forward, structural & savoury, powerful
|
||||
public country: Alpha2Code, // two-letter country codes defined in ISO 3166-1 (https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)
|
||||
public region: WineRegion, // appellation, village, sub-region, vineyard
|
||||
public name: string, // label
|
||||
public producerId: ObjectId, // product producer
|
||||
public grapeVarietal: GrapeVarietal[], // if more than one, list as 'blend'
|
||||
public vintage: number | VintageOption, // year, nv (non-vintage) or mv (multi-vintage)
|
||||
public varietal: string, // if more than one, list as 'blend'
|
||||
public vintage: number | VintageOptions, // year, nv (non-vintage) or mv (multi-vintage)
|
||||
public volume: WineVolume, // bottle volume
|
||||
public alcohol: number, // alcohol percentage
|
||||
public standardDrinks: StandardDrink, // an amount of standard drinks per 100ml and bottle in AU, UK and US
|
||||
public standardDrinks: StandardDrinks, // 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))
|
||||
@ -49,8 +38,7 @@ export class Wine {
|
||||
public RRPcurrency: CurrencyCode, // USD
|
||||
public description: string, // detailed description of the product
|
||||
public url?: string, // e.g. producer's website
|
||||
public images?: string[], // (optional image URL)cellar.social
|
||||
public _id?: ObjectId, // database object id
|
||||
public id?: string // string representing database object id
|
||||
public image?: string, // (optional image URL)cellar.social
|
||||
public id?: ObjectId // database object id
|
||||
) {}
|
||||
}
|
||||
|
@ -1,15 +1,6 @@
|
||||
import express, { Request, Response } from 'express'
|
||||
import { collections } from '../services/database.service'
|
||||
import { Coffee } from '../models'
|
||||
import {
|
||||
coffeeValidation,
|
||||
productCodeValidation,
|
||||
handleReqError,
|
||||
handleReqSuccess,
|
||||
handleReqNotModified
|
||||
} from '../utils'
|
||||
import Joi from 'joi'
|
||||
import { DBcollection, DBinstance, HTTPmethod, ResponseStatus } from '../types'
|
||||
|
||||
export const coffeeRouter = express.Router()
|
||||
|
||||
@ -33,39 +24,22 @@ coffeeRouter.get('/', async (_req: Request, res: Response) => {
|
||||
// POST
|
||||
coffeeRouter.post('/', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const {
|
||||
error,
|
||||
value: coffee
|
||||
}: { error: Joi.ValidationError | undefined; value: Coffee } =
|
||||
coffeeValidation(req.body)
|
||||
const coffee = req.body as 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)
|
||||
const result = await collections.coffee?.insertOne(coffee)
|
||||
|
||||
if (result) {
|
||||
handleReqSuccess(
|
||||
res,
|
||||
DBinstance.Coffee,
|
||||
result.insertedId.toString(),
|
||||
HTTPmethod.POST
|
||||
)
|
||||
res
|
||||
.status(201)
|
||||
.send(`Successfully created a new coffee with id ${result.insertedId}`)
|
||||
} else {
|
||||
handleReqNotModified(res)
|
||||
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)
|
||||
}
|
||||
} catch (err) {
|
||||
handleReqError(res, err, ResponseStatus.BadRequest)
|
||||
}
|
||||
})
|
||||
|
@ -4,12 +4,10 @@ import { NostrEvent } from '../models'
|
||||
import {
|
||||
nostrEventValidation,
|
||||
handleReqError,
|
||||
handleReqSuccess,
|
||||
handleReqNotModified
|
||||
handleReqSuccess
|
||||
} from '../utils'
|
||||
import { Event } from 'nostr-tools'
|
||||
import Joi from 'joi'
|
||||
import { DBinstance, HTTPmethod, ResponseStatus } from '../types'
|
||||
|
||||
export const nostrRouter = express.Router()
|
||||
|
||||
@ -64,20 +62,8 @@ nostrRouter.post('/', async (req: Request, res: Response) => {
|
||||
|
||||
const result = await collections.nostrEvents?.insertOne(nostrEvent)
|
||||
|
||||
if (result) {
|
||||
handleReqSuccess(
|
||||
res,
|
||||
DBinstance.NostrEvent,
|
||||
result.insertedId.toString(),
|
||||
HTTPmethod.POST
|
||||
)
|
||||
} else {
|
||||
handleReqNotModified(res)
|
||||
}
|
||||
} catch (err) {
|
||||
handleReqError(res, err, ResponseStatus.BadRequest)
|
||||
handleReqSuccess(res, result, 'nostrEvent')
|
||||
} catch (error: unknown) {
|
||||
handleReqError(res, error)
|
||||
}
|
||||
})
|
||||
|
||||
// TODO:
|
||||
// Add delete route
|
||||
|
@ -1,41 +1,26 @@
|
||||
import express, { Request, Response } from 'express'
|
||||
import { collections } from '../services/database.service'
|
||||
import { Review, Sake, Spirit, User, Wine, NostrEvent } from '../models'
|
||||
import {
|
||||
reviewValidation,
|
||||
handleReqError,
|
||||
handleReqSuccess,
|
||||
handleReqNotModified,
|
||||
handleGETreq,
|
||||
handleReqUnauthorized,
|
||||
handleReqNotFound,
|
||||
compareObjects,
|
||||
idValidation,
|
||||
modificationPeriodExpired,
|
||||
handleProductDELETEreq
|
||||
} from '../utils'
|
||||
import { Review } from '../models'
|
||||
import { reviewValidation, handleReqError, handleReqSuccess } from '../utils'
|
||||
import Joi from 'joi'
|
||||
import {
|
||||
DBcollection,
|
||||
HTTPmethod,
|
||||
ResponseStatus,
|
||||
TastingNoteKey,
|
||||
DBinstance,
|
||||
UserRole,
|
||||
ProductType
|
||||
} from '../types'
|
||||
import { ObjectId } from 'mongodb'
|
||||
|
||||
export const reviewsRouter = express.Router()
|
||||
|
||||
const dbInstance = DBinstance.Review
|
||||
const dbCollection = DBcollection.Reviews
|
||||
|
||||
reviewsRouter.use(express.json())
|
||||
|
||||
// GET
|
||||
reviewsRouter.get('/', async (_req: Request, res: Response) => {
|
||||
await handleGETreq(res, dbCollection, dbInstance)
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// POST
|
||||
@ -51,257 +36,18 @@ reviewsRouter.post('/', async (req: Request, res: Response) => {
|
||||
throw error.details[0].message
|
||||
}
|
||||
|
||||
// user check
|
||||
const { userId } = res.locals
|
||||
|
||||
const user = await collections[DBcollection.Users]?.findOne<User>({
|
||||
_id: userId
|
||||
const existingReview = await collections.reviews?.findOne({
|
||||
eventId: review.eventId
|
||||
})
|
||||
|
||||
if ((user && user.role !== UserRole.Reviewer) || !user?._id) {
|
||||
handleReqUnauthorized(res)
|
||||
} else {
|
||||
review.reviewerId = user?._id
|
||||
|
||||
const { nostrId, productId, productType } = review
|
||||
|
||||
// nostr event check
|
||||
const existingNostrEvent = await collections[
|
||||
DBcollection.NostrEvents
|
||||
]?.findOne<NostrEvent>({
|
||||
nostrId
|
||||
})
|
||||
|
||||
if (!existingNostrEvent || !existingNostrEvent._id) {
|
||||
throw new Error('associated nostr event not found')
|
||||
}
|
||||
|
||||
review.nostrEventId = existingNostrEvent._id
|
||||
|
||||
const existingReviewWithNostrEventId = await collections[
|
||||
dbCollection
|
||||
]?.findOne({
|
||||
nostrEventId: existingNostrEvent._id
|
||||
})
|
||||
|
||||
if (existingReviewWithNostrEventId) {
|
||||
throw new Error('review with provided "nostrEventId" exists')
|
||||
}
|
||||
|
||||
let product: Wine | Sake | Spirit | null | undefined = undefined
|
||||
|
||||
switch (productType) {
|
||||
case ProductType.Wine:
|
||||
product = await collections[DBcollection.Wines]?.findOne<Wine>({
|
||||
_id: new ObjectId(productId)
|
||||
})
|
||||
|
||||
break
|
||||
|
||||
case ProductType.Sake:
|
||||
product = await collections[DBcollection.Sake]?.findOne<Sake>({
|
||||
_id: new ObjectId(productId)
|
||||
})
|
||||
|
||||
break
|
||||
|
||||
case ProductType.Spirit:
|
||||
product = await collections[DBcollection.Spirits]?.findOne<Spirit>({
|
||||
_id: new ObjectId(productId)
|
||||
})
|
||||
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if (!product || !product._id) {
|
||||
throw new Error('product not found')
|
||||
}
|
||||
|
||||
review.productId = product._id
|
||||
|
||||
// Add age property to tasting note, if product has vintage property
|
||||
const { vintage } = product
|
||||
|
||||
if (typeof vintage === 'number') {
|
||||
review.tastingNote[TastingNoteKey.TextureAndBalance].age =
|
||||
new Date().getFullYear() - vintage
|
||||
}
|
||||
|
||||
delete review.id
|
||||
|
||||
const result = await collections[dbCollection]?.insertOne(review)
|
||||
|
||||
if (result) {
|
||||
handleReqSuccess(
|
||||
res,
|
||||
dbInstance,
|
||||
result.insertedId.toString(),
|
||||
HTTPmethod.POST
|
||||
)
|
||||
} else {
|
||||
handleReqNotModified(res)
|
||||
}
|
||||
if (existingReview) {
|
||||
throw new Error('review with provided "eventId" exists')
|
||||
}
|
||||
} catch (err) {
|
||||
handleReqError(res, err, ResponseStatus.BadRequest)
|
||||
|
||||
const result = await collections.reviews?.insertOne(review)
|
||||
|
||||
handleReqSuccess(res, result, 'review')
|
||||
} catch (error: unknown) {
|
||||
handleReqError(res, error)
|
||||
}
|
||||
})
|
||||
|
||||
// PUT
|
||||
reviewsRouter.put('/', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const {
|
||||
error,
|
||||
value: newReview
|
||||
}: { error: Joi.ValidationError | undefined; value: Review } =
|
||||
reviewValidation(req.body)
|
||||
|
||||
if (error) {
|
||||
throw error.details[0].message
|
||||
}
|
||||
|
||||
// user check
|
||||
const { userId } = res.locals
|
||||
|
||||
const user = await collections[DBcollection.Users]?.findOne<User>({
|
||||
_id: userId
|
||||
})
|
||||
|
||||
if ((user && user.role !== UserRole.Reviewer) || !user?._id) {
|
||||
handleReqUnauthorized(res)
|
||||
} else {
|
||||
newReview.reviewerId = user?._id
|
||||
|
||||
const { id, nostrId, productId, productType } = newReview
|
||||
|
||||
idValidation(id)
|
||||
|
||||
const existingReview = await collections[dbCollection]?.findOne<Review>({
|
||||
_id: new ObjectId(id)
|
||||
})
|
||||
|
||||
if (!existingReview) {
|
||||
handleReqNotFound(res, dbInstance)
|
||||
} else {
|
||||
// check creation timestamp
|
||||
const creationTimestamp = existingReview._id!.getTimestamp().getTime()
|
||||
|
||||
if (modificationPeriodExpired(creationTimestamp)) {
|
||||
handleReqNotModified(res)
|
||||
}
|
||||
|
||||
// productId check
|
||||
if (existingReview?.productId.toString() !== `${newReview.productId}`) {
|
||||
throw new Error('"productId" is not valid')
|
||||
}
|
||||
|
||||
// productType check
|
||||
if (existingReview?.productType !== newReview.productType) {
|
||||
throw new Error('"productType" is not valid')
|
||||
}
|
||||
|
||||
if (existingReview?.nostrId === nostrId) {
|
||||
throw new Error('Nostr id has to be different')
|
||||
}
|
||||
|
||||
// nostr event check
|
||||
const existingNostrEvent = await collections[
|
||||
DBcollection.NostrEvents
|
||||
]?.findOne<NostrEvent>({
|
||||
nostrId
|
||||
})
|
||||
|
||||
if (!existingNostrEvent || !existingNostrEvent._id) {
|
||||
throw new Error('associated nostr event not found')
|
||||
}
|
||||
|
||||
let product: Wine | Sake | Spirit | null | undefined = undefined
|
||||
|
||||
switch (productType) {
|
||||
case ProductType.Wine:
|
||||
product = await collections[DBcollection.Wines]?.findOne<Wine>({
|
||||
_id: new ObjectId(productId)
|
||||
})
|
||||
|
||||
break
|
||||
|
||||
case ProductType.Sake:
|
||||
product = await collections[DBcollection.Sake]?.findOne<Sake>({
|
||||
_id: new ObjectId(productId)
|
||||
})
|
||||
|
||||
break
|
||||
|
||||
case ProductType.Spirit:
|
||||
product = await collections[DBcollection.Spirits]?.findOne<Spirit>({
|
||||
_id: new ObjectId(productId)
|
||||
})
|
||||
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if (!product) {
|
||||
throw new Error(`Associated "product" not found`)
|
||||
}
|
||||
|
||||
// Add age property to tasting note, if product has vintage property
|
||||
const { vintage } = product
|
||||
|
||||
if (typeof vintage === 'number') {
|
||||
newReview.tastingNote[TastingNoteKey.TextureAndBalance].age =
|
||||
new Date().getFullYear() - vintage
|
||||
}
|
||||
|
||||
const existingReviewCopy = {
|
||||
rating: existingReview.rating,
|
||||
reviewText: existingReview.reviewText,
|
||||
tastingNote: existingReview.tastingNote
|
||||
}
|
||||
|
||||
const newReviewCopy = {
|
||||
rating: newReview.rating,
|
||||
reviewText: newReview.reviewText,
|
||||
tastingNote: newReview.tastingNote
|
||||
}
|
||||
|
||||
// compare existing and new review
|
||||
if (compareObjects(existingReviewCopy, newReviewCopy)) {
|
||||
handleReqNotModified(res)
|
||||
}
|
||||
|
||||
// update review
|
||||
const result = await collections[dbCollection]?.updateOne(
|
||||
{
|
||||
_id: existingReview._id
|
||||
},
|
||||
{ $set: newReviewCopy },
|
||||
{ upsert: false }
|
||||
)
|
||||
|
||||
if (result && result.modifiedCount === 1) {
|
||||
handleReqSuccess(
|
||||
res,
|
||||
dbInstance,
|
||||
existingReview._id!.toHexString(),
|
||||
HTTPmethod.PUT
|
||||
)
|
||||
} else {
|
||||
handleReqNotModified(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
handleReqError(res, err, ResponseStatus.BadRequest)
|
||||
}
|
||||
})
|
||||
|
||||
// DELETE
|
||||
reviewsRouter.delete('/', async (req: Request, res: Response) => {
|
||||
await handleProductDELETEreq(req, res, dbCollection, dbInstance)
|
||||
})
|
||||
|
@ -1,36 +1,45 @@
|
||||
import express, { Request, Response } from 'express'
|
||||
import {
|
||||
sakeValidation,
|
||||
handleGETreq,
|
||||
handleProductPOSTreq,
|
||||
handleProductPUTreq,
|
||||
handleProductDELETEreq
|
||||
} from '../utils'
|
||||
import { DBcollection, DBinstance } from '../types'
|
||||
import { collections } from '../services/database.service'
|
||||
import { Sake } from '../models'
|
||||
|
||||
export const sakeRouter = express.Router()
|
||||
|
||||
const dbInstance = DBinstance.Sake
|
||||
const dbCollection = DBcollection.Sake
|
||||
|
||||
sakeRouter.use(express.json())
|
||||
|
||||
// GET
|
||||
sakeRouter.get('/', async (_req: Request, res: Response) => {
|
||||
await handleGETreq(res, dbCollection, dbInstance)
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// POST
|
||||
sakeRouter.post('/', async (req: Request, res: Response) => {
|
||||
await handleProductPOSTreq(req, res, dbCollection, dbInstance, sakeValidation)
|
||||
})
|
||||
try {
|
||||
const sake = req.body as Sake
|
||||
|
||||
// PUT
|
||||
sakeRouter.put('/', async (req: Request, res: Response) => {
|
||||
await handleProductPUTreq(req, res, dbCollection, dbInstance, sakeValidation)
|
||||
})
|
||||
const result = await collections.sake?.insertOne(sake)
|
||||
|
||||
// DELETE
|
||||
sakeRouter.delete('/', async (req: Request, res: Response) => {
|
||||
await handleProductDELETEreq(req, res, dbCollection, dbInstance)
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -1,48 +1,45 @@
|
||||
import express, { Request, Response } from 'express'
|
||||
import {
|
||||
spiritValidation,
|
||||
handleGETreq,
|
||||
handleProductPOSTreq,
|
||||
handleProductPUTreq,
|
||||
handleProductDELETEreq
|
||||
} from '../utils'
|
||||
import { DBcollection, DBinstance } from '../types'
|
||||
import { collections } from '../services/database.service'
|
||||
import { Spirit } from '../models'
|
||||
|
||||
export const spiritsRouter = express.Router()
|
||||
|
||||
const dbInstance = DBinstance.Spirit
|
||||
const dbCollection = DBcollection.Spirits
|
||||
|
||||
spiritsRouter.use(express.json())
|
||||
|
||||
// GET
|
||||
spiritsRouter.get('/', async (_req: Request, res: Response) => {
|
||||
await handleGETreq(res, dbCollection, dbInstance)
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// POST
|
||||
spiritsRouter.post('/', async (req: Request, res: Response) => {
|
||||
await handleProductPOSTreq(
|
||||
req,
|
||||
res,
|
||||
dbCollection,
|
||||
dbInstance,
|
||||
spiritValidation
|
||||
)
|
||||
})
|
||||
try {
|
||||
const spirit = req.body as Spirit
|
||||
|
||||
// PUT
|
||||
spiritsRouter.put('/', async (req: Request, res: Response) => {
|
||||
await handleProductPUTreq(
|
||||
req,
|
||||
res,
|
||||
dbCollection,
|
||||
dbInstance,
|
||||
spiritValidation
|
||||
)
|
||||
})
|
||||
const result = await collections.spirits?.insertOne(spirit)
|
||||
|
||||
// DELETE
|
||||
spiritsRouter.delete('/', async (req: Request, res: Response) => {
|
||||
await handleProductDELETEreq(req, res, dbCollection, dbInstance)
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -1,26 +1,26 @@
|
||||
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 { userValidation, handleReqError, handleReqSuccess } from '../utils'
|
||||
import Joi from 'joi'
|
||||
import { DBcollection, DBinstance, HTTPmethod, ResponseStatus } from '../types'
|
||||
|
||||
export const usersRouter = express.Router()
|
||||
|
||||
const dbInstance = DBinstance.User
|
||||
const dbCollection = DBcollection.Users
|
||||
|
||||
usersRouter.use(express.json())
|
||||
|
||||
// GET
|
||||
usersRouter.get('/', async (_req: Request, res: Response) => {
|
||||
await handleGETreq(res, dbCollection, dbInstance)
|
||||
try {
|
||||
const users = await collections.users?.find({}).toArray()
|
||||
|
||||
res.status(200).send(users)
|
||||
} catch (error: unknown) {
|
||||
console.error(error)
|
||||
|
||||
if (error instanceof Error) {
|
||||
res.status(500).send(error.message)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// POST
|
||||
@ -37,70 +37,18 @@ usersRouter.post('/', async (req: Request, res: Response) => {
|
||||
throw error.details[0].message
|
||||
}
|
||||
|
||||
const { npub } = res.locals
|
||||
|
||||
const existingUser = await collections[dbCollection]?.findOne({
|
||||
npub
|
||||
const existingUser = await collections.users?.findOne({
|
||||
npub: newUser.npub
|
||||
})
|
||||
|
||||
if (existingUser) {
|
||||
throw new Error('user with provided "npub" exists')
|
||||
}
|
||||
|
||||
newUser.npub = npub
|
||||
const result = await collections.users?.insertOne(newUser)
|
||||
|
||||
const result = await collections[dbCollection]?.insertOne(newUser)
|
||||
|
||||
if (result) {
|
||||
handleReqSuccess(
|
||||
res,
|
||||
DBinstance.User,
|
||||
result.insertedId.toString(),
|
||||
HTTPmethod.POST
|
||||
)
|
||||
} else {
|
||||
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)
|
||||
handleReqSuccess(res, result, 'user')
|
||||
} catch (error) {
|
||||
handleReqError(res, error)
|
||||
}
|
||||
})
|
||||
|
@ -1,42 +1,91 @@
|
||||
import express, { Request, Response } from 'express'
|
||||
import { collections } from '../services/database.service'
|
||||
import { Wine } from '../models'
|
||||
import {
|
||||
wineValidation,
|
||||
handleGETreq,
|
||||
handleProductPOSTreq,
|
||||
handleProductPUTreq,
|
||||
handleProductDELETEreq
|
||||
handleReqError,
|
||||
handleReqSuccess,
|
||||
alcoholToStandardDrinks,
|
||||
volumeToMl
|
||||
} from '../utils'
|
||||
import { DBcollection, DBinstance, UserRole } from '../types'
|
||||
import Joi from 'joi'
|
||||
|
||||
export const winesRouter = express.Router()
|
||||
|
||||
const dbInstance = DBinstance.Wine
|
||||
const dbCollection = DBcollection.Wines
|
||||
|
||||
winesRouter.use(express.json())
|
||||
|
||||
// GET
|
||||
winesRouter.get('/', async (_req: Request, res: Response) => {
|
||||
await handleGETreq(res, dbCollection, dbInstance)
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// POST
|
||||
winesRouter.post('/', async (req: Request, res: Response) => {
|
||||
await handleProductPOSTreq(req, res, dbCollection, dbInstance, wineValidation)
|
||||
})
|
||||
try {
|
||||
const {
|
||||
error,
|
||||
value: wine
|
||||
}: { error: Joi.ValidationError | undefined; value: Wine } = wineValidation(
|
||||
req.body
|
||||
)
|
||||
|
||||
// PUT
|
||||
winesRouter.put('/', async (req: Request, res: Response) => {
|
||||
await handleProductPUTreq(req, res, dbCollection, dbInstance, wineValidation)
|
||||
})
|
||||
if (error) {
|
||||
throw error.details[0].message
|
||||
}
|
||||
|
||||
// DELETE
|
||||
winesRouter.delete('/', async (req: Request, res: Response) => {
|
||||
await handleProductDELETEreq(
|
||||
req,
|
||||
res,
|
||||
dbCollection,
|
||||
dbInstance,
|
||||
UserRole.Reviewer
|
||||
)
|
||||
const { productCodeEAN, productCodeUPC, productCodeSKU } = wine
|
||||
|
||||
if (!productCodeEAN && !productCodeUPC && !productCodeSKU) {
|
||||
throw new Error(
|
||||
'provide "productCodeEAN", "productCodeUPC" or "productCodeSKU"'
|
||||
)
|
||||
}
|
||||
|
||||
if (productCodeEAN) {
|
||||
const existingWine = await collections.wines?.findOne({
|
||||
productCodeEAN
|
||||
})
|
||||
|
||||
if (existingWine) {
|
||||
throw new Error('wine with provided "productCodeEAN" exists')
|
||||
}
|
||||
} else if (productCodeUPC) {
|
||||
const existingWine = await collections.wines?.findOne({
|
||||
productCodeUPC
|
||||
})
|
||||
|
||||
if (existingWine) {
|
||||
throw new Error('wine with provided "productCodeUPC" exists')
|
||||
}
|
||||
} else {
|
||||
const existingWine = await collections.wines?.findOne({
|
||||
productCodeSKU
|
||||
})
|
||||
|
||||
if (existingWine) {
|
||||
throw new Error('wine with provided "productCodeSKU" exists')
|
||||
}
|
||||
}
|
||||
|
||||
wine.standardDrinks = alcoholToStandardDrinks(
|
||||
wine.alcohol,
|
||||
volumeToMl(wine.volume)
|
||||
)
|
||||
|
||||
const result = await collections.wines?.insertOne(wine)
|
||||
|
||||
handleReqSuccess(res, result, 'wine')
|
||||
} catch (error: unknown) {
|
||||
handleReqError(res, error)
|
||||
}
|
||||
})
|
||||
|
@ -1,16 +1,16 @@
|
||||
import * as mongoDB from 'mongodb'
|
||||
import * as dotenv from 'dotenv'
|
||||
import { DBcollection } from '../types'
|
||||
import { DBcollections } from '../types'
|
||||
|
||||
// Global Variables
|
||||
export const collections: {
|
||||
[DBcollection.Users]?: mongoDB.Collection
|
||||
[DBcollection.NostrEvents]?: mongoDB.Collection
|
||||
[DBcollection.Reviews]?: mongoDB.Collection
|
||||
[DBcollection.Wines]?: mongoDB.Collection
|
||||
[DBcollection.Sake]?: mongoDB.Collection
|
||||
[DBcollection.Spirits]?: mongoDB.Collection
|
||||
[DBcollection.Coffee]?: mongoDB.Collection
|
||||
[DBcollections.Users]?: mongoDB.Collection
|
||||
[DBcollections.NostrEvents]?: mongoDB.Collection
|
||||
[DBcollections.Reviews]?: mongoDB.Collection
|
||||
[DBcollections.Wines]?: mongoDB.Collection
|
||||
[DBcollections.Sake]?: mongoDB.Collection
|
||||
[DBcollections.Spirits]?: mongoDB.Collection
|
||||
[DBcollections.Coffee]?: mongoDB.Collection
|
||||
} = {}
|
||||
|
||||
// Initialize Connection
|
||||
@ -29,20 +29,20 @@ export async function connectToDatabase() {
|
||||
|
||||
const db: mongoDB.Db = client.db(process.env.DB_NAME)
|
||||
|
||||
const usersCollection: mongoDB.Collection = db.collection(DBcollection.Users)
|
||||
const usersCollection: mongoDB.Collection = db.collection(DBcollections.Users)
|
||||
const nostrEventsCollection: mongoDB.Collection = db.collection(
|
||||
DBcollection.NostrEvents
|
||||
DBcollections.NostrEvents
|
||||
)
|
||||
const reviewsCollection: mongoDB.Collection = db.collection(
|
||||
DBcollection.Reviews
|
||||
DBcollections.Reviews
|
||||
)
|
||||
const winesCollection: mongoDB.Collection = db.collection(DBcollection.Wines)
|
||||
const sakeCollection: mongoDB.Collection = db.collection(DBcollection.Sake)
|
||||
const winesCollection: mongoDB.Collection = db.collection(DBcollections.Wines)
|
||||
const sakeCollection: mongoDB.Collection = db.collection(DBcollections.Sake)
|
||||
const spiritsCollection: mongoDB.Collection = db.collection(
|
||||
DBcollection.Spirits
|
||||
DBcollections.Spirits
|
||||
)
|
||||
const coffeeCollection: mongoDB.Collection = db.collection(
|
||||
DBcollection.Coffee
|
||||
DBcollections.Coffee
|
||||
)
|
||||
|
||||
collections.users = usersCollection
|
||||
|
@ -1,14 +0,0 @@
|
||||
export enum BufferEncoding {
|
||||
ASCII = 'ascii',
|
||||
UTF8 = 'utf8',
|
||||
UTF_8 = 'utf-8',
|
||||
UTF16LE = 'utf16le',
|
||||
UTF_16LE = 'utf-16le',
|
||||
UCS2 = 'ucs2',
|
||||
UCS_2 = 'ucs-2',
|
||||
BASE64 = 'base64',
|
||||
BASE64URL = 'base64url',
|
||||
LATIN1 = 'latin1',
|
||||
BINARY = 'binary',
|
||||
HEX = 'hex'
|
||||
}
|
@ -1,47 +1,38 @@
|
||||
export enum CoffeeProcessingType {
|
||||
DeCaff = 'De-caff',
|
||||
Honey = 'Honey',
|
||||
SemiDry = 'Semi-dry',
|
||||
SwissWater = 'Swiss water',
|
||||
Sundried = 'Sundried',
|
||||
Washed = 'Washed'
|
||||
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 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'
|
||||
}
|
||||
export type CoffeeRoast =
|
||||
| 'Light'
|
||||
| 'Medium'
|
||||
| 'Medium-Dark'
|
||||
| 'Dark'
|
||||
| 'Very Dark'
|
||||
|
@ -1,4 +1,4 @@
|
||||
export enum DBcollection {
|
||||
export enum DBcollections {
|
||||
Users = 'users',
|
||||
NostrEvents = 'nostrEvents',
|
||||
Reviews = 'reviews',
|
||||
@ -7,13 +7,3 @@ export enum DBcollection {
|
||||
Spirits = 'spirits',
|
||||
Coffee = 'coffee'
|
||||
}
|
||||
|
||||
export enum DBinstance {
|
||||
User = 'User',
|
||||
NostrEvent = 'NostrEvent',
|
||||
Review = 'Review',
|
||||
Wine = 'Wine',
|
||||
Sake = 'Sake',
|
||||
Spirit = 'Spirit',
|
||||
Coffee = 'Coffee'
|
||||
}
|
||||
|
@ -6,6 +6,3 @@ 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
13
src/types/locals.d.ts
vendored
@ -1,13 +0,0 @@
|
||||
import 'express'
|
||||
import { ObjectId } from 'mongodb'
|
||||
import { UserRole } from './user'
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Locals {
|
||||
userId: ObjectId
|
||||
userRole: UserRole
|
||||
npub: string
|
||||
}
|
||||
}
|
||||
}
|
@ -1,85 +1,72 @@
|
||||
export type Availability = 'In stock' | 'Out of stock' | 'Discontinued'
|
||||
export type Availability = 'in stock' | 'out of stock' | 'discontinued'
|
||||
|
||||
export enum Ingredient {
|
||||
Blanche = 'Blanche',
|
||||
Anise = 'Anise',
|
||||
Fennel = 'Fennel',
|
||||
Hyssop = 'Hyssop',
|
||||
Mint = 'Mint',
|
||||
CitrusPeel = 'Citrus Peel',
|
||||
CorianderSeeds = 'Coriander Seeds',
|
||||
AngelicaRoot = 'Angelica Root',
|
||||
Cinnamon = 'Cinnamon',
|
||||
Clove = 'Clove',
|
||||
Wheat = 'Wheat',
|
||||
Rye = 'Rye',
|
||||
Corn = 'Corn',
|
||||
Potato = 'Potato',
|
||||
Barley = 'Barley',
|
||||
Sugarcane = 'Sugarcane',
|
||||
Fruits = 'Fruits',
|
||||
Grains = 'Grains',
|
||||
Juniper = 'Juniper',
|
||||
Coriander = 'Coriander',
|
||||
LemonPeel = 'Lemon peel',
|
||||
OrangePeel = 'Orange peel',
|
||||
OrrisRoot = 'Orris root',
|
||||
CassiaBark = 'Cassia bark',
|
||||
LicoriceRoot = 'Licorice root',
|
||||
GrapefruitPeel = 'Grapefruit peel',
|
||||
Elderflower = 'Elderflower',
|
||||
Apple = 'Apple',
|
||||
Blackcurrant = 'Blackcurrant',
|
||||
Butterscotch = 'Butterscotch',
|
||||
Peach = 'Peach',
|
||||
Pear = 'Pear',
|
||||
Plum = 'Plum',
|
||||
Raspberries = 'Raspberries',
|
||||
Sorghum = 'Sorghum',
|
||||
Rice = 'Rice',
|
||||
Millet = 'Millet',
|
||||
BrownSugar = 'Brown sugar',
|
||||
Buckwheat = 'Buckwheat',
|
||||
SweetPotato = 'Sweet Potato',
|
||||
Oat = 'Oat',
|
||||
EggAdvocaat = 'Egg (Advocaat)',
|
||||
Strawberry = 'Strawberry',
|
||||
Almond = 'Almond',
|
||||
Banana = 'Banana',
|
||||
Chocolate = 'Chocolate',
|
||||
SourCherry = 'Sour Cherry',
|
||||
Violet = 'Violet',
|
||||
Lemon = 'Lemon',
|
||||
Melon = 'Melon',
|
||||
Orange = 'Orange',
|
||||
Raspberry = 'Raspberry',
|
||||
Yuzu = 'Yuzu',
|
||||
ApricotKernel = 'Apricot Kernel',
|
||||
Hazelnut = 'Hazelnut',
|
||||
Peanut = 'Peanut',
|
||||
Pecan = 'Pecan',
|
||||
Walnut = 'Walnut'
|
||||
export type Ingredient =
|
||||
| 'Blanche'
|
||||
| 'Anise'
|
||||
| 'Fennel'
|
||||
| 'Hyssop'
|
||||
| 'Mint'
|
||||
| 'Citrus Peel'
|
||||
| 'Coriander Seeds'
|
||||
| 'Angelica Root'
|
||||
| 'Cinnamon'
|
||||
| 'Clove'
|
||||
| 'Wheat'
|
||||
| 'Rye'
|
||||
| 'Corn'
|
||||
| 'Potato'
|
||||
| 'Barley'
|
||||
| 'Sugarcane'
|
||||
| 'Fruits'
|
||||
| 'Grains'
|
||||
| 'Juniper'
|
||||
| 'Coriander'
|
||||
| 'Lemon peel'
|
||||
| 'Orange peel'
|
||||
| 'Orris root'
|
||||
| 'Cassia bark'
|
||||
| 'Licorice root'
|
||||
| 'Grapefruit peel'
|
||||
| 'Elderflower'
|
||||
| 'Apple'
|
||||
| 'Blackcurrant'
|
||||
| 'Butterscotch'
|
||||
| 'Peach'
|
||||
| 'Pear'
|
||||
| 'Plum'
|
||||
| 'Raspberries'
|
||||
| 'Sorghum'
|
||||
| 'Rice'
|
||||
| 'Millet'
|
||||
| 'Brown sugar'
|
||||
| 'Buckwheat'
|
||||
| 'Sweet Potato'
|
||||
| 'Oat'
|
||||
| 'Egg (Advocaat)'
|
||||
| 'Strawberry'
|
||||
| 'Almond'
|
||||
| 'Banana'
|
||||
| 'Chocolate'
|
||||
| 'Sour Cherry'
|
||||
| 'Violet'
|
||||
| 'Lemon'
|
||||
| 'Melon'
|
||||
| 'Orange'
|
||||
| 'Raspberry'
|
||||
| 'Yuzu'
|
||||
| 'Almond'
|
||||
| 'Apricot Kernel'
|
||||
| 'Hazelnut'
|
||||
| 'Peanut'
|
||||
| 'Pecan'
|
||||
| 'Walnut'
|
||||
|
||||
export enum VintageOptions {
|
||||
NV = 'nv',
|
||||
MV = 'mv'
|
||||
}
|
||||
|
||||
export enum VintageOption {
|
||||
NV = 'NV',
|
||||
MV = 'MV'
|
||||
}
|
||||
|
||||
export interface StandardDrink {
|
||||
export interface StandardDrinks {
|
||||
'100ml': { AU: number; UK: number; US: number }
|
||||
bottle: { AU: number; UK: number; US: number }
|
||||
}
|
||||
|
||||
export enum ProductType {
|
||||
Wine = 'Wine',
|
||||
Spirit = 'Spirit',
|
||||
Sake = 'Sake',
|
||||
Coffee = 'Coffee'
|
||||
}
|
||||
|
||||
export enum ProductCode {
|
||||
EAN = 'EAN',
|
||||
UPC = 'UPC',
|
||||
SKU = 'SKU'
|
||||
}
|
||||
|
@ -1,122 +0,0 @@
|
||||
import {
|
||||
VisualAssessmentKey,
|
||||
ClarityVisualAssessment,
|
||||
NatureVisualAssessment,
|
||||
WhiteColour,
|
||||
AmberColour,
|
||||
RoseColour,
|
||||
RedColour,
|
||||
BlueColour,
|
||||
GreenColour,
|
||||
PrimaryFlavoursAndAromasKey,
|
||||
Condition,
|
||||
Intensity,
|
||||
Age,
|
||||
CitrusFruit,
|
||||
AppleFruit,
|
||||
StoneFruit,
|
||||
RedFruit,
|
||||
BlackFruit,
|
||||
TropicalFruit,
|
||||
MelonFruit,
|
||||
Floral,
|
||||
Vegetal,
|
||||
Earth,
|
||||
Microbiological,
|
||||
Oxidation,
|
||||
Umami,
|
||||
Grain,
|
||||
Dairy,
|
||||
TextureAndBalanceKey,
|
||||
Sweetness,
|
||||
Concentration,
|
||||
Body,
|
||||
FlavourIntensity,
|
||||
PalateLength,
|
||||
ReasoningConcentration,
|
||||
Quality,
|
||||
ReadinessToDrink,
|
||||
ReasoningKey,
|
||||
TanninString,
|
||||
TanninObject,
|
||||
GrapeFruit,
|
||||
DriedFruit,
|
||||
Fault,
|
||||
Sweet,
|
||||
Botanical,
|
||||
Nutty,
|
||||
Salt,
|
||||
Burnt,
|
||||
SmokeEnum
|
||||
} from './review/'
|
||||
|
||||
export enum RatingOption {
|
||||
NoScore = 'NS'
|
||||
}
|
||||
|
||||
export enum TastingNoteKey {
|
||||
VisualAssessment = 'visualAssessment',
|
||||
PrimaryFlavoursAndAromas = 'primaryFlavoursAndAromas',
|
||||
TextureAndBalance = 'textureAndBalance'
|
||||
}
|
||||
|
||||
export interface TastingNote {
|
||||
[TastingNoteKey.VisualAssessment]: {
|
||||
[VisualAssessmentKey.Clarity]: ClarityVisualAssessment
|
||||
[VisualAssessmentKey.Nature]: NatureVisualAssessment
|
||||
[VisualAssessmentKey.Colour]:
|
||||
| WhiteColour
|
||||
| AmberColour
|
||||
| RoseColour
|
||||
| RedColour
|
||||
| BlueColour
|
||||
| GreenColour
|
||||
}
|
||||
[TastingNoteKey.PrimaryFlavoursAndAromas]: {
|
||||
[PrimaryFlavoursAndAromasKey.Condition]: Condition
|
||||
[PrimaryFlavoursAndAromasKey.Intensity]: Intensity
|
||||
[PrimaryFlavoursAndAromasKey.Age]: Age
|
||||
[PrimaryFlavoursAndAromasKey.Fruit]:
|
||||
| CitrusFruit
|
||||
| AppleFruit
|
||||
| StoneFruit
|
||||
| RedFruit
|
||||
| GrapeFruit
|
||||
| BlackFruit
|
||||
| TropicalFruit
|
||||
| MelonFruit
|
||||
| DriedFruit
|
||||
[PrimaryFlavoursAndAromasKey.Floral]: Floral
|
||||
[PrimaryFlavoursAndAromasKey.Sweet]: Sweet
|
||||
[PrimaryFlavoursAndAromasKey.Vegetal]: Vegetal
|
||||
[PrimaryFlavoursAndAromasKey.Grain]: Grain
|
||||
[PrimaryFlavoursAndAromasKey.Botanical]: Botanical
|
||||
[PrimaryFlavoursAndAromasKey.Nutty]: Nutty
|
||||
[PrimaryFlavoursAndAromasKey.Earth]: Earth
|
||||
[PrimaryFlavoursAndAromasKey.Dairy]: Dairy
|
||||
[PrimaryFlavoursAndAromasKey.Umami]: Umami
|
||||
[PrimaryFlavoursAndAromasKey.Microbiological]: Microbiological
|
||||
[PrimaryFlavoursAndAromasKey.Salt]: Salt
|
||||
[PrimaryFlavoursAndAromasKey.Burnt]: Burnt
|
||||
[PrimaryFlavoursAndAromasKey.Smoke]: SmokeEnum
|
||||
[PrimaryFlavoursAndAromasKey.Oxidation]: Oxidation
|
||||
[PrimaryFlavoursAndAromasKey.Fault]: Fault
|
||||
}
|
||||
[TastingNoteKey.TextureAndBalance]: {
|
||||
[TextureAndBalanceKey.Sweetness]: Sweetness
|
||||
[TextureAndBalanceKey.Acidity]: Concentration
|
||||
[TextureAndBalanceKey.Tannin]: TanninString | TanninObject
|
||||
[TextureAndBalanceKey.Alcohol]: Concentration
|
||||
[TextureAndBalanceKey.Body]: Body
|
||||
[TextureAndBalanceKey.FlavourIntensity]: FlavourIntensity
|
||||
[TextureAndBalanceKey.PalateLength]: PalateLength
|
||||
[TextureAndBalanceKey.Reasoning]: {
|
||||
[ReasoningKey.Balance]: boolean
|
||||
[ReasoningKey.Concentration]: ReasoningConcentration
|
||||
[ReasoningKey.Complex]: boolean
|
||||
}
|
||||
[TextureAndBalanceKey.Quality]: Quality
|
||||
[TextureAndBalanceKey.Age]: number // tasting date-producers bottling date
|
||||
[TextureAndBalanceKey.ReadinessToDrink]: ReadinessToDrink
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
// Free Run (sake, wine, spirits)
|
||||
export enum WhiteColour {
|
||||
WaterWhite = 'Water white',
|
||||
LemonGreen = 'Lemon-Green',
|
||||
Lemon = 'Lemon',
|
||||
Gold = 'Gold',
|
||||
WhiteBrown = 'White-Brown'
|
||||
}
|
||||
|
||||
// Skin Contact (wine, spirits)
|
||||
export enum AmberColour {
|
||||
AmberOrange = 'Amber-Orange',
|
||||
Amber = 'Amber'
|
||||
}
|
||||
|
||||
// Blush (wine, spirits)
|
||||
export enum RoseColour {
|
||||
Pink = 'Pink',
|
||||
Salmon = 'Salmon',
|
||||
RoseOrange = 'Rose-Orange',
|
||||
OnionSkin = 'Onion-Skin'
|
||||
}
|
||||
|
||||
// Extended Maceration (wine, spirits)
|
||||
export enum RedColour {
|
||||
Purple = 'Purple',
|
||||
Ruby = 'Ruby',
|
||||
Garnet = 'Garnet',
|
||||
Tawny = 'Tawny',
|
||||
RedBrown = 'Red-Brown'
|
||||
}
|
||||
|
||||
// Liqueurs (spirits)
|
||||
export enum BlueColour {
|
||||
BluePale = 'Blue-Pale',
|
||||
BlueDark = 'Blue-Dark'
|
||||
}
|
||||
|
||||
// Liqueurs (spirits)
|
||||
export enum GreenColour {
|
||||
GreenPale = 'Green-Pale',
|
||||
GreenDark = 'Green-Dark'
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
export enum CitrusFruit {
|
||||
Grapefruit = 'Grapefruit',
|
||||
Lemon = 'Lemon',
|
||||
Lime = 'Lime',
|
||||
Marmalade = 'Marmalade',
|
||||
Orange = 'Orange',
|
||||
Yuzu = 'Yuzu'
|
||||
}
|
||||
|
||||
export enum AppleFruit {
|
||||
Green = 'Green',
|
||||
Red = 'Red',
|
||||
Ripe = 'Ripe'
|
||||
}
|
||||
|
||||
export enum StoneFruit {
|
||||
Apricot = 'Apricot ',
|
||||
Nectarine = 'Nectarine',
|
||||
Peach = 'Peach',
|
||||
Plum = 'Plum '
|
||||
}
|
||||
|
||||
export enum RedFruit {
|
||||
Cherry = 'Cherry',
|
||||
Cranberry = 'Cranberry',
|
||||
Pomegranate = 'Pomegranate',
|
||||
Raspberry = 'Raspberry',
|
||||
SourCherry = 'Sour Cherry',
|
||||
Strawberry = 'Strawberry'
|
||||
}
|
||||
|
||||
export enum GrapeFruit {
|
||||
Grape = 'Grape'
|
||||
}
|
||||
|
||||
export enum BlackFruit {
|
||||
Blackberry = 'Blackberry',
|
||||
Blackcurrant = 'Blackcurrant',
|
||||
Boysenberry = 'Boysenberry',
|
||||
Blueberry = 'Blueberry',
|
||||
Olive = 'Olive'
|
||||
}
|
||||
|
||||
export enum TropicalFruit {
|
||||
Banana = 'Banana',
|
||||
Lychee = 'Lychee',
|
||||
Mango = 'Mango',
|
||||
PassionFruit = 'Passion Fruit',
|
||||
Pineapple = 'Pineapple',
|
||||
Guava = 'Guava'
|
||||
}
|
||||
|
||||
export enum MelonFruit {
|
||||
Cantaloupe = 'Cantaloupe',
|
||||
Honeydew = 'Honeydew'
|
||||
}
|
||||
|
||||
export enum DriedFruit {
|
||||
Raisin = 'Raisin',
|
||||
Prune = 'Prune',
|
||||
Fig = 'Fig',
|
||||
Date = 'Date'
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
export * from './visualAssessment'
|
||||
export * from './colour'
|
||||
export * from './primaryFlavoursAndAromas'
|
||||
export * from './textureAndBalance'
|
||||
export * from './fruit'
|
||||
export * from './productSpecific'
|
@ -1,213 +0,0 @@
|
||||
export enum PrimaryFlavoursAndAromasKey {
|
||||
Condition = 'condition',
|
||||
Intensity = 'intensity',
|
||||
Age = 'age',
|
||||
Fruit = 'fruit',
|
||||
Floral = 'floral',
|
||||
Sweet = 'sweet',
|
||||
Vegetal = 'vegetal',
|
||||
Grain = 'grain',
|
||||
Botanical = 'botanical',
|
||||
Nutty = 'nutty',
|
||||
Earth = 'earth',
|
||||
Dairy = 'dairy',
|
||||
Umami = 'umami',
|
||||
Microbiological = 'microbiological',
|
||||
Salt = 'salt',
|
||||
Burnt = 'burnt',
|
||||
Smoke = 'smoke',
|
||||
Oxidation = 'oxidation',
|
||||
Fault = 'fault'
|
||||
}
|
||||
|
||||
export enum RequiredPrimaryFlavoursAndAromasKey {
|
||||
Condition = PrimaryFlavoursAndAromasKey.Condition,
|
||||
Intensity = PrimaryFlavoursAndAromasKey.Intensity,
|
||||
Age = PrimaryFlavoursAndAromasKey.Age
|
||||
}
|
||||
|
||||
export enum Condition {
|
||||
Clean = 'Clean',
|
||||
Unclean = 'Unclean'
|
||||
}
|
||||
|
||||
export enum Intensity {
|
||||
Light = 'Light',
|
||||
Medium = 'Medium',
|
||||
Pronounced = 'Pronounced'
|
||||
}
|
||||
|
||||
export enum Age {
|
||||
Youthful = 'Youthful',
|
||||
Developing = 'Developing',
|
||||
Developed = 'Developed',
|
||||
Oxidised = 'Oxidised',
|
||||
Passed = 'Passed'
|
||||
}
|
||||
|
||||
export enum Floral {
|
||||
Acacia = 'Acacia',
|
||||
Elderflower = 'Elderflower',
|
||||
Hibiscus = 'Hibiscus',
|
||||
Honeysuckle = 'Honeysuckle',
|
||||
Jasmine = 'Jasmine',
|
||||
Lavender = 'Lavender',
|
||||
Lilac = 'Lilac',
|
||||
OrangeBlossom = 'Orange Blossom',
|
||||
Potpourri = 'Potpourri',
|
||||
Rose = 'Rose',
|
||||
Vanilla = 'Vanilla',
|
||||
Violet = 'Violet'
|
||||
}
|
||||
|
||||
export enum Sweet {
|
||||
Butterscotch = 'Butterscotch',
|
||||
Caramel = 'Caramel',
|
||||
Honey = 'Honey',
|
||||
Molasses = 'Molasses',
|
||||
Sugarcane = 'Sugarcane',
|
||||
Toffee = 'Toffee'
|
||||
}
|
||||
|
||||
export enum Vegetal {
|
||||
Artichoke = 'Artichoke',
|
||||
Asparagus = 'Asparagus',
|
||||
Basil = 'Basil',
|
||||
BellPepper = 'Bell Pepper',
|
||||
BlackTea = 'Black Tea',
|
||||
Capsicum = 'Capsicum',
|
||||
Coriander = 'Coriander',
|
||||
Eucalyptus = 'Eucalyptus',
|
||||
Gooseberry = 'Gooseberry',
|
||||
Grass = 'Grass',
|
||||
Hay = 'Hay',
|
||||
Herbaceous = 'Herbaceous',
|
||||
Lemongrass = 'Lemongrass',
|
||||
Mint = 'Mint',
|
||||
Tomato = 'Tomato'
|
||||
}
|
||||
|
||||
export enum Grain {
|
||||
Barley = 'Barley',
|
||||
Bran = 'Bran',
|
||||
Cereal = 'Cereal',
|
||||
CookedRice = 'Cooked Rice',
|
||||
Corn = 'Corn',
|
||||
Grains = 'Grains',
|
||||
Malt = 'Malt',
|
||||
Oats = 'Oats',
|
||||
Porridge = 'Porridge',
|
||||
RawRice = 'Raw Rice',
|
||||
Rye = 'Rye',
|
||||
SteamedRice = 'Steamed Rice',
|
||||
Wheat = 'Wheat'
|
||||
}
|
||||
|
||||
export enum Botanical {
|
||||
Allspice = 'Allspice',
|
||||
Anise = 'Anise',
|
||||
Cedar = 'Cedar',
|
||||
Cinnamon = 'Cinnamon',
|
||||
Cloves = 'Cloves',
|
||||
Coconut = 'Coconut',
|
||||
Cumin = 'Cumin',
|
||||
Dill = 'Dill',
|
||||
Ginger = 'Ginger',
|
||||
Juniper = 'Juniper',
|
||||
Liquorice = 'Liquorice',
|
||||
Nutmeg = 'Nutmeg',
|
||||
Pepper = 'Pepper',
|
||||
Sandalwood = 'Sandalwood',
|
||||
Spices = 'Spices',
|
||||
Tobacco = 'Tobacco'
|
||||
}
|
||||
|
||||
export enum Nutty {
|
||||
Almond = 'Almond',
|
||||
Cashew = 'Cashew',
|
||||
Chestnut = 'Chestnut',
|
||||
Hazelnut = 'Hazelnut',
|
||||
Marzipan = 'Marzipan',
|
||||
Walnut = 'Walnut'
|
||||
}
|
||||
|
||||
export enum Earth {
|
||||
Gravel = 'Gravel',
|
||||
Kerosene = 'Kerosene',
|
||||
RedBeet = 'Red Beet',
|
||||
Rocks = 'Rocks',
|
||||
Slate = 'Slate',
|
||||
Soil = 'Soil',
|
||||
Terracotta = 'Terracotta'
|
||||
}
|
||||
|
||||
export enum Dairy {
|
||||
Butter = 'Butter',
|
||||
Cheese = 'Cheese',
|
||||
Cream = 'Cream',
|
||||
Milk = 'Milk',
|
||||
Yoghurt = 'Yoghurt'
|
||||
}
|
||||
|
||||
export enum Umami {
|
||||
FishSauce = 'Fish Sauce',
|
||||
Seaweed = 'Seaweed',
|
||||
Soy = 'Soy'
|
||||
}
|
||||
|
||||
export enum Microbiological {
|
||||
Animal = 'Animal',
|
||||
BreadDough = 'Bread Dough',
|
||||
Brioche = 'Brioche',
|
||||
Farmyard = 'Farmyard',
|
||||
Iodine = 'Iodine',
|
||||
Leather = 'Leather',
|
||||
Meaty = 'Meaty',
|
||||
Mouse = 'Mouse',
|
||||
Mushroom = 'Mushroom',
|
||||
Truffles = 'Truffles',
|
||||
Vinyl = 'Vinyl',
|
||||
Yeasty = 'Yeasty'
|
||||
}
|
||||
|
||||
export enum Salt {
|
||||
Brine = 'Brine'
|
||||
}
|
||||
|
||||
export enum Burnt {
|
||||
Chocolate = 'Chocolate',
|
||||
Coffee = 'Coffee',
|
||||
ToastedBread = 'Toasted Bread',
|
||||
RoastedNuts = 'Roasted Nuts',
|
||||
CaramelisedNuts = 'Caramelised Nuts'
|
||||
}
|
||||
|
||||
export enum SmokeEnum {
|
||||
Ash = 'Ash',
|
||||
Peat = 'Peat',
|
||||
Smoke = 'Smoke'
|
||||
}
|
||||
|
||||
export enum Oxidation {
|
||||
Aldehydes = 'Aldehydes',
|
||||
Madeirised = 'Madeirised',
|
||||
Sherry = 'Sherry',
|
||||
Staleness = 'Staleness'
|
||||
}
|
||||
|
||||
export enum Fault {
|
||||
BalsamicVinegar = 'Balsamic Vinegar',
|
||||
Cabbage = 'Cabbage',
|
||||
Eggs = 'Eggs',
|
||||
Garlic = 'Garlic',
|
||||
Mercaptans = 'Mercaptans',
|
||||
Mustiness = 'Mustiness',
|
||||
NailVarnishRemover = 'Nail Varnish Remover',
|
||||
Onion = 'Onion',
|
||||
Rubber = 'Rubber',
|
||||
Solvent = 'Solvent',
|
||||
SourMilk = 'Sour Milk',
|
||||
Sweat = 'Sweat',
|
||||
Trichloroanisole = 'Trichloroanisole',
|
||||
WetCardboard = 'Wet Cardboard'
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
import {
|
||||
Floral,
|
||||
Sweet,
|
||||
Vegetal,
|
||||
Botanical,
|
||||
Nutty,
|
||||
Earth,
|
||||
Salt,
|
||||
Burnt,
|
||||
Fault,
|
||||
CitrusFruit,
|
||||
StoneFruit,
|
||||
RedFruit,
|
||||
GrapeFruit,
|
||||
BlackFruit,
|
||||
DriedFruit
|
||||
} from '../'
|
||||
|
||||
export enum CoffeeCitrusFruit {
|
||||
Grapefruit = CitrusFruit.Grapefruit,
|
||||
Lemon = CitrusFruit.Lemon,
|
||||
Orange = CitrusFruit.Orange
|
||||
}
|
||||
|
||||
export enum CoffeeStoneFruit {
|
||||
Apricot = StoneFruit.Apricot
|
||||
}
|
||||
|
||||
export enum CoffeeRedFruit {
|
||||
Raspberry = RedFruit.Raspberry
|
||||
}
|
||||
|
||||
export enum CoffeeGrapeFruit {
|
||||
Grape = GrapeFruit.Grape
|
||||
}
|
||||
|
||||
export enum CoffeeBlackFruit {
|
||||
Blueberry = BlackFruit.Blueberry
|
||||
}
|
||||
|
||||
export enum CoffeeDriedFruit {
|
||||
Raisin = DriedFruit.Raisin,
|
||||
Date = DriedFruit.Date
|
||||
}
|
||||
|
||||
export enum CoffeeFloral {
|
||||
Jasmine = Floral.Jasmine,
|
||||
Lavender = Floral.Lavender,
|
||||
Rose = Floral.Rose,
|
||||
Vanilla = Floral.Vanilla
|
||||
}
|
||||
|
||||
export enum CoffeeSweet {
|
||||
Caramel = Sweet.Caramel,
|
||||
Honey = Sweet.Honey,
|
||||
Molasses = Sweet.Molasses
|
||||
}
|
||||
|
||||
export enum CoffeeVegetal {
|
||||
Grass = Vegetal.Grass
|
||||
}
|
||||
|
||||
export enum CoffeeBotanical {
|
||||
Cinnamon = Botanical.Cinnamon,
|
||||
Cloves = Botanical.Cloves,
|
||||
Nutmeg = Botanical.Nutmeg,
|
||||
Pepper = Botanical.Pepper,
|
||||
Tobacco = Botanical.Tobacco
|
||||
}
|
||||
|
||||
export enum CoffeeNutty {
|
||||
Almond = Nutty.Almond,
|
||||
Hazelnut = Nutty.Hazelnut,
|
||||
Walnut = Nutty.Walnut
|
||||
}
|
||||
|
||||
export enum CoffeeEarth {
|
||||
Soil = Earth.Soil
|
||||
}
|
||||
|
||||
export enum CoffeeSalt {
|
||||
Brine = Salt.Brine
|
||||
}
|
||||
|
||||
export enum CoffeeBurnt {
|
||||
Chocolate = Burnt.Chocolate,
|
||||
ToastedBread = Burnt.ToastedBread,
|
||||
CaramelisedNuts = Burnt.CaramelisedNuts
|
||||
}
|
||||
|
||||
export enum CoffeeFault {
|
||||
BalsamicVinegar = Fault.BalsamicVinegar,
|
||||
Mustiness = Fault.Mustiness,
|
||||
WetCardboard = Fault.WetCardboard
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
export * from './wine'
|
||||
export * from './spirit'
|
||||
export * from './sake'
|
||||
export * from './coffee'
|
@ -1,140 +0,0 @@
|
||||
import {
|
||||
WhiteColour,
|
||||
Floral,
|
||||
Sweet,
|
||||
Vegetal,
|
||||
Grain,
|
||||
Botanical,
|
||||
Nutty,
|
||||
Dairy,
|
||||
Umami,
|
||||
Microbiological,
|
||||
Burnt,
|
||||
SmokeEnum,
|
||||
CitrusFruit,
|
||||
AppleFruit,
|
||||
StoneFruit,
|
||||
RedFruit,
|
||||
TropicalFruit,
|
||||
MelonFruit,
|
||||
DriedFruit
|
||||
} from '../'
|
||||
|
||||
export enum SakeColour {
|
||||
WaterWhite = WhiteColour.WaterWhite,
|
||||
LemonGreen = WhiteColour.LemonGreen,
|
||||
Lemon = WhiteColour.Lemon,
|
||||
Gold = WhiteColour.Gold,
|
||||
WhiteBrown = WhiteColour.WhiteBrown
|
||||
}
|
||||
|
||||
export enum SakeCitrusFruit {
|
||||
Lemon = CitrusFruit.Lemon,
|
||||
Orange = CitrusFruit.Orange,
|
||||
Yuzu = CitrusFruit.Yuzu
|
||||
}
|
||||
|
||||
export enum SakeAppleFruit {
|
||||
Green = AppleFruit.Green,
|
||||
Red = AppleFruit.Red,
|
||||
Ripe = AppleFruit.Ripe
|
||||
}
|
||||
|
||||
export enum SakeStoneFruit {
|
||||
Apricot = StoneFruit.Apricot,
|
||||
Nectarine = StoneFruit.Nectarine,
|
||||
Peach = StoneFruit.Peach,
|
||||
Plum = StoneFruit.Plum
|
||||
}
|
||||
|
||||
export enum SakeRedFruit {
|
||||
Cherry = RedFruit.Cherry,
|
||||
Strawberry = RedFruit.Strawberry
|
||||
}
|
||||
|
||||
export enum SakeTropicalFruit {
|
||||
Banana = TropicalFruit.Banana,
|
||||
Lychee = TropicalFruit.Lychee,
|
||||
Mango = TropicalFruit.Mango,
|
||||
PassionFruit = TropicalFruit.PassionFruit,
|
||||
Pineapple = TropicalFruit.Pineapple,
|
||||
Guava = TropicalFruit.Guava
|
||||
}
|
||||
|
||||
export enum SakeMelonFruit {
|
||||
Cantaloupe = MelonFruit.Cantaloupe,
|
||||
Honeydew = MelonFruit.Honeydew
|
||||
}
|
||||
|
||||
export enum SakeDriedFruit {
|
||||
Date = DriedFruit.Date
|
||||
}
|
||||
|
||||
export enum SakeFloral {
|
||||
Elderflower = Floral.Elderflower,
|
||||
Lilac = Floral.Lilac,
|
||||
Potpourri = Floral.Potpourri
|
||||
}
|
||||
|
||||
export enum SakeSweet {
|
||||
Caramel = Sweet.Caramel,
|
||||
Honey = Sweet.Honey,
|
||||
Molasses = Sweet.Molasses,
|
||||
Sugarcane = Sweet.Sugarcane
|
||||
}
|
||||
|
||||
export enum SakeVegetal {
|
||||
Basil = Vegetal.Basil,
|
||||
Lemongrass = Vegetal.Lemongrass,
|
||||
Mint = Vegetal.Mint
|
||||
}
|
||||
|
||||
export enum SakeGrain {
|
||||
Bran = Grain.Bran,
|
||||
Malt = Grain.Malt,
|
||||
Porridge = Grain.Porridge,
|
||||
RawRice = Grain.RawRice,
|
||||
SteamedRice = Grain.SteamedRice
|
||||
}
|
||||
|
||||
export enum SakeBotanical {
|
||||
Anise = Botanical.Anise,
|
||||
Cinnamon = Botanical.Cinnamon,
|
||||
Cloves = Botanical.Cloves,
|
||||
Nutmeg = Botanical.Nutmeg,
|
||||
Pepper = Botanical.Pepper
|
||||
}
|
||||
|
||||
export enum SakeNutty {
|
||||
Almond = Nutty.Almond,
|
||||
Chestnut = Nutty.Chestnut,
|
||||
Walnut = Nutty.Walnut
|
||||
}
|
||||
|
||||
export enum SakeDairy {
|
||||
Butter = Dairy.Butter,
|
||||
Cheese = Dairy.Cheese,
|
||||
Cream = Dairy.Cream,
|
||||
Yoghurt = Dairy.Yoghurt
|
||||
}
|
||||
|
||||
export enum SakeUmami {
|
||||
Seaweed = Umami.Seaweed,
|
||||
Soy = Umami.Soy
|
||||
}
|
||||
|
||||
export enum SakeMicrobiological {
|
||||
Meaty = Microbiological.Meaty
|
||||
}
|
||||
|
||||
export enum SakeBurnt {
|
||||
Chocolate = Burnt.Chocolate,
|
||||
Coffee = Burnt.Coffee,
|
||||
ToastedBread = Burnt.ToastedBread,
|
||||
RoastedNuts = Burnt.RoastedNuts,
|
||||
CaramelisedNuts = Burnt.CaramelisedNuts
|
||||
}
|
||||
|
||||
export enum SakeSmoke {
|
||||
Smoke = SmokeEnum.Smoke
|
||||
}
|
@ -1,206 +0,0 @@
|
||||
import {
|
||||
WhiteColour,
|
||||
AmberColour,
|
||||
RoseColour,
|
||||
RedColour,
|
||||
BlueColour,
|
||||
GreenColour,
|
||||
Floral,
|
||||
Vegetal,
|
||||
Sweet,
|
||||
Grain,
|
||||
Botanical,
|
||||
Dairy,
|
||||
Umami,
|
||||
Microbiological,
|
||||
Burnt,
|
||||
SmokeEnum,
|
||||
Oxidation,
|
||||
Fault,
|
||||
CitrusFruit,
|
||||
AppleFruit,
|
||||
StoneFruit,
|
||||
RedFruit,
|
||||
GrapeFruit,
|
||||
BlackFruit,
|
||||
TropicalFruit,
|
||||
MelonFruit,
|
||||
DriedFruit
|
||||
} from '..'
|
||||
|
||||
export enum SpiritColour {
|
||||
WaterWhite = WhiteColour.WaterWhite,
|
||||
LemonGreen = WhiteColour.LemonGreen,
|
||||
Lemon = WhiteColour.Lemon,
|
||||
Gold = WhiteColour.Gold,
|
||||
WhiteBrown = WhiteColour.WhiteBrown,
|
||||
Amber = AmberColour.Amber,
|
||||
AmberOrange = AmberColour.AmberOrange,
|
||||
Pink = RoseColour.Pink,
|
||||
Salmon = RoseColour.Salmon,
|
||||
OnionSkin = RoseColour.OnionSkin,
|
||||
RoseOrange = RoseColour.RoseOrange,
|
||||
Purple = RedColour.Purple,
|
||||
Ruby = RedColour.Ruby,
|
||||
Garnet = RedColour.Garnet,
|
||||
Tawny = RedColour.Tawny,
|
||||
RedBrown = RedColour.RedBrown,
|
||||
BluePale = BlueColour.BluePale,
|
||||
BlueDark = BlueColour.BlueDark,
|
||||
GreenPale = GreenColour.GreenPale,
|
||||
GreenDark = GreenColour.GreenDark
|
||||
}
|
||||
|
||||
export enum SpiritCitrusFruit {
|
||||
Grapefruit = CitrusFruit.Grapefruit,
|
||||
Lemon = CitrusFruit.Lemon,
|
||||
Lime = CitrusFruit.Lime,
|
||||
Marmalade = CitrusFruit.Marmalade,
|
||||
Orange = CitrusFruit.Orange,
|
||||
Yuzu = CitrusFruit.Yuzu
|
||||
}
|
||||
|
||||
export enum SpiritAppleFruit {
|
||||
Green = AppleFruit.Green,
|
||||
Red = AppleFruit.Red,
|
||||
Ripe = AppleFruit.Ripe
|
||||
}
|
||||
|
||||
export enum SpiritStoneFruit {
|
||||
Apricot = StoneFruit.Apricot,
|
||||
Nectarine = StoneFruit.Nectarine,
|
||||
Peach = StoneFruit.Peach,
|
||||
Plum = StoneFruit.Plum
|
||||
}
|
||||
|
||||
export enum SpiritRedFruit {
|
||||
Cherry = RedFruit.Cherry,
|
||||
Cranberry = RedFruit.Cranberry,
|
||||
Pomegranate = RedFruit.Pomegranate,
|
||||
Raspberry = RedFruit.Raspberry,
|
||||
SourCherry = RedFruit.SourCherry,
|
||||
Strawberry = RedFruit.Strawberry
|
||||
}
|
||||
|
||||
export enum SpiritGrapeFruit {
|
||||
Grape = GrapeFruit.Grape
|
||||
}
|
||||
|
||||
export enum SpiritBlackFruit {
|
||||
Blackberry = BlackFruit.Blackberry,
|
||||
Blackcurrant = BlackFruit.Blackcurrant,
|
||||
Boysenberry = BlackFruit.Boysenberry,
|
||||
Blueberry = BlackFruit.Blueberry,
|
||||
Olive = BlackFruit.Olive
|
||||
}
|
||||
|
||||
export enum SpiritTropicalFruit {
|
||||
Banana = TropicalFruit.Banana,
|
||||
Lychee = TropicalFruit.Lychee,
|
||||
Mango = TropicalFruit.Mango,
|
||||
PassionFruit = TropicalFruit.PassionFruit,
|
||||
Pineapple = TropicalFruit.Pineapple,
|
||||
Guava = TropicalFruit.Guava
|
||||
}
|
||||
|
||||
export enum SpiritMelonFruit {
|
||||
Cantaloupe = MelonFruit.Cantaloupe,
|
||||
Honeydew = MelonFruit.Honeydew
|
||||
}
|
||||
|
||||
export enum SpiritDriedFruit {
|
||||
Raisin = DriedFruit.Raisin,
|
||||
Prune = DriedFruit.Prune,
|
||||
Fig = DriedFruit.Fig,
|
||||
Date = DriedFruit.Date
|
||||
}
|
||||
|
||||
export enum SpiritFloral {
|
||||
Rose = Floral.Rose,
|
||||
Vanilla = Floral.Vanilla
|
||||
}
|
||||
|
||||
export enum SpiritSweet {
|
||||
Butterscotch = Sweet.Butterscotch,
|
||||
Caramel = Sweet.Caramel,
|
||||
Honey = Sweet.Honey,
|
||||
Molasses = Sweet.Molasses,
|
||||
Sugarcane = Sweet.Sugarcane,
|
||||
Toffee = Sweet.Toffee
|
||||
}
|
||||
|
||||
export enum SpiritVegetal {
|
||||
Basil = Vegetal.Basil,
|
||||
Coriander = Vegetal.Coriander,
|
||||
Grass = Vegetal.Grass,
|
||||
Herbaceous = Vegetal.Herbaceous,
|
||||
Mint = Vegetal.Mint
|
||||
}
|
||||
|
||||
export enum SpiritGrain {
|
||||
Barley = Grain.Barley,
|
||||
Bran = Grain.Bran,
|
||||
Cereal = Grain.Cereal,
|
||||
Corn = Grain.Corn,
|
||||
Grains = Grain.Grains,
|
||||
Oats = Grain.Oats,
|
||||
Porridge = Grain.Porridge,
|
||||
Rye = Grain.Rye,
|
||||
SteamedRice = Grain.SteamedRice
|
||||
}
|
||||
|
||||
export enum SpiritBotanical {
|
||||
Allspice = Botanical.Allspice,
|
||||
Anise = Botanical.Anise,
|
||||
Cedar = Botanical.Cedar,
|
||||
Cinnamon = Botanical.Cinnamon,
|
||||
Coconut = Botanical.Coconut,
|
||||
Cumin = Botanical.Cumin,
|
||||
Dill = Botanical.Dill,
|
||||
Ginger = Botanical.Ginger,
|
||||
Juniper = Botanical.Juniper,
|
||||
Liquorice = Botanical.Liquorice,
|
||||
Nutmeg = Botanical.Nutmeg,
|
||||
Pepper = Botanical.Pepper,
|
||||
Spices = Botanical.Spices,
|
||||
Tobacco = Botanical.Tobacco
|
||||
}
|
||||
|
||||
export enum SpiritDairy {
|
||||
Cheese = Dairy.Cheese,
|
||||
Cream = Dairy.Cream
|
||||
}
|
||||
|
||||
export enum SpiritUmami {
|
||||
Seaweed = Umami.Seaweed
|
||||
}
|
||||
|
||||
export enum SpiritMicrobiological {
|
||||
Farmyard = Microbiological.Farmyard,
|
||||
Leather = Microbiological.Leather,
|
||||
Meaty = Microbiological.Meaty,
|
||||
Mushroom = Microbiological.Mushroom,
|
||||
Yeasty = Microbiological.Yeasty
|
||||
}
|
||||
|
||||
export enum SpiritBurnt {
|
||||
Chocolate = Burnt.Chocolate,
|
||||
Coffee = Burnt.Coffee,
|
||||
ToastedBread = Burnt.ToastedBread,
|
||||
RoastedNuts = Burnt.RoastedNuts,
|
||||
CaramelisedNuts = Burnt.CaramelisedNuts
|
||||
}
|
||||
|
||||
export enum SpiritSmoke {
|
||||
Smoke = SmokeEnum.Smoke
|
||||
}
|
||||
|
||||
export enum SpiritOxidation {
|
||||
Sherry = Oxidation.Sherry
|
||||
}
|
||||
|
||||
export enum SpiritFault {
|
||||
NailVarnishRemover = Fault.NailVarnishRemover,
|
||||
Rubber = Fault.Rubber,
|
||||
Solvent = Fault.Solvent
|
||||
}
|
@ -1,257 +0,0 @@
|
||||
import {
|
||||
WhiteColour,
|
||||
AmberColour,
|
||||
RoseColour,
|
||||
RedColour,
|
||||
Floral,
|
||||
Sweet,
|
||||
Vegetal,
|
||||
Botanical,
|
||||
Nutty,
|
||||
Earth,
|
||||
Dairy,
|
||||
Umami,
|
||||
Microbiological,
|
||||
Salt,
|
||||
Burnt,
|
||||
SmokeEnum,
|
||||
Oxidation,
|
||||
Fault,
|
||||
CitrusFruit,
|
||||
AppleFruit,
|
||||
StoneFruit,
|
||||
RedFruit,
|
||||
GrapeFruit,
|
||||
BlackFruit,
|
||||
TropicalFruit,
|
||||
MelonFruit,
|
||||
DriedFruit
|
||||
} from '../'
|
||||
|
||||
export enum WineColour {
|
||||
WaterWhite = WhiteColour.WaterWhite,
|
||||
LemonGreen = WhiteColour.LemonGreen,
|
||||
Lemon = WhiteColour.Lemon,
|
||||
Gold = WhiteColour.Gold,
|
||||
WhiteBrown = WhiteColour.WhiteBrown,
|
||||
AmberOrange = AmberColour.AmberOrange,
|
||||
Amber = AmberColour.Amber,
|
||||
Pink = RoseColour.Pink,
|
||||
Salmon = RoseColour.Salmon,
|
||||
OnionSkin = RoseColour.OnionSkin,
|
||||
RoseOrange = RoseColour.RoseOrange,
|
||||
Purple = RedColour.Purple,
|
||||
Ruby = RedColour.Ruby,
|
||||
Garnet = RedColour.Garnet,
|
||||
Tawny = RedColour.Tawny,
|
||||
RedBrown = RedColour.RedBrown
|
||||
}
|
||||
|
||||
export enum WineCitrusFruit {
|
||||
Grapefruit = CitrusFruit.Grapefruit,
|
||||
Lemon = CitrusFruit.Lemon,
|
||||
Lime = CitrusFruit.Lime,
|
||||
Marmalade = CitrusFruit.Marmalade,
|
||||
Orange = CitrusFruit.Orange,
|
||||
Yuzu = CitrusFruit.Yuzu
|
||||
}
|
||||
|
||||
export enum WineAppleFruit {
|
||||
Green = AppleFruit.Green,
|
||||
Red = AppleFruit.Red,
|
||||
Ripe = AppleFruit.Ripe
|
||||
}
|
||||
|
||||
export enum WineStoneFruit {
|
||||
Apricot = StoneFruit.Apricot,
|
||||
Nectarine = StoneFruit.Nectarine,
|
||||
Peach = StoneFruit.Peach,
|
||||
Plum = StoneFruit.Plum
|
||||
}
|
||||
|
||||
export enum WineRedFruit {
|
||||
Cherry = RedFruit.Cherry,
|
||||
Cranberry = RedFruit.Cranberry,
|
||||
Pomegranate = RedFruit.Pomegranate,
|
||||
Raspberry = RedFruit.Raspberry,
|
||||
SourCherry = RedFruit.SourCherry,
|
||||
Strawberry = RedFruit.Strawberry
|
||||
}
|
||||
|
||||
export enum WineGrapeFruit {
|
||||
Grape = GrapeFruit.Grape
|
||||
}
|
||||
|
||||
export enum WineBlackFruit {
|
||||
Blackberry = BlackFruit.Blackberry,
|
||||
Blackcurrant = BlackFruit.Blackcurrant,
|
||||
Boysenberry = BlackFruit.Boysenberry,
|
||||
Blueberry = BlackFruit.Blueberry,
|
||||
Olive = BlackFruit.Olive
|
||||
}
|
||||
|
||||
export enum WineTropicalFruit {
|
||||
Banana = TropicalFruit.Banana,
|
||||
Lychee = TropicalFruit.Lychee,
|
||||
Mango = TropicalFruit.Mango,
|
||||
PassionFruit = TropicalFruit.PassionFruit,
|
||||
Pineapple = TropicalFruit.Pineapple,
|
||||
Guava = TropicalFruit.Guava
|
||||
}
|
||||
|
||||
export enum WineMelonFruit {
|
||||
Cantaloupe = MelonFruit.Cantaloupe,
|
||||
Honeydew = MelonFruit.Honeydew
|
||||
}
|
||||
|
||||
export enum WineDriedFruit {
|
||||
Raisin = DriedFruit.Raisin,
|
||||
Prune = DriedFruit.Prune,
|
||||
Fig = DriedFruit.Fig,
|
||||
Date = DriedFruit.Date
|
||||
}
|
||||
|
||||
export enum WineFloral {
|
||||
Acacia = Floral.Acacia,
|
||||
Elderflower = Floral.Elderflower,
|
||||
Hibiscus = Floral.Hibiscus,
|
||||
Honeysuckle = Floral.Honeysuckle,
|
||||
Jasmine = Floral.Jasmine,
|
||||
Lavender = Floral.Lavender,
|
||||
Lilac = Floral.Lilac,
|
||||
OrangeBlossom = Floral.OrangeBlossom,
|
||||
Potpourri = Floral.Potpourri,
|
||||
Rose = Floral.Rose,
|
||||
Vanilla = Floral.Vanilla,
|
||||
Violet = Floral.Violet
|
||||
}
|
||||
|
||||
export enum WineSweet {
|
||||
Butterscotch = Sweet.Butterscotch,
|
||||
Caramel = Sweet.Caramel,
|
||||
Honey = Sweet.Honey,
|
||||
Molasses = Sweet.Molasses,
|
||||
Toffee = Sweet.Toffee
|
||||
}
|
||||
|
||||
export enum WineVegetal {
|
||||
Artichoke = Vegetal.Artichoke,
|
||||
Asparagus = Vegetal.Asparagus,
|
||||
Basil = Vegetal.Basil,
|
||||
BellPepper = Vegetal.BellPepper,
|
||||
BlackTea = Vegetal.BlackTea,
|
||||
Capsicum = Vegetal.Capsicum,
|
||||
Coriander = Vegetal.Coriander,
|
||||
Eucalyptus = Vegetal.Eucalyptus,
|
||||
Gooseberry = Vegetal.Gooseberry,
|
||||
Grass = Vegetal.Grass,
|
||||
Hay = Vegetal.Hay,
|
||||
Herbaceous = Vegetal.Herbaceous,
|
||||
Lemongrass = Vegetal.Lemongrass,
|
||||
Mint = Vegetal.Mint
|
||||
}
|
||||
|
||||
export enum WineBotanical {
|
||||
Allspice = Botanical.Allspice,
|
||||
Anise = Botanical.Anise,
|
||||
Cedar = Botanical.Cedar,
|
||||
Cinnamon = Botanical.Cinnamon,
|
||||
Cloves = Botanical.Cloves,
|
||||
Coconut = Botanical.Coconut,
|
||||
Cumin = Botanical.Cumin,
|
||||
Dill = Botanical.Dill,
|
||||
Ginger = Botanical.Ginger,
|
||||
Juniper = Botanical.Juniper,
|
||||
Liquorice = Botanical.Liquorice,
|
||||
Nutmeg = Botanical.Nutmeg,
|
||||
Pepper = Botanical.Pepper,
|
||||
Sandalwood = Botanical.Sandalwood,
|
||||
Spices = Botanical.Spices,
|
||||
Tobacco = Botanical.Tobacco
|
||||
}
|
||||
|
||||
export enum WineNutty {
|
||||
Almond = Nutty.Almond,
|
||||
Cashew = Nutty.Cashew,
|
||||
Chestnut = Nutty.Chestnut,
|
||||
Hazelnut = Nutty.Hazelnut,
|
||||
Marzipan = Nutty.Marzipan,
|
||||
Walnut = Nutty.Walnut
|
||||
}
|
||||
|
||||
export enum WineEarth {
|
||||
Gravel = Earth.Gravel,
|
||||
Kerosene = Earth.Kerosene,
|
||||
RedBeet = Earth.RedBeet,
|
||||
Rocks = Earth.Rocks,
|
||||
Slate = Earth.Slate,
|
||||
Soil = Earth.Soil,
|
||||
Terracotta = Earth.Terracotta
|
||||
}
|
||||
|
||||
export enum WineDairy {
|
||||
Butter = Dairy.Butter,
|
||||
Cheese = Dairy.Cheese,
|
||||
Cream = Dairy.Cream,
|
||||
Yoghurt = Dairy.Yoghurt
|
||||
}
|
||||
|
||||
export enum WineUmami {
|
||||
Seaweed = Umami.Seaweed,
|
||||
Soy = Umami.Soy
|
||||
}
|
||||
|
||||
export enum WineMicrobiological {
|
||||
Animal = Microbiological.Animal,
|
||||
BreadDough = Microbiological.BreadDough,
|
||||
Brioche = Microbiological.Brioche,
|
||||
Farmyard = Microbiological.Farmyard,
|
||||
Iodine = Microbiological.Iodine,
|
||||
Leather = Microbiological.Leather,
|
||||
Meaty = Microbiological.Meaty,
|
||||
Mouse = Microbiological.Mouse,
|
||||
Mushroom = Microbiological.Mushroom,
|
||||
Truffles = Microbiological.Truffles,
|
||||
Vinyl = Microbiological.Vinyl,
|
||||
Yeasty = Microbiological.Yeasty
|
||||
}
|
||||
|
||||
export enum WineSalt {
|
||||
Brine = Salt.Brine
|
||||
}
|
||||
|
||||
export enum WineBurnt {
|
||||
Chocolate = Burnt.Chocolate,
|
||||
Coffee = Burnt.Coffee,
|
||||
ToastedBread = Burnt.ToastedBread,
|
||||
RoastedNuts = Burnt.RoastedNuts,
|
||||
CaramelisedNuts = Burnt.CaramelisedNuts
|
||||
}
|
||||
|
||||
export enum WineSmoke {
|
||||
Smoke = SmokeEnum.Smoke
|
||||
}
|
||||
|
||||
export enum WineOxidation {
|
||||
Aldehydes = Oxidation.Aldehydes,
|
||||
Madeirised = Oxidation.Madeirised,
|
||||
Sherry = Oxidation.Sherry,
|
||||
Staleness = Oxidation.Staleness
|
||||
}
|
||||
|
||||
export enum WineFault {
|
||||
BalsamicVinegar = Fault.BalsamicVinegar,
|
||||
Cabbage = Fault.Cabbage,
|
||||
Eggs = Fault.Eggs,
|
||||
Garlic = Fault.Garlic,
|
||||
Mercaptans = Fault.Mercaptans,
|
||||
Mustiness = Fault.Mustiness,
|
||||
NailVarnishRemover = Fault.NailVarnishRemover,
|
||||
Onion = Fault.Onion,
|
||||
Rubber = Fault.Rubber,
|
||||
Solvent = Fault.Solvent,
|
||||
SourMilk = Fault.SourMilk,
|
||||
Sweat = Fault.Sweat,
|
||||
Trichloroanisole = Fault.Trichloroanisole,
|
||||
WetCardboard = Fault.WetCardboard
|
||||
}
|
@ -1,106 +0,0 @@
|
||||
export enum TextureAndBalanceKey {
|
||||
Sweetness = 'sweetness',
|
||||
Acidity = 'acidity',
|
||||
Tannin = 'tannin',
|
||||
Alcohol = 'alcohol',
|
||||
Body = 'body',
|
||||
FlavourIntensity = 'flavourIntensity',
|
||||
PalateLength = 'palateLength',
|
||||
Reasoning = 'reasoning',
|
||||
Quality = 'quality',
|
||||
Age = 'age',
|
||||
ReadinessToDrink = 'readinessToDrink'
|
||||
}
|
||||
|
||||
export enum Sweetness {
|
||||
Dry = 'Dry',
|
||||
OffDry = 'Off-dry',
|
||||
Medium = 'Medium',
|
||||
Sweet = 'Sweet',
|
||||
Luscious = 'Luscious'
|
||||
}
|
||||
|
||||
export enum Concentration {
|
||||
Low = 'Low',
|
||||
Medium = 'Medium',
|
||||
High = 'High'
|
||||
}
|
||||
|
||||
export enum TanninString {
|
||||
NA = 'NA'
|
||||
}
|
||||
|
||||
export interface TanninObject {
|
||||
[Concentration.Low]: {
|
||||
[key in TanninType]: RipeTannin | UnripeTannin
|
||||
}
|
||||
[Concentration.Medium]: {
|
||||
[key in TanninType]: RipeTannin | UnripeTannin
|
||||
}
|
||||
[Concentration.High]: {
|
||||
[key in TanninType]: RipeTannin | UnripeTannin
|
||||
}
|
||||
}
|
||||
|
||||
export enum TanninType {
|
||||
Ripe = 'Ripe',
|
||||
Unripe = 'Unripe'
|
||||
}
|
||||
|
||||
export enum RipeTannin {
|
||||
Soft = 'Soft',
|
||||
FineGrained = 'Fine-grained',
|
||||
Coarse = 'Coarse'
|
||||
}
|
||||
|
||||
export enum UnripeTannin {
|
||||
Green = 'Green',
|
||||
Stalky = 'Stalky'
|
||||
}
|
||||
|
||||
export enum Body {
|
||||
Light = 'Light',
|
||||
Medium = 'Medium',
|
||||
Full = 'Full'
|
||||
}
|
||||
|
||||
export enum FlavourIntensity {
|
||||
Light = 'Light',
|
||||
Medium = 'Medium',
|
||||
Pronounced = 'Pronounced'
|
||||
}
|
||||
|
||||
export enum PalateLength {
|
||||
Short = 'Short',
|
||||
Medium = 'Medium',
|
||||
Pronounced = 'Pronounced',
|
||||
Exceptional = 'Exceptional'
|
||||
}
|
||||
|
||||
export enum ReasoningConcentration {
|
||||
Low = 'Low',
|
||||
High = 'High'
|
||||
}
|
||||
|
||||
export enum Quality {
|
||||
NS = 'NS',
|
||||
Poor = 'Poor',
|
||||
Acceptable = 'Acceptable',
|
||||
Good = 'Good',
|
||||
VeryGood = 'Very good',
|
||||
Excellent = 'Excellent',
|
||||
Outstanding = 'Outstanding'
|
||||
}
|
||||
|
||||
export enum ReadinessToDrink {
|
||||
TooYoung = 'Too young',
|
||||
DrinkWithPotentialForAgeing = 'Drink with potential for ageing',
|
||||
DrinkNow = 'Drink Now',
|
||||
TooOldPassed = 'Too old/Passed'
|
||||
}
|
||||
|
||||
export enum ReasoningKey {
|
||||
Balance = 'balance',
|
||||
Concentration = 'concentration',
|
||||
Complex = 'complex'
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
export enum VisualAssessmentKey {
|
||||
Clarity = 'clarity',
|
||||
Nature = 'nature',
|
||||
Colour = 'colour'
|
||||
}
|
||||
|
||||
export enum ClarityVisualAssessment {
|
||||
Clear = 'Clear',
|
||||
Cloudy = 'Cloudy',
|
||||
Opaque = 'Opaque'
|
||||
}
|
||||
|
||||
export enum NatureVisualAssessment {
|
||||
Still = 'Still',
|
||||
Frizzante = 'Frizzante',
|
||||
Sparkling = 'Sparkling'
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
export enum Route {
|
||||
export enum Routes {
|
||||
Users = '/users',
|
||||
NostrEvents = '/nostr',
|
||||
Reviews = '/reviews',
|
||||
@ -7,20 +7,3 @@ export enum Route {
|
||||
Spirits = '/spirits',
|
||||
Coffee = '/coffee'
|
||||
}
|
||||
|
||||
export enum ResponseStatus {
|
||||
OK = 200,
|
||||
Created = 201,
|
||||
NotModified = 304,
|
||||
BadRequest = 400,
|
||||
Unauthorized = 401,
|
||||
NotFound = 404,
|
||||
InternalServerError = 500
|
||||
}
|
||||
|
||||
export enum HTTPmethod {
|
||||
GET = 'GET',
|
||||
POST = 'POST',
|
||||
PUT = 'PUT',
|
||||
DELETE = 'DELETE'
|
||||
}
|
||||
|
@ -1,130 +1,9 @@
|
||||
export enum SakeDesignation {
|
||||
Table = 'Table',
|
||||
Pure = 'Pure',
|
||||
Blended = 'Blended' // Blended with Spirit - up to 10% of final volume
|
||||
}
|
||||
export type SakeDesignation =
|
||||
| 'table'
|
||||
| 'pure'
|
||||
| 'blended'
|
||||
| 'mirin:new'
|
||||
| 'mirin:true'
|
||||
| 'mirin:salt'
|
||||
|
||||
export enum TableSakeDesignation {
|
||||
FutsūShu = 'Futsū-shu'
|
||||
}
|
||||
|
||||
export enum PureSakeDesignation {
|
||||
Junmai = 'Junmai',
|
||||
JunmaiGinjo = 'Junmai Ginjo',
|
||||
JunmaiDaiginjo = 'Junmai Daiginjo'
|
||||
}
|
||||
|
||||
export enum BlendedSakeDesignation {
|
||||
Honjozo = 'Honjozo',
|
||||
Ginjo = 'Ginjo',
|
||||
Daiginjo = 'Daiginjo'
|
||||
}
|
||||
|
||||
export enum SakeStarter {
|
||||
Kimoto = 'Kimoto',
|
||||
Sokujō = 'Sokujō',
|
||||
Yamahai = 'Yamahai'
|
||||
}
|
||||
|
||||
export enum SakeYeastStrain {
|
||||
KyokaiNo6 = 'Kyokai No. 6',
|
||||
KyokaiNo7 = 'Kyokai No. 7',
|
||||
KyokaiNo9 = 'Kyokai No. 9',
|
||||
Sake101 = 'Sake 101',
|
||||
Sake301 = 'Sake 301',
|
||||
AK1 = 'AK-1',
|
||||
K61 = 'K6-1',
|
||||
K91 = 'K9-1',
|
||||
KA11 = 'KA-11',
|
||||
ShinseiYeast = 'Shinsei Yeast',
|
||||
HokkaidoYeast = 'Hokkaido Yeast',
|
||||
TohokuYeast = 'Tohoku Yeast',
|
||||
KansaiYeast = 'Kansai Yeast',
|
||||
ChugokuYeast = 'Chugoku Yeast',
|
||||
ShikokuYeast = 'Shikoku Yeast',
|
||||
KyushuYeast = 'Kyushu Yeast',
|
||||
GinjoYeast = 'Ginjo Yeast',
|
||||
DaiginjoYeast = 'Daiginjo Yeast',
|
||||
JunmaiYeast = 'Junmai Yeast',
|
||||
KimotoYeast = 'Kimoto Yeast',
|
||||
YamahaiYeast = 'Yamahai Yeast'
|
||||
}
|
||||
|
||||
export enum SakeKoji {
|
||||
AkitaKoji = 'Akita Koji',
|
||||
HiguchiKoji = 'Higuchi Koji',
|
||||
HiroshimaKoji = 'Hiroshima Koji',
|
||||
KiKoji = 'Ki Koji',
|
||||
KumamotoKoji = 'Kumamoto Koji',
|
||||
KyokaiKoji = 'Kyokai Koji',
|
||||
NaganoKoji = 'Nagano Koji',
|
||||
NishikiKoji = 'Nishiki Koji',
|
||||
ShinmeiKoji = 'Shinmei Koji',
|
||||
TakahashiKoji = 'Takahashi Koji',
|
||||
AssociationNo6 = 'Association No. 6',
|
||||
AssociationNo9 = 'Association No. 9',
|
||||
AssociationNo10 = 'Association No. 10',
|
||||
KA1 = 'KA-1',
|
||||
KA4 = 'KA-4',
|
||||
K7 = 'K7',
|
||||
ShinseiKoji = 'Shinsei Koji',
|
||||
HokkaidoKoji = 'Hokkaido Koji',
|
||||
TohokuKoji = 'Tohoku Koji',
|
||||
KansaiKoji = 'Kansai Koji',
|
||||
ChugokuKoji = 'Chugoku Koji',
|
||||
ShikokuKoji = 'Shikoku Koji',
|
||||
KyushuKoji = 'Kyushu Koji'
|
||||
}
|
||||
|
||||
export interface SakePolishMin {
|
||||
min: number
|
||||
}
|
||||
|
||||
export enum SakeCharacteristic {
|
||||
LightAndRefreshing = 'Light and Refreshing',
|
||||
CleanAndCrisp = 'Clean and Crisp',
|
||||
FruityAndAromatic = 'Fruity and Aromatic',
|
||||
RichAndUmami = 'Rich and Umami',
|
||||
ComplexAndLayered = 'Complex and Layered',
|
||||
RobustAndFullBodied = 'Robust and Full-Bodied'
|
||||
}
|
||||
|
||||
export enum SakeVolume {
|
||||
'0.18L' = '0.18L',
|
||||
'0.3L' = '0.3L',
|
||||
'0.5L' = '0.5L',
|
||||
'0.72L' = '0.72L',
|
||||
'1.8L' = '1.8L',
|
||||
'3.6L' = '3.6L'
|
||||
}
|
||||
|
||||
export enum RiceVarietal {
|
||||
Blended = 'BLENDED',
|
||||
AkitaSakeKomachi = 'Akita Sake Komachi',
|
||||
Akitakomachi = 'Akitakomachi',
|
||||
DewaSansan = 'Dewa Sansan',
|
||||
Ginnosei = 'Ginnosei',
|
||||
Gohyakumangoku = 'Gohyakumangoku',
|
||||
HanaFubuki = 'Hana-Fubuki',
|
||||
HattanNishiki = 'Hattan-Nishiki',
|
||||
Hinohikari = 'Hinohikari',
|
||||
Hitomebore = 'Hitomebore',
|
||||
HyogoKitaNishiki = 'Hyogo Kita Nishiki',
|
||||
Ibaraki5 = 'Ibaraki 5',
|
||||
KairyōMai = 'Kairyō-mai',
|
||||
KitaNishiki = 'Kita Nishiki',
|
||||
KokuryūMai = 'Kokuryū-mai',
|
||||
MiyamaNishiki = 'Miyama Nishiki',
|
||||
NakateShinseiki = 'Nakate Shinseiki',
|
||||
NiigataKoshihikari = 'Niigata Koshihikari',
|
||||
Notohikari = 'Notohikari',
|
||||
Ōmachi = 'Ōmachi',
|
||||
Sakamai = 'Sakamai',
|
||||
Sankei65 = 'Sankei 65',
|
||||
Shinriki = 'Shinriki',
|
||||
Tamazakae = 'Tamazakae',
|
||||
Tōkai14 = 'Tōkai 14',
|
||||
YamadaNishiki = 'Yamada Nishiki',
|
||||
Yamagata4 = 'Yamagata 4',
|
||||
YumeIkkon = 'Yume-Ikkon'
|
||||
}
|
||||
export type SakeStarter = 'Kimoto' | 'Sokujō' | 'Yamahai'
|
||||
|
@ -1,93 +1,172 @@
|
||||
export enum SpiritType {
|
||||
White = 'White',
|
||||
Dark = 'Dark',
|
||||
Liqueurs = 'Liqueurs'
|
||||
export type SpiritType = 'white' | 'dark' | 'liqueurs'
|
||||
|
||||
export type SpiritVariant =
|
||||
| 'Absinthe'
|
||||
| 'Pastis'
|
||||
| 'Vodka'
|
||||
| 'Genever'
|
||||
| 'Gin'
|
||||
| 'Mezcal'
|
||||
| 'Rum'
|
||||
| 'Eau de Vie'
|
||||
| 'Grappa'
|
||||
| 'Baijiu'
|
||||
| 'Soju'
|
||||
| 'Absinthe'
|
||||
| 'Brandy'
|
||||
| 'Calvados'
|
||||
| 'Chartreuse'
|
||||
| 'Genever'
|
||||
| 'Mezcal'
|
||||
| 'Rum'
|
||||
| 'Slivovitz'
|
||||
| 'Whiskey'
|
||||
| 'Amaro'
|
||||
| 'Coffee'
|
||||
| 'Cream'
|
||||
| 'Creme'
|
||||
| 'Flowers'
|
||||
| 'Fruit'
|
||||
| 'Herb'
|
||||
| 'Honey'
|
||||
| 'Nut'
|
||||
|
||||
export interface WhiteSpiritKind {
|
||||
Absinthe: ['Blanche']
|
||||
Pastis: [
|
||||
'Anise',
|
||||
'Fennel',
|
||||
'Licorice Root',
|
||||
'Hyssop',
|
||||
'Mint',
|
||||
'Citrus Peel',
|
||||
'Coriander Seeds',
|
||||
'Angelica Root',
|
||||
'Cinnamon',
|
||||
'Clove'
|
||||
]
|
||||
Vodka: [
|
||||
'Wheat',
|
||||
'Rye',
|
||||
'Corn',
|
||||
'Potato',
|
||||
'Barley',
|
||||
'Sugarcane',
|
||||
'Fruits',
|
||||
'Grains'
|
||||
]
|
||||
Genever: [
|
||||
{
|
||||
Young: 'Juniper'
|
||||
}
|
||||
]
|
||||
Gin: [
|
||||
{
|
||||
'London Dry': [
|
||||
'Juniper',
|
||||
'Coriander',
|
||||
'Angelica root',
|
||||
'Lemon peel',
|
||||
'Orange peel',
|
||||
'Orris root',
|
||||
'Cassia bark',
|
||||
'Licorice root',
|
||||
'Grapefruit peel',
|
||||
'Elderflower'
|
||||
]
|
||||
},
|
||||
'Plymouth'
|
||||
]
|
||||
Mezcal: [{ Joven: ['espadín', 'tepeztate', 'Tequilana (blue)', 'tobalá'] }]
|
||||
Rum: ['Blanco', 'Cachaça', 'Platino', 'Agricole']
|
||||
'Eau de Vie': [
|
||||
'Apple (Pomme)',
|
||||
'Blackcurrant (Kirsch)',
|
||||
'Butterscotch (Schnapps)',
|
||||
'Peach (Pêche, Schnapps)',
|
||||
'Pear (Poire William)',
|
||||
'Plum (Mirabelle, Slivovitz, Rakia)',
|
||||
'Raspberries (Framboise)'
|
||||
]
|
||||
Grappa: ['Marc', 'Pisco']
|
||||
Baijiu: ['Sorghum', 'Wheat', 'Barley', 'Rice', 'Millet']
|
||||
Soju: ['Barley', 'Brown sugar', 'Buckwheat', 'Rice', 'Sweet Potato']
|
||||
}
|
||||
|
||||
export enum SpiritVariant {
|
||||
Absinthe = 'Absinthe',
|
||||
Pastis = 'Pastis',
|
||||
Vodka = 'Vodka',
|
||||
Gin = 'Gin',
|
||||
Mezcal = 'Mezcal',
|
||||
Eau = 'Eau de Vie',
|
||||
Grappa = 'Grappa',
|
||||
Baijiu = 'Baijiu',
|
||||
Soju = 'Soju',
|
||||
Brandy = 'Brandy',
|
||||
Calvados = 'Calvados',
|
||||
Chartreuse = 'Chartreuse',
|
||||
Genever = 'Genever',
|
||||
Rum = 'Rum',
|
||||
Slivovitz = 'Slivovitz',
|
||||
Whiskey = 'Whiskey',
|
||||
Amaro = 'Amaro',
|
||||
Coffee = 'Coffee',
|
||||
Cream = 'Cream',
|
||||
Creme = 'Creme',
|
||||
Flowers = 'Flowers',
|
||||
Fruit = 'Fruit',
|
||||
Herb = 'Herb',
|
||||
Honey = 'Honey',
|
||||
Nut = 'Nut'
|
||||
export interface DarkSpiritKind {
|
||||
Absinthe: ['Jaune', 'Verte']
|
||||
Brandy: [
|
||||
{
|
||||
Grape: [
|
||||
'VS',
|
||||
'VSOP',
|
||||
'XO',
|
||||
'Beyond Age',
|
||||
'Solera',
|
||||
'Solera Reserva',
|
||||
'Solera Gran Reserva'
|
||||
]
|
||||
}
|
||||
]
|
||||
Calvados: ['Apple', 'Pear']
|
||||
Chartreuse: ['Green', 'Yellow']
|
||||
Genever: [{ Old: ['Juniper'] }, { Coren: ['Juniper'] }]
|
||||
Mezcal: ['Reposado', 'Abuelo', 'Añejo', 'Extra Añejo']
|
||||
Rum: [
|
||||
{
|
||||
Sugar: [
|
||||
'Cachaca (amarela/ouro)',
|
||||
'Dark Rum',
|
||||
'Gold Rum',
|
||||
'Over-proof',
|
||||
'Premium',
|
||||
'Spiced'
|
||||
]
|
||||
}
|
||||
]
|
||||
Slivovitz: []
|
||||
Whiskey: ['Barley', 'Rye', 'Wheat', 'Corn', 'Oat', 'Rice']
|
||||
}
|
||||
|
||||
export enum SpiritVolume {
|
||||
'0.05L' = '0.05L',
|
||||
'0.15L' = '0.15L',
|
||||
'0.25L' = '0.25L',
|
||||
'0.375L' = '0.375L',
|
||||
'0.5L' = '0.5L',
|
||||
'0.7L' = '0.7L',
|
||||
'1L' = '1L'
|
||||
}
|
||||
|
||||
export enum SpiritCharacteristic {
|
||||
LightAndNeutral = 'Light and Neutral',
|
||||
FruityAndAromatic = 'Fruity and Aromatic',
|
||||
HerbalAndBotanical = 'Herbal and Botanical',
|
||||
SweetAndSyrupy = 'Sweet and Syrupy',
|
||||
SmokyAndSpicy = 'Smoky and Spicy',
|
||||
RichAndFullBodied = 'Rich and Full-Bodied'
|
||||
}
|
||||
|
||||
export enum WhiteSpiritVariant {
|
||||
Absinthe = 'Absinthe',
|
||||
Pastis = 'Pastis',
|
||||
Vodka = 'Vodka',
|
||||
Genever = 'Genever',
|
||||
Gin = 'Gin',
|
||||
Mezcal = 'Mezcal',
|
||||
Rum = 'Rum',
|
||||
EauDeVie = 'Eau de Vie',
|
||||
Grappa = 'Grappa',
|
||||
Baijiu = 'Baijiu',
|
||||
Soju = 'Soju',
|
||||
Aquavit = 'Aquavit',
|
||||
Arrack = 'Arrack'
|
||||
}
|
||||
|
||||
export enum DarkSpiritVariant {
|
||||
Absinthe = 'Absinthe',
|
||||
Brandy = 'Brandy',
|
||||
Calvados = 'Calvados',
|
||||
Chartreuse = 'Chartreuse',
|
||||
Genever = 'Genever',
|
||||
Mezcal = 'Mezcal',
|
||||
Rum = 'Rum',
|
||||
Slivovitz = 'Slivovitz',
|
||||
Arrack = 'Arrack',
|
||||
Whiskey = 'Whiskey'
|
||||
}
|
||||
|
||||
export enum LiqueursSpiritVariant {
|
||||
Amaro = 'Amaro',
|
||||
Coffee = 'Coffee',
|
||||
Cream = 'Cream',
|
||||
Creme = 'Creme',
|
||||
Flowers = 'Flowers',
|
||||
Fruit = 'Fruit',
|
||||
Herb = 'Herb',
|
||||
Honey = 'Honey',
|
||||
Nut = 'Nut'
|
||||
export interface LiqueursSpiritKind {
|
||||
Amaro: []
|
||||
Coffee: []
|
||||
Cream: [
|
||||
'Egg (Advocaat)',
|
||||
'Amarula',
|
||||
'Rum',
|
||||
'Strawberry',
|
||||
'Whiskey (Baileys etc)'
|
||||
]
|
||||
Creme: [
|
||||
'Almond',
|
||||
'Banana',
|
||||
'Blackcurrant',
|
||||
'Chocolate',
|
||||
'Peach',
|
||||
'Sour Cherry',
|
||||
'Violet'
|
||||
]
|
||||
Flowers: ['Rose', 'Violet', 'Elderflower']
|
||||
Fruit: [
|
||||
'Blackcurrant',
|
||||
'Lemon',
|
||||
'Melon',
|
||||
'Orange',
|
||||
'Peach',
|
||||
'Plum',
|
||||
'Raspberry',
|
||||
'Yuzu'
|
||||
]
|
||||
Herb: [
|
||||
'Anise',
|
||||
'Dom Benedictine',
|
||||
'Bitters',
|
||||
'Ginger',
|
||||
'Jägermeister',
|
||||
'Metaxa',
|
||||
'Mint'
|
||||
]
|
||||
Honey: ['Licor 43', 'Rum', 'Vodka', 'Whiskey']
|
||||
Nut: ['Almond', 'Apricot Kernel', 'Hazelnut', 'Peanut', 'Pecan', 'Walnut']
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
export enum UserRole {
|
||||
Reviewer = 'Reviewer', // the only user role that can submit a review
|
||||
Producer = 'Producer' // the only user role that can submit a product
|
||||
User = 'user',
|
||||
Reviewer = 'reviewer',
|
||||
Producer = 'producer'
|
||||
}
|
||||
|
@ -1,53 +1,20 @@
|
||||
export enum WineType {
|
||||
White = 'White',
|
||||
Amber = 'Amber',
|
||||
Rose = 'Rose',
|
||||
Red = 'Red'
|
||||
}
|
||||
|
||||
export enum WhiteWineCharacteristic {
|
||||
LightAromatic = 'Light Aromatic',
|
||||
TexturalAndSavory = 'Textural and Savory',
|
||||
RichAndFruitForward = 'Rich and Fruit Forward'
|
||||
}
|
||||
|
||||
export enum AmberWineCharacteristic {
|
||||
TexturalAndAromatic = 'Textural and Aromatic',
|
||||
StructuralAndSavory = 'Structural and Savory',
|
||||
PowerAndPresence = 'Power and Presence'
|
||||
}
|
||||
|
||||
export enum RoseWineCharacteristic {
|
||||
LightAndFruitForward = 'Light and Fruit Forward',
|
||||
TexturalAndSavory = 'Textural and Savory',
|
||||
StructuralAndAromatic = 'Structural and Aromatic'
|
||||
}
|
||||
|
||||
export enum RedWineCharacteristic {
|
||||
LightAndFruitForward = 'Light and Fruit Forward',
|
||||
StructuralAndAromatic = 'Structural and Aromatic',
|
||||
TexturalAndSavory = 'Textural and Savory',
|
||||
PowerAndPresence = 'Power and Presence'
|
||||
}
|
||||
|
||||
export enum WineStyle {
|
||||
BubblesAndFizz = 'Bubbles + Fizz',
|
||||
Table = 'Table',
|
||||
Dessert = 'Dessert',
|
||||
Fortified = 'Fortified',
|
||||
Vermouth = 'Vermouth'
|
||||
White = 'white',
|
||||
Amber = 'amber',
|
||||
Rose = 'rose',
|
||||
Red = 'red'
|
||||
}
|
||||
|
||||
export enum Viticulture {
|
||||
Biodynamic = 'Biodynamic',
|
||||
Organic = 'Organic',
|
||||
Conventional = 'Conventional'
|
||||
Biodynamic = 'biodynamic',
|
||||
Organic = 'organic',
|
||||
Conventional = 'conventional'
|
||||
}
|
||||
|
||||
export enum BottleClosure {
|
||||
Cork = 'Cork',
|
||||
CrownSeal = 'Crown-seal',
|
||||
Screwcap = 'Screwcap'
|
||||
Cork = 'cork',
|
||||
CrownSeal = 'crown-seal',
|
||||
Screwcap = 'screwcap'
|
||||
}
|
||||
|
||||
export interface WineRegion {
|
||||
@ -67,292 +34,3 @@ export enum WineVolume {
|
||||
'6L' = '6L',
|
||||
'12L' = '12L'
|
||||
}
|
||||
|
||||
export enum GrapeVarietal {
|
||||
FieldBlend = 'FIELD BLEND',
|
||||
Acolon = 'Acolon',
|
||||
Albariño = 'Albariño',
|
||||
Aligoté = 'Aligoté',
|
||||
Altesse = 'Altesse',
|
||||
Amigne = 'Amigne',
|
||||
Ansonica = 'Ansonica',
|
||||
AntãoVaz = 'Antão Vaz',
|
||||
Arbane = 'Arbane',
|
||||
ArboisBlanc = 'Arbois Blanc',
|
||||
Arneis = 'Arneis',
|
||||
Arrufiac = 'Arrufiac',
|
||||
Assyrtiko = 'Assyrtiko',
|
||||
Auxerrois = 'Auxerrois',
|
||||
Bacchus = 'Bacchus',
|
||||
Biancolella = 'Biancolella',
|
||||
Bical = 'Bical',
|
||||
BlancDuBois = 'Blanc du Bois',
|
||||
BombinoBianco = 'Bombino Bianco',
|
||||
Bourboulenc = 'Bourboulenc',
|
||||
Bovale = 'Bovale',
|
||||
Catarratto = 'Catarratto',
|
||||
Chardonnay = 'Chardonnay',
|
||||
Chasselas = 'Chasselas',
|
||||
CheninBlanc = 'Chenin Blanc',
|
||||
Clairette = 'Clairette',
|
||||
Colombard = 'Colombard',
|
||||
Cortese = 'Cortese',
|
||||
Courbu = 'Courbu',
|
||||
Couston = 'Couston',
|
||||
Crouchen = 'Crouchen',
|
||||
Duras = 'Duras',
|
||||
Elbling = 'Elbling',
|
||||
Emir = 'Emir',
|
||||
Falanghina = 'Falanghina',
|
||||
FernãoPires = 'Fernão Pires',
|
||||
Fiano = 'Fiano',
|
||||
FolleBlanche = 'Folle Blanche',
|
||||
Friulano = 'Friulano',
|
||||
Furmint = 'Furmint',
|
||||
Gaglioppo = 'Gaglioppo',
|
||||
GamayBlanc = 'Gamay Blanc',
|
||||
GarnachaBlanca = 'Garnacha Blanca',
|
||||
Gascon = 'Gascon',
|
||||
Gavi = 'Gavi',
|
||||
Gewürztraminer = 'Gewürztraminer',
|
||||
Godello = 'Godello',
|
||||
GouaisBlanc = 'Gouais Blanc',
|
||||
Grechetto = 'Grechetto',
|
||||
GrenacheBlanc = 'Grenache Blanc',
|
||||
GrosManseng = 'Gros Manseng',
|
||||
GrünerVeltliner = 'Grüner Veltliner',
|
||||
Hárslevelü = 'Hárslevelü',
|
||||
Huxelrebe = 'Huxelrebe',
|
||||
Inzolia = 'Inzolia',
|
||||
Jacquère = 'Jacquère',
|
||||
Kerner = 'Kerner',
|
||||
KleinConstantia = 'Klein Constantia',
|
||||
Kunegund = 'Kunegund',
|
||||
Lagarino = 'Lagarino',
|
||||
Luglienga = 'Luglienga',
|
||||
Macabeo = 'Macabeo',
|
||||
Malvasia = 'Malvasia',
|
||||
Marsanne = 'Marsanne',
|
||||
MelonDeBourgogne = 'Melon de Bourgogne',
|
||||
MerlotBlanc = 'Merlot Blanc',
|
||||
Minutolo = 'Minutolo',
|
||||
Moscato = 'Moscato',
|
||||
MüllerThurgau = 'Müller-Thurgau',
|
||||
Muscadelle = 'Muscadelle',
|
||||
Muscat = 'Muscat',
|
||||
Nascetta = 'Nascetta',
|
||||
Nosiola = 'Nosiola',
|
||||
Nuragus = 'Nuragus',
|
||||
Okçular = 'Okçular',
|
||||
Ondenc = 'Ondenc',
|
||||
Oran = 'Oran',
|
||||
PacherencDuVicBilh = 'Pacherenc du Vic-Bilh',
|
||||
PansaBlanca = 'Pansa Blanca',
|
||||
Parellada = 'Parellada',
|
||||
Pecorino = 'Pecorino',
|
||||
PedroXiménez = 'Pedro Ximénez',
|
||||
PetitManseng = 'Petit Manseng',
|
||||
PetitMeslier = 'Petit Meslier',
|
||||
Picolit = 'Picolit',
|
||||
Picpoul = 'Picpoul',
|
||||
PinotBlanc = 'Pinot Blanc',
|
||||
PinotGrigio = 'Pinot Grigio',
|
||||
PinotGris = 'Pinot Gris',
|
||||
PinotMeunier = 'Pinot Meunier',
|
||||
PinotNoirBlanc = 'Pinot Noir Blanc',
|
||||
PiquepoulBlanc = 'Piquepoul Blanc',
|
||||
PlavacMali = 'Plavac Mali',
|
||||
Raboso = 'Raboso',
|
||||
Riesling = 'Riesling',
|
||||
RoterVeltliner = 'Roter Veltliner',
|
||||
Roupeiro = 'Roupeiro',
|
||||
Roussanne = 'Roussanne',
|
||||
SauvignonBlanc = 'Sauvignon Blanc',
|
||||
Savagnin = 'Savagnin',
|
||||
Scheurebe = 'Scheurebe',
|
||||
Sémillon = 'Sémillon',
|
||||
Sercial = 'Sercial',
|
||||
Siegerrebe = 'Siegerrebe',
|
||||
Silvaner = 'Silvaner',
|
||||
SouvignierGris = 'Souvignier Gris',
|
||||
Sylvaner = 'Sylvaner',
|
||||
Taminga = 'Taminga',
|
||||
TintaAmarela = 'Tinta Amarela',
|
||||
TintaBarroca = 'Tinta Barroca',
|
||||
Torrontés = 'Torrontés',
|
||||
Trebbiano = 'Trebbiano',
|
||||
Treixadura = 'Treixadura',
|
||||
UgniBlanc = 'Ugni Blanc',
|
||||
Verdejo = 'Verdejo',
|
||||
Verdelho = 'Verdelho',
|
||||
Verdicchio = 'Verdicchio',
|
||||
Vermentino = 'Vermentino',
|
||||
Viennoise = 'Viennoise',
|
||||
Viognier = 'Viognier',
|
||||
Vitovska = 'Vitovska',
|
||||
Xarello = 'Xarello',
|
||||
Xynomavro = 'Xynomavro',
|
||||
ZeleniVrh = 'Zeleni Vrh',
|
||||
Abbuoto = 'Abbuoto',
|
||||
Agiorgitiko = 'Agiorgitiko',
|
||||
Aglianico = 'Aglianico',
|
||||
Aladasturi = 'Aladasturi',
|
||||
Albarossa = 'Albarossa',
|
||||
AlicanteBouschet = 'Alicante Bouschet',
|
||||
Ancellotta = 'Ancellotta',
|
||||
Aragonez = 'Aragonez',
|
||||
Aramon = 'Aramon',
|
||||
Areni = 'Areni',
|
||||
Baga = 'Baga',
|
||||
Barbera = 'Barbera',
|
||||
Bastardo = 'Bastardo',
|
||||
Béquignol = 'Béquignol',
|
||||
BlackMuscat = 'Black Muscat',
|
||||
Blaufränkisch = 'Blaufränkisch',
|
||||
Bobal = 'Bobal',
|
||||
Boğazkere = 'Boğazkere',
|
||||
Bonarda = 'Bonarda',
|
||||
Bouchet = 'Bouchet',
|
||||
Brachetto = 'Brachetto',
|
||||
CabernetFranc = 'Cabernet Franc',
|
||||
CabernetSauvignon = 'Cabernet Sauvignon',
|
||||
CaiñoTinto = 'Caiño Tinto',
|
||||
Calabrese = 'Calabrese',
|
||||
Canaiolo = 'Canaiolo',
|
||||
Cannonau = 'Cannonau',
|
||||
Carignan = 'Carignan',
|
||||
Carmenère = 'Carmenère',
|
||||
Castelão = 'Castelão',
|
||||
Cataratto = 'Cataratto',
|
||||
Chambourcin = 'Chambourcin',
|
||||
Charbono = 'Charbono',
|
||||
Chenanson = 'Chenanson',
|
||||
Cinsault = 'Cinsault',
|
||||
Colonnata = 'Colonnata',
|
||||
Colorino = 'Colorino',
|
||||
Corvina = 'Corvina',
|
||||
Corvinone = 'Corvinone',
|
||||
Counoise = 'Counoise',
|
||||
Croatina = 'Croatina',
|
||||
Dolcetto = 'Dolcetto',
|
||||
Dornfelder = 'Dornfelder',
|
||||
Durif = 'Durif',
|
||||
Enantio = 'Enantio',
|
||||
Fer = 'Fer',
|
||||
Ferrandina = 'Ferrandina',
|
||||
FeteascăNeagră = 'Fetească Neagră',
|
||||
FogliaTonda = 'Foglia Tonda',
|
||||
Freisa = 'Freisa',
|
||||
Frühburgunder = 'Frühburgunder',
|
||||
Gamay = 'Gamay',
|
||||
Garnacha = 'Garnacha',
|
||||
Girò = 'Girò',
|
||||
GodelloTinto = 'Godello Tinto',
|
||||
Graciano = 'Graciano',
|
||||
Greco = 'Greco',
|
||||
Grenache = 'Grenache',
|
||||
Grolleau = 'Grolleau',
|
||||
GrosCabernet = 'Gros Cabernet',
|
||||
Guanciale = 'Guanciale',
|
||||
Helfensteiner = 'Helfensteiner',
|
||||
Heroldrebe = 'Heroldrebe',
|
||||
Kadarka = 'Kadarka',
|
||||
KalecikKarasi = 'Kalecik Karasi',
|
||||
Kékfrankos = 'Kékfrankos',
|
||||
Lagrein = 'Lagrein',
|
||||
Lambrusco = 'Lambrusco',
|
||||
Liatiko = 'Liatiko',
|
||||
ListánNegro = 'Listán Negro',
|
||||
LoureiroTinto = 'Loureiro Tinto',
|
||||
Magliocco = 'Magliocco',
|
||||
Malbec = 'Malbec',
|
||||
MalvasiaNera = 'Malvasia Nera',
|
||||
Mammolo = 'Mammolo',
|
||||
Mandolari = 'Mandolari',
|
||||
MansengNoir = 'Manseng Noir',
|
||||
Marzemino = 'Marzemino',
|
||||
Mauzac = 'Mauzac',
|
||||
Mavroudi = 'Mavroudi',
|
||||
Mencia = 'Mencia',
|
||||
Merlot = 'Merlot',
|
||||
Miro = 'Miro',
|
||||
Mission = 'Mission',
|
||||
Molinara = 'Molinara',
|
||||
Monastrell = 'Monastrell',
|
||||
Montepulciano = 'Montepulciano',
|
||||
MoraviaAgria = 'Moravia Agria',
|
||||
Morellino = 'Morellino',
|
||||
Mourvèdre = 'Mourvèdre',
|
||||
Müllerrebe = 'Müllerrebe',
|
||||
MuscatRouge = 'Muscat Rouge',
|
||||
Narince = 'Narince',
|
||||
Nebbiolo = 'Nebbiolo',
|
||||
Negoska = 'Negoska',
|
||||
NerelloCappuccio = 'Nerello Cappuccio',
|
||||
NerelloMascalese = 'Nerello Mascalese',
|
||||
Öküzgözü = 'Öküzgözü',
|
||||
Pais = 'Pais',
|
||||
Pallagrello = 'Pallagrello',
|
||||
Passetoutgrain = 'Passetoutgrain',
|
||||
Patrigone = 'Patrigone',
|
||||
PetitBouschet = 'Petit Bouschet',
|
||||
PetitVerdot = 'Petit Verdot',
|
||||
Pignatello = 'Pignatello',
|
||||
PinotNoir = 'Pinot Noir',
|
||||
Pinotage = 'Pinotage',
|
||||
PiquepoulNoir = 'Piquepoul Noir',
|
||||
Primitivo = 'Primitivo',
|
||||
Priorat = 'Priorat',
|
||||
Prokupac = 'Prokupac',
|
||||
Refosco = 'Refosco',
|
||||
RibollaGialla = 'Ribolla Gialla',
|
||||
Robola = 'Robola',
|
||||
Romano = 'Romano',
|
||||
Rondinella = 'Rondinella',
|
||||
Rossese = 'Rossese',
|
||||
Roussin = 'Roussin',
|
||||
RubyCabernet = 'Ruby Cabernet',
|
||||
Sagrantino = 'Sagrantino',
|
||||
Sangiovese = 'Sangiovese',
|
||||
Sansovino = 'Sansovino',
|
||||
Saperavi = 'Saperavi',
|
||||
Schioppettino = 'Schioppettino',
|
||||
Sciacarello = 'Sciacarello',
|
||||
SémillonRouge = 'Sémillon Rouge',
|
||||
Shiraz = 'Shiraz',
|
||||
SilvanerRouge = 'Silvaner Rouge',
|
||||
Souzão = 'Souzão',
|
||||
Spanna = 'Spanna',
|
||||
StLaurent = 'St. Laurent',
|
||||
Sultani = 'Sultani',
|
||||
Syrah = 'Syrah',
|
||||
Tannat = 'Tannat',
|
||||
Tarrango = 'Tarrango',
|
||||
Tempranillo = 'Tempranillo',
|
||||
Teroldego = 'Teroldego',
|
||||
TintaFrancisca = 'Tinta Francisca',
|
||||
TintaRoriz = 'Tinta Roriz',
|
||||
TintoFino = 'Tinto Fino',
|
||||
TourigaFranca = 'Touriga Franca',
|
||||
TourigaNacional = 'Touriga Nacional',
|
||||
Trincadeira = 'Trincadeira',
|
||||
Trollinger = 'Trollinger',
|
||||
UvaDiTroia = 'Uva di Troia',
|
||||
UvaLonganesi = 'Uva Longanesi',
|
||||
UvaRara = 'Uva Rara',
|
||||
Vaccarèse = 'Vaccarèse',
|
||||
Valdiguié = 'Valdiguié',
|
||||
Valpolicella = 'Valpolicella',
|
||||
VermentinoNero = 'Vermentino Nero',
|
||||
ViennoiseRouge = 'Viennoise Rouge',
|
||||
Vignoles = 'Vignoles',
|
||||
Vinhão = 'Vinhão',
|
||||
ViognierRouge = 'Viognier Rouge',
|
||||
VitisRiparia = 'Vitis Riparia',
|
||||
ZanteCurrant = 'Zante Currant',
|
||||
Zeni = 'Zeni',
|
||||
Žilavka = 'Žilavka',
|
||||
Zweigelt = 'Zweigelt'
|
||||
}
|
||||
|
||||
// retsina greek wine with pine essences should be considered a vermouth
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { SakeVolume, SpiritVolume, StandardDrink, WineVolume } from '../types'
|
||||
import { StandardDrinks, WineVolume } from '../types'
|
||||
import { roundToOneDecimal } from './'
|
||||
|
||||
export const alcoholToStandardDrinks = (
|
||||
alcohol: number,
|
||||
bottle: number
|
||||
): StandardDrink => {
|
||||
): StandardDrinks => {
|
||||
const UK100ml = roundToOneDecimal(10 * alcohol)
|
||||
const AU100ml = roundToOneDecimal(7.91 * alcohol)
|
||||
const US100ml = roundToOneDecimal(5.64 * alcohol)
|
||||
@ -25,9 +25,7 @@ export const alcoholToStandardDrinks = (
|
||||
}
|
||||
}
|
||||
|
||||
export const volumeToMl = (
|
||||
volume: WineVolume | SpiritVolume | SakeVolume
|
||||
): number => {
|
||||
export const volumeToMl = (volume: WineVolume): number => {
|
||||
if (volume.endsWith('L')) {
|
||||
const volumeMl = volume.replace('L', '')
|
||||
|
||||
|
@ -1,7 +0,0 @@
|
||||
import { BufferEncoding } from '../types'
|
||||
|
||||
export const encodeBase64 = (str: string): string =>
|
||||
Buffer.from(str, BufferEncoding.UTF8).toString(BufferEncoding.BASE64)
|
||||
|
||||
export const decodeBase64 = (str: string): string =>
|
||||
Buffer.from(str, BufferEncoding.BASE64).toString(BufferEncoding.UTF8)
|
@ -1 +0,0 @@
|
||||
export const MODIFICATION_PERIOD = 24 * 60 * 60 * 1000 // 24h in milliseconds
|
@ -1,2 +0,0 @@
|
||||
export const errorMessage = (err: unknown) =>
|
||||
err instanceof Error ? err.message : err
|
@ -4,6 +4,3 @@ export * from './route'
|
||||
export * from './utils'
|
||||
export * from './alcohol'
|
||||
export * from './wine'
|
||||
export * from './spirit'
|
||||
export * from './sake'
|
||||
export * from './const'
|
||||
|
@ -1,8 +1,4 @@
|
||||
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
|
||||
@ -36,38 +32,3 @@ export const npubToHex = (pubKey: string): string | null => {
|
||||
export const validateHex = (hexKey: string) => {
|
||||
return hexKey.match(/^[a-f0-9]{64}$/)
|
||||
}
|
||||
|
||||
const serializeEvent = (evt: Event) => {
|
||||
return JSON.stringify([
|
||||
0,
|
||||
evt.pubkey,
|
||||
evt.created_at,
|
||||
evt.kind,
|
||||
evt.tags,
|
||||
evt.content
|
||||
])
|
||||
}
|
||||
|
||||
const getEventHash = (event: Event) => {
|
||||
return sha256(serializeEvent(event)).toString(Hex)
|
||||
}
|
||||
|
||||
export const verifyNostrSignature = async (event: Event) => {
|
||||
const { sig, pubkey } = event
|
||||
|
||||
// FIXME: enable event expiry check
|
||||
// const eventCreatedAt = event.created_at
|
||||
// const timeNow = Math.round(Date.now() / 1000)
|
||||
// const timeDifference = timeNow - eventCreatedAt
|
||||
|
||||
// const maxTimeDifference = 300 // 5 minutes in seconds
|
||||
|
||||
// if (timeDifference > maxTimeDifference) {
|
||||
// throw new Error('Nostr signature verification failed. Event Expired.')
|
||||
// }
|
||||
|
||||
const eventHash = getEventHash(event)
|
||||
|
||||
// verify nostr signature
|
||||
return schnorr.verify(sig, eventHash, pubkey)
|
||||
}
|
||||
|
@ -1,312 +1,28 @@
|
||||
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'
|
||||
import { Response } from 'express'
|
||||
import { InsertOneResult } from 'mongodb'
|
||||
|
||||
export const handleGETreq = async (
|
||||
res: Response,
|
||||
dbCollection: DBcollection,
|
||||
item: DBinstance
|
||||
) => {
|
||||
try {
|
||||
const items = await collections[dbCollection]?.find({}).toArray()
|
||||
export const handleReqError = (res: Response, error: unknown) => {
|
||||
console.error(error)
|
||||
|
||||
handleReqSuccess(res, item, '', HTTPmethod.GET, items)
|
||||
} catch (err) {
|
||||
handleReqError(res, err, ResponseStatus.InternalServerError)
|
||||
if (error instanceof Error) {
|
||||
res.status(400).send(error.message)
|
||||
} else if (typeof error === 'string') {
|
||||
res.status(400).send(error)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
result: InsertOneResult<Document> | undefined,
|
||||
itemName: string
|
||||
) => {
|
||||
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
|
||||
if (result) {
|
||||
res
|
||||
.status(201)
|
||||
.send(
|
||||
`Successfully created a new ${itemName} with id ${result.insertedId}`
|
||||
)
|
||||
} else {
|
||||
res.status(500).send(`Failed to create a new ${itemName}.`)
|
||||
}
|
||||
}
|
||||
|
||||
export const handleReqNotModified = (res: Response) =>
|
||||
res.status(ResponseStatus.NotModified).send()
|
||||
|
||||
export const handleReqUnauthorized = (res: Response) =>
|
||||
res.status(ResponseStatus.Unauthorized).send()
|
||||
|
||||
export const handleReqNotFound = (res: Response, item: DBinstance) =>
|
||||
res
|
||||
.status(ResponseStatus.NotFound)
|
||||
.send(`"${item}" with provided "id" and associated with user not found`)
|
||||
|
@ -1,36 +0,0 @@
|
||||
import {
|
||||
SakeDesignation,
|
||||
TableSakeDesignation,
|
||||
PureSakeDesignation,
|
||||
BlendedSakeDesignation,
|
||||
SakePolishMin
|
||||
} from '../types'
|
||||
|
||||
export const sakePolishMap:
|
||||
| {
|
||||
[key in SakeDesignation.Table]: {
|
||||
[key in TableSakeDesignation]: SakePolishMin
|
||||
}
|
||||
}
|
||||
| {
|
||||
[key in SakeDesignation.Pure]: {
|
||||
[key in PureSakeDesignation]: SakePolishMin
|
||||
}
|
||||
}
|
||||
| {
|
||||
[key in SakeDesignation.Blended]: {
|
||||
[key in BlendedSakeDesignation]: SakePolishMin
|
||||
}
|
||||
} = {
|
||||
[SakeDesignation.Table]: { [TableSakeDesignation.FutsūShu]: { min: 0 } },
|
||||
[SakeDesignation.Pure]: {
|
||||
[PureSakeDesignation.Junmai]: { min: 0.3 },
|
||||
[PureSakeDesignation.JunmaiGinjo]: { min: 0.4 },
|
||||
[PureSakeDesignation.JunmaiDaiginjo]: { min: 0.5 }
|
||||
},
|
||||
[SakeDesignation.Blended]: {
|
||||
[BlendedSakeDesignation.Honjozo]: { min: 0.3 },
|
||||
[BlendedSakeDesignation.Ginjo]: { min: 0.4 },
|
||||
[BlendedSakeDesignation.Daiginjo]: { min: 0.5 }
|
||||
}
|
||||
}
|
@ -1,251 +0,0 @@
|
||||
import {
|
||||
SpiritCharacteristic,
|
||||
SpiritType,
|
||||
WhiteSpiritVariant,
|
||||
DarkSpiritVariant,
|
||||
LiqueursSpiritVariant
|
||||
} from '../types'
|
||||
|
||||
// TODO: improve types
|
||||
export const spiritVariantMap: {
|
||||
[key in SpiritType]:
|
||||
| {
|
||||
[key in WhiteSpiritVariant]: (string | { [key: string]: string[] })[]
|
||||
}
|
||||
| {
|
||||
[key in DarkSpiritVariant]: (string | { [key: string]: string[] })[]
|
||||
}
|
||||
| {
|
||||
[key in LiqueursSpiritVariant]: (string | { [key: string]: string[] })[]
|
||||
}
|
||||
} = {
|
||||
[SpiritType.White]: {
|
||||
[WhiteSpiritVariant.Absinthe]: ['Blanche'],
|
||||
[WhiteSpiritVariant.Pastis]: [
|
||||
'Anise',
|
||||
'Fennel',
|
||||
'Licorice Root',
|
||||
'Hyssop',
|
||||
'Mint',
|
||||
'Citrus Peel',
|
||||
'Coriander Seeds',
|
||||
'Angelica Root',
|
||||
'Cinnamon',
|
||||
'Clove'
|
||||
],
|
||||
[WhiteSpiritVariant.Vodka]: [
|
||||
'Wheat',
|
||||
'Rye',
|
||||
'Corn',
|
||||
'Potato',
|
||||
'Barley',
|
||||
'Sugarcane',
|
||||
'Fruits',
|
||||
'Grains'
|
||||
],
|
||||
[WhiteSpiritVariant.Genever]: [
|
||||
{
|
||||
Young: ['Juniper']
|
||||
}
|
||||
],
|
||||
[WhiteSpiritVariant.Gin]: [
|
||||
{
|
||||
'London Dry': [
|
||||
'Juniper',
|
||||
'Coriander',
|
||||
'Angelica root',
|
||||
'Lemon peel',
|
||||
'Orange peel',
|
||||
'Orris root',
|
||||
'Cassia bark',
|
||||
'Licorice root',
|
||||
'Grapefruit peel',
|
||||
'Elderflower'
|
||||
]
|
||||
},
|
||||
'Plymouth'
|
||||
],
|
||||
[WhiteSpiritVariant.Mezcal]: [
|
||||
{ Joven: ['Espadín', 'Tepeztate', 'Tequilana (blue)', 'Tobalá'] }
|
||||
],
|
||||
[WhiteSpiritVariant.Rum]: ['Blanco', 'Cachaça', 'Platino', 'Agricole'],
|
||||
[WhiteSpiritVariant.EauDeVie]: [
|
||||
'Apple',
|
||||
'Blackcurrant',
|
||||
'Butterscotch',
|
||||
'Peach',
|
||||
'Pear',
|
||||
'Plum',
|
||||
'Raspberries'
|
||||
],
|
||||
[WhiteSpiritVariant.Grappa]: ['Marc', 'Pisco'],
|
||||
[WhiteSpiritVariant.Baijiu]: [
|
||||
'Sorghum',
|
||||
'Wheat',
|
||||
'Barley',
|
||||
'Rice',
|
||||
'Millet'
|
||||
],
|
||||
[WhiteSpiritVariant.Soju]: [
|
||||
'Barley',
|
||||
'Brown sugar',
|
||||
'Buckwheat',
|
||||
'Rice',
|
||||
'Sweet Potato'
|
||||
],
|
||||
[WhiteSpiritVariant.Aquavit]: [],
|
||||
[WhiteSpiritVariant.Arrack]: []
|
||||
},
|
||||
[SpiritType.Dark]: {
|
||||
[DarkSpiritVariant.Absinthe]: ['Jaune', 'Verte'],
|
||||
[DarkSpiritVariant.Brandy]: [
|
||||
{
|
||||
Grape: [
|
||||
'VS',
|
||||
'VSOP',
|
||||
'XO',
|
||||
'Beyond Age',
|
||||
'Solera',
|
||||
'Solera Reserva',
|
||||
'Solera Gran Reserva'
|
||||
]
|
||||
}
|
||||
],
|
||||
[DarkSpiritVariant.Calvados]: ['Apple', 'Pear'],
|
||||
[DarkSpiritVariant.Chartreuse]: ['Green', 'Yellow'],
|
||||
[DarkSpiritVariant.Genever]: [{ Old: ['Juniper'] }, { Coren: ['Juniper'] }],
|
||||
[DarkSpiritVariant.Mezcal]: ['Reposado', 'Abuelo', 'Añejo', 'Extra Añejo'],
|
||||
[DarkSpiritVariant.Rum]: [
|
||||
{
|
||||
Sugar: [
|
||||
'Cachaca (amarela/ouro)',
|
||||
'Dark Rum',
|
||||
'Gold Rum',
|
||||
'Over-proof',
|
||||
'Premium',
|
||||
'Spiced'
|
||||
]
|
||||
}
|
||||
],
|
||||
[DarkSpiritVariant.Slivovitz]: [],
|
||||
[DarkSpiritVariant.Whiskey]: [
|
||||
'Barley',
|
||||
'Rye',
|
||||
'Wheat',
|
||||
'Corn',
|
||||
'Oat',
|
||||
'Rice'
|
||||
],
|
||||
[DarkSpiritVariant.Arrack]: []
|
||||
},
|
||||
[SpiritType.Liqueurs]: {
|
||||
[LiqueursSpiritVariant.Amaro]: [],
|
||||
[LiqueursSpiritVariant.Coffee]: [],
|
||||
[LiqueursSpiritVariant.Cream]: [
|
||||
'Egg (Advocaat)',
|
||||
'Rum',
|
||||
'Strawberry',
|
||||
'Whiskey (Baileys etc)'
|
||||
],
|
||||
[LiqueursSpiritVariant.Creme]: [
|
||||
'Almond',
|
||||
'Banana',
|
||||
'Blackcurrant',
|
||||
'Chocolate',
|
||||
'Peach',
|
||||
'Sour Cherry',
|
||||
'Violet'
|
||||
],
|
||||
[LiqueursSpiritVariant.Flowers]: ['Rose', 'Violet', 'Elderflower'],
|
||||
[LiqueursSpiritVariant.Fruit]: [
|
||||
'Blackcurrant',
|
||||
'Lemon',
|
||||
'Melon',
|
||||
'Orange',
|
||||
'Peach',
|
||||
'Plum',
|
||||
'Raspberry',
|
||||
'Yuzu'
|
||||
],
|
||||
[LiqueursSpiritVariant.Herb]: [
|
||||
'Anise',
|
||||
'Dom Benedictine',
|
||||
'Bitters',
|
||||
'Ginger',
|
||||
'Jägermeister',
|
||||
'Metaxa',
|
||||
'Mint'
|
||||
],
|
||||
[LiqueursSpiritVariant.Honey]: ['Licor 43', 'Rum', 'Vodka', 'Whiskey'],
|
||||
[LiqueursSpiritVariant.Nut]: [
|
||||
'Almond',
|
||||
'Apricot Kernel',
|
||||
'Hazelnut',
|
||||
'Peanut',
|
||||
'Pecan',
|
||||
'Walnut',
|
||||
'Amarula'
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
export const spiritCharacteristicsMap: {
|
||||
[key in SpiritCharacteristic]: string[]
|
||||
} = {
|
||||
[SpiritCharacteristic.LightAndNeutral]: [
|
||||
WhiteSpiritVariant.Mezcal,
|
||||
WhiteSpiritVariant.Soju,
|
||||
WhiteSpiritVariant.Vodka,
|
||||
WhiteSpiritVariant.Rum,
|
||||
WhiteSpiritVariant.Aquavit
|
||||
],
|
||||
[SpiritCharacteristic.FruityAndAromatic]: [
|
||||
WhiteSpiritVariant.Rum,
|
||||
DarkSpiritVariant.Calvados,
|
||||
WhiteSpiritVariant.EauDeVie,
|
||||
LiqueursSpiritVariant.Fruit,
|
||||
WhiteSpiritVariant.Gin,
|
||||
WhiteSpiritVariant.Grappa
|
||||
],
|
||||
[SpiritCharacteristic.HerbalAndBotanical]: [
|
||||
WhiteSpiritVariant.Absinthe,
|
||||
DarkSpiritVariant.Absinthe,
|
||||
LiqueursSpiritVariant.Amaro,
|
||||
WhiteSpiritVariant.Genever,
|
||||
DarkSpiritVariant.Genever,
|
||||
WhiteSpiritVariant.Gin,
|
||||
WhiteSpiritVariant.Pastis,
|
||||
DarkSpiritVariant.Chartreuse,
|
||||
WhiteSpiritVariant.Aquavit
|
||||
],
|
||||
|
||||
[SpiritCharacteristic.SweetAndSyrupy]: [
|
||||
DarkSpiritVariant.Brandy,
|
||||
LiqueursSpiritVariant.Cream,
|
||||
LiqueursSpiritVariant.Creme,
|
||||
DarkSpiritVariant.Rum,
|
||||
LiqueursSpiritVariant.Nut
|
||||
],
|
||||
[SpiritCharacteristic.SmokyAndSpicy]: [
|
||||
WhiteSpiritVariant.Baijiu,
|
||||
DarkSpiritVariant.Rum,
|
||||
WhiteSpiritVariant.Gin,
|
||||
WhiteSpiritVariant.EauDeVie,
|
||||
DarkSpiritVariant.Mezcal,
|
||||
DarkSpiritVariant.Whiskey,
|
||||
LiqueursSpiritVariant.Cream,
|
||||
LiqueursSpiritVariant.Honey,
|
||||
WhiteSpiritVariant.Arrack,
|
||||
DarkSpiritVariant.Arrack
|
||||
],
|
||||
[SpiritCharacteristic.RichAndFullBodied]: [
|
||||
WhiteSpiritVariant.Baijiu,
|
||||
DarkSpiritVariant.Brandy,
|
||||
DarkSpiritVariant.Rum,
|
||||
WhiteSpiritVariant.Grappa,
|
||||
WhiteSpiritVariant.EauDeVie,
|
||||
LiqueursSpiritVariant.Cream,
|
||||
LiqueursSpiritVariant.Honey,
|
||||
DarkSpiritVariant.Whiskey
|
||||
]
|
||||
}
|
@ -1,18 +1,5 @@
|
||||
import { MODIFICATION_PERIOD } from './const'
|
||||
|
||||
export const roundToOneDecimal = (number: number) =>
|
||||
Math.round(number * 10) / 10
|
||||
|
||||
export const isObject = (item: unknown) =>
|
||||
typeof item === 'object' && !Array.isArray(item) && item !== null
|
||||
|
||||
export const compareArrays = (a: unknown[], b: unknown[]) =>
|
||||
JSON.stringify(a.sort()) === JSON.stringify(b.sort())
|
||||
|
||||
export const compareObjects = (a: object, b: object) =>
|
||||
JSON.stringify(a) === JSON.stringify(b)
|
||||
|
||||
export const copyObject = (obj: object) => JSON.parse(JSON.stringify(obj))
|
||||
|
||||
export const modificationPeriodExpired = (creationTimestamp: number) =>
|
||||
Date.now() - creationTimestamp > MODIFICATION_PERIOD
|
||||
|
@ -1,110 +0,0 @@
|
||||
import Joi from 'joi'
|
||||
import {
|
||||
CoffeeVariety,
|
||||
CoffeeType,
|
||||
ArabicaKind,
|
||||
RobustaKind,
|
||||
CoffeeProcessingType,
|
||||
CoffeeRoast
|
||||
} from '../../types'
|
||||
import {
|
||||
productCodeEANvalidation,
|
||||
productCodeUPCvalidation,
|
||||
productCodeSKUvalidation,
|
||||
countryValidation,
|
||||
nameValidation,
|
||||
idJoiValidation,
|
||||
RRPamountValidation,
|
||||
RRPcurrencyValidation,
|
||||
descriptionValidation,
|
||||
urlValidation,
|
||||
imagesValidation
|
||||
} from './'
|
||||
|
||||
export const coffeeValidation = (data: unknown): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
productCodeEAN: productCodeEANvalidation,
|
||||
productCodeUPC: productCodeUPCvalidation,
|
||||
productCodeSKU: productCodeSKUvalidation,
|
||||
country: countryValidation,
|
||||
region: Joi.string(),
|
||||
origin: Joi.string(),
|
||||
name: nameValidation,
|
||||
producerId: idJoiValidation,
|
||||
variety: Joi.object().custom((variety: CoffeeVariety, helper) => {
|
||||
const message = (str: string) =>
|
||||
helper.message({
|
||||
custom: Joi.expression(str)
|
||||
})
|
||||
|
||||
/**
|
||||
* Variety key validation
|
||||
*/
|
||||
const validVarietyKeys = Object.values(CoffeeType)
|
||||
const varietyKeys = Object.keys(variety)
|
||||
|
||||
if (varietyKeys.length !== 1) {
|
||||
return message(
|
||||
`provided "variety" is not valid. "variety" object has to contain only one key. Valid keys are: [${validVarietyKeys.join(', ')}]`
|
||||
)
|
||||
}
|
||||
|
||||
const varietyType = varietyKeys[0] as CoffeeType
|
||||
|
||||
if (!validVarietyKeys.includes(varietyType)) {
|
||||
return message(
|
||||
`provided "variety" is not valid. Valid keys for "variety" object are: [${validVarietyKeys.join(', ')}]`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Variety value validation
|
||||
*/
|
||||
const varietyKind = variety[varietyType]
|
||||
const validArabicaKinds = Object.values(ArabicaKind)
|
||||
const validRobustaKinds = Object.values(RobustaKind)
|
||||
|
||||
switch (varietyType) {
|
||||
case CoffeeType.Arabica:
|
||||
if (
|
||||
typeof varietyKind !== 'string' ||
|
||||
!validArabicaKinds.includes(varietyKind as ArabicaKind)
|
||||
) {
|
||||
return message(
|
||||
`provided "variety" is not valid. Valid options for "${varietyType}" kind are: [${validArabicaKinds.join(', ')}]`
|
||||
)
|
||||
}
|
||||
|
||||
break
|
||||
|
||||
case CoffeeType.Robusta:
|
||||
if (
|
||||
typeof varietyKind !== 'string' ||
|
||||
!validRobustaKinds.includes(varietyKind as RobustaKind)
|
||||
) {
|
||||
return message(
|
||||
`provided "variety" is not valid. Valid options for "${varietyType}" kind are: [${validRobustaKinds.join(', ')}]`
|
||||
)
|
||||
}
|
||||
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
return variety
|
||||
}),
|
||||
|
||||
processingType: Joi.string()
|
||||
.valid(...Object.values(CoffeeProcessingType))
|
||||
.required(),
|
||||
roast: Joi.string()
|
||||
.valid(...Object.values(CoffeeRoast))
|
||||
.required(),
|
||||
RRPamount: RRPamountValidation,
|
||||
RRPcurrency: RRPcurrencyValidation,
|
||||
description: descriptionValidation,
|
||||
url: urlValidation,
|
||||
images: imagesValidation
|
||||
}).validate(data)
|
@ -1,10 +1,4 @@
|
||||
export * from './user'
|
||||
export * from './nostr'
|
||||
export * from './review'
|
||||
export * from './review'
|
||||
export * from './wine'
|
||||
export * from './spirit'
|
||||
export * from './product'
|
||||
export * from './validations'
|
||||
export * from './sake'
|
||||
export * from './coffee'
|
||||
|
@ -1,53 +0,0 @@
|
||||
import { collections } from '../../services/database.service'
|
||||
import { DBcollection, DBinstance, ProductCode } from '../../types'
|
||||
|
||||
export const throwProductCodeError = (
|
||||
instance: DBinstance,
|
||||
code: ProductCode
|
||||
) => {
|
||||
throw new Error(`${instance} with provided "productCode${code}" exists`)
|
||||
}
|
||||
|
||||
export const productCodeValidation = async (
|
||||
ean: string,
|
||||
upc: string,
|
||||
sku: string,
|
||||
collection: DBcollection,
|
||||
dbInstance: DBinstance
|
||||
) => {
|
||||
if (!ean && !upc && !sku) {
|
||||
throw new Error(
|
||||
`provide "productCode${ProductCode.EAN}", "productCode${ProductCode.UPC}" or "productCode${ProductCode.SKU}"`
|
||||
)
|
||||
}
|
||||
|
||||
if (ean) {
|
||||
const existingProduct = await collections[collection]?.findOne({
|
||||
productCodeEAN: ean
|
||||
})
|
||||
|
||||
if (existingProduct) {
|
||||
throwProductCodeError(dbInstance, ProductCode.EAN)
|
||||
}
|
||||
}
|
||||
|
||||
if (upc) {
|
||||
const existingProduct = await collections[collection]?.findOne({
|
||||
productCodeUPC: upc
|
||||
})
|
||||
|
||||
if (existingProduct) {
|
||||
throwProductCodeError(dbInstance, ProductCode.UPC)
|
||||
}
|
||||
}
|
||||
|
||||
if (sku) {
|
||||
const existingProduct = await collections[collection]?.findOne({
|
||||
productCodeSKU: sku
|
||||
})
|
||||
|
||||
if (existingProduct) {
|
||||
throwProductCodeError(dbInstance, ProductCode.SKU)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,942 +1,10 @@
|
||||
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(),
|
||||
eventId: Joi.string().required(),
|
||||
productId: Joi.string().required(),
|
||||
rating: Joi.number().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()
|
||||
tastingNotes: Joi.array().items(Joi.string())
|
||||
}).validate(data)
|
||||
|
@ -1,137 +0,0 @@
|
||||
import Joi from 'joi'
|
||||
import {
|
||||
SakeDesignation,
|
||||
SakeCharacteristic,
|
||||
SakeVolume,
|
||||
SakeStarter,
|
||||
RiceVarietal,
|
||||
SakePolishMin,
|
||||
SakeYeastStrain,
|
||||
SakeKoji
|
||||
} from '../../types'
|
||||
import {
|
||||
vintageValidation,
|
||||
productCodeEANvalidation,
|
||||
productCodeUPCvalidation,
|
||||
productCodeSKUvalidation,
|
||||
countryValidation,
|
||||
nameValidation,
|
||||
idJoiValidation,
|
||||
volumeValidation,
|
||||
alcoholValidation,
|
||||
RRPamountValidation,
|
||||
RRPcurrencyValidation,
|
||||
descriptionValidation,
|
||||
urlValidation,
|
||||
imagesValidation
|
||||
} from './'
|
||||
import { sakePolishMap } from '../'
|
||||
|
||||
export const sakeValidation = (data: unknown): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
id: idJoiValidation.optional(),
|
||||
productCodeEAN: productCodeEANvalidation,
|
||||
productCodeUPC: productCodeUPCvalidation,
|
||||
productCodeSKU: productCodeSKUvalidation,
|
||||
country: countryValidation,
|
||||
region: Joi.string(),
|
||||
name: nameValidation,
|
||||
designation: Joi.object()
|
||||
.custom((designation: { [key in SakeDesignation]: string }, helper) => {
|
||||
const descriptionKeys = Object.keys(designation)
|
||||
|
||||
if (descriptionKeys.length !== 1) {
|
||||
return helper.message({
|
||||
custom: Joi.expression(`provide designation is not valid.`)
|
||||
})
|
||||
}
|
||||
|
||||
const designationKey = descriptionKeys[0]
|
||||
const designationKeyOptions: string[] = Object.values(SakeDesignation)
|
||||
|
||||
if (!designationKeyOptions.includes(designationKey)) {
|
||||
return helper.message({
|
||||
custom: Joi.expression(
|
||||
`provide designation key "${designationKey}" is not valid. Valid options are [${designationKeyOptions.map((option) => `"${option}"`).join(', ')}]`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const descriptionValues = Object.values(designation)
|
||||
|
||||
if (descriptionValues.length !== 1) {
|
||||
return helper.message({
|
||||
custom: Joi.expression(`provide designation is not valid.`)
|
||||
})
|
||||
}
|
||||
|
||||
const descriptionValue = descriptionValues[0]
|
||||
|
||||
if (typeof descriptionValue !== 'string') {
|
||||
return helper.message({
|
||||
custom: Joi.expression(`provide designation is not valid.`)
|
||||
})
|
||||
}
|
||||
|
||||
const designationValueOptions: string[] = Object.keys(
|
||||
(sakePolishMap as { [key: string]: { [key: string]: unknown } })[
|
||||
designationKey
|
||||
]
|
||||
)
|
||||
|
||||
if (!designationValueOptions.includes(descriptionValue)) {
|
||||
return helper.message({
|
||||
custom: Joi.expression(
|
||||
`provide designation value "${descriptionValue}" is not valid. Valid options are [${designationValueOptions.map((option) => `"${option}"`).join(', ')}]`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return designation
|
||||
})
|
||||
.required(),
|
||||
polishRate: Joi.number().custom((polishRate, helper) => {
|
||||
// return if no state ancestors
|
||||
if (!helper.state.ancestors) {
|
||||
return polishRate
|
||||
}
|
||||
|
||||
const designation = helper.state.ancestors[0].designation
|
||||
const designationKey = Object.keys(designation)[0]
|
||||
const designationValue: string = Object.values(
|
||||
designation as { [key: string]: string }
|
||||
)[0]
|
||||
const minPolishRate: number = (
|
||||
sakePolishMap as { [key: string]: { [key: string]: SakePolishMin } }
|
||||
)[designationKey][designationValue].min
|
||||
|
||||
if (polishRate < minPolishRate || polishRate > 0.99) {
|
||||
return helper.message({
|
||||
custom: Joi.expression(
|
||||
`provide polishRate "${polishRate}" is not valid for "${designationKey} -> ${designationValue}". Valid range is "${minPolishRate} - 0.99"`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return polishRate
|
||||
}),
|
||||
characteristic: Joi.string()
|
||||
.valid(...Object.values(SakeCharacteristic))
|
||||
.required(),
|
||||
starter: Joi.string().valid(...Object.values(SakeStarter)),
|
||||
yeastStrain: Joi.string().valid(...Object.values(SakeYeastStrain)),
|
||||
volume: volumeValidation(SakeVolume),
|
||||
alcohol: alcoholValidation,
|
||||
riceVarietal: Joi.array()
|
||||
.items(Joi.string().valid(...Object.values(RiceVarietal)))
|
||||
.required(),
|
||||
koji: Joi.string()
|
||||
.valid(...Object.values(SakeKoji))
|
||||
.required(),
|
||||
vintage: vintageValidation,
|
||||
RRPamount: RRPamountValidation,
|
||||
RRPcurrency: RRPcurrencyValidation,
|
||||
description: descriptionValidation,
|
||||
url: urlValidation,
|
||||
images: imagesValidation
|
||||
}).validate(data)
|
@ -1,293 +0,0 @@
|
||||
import Joi from 'joi'
|
||||
import {
|
||||
Ingredient,
|
||||
SpiritType,
|
||||
SpiritVolume,
|
||||
SpiritCharacteristic
|
||||
} from '../../types'
|
||||
import { isObject, spiritVariantMap, spiritCharacteristicsMap } from '../'
|
||||
import {
|
||||
vintageValidation,
|
||||
productCodeEANvalidation,
|
||||
productCodeUPCvalidation,
|
||||
productCodeSKUvalidation,
|
||||
countryValidation,
|
||||
nameValidation,
|
||||
typeValidation,
|
||||
idJoiValidation,
|
||||
volumeValidation,
|
||||
alcoholValidation,
|
||||
RRPamountValidation,
|
||||
RRPcurrencyValidation,
|
||||
descriptionValidation,
|
||||
urlValidation,
|
||||
imagesValidation
|
||||
} from './'
|
||||
|
||||
export const spiritValidation = (data: unknown): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
id: idJoiValidation.optional(),
|
||||
productCodeEAN: productCodeEANvalidation,
|
||||
productCodeUPC: productCodeUPCvalidation,
|
||||
productCodeSKU: productCodeSKUvalidation,
|
||||
country: countryValidation,
|
||||
region: Joi.string(),
|
||||
name: nameValidation,
|
||||
type: typeValidation(SpiritType),
|
||||
variant: Joi.alternatives().try(
|
||||
Joi.string().custom((variant, helper) => {
|
||||
// return if no value
|
||||
if (!variant) {
|
||||
return variant
|
||||
}
|
||||
// return if no state ancestors
|
||||
if (!helper.state.ancestors) {
|
||||
return variant
|
||||
}
|
||||
|
||||
const spiritType: SpiritType = helper.state.ancestors[0].type
|
||||
|
||||
// return if no spiritType
|
||||
if (!spiritType) {
|
||||
return variant
|
||||
}
|
||||
|
||||
const options: string[] = Object.keys(spiritVariantMap[spiritType])
|
||||
|
||||
if (!options.length) {
|
||||
return helper.message({
|
||||
custom: Joi.expression(
|
||||
`no variants found for provided type of spirit`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
if (!options.includes(variant)) {
|
||||
return helper.message({
|
||||
custom: Joi.expression(
|
||||
`"${variant}" is not a valid variant for "${spiritType}" spirit. Valid options are [${options.map((option) => `"${option}"`).join(', ')}]`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return variant
|
||||
}),
|
||||
Joi.object().custom(
|
||||
(
|
||||
variant: { [key: string]: string | { [key: string]: string } },
|
||||
helper
|
||||
) => {
|
||||
// return if no value
|
||||
if (!variant) {
|
||||
return variant
|
||||
}
|
||||
|
||||
// return if multiple variant provided
|
||||
if (Object.keys(variant).length !== 1) {
|
||||
return helper.message({
|
||||
custom: Joi.expression(`multiple variants provided`)
|
||||
})
|
||||
}
|
||||
|
||||
// return if no state ancestors
|
||||
if (!helper.state.ancestors) {
|
||||
return variant
|
||||
}
|
||||
|
||||
const spiritType: SpiritType = helper.state.ancestors[0].type
|
||||
|
||||
// return if no spiritType
|
||||
if (!spiritType) {
|
||||
return variant
|
||||
}
|
||||
|
||||
/**
|
||||
* Variant
|
||||
*/
|
||||
const variantOptions: string[] = Object.keys(
|
||||
spiritVariantMap[spiritType]
|
||||
)
|
||||
|
||||
if (!variantOptions.length) {
|
||||
return helper.message({
|
||||
custom: Joi.expression(
|
||||
`no variants found for provided type of spirit`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const variantName = Object.keys(variant)[0]
|
||||
|
||||
if (!variantOptions.includes(variantName)) {
|
||||
return helper.message({
|
||||
custom: Joi.expression(
|
||||
`"${variantName}" is not a valid variant for "${spiritType}" spirit. Valid options are [${variantOptions.map((option) => `"${option}"`).join(', ')}]`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* SubVariant
|
||||
*/
|
||||
const subVariants: (string | { [key: string]: string[] })[] = (
|
||||
spiritVariantMap[spiritType] as {
|
||||
[key: string]: (string | { [key: string]: string[] })[]
|
||||
}
|
||||
)[variantName]
|
||||
|
||||
const subVariantOptions: string[] =
|
||||
typeof subVariants[0] === 'string'
|
||||
? (subVariants as string[])
|
||||
: subVariants.map((subVariant) => Object.keys(subVariant)[0])
|
||||
|
||||
const subVariantValues = Object.values(variant)
|
||||
|
||||
if (subVariantValues.length !== 1) {
|
||||
return helper.message({
|
||||
custom: Joi.expression(
|
||||
`"${variantName}" is not a valid variant for "${spiritType}" spirit. Valid options are [${variantOptions.map((option) => `"${option}"`).join(', ')}]`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const subVariant = subVariantValues[0]
|
||||
|
||||
if (
|
||||
typeof subVariant === 'string' &&
|
||||
!subVariantOptions.includes(subVariant)
|
||||
) {
|
||||
return helper.message({
|
||||
custom: Joi.expression(
|
||||
`"${subVariant}" is not a valid variant for "${spiritType} -> ${variantName}" spirit. Valid options are [${subVariantOptions.map((option) => `"${option}"`).join(', ')}]`
|
||||
)
|
||||
})
|
||||
} else if (isObject(subVariant)) {
|
||||
const providedSubVariants = Object.keys(subVariant)
|
||||
|
||||
if (providedSubVariants.length !== 1) {
|
||||
return helper.message({
|
||||
custom: Joi.expression(
|
||||
`"${subVariant}" is not a valid variant for "${spiritType} -> ${variantName}" spirit. Valid options are [${subVariantOptions.map((option) => `"${option}"`).join(', ')}]`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const subVariantName = providedSubVariants[0]
|
||||
|
||||
if (!subVariantOptions.includes(subVariantName)) {
|
||||
return helper.message({
|
||||
custom: Joi.expression(
|
||||
`"${subVariantName}" is not a valid variant for "${spiritType} -> ${variantName}" spirit. Valid options are [${subVariantOptions.map((option) => `"${option}"`).join(', ')}]`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* SubSubVariant
|
||||
*/
|
||||
if (typeof subVariants[0] === 'string') {
|
||||
return helper.message({
|
||||
custom: Joi.expression(
|
||||
`Provided not valid variant for "${spiritType} -> ${variantName} -> ${subVariantName}" spirit. Valid options are [${subVariantOptions.map((option) => `"${option}"`).join(', ')}]`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const subSubVariantOption = (
|
||||
subVariants as { [key: string]: string[] }[]
|
||||
).find((subVariant) => subVariant[subVariantName] !== undefined)
|
||||
|
||||
if (!subSubVariantOption) {
|
||||
return helper.message({
|
||||
custom: Joi.expression(
|
||||
`"${subVariantName}" is not a valid variant for "${spiritType} -> ${variantName} -> ${subVariantName}" spirit. Valid options are [${subVariantOptions.map((option) => `"${option}"`).join(', ')}]`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const subSubVariantOptions: string[] =
|
||||
subSubVariantOption[subVariantName]
|
||||
|
||||
const providedSubSubVariant = Object.values(subVariant)[0]
|
||||
|
||||
if (typeof providedSubSubVariant !== 'string') {
|
||||
return helper.message({
|
||||
custom: Joi.expression(
|
||||
`Provided variant is not valid for "${spiritType} -> ${variantName} -> ${subVariantName}" spirit. Valid options are [${subSubVariantOptions.map((option) => `"${option}"`).join(', ')}]`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
if (!subSubVariantOptions.includes(providedSubSubVariant)) {
|
||||
return helper.message({
|
||||
custom: Joi.expression(
|
||||
`"${providedSubSubVariant}" is not a valid variant for "${spiritType} -> ${variantName} -> ${subVariantName}" spirit. Valid options are [${subSubVariantOptions.map((option) => `"${option}"`).join(', ')}]`
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return variant
|
||||
}
|
||||
)
|
||||
),
|
||||
characteristic: Joi.string()
|
||||
.custom((characteristic, helper) => {
|
||||
if (!Object.values(SpiritCharacteristic).includes(characteristic)) {
|
||||
return helper.message({
|
||||
custom: Joi.expression(
|
||||
`"${characteristic}" is not a valid characteristic. Valid options are [${Object.values(
|
||||
SpiritCharacteristic
|
||||
)
|
||||
.map((option) => `"${option}"`)
|
||||
.join(', ')}]`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const spiritType: SpiritType = helper.state.ancestors[0].type
|
||||
|
||||
const spiritVariant: string | { [key: string]: unknown } =
|
||||
helper.state.ancestors[0].variant
|
||||
|
||||
const spiritVariantName =
|
||||
typeof spiritVariant === 'string'
|
||||
? spiritVariant
|
||||
: Object.keys(spiritVariant)[0]
|
||||
|
||||
const variantsInCharacteristic =
|
||||
spiritCharacteristicsMap[characteristic as SpiritCharacteristic]
|
||||
|
||||
const characteristicsInVariant = Object.keys(
|
||||
spiritCharacteristicsMap
|
||||
).filter((char) =>
|
||||
spiritCharacteristicsMap[char as SpiritCharacteristic].includes(
|
||||
spiritVariantName
|
||||
)
|
||||
)
|
||||
|
||||
if (!variantsInCharacteristic.includes(spiritVariantName)) {
|
||||
return helper.message({
|
||||
custom: Joi.expression(
|
||||
`"${characteristic}" is not a valid characteristic for "${spiritType} -> ${spiritVariantName}". Valid options are [${characteristicsInVariant
|
||||
.map((option) => `"${option}"`)
|
||||
.join(', ')}]`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return characteristic
|
||||
})
|
||||
.required(),
|
||||
ingredients: Joi.array()
|
||||
.items(Joi.string().valid(...Object.values(Ingredient)))
|
||||
.required(),
|
||||
volume: volumeValidation(SpiritVolume),
|
||||
alcohol: alcoholValidation,
|
||||
vintage: vintageValidation,
|
||||
RRPamount: RRPamountValidation,
|
||||
RRPcurrency: RRPcurrencyValidation,
|
||||
description: descriptionValidation,
|
||||
url: urlValidation,
|
||||
images: imagesValidation
|
||||
}).validate(data)
|
@ -1,9 +1,23 @@
|
||||
import Joi from 'joi'
|
||||
import { UserRole } from '../../types'
|
||||
import { npubToHex, validateHex } from '../nostr'
|
||||
|
||||
export const userValidation = (data: unknown): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
name: Joi.string().required(),
|
||||
npub: Joi.string()
|
||||
.custom((value, helper) => {
|
||||
const hex = npubToHex(value as string)
|
||||
|
||||
if (!hex || !validateHex(hex)) {
|
||||
return helper.message({
|
||||
custom: Joi.expression('"npub" contains an invalid value')
|
||||
})
|
||||
}
|
||||
|
||||
return hex
|
||||
})
|
||||
.required(),
|
||||
role: Joi.string()
|
||||
.valid(...Object.values(UserRole))
|
||||
.required()
|
||||
|
@ -1,190 +0,0 @@
|
||||
import Joi from 'joi'
|
||||
import {
|
||||
WhiteColour,
|
||||
AmberColour,
|
||||
RoseColour,
|
||||
RedColour,
|
||||
BlueColour,
|
||||
GreenColour,
|
||||
CitrusFruit,
|
||||
AppleFruit,
|
||||
StoneFruit,
|
||||
RedFruit,
|
||||
GrapeFruit,
|
||||
BlackFruit,
|
||||
TropicalFruit,
|
||||
MelonFruit,
|
||||
DriedFruit,
|
||||
Floral,
|
||||
ProductType,
|
||||
VintageOption,
|
||||
TastingNoteKey,
|
||||
VisualAssessmentKey,
|
||||
PrimaryFlavoursAndAromasKey,
|
||||
TextureAndBalanceKey,
|
||||
Sweet,
|
||||
Vegetal,
|
||||
Grain,
|
||||
Botanical,
|
||||
Nutty,
|
||||
Earth,
|
||||
Dairy,
|
||||
Umami,
|
||||
Microbiological,
|
||||
Salt,
|
||||
Burnt,
|
||||
SmokeEnum,
|
||||
Oxidation,
|
||||
Fault
|
||||
} from '../../types'
|
||||
import { npubToHex, validateHex } from '../nostr'
|
||||
|
||||
export const vintageValidation = Joi.alternatives()
|
||||
.try(
|
||||
Joi.string().valid(...Object.values(VintageOption)),
|
||||
Joi.number().min(1700).max(new Date().getFullYear())
|
||||
)
|
||||
.required()
|
||||
|
||||
export const productCodeEANvalidation = Joi.string().allow('').required()
|
||||
export const productCodeUPCvalidation = Joi.string().allow('').required()
|
||||
export const productCodeSKUvalidation = Joi.string().allow('').required()
|
||||
|
||||
export const countryValidation = Joi.string().length(2)
|
||||
|
||||
export const nameValidation = Joi.string().required()
|
||||
|
||||
export const typeValidation = (typeEnum: { [key: string]: string }) =>
|
||||
Joi.string()
|
||||
.valid(...Object.values(typeEnum))
|
||||
.required()
|
||||
|
||||
export const idJoiValidation = Joi.string().length(24).required()
|
||||
export const nostrIdValidation = Joi.string().min(64).max(64).required()
|
||||
|
||||
export const idValidation = (id?: string, key = 'id') => {
|
||||
if (!id) {
|
||||
throw new Error(`"${key}" is required`)
|
||||
} else if (id.length !== 24) {
|
||||
throw new Error(`"${key}" is not valid`)
|
||||
}
|
||||
}
|
||||
|
||||
export const volumeValidation = (volumeEnum: { [key: string]: string }) =>
|
||||
Joi.string()
|
||||
.valid(...Object.values(volumeEnum))
|
||||
.required()
|
||||
|
||||
export const alcoholValidation = Joi.number().min(0).max(0.99).required()
|
||||
|
||||
export const RRPamountValidation = Joi.number().required()
|
||||
export const RRPcurrencyValidation = Joi.string().length(3).required()
|
||||
|
||||
export const descriptionValidation = Joi.string().required()
|
||||
export const urlValidation = Joi.string()
|
||||
// TODO: improve url validation
|
||||
export const imagesValidation = Joi.array().items(Joi.string())
|
||||
|
||||
export const validateStringValue = (
|
||||
value: unknown,
|
||||
validOptions: { [key: string]: string }
|
||||
) => typeof value !== 'string' || !Object.values(validOptions).includes(value)
|
||||
|
||||
export const productSpecificValidation = (
|
||||
productType: ProductType,
|
||||
value:
|
||||
| WhiteColour
|
||||
| AmberColour
|
||||
| RoseColour
|
||||
| RedColour
|
||||
| BlueColour
|
||||
| GreenColour
|
||||
| CitrusFruit
|
||||
| AppleFruit
|
||||
| StoneFruit
|
||||
| RedFruit
|
||||
| GrapeFruit
|
||||
| BlackFruit
|
||||
| TropicalFruit
|
||||
| MelonFruit
|
||||
| DriedFruit
|
||||
| Floral
|
||||
| Sweet
|
||||
| Vegetal
|
||||
| Grain
|
||||
| Botanical
|
||||
| Nutty
|
||||
| Earth
|
||||
| Dairy
|
||||
| Umami
|
||||
| Microbiological
|
||||
| Salt
|
||||
| Burnt
|
||||
| SmokeEnum
|
||||
| Oxidation
|
||||
| Fault,
|
||||
propertyValidOptions: { [key: string]: string },
|
||||
productSpecificValidOptions: {
|
||||
[key in ProductType]: { [key: string]: string } | undefined
|
||||
},
|
||||
messageCallback: (
|
||||
noteKey: TastingNoteKey,
|
||||
noteSubKey:
|
||||
| VisualAssessmentKey
|
||||
| PrimaryFlavoursAndAromasKey
|
||||
| TextureAndBalanceKey,
|
||||
options: object,
|
||||
product?: string,
|
||||
messageString?: string
|
||||
) => Joi.ErrorReport,
|
||||
noteKey: TastingNoteKey,
|
||||
noteSubKey:
|
||||
| VisualAssessmentKey
|
||||
| PrimaryFlavoursAndAromasKey
|
||||
| TextureAndBalanceKey
|
||||
) => {
|
||||
// FIXME: fix coffee-color validation
|
||||
if (
|
||||
noteSubKey === VisualAssessmentKey.Colour &&
|
||||
productType === ProductType.Coffee
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
if (value && validateStringValue(value, propertyValidOptions)) {
|
||||
return messageCallback(noteKey, noteSubKey, propertyValidOptions)
|
||||
}
|
||||
|
||||
const productValidOptions = productSpecificValidOptions[productType]
|
||||
|
||||
if (value && !productValidOptions) {
|
||||
return messageCallback(
|
||||
noteKey,
|
||||
noteSubKey,
|
||||
{},
|
||||
productType,
|
||||
`provided "tastingNote" is not valid. "${[noteKey, noteSubKey].join('-')}" is not supported for "${productType}"`
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
value &&
|
||||
productValidOptions &&
|
||||
validateStringValue(value, productValidOptions)
|
||||
) {
|
||||
return messageCallback(
|
||||
noteKey,
|
||||
noteSubKey,
|
||||
productValidOptions,
|
||||
productType
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const npubValidation = (npub: string) => {
|
||||
const hex = npubToHex(npub)
|
||||
|
||||
if (!hex || !validateHex(hex)) {
|
||||
throw new Error('"npub" is not valid')
|
||||
}
|
||||
}
|
@ -1,44 +1,25 @@
|
||||
import Joi from 'joi'
|
||||
import {
|
||||
WineType,
|
||||
VintageOptions,
|
||||
BottleClosure,
|
||||
Viticulture,
|
||||
WineRegion,
|
||||
WineVolume,
|
||||
WineStyle,
|
||||
WhiteWineCharacteristic,
|
||||
AmberWineCharacteristic,
|
||||
RoseWineCharacteristic,
|
||||
RedWineCharacteristic,
|
||||
GrapeVarietal
|
||||
WineVolume
|
||||
} 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
|
||||
productCodeEAN: Joi.string().allow('').required(),
|
||||
productCodeUPC: Joi.string().allow('').required(),
|
||||
productCodeSKU: Joi.string().allow('').required(),
|
||||
type: Joi.string()
|
||||
.valid(...Object.values(WineType))
|
||||
.required(),
|
||||
style: Joi.string().required(),
|
||||
characteristic: Joi.string().required(),
|
||||
country: Joi.string().length(2),
|
||||
region: Joi.alternatives()
|
||||
.try(
|
||||
Joi.string().custom((value, helper) => {
|
||||
@ -125,9 +106,7 @@ export const wineValidation = (data: unknown): Joi.ValidationResult =>
|
||||
): 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`
|
||||
`"region" contains an invalid value. Valid values for ${map.join(' -> ')} are [${options.join(', ')}]`
|
||||
)
|
||||
})
|
||||
|
||||
@ -187,24 +166,26 @@ export const wineValidation = (data: unknown): Joi.ValidationResult =>
|
||||
/**
|
||||
* 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 villages: string[] = Array.isArray(
|
||||
(
|
||||
(regionMap as WineRegion)[providedRegion] as {
|
||||
[key: string]: string | { [key: string]: string[] }
|
||||
}
|
||||
)[providedSubRegionName]
|
||||
)
|
||||
? ((
|
||||
(regionMap as WineRegion)[providedRegion] as {
|
||||
[key: string]: string | { [key: string]: string[] }
|
||||
}
|
||||
)[providedSubRegionName] as unknown as string[])
|
||||
: Object.keys(
|
||||
(
|
||||
(regionMap as WineRegion)[providedRegion] as {
|
||||
[key: string]: string | { [key: string]: string[] }
|
||||
}
|
||||
)[providedSubRegionName]
|
||||
)
|
||||
|
||||
const providedVillage: string | { [key: string]: string } = (
|
||||
value[providedRegion] as {
|
||||
@ -260,14 +241,6 @@ export const wineValidation = (data: unknown): Joi.ValidationResult =>
|
||||
}
|
||||
)[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]: {
|
||||
@ -297,84 +270,16 @@ export const wineValidation = (data: unknown): Joi.ValidationResult =>
|
||||
)
|
||||
.allow('')
|
||||
.required(),
|
||||
name: nameValidation,
|
||||
type: typeValidation(WineType),
|
||||
style: Joi.string()
|
||||
.valid(...Object.values(WineStyle))
|
||||
name: Joi.string().required(),
|
||||
producerId: Joi.string().length(24).required(),
|
||||
varietal: Joi.string().required(),
|
||||
vintage: Joi.alternatives()
|
||||
.try(Joi.string().valid(...Object.values(VintageOptions)), Joi.number())
|
||||
.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)))
|
||||
volume: Joi.string()
|
||||
.valid(...Object.values(WineVolume))
|
||||
.required(),
|
||||
vintage: vintageValidation,
|
||||
alcohol: Joi.number().min(0).max(0.99).required(),
|
||||
viticulture: Joi.string()
|
||||
.valid(...Object.values(Viticulture))
|
||||
.required(),
|
||||
@ -385,9 +290,9 @@ export const wineValidation = (data: unknown): Joi.ValidationResult =>
|
||||
closure: Joi.string()
|
||||
.valid(...Object.values(BottleClosure))
|
||||
.required(),
|
||||
RRPamount: RRPamountValidation,
|
||||
RRPcurrency: RRPcurrencyValidation,
|
||||
description: descriptionValidation,
|
||||
url: urlValidation,
|
||||
images: imagesValidation
|
||||
RRPamount: Joi.number().required(),
|
||||
RRPcurrency: Joi.string().length(3).required(),
|
||||
description: Joi.string().required(),
|
||||
url: Joi.string(),
|
||||
image: Joi.string()
|
||||
}).validate(data)
|
||||
|
@ -389,13 +389,13 @@ export const wineRegionsMap: { [key: string]: string[] | WineRegion } = {
|
||||
'Bitlis',
|
||||
'İzmir',
|
||||
'Manisa',
|
||||
'Ayvalik'
|
||||
'Ayvalık'
|
||||
],
|
||||
'Aegean Region': ['Urla', 'Foça', 'Bornova'],
|
||||
Ayvalik: [],
|
||||
Ayvalık: [],
|
||||
'Marmara Region': ['İstanbul', 'Edirne'],
|
||||
'Black Sea Region': ['Rize', 'Artvin', 'Trabzon'],
|
||||
'Southeastern Anatolia': ['Gaziantep', 'Şanliurfa', 'Adana']
|
||||
'Southeastern Anatolia': ['Gaziantep', 'Şanlıurfa', 'Adana']
|
||||
},
|
||||
AM: ['Vayots Dzor', 'Ararat Valley', 'Gegharkunik', 'Tavush', 'Syunik'],
|
||||
CA: {
|
||||
|
Loading…
x
Reference in New Issue
Block a user