custom validation -> parse arrays from string / Rough transaction implementaion

This commit is contained in:
Leon Meier 2025-03-09 23:11:59 +01:00
parent bd43f03507
commit fe04ad9ce3
6 changed files with 299 additions and 0 deletions

7
package-lock.json generated
View File

@ -9,6 +9,7 @@
"version": "1.0.0",
"license": "GPL-3.0",
"dependencies": {
"@hapi/bourne": "^3.0.0",
"@prisma/client": "^6.4.1",
"bootstrap-icons": "^1.11.3",
"bulma": "^1.0.3",
@ -587,6 +588,12 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@hapi/bourne": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-3.0.0.tgz",
"integrity": "sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==",
"license": "BSD-3-Clause"
},
"node_modules/@hapi/hoek": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",

View File

@ -33,6 +33,7 @@
"typescript": "^5.8.2"
},
"dependencies": {
"@hapi/bourne": "^3.0.0",
"@prisma/client": "^6.4.1",
"bootstrap-icons": "^1.11.3",
"bulma": "^1.0.3",

View File

@ -0,0 +1,23 @@
import Bourne from '@hapi/bourne';
import Joi from 'joi';
const validator = Joi.extend((joi) => ({
type: 'array',
base: Joi.array(),
coerce: {
from: 'string',
method(value, helpers) {
if (typeof value !== 'string' || (value[0] !== '[' && !/^\s*\[/.test(value))) {
return { value };
}
try {
return { value: Bourne.parse(value) };
} catch (ignoreErr) {
return { value };
}
}
}
}));
export default validator;

View File

@ -16,6 +16,9 @@ import products_schema from './products/products_schema.js';
import image_route from './image/image.js';
import image_schema from './image/image_schema.js';
import transaction_route from './transaction/transaction.js';
import transaction_schema from './transaction/transaction_schema.js';
// Router base is '/api/v1'
const Router = express.Router({ strict: false });
@ -52,6 +55,9 @@ Router.route('/products/describe').get(products_schema);
Router.route('/image').get(image_route.get).post(image_route.post).patch(image_route.post).delete(image_route.del); // POST and PATCH are handled in 'image_route.post'
Router.route('/image/describe').get(image_schema);
Router.route('/transaction').get(transaction_route.get).post(transaction_route.post).patch(transaction_route.patch).delete(transaction_route.del);
Router.route('/transaction/describe').get(transaction_schema);
Router.route('/version').get(versionRoute.get);
Router.route('/test').get(testRoute.get);

View File

@ -0,0 +1,204 @@
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 './transaction_schema.js';
import { Prisma } from '@prisma/client';
// MARK: GET transaction
async function get(req: Request, res: Response) {
const { error, value } = schema_get.validate(req.query);
if (error) {
log.api?.debug('GET transaction 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 transaction Success:', req.query, value);
if (value.id !== undefined || value.user_id !== undefined) {
// get by id or user_id
await db
.$transaction([
// Same query for count and findMany
db.transactions.count({
where: {
OR: [{ id: value.id }, { userId: value.user_id }],
paid: value.paid
},
orderBy: parseDynamicSortBy(value.sort.toString(), value.order.toString()),
skip: value.skip,
take: value.take
}),
db.transactions.findMany({
where: {
OR: [{ id: value.id }, { userId: value.user_id }],
paid: value.paid
},
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 transaction' });
}
})
.catch((err) => {
handlePrismaError(err, res, 'GET transaction');
});
} else {
// get all
await db
.$transaction([
// Same query for count and findMany
db.transactions.count({
where: {
paid: value.paid
},
orderBy: parseDynamicSortBy(value.sort.toString(), value.order.toString()),
skip: value.skip,
take: value.take
}),
db.transactions.findMany({
where: {
paid: value.paid
},
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 transaction' });
}
})
.catch((err) => {
handlePrismaError(err, res, 'GET transaction');
});
}
}
}
// MARK: CREATE transaction
async function post(req: Request, res: Response) {
const { error, value } = schema_post.validate(req.body);
if (error) {
log.api?.debug('POST transaction 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 transaction Success:', req.body, value);
const products: Array<number> = value.products;
let total = new Prisma.Decimal(0);
const salesData: { productId: number; price: number }[] = [];
try {
// Start Prisma transaction
await db.$transaction(async (prisma) => {
for (let i = 0; i < products.length; i++) {
log.api?.debug('Product:', products[i]);
const product = await prisma.products.findUnique({
where: { id: products[i] },
select: { price: true }
});
if (product) {
log.api?.debug('Price:', product.price, Number(product.price));
//total += Number(product.price);
total = total.add(product.price);
salesData.push({
productId: products[i],
price: Number(product.price)
});
} else {
log.api?.debug('Product not found:', products[i]);
}
}
log.api?.debug('Total:', total.toFixed(2));
// TODO: Check if user exists
// Create transaction with sales
const transaction = await prisma.transactions.create({
data: {
userId: value.user_id,
total: total,
paid: false,
sales: {
create: salesData
}
},
select: {
id: true
}
});
res.status(201).json({ status: 'CREATED', message: 'Successfully created transaction', id: transaction.id });
});
} catch (err) {
handlePrismaError(err, res, 'POST transaction');
}
}
}
// MARK: UPDATE transaction
async function patch(req: Request, res: Response) {
const { error, value } = schema_patch.validate(req.body);
if (error) {
log.api?.debug('PATCH transaction 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 transaction Success:', req.body, value);
await db.transactions
.update({
where: {
id: value.id
},
data: {
userId: value.user_id,
paid: value.paid
},
select: {
id: true
}
})
.then((result) => {
res.status(200).json({ status: 'UPDATED', message: 'Successfully updated transaction', id: result.id });
})
.catch((err) => {
handlePrismaError(err, res, 'PATCH transaction');
});
}
}
// MARK: DELETE transaction
async function del(req: Request, res: Response) {
const { error, value } = schema_del.validate(req.body);
if (error) {
log.api?.debug('DEL transaction 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 transaction Success:', req.body, value);
await db.transactions
.delete({
where: {
id: value.id
}
})
.then((result) => {
res.status(200).json({ status: 'DELETED', message: 'Successfully deleted transaction', id: result.id });
})
.catch((err) => {
handlePrismaError(err, res, 'DEL transaction');
});
}
}
export default { get, post, patch, del };

View File

@ -0,0 +1,58 @@
import { Request, Response } from 'express';
//import validator from 'joi'; // DOCS: https://joi.dev/api
import validator from '../../../../handlers/validation.js';
import { Prisma } from '@prisma/client';
// MARK: GET transaction
const schema_get = validator
.object({
sort: validator
.string()
.valid(...Object.keys(Prisma.TransactionsScalarFieldEnum))
.default('id'),
order: validator.string().valid('asc', 'desc').default('asc'),
take: validator.number().min(1).max(512),
skip: validator.number().min(0),
id: validator.number().positive().precision(0),
user_id: validator.number().positive().precision(0),
paid: validator.boolean().note('true-> Only paid / false-> Only unpaid / undefined-> both')
})
.nand('id', 'user_id'); // Allow id or user_id. not both.
// MARK: CREATE transaction
const schema_post = validator.object({
products: validator.array().items(validator.number().positive().precision(0)).required(),
user_id: validator.number().positive().precision(0).required(),
paid: validator.boolean().default(false)
});
// MARK: UPDATE transaction
const schema_patch = validator.object({
user_id: validator.number().positive().precision(0).required(),
paid: validator.boolean().default(false)
});
// MARK: DELETE transaction
const schema_del = validator.object({
id: validator.number().positive().precision(0).required()
});
// 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 };