UNPKG

apialize

Version:

Turn a database model into a production ready REST(ish) CRUD API in a few lines.

229 lines (207 loc) 6.89 kB
const { express, apializeContext, ensureFn, asyncHandler, defaultNotFound, getProvidedValues, getOwnershipWhere, filterMiddlewareFns, extractMiddleware, extractOption, extractBooleanOption, buildWhereClause, buildHandlers, } = require('./utils'); const { validateData } = require('./validationMiddleware'); const operationUtilFunctions = require('./operationUtils'); const withTransactionAndHooks = operationUtilFunctions.withTransactionAndHooks; const optionsWithTransaction = operationUtilFunctions.optionsWithTransaction; const notFoundWithRollback = operationUtilFunctions.notFoundWithRollback; function patch(model, options = {}, modelOptions = {}) { ensureFn(model, 'update'); const middleware = extractMiddleware(options); const validate = extractBooleanOption(options, 'validate', true); const id_mapping = extractOption(options, 'id_mapping', 'id'); const pre = extractOption(options, 'pre', null); const post = extractOption(options, 'post', null); const inline = filterMiddlewareFns(middleware); const router = express.Router({ mergeParams: true }); const handlers = buildHandlers(inline, async (req, res) => { const payload = await withTransactionAndHooks( { model, options: Object.assign({}, options, { pre: pre, post: post }), req, res, modelOptions, idMapping: id_mapping, }, async (context) => { function removeIdMappingFromProvided(provided, id_mapping) { if (Object.prototype.hasOwnProperty.call(provided, id_mapping)) { delete provided[id_mapping]; } } function extractRawAttributes(model) { if (model && model.rawAttributes) { return model.rawAttributes; } if (model && model.prototype && model.prototype.rawAttributes) { return model.prototype.rawAttributes; } return {}; } function isFieldUpdatable(key, rawAttributes, id_mapping) { const hasAttribute = Object.prototype.hasOwnProperty.call( rawAttributes, key ); const isIdMappingField = key === id_mapping; const isAutoGenerated = !!( rawAttributes[key] && rawAttributes[key]._autoGenerated ); return hasAttribute && !isIdMappingField && !isAutoGenerated; } function getUpdatableKeys(provided, rawAttributes, id_mapping) { const updatableKeys = []; const providedKeys = Object.keys(provided); for (let i = 0; i < providedKeys.length; i += 1) { const key = providedKeys[i]; if (isFieldUpdatable(key, rawAttributes, id_mapping)) { updatableKeys.push(key); } } return updatableKeys; } const id = req.params.id; const provided = getProvidedValues(req); // Run validation if enabled (after middleware and pre-hooks have run) if (validate) { try { // For PATCH, validate only the fields being updated (partial validation) await validateData(model, provided, { isPartial: true }); } catch (error) { if (error.name === 'ValidationError') { context.res.status(400).json({ success: false, error: error.message, details: error.details, }); return; } throw error; } } removeIdMappingFromProvided(provided, id_mapping); const rawAttributes = extractRawAttributes(model); const updatableKeys = getUpdatableKeys( provided, rawAttributes, id_mapping ); function createFindOptions(modelOptions, id_mapping, id, transaction) { const findOptionsBase = Object.assign({}, modelOptions); findOptionsBase.where = {}; findOptionsBase.where[id_mapping] = id; findOptionsBase.attributes = [id_mapping]; return optionsWithTransaction(findOptionsBase, transaction); } async function handleNoUpdatableKeys( model, modelOptions, id_mapping, id, context ) { const findOptions = createFindOptions( modelOptions, id_mapping, id, context.transaction ); const exists = await model.findOne(findOptions); if (!exists) { return notFoundWithRollback(context); } context.payload = { success: true, id: id }; } function createUpdateOptions( modelOptions, ownershipWhere, id_mapping, id, updatableKeys, transaction ) { const updateOptionsBase = Object.assign({}, modelOptions); const where = buildWhereClause(ownershipWhere, id_mapping, id); updateOptionsBase.where = where; updateOptionsBase.fields = updatableKeys.slice(); return optionsWithTransaction(updateOptionsBase, transaction); } function extractAffectedCount(updateResult) { if (Array.isArray(updateResult)) { return updateResult[0]; } return updateResult; } async function handleUpdatableKeys( model, provided, modelOptions, ownershipWhere, id_mapping, id, updatableKeys, context ) { const updateOptions = createUpdateOptions( modelOptions, ownershipWhere, id_mapping, id, updatableKeys, context.transaction ); const updateResult = await model.update(provided, updateOptions); const affected = extractAffectedCount(updateResult); return affected; } const ownershipWhere = getOwnershipWhere(req); if (updatableKeys.length === 0) { await handleNoUpdatableKeys( model, modelOptions, id_mapping, id, context ); } else { const affected = await handleUpdatableKeys( model, provided, modelOptions, ownershipWhere, id_mapping, id, updatableKeys, context ); if (!affected) { return notFoundWithRollback(context); } context.payload = { success: true, id: id }; } return context.payload; } ); if (!res.headersSent) { res.json(payload); } }); router.patch('/:id', handlers); router.apialize = {}; return router; } module.exports = patch;