diff --git a/package.json b/package.json index 14c5002112..b7ff5ac3c1 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "express-rate-limit": "^5.1.3", "extract-text-webpack-plugin": "^4.0.0-beta.0", "falsey": "^1.0.0", + "fast-memoize": "^2.5.2", "file-loader": "^1.1.11", "flatley": "^5.2.0", "graphql": "^15.0.0", diff --git a/src/fields/accessPromise.js b/src/fields/accessPromise.js index fde7650a05..9a54275687 100644 --- a/src/fields/accessPromise.js +++ b/src/fields/accessPromise.js @@ -9,10 +9,8 @@ const accessPromise = async ({ req, id, relationshipPopulations, - depth, - currentDepth, hook, - payload, + populate, }) => { const resultingData = data; @@ -30,11 +28,7 @@ const accessPromise = async ({ relationshipPopulations.push(relationshipPopulationPromise({ data, field, - depth, - currentDepth, - req, - overrideAccess, - payload, + populate, })); } }; diff --git a/src/fields/performFieldOperations.js b/src/fields/performFieldOperations.js index 58482fba2f..15a752cef8 100644 --- a/src/fields/performFieldOperations.js +++ b/src/fields/performFieldOperations.js @@ -1,6 +1,8 @@ +const memoize = require('fast-memoize'); const { ValidationError } = require('../errors'); const sanitizeFallbackLocale = require('../localization/sanitizeFallbackLocale'); const traverseFields = require('./traverseFields'); +const executeAccess = require('../auth/executeAccess'); async function performFieldOperations(entityConfig, args) { const { @@ -35,12 +37,72 @@ async function performFieldOperations(entityConfig, args) { // Maintain a top-level list of promises // so that all async field access / validations / hooks // can run in parallel + const validationPromises = []; const accessPromises = []; const relationshipPopulations = []; const hookPromises = []; const errors = []; + // //////////////////////////////////////////////////////////////// + // Create memoized Populate function — with the same input (ID), + // it will return the same result without re-querying. + // Note - this function is recreated each time a request comes in + // //////////////////////////////////////////////////////////////// + + const populate = memoize(async ( + data, + dataReference, + field, + index, + ) => { + const dataToUpdate = dataReference; + + const relation = Array.isArray(field.relationTo) ? data.relationTo : field.relationTo; + const relatedCollection = this.collections[relation]; + + if (relatedCollection) { + const accessResult = !overrideAccess ? await executeAccess({ req, disableErrors: true, id }, relatedCollection.config.access.read) : true; + + let populatedRelationship = null; + + if (accessResult && (depth && currentDepth <= depth)) { + let idString = Array.isArray(field.relationTo) ? data.value : data; + + if (typeof idString !== 'string') { + idString = idString.toString(); + } + + populatedRelationship = await this.operations.collections.findByID({ + req, + collection: relatedCollection, + id: idString, + currentDepth: currentDepth + 1, + disableErrors: true, + depth, + }); + } + + // If access control fails, update value to null + // If populatedRelationship comes back, update value + if (!accessResult || populatedRelationship) { + if (typeof index === 'number') { + if (Array.isArray(field.relationTo)) { + dataToUpdate[field.name][index].value = populatedRelationship; + } else { + dataToUpdate[field.name][index] = populatedRelationship; + } + } else if (Array.isArray(field.relationTo)) { + dataToUpdate[field.name].value = populatedRelationship; + } else { + dataToUpdate[field.name] = populatedRelationship; + } + } + } + }, { + serializer: (populateArgs) => JSON.stringify(populateArgs[0]), + }); + // ////////////////////////////////////////// // Entry point for field validation // ////////////////////////////////////////// @@ -69,6 +131,7 @@ async function performFieldOperations(entityConfig, args) { validationPromises, errors, payload: this, + populate, }); await Promise.all(hookPromises); diff --git a/src/fields/relationshipPopulationPromise.js b/src/fields/relationshipPopulationPromise.js index c93c91321a..60d3b6baad 100644 --- a/src/fields/relationshipPopulationPromise.js +++ b/src/fields/relationshipPopulationPromise.js @@ -1,70 +1,7 @@ -const executeAccess = require('../auth/executeAccess'); - -const populate = async ({ - depth, - currentDepth, - req, - overrideAccess, - dataReference, - data, - field, - index, - id, - payload, -}) => { - const dataToUpdate = dataReference; - - const relation = Array.isArray(field.relationTo) ? data.relationTo : field.relationTo; - const relatedCollection = payload.collections[relation]; - - if (relatedCollection) { - const accessResult = !overrideAccess ? await executeAccess({ req, disableErrors: true, id }, relatedCollection.config.access.read) : true; - - let populatedRelationship = null; - - if (accessResult && (depth && currentDepth <= depth)) { - let idString = Array.isArray(field.relationTo) ? data.value : data; - - if (typeof idString !== 'string') { - idString = idString.toString(); - } - - populatedRelationship = await payload.operations.collections.findByID({ - req, - collection: relatedCollection, - id: idString, - currentDepth: currentDepth + 1, - disableErrors: true, - depth, - }); - } - - // If access control fails, update value to null - // If populatedRelationship comes back, update value - if (!accessResult || populatedRelationship) { - if (typeof index === 'number') { - if (Array.isArray(field.relationTo)) { - dataToUpdate[field.name][index].value = populatedRelationship; - } else { - dataToUpdate[field.name][index] = populatedRelationship; - } - } else if (Array.isArray(field.relationTo)) { - dataToUpdate[field.name].value = populatedRelationship; - } else { - dataToUpdate[field.name] = populatedRelationship; - } - } - } -}; - const relationshipPopulationPromise = ({ data, field, - depth, - currentDepth, - req, - overrideAccess, - payload, + populate, }) => async () => { const resultingData = data; @@ -74,17 +11,12 @@ const relationshipPopulationPromise = ({ data[field.name].forEach((relatedDoc, index) => { const rowPromise = async () => { if (relatedDoc) { - await populate({ - depth, - currentDepth, - req, - overrideAccess, - data: relatedDoc, - dataReference: resultingData, + await populate( + relatedDoc, + resultingData, field, index, - payload, - }); + ); } }; @@ -93,16 +25,11 @@ const relationshipPopulationPromise = ({ await Promise.all(rowPromises); } else if (data[field.name]) { - await populate({ - depth, - currentDepth, - req, - overrideAccess, - dataReference: resultingData, - data: data[field.name], + await populate( + data[field.name], + resultingData, field, - payload, - }); + ); } }; diff --git a/src/fields/traverseFields.js b/src/fields/traverseFields.js index af58be20a8..010503f807 100644 --- a/src/fields/traverseFields.js +++ b/src/fields/traverseFields.js @@ -27,6 +27,7 @@ const traverseFields = (args) => { validationPromises, errors, payload, + populate, } = args; fields.forEach((field) => { @@ -60,6 +61,7 @@ const traverseFields = (args) => { } accessPromises.push(accessPromise({ + populate, data, originalDoc, field, @@ -68,10 +70,7 @@ const traverseFields = (args) => { req, id, relationshipPopulations, - depth, - currentDepth, hook, - payload, })); hookPromises.push(hookPromise({ diff --git a/yarn.lock b/yarn.lock index c8a6e86830..d2a6ca673a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4722,6 +4722,11 @@ fast-levenshtein@~2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= +fast-memoize@^2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/fast-memoize/-/fast-memoize-2.5.2.tgz#79e3bb6a4ec867ea40ba0e7146816f6cdce9b57e" + integrity sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw== + fast-redact@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-2.1.0.tgz#dfe3c1ca69367fb226f110aa4ec10ec85462ffdf"