Skip to content

Instantly share code, notes, and snippets.

@oleoneto
Last active June 11, 2021 20:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save oleoneto/cbf3f5ab5cff9660b3c685cad0e14bad to your computer and use it in GitHub Desktop.
Save oleoneto/cbf3f5ab5cff9660b3c685cad0e14bad to your computer and use it in GitHub Desktop.
Helper functions and wrapping controller to add some convenience around the use of Prisma.js in the context of an Express application.
/**
* Wraps database read query in an exception handler,
* which allows for a consistent return value even if an error occurs.
*
* @param {*} query
* @return {data} on success
* @return {[]} on error
*/
const handleDatabaseRead = async (query) => {
try {
return await query;
} catch (err) {
// Add logger here...
return [];
}
}
/**
* Wraps database write query in an exception handler,
* which allows for a consistent return value even if an error occurs.
*
* @param {*} query
* @return {data} on success
* @return {null} on error
*/
const handleDatabaseWrite = async (query) => {
try {
return await query;
} catch (err) {
// Add logger here...
return null;
}
}
module.exports = {
handleDatabaseRead,
handleDatabaseWrite,
};
/**
* Constructs a Prisma query by parsing all the query parameters
*
* @param {Array} queryArray
* @return {Object}
*/
const parsedQuery = (query, searchAttributes = {}) => {
const entries = Object.entries(query);
const parsed = Object.assign(
{},
{
AND: [
...entries
.map(([key, value]) => {
if (searchAttributes[key] === false) return null;
const transformation = Object.fromEntries([[key, value]]);
try {
const parsedValue = JSON.parse(value);
transformation[key] = parsedValue;
} catch (error) {
transformation[key] = value;
}
if (typeof transformation[key] === 'string' && key !== 'id') {
transformation[key] = {
contains: value,
mode: 'insensitive',
};
}
return transformation;
}),
].filter(key => key !== null),
},
);
return parsed;
};
module.exports = {
parsedQuery,
};
const { readStore, writeStore } = require('./store');
const { parsedQuery } = require('./parsedQuery');
const {
handleDatabaseRead,
handleDatabaseWrite,
} = require('./handleDatabaseQuery');
const defaultErrorMessages = {
create: 'An error occurred while attempting to create this resource.',
list: 'An error occurred while attempting to list the resources.',
get: 'An error occurred while attempting to get this resource',
update: 'An error occurred while attempting to update this resource.',
delete: 'An error occurred while attempting to delete this resource.',
notFound: 'Resource not found.',
}
class Controller {
/**
* Creates an instance of Controller.
* @param {*} resource
* @memberof Controller
*/
constructor(resourceName, { queryOptions = {}, errorMessages = {}, searchAttributes = {} }) {
this.resourceName = resourceName;
this.readableResource = readStore[resourceName];
this.writableResource = writeStore[resourceName];
this.create = this.create.bind(this);
this.list = this.list.bind(this);
this.get = this.get.bind(this);
this.update = this.update.bind(this);
this.delete = this.delete.bind(this);
this.searchOptions = searchAttributes;
this.queryOptions = {
read: queryOptions.read ? queryOptions.read : {},
get: queryOptions.get ? queryOptions.get : {},
list: queryOptions.list ? queryOptions.list : {},
update: queryOptions.update ? queryOptions.update : {},
delete: queryOptions.delete ? queryOptions.delete : {},
};
this.errorMessages = {
create: errorMessages.create ? errorMessages.create : defaultErrorMessages.create,
list: errorMessages.list ? errorMessages.list : defaultErrorMessages.list,
get: errorMessages.get ? errorMessages.get : defaultErrorMessages.get,
update: errorMessages.update ? errorMessages.update : defaultErrorMessages.update,
delete: errorMessages.delete ? errorMessages.delete : defaultErrorMessages.delete,
}
}
/**
* POST and create a resource
*
* @param {*} req
* @param {*} res
* @return {status} 201 Created
* @return {status} 500 Server Error
* @memberof Controller
*/
async create(req, res) {
const {body} = req;
const data = await handleDatabaseWrite(this.writableResource.create({data: body}));
return data ?
res.status(201).send({data}) :
res.status(500).send({ message: this.errorMessages.create });
};
/**
* GET a paginated list of resources
*
* @param {*} req
* @param {*} res
* @return {status} 200 OK
* @memberof Controller
*/
async list(req, res) {
let { limit, page, ...query } = req.query;
query = parsedQuery(query, this.searchOptions);
limit = Number(limit || 100);
page = Number(page || 1);
const data = await handleDatabaseRead(this.readableResource.findMany({
where: query,
take: limit,
skip: (page - 1) * limit,
...this.queryOptions.read,
...this.queryOptions.list,
}));
return res.status(200).send({
pagination: {
page,
limit,
total: data.length,
},
data,
});
}
/**
* GET a single resource identified by :id
*
* @param {*} req
* @param {*} res
* @return {status} 200 OK
* @return {status} 404 Not Found
* @memberof Controller
*/
async get(req, res) {
const {id} = req.params;
const data = await handleDatabaseRead(this.readableResource.findUnique({
where: { id },
...this.queryOptions.read,
...this.queryOptions.get,
}));
return data ?
res.status(200).send({data}) :
res.status(404).send({ message: this.errorMessages.get });
}
/**
* PATCH/UPDATE a single resource identified by :id
*
* @param {*} req
* @param {*} res
* @return {status} 202 Accepted
* @return {status} 500 Server Error
* @memberof Controller
*/
async update(req, res) {
const {id} = req.params;
const {body} = req;
const data = await handleDatabaseWrite(this.writableResource.update({
where: {id},
data: body,
...this.queryOptions.update,
}));
return data ?
res.status(202).send({data}) :
res.status(500).send({ message: this.errorMessages.update });
}
/**
* DELETE a single resource identified by :id
*
* @param {*} req
* @param {*} res
* @return {status} 204 No Content
* @return {status} 500 Server Error
* @memberof Controller
*/
async delete(req, res) {
const {id} = req.params;
try {
await this.writableResource.delete({
where: { id },
...this.queryOptions.delete
});
return res.status(204).send();
} catch (err) {
return res.status(500).send({ message: this.errorMessages.delete });
}
}
}
module.exports = {
Controller
};
const listUsers = async (req, res) => {
res.routeName = 'listUsers';
const { query } = req;
try {
// This could throw. Thus the try-catch
const users = await database.getUsers({ query });
return res.status(200).send({
data: {
users
}
})
} catch (error) {
return res
.status(500)
.send({message: 'Something went wrong', error});
}
};
const {PrismaClient} = require('@prisma/client');
const readStore = new PrismaClient();
const writeStore = new PrismaClient({
datasources: {
db: {
url: process.env.DATABASE_WRITE_URL
}
}
});
module.exports = {
readStore,
writeStore,
};
const express = require('express');
const router = express.Router();
const { UsersController } = require('./usersController');
router.route('')
.post(UsersController.create)
.get(UsersController.list);
router.route('/:id')
.get(UsersController.get)
.patch(UsersController.update);
module.exports = {
userRouter: router,
};
const { Controller } = require('./prismaController');
const UsersController = new Controller('user', {
searchAttributes: {
isAdmin: false,
},
queryOptions: {
read: {
select: {
id: true,
firstName: true,
lastName: true,
notes: {
select: {
id: true,
title: true,
},
},
},
},
},
errorMessages: {
create: "User could not be created"
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment