diff --git a/src/auth/operations/forgotPassword.js b/src/auth/operations/forgotPassword.js index 77e53f1f4d..d9dc731902 100644 --- a/src/auth/operations/forgotPassword.js +++ b/src/auth/operations/forgotPassword.js @@ -2,73 +2,69 @@ const crypto = require('crypto'); const { APIError } = require('../../errors'); const forgotPassword = async (args) => { - try { - if (!Object.prototype.hasOwnProperty.call(args.data, 'email')) { - throw new APIError('Missing email.'); - } + if (!Object.prototype.hasOwnProperty.call(args.data, 'email')) { + throw new APIError('Missing email.'); + } - let options = { ...args }; + let options = { ...args }; - // ///////////////////////////////////// - // 1. Execute before login hook - // ///////////////////////////////////// + // ///////////////////////////////////// + // 1. Execute before login hook + // ///////////////////////////////////// - const { beforeForgotPassword } = args.collection.config.hooks; + const { beforeForgotPassword } = args.collection.config.hooks; - if (typeof beforeForgotPassword === 'function') { - options = await beforeForgotPassword(options); - } + if (typeof beforeForgotPassword === 'function') { + options = await beforeForgotPassword(options); + } - // ///////////////////////////////////// - // 2. Perform forgot password - // ///////////////////////////////////// + // ///////////////////////////////////// + // 2. Perform forgot password + // ///////////////////////////////////// - const { - collection: { - Model, - }, - config, - data, - email, - } = options; + const { + collection: { + Model, + }, + config, + data, + email, + } = options; - let token = await crypto.randomBytes(20); - token = token.toString('hex'); + let token = await crypto.randomBytes(20); + token = token.toString('hex'); - const user = await Model.findOne({ email: data.email }); + const user = await Model.findOne({ email: data.email }); - if (!user) return; + if (!user) return; - user.resetPasswordToken = token; - user.resetPasswordExpiration = Date.now() + 3600000; // 1 hour + user.resetPasswordToken = token; + user.resetPasswordExpiration = Date.now() + 3600000; // 1 hour - await user.save(); + await user.save(); - const html = `You are receiving this because you (or someone else) have requested the reset of the password for your account. + const html = `You are receiving this because you (or someone else) have requested the reset of the password for your account. Please click on the following link, or paste this into your browser to complete the process: ${config.serverURL}${config.routes.admin}/reset/${token} If you did not request this, please ignore this email and your password will remain unchanged.`; - email({ - from: `"${config.email.fromName}" <${config.email.fromAddress}>`, - to: data.email, - subject: 'Password Reset', - html, - }); + email({ + from: `"${config.email.fromName}" <${config.email.fromAddress}>`, + to: data.email, + subject: 'Password Reset', + html, + }); - // ///////////////////////////////////// - // 3. Execute after forgot password hook - // ///////////////////////////////////// + // ///////////////////////////////////// + // 3. Execute after forgot password hook + // ///////////////////////////////////// - const { afterForgotPassword } = args.req.collection.config.hooks; + const { afterForgotPassword } = args.req.collection.config.hooks; - if (typeof afterForgotPassword === 'function') { - await afterForgotPassword(options); - } - } catch (error) { - throw error; + if (typeof afterForgotPassword === 'function') { + await afterForgotPassword(options); } }; diff --git a/src/auth/operations/init.js b/src/auth/operations/init.js index 0218b37999..ecb46d3b79 100644 --- a/src/auth/operations/init.js +++ b/src/auth/operations/init.js @@ -1,17 +1,13 @@ const init = async (args) => { - try { - const { - Model, - } = args; + const { + Model, + } = args; - const count = await Model.countDocuments({}); + const count = await Model.countDocuments({}); - if (count >= 1) return true; + if (count >= 1) return true; - return false; - } catch (error) { - throw error; - } + return false; }; module.exports = init; diff --git a/src/auth/operations/login.js b/src/auth/operations/login.js index bd07246bce..98f3c8fcfe 100644 --- a/src/auth/operations/login.js +++ b/src/auth/operations/login.js @@ -2,100 +2,96 @@ const jwt = require('jsonwebtoken'); const { AuthenticationError } = require('../../errors'); const login = async (args) => { - try { - // Await validation here + // Await validation here - let options = { ...args }; + let options = { ...args }; - // ///////////////////////////////////// - // 1. Execute before login hook - // ///////////////////////////////////// + // ///////////////////////////////////// + // 1. Execute before login hook + // ///////////////////////////////////// - const beforeLoginHook = args.collection.config.hooks.beforeLogin; + const beforeLoginHook = args.collection.config.hooks.beforeLogin; - if (typeof beforeLoginHook === 'function') { - options = await beforeLoginHook(options); - } - - // ///////////////////////////////////// - // 2. Perform login - // ///////////////////////////////////// - - const { - collection: { - Model, - config: collectionConfig, - }, - config, - data, - } = options; - - const { email, password } = data; - - const user = await Model.findByUsername(email); - - if (!user) throw new AuthenticationError(); - - const authResult = await user.authenticate(password); - - if (!authResult.user) { - throw new AuthenticationError(); - } - - const fieldsToSign = collectionConfig.fields.reduce((signedFields, field) => { - if (field.saveToJWT) { - return { - ...signedFields, - [field.name]: user[field.name], - }; - } - return signedFields; - }, { - email, - id: user.id, - }); - - fieldsToSign.collection = collectionConfig.slug; - - const token = jwt.sign( - fieldsToSign, - config.secret, - { - expiresIn: collectionConfig.auth.tokenExpiration, - }, - ); - - if (args.res) { - const cookieOptions = { - path: '/', - httpOnly: true, - }; - - if (collectionConfig.auth.secureCookie) { - cookieOptions.secure = true; - } - - args.res.cookie(`${config.cookiePrefix}-token`, token, cookieOptions); - } - - // ///////////////////////////////////// - // 3. Execute after login hook - // ///////////////////////////////////// - - const afterLoginHook = args.collection.config.hooks.afterLogin; - - if (typeof afterLoginHook === 'function') { - await afterLoginHook({ ...options, token, user }); - } - - // ///////////////////////////////////// - // 4. Return token - // ///////////////////////////////////// - - return token; - } catch (error) { - throw error; + if (typeof beforeLoginHook === 'function') { + options = await beforeLoginHook(options); } + + // ///////////////////////////////////// + // 2. Perform login + // ///////////////////////////////////// + + const { + collection: { + Model, + config: collectionConfig, + }, + config, + data, + } = options; + + const { email, password } = data; + + const user = await Model.findByUsername(email); + + if (!user) throw new AuthenticationError(); + + const authResult = await user.authenticate(password); + + if (!authResult.user) { + throw new AuthenticationError(); + } + + const fieldsToSign = collectionConfig.fields.reduce((signedFields, field) => { + if (field.saveToJWT) { + return { + ...signedFields, + [field.name]: user[field.name], + }; + } + return signedFields; + }, { + email, + id: user.id, + }); + + fieldsToSign.collection = collectionConfig.slug; + + const token = jwt.sign( + fieldsToSign, + config.secret, + { + expiresIn: collectionConfig.auth.tokenExpiration, + }, + ); + + if (args.res) { + const cookieOptions = { + path: '/', + httpOnly: true, + }; + + if (collectionConfig.auth.secureCookie) { + cookieOptions.secure = true; + } + + args.res.cookie(`${config.cookiePrefix}-token`, token, cookieOptions); + } + + // ///////////////////////////////////// + // 3. Execute after login hook + // ///////////////////////////////////// + + const afterLoginHook = args.collection.config.hooks.afterLogin; + + if (typeof afterLoginHook === 'function') { + await afterLoginHook({ ...options, token, user }); + } + + // ///////////////////////////////////// + // 4. Return token + // ///////////////////////////////////// + + return token; }; module.exports = login; diff --git a/src/auth/operations/me.js b/src/auth/operations/me.js index 7c816da118..b5ade8b6d1 100644 --- a/src/auth/operations/me.js +++ b/src/auth/operations/me.js @@ -2,28 +2,24 @@ const jwt = require('jsonwebtoken'); const getExtractJWT = require('../getExtractJWT'); const me = async ({ req, config }) => { - try { - const extractJWT = getExtractJWT(config); + const extractJWT = getExtractJWT(config); - if (req.user) { - const response = req.user; + if (req.user) { + const response = req.user; - const token = extractJWT(req); + const token = extractJWT(req); - if (token) { - const decoded = jwt.decode(token); - if (decoded) { - response.exp = decoded.exp; - } + if (token) { + const decoded = jwt.decode(token); + if (decoded) { + response.exp = decoded.exp; } - - return response; } - return null; - } catch (error) { - throw error; + return response; } + + return null; }; module.exports = me; diff --git a/src/auth/operations/policies.js b/src/auth/operations/policies.js index c6c43d823c..e189268319 100644 --- a/src/auth/operations/policies.js +++ b/src/auth/operations/policies.js @@ -11,7 +11,7 @@ const policies = async (args) => { const promises = []; const isLoggedIn = !!(user); - const userCollectionConfig = (user && user.collection) ? config.collections.find(collection => collection.slug === user.collection) : null; + const userCollectionConfig = (user && user.collection) ? config.collections.find((collection) => collection.slug === user.collection) : null; const createPolicyPromise = async (obj, policy, operation, disableWhere = false) => { const updatedObj = obj; @@ -72,27 +72,23 @@ const policies = async (args) => { }); }; - try { - if (userCollectionConfig) { - results.canAccessAdmin = userCollectionConfig.policies.admin ? userCollectionConfig.policies.admin(args) : isLoggedIn; - } else { - results.canAccessAdmin = false; - } - - config.collections.forEach((collection) => { - executeEntityPolicies(collection, allOperations); - }); - - config.globals.forEach((global) => { - executeEntityPolicies(global, ['read', 'update']); - }); - - await Promise.all(promises); - - return results; - } catch (error) { - throw error; + if (userCollectionConfig) { + results.canAccessAdmin = userCollectionConfig.policies.admin ? userCollectionConfig.policies.admin(args) : isLoggedIn; + } else { + results.canAccessAdmin = false; } + + config.collections.forEach((collection) => { + executeEntityPolicies(collection, allOperations); + }); + + config.globals.forEach((global) => { + executeEntityPolicies(global, ['read', 'update']); + }); + + await Promise.all(promises); + + return results; }; module.exports = policies; diff --git a/src/auth/operations/refresh.js b/src/auth/operations/refresh.js index 729efdd68f..9af70d0fdb 100644 --- a/src/auth/operations/refresh.js +++ b/src/auth/operations/refresh.js @@ -2,73 +2,69 @@ const jwt = require('jsonwebtoken'); const { Forbidden } = require('../../errors'); const refresh = async (args) => { - try { - // Await validation here + // Await validation here - let options = { ...args }; + let options = { ...args }; - // ///////////////////////////////////// - // 1. Execute before refresh hook - // ///////////////////////////////////// + // ///////////////////////////////////// + // 1. Execute before refresh hook + // ///////////////////////////////////// - const { beforeRefresh } = args.collection.config.hooks; + const { beforeRefresh } = args.collection.config.hooks; - if (typeof beforeRefresh === 'function') { - options = await beforeRefresh(options); - } - - // ///////////////////////////////////// - // 2. Perform refresh - // ///////////////////////////////////// - - const { secret, cookiePrefix } = options.config; - const opts = {}; - opts.expiresIn = options.collection.config.auth.tokenExpiration; - - if (typeof options.token !== 'string') throw new Forbidden(); - - const payload = jwt.verify(options.token, secret, {}); - delete payload.iat; - delete payload.exp; - const refreshedToken = jwt.sign(payload, secret, opts); - - if (args.res) { - const cookieOptions = { - path: '/', - httpOnly: true, - }; - - if (options.collection.config.auth.secureCookie) { - cookieOptions.secure = true; - } - - args.res.cookie(`${cookiePrefix}-token`, refreshedToken, cookieOptions); - } - - - // ///////////////////////////////////// - // 3. Execute after login hook - // ///////////////////////////////////// - - const { afterRefresh } = args.collection.config.hooks; - - if (typeof afterRefresh === 'function') { - await afterRefresh(options, refreshedToken); - } - - // ///////////////////////////////////// - // 4. Return refreshed token - // ///////////////////////////////////// - - payload.exp = jwt.decode(refreshedToken).exp; - - return { - refreshedToken, - user: payload, - }; - } catch (error) { - throw error; + if (typeof beforeRefresh === 'function') { + options = await beforeRefresh(options); } + + // ///////////////////////////////////// + // 2. Perform refresh + // ///////////////////////////////////// + + const { secret, cookiePrefix } = options.config; + const opts = {}; + opts.expiresIn = options.collection.config.auth.tokenExpiration; + + if (typeof options.token !== 'string') throw new Forbidden(); + + const payload = jwt.verify(options.token, secret, {}); + delete payload.iat; + delete payload.exp; + const refreshedToken = jwt.sign(payload, secret, opts); + + if (args.res) { + const cookieOptions = { + path: '/', + httpOnly: true, + }; + + if (options.collection.config.auth.secureCookie) { + cookieOptions.secure = true; + } + + args.res.cookie(`${cookiePrefix}-token`, refreshedToken, cookieOptions); + } + + + // ///////////////////////////////////// + // 3. Execute after login hook + // ///////////////////////////////////// + + const { afterRefresh } = args.collection.config.hooks; + + if (typeof afterRefresh === 'function') { + await afterRefresh(options, refreshedToken); + } + + // ///////////////////////////////////// + // 4. Return refreshed token + // ///////////////////////////////////// + + payload.exp = jwt.decode(refreshedToken).exp; + + return { + refreshedToken, + user: payload, + }; }; module.exports = refresh; diff --git a/src/auth/operations/register.js b/src/auth/operations/register.js index a91bc985d2..23e08aa55b 100644 --- a/src/auth/operations/register.js +++ b/src/auth/operations/register.js @@ -3,91 +3,87 @@ const executePolicy = require('../executePolicy'); const performFieldOperations = require('../../fields/performFieldOperations'); const register = async (args) => { - try { - // ///////////////////////////////////// - // 1. Retrieve and execute policy - // ///////////////////////////////////// + // ///////////////////////////////////// + // 1. Retrieve and execute policy + // ///////////////////////////////////// - if (!args.overridePolicy) { - await executePolicy(args, args.collection.config.policies.create); - } - - let options = { ...args }; - - // ///////////////////////////////////// - // 2. Execute before register hook - // ///////////////////////////////////// - - const { beforeRegister } = args.collection.config.hooks; - - if (typeof beforeRegister === 'function') { - options = await beforeRegister(options); - } - - // ///////////////////////////////////// - // 3. Execute field-level hooks, policies, and validation - // ///////////////////////////////////// - - options.data = await performFieldOperations(args.collection.config, { ...options, hook: 'beforeCreate', operationName: 'create' }); - - // ///////////////////////////////////// - // 6. Perform register - // ///////////////////////////////////// - - const { - collection: { - Model, - }, - data, - req: { - locale, - fallbackLocale, - }, - } = options; - - const modelData = { ...data }; - delete modelData.password; - - const user = new Model(); - - if (locale && user.setLocale) { - user.setLocale(locale, fallbackLocale); - } - - Object.assign(user, modelData); - - let result = await Model.register(user, data.password); - - await passport.authenticate('local'); - - result = result.toJSON({ virtuals: true }); - - // ///////////////////////////////////// - // 7. Execute field-level hooks and policies - // ///////////////////////////////////// - - result = await performFieldOperations(args.collection.config, { - ...options, data: result, hook: 'afterRead', operationName: 'read', - }); - - // ///////////////////////////////////// - // 8. Execute after register hook - // ///////////////////////////////////// - - const afterRegister = args.collection.config.hooks; - - if (typeof afterRegister === 'function') { - result = await afterRegister(options, result); - } - - // ///////////////////////////////////// - // 9. Return user - // ///////////////////////////////////// - - return result; - } catch (error) { - throw error; + if (!args.overridePolicy) { + await executePolicy(args, args.collection.config.policies.create); } + + let options = { ...args }; + + // ///////////////////////////////////// + // 2. Execute before register hook + // ///////////////////////////////////// + + const { beforeRegister } = args.collection.config.hooks; + + if (typeof beforeRegister === 'function') { + options = await beforeRegister(options); + } + + // ///////////////////////////////////// + // 3. Execute field-level hooks, policies, and validation + // ///////////////////////////////////// + + options.data = await performFieldOperations(args.collection.config, { ...options, hook: 'beforeCreate', operationName: 'create' }); + + // ///////////////////////////////////// + // 6. Perform register + // ///////////////////////////////////// + + const { + collection: { + Model, + }, + data, + req: { + locale, + fallbackLocale, + }, + } = options; + + const modelData = { ...data }; + delete modelData.password; + + const user = new Model(); + + if (locale && user.setLocale) { + user.setLocale(locale, fallbackLocale); + } + + Object.assign(user, modelData); + + let result = await Model.register(user, data.password); + + await passport.authenticate('local'); + + result = result.toJSON({ virtuals: true }); + + // ///////////////////////////////////// + // 7. Execute field-level hooks and policies + // ///////////////////////////////////// + + result = await performFieldOperations(args.collection.config, { + ...options, data: result, hook: 'afterRead', operationName: 'read', + }); + + // ///////////////////////////////////// + // 8. Execute after register hook + // ///////////////////////////////////// + + const afterRegister = args.collection.config.hooks; + + if (typeof afterRegister === 'function') { + result = await afterRegister(options, result); + } + + // ///////////////////////////////////// + // 9. Return user + // ///////////////////////////////////// + + return result; }; module.exports = register; diff --git a/src/auth/operations/registerFirstUser.js b/src/auth/operations/registerFirstUser.js index f37bb50a23..dec92ec402 100644 --- a/src/auth/operations/registerFirstUser.js +++ b/src/auth/operations/registerFirstUser.js @@ -3,62 +3,58 @@ const login = require('./login'); const { Forbidden } = require('../../errors'); const registerFirstUser = async (args) => { - try { - const count = await args.collection.Model.countDocuments({}); + const count = await args.collection.Model.countDocuments({}); - if (count >= 1) throw new Forbidden(); + if (count >= 1) throw new Forbidden(); - // Await validation here + // Await validation here - let options = { ...args }; + let options = { ...args }; - // ///////////////////////////////////// - // 1. Execute before register first user hook - // ///////////////////////////////////// + // ///////////////////////////////////// + // 1. Execute before register first user hook + // ///////////////////////////////////// - const { beforeRegister } = args.collection.config.hooks; + const { beforeRegister } = args.collection.config.hooks; - if (typeof beforeRegister === 'function') { - options = await beforeRegister(options); - } - - // ///////////////////////////////////// - // 2. Perform register first user - // ///////////////////////////////////// - - let result = await register({ - ...options, - overridePolicy: true, - }); - - - // ///////////////////////////////////// - // 3. Log in new user - // ///////////////////////////////////// - - const token = await login({ - ...options, - }); - - result = { - ...result, - token, - }; - - // ///////////////////////////////////// - // 4. Execute after register first user hook - // ///////////////////////////////////// - - const afterRegister = args.config.hooks; - - if (typeof afterRegister === 'function') { - result = await afterRegister(options, result); - } - - return result; - } catch (error) { - throw error; + if (typeof beforeRegister === 'function') { + options = await beforeRegister(options); } + + // ///////////////////////////////////// + // 2. Perform register first user + // ///////////////////////////////////// + + let result = await register({ + ...options, + overridePolicy: true, + }); + + + // ///////////////////////////////////// + // 3. Log in new user + // ///////////////////////////////////// + + const token = await login({ + ...options, + }); + + result = { + ...result, + token, + }; + + // ///////////////////////////////////// + // 4. Execute after register first user hook + // ///////////////////////////////////// + + const afterRegister = args.config.hooks; + + if (typeof afterRegister === 'function') { + result = await afterRegister(options, result); + } + + return result; }; module.exports = registerFirstUser; diff --git a/src/auth/operations/resetPassword.js b/src/auth/operations/resetPassword.js index 53e6af2139..7994b92563 100644 --- a/src/auth/operations/resetPassword.js +++ b/src/auth/operations/resetPassword.js @@ -2,93 +2,89 @@ const jwt = require('jsonwebtoken'); const { APIError } = require('../../errors'); const resetPassword = async (args) => { - try { - if (!Object.prototype.hasOwnProperty.call(args.data, 'token') - || !Object.prototype.hasOwnProperty.call(args.data, 'password')) { - throw new APIError('Missing required data.'); - } - - let options = { ...args }; - - // ///////////////////////////////////// - // 1. Execute before reset password hook - // ///////////////////////////////////// - - const { beforeResetPassword } = args.collection.config.hooks; - - if (typeof beforeResetPassword === 'function') { - options = await beforeResetPassword(options); - } - - // ///////////////////////////////////// - // 2. Perform password reset - // ///////////////////////////////////// - - const { - collection: { - Model, - config: collectionConfig, - }, - config, - data, - } = options; - - const { email } = data; - - const user = await Model.findOne({ - resetPasswordToken: data.token, - resetPasswordExpiration: { $gt: Date.now() }, - }); - - if (!user) throw new APIError('Token is either invalid or has expired.'); - - - await user.setPassword(data.password); - - user.resetPasswordExpiration = Date.now(); - - await user.save(); - - await user.authenticate(data.password); - - const fieldsToSign = collectionConfig.fields.reduce((signedFields, field) => { - if (field.saveToJWT) { - return { - ...signedFields, - [field.name]: user[field.name], - }; - } - return signedFields; - }, { - email, - }); - - const token = jwt.sign( - fieldsToSign, - config.secret, - { - expiresIn: collectionConfig.auth.tokenExpiration, - }, - ); - - // ///////////////////////////////////// - // 3. Execute after reset password hook - // ///////////////////////////////////// - - const { afterResetPassword } = collectionConfig.hooks; - - if (typeof afterResetPassword === 'function') { - await afterResetPassword(options, user); - } - - // ///////////////////////////////////// - // 4. Return updated user - // ///////////////////////////////////// - - return token; - } catch (error) { - throw error; + if (!Object.prototype.hasOwnProperty.call(args.data, 'token') + || !Object.prototype.hasOwnProperty.call(args.data, 'password')) { + throw new APIError('Missing required data.'); } + + let options = { ...args }; + + // ///////////////////////////////////// + // 1. Execute before reset password hook + // ///////////////////////////////////// + + const { beforeResetPassword } = args.collection.config.hooks; + + if (typeof beforeResetPassword === 'function') { + options = await beforeResetPassword(options); + } + + // ///////////////////////////////////// + // 2. Perform password reset + // ///////////////////////////////////// + + const { + collection: { + Model, + config: collectionConfig, + }, + config, + data, + } = options; + + const { email } = data; + + const user = await Model.findOne({ + resetPasswordToken: data.token, + resetPasswordExpiration: { $gt: Date.now() }, + }); + + if (!user) throw new APIError('Token is either invalid or has expired.'); + + + await user.setPassword(data.password); + + user.resetPasswordExpiration = Date.now(); + + await user.save(); + + await user.authenticate(data.password); + + const fieldsToSign = collectionConfig.fields.reduce((signedFields, field) => { + if (field.saveToJWT) { + return { + ...signedFields, + [field.name]: user[field.name], + }; + } + return signedFields; + }, { + email, + }); + + const token = jwt.sign( + fieldsToSign, + config.secret, + { + expiresIn: collectionConfig.auth.tokenExpiration, + }, + ); + + // ///////////////////////////////////// + // 3. Execute after reset password hook + // ///////////////////////////////////// + + const { afterResetPassword } = collectionConfig.hooks; + + if (typeof afterResetPassword === 'function') { + await afterResetPassword(options, user); + } + + // ///////////////////////////////////// + // 4. Return updated user + // ///////////////////////////////////// + + return token; }; module.exports = resetPassword; diff --git a/src/auth/operations/update.js b/src/auth/operations/update.js index 52d8de41fb..8cbab93120 100644 --- a/src/auth/operations/update.js +++ b/src/auth/operations/update.js @@ -5,119 +5,115 @@ const executePolicy = require('../executePolicy'); const performFieldOperations = require('../../fields/performFieldOperations'); const update = async (args) => { - try { - // ///////////////////////////////////// - // 1. Execute policy - // ///////////////////////////////////// + // ///////////////////////////////////// + // 1. Execute policy + // ///////////////////////////////////// - const policyResults = await executePolicy(args, args.config.policies.update); - const hasWherePolicy = typeof policyResults === 'object'; + const policyResults = await executePolicy(args, args.config.policies.update); + const hasWherePolicy = typeof policyResults === 'object'; - let options = { ...args }; + let options = { ...args }; - // ///////////////////////////////////// - // 2. Retrieve document - // ///////////////////////////////////// + // ///////////////////////////////////// + // 2. Retrieve document + // ///////////////////////////////////// - const { - Model, - id, - req: { - locale, - fallbackLocale, - }, - } = options; + const { + Model, + id, + req: { + locale, + fallbackLocale, + }, + } = options; - let query = { _id: id }; + let query = { _id: id }; - if (hasWherePolicy) { - query = { - ...query, - ...policyResults, - }; - } - - let user = await Model.findOne(query); - - if (!user && !hasWherePolicy) throw new NotFound(); - if (!user && hasWherePolicy) throw new Forbidden(); - - if (locale && user.setLocale) { - user.setLocale(locale, fallbackLocale); - } - - const userJSON = user.toJSON({ virtuals: true }); - - // ///////////////////////////////////// - // 2. Execute before update hook - // ///////////////////////////////////// - - const { beforeUpdate } = args.config.hooks; - - if (typeof beforeUpdate === 'function') { - options = await beforeUpdate(options); - } - - // ///////////////////////////////////// - // 3. Merge updates into existing data - // ///////////////////////////////////// - - options.data = deepmerge(userJSON, options.data, { arrayMerge: overwriteMerge }); - - // ///////////////////////////////////// - // 4. Execute field-level hooks, policies, and validation - // ///////////////////////////////////// - - options.data = await performFieldOperations(args.config, { ...options, hook: 'beforeUpdate', operationName: 'update' }); - - // ///////////////////////////////////// - // 5. Handle password update - // ///////////////////////////////////// - - const dataToUpdate = { ...options.data }; - const { password } = dataToUpdate; - - if (password) { - delete dataToUpdate.password; - await user.setPassword(password); - } - - // ///////////////////////////////////// - // 6. Perform database operation - // ///////////////////////////////////// - - Object.assign(user, dataToUpdate); - - await user.save(); - - user = user.toJSON({ virtuals: true }); - - // ///////////////////////////////////// - // 7. Execute field-level hooks and policies - // ///////////////////////////////////// - - user = performFieldOperations(args.config, { - ...options, data: user, hook: 'afterRead', operationName: 'read', - }); - - // ///////////////////////////////////// - // 8. Execute after update hook - // ///////////////////////////////////// - - const afterUpdateHook = args.config.hooks && args.config.hooks.afterUpdate; - - if (typeof afterUpdateHook === 'function') { - user = await afterUpdateHook(options, user); - } - - // ///////////////////////////////////// - // 9. Return user - // ///////////////////////////////////// - - return user; - } catch (error) { - throw error; + if (hasWherePolicy) { + query = { + ...query, + ...policyResults, + }; } + + let user = await Model.findOne(query); + + if (!user && !hasWherePolicy) throw new NotFound(); + if (!user && hasWherePolicy) throw new Forbidden(); + + if (locale && user.setLocale) { + user.setLocale(locale, fallbackLocale); + } + + const userJSON = user.toJSON({ virtuals: true }); + + // ///////////////////////////////////// + // 2. Execute before update hook + // ///////////////////////////////////// + + const { beforeUpdate } = args.config.hooks; + + if (typeof beforeUpdate === 'function') { + options = await beforeUpdate(options); + } + + // ///////////////////////////////////// + // 3. Merge updates into existing data + // ///////////////////////////////////// + + options.data = deepmerge(userJSON, options.data, { arrayMerge: overwriteMerge }); + + // ///////////////////////////////////// + // 4. Execute field-level hooks, policies, and validation + // ///////////////////////////////////// + + options.data = await performFieldOperations(args.config, { ...options, hook: 'beforeUpdate', operationName: 'update' }); + + // ///////////////////////////////////// + // 5. Handle password update + // ///////////////////////////////////// + + const dataToUpdate = { ...options.data }; + const { password } = dataToUpdate; + + if (password) { + delete dataToUpdate.password; + await user.setPassword(password); + } + + // ///////////////////////////////////// + // 6. Perform database operation + // ///////////////////////////////////// + + Object.assign(user, dataToUpdate); + + await user.save(); + + user = user.toJSON({ virtuals: true }); + + // ///////////////////////////////////// + // 7. Execute field-level hooks and policies + // ///////////////////////////////////// + + user = performFieldOperations(args.config, { + ...options, data: user, hook: 'afterRead', operationName: 'read', + }); + + // ///////////////////////////////////// + // 8. Execute after update hook + // ///////////////////////////////////// + + const afterUpdateHook = args.config.hooks && args.config.hooks.afterUpdate; + + if (typeof afterUpdateHook === 'function') { + user = await afterUpdateHook(options, user); + } + + // ///////////////////////////////////// + // 9. Return user + // ///////////////////////////////////// + + return user; }; module.exports = update; diff --git a/src/auth/requestHandlers/login.js b/src/auth/requestHandlers/login.js index d72dfa305e..0bf3152743 100644 --- a/src/auth/requestHandlers/login.js +++ b/src/auth/requestHandlers/login.js @@ -1,7 +1,7 @@ const httpStatus = require('http-status'); const { login } = require('../operations'); -const loginHandler = config => async (req, res, next) => { +const loginHandler = (config) => async (req, res, next) => { try { const token = await login({ req, diff --git a/src/collections/operations/create.js b/src/collections/operations/create.js index b6c58ce1e8..81872c9806 100644 --- a/src/collections/operations/create.js +++ b/src/collections/operations/create.js @@ -11,120 +11,116 @@ const imageMIMETypes = require('../../uploads/imageMIMETypes'); const performFieldOperations = require('../../fields/performFieldOperations'); const create = async (args) => { - try { - // ///////////////////////////////////// - // 1. Retrieve and execute policy - // ///////////////////////////////////// + // ///////////////////////////////////// + // 1. Retrieve and execute policy + // ///////////////////////////////////// - await executePolicy(args, args.config.policies.create); + await executePolicy(args, args.config.policies.create); - let options = { ...args }; + let options = { ...args }; - // ///////////////////////////////////// - // 2. Execute before collection hook - // ///////////////////////////////////// + // ///////////////////////////////////// + // 2. Execute before collection hook + // ///////////////////////////////////// - const { beforeCreate } = args.config.hooks; + const { beforeCreate } = args.config.hooks; - if (typeof beforeCreate === 'function') { - options = await beforeCreate(options); - } - - // ///////////////////////////////////// - // 3. Execute field-level policies, hooks, and validation - // ///////////////////////////////////// - - options.data = await performFieldOperations(args.config, { ...options, hook: 'beforeCreate', operationName: 'create' }); - - // ///////////////////////////////////// - // 4. Upload and resize any files that may be present - // ///////////////////////////////////// - - if (args.config.upload) { - const { staticDir, imageSizes } = options.req.collection.config.upload; - - const fileData = {}; - - if (!args.req.files || Object.keys(args.req.files).length === 0) { - throw new MissingFile(); - } - - await mkdirp(staticDir); - - const fsSafeName = await getSafeFilename(staticDir, options.req.files.file.name); - - await options.req.files.file.mv(`${staticDir}/${fsSafeName}`); - - if (imageMIMETypes.indexOf(options.req.files.file.mimetype) > -1) { - const dimensions = await getImageSize(`${staticDir}/${fsSafeName}`); - fileData.width = dimensions.width; - fileData.height = dimensions.height; - - if (Array.isArray(imageSizes) && options.req.files.file.mimetype !== 'image/svg+xml') { - fileData.sizes = await resizeAndSave(options.config, fsSafeName, fileData.mimeType); - } - } - - fileData.filename = fsSafeName; - fileData.filesize = options.req.files.file.size; - fileData.mimeType = options.req.files.file.mimetype; - - options.data = { - ...options.data, - ...fileData, - }; - } - - // ///////////////////////////////////// - // 5. Perform database operation - // ///////////////////////////////////// - - const { - Model, - data, - req: { - locale, - fallbackLocale, - }, - } = options; - - let result = new Model(); - - if (locale && result.setLocale) { - result.setLocale(locale, fallbackLocale); - } - - Object.assign(result, data); - await result.save(); - - result = result.toJSON({ virtuals: true }); - - // ///////////////////////////////////// - // 6. Execute field-level hooks and policies - // ///////////////////////////////////// - - result = await performFieldOperations(args.config, { - ...options, data: result, hook: 'afterRead', operationName: 'read', - }); - - // ///////////////////////////////////// - // 7. Execute after collection hook - // ///////////////////////////////////// - - const { afterCreate } = args.config.hooks; - - if (typeof afterCreate === 'function') { - result = await afterCreate(options, result); - } - - // ///////////////////////////////////// - // 8. Return results - // ///////////////////////////////////// - - return result; - } catch (err) { - throw err; + if (typeof beforeCreate === 'function') { + options = await beforeCreate(options); } + + // ///////////////////////////////////// + // 3. Execute field-level policies, hooks, and validation + // ///////////////////////////////////// + + options.data = await performFieldOperations(args.config, { ...options, hook: 'beforeCreate', operationName: 'create' }); + + // ///////////////////////////////////// + // 4. Upload and resize any files that may be present + // ///////////////////////////////////// + + if (args.config.upload) { + const { staticDir, imageSizes } = options.req.collection.config.upload; + + const fileData = {}; + + if (!args.req.files || Object.keys(args.req.files).length === 0) { + throw new MissingFile(); + } + + await mkdirp(staticDir); + + const fsSafeName = await getSafeFilename(staticDir, options.req.files.file.name); + + await options.req.files.file.mv(`${staticDir}/${fsSafeName}`); + + if (imageMIMETypes.indexOf(options.req.files.file.mimetype) > -1) { + const dimensions = await getImageSize(`${staticDir}/${fsSafeName}`); + fileData.width = dimensions.width; + fileData.height = dimensions.height; + + if (Array.isArray(imageSizes) && options.req.files.file.mimetype !== 'image/svg+xml') { + fileData.sizes = await resizeAndSave(options.config, fsSafeName, fileData.mimeType); + } + } + + fileData.filename = fsSafeName; + fileData.filesize = options.req.files.file.size; + fileData.mimeType = options.req.files.file.mimetype; + + options.data = { + ...options.data, + ...fileData, + }; + } + + // ///////////////////////////////////// + // 5. Perform database operation + // ///////////////////////////////////// + + const { + Model, + data, + req: { + locale, + fallbackLocale, + }, + } = options; + + let result = new Model(); + + if (locale && result.setLocale) { + result.setLocale(locale, fallbackLocale); + } + + Object.assign(result, data); + await result.save(); + + result = result.toJSON({ virtuals: true }); + + // ///////////////////////////////////// + // 6. Execute field-level hooks and policies + // ///////////////////////////////////// + + result = await performFieldOperations(args.config, { + ...options, data: result, hook: 'afterRead', operationName: 'read', + }); + + // ///////////////////////////////////// + // 7. Execute after collection hook + // ///////////////////////////////////// + + const { afterCreate } = args.config.hooks; + + if (typeof afterCreate === 'function') { + result = await afterCreate(options, result); + } + + // ///////////////////////////////////// + // 8. Return results + // ///////////////////////////////////// + + return result; }; module.exports = create; diff --git a/src/collections/operations/delete.js b/src/collections/operations/delete.js index e4e00777b2..5fef9691ce 100644 --- a/src/collections/operations/delete.js +++ b/src/collections/operations/delete.js @@ -1,113 +1,109 @@ const fs = require('fs'); -const { NotFound, Forbidden } = require('../../errors'); +const { NotFound, Forbidden, ErrorDeletingFile } = require('../../errors'); const executePolicy = require('../../auth/executePolicy'); const deleteQuery = async (args) => { - try { - // ///////////////////////////////////// - // 1. Retrieve and execute policy - // ///////////////////////////////////// + // ///////////////////////////////////// + // 1. Retrieve and execute policy + // ///////////////////////////////////// - const policyResults = await executePolicy(args, args.config.policies.delete); - const hasWherePolicy = typeof policyResults === 'object'; + const policyResults = await executePolicy(args, args.config.policies.delete); + const hasWherePolicy = typeof policyResults === 'object'; - let options = { - ...args, - }; + let options = { + ...args, + }; - // ///////////////////////////////////// - // 2. Execute before collection hook - // ///////////////////////////////////// + // ///////////////////////////////////// + // 2. Execute before collection hook + // ///////////////////////////////////// - const { beforeDelete } = args.config.hooks; + const { beforeDelete } = args.config.hooks; - if (typeof beforeDelete === 'function') { - options = await beforeDelete(options); - } - - // ///////////////////////////////////// - // 3. Get existing document - // ///////////////////////////////////// - - const { - Model, - id, - req: { - locale, - fallbackLocale, - }, - } = options; - - let query = { _id: id }; - - if (hasWherePolicy) { - query = { - ...query, - ...policyResults, - }; - } - - let resultToDelete = await Model.findOne(query); - - if (!resultToDelete && !hasWherePolicy) throw new NotFound(); - if (!resultToDelete && hasWherePolicy) throw new Forbidden(); - - resultToDelete = resultToDelete.toJSON({ virtuals: true }); - - if (locale && resultToDelete.setLocale) { - resultToDelete.setLocale(locale, fallbackLocale); - } - - // ///////////////////////////////////// - // 4. Delete any associated files - // ///////////////////////////////////// - - if (options.req.collection.config.upload) { - const { staticDir } = options.req.collection.config.upload; - - fs.unlink(`${staticDir}/${resultToDelete.filename}`, (err) => { - console.log('Error deleting file:', err); - }); - - if (resultToDelete.sizes) { - Object.values(resultToDelete.sizes).forEach((size) => { - fs.unlink(`${staticDir}/${size.filename}`, (err) => { - console.log('Error deleting file:', err); - }); - }); - } - } - - // ///////////////////////////////////// - // 5. Delete database document - // ///////////////////////////////////// - - let result = await Model.findOneAndDelete({ _id: id }); - - result = result.toJSON({ virtuals: true }); - - if (locale && result.setLocale) { - result.setLocale(locale, fallbackLocale); - } - - // ///////////////////////////////////// - // 4. Execute after collection hook - // ///////////////////////////////////// - - const { afterDelete } = args.config.hooks; - - if (typeof afterDelete === 'function') { - result = await afterDelete(options, result) || result; - } - - // ///////////////////////////////////// - // 5. Return results - // ///////////////////////////////////// - - return result; - } catch (err) { - throw err; + if (typeof beforeDelete === 'function') { + options = await beforeDelete(options); } + + // ///////////////////////////////////// + // 3. Get existing document + // ///////////////////////////////////// + + const { + Model, + id, + req: { + locale, + fallbackLocale, + }, + } = options; + + let query = { _id: id }; + + if (hasWherePolicy) { + query = { + ...query, + ...policyResults, + }; + } + + let resultToDelete = await Model.findOne(query); + + if (!resultToDelete && !hasWherePolicy) throw new NotFound(); + if (!resultToDelete && hasWherePolicy) throw new Forbidden(); + + resultToDelete = resultToDelete.toJSON({ virtuals: true }); + + if (locale && resultToDelete.setLocale) { + resultToDelete.setLocale(locale, fallbackLocale); + } + + // ///////////////////////////////////// + // 4. Delete any associated files + // ///////////////////////////////////// + + if (options.req.collection.config.upload) { + const { staticDir } = options.req.collection.config.upload; + + fs.unlink(`${staticDir}/${resultToDelete.filename}`, () => { + throw new ErrorDeletingFile(); + }); + + if (resultToDelete.sizes) { + Object.values(resultToDelete.sizes).forEach((size) => { + fs.unlink(`${staticDir}/${size.filename}`, () => { + throw new ErrorDeletingFile(); + }); + }); + } + } + + // ///////////////////////////////////// + // 5. Delete database document + // ///////////////////////////////////// + + let result = await Model.findOneAndDelete({ _id: id }); + + result = result.toJSON({ virtuals: true }); + + if (locale && result.setLocale) { + result.setLocale(locale, fallbackLocale); + } + + // ///////////////////////////////////// + // 4. Execute after collection hook + // ///////////////////////////////////// + + const { afterDelete } = args.config.hooks; + + if (typeof afterDelete === 'function') { + result = await afterDelete(options, result) || result; + } + + // ///////////////////////////////////// + // 5. Return results + // ///////////////////////////////////// + + return result; }; module.exports = deleteQuery; diff --git a/src/collections/operations/find.js b/src/collections/operations/find.js index c4950f86fd..7c9a54def6 100644 --- a/src/collections/operations/find.js +++ b/src/collections/operations/find.js @@ -2,146 +2,140 @@ const executePolicy = require('../../auth/executePolicy'); const performFieldOperations = require('../../fields/performFieldOperations'); const find = async (args) => { - try { - // ///////////////////////////////////// - // 1. Retrieve and execute policy - // ///////////////////////////////////// + // ///////////////////////////////////// + // 1. Retrieve and execute policy + // ///////////////////////////////////// - const policyResults = await executePolicy(args, args.config.policies.read); - const hasWherePolicy = typeof policyResults === 'object'; + const policyResults = await executePolicy(args, args.config.policies.read); + const hasWherePolicy = typeof policyResults === 'object'; - const queryToBuild = {}; + const queryToBuild = {}; - if (args.where) { - queryToBuild.where = { - and: [args.where], - }; - } - - if (hasWherePolicy) { - if (!args.where) { - queryToBuild.where = { - and: [ - policyResults, - ], - }; - } else { - queryToBuild.where.and.push(policyResults); - } - } - - let options = { - ...args, - query: await args.Model.buildQuery(queryToBuild, args.req.locale), + if (args.where) { + queryToBuild.where = { + and: [args.where], }; - - // ///////////////////////////////////// - // 2. Execute before collection hook - // ///////////////////////////////////// - - const { beforeRead } = args.config.hooks; - - if (typeof beforeRead === 'function') { - options = await beforeRead(options); - } - - // ///////////////////////////////////// - // 3. Perform database operation - // ///////////////////////////////////// - - const { - query, - page, - limit, - depth, - Model, - req: { - locale, - fallbackLocale, - payloadAPI, - }, - config, - } = options; - - let { sort } = options; - - if (!sort) { - if (config.timestamps) { - sort = '-createdAt'; - } else { - sort = '-_id'; - } - } - - const optionsToExecute = { - page: page || 1, - limit: limit || 10, - sort, - collation: sort ? { locale: 'en' } : {}, // case-insensitive sort in MongoDB - options: {}, - }; - - // Only allow depth override within REST. - // If allowed in GraphQL, it would break resolvers - // as a full object will be returned instead of an ID string - if (payloadAPI === 'REST') { - if (depth && depth !== '0') { - optionsToExecute.options.autopopulate = { - maxDepth: parseInt(depth, 10), - }; - } else { - optionsToExecute.options.autopopulate = false; - } - } - - let result = await Model.paginate(query, optionsToExecute); - - // ///////////////////////////////////// - // 4. Execute field-level policies - // ///////////////////////////////////// - - result = { - ...result, - docs: await Promise.all(result.docs.map(async (doc) => { - if (locale && doc.setLocale) { - doc.setLocale(locale, fallbackLocale); - } - - const data = doc.toJSON({ virtuals: true }); - - return performFieldOperations(args.config, { - ...options, data, hook: 'afterRead', operationName: 'read', - }); - })), - }; - - // ///////////////////////////////////// - // 6. Execute afterRead collection hook - // ///////////////////////////////////// - - const { afterRead } = args.config.hooks; - let afterReadResult = null; - - if (typeof afterRead === 'function') { - afterReadResult = { - ...result, - docs: await Promise.all(result.docs.map(async (doc) => { - return afterRead({ - options, - doc, - }) || doc; - })), - }; - } - - // ///////////////////////////////////// - // 7. Return results - // ///////////////////////////////////// - - return afterReadResult || result; - } catch (err) { - throw err; } + + if (hasWherePolicy) { + if (!args.where) { + queryToBuild.where = { + and: [ + policyResults, + ], + }; + } else { + queryToBuild.where.and.push(policyResults); + } + } + + let options = { + ...args, + query: await args.Model.buildQuery(queryToBuild, args.req.locale), + }; + + // ///////////////////////////////////// + // 2. Execute before collection hook + // ///////////////////////////////////// + + const { beforeRead } = args.config.hooks; + + if (typeof beforeRead === 'function') { + options = await beforeRead(options); + } + + // ///////////////////////////////////// + // 3. Perform database operation + // ///////////////////////////////////// + + const { + query, + page, + limit, + depth, + Model, + req: { + locale, + fallbackLocale, + payloadAPI, + }, + config, + } = options; + + let { sort } = options; + + if (!sort) { + if (config.timestamps) { + sort = '-createdAt'; + } else { + sort = '-_id'; + } + } + + const optionsToExecute = { + page: page || 1, + limit: limit || 10, + sort, + collation: sort ? { locale: 'en' } : {}, // case-insensitive sort in MongoDB + options: {}, + }; + + // Only allow depth override within REST. + // If allowed in GraphQL, it would break resolvers + // as a full object will be returned instead of an ID string + if (payloadAPI === 'REST') { + if (depth && depth !== '0') { + optionsToExecute.options.autopopulate = { + maxDepth: parseInt(depth, 10), + }; + } else { + optionsToExecute.options.autopopulate = false; + } + } + + let result = await Model.paginate(query, optionsToExecute); + + // ///////////////////////////////////// + // 4. Execute field-level policies + // ///////////////////////////////////// + + result = { + ...result, + docs: await Promise.all(result.docs.map(async (doc) => { + if (locale && doc.setLocale) { + doc.setLocale(locale, fallbackLocale); + } + + const data = doc.toJSON({ virtuals: true }); + + return performFieldOperations(args.config, { + ...options, data, hook: 'afterRead', operationName: 'read', + }); + })), + }; + + // ///////////////////////////////////// + // 6. Execute afterRead collection hook + // ///////////////////////////////////// + + const { afterRead } = args.config.hooks; + let afterReadResult = null; + + if (typeof afterRead === 'function') { + afterReadResult = { + ...result, + docs: await Promise.all(result.docs.map(async (doc) => afterRead({ + options, + doc, + }) || doc)), + }; + } + + // ///////////////////////////////////// + // 7. Return results + // ///////////////////////////////////// + + return afterReadResult || result; }; module.exports = find; diff --git a/src/collections/operations/findByID.js b/src/collections/operations/findByID.js index 825ffa19dd..5373cc6ad1 100644 --- a/src/collections/operations/findByID.js +++ b/src/collections/operations/findByID.js @@ -3,118 +3,114 @@ const executePolicy = require('../../auth/executePolicy'); const performFieldOperations = require('../../fields/performFieldOperations'); const findByID = async (args) => { - try { - // ///////////////////////////////////// - // 1. Retrieve and execute policy - // ///////////////////////////////////// + // ///////////////////////////////////// + // 1. Retrieve and execute policy + // ///////////////////////////////////// - const policyResults = await executePolicy(args, args.config.policies.read); - const hasWherePolicy = typeof policyResults === 'object'; + const policyResults = await executePolicy(args, args.config.policies.read); + const hasWherePolicy = typeof policyResults === 'object'; - const queryToBuild = { - where: { - and: [ - { - _id: { - equals: args.id, - }, + const queryToBuild = { + where: { + and: [ + { + _id: { + equals: args.id, }, - ], - }, - }; + }, + ], + }, + }; - if (hasWherePolicy) { - queryToBuild.where.and.push(policyResults); - } - - let options = { - ...args, - query: await args.Model.buildQuery(queryToBuild, args.req.locale), - }; - - // ///////////////////////////////////// - // 2. Execute before collection hook - // ///////////////////////////////////// - - const { beforeRead } = args.config.hooks; - - if (typeof beforeRead === 'function') { - options = await beforeRead(options); - } - - // ///////////////////////////////////// - // 3. Perform database operation - // ///////////////////////////////////// - - const { - depth, - Model, - query, - req: { - locale, - fallbackLocale, - payloadAPI, - }, - } = options; - - const queryOptionsToExecute = { - options: {}, - }; - - // Only allow depth override within REST. - // If allowed in GraphQL, it would break resolvers - // as a full object will be returned instead of an ID string - if (payloadAPI === 'REST') { - if (depth && depth !== '0') { - queryOptionsToExecute.options.autopopulate = { - maxDepth: parseInt(depth, 10), - }; - } else { - queryOptionsToExecute.options.autopopulate = false; - } - } - - let result = await Model.findOne(query, {}, queryOptionsToExecute); - - if (!result && !hasWherePolicy) throw new NotFound(); - if (!result && hasWherePolicy) throw new Forbidden(); - - if (locale && result.setLocale) { - result.setLocale(locale, fallbackLocale); - } - - result = result.toJSON({ virtuals: true }); - - // ///////////////////////////////////// - // 4. Execute field-level hooks and policies - // ///////////////////////////////////// - - result = await performFieldOperations(args.config, { - ...options, data: result, hook: 'afterRead', operationName: 'read', - }); - - - // ///////////////////////////////////// - // 5. Execute after collection hook - // ///////////////////////////////////// - - const { afterRead } = args.config.hooks; - - if (typeof afterRead === 'function') { - result = await afterRead({ - ...options, - doc: result, - }) || result; - } - - // ///////////////////////////////////// - // 6. Return results - // ///////////////////////////////////// - - return result; - } catch (err) { - throw err; + if (hasWherePolicy) { + queryToBuild.where.and.push(policyResults); } + + let options = { + ...args, + query: await args.Model.buildQuery(queryToBuild, args.req.locale), + }; + + // ///////////////////////////////////// + // 2. Execute before collection hook + // ///////////////////////////////////// + + const { beforeRead } = args.config.hooks; + + if (typeof beforeRead === 'function') { + options = await beforeRead(options); + } + + // ///////////////////////////////////// + // 3. Perform database operation + // ///////////////////////////////////// + + const { + depth, + Model, + query, + req: { + locale, + fallbackLocale, + payloadAPI, + }, + } = options; + + const queryOptionsToExecute = { + options: {}, + }; + + // Only allow depth override within REST. + // If allowed in GraphQL, it would break resolvers + // as a full object will be returned instead of an ID string + if (payloadAPI === 'REST') { + if (depth && depth !== '0') { + queryOptionsToExecute.options.autopopulate = { + maxDepth: parseInt(depth, 10), + }; + } else { + queryOptionsToExecute.options.autopopulate = false; + } + } + + let result = await Model.findOne(query, {}, queryOptionsToExecute); + + if (!result && !hasWherePolicy) throw new NotFound(); + if (!result && hasWherePolicy) throw new Forbidden(); + + if (locale && result.setLocale) { + result.setLocale(locale, fallbackLocale); + } + + result = result.toJSON({ virtuals: true }); + + // ///////////////////////////////////// + // 4. Execute field-level hooks and policies + // ///////////////////////////////////// + + result = await performFieldOperations(args.config, { + ...options, data: result, hook: 'afterRead', operationName: 'read', + }); + + + // ///////////////////////////////////// + // 5. Execute after collection hook + // ///////////////////////////////////// + + const { afterRead } = args.config.hooks; + + if (typeof afterRead === 'function') { + result = await afterRead({ + ...options, + doc: result, + }) || result; + } + + // ///////////////////////////////////// + // 6. Return results + // ///////////////////////////////////// + + return result; }; module.exports = findByID; diff --git a/src/collections/operations/update.js b/src/collections/operations/update.js index eed1b31876..e3c5586755 100644 --- a/src/collections/operations/update.js +++ b/src/collections/operations/update.js @@ -10,157 +10,153 @@ const getSafeFilename = require('../../uploads/getSafeFilename'); const resizeAndSave = require('../../uploads/imageResizer'); const update = async (args) => { - try { - // ///////////////////////////////////// - // 1. Execute policy - // ///////////////////////////////////// + // ///////////////////////////////////// + // 1. Execute policy + // ///////////////////////////////////// - const policyResults = await executePolicy(args, args.config.policies.update); - const hasWherePolicy = typeof policyResults === 'object'; + const policyResults = await executePolicy(args, args.config.policies.update); + const hasWherePolicy = typeof policyResults === 'object'; - let options = { ...args }; + let options = { ...args }; - // ///////////////////////////////////// - // 2. Retrieve document - // ///////////////////////////////////// + // ///////////////////////////////////// + // 2. Retrieve document + // ///////////////////////////////////// - const { - Model, - id, - req: { - locale, - fallbackLocale, - }, - } = options; + const { + Model, + id, + req: { + locale, + fallbackLocale, + }, + } = options; - const queryToBuild = { - where: { - and: [ - { - id: { - equals: id, - }, + const queryToBuild = { + where: { + and: [ + { + id: { + equals: id, }, - ], - }, - }; + }, + ], + }, + }; - if (hasWherePolicy) { - queryToBuild.where.and.push(hasWherePolicy); - } - - options.query = await args.Model.buildQuery(queryToBuild, locale); - - let doc = await Model.findOne(options.query); - - if (!doc && !hasWherePolicy) throw new NotFound(); - if (!doc && hasWherePolicy) throw new Forbidden(); - - if (locale && doc.setLocale) { - doc.setLocale(locale, fallbackLocale); - } - - options.originalDoc = doc.toJSON({ virtuals: true }); - - // ///////////////////////////////////// - // 2. Execute before update hook - // ///////////////////////////////////// - - const { beforeUpdate } = args.config.hooks; - - if (typeof beforeUpdate === 'function') { - options = await beforeUpdate(options); - } - - // ///////////////////////////////////// - // 3. Merge updates into existing data - // ///////////////////////////////////// - - options.data = deepmerge(options.originalDoc, options.data, { arrayMerge: overwriteMerge }); - - // ///////////////////////////////////// - // 4. Execute field-level hooks, policies, and validation - // ///////////////////////////////////// - - options.data = await performFieldOperations(args.config, { ...options, hook: 'beforeUpdate', operationName: 'update' }); - - // ///////////////////////////////////// - // 5. Upload and resize any files that may be present - // ///////////////////////////////////// - - if (args.config.upload) { - const fileData = {}; - - const { staticDir, imageSizes } = args.config.upload; - - if (options.req.files && options.req.files.file) { - const fsSafeName = await getSafeFilename(staticDir, options.req.files.file.name); - - await options.req.files.file.mv(`${staticDir}/${fsSafeName}`); - - fileData.filename = fsSafeName; - fileData.filesize = options.req.files.file.size; - fileData.mimeType = options.req.files.file.mimetype; - - if (imageMIMETypes.indexOf(options.req.files.file.mimetype) > -1) { - const dimensions = await getImageSize(`${staticDir}/${fsSafeName}`); - fileData.width = dimensions.width; - fileData.height = dimensions.height; - - if (Array.isArray(imageSizes) && options.req.files.file.mimetype !== 'image/svg+xml') { - fileData.sizes = await resizeAndSave(options.config, fsSafeName, fileData.mimeType); - } - } - - options.data = { - ...options.data, - ...fileData, - }; - } else if (options.data.file === null) { - options.data = { - ...options.data, - filename: null, - sizes: null, - }; - } - } - - // ///////////////////////////////////// - // 6. Perform database operation - // ///////////////////////////////////// - - Object.assign(doc, options.data); - - await doc.save(); - - doc = doc.toJSON({ virtuals: true }); - - // ///////////////////////////////////// - // 7. Execute field-level hooks and policies - // ///////////////////////////////////// - - doc = await performFieldOperations(args.config, { - ...options, data: doc, hook: 'afterRead', operationName: 'read', - }); - - // ///////////////////////////////////// - // 8. Execute after collection hook - // ///////////////////////////////////// - - const { afterUpdate } = args.config.hooks; - - if (typeof afterUpdate === 'function') { - doc = await afterUpdate(options, doc) || doc; - } - - // ///////////////////////////////////// - // 9. Return updated document - // ///////////////////////////////////// - - return doc; - } catch (err) { - throw err; + if (hasWherePolicy) { + queryToBuild.where.and.push(hasWherePolicy); } + + options.query = await args.Model.buildQuery(queryToBuild, locale); + + let doc = await Model.findOne(options.query); + + if (!doc && !hasWherePolicy) throw new NotFound(); + if (!doc && hasWherePolicy) throw new Forbidden(); + + if (locale && doc.setLocale) { + doc.setLocale(locale, fallbackLocale); + } + + options.originalDoc = doc.toJSON({ virtuals: true }); + + // ///////////////////////////////////// + // 2. Execute before update hook + // ///////////////////////////////////// + + const { beforeUpdate } = args.config.hooks; + + if (typeof beforeUpdate === 'function') { + options = await beforeUpdate(options); + } + + // ///////////////////////////////////// + // 3. Merge updates into existing data + // ///////////////////////////////////// + + options.data = deepmerge(options.originalDoc, options.data, { arrayMerge: overwriteMerge }); + + // ///////////////////////////////////// + // 4. Execute field-level hooks, policies, and validation + // ///////////////////////////////////// + + options.data = await performFieldOperations(args.config, { ...options, hook: 'beforeUpdate', operationName: 'update' }); + + // ///////////////////////////////////// + // 5. Upload and resize any files that may be present + // ///////////////////////////////////// + + if (args.config.upload) { + const fileData = {}; + + const { staticDir, imageSizes } = args.config.upload; + + if (options.req.files && options.req.files.file) { + const fsSafeName = await getSafeFilename(staticDir, options.req.files.file.name); + + await options.req.files.file.mv(`${staticDir}/${fsSafeName}`); + + fileData.filename = fsSafeName; + fileData.filesize = options.req.files.file.size; + fileData.mimeType = options.req.files.file.mimetype; + + if (imageMIMETypes.indexOf(options.req.files.file.mimetype) > -1) { + const dimensions = await getImageSize(`${staticDir}/${fsSafeName}`); + fileData.width = dimensions.width; + fileData.height = dimensions.height; + + if (Array.isArray(imageSizes) && options.req.files.file.mimetype !== 'image/svg+xml') { + fileData.sizes = await resizeAndSave(options.config, fsSafeName, fileData.mimeType); + } + } + + options.data = { + ...options.data, + ...fileData, + }; + } else if (options.data.file === null) { + options.data = { + ...options.data, + filename: null, + sizes: null, + }; + } + } + + // ///////////////////////////////////// + // 6. Perform database operation + // ///////////////////////////////////// + + Object.assign(doc, options.data); + + await doc.save(); + + doc = doc.toJSON({ virtuals: true }); + + // ///////////////////////////////////// + // 7. Execute field-level hooks and policies + // ///////////////////////////////////// + + doc = await performFieldOperations(args.config, { + ...options, data: doc, hook: 'afterRead', operationName: 'read', + }); + + // ///////////////////////////////////// + // 8. Execute after collection hook + // ///////////////////////////////////// + + const { afterUpdate } = args.config.hooks; + + if (typeof afterUpdate === 'function') { + doc = await afterUpdate(options, doc) || doc; + } + + // ///////////////////////////////////// + // 9. Return updated document + // ///////////////////////////////////// + + return doc; }; module.exports = update; diff --git a/src/errors/ErrorDeletingFile.js b/src/errors/ErrorDeletingFile.js new file mode 100644 index 0000000000..328b6c1342 --- /dev/null +++ b/src/errors/ErrorDeletingFile.js @@ -0,0 +1,10 @@ +const httpStatus = require('http-status'); +const APIError = require('./APIError'); + +class ErrorDeletingFile extends APIError { + constructor() { + super('There was an error deleting file.', httpStatus.INTERNAL_SERVER_ERROR); + } +} + +module.exports = ErrorDeletingFile; diff --git a/src/errors/index.js b/src/errors/index.js index 25b4962042..0a89bf9ea6 100644 --- a/src/errors/index.js +++ b/src/errors/index.js @@ -10,9 +10,11 @@ const ValidationError = require('./ValidationError'); const errorHandler = require('../express/middleware/errorHandler'); const MissingFile = require('./MissingFile'); const MissingSelectOptions = require('./MissingSelectOptions'); +const ErrorDeletingFile = require('./ErrorDeletingFile'); module.exports = { errorHandler, + ErrorDeletingFile, APIError, AuthenticationError, DuplicateCollection, diff --git a/src/fields/performFieldOperations.js b/src/fields/performFieldOperations.js index d6a9aae5fa..5d5b2fc64f 100644 --- a/src/fields/performFieldOperations.js +++ b/src/fields/performFieldOperations.js @@ -101,19 +101,15 @@ module.exports = async (config, operation) => { // Entry point for field validation // ////////////////////////////////////////// - try { - traverseFields(config.fields, fullData, fullOriginalDoc, ''); - await Promise.all(validationPromises); + traverseFields(config.fields, fullData, fullOriginalDoc, ''); + await Promise.all(validationPromises); - if (errors.length > 0) { - throw new ValidationError(errors); - } - - await Promise.all(policyPromises); - await Promise.all(hookPromises); - - return fullData; - } catch (error) { - throw error; + if (errors.length > 0) { + throw new ValidationError(errors); } + + await Promise.all(policyPromises); + await Promise.all(hookPromises); + + return fullData; }; diff --git a/src/globals/operations/findOne.js b/src/globals/operations/findOne.js index 3f71d4dec5..b89ec251c6 100644 --- a/src/globals/operations/findOne.js +++ b/src/globals/operations/findOne.js @@ -2,97 +2,93 @@ const executePolicy = require('../../auth/executePolicy'); const performFieldOperations = require('../../fields/performFieldOperations'); const findOne = async (args) => { - try { - // ///////////////////////////////////// - // 1. Retrieve and execute policy - // ///////////////////////////////////// + // ///////////////////////////////////// + // 1. Retrieve and execute policy + // ///////////////////////////////////// - await executePolicy(args, args.config.policies.read); + await executePolicy(args, args.config.policies.read); - let options = { ...args }; + let options = { ...args }; - // ///////////////////////////////////// - // 2. Execute before collection hook - // ///////////////////////////////////// + // ///////////////////////////////////// + // 2. Execute before collection hook + // ///////////////////////////////////// - const { beforeRead } = args.config.hooks; + const { beforeRead } = args.config.hooks; - if (typeof beforeRead === 'function') { - options = await beforeRead(options); - } - - // ///////////////////////////////////// - // 3. Perform database operation - // ///////////////////////////////////// - - const { - depth, - Model, - slug, - req: { - payloadAPI, - locale, - fallbackLocale, - }, - } = options; - - const queryOptionsToExecute = { - options: {}, - }; - - // Only allow depth override within REST. - // If allowed in GraphQL, it would break resolvers - // as a full object will be returned instead of an ID string - if (payloadAPI === 'REST') { - if (depth && depth !== '0') { - queryOptionsToExecute.options.autopopulate = { - maxDepth: parseInt(depth, 10), - }; - } else { - queryOptionsToExecute.options.autopopulate = false; - } - } - - let result = await Model.findOne({ globalType: slug }); - let data = {}; - - if (!result) { - result = {}; - } else { - if (locale && result.setLocale) { - result.setLocale(locale, fallbackLocale); - } - - data = result.toJSON({ virtuals: true }); - } - - - // ///////////////////////////////////// - // 4. Execute field-level hooks and policies - // ///////////////////////////////////// - - result = performFieldOperations(args.config, { - ...options, data, hook: 'afterRead', operationName: 'read', - }); - - // ///////////////////////////////////// - // 5. Execute after collection hook - // ///////////////////////////////////// - - const { afterRead } = args.config.hooks; - - if (typeof afterRead === 'function') { - data = await afterRead(options, result, data) || data; - } - - // ///////////////////////////////////// - // 6. Return results - // ///////////////////////////////////// - - return data; - } catch (error) { - throw error; + if (typeof beforeRead === 'function') { + options = await beforeRead(options); } + + // ///////////////////////////////////// + // 3. Perform database operation + // ///////////////////////////////////// + + const { + depth, + Model, + slug, + req: { + payloadAPI, + locale, + fallbackLocale, + }, + } = options; + + const queryOptionsToExecute = { + options: {}, + }; + + // Only allow depth override within REST. + // If allowed in GraphQL, it would break resolvers + // as a full object will be returned instead of an ID string + if (payloadAPI === 'REST') { + if (depth && depth !== '0') { + queryOptionsToExecute.options.autopopulate = { + maxDepth: parseInt(depth, 10), + }; + } else { + queryOptionsToExecute.options.autopopulate = false; + } + } + + let result = await Model.findOne({ globalType: slug }); + let data = {}; + + if (!result) { + result = {}; + } else { + if (locale && result.setLocale) { + result.setLocale(locale, fallbackLocale); + } + + data = result.toJSON({ virtuals: true }); + } + + + // ///////////////////////////////////// + // 4. Execute field-level hooks and policies + // ///////////////////////////////////// + + result = performFieldOperations(args.config, { + ...options, data, hook: 'afterRead', operationName: 'read', + }); + + // ///////////////////////////////////// + // 5. Execute after collection hook + // ///////////////////////////////////// + + const { afterRead } = args.config.hooks; + + if (typeof afterRead === 'function') { + data = await afterRead(options, result, data) || data; + } + + // ///////////////////////////////////// + // 6. Return results + // ///////////////////////////////////// + + return data; }; module.exports = findOne; diff --git a/src/globals/operations/update.js b/src/globals/operations/update.js index c63bacaa85..7a0828c2fa 100644 --- a/src/globals/operations/update.js +++ b/src/globals/operations/update.js @@ -4,98 +4,94 @@ const executePolicy = require('../../auth/executePolicy'); const performFieldOperations = require('../../fields/performFieldOperations'); const update = async (args) => { - try { - // ///////////////////////////////////// - // 1. Retrieve and execute policy - // ///////////////////////////////////// + // ///////////////////////////////////// + // 1. Retrieve and execute policy + // ///////////////////////////////////// - await executePolicy(args, args.config.policies.update); + await executePolicy(args, args.config.policies.update); - let options = { ...args }; + let options = { ...args }; - // ///////////////////////////////////// - // 2. Retrieve document - // ///////////////////////////////////// + // ///////////////////////////////////// + // 2. Retrieve document + // ///////////////////////////////////// - const { - Model, - slug, - req: { - locale, - fallbackLocale, - }, - } = options; + const { + Model, + slug, + req: { + locale, + fallbackLocale, + }, + } = options; - let global = await Model.findOne({ globalType: slug }); + let global = await Model.findOne({ globalType: slug }); - if (!global) { - global = new Model({ globalType: slug }); - } - - if (locale && global.setLocale) { - global.setLocale(locale, fallbackLocale); - } - - const globalJSON = global.toJSON({ virtuals: true }); - - // ///////////////////////////////////// - // 3. Execute before global hook - // ///////////////////////////////////// - - const { beforeUpdate } = args.config.hooks; - - if (typeof beforeUpdate === 'function') { - options = await beforeUpdate(options); - } - - // ///////////////////////////////////// - // 4. Merge updates into existing data - // ///////////////////////////////////// - - options.data = deepmerge(globalJSON, options.data, { arrayMerge: overwriteMerge }); - - // ///////////////////////////////////// - // 5. Execute field-level hooks, policies, and validation - // ///////////////////////////////////// - - options.data = await performFieldOperations(args.config, { ...options, hook: 'beforeUpdate', operationName: 'update' }); - - // ///////////////////////////////////// - // 6. Perform database operation - // ///////////////////////////////////// - - Object.assign(global, options.data); - - await global.save(); - - global = global.toJSON({ virtuals: true }); - - // ///////////////////////////////////// - // 7. Execute field-level hooks and policies - // ///////////////////////////////////// - - global = await performFieldOperations(args.config, { - ...options, data: global, hook: 'afterRead', operationName: 'read', - }); - - // ///////////////////////////////////// - // 8. Execute after global hook - // ///////////////////////////////////// - - const { afterUpdate } = args.config.hooks; - - if (typeof afterUpdate === 'function') { - global = await afterUpdate(options, global); - } - - // ///////////////////////////////////// - // 9. Return global - // ///////////////////////////////////// - - return global; - } catch (error) { - throw error; + if (!global) { + global = new Model({ globalType: slug }); } + + if (locale && global.setLocale) { + global.setLocale(locale, fallbackLocale); + } + + const globalJSON = global.toJSON({ virtuals: true }); + + // ///////////////////////////////////// + // 3. Execute before global hook + // ///////////////////////////////////// + + const { beforeUpdate } = args.config.hooks; + + if (typeof beforeUpdate === 'function') { + options = await beforeUpdate(options); + } + + // ///////////////////////////////////// + // 4. Merge updates into existing data + // ///////////////////////////////////// + + options.data = deepmerge(globalJSON, options.data, { arrayMerge: overwriteMerge }); + + // ///////////////////////////////////// + // 5. Execute field-level hooks, policies, and validation + // ///////////////////////////////////// + + options.data = await performFieldOperations(args.config, { ...options, hook: 'beforeUpdate', operationName: 'update' }); + + // ///////////////////////////////////// + // 6. Perform database operation + // ///////////////////////////////////// + + Object.assign(global, options.data); + + await global.save(); + + global = global.toJSON({ virtuals: true }); + + // ///////////////////////////////////// + // 7. Execute field-level hooks and policies + // ///////////////////////////////////// + + global = await performFieldOperations(args.config, { + ...options, data: global, hook: 'afterRead', operationName: 'read', + }); + + // ///////////////////////////////////// + // 8. Execute after global hook + // ///////////////////////////////////// + + const { afterUpdate } = args.config.hooks; + + if (typeof afterUpdate === 'function') { + global = await afterUpdate(options, global); + } + + // ///////////////////////////////////// + // 9. Return global + // ///////////////////////////////////// + + return global; }; module.exports = update; diff --git a/src/index.js b/src/index.js index 164ad47f67..98bc3f74f6 100644 --- a/src/index.js +++ b/src/index.js @@ -58,7 +58,6 @@ class Payload { this.router.use( this.config.routes.graphQL, identifyAPI('GraphQL'), - authenticate(this.config), new GraphQL(this).init(), );