schemas #22

Merged
y merged 3 commits from schemas into staging 2025-03-31 12:25:49 +00:00
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", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"currency-codes-ts": "^3.0.0",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"express": "^4.21.2", "express": "^4.21.2",
"i18n-iso-countries": "^7.14.0",
"mongodb": "^6.15.0" "mongodb": "^6.15.0"
}, },
"devDependencies": { "devDependencies": {
@ -2971,6 +2973,18 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/debug": {
"version": "2.6.9", "version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@ -3071,6 +3085,12 @@
"wrappy": "1" "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": { "node_modules/diff": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
@ -4553,6 +4573,18 @@
"node": ">=18.18.0" "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": { "node_modules/iconv-lite": {
"version": "0.4.24", "version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -5482,7 +5514,6 @@
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.capitalize": { "node_modules/lodash.capitalize": {

@ -26,8 +26,10 @@
"license": "ISC", "license": "ISC",
"description": "Cellar Social API", "description": "Cellar Social API",
"dependencies": { "dependencies": {
"currency-codes-ts": "^3.0.0",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"express": "^4.21.2", "express": "^4.21.2",
"i18n-iso-countries": "^7.14.0",
"mongodb": "^6.15.0" "mongodb": "^6.15.0"
}, },
"devDependencies": { "devDependencies": {

@ -1,7 +1,7 @@
import express, { Express } from 'express' import express, { Express } from 'express'
import dotenv from 'dotenv' import dotenv from 'dotenv'
import { connectToDatabase } from './services/database.service' import { connectToDatabase } from './services/database.service'
import { usersRouter, nostrRouter, reviewRouter } from './routes' import { usersRouter, nostrRouter, reviewsRouter, winesRouter } from './routes'
import { Routes } from './types' import { Routes } from './types'
dotenv.config() dotenv.config()
@ -13,7 +13,8 @@ connectToDatabase()
.then(() => { .then(() => {
app.use(Routes.Users, usersRouter) app.use(Routes.Users, usersRouter)
app.use(Routes.NostrEvents, nostrRouter) app.use(Routes.NostrEvents, nostrRouter)
app.use(Routes.Reviews, reviewRouter) app.use(Routes.Reviews, reviewsRouter)
app.use(Routes.Wines, winesRouter)
app.listen(port, () => { app.listen(port, () => {
console.log(`Server started at http://localhost:${port}`) console.log(`Server started at http://localhost:${port}`)

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

@ -1,10 +1,10 @@
export class NostrEvent { export class NostrEvent {
constructor( constructor(
public id: string, public nostrId: string, // nostr unique identifier
public pubkey: string, public pubkey: string, // public key of the event creator
public created_at: number, public created_at: number, // timestamp
public kind: number, public kind: number, // event type, e.g., review, article, comment
public tags: [string][], public tags: [string][], // array of keywords or hashtags
public content: string public content: string // text content of the event
) {} ) {}
} }

@ -2,11 +2,11 @@ import { ObjectId } from 'mongodb'
export class Review { export class Review {
constructor( constructor(
public eventId: string, public eventId: string, // foreign key referencing the nostrEvents collection
public productId: string, public productId: string, // unique identifier for the product
public rating: number, public rating: number, // numerical rating, e.g., 1-100
public reviewText: string, public reviewText: string, // text content of the review
public testingNotes: string[], public tastingNotes: string[], // array of tasting notes, e.g., flavours, aromas
public id?: ObjectId public id?: ObjectId // database object id
) {} ) {}
} }

@ -1,8 +1,11 @@
import { ObjectId } from 'mongodb' import { ObjectId } from 'mongodb'
import { UserRole } from '../types'
export class User { export class User {
constructor( constructor(
public name: string, public name: string,
public npub: string[],
public role: UserRole,
public id?: ObjectId 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 './users.router'
export * from './nostr.router' export * from './nostr.router'
export * from './reviews.router' export * from './reviews.router'
export * from './wines.router'

@ -3,12 +3,12 @@ import { collections } from '../services/database.service'
import { Review } from '../models' import { Review } from '../models'
// Global Config // Global Config
export const reviewRouter = express.Router() export const reviewsRouter = express.Router()
reviewRouter.use(express.json()) reviewsRouter.use(express.json())
// GET // GET
reviewRouter.get('/', async (_req: Request, res: Response) => { reviewsRouter.get('/', async (_req: Request, res: Response) => {
try { try {
const reviews = await collections.reviews?.find({}).toArray() const reviews = await collections.reviews?.find({}).toArray()
@ -23,9 +23,10 @@ reviewRouter.get('/', async (_req: Request, res: Response) => {
}) })
// POST // POST
reviewRouter.post('/', async (req: Request, res: Response) => { reviewsRouter.post('/', async (req: Request, res: Response) => {
try { try {
const review = req.body as Review const review = req.body as Review
const result = await collections.reviews?.insertOne(review) const result = await collections.reviews?.insertOne(review)
if (result) { 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.Users]?: mongoDB.Collection
[DBcollections.NostrEvents]?: mongoDB.Collection [DBcollections.NostrEvents]?: mongoDB.Collection
[DBcollections.Reviews]?: mongoDB.Collection [DBcollections.Reviews]?: mongoDB.Collection
[DBcollections.Wines]?: mongoDB.Collection
} = {} } = {}
// Initialize Connection // Initialize Connection
@ -32,10 +33,12 @@ export async function connectToDatabase() {
const reviewsCollection: mongoDB.Collection = db.collection( const reviewsCollection: mongoDB.Collection = db.collection(
DBcollections.Reviews DBcollections.Reviews
) )
const winesCollection: mongoDB.Collection = db.collection(DBcollections.Wines)
collections.users = usersCollection collections.users = usersCollection
collections.nostrEvents = nostrEventsCollection collections.nostrEvents = nostrEventsCollection
collections.reviews = reviewsCollection collections.reviews = reviewsCollection
collections.wines = winesCollection
console.log( console.log(
`Successfully connected to database: ${db.databaseName} and collections: `Successfully connected to database: ${db.databaseName} and collections:

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

@ -1,2 +1,4 @@
export * from './database' export * from './database'
export * from './routes' 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 { export enum Routes {
Users = '/users', Users = '/users',
NostrEvents = '/nostr', 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'
}