apialize
Version:
Turn a database model into a production ready REST(ish) CRUD API in a few lines.
190 lines (166 loc) • 5.28 kB
JavaScript
const {
express,
apializeContext,
ensureFn,
asyncHandler,
defaultNotFound,
filterMiddlewareFns,
buildHandlers,
getProvidedValues,
getOwnershipWhere,
buildWhereClause,
convertInstanceToPlainObject,
extractBooleanOption,
} = require('./utils');
const { validateData } = require('./validationMiddleware');
const {
withTransactionAndHooks,
optionsWithTransaction,
notFoundWithRollback,
} = require('./operationUtils');
function getUpdatableAttributes(model, idMapping) {
const rawAttributes = model.constructor.rawAttributes;
const updatableAttributes = [];
for (const attributeName of Object.keys(rawAttributes)) {
const isIdField = attributeName === idMapping;
const isAutoGenerated = rawAttributes[attributeName]._autoGenerated;
if (!isIdField && !isAutoGenerated) {
updatableAttributes.push(attributeName);
}
}
return updatableAttributes;
}
function buildNextValues(
updatableAttributes,
providedValues,
rawAttributes,
idMapping,
id
) {
const nextValues = {};
for (const attributeName of updatableAttributes) {
if (Object.prototype.hasOwnProperty.call(providedValues, attributeName)) {
nextValues[attributeName] = providedValues[attributeName];
} else {
const defaultValue = rawAttributes[attributeName].defaultValue;
if (typeof defaultValue !== 'undefined') {
nextValues[attributeName] = defaultValue;
} else {
nextValues[attributeName] = null;
}
}
}
nextValues[idMapping] = id;
return nextValues;
}
function validateOwnership(instance, ownershipWhere) {
if (!ownershipWhere || Object.keys(ownershipWhere).length === 0) {
return true;
}
for (const [key, expectedValue] of Object.entries(ownershipWhere)) {
if (instance[key] !== expectedValue) {
return false;
}
}
return true;
}
function update(model, options = {}, modelOptions = {}) {
ensureFn(model, 'update');
const {
middleware = [],
id_mapping = 'id',
pre = null,
post = null,
} = options;
const validate = extractBooleanOption(options, 'validate', true);
const middlewareFunctions = filterMiddlewareFns(middleware);
const router = express.Router({ mergeParams: true });
const handlers = buildHandlers(
middlewareFunctions,
async function (req, res) {
const combinedOptions = Object.assign({}, options, { pre, post });
const payload = await withTransactionAndHooks(
{
model,
options: combinedOptions,
req,
res,
modelOptions,
idMapping: id_mapping,
},
async (context) => {
const id = req.params.id;
const providedValues = getProvidedValues(req);
const ownershipWhere = getOwnershipWhere(req);
const where = buildWhereClause(ownershipWhere, id_mapping, id);
const findModelOptions = Object.assign({}, modelOptions, { where });
const findOptions = optionsWithTransaction(
findModelOptions,
context.transaction
);
const existingInstance = await model.findOne(findOptions);
if (!existingInstance) {
return notFoundWithRollback(context);
}
context.existing = existingInstance;
// Run validation if enabled (after we confirm the record exists)
if (validate) {
try {
// For UPDATE, validate the full object that will replace the existing one
await validateData(model, providedValues, { isPartial: false });
} catch (error) {
if (error.name === 'ValidationError') {
context.res.status(400).json({
success: false,
error: error.message,
details: error.details,
});
return;
}
throw error;
}
}
const updatableAttributes = getUpdatableAttributes(
existingInstance,
id_mapping
);
const rawAttributes = existingInstance.constructor.rawAttributes;
const nextValues = buildNextValues(
updatableAttributes,
providedValues,
rawAttributes,
id_mapping,
id
);
existingInstance.set(nextValues);
context.nextValues = nextValues;
const isOwnershipValid = validateOwnership(
existingInstance,
ownershipWhere
);
if (!isOwnershipValid) {
return notFoundWithRollback(context);
}
const fieldsToUpdate = Object.keys(nextValues);
const saveModelOptions = Object.assign({}, modelOptions, {
fields: fieldsToUpdate,
});
const saveOptions = optionsWithTransaction(
saveModelOptions,
context.transaction
);
await existingInstance.save(saveOptions);
context.payload = { success: true };
return context.payload;
}
);
if (!res.headersSent) {
res.json(payload);
}
}
);
router.put('/:id', handlers);
router.apialize = {};
return router;
}
module.exports = update;