From 13b546cee770dc984fcbaaa396ae48f57c4af87a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 27 Jun 2026 22:36:34 +0000 Subject: [PATCH] Improved API output serializer to avoid per-request allocations no ref - removeXBY runs on every API response and walked the full tree allocating a fresh `['published_by']` array for every key and calling lodash for every value - replaced the per-key includes() with a direct comparison, the lodash type checks with a typeof check, and dropped the now-unused lodash import - behavior is unchanged: published_by is still stripped anywhere in the response, including null values and nested resources, covered by the extended unit test --- .../endpoints/utils/serializers/output/all.js | 17 ++++++++---- .../utils/serializers/output/all.test.js | 27 +++++++++++++++++++ 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/ghost/core/core/server/api/endpoints/utils/serializers/output/all.js b/ghost/core/core/server/api/endpoints/utils/serializers/output/all.js index 55a8cc9a352..34a5b828e5a 100644 --- a/ghost/core/core/server/api/endpoints/utils/serializers/output/all.js +++ b/ghost/core/core/server/api/endpoints/utils/serializers/output/all.js @@ -1,13 +1,20 @@ const debug = require('@tryghost/debug')('api:endpoints:utils:serializers:output:all'); -const _ = require('lodash'); +// Strips the legacy `published_by` field (a posts/pages column) from anywhere in +// the response. This runs after every endpoint, so the walk stays allocation-free +// in the hot path: a direct key comparison and a typeof check, no per-key array +// literal or lodash calls. const removeXBY = (object) => { - for (const [key, value] of Object.entries(object)) { + for (const key of Object.keys(object)) { + if (key === 'published_by') { + delete object[key]; + continue; + } + // CASE: go deeper - if (_.isObject(value) || _.isArray(value)) { + const value = object[key]; + if (value !== null && typeof value === 'object') { removeXBY(value); - } else if (['published_by'].includes(key)) { - delete object[key]; } } diff --git a/ghost/core/test/unit/api/canary/utils/serializers/output/all.test.js b/ghost/core/test/unit/api/canary/utils/serializers/output/all.test.js index d698eb7205d..4ae8001f398 100644 --- a/ghost/core/test/unit/api/canary/utils/serializers/output/all.test.js +++ b/ghost/core/test/unit/api/canary/utils/serializers/output/all.test.js @@ -62,5 +62,32 @@ describe('Unit: endpoints/utils/serializers/output/all', function () { assertExists(response.pages[0].authors); assertExists(response.pages[0].authors[0].slug); }); + + it('removes a null published_by', function () { + const response = {post: {published_by: null, title: 'xxx'}}; + + serializers.output.all.after({}, {response}); + + assert.equal('published_by' in response.post, false); + assertExists(response.post.title); + }); + + it('removes published_by from deeply nested resources', function () { + const response = { + posts: [ + { + title: 'xxx', + published_by: 'xxx', + tiers: [{name: 'free', published_by: 'yyy'}] + } + ] + }; + + serializers.output.all.after({}, {response}); + + assert.equal('published_by' in response.posts[0], false); + assert.equal('published_by' in response.posts[0].tiers[0], false); + assert.equal(response.posts[0].tiers[0].name, 'free'); + }); }); });