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
JavaScript
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;