Merge pull request 'schemas' () from schemas into staging

Reviewed-on: 
This commit is contained in:
Otto 2025-03-31 12:25:49 +00:00
commit eb0f389e07
18 changed files with 162 additions and 22 deletions

@ -1,3 +1,3 @@
{
"cSpell.words": ["Nostr"]
"cSpell.words": ["biodynamic", "Nostr", "npub", "RRP", "screwcap"]
}

33
package-lock.json generated

@ -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": {

@ -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": {

@ -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}`)

@ -1,3 +1,4 @@
export * from './user'
export * from './nostrEvent'
export * from './review'
export * from './wine'

@ -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
) {}
}

@ -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
) {}
}

@ -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
) {}
}

35
src/models/wine.ts Normal file

@ -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
) {}
}

@ -1,3 +1,4 @@
export * from './users.router'
export * from './nostr.router'
export * from './reviews.router'
export * from './wines.router'

@ -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) {

@ -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)
}
}
})

@ -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:

@ -1,5 +1,6 @@
export enum DBcollections {
Users = 'users',
NostrEvents = 'nostrEvents',
Reviews = 'reviews'
Reviews = 'reviews',
Wines = 'wines'
}

@ -1,2 +1,4 @@
export * from './database'
export * from './routes'
export * from './products'
export * from './user'

7
src/types/products.ts Normal file

@ -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'

@ -1,5 +1,6 @@
export enum Routes {
Users = '/users',
NostrEvents = '/nostr',
Reviews = '/reviews'
Reviews = '/reviews',
Wines = '/wines'
}

5
src/types/user.ts Normal file

@ -0,0 +1,5 @@
export enum UserRole {
User = 'user',
Reviewer = 'reviewer',
Producer = 'producer'
}