Compare commits
	
		
			3 Commits
		
	
	
		
			cd8d0bc497
			...
			eb59a980a6
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| eb59a980a6 | |||
| e2da37959f | |||
| 7693d793c0 | 
@@ -52,8 +52,9 @@ model sales {
 | 
			
		||||
 | 
			
		||||
model products {
 | 
			
		||||
  id   Int    @id @unique @default(autoincrement())
 | 
			
		||||
  gtin String @unique // Dont try to use BigInt -> https://github.com/prisma/studio/issues/614
 | 
			
		||||
  name String @unique
 | 
			
		||||
  price Float
 | 
			
		||||
  price Float // FIXME: input:  77.80 -> output: 77.8
 | 
			
		||||
  stock Int
 | 
			
		||||
  visible Boolean @default(true)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -38,3 +38,5 @@ export function handlePrismaError(errorObj: any, res: Response, source: string)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default prisma;
 | 
			
		||||
 | 
			
		||||
//FIXME: https://www.prisma.io/docs/orm/prisma-client/special-fields-and-types/null-and-undefined
 | 
			
		||||
@@ -3,17 +3,13 @@ import passport from 'passport';
 | 
			
		||||
 | 
			
		||||
// Route imports
 | 
			
		||||
import testRoute from './test.js';
 | 
			
		||||
import versionRoute from './version.js'
 | 
			
		||||
import versionRoute from './version.js';
 | 
			
		||||
 | 
			
		||||
import userRoute from './user.js';
 | 
			
		||||
import userRoute_schema from './user_schema.js';
 | 
			
		||||
 | 
			
		||||
// import content_route from './content.js';
 | 
			
		||||
// import content_schema from './content_schema.js';
 | 
			
		||||
 | 
			
		||||
// import * as content_s3_sub_route from './content_s3_sub.js';
 | 
			
		||||
// import * as content_s3_sub_schema from './content_s3_sub_schema.js';
 | 
			
		||||
import user_route from './user.js';
 | 
			
		||||
import user_schema from './user_schema.js';
 | 
			
		||||
 | 
			
		||||
import products_route from './products.js';
 | 
			
		||||
import products_schema from './products_schema.js';
 | 
			
		||||
 | 
			
		||||
// Router base is '/api/v1'
 | 
			
		||||
const Router = express.Router({ strict: false });
 | 
			
		||||
@@ -28,24 +24,14 @@ Router.use('*', function (req, res, next) {
 | 
			
		||||
	next();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// All api routes lowercase! Yea I know but when strict: true it matters.
 | 
			
		||||
Router.route('/user').get(userRoute.get).post(userRoute.post).patch(userRoute.patch).delete(userRoute.del);
 | 
			
		||||
Router.route('/user/describe').get(userRoute_schema);
 | 
			
		||||
 | 
			
		||||
// Router.route('/content').get(content_route.get).post(content_route.post).patch(content_route.patch).delete(content_route.del);
 | 
			
		||||
// Router.route('/content/describe').get(content_schema);
 | 
			
		||||
 | 
			
		||||
// Router.route('/content/downloadurl').get(content_s3_sub_route.get_downloadurl);
 | 
			
		||||
// Router.route('/content/uploadurl').get(content_s3_sub_route.get_uploadurl);
 | 
			
		||||
// Router.route('/content/downloadurl/describe').get(content_s3_sub_schema.get_describe_downloadurl);
 | 
			
		||||
// Router.route('/content/uploadurl/describe').get(content_s3_sub_schema.get_describe_uploadurl);
 | 
			
		||||
 | 
			
		||||
Router.route('/user').get(user_route.get).post(user_route.post).patch(user_route.patch).delete(user_route.del);
 | 
			
		||||
Router.route('/user/describe').get(user_schema);
 | 
			
		||||
 | 
			
		||||
Router.route('/products').get(products_route.get).post(products_route.post).patch(products_route.patch).delete(products_route.del);
 | 
			
		||||
Router.route('/products/describe').get(products_schema);
 | 
			
		||||
 | 
			
		||||
Router.route('/version').get(versionRoute.get);
 | 
			
		||||
//Router.use('/search', search_routes);
 | 
			
		||||
 | 
			
		||||
Router.route('/test').get(testRoute.get);
 | 
			
		||||
 | 
			
		||||
export default Router;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										164
									
								
								src/routes/api/v1/products.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								src/routes/api/v1/products.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,164 @@
 | 
			
		||||
import { Request, Response } from 'express';
 | 
			
		||||
import db, { handlePrismaError } from '../../../handlers/db.js'; // Database
 | 
			
		||||
import log from '../../../handlers/log.js';
 | 
			
		||||
import { parseDynamicSortBy } from '../../../helpers/prisma_helpers.js';
 | 
			
		||||
import { schema_get, schema_post, schema_patch, schema_del } from './products_schema.js';
 | 
			
		||||
 | 
			
		||||
// MARK: GET products
 | 
			
		||||
async function get(req: Request, res: Response) {
 | 
			
		||||
	const { error, value } = schema_get.validate(req.query);
 | 
			
		||||
	if (error) {
 | 
			
		||||
		log.api?.debug('GET products Error:', req.query, value, error.details[0].message);
 | 
			
		||||
		res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: error.details[0].message });
 | 
			
		||||
	} else {
 | 
			
		||||
		log.api?.debug('GET products Success:', req.query, value);
 | 
			
		||||
 | 
			
		||||
		if (value.search !== undefined || value.id !== undefined) {
 | 
			
		||||
			// if search or get by id
 | 
			
		||||
			await db
 | 
			
		||||
				.$transaction([
 | 
			
		||||
					// Same query for count and findMany
 | 
			
		||||
					db.products.count({
 | 
			
		||||
						where: {
 | 
			
		||||
							OR: [{ id: value.id }, { gtin: value.gtin }, { name: { search: value.search } }]
 | 
			
		||||
						},
 | 
			
		||||
						orderBy: parseDynamicSortBy(value.sort.toString(), value.order.toString()),
 | 
			
		||||
						skip: value.skip,
 | 
			
		||||
						take: value.take
 | 
			
		||||
					}),
 | 
			
		||||
					db.products.findMany({
 | 
			
		||||
						where: {
 | 
			
		||||
							OR: [{ id: value.id }, { gtin: value.gtin }, { name: { search: value.search } }]
 | 
			
		||||
						},
 | 
			
		||||
						orderBy: parseDynamicSortBy(value.sort.toString(), value.order.toString()),
 | 
			
		||||
						skip: value.skip,
 | 
			
		||||
						take: value.take
 | 
			
		||||
					})
 | 
			
		||||
				])
 | 
			
		||||
				.then(([count, result]) => {
 | 
			
		||||
					if (result.length !== 0) {
 | 
			
		||||
						res.status(200).json({ count, result });
 | 
			
		||||
					} else {
 | 
			
		||||
						res.status(404).json({ status: 'ERROR', errorcode: 'NOT_FOUND', message: 'Could not find specified product' });
 | 
			
		||||
					}
 | 
			
		||||
				})
 | 
			
		||||
				.catch((err) => {
 | 
			
		||||
					handlePrismaError(err, res, 'GET products');
 | 
			
		||||
				});
 | 
			
		||||
		} else {
 | 
			
		||||
			// get all
 | 
			
		||||
			await db
 | 
			
		||||
				.$transaction([
 | 
			
		||||
					// Same query for count and findMany
 | 
			
		||||
					db.products.count({
 | 
			
		||||
						orderBy: parseDynamicSortBy(value.sort.toString(), value.order.toString()),
 | 
			
		||||
						skip: value.skip,
 | 
			
		||||
						take: value.take
 | 
			
		||||
					}),
 | 
			
		||||
					db.products.findMany({
 | 
			
		||||
						orderBy: parseDynamicSortBy(value.sort.toString(), value.order.toString()),
 | 
			
		||||
						skip: value.skip,
 | 
			
		||||
						take: value.take
 | 
			
		||||
					})
 | 
			
		||||
				])
 | 
			
		||||
				.then(([count, result]) => {
 | 
			
		||||
					if (result.length !== 0) {
 | 
			
		||||
						res.status(200).json({ count, result });
 | 
			
		||||
					} else {
 | 
			
		||||
						res.status(404).json({ status: 'ERROR', errorcode: 'NOT_FOUND', message: 'Could not find specified product' });
 | 
			
		||||
					}
 | 
			
		||||
				})
 | 
			
		||||
				.catch((err) => {
 | 
			
		||||
					handlePrismaError(err, res, 'GET products');
 | 
			
		||||
				});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MARK: CREATE products
 | 
			
		||||
async function post(req: Request, res: Response) {
 | 
			
		||||
	const { error, value } = schema_post.validate(req.body);
 | 
			
		||||
	if (error) {
 | 
			
		||||
		log.api?.debug('POST products Error:', req.body, value, error.details[0].message);
 | 
			
		||||
		res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: error.details[0].message });
 | 
			
		||||
	} else {
 | 
			
		||||
		log.api?.debug('POST products Success:', req.body, value);
 | 
			
		||||
		await db.products
 | 
			
		||||
			.create({
 | 
			
		||||
				data: {
 | 
			
		||||
					id: value.id,
 | 
			
		||||
					gtin: value.gtin,
 | 
			
		||||
					name: value.name,
 | 
			
		||||
					price: value.price,
 | 
			
		||||
					stock: value.stock,
 | 
			
		||||
					visible: value.visible
 | 
			
		||||
				},
 | 
			
		||||
				select: {
 | 
			
		||||
					id: true
 | 
			
		||||
				}
 | 
			
		||||
			})
 | 
			
		||||
			.then((result) => {
 | 
			
		||||
				res.status(201).json({ status: 'CREATED', message: 'Successfully created product', id: result.id });
 | 
			
		||||
			})
 | 
			
		||||
			.catch((err) => {
 | 
			
		||||
				handlePrismaError(err, res, 'POST products');
 | 
			
		||||
			});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MARK: UPDATE products
 | 
			
		||||
async function patch(req: Request, res: Response) {
 | 
			
		||||
	const { error, value } = schema_patch.validate(req.body);
 | 
			
		||||
	if (error) {
 | 
			
		||||
		log.api?.debug('PATCH products Error:', req.body, value, error.details[0].message);
 | 
			
		||||
		res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: error.details[0].message });
 | 
			
		||||
	} else {
 | 
			
		||||
		log.api?.debug('PATCH products Success:', req.body, value);
 | 
			
		||||
		await db.products
 | 
			
		||||
			.update({
 | 
			
		||||
				where: {
 | 
			
		||||
					id: value.id
 | 
			
		||||
				},
 | 
			
		||||
				data: {
 | 
			
		||||
					gtin: value.gtin,
 | 
			
		||||
					name: value.name,
 | 
			
		||||
					price: value.price,
 | 
			
		||||
					stock: value.stock,
 | 
			
		||||
					visible: value.visible
 | 
			
		||||
				},
 | 
			
		||||
				select: {
 | 
			
		||||
					id: true
 | 
			
		||||
				}
 | 
			
		||||
			})
 | 
			
		||||
			.then((result) => {
 | 
			
		||||
				res.status(200).json({ status: 'UPDATED', message: 'Successfully updated product', id: result.id });
 | 
			
		||||
			})
 | 
			
		||||
			.catch((err) => {
 | 
			
		||||
				handlePrismaError(err, res, 'PATCH products');
 | 
			
		||||
			});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MARK: DELETE products
 | 
			
		||||
async function del(req: Request, res: Response) {
 | 
			
		||||
	const { error, value } = schema_del.validate(req.body);
 | 
			
		||||
	if (error) {
 | 
			
		||||
		log.api?.debug('DEL products Error:', req.body, value, error.details[0].message);
 | 
			
		||||
		res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: error.details[0].message });
 | 
			
		||||
	} else {
 | 
			
		||||
		log.api?.debug('DEL products Success:', req.body, value);
 | 
			
		||||
		await db.products
 | 
			
		||||
			.delete({
 | 
			
		||||
				where: {
 | 
			
		||||
					id: value.id
 | 
			
		||||
				}
 | 
			
		||||
			})
 | 
			
		||||
			.then((result) => {
 | 
			
		||||
				res.status(200).json({ status: 'DELETED', message: 'Successfully deleted product', id: result.id });
 | 
			
		||||
			}).catch((err) => {
 | 
			
		||||
				handlePrismaError(err, res, 'DEL products');
 | 
			
		||||
			});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default { get, post, patch, del };
 | 
			
		||||
							
								
								
									
										85
									
								
								src/routes/api/v1/products_schema.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								src/routes/api/v1/products_schema.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,85 @@
 | 
			
		||||
import { Request, Response } from 'express';
 | 
			
		||||
import validator from 'joi'; // DOCS: https://joi.dev/api
 | 
			
		||||
import { Prisma } from '@prisma/client';
 | 
			
		||||
 | 
			
		||||
// MARK: GET products
 | 
			
		||||
const schema_get = validator
 | 
			
		||||
	.object({
 | 
			
		||||
		sort: validator
 | 
			
		||||
			.string()
 | 
			
		||||
			.valid(...Object.keys(Prisma.ProductsScalarFieldEnum))
 | 
			
		||||
			.default('id'),
 | 
			
		||||
 | 
			
		||||
		order: validator.string().valid('asc', 'desc').default('asc'),
 | 
			
		||||
		take: validator.number().min(1).max(512),
 | 
			
		||||
		skip: validator.number().min(0),
 | 
			
		||||
		// This regex ensures that the search string does not contain consecutive asterisks (**) and is at least 3 characters long.
 | 
			
		||||
		search: validator.string().min(3).max(20).regex(new RegExp('^(?!.*\\*{2,}).*$')),
 | 
			
		||||
		id: validator.number().positive().precision(0),
 | 
			
		||||
		gtin: validator
 | 
			
		||||
			.string()
 | 
			
		||||
			.min(8)
 | 
			
		||||
			.max(14)
 | 
			
		||||
			.trim()
 | 
			
		||||
			.regex(new RegExp(/^[0-9]+$/))
 | 
			
		||||
	})
 | 
			
		||||
	// Allow only id, gtin, search or none of them
 | 
			
		||||
	// xor does not work because id, gtin and search fields are not mandatory
 | 
			
		||||
	.without('id', ['gtin', 'search'])
 | 
			
		||||
	.without('gtin', ['id', 'search'])
 | 
			
		||||
	.without('search', ['id', 'gtin']);
 | 
			
		||||
 | 
			
		||||
// MARK: CREATE products
 | 
			
		||||
const schema_post = validator.object({
 | 
			
		||||
	gtin: validator
 | 
			
		||||
		.string()
 | 
			
		||||
		.min(8)
 | 
			
		||||
		.max(14)
 | 
			
		||||
		.trim()
 | 
			
		||||
		.regex(new RegExp(/^[0-9]+$/))
 | 
			
		||||
		.required(),
 | 
			
		||||
	name: validator.string().min(1).max(32).required(),
 | 
			
		||||
	price: validator.number().positive().allow(0).precision(2).required(), // TODO: Check if 0 is allowed (https://github.com/hapijs/joi/issues/772)
 | 
			
		||||
	stock: validator.number().precision(0).required(),
 | 
			
		||||
	visible: validator.boolean().default(true)
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// MARK: UPDATE products
 | 
			
		||||
const schema_patch = validator
 | 
			
		||||
	.object({
 | 
			
		||||
		id: validator.number().positive().precision(0),
 | 
			
		||||
		gtin: validator
 | 
			
		||||
			.string()
 | 
			
		||||
			.min(8)
 | 
			
		||||
			.max(14)
 | 
			
		||||
			.trim()
 | 
			
		||||
			.regex(new RegExp(/^[0-9]+$/)),
 | 
			
		||||
		name: validator.string().min(1).max(32),
 | 
			
		||||
		price: validator.number().positive().allow(0).precision(2), // TODO: Check if 0 is allowed (https://github.com/hapijs/joi/issues/772)
 | 
			
		||||
		stock: validator.number().precision(0),
 | 
			
		||||
		visible: validator.boolean()
 | 
			
		||||
	})
 | 
			
		||||
	.or('gtin', 'name', 'price', 'stock', 'visible');
 | 
			
		||||
 | 
			
		||||
// MARK: DELETE products
 | 
			
		||||
const schema_del = validator.object({
 | 
			
		||||
	id: validator.number().positive().precision(0)
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Describe all schemas
 | 
			
		||||
const schema_get_desc = schema_get.describe();
 | 
			
		||||
const schema_post_desc = schema_post.describe();
 | 
			
		||||
const schema_patch_desc = schema_patch.describe();
 | 
			
		||||
const schema_del_desc = schema_del.describe();
 | 
			
		||||
 | 
			
		||||
// GET route
 | 
			
		||||
export default async function get(req: Request, res: Response) {
 | 
			
		||||
	res.status(200).json({
 | 
			
		||||
		GET: schema_get_desc,
 | 
			
		||||
		POST: schema_post_desc,
 | 
			
		||||
		PATCH: schema_patch_desc,
 | 
			
		||||
		DELETE: schema_del_desc
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { schema_get, schema_post, schema_patch, schema_del };
 | 
			
		||||
@@ -125,7 +125,7 @@ async function patch(req: Request, res: Response) {
 | 
			
		||||
				},
 | 
			
		||||
				data: {
 | 
			
		||||
					name: value.name,
 | 
			
		||||
					code: value.code
 | 
			
		||||
					code: (value.code === '0000') ? null : value.code
 | 
			
		||||
				},
 | 
			
		||||
				select: {
 | 
			
		||||
					id: true
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,7 @@ const schema_get = validator
 | 
			
		||||
// MARK: CREATE user
 | 
			
		||||
const schema_post = validator.object({
 | 
			
		||||
	name: validator.string().min(1).max(32).required(),
 | 
			
		||||
	code: validator.string().min(4).max(4).trim().regex(new RegExp('/^[0-9]+$/'))
 | 
			
		||||
	code: validator.string().min(4).max(4).trim().regex(new RegExp(/^[0-9]+$/))
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// MARK: UPDATE user
 | 
			
		||||
@@ -30,7 +30,7 @@ const schema_patch = validator
 | 
			
		||||
	.object({
 | 
			
		||||
		id: validator.number().positive().precision(0).required(),
 | 
			
		||||
		name: validator.string().min(1).max(32),
 | 
			
		||||
		code: validator.string().min(4).max(4).trim().regex(new RegExp('/^[0-9]+$/'))
 | 
			
		||||
		code: validator.string().min(4).max(4).trim().regex(new RegExp(/^[0-9]+$/))
 | 
			
		||||
	})
 | 
			
		||||
	.or('name', 'code');
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user