diff --git a/.vscode/settings.json b/.vscode/settings.json index f5ce7d1..a26694b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { - "cSpell.words": ["Nostr"] + "cSpell.words": ["biodynamic", "Nostr", "npub", "RRP", "screwcap"] } diff --git a/package-lock.json b/package-lock.json index efa2100..524634c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,10 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "currency-codes-ts": "^3.0.0", "dotenv": "^16.4.7", "express": "^4.21.2", + "i18n-iso-countries": "^7.14.0", "mongodb": "^6.15.0" }, "devDependencies": { @@ -2971,6 +2973,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/currency-codes-ts": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/currency-codes-ts/-/currency-codes-ts-3.0.0.tgz", + "integrity": "sha512-ZJeCpq5uY2t8dDl4xdF15shkp5o8jrHcD4lHftK/O8j8xTHlXg0E5YhpZbRJvnLRaKe+JQh1/q1AI9Wc2Dl3Nw==", + "license": "MIT", + "dependencies": { + "lodash-es": "^4.17.21" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -3071,6 +3085,12 @@ "wrappy": "1" } }, + "node_modules/diacritics": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz", + "integrity": "sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==", + "license": "MIT" + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -4553,6 +4573,18 @@ "node": ">=18.18.0" } }, + "node_modules/i18n-iso-countries": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.14.0.tgz", + "integrity": "sha512-nXHJZYtNrfsi1UQbyRqm3Gou431elgLjKl//CYlnBGt5aTWdRPH1PiS2T/p/n8Q8LnqYqzQJik3Q7mkwvLokeg==", + "license": "MIT", + "dependencies": { + "diacritics": "1.3.0" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -5482,7 +5514,6 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "dev": true, "license": "MIT" }, "node_modules/lodash.capitalize": { diff --git a/package.json b/package.json index 89a2494..211a380 100644 --- a/package.json +++ b/package.json @@ -26,8 +26,10 @@ "license": "ISC", "description": "Cellar Social API", "dependencies": { + "currency-codes-ts": "^3.0.0", "dotenv": "^16.4.7", "express": "^4.21.2", + "i18n-iso-countries": "^7.14.0", "mongodb": "^6.15.0" }, "devDependencies": { diff --git a/src/index.ts b/src/index.ts index 4414a54..9f865f2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import express, { Express } from 'express' import dotenv from 'dotenv' import { connectToDatabase } from './services/database.service' -import { usersRouter, nostrRouter, reviewRouter } from './routes' +import { usersRouter, nostrRouter, reviewsRouter, winesRouter } from './routes' import { Routes } from './types' dotenv.config() @@ -13,7 +13,8 @@ connectToDatabase() .then(() => { app.use(Routes.Users, usersRouter) app.use(Routes.NostrEvents, nostrRouter) - app.use(Routes.Reviews, reviewRouter) + app.use(Routes.Reviews, reviewsRouter) + app.use(Routes.Wines, winesRouter) app.listen(port, () => { console.log(`Server started at http://localhost:${port}`) diff --git a/src/models/index.ts b/src/models/index.ts index e85d0a3..36838dc 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -1,3 +1,4 @@ export * from './user' export * from './nostrEvent' export * from './review' +export * from './wine' diff --git a/src/models/nostrEvent.ts b/src/models/nostrEvent.ts index 2d223f4..954f0bf 100644 --- a/src/models/nostrEvent.ts +++ b/src/models/nostrEvent.ts @@ -1,10 +1,10 @@ export class NostrEvent { constructor( - public id: string, - public pubkey: string, - public created_at: number, - public kind: number, - public tags: [string][], - public content: string + public nostrId: string, // nostr unique identifier + public pubkey: string, // public key of the event creator + public created_at: number, // timestamp + public kind: number, // event type, e.g., review, article, comment + public tags: [string][], // array of keywords or hashtags + public content: string // text content of the event ) {} } diff --git a/src/models/review.ts b/src/models/review.ts index edf353d..85b32c6 100644 --- a/src/models/review.ts +++ b/src/models/review.ts @@ -2,11 +2,11 @@ import { ObjectId } from 'mongodb' export class Review { constructor( - public eventId: string, - public productId: string, - public rating: number, - public reviewText: string, - public testingNotes: string[], - public id?: ObjectId + 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 tastingNotes: string[], // array of tasting notes, e.g., flavours, aromas + public id?: ObjectId // database object id ) {} } diff --git a/src/models/user.ts b/src/models/user.ts index b5a2270..1b64321 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -1,8 +1,11 @@ import { ObjectId } from 'mongodb' +import { UserRole } from '../types' export class User { constructor( public name: string, + public npub: string[], + public role: UserRole, public id?: ObjectId ) {} } diff --git a/src/models/wine.ts b/src/models/wine.ts new file mode 100644 index 0000000..9a9ebc5 --- /dev/null +++ b/src/models/wine.ts @@ -0,0 +1,35 @@ +import { ObjectId } from 'mongodb' +import { WineType, Viticulture, BottleClosure } from '../types' +import { Alpha2Code } from 'i18n-iso-countries' +import { CurrencyCode } from 'currency-codes-ts/dist/types' + +export class Wine { + constructor( + public productCodeEAN: string, // Article Number (https://en.wikipedia.org/wiki/International_Article_Number) + public productCodeUPC: string, // Product Code (https://en.wikipedia.org/wiki/Universal_Product_Code) + public productCodeSKU: string, // Stock keeping unit (https://en.wikipedia.org/wiki/Stock_keeping_unit) + public type: WineType, // numerical rating, e.g., 1-100 + public style: 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: string, // appellation, village, sub-region, vineyard + public name: string, // label + public producerId: ObjectId, // product producer + public varietal: string, // if more than one, list as 'blend' + public vintage: string, // year, nv (non-vintage) or mv (multi-vintage) + public alcohol: number, // alcohol percentage + public standardDrinks: number, // number representing an amount of standard drinks per bottle + public viticulture: Viticulture, // two-letter country codes + public sulfites: number, // parts per million + public filtered: boolean, // is wine filtered (fined (egg or fish)) + public vegan: boolean, + public kosher: boolean, + public closure: BottleClosure, // cork, crown-seal, screwcap + public RRPamount: number, // 20 + public RRPcurrency: CurrencyCode, // USD + public description: string, // detailed description of the product + public url?: string, // e.g. producer's website + public image?: string, // (optional image URL)cellar.social + public id?: ObjectId // database object id + ) {} +} diff --git a/src/routes/index.ts b/src/routes/index.ts index 850155e..9a7e1e0 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,3 +1,4 @@ export * from './users.router' export * from './nostr.router' export * from './reviews.router' +export * from './wines.router' diff --git a/src/routes/reviews.router.ts b/src/routes/reviews.router.ts index 7040cf4..5ca69f2 100644 --- a/src/routes/reviews.router.ts +++ b/src/routes/reviews.router.ts @@ -3,12 +3,12 @@ import { collections } from '../services/database.service' import { Review } from '../models' // Global Config -export const reviewRouter = express.Router() +export const reviewsRouter = express.Router() -reviewRouter.use(express.json()) +reviewsRouter.use(express.json()) // GET -reviewRouter.get('/', async (_req: Request, res: Response) => { +reviewsRouter.get('/', async (_req: Request, res: Response) => { try { const reviews = await collections.reviews?.find({}).toArray() @@ -23,9 +23,10 @@ reviewRouter.get('/', async (_req: Request, res: Response) => { }) // POST -reviewRouter.post('/', async (req: Request, res: Response) => { +reviewsRouter.post('/', async (req: Request, res: Response) => { try { const review = req.body as Review + const result = await collections.reviews?.insertOne(review) if (result) { diff --git a/src/routes/wines.router.ts b/src/routes/wines.router.ts new file mode 100644 index 0000000..7d081bf --- /dev/null +++ b/src/routes/wines.router.ts @@ -0,0 +1,46 @@ +import express, { Request, Response } from 'express' +import { collections } from '../services/database.service' +import { Wine } from '../models' + +// Global Config +export const winesRouter = express.Router() + +winesRouter.use(express.json()) + +// GET +winesRouter.get('/', async (_req: Request, res: Response) => { + try { + const wines = await collections.wines?.find({}).toArray() + + res.status(200).send(wines) + } catch (error: unknown) { + console.error(error) + + if (error instanceof Error) { + res.status(500).send(error.message) + } + } +}) + +// POST +winesRouter.post('/', async (req: Request, res: Response) => { + try { + const wine = req.body as Wine + + const result = await collections.wines?.insertOne(wine) + + if (result) { + res + .status(201) + .send(`Successfully created a new wine with id ${result.insertedId}`) + } else { + res.status(500).send('Failed to create a new wine.') + } + } catch (error: unknown) { + console.error(error) + + if (error instanceof Error) { + res.status(400).send(error.message) + } + } +}) diff --git a/src/services/database.service.ts b/src/services/database.service.ts index c96715b..1d2691c 100644 --- a/src/services/database.service.ts +++ b/src/services/database.service.ts @@ -7,6 +7,7 @@ export const collections: { [DBcollections.Users]?: mongoDB.Collection [DBcollections.NostrEvents]?: mongoDB.Collection [DBcollections.Reviews]?: mongoDB.Collection + [DBcollections.Wines]?: mongoDB.Collection } = {} // Initialize Connection @@ -32,10 +33,12 @@ export async function connectToDatabase() { const reviewsCollection: mongoDB.Collection = db.collection( DBcollections.Reviews ) + const winesCollection: mongoDB.Collection = db.collection(DBcollections.Wines) collections.users = usersCollection collections.nostrEvents = nostrEventsCollection collections.reviews = reviewsCollection + collections.wines = winesCollection console.log( `Successfully connected to database: ${db.databaseName} and collections: diff --git a/src/types/database.ts b/src/types/database.ts index 93a45a5..8990fe4 100644 --- a/src/types/database.ts +++ b/src/types/database.ts @@ -1,5 +1,6 @@ export enum DBcollections { Users = 'users', NostrEvents = 'nostrEvents', - Reviews = 'reviews' + Reviews = 'reviews', + Wines = 'wines' } diff --git a/src/types/index.ts b/src/types/index.ts index 7858f5f..19a0935 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,2 +1,4 @@ export * from './database' export * from './routes' +export * from './products' +export * from './user' diff --git a/src/types/products.ts b/src/types/products.ts new file mode 100644 index 0000000..b3af0b5 --- /dev/null +++ b/src/types/products.ts @@ -0,0 +1,7 @@ +export type WineType = 'white' | 'amber' | 'rose' | 'red' + +export type Viticulture = 'biodynamic' | 'organic' | 'conventional' + +export type BottleClosure = 'cork' | 'crown-seal' | 'screwcap' + +export type Availability = 'in stock' | 'out of stock' | 'discontinued' diff --git a/src/types/routes.ts b/src/types/routes.ts index 095187a..843e464 100644 --- a/src/types/routes.ts +++ b/src/types/routes.ts @@ -1,5 +1,6 @@ export enum Routes { Users = '/users', NostrEvents = '/nostr', - Reviews = '/reviews' + Reviews = '/reviews', + Wines = '/wines' } diff --git a/src/types/user.ts b/src/types/user.ts new file mode 100644 index 0000000..4e70fdb --- /dev/null +++ b/src/types/user.ts @@ -0,0 +1,5 @@ +export enum UserRole { + User = 'user', + Reviewer = 'reviewer', + Producer = 'producer' +}