Last active
June 11, 2021 20:41
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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, | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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, | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}); | |
} | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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