Skip to content

Commit 6ef2f9b

Browse files
adbw-pgeyanthomasdevmartinjagodic
authored
fix(core): return full entry object from invokeEvent instead of just data (#7667)
The `invokeEvent` function in registry.js was returning only the data payload (`_data.entry.get('data')`) instead of returning the complete entry object. This caused entry metadata fields (slug, path, meta, etc.) to be lost during event processing, particularly affecting file collections that rely on slug for file lookups. When `invokePreSaveEvent` was called during entry persistence: 1. Entry starts with all metadata: slug, path, meta, isModification, etc. 2. `invokeEvent` is called with the full entry object 3. `invokeEvent` returns ONLY the data payload, stripping metadata 4. Calling code replaces the full entry with just the data payload 5. Downstream operations like `entryToRaw` receive incomplete entry 6. `fieldsOrder` method fails trying to match slug to file definitions This manifested as errors like: - "No file found for undefined in [collection]" - "TypeError: can't access property 'toJS', file is undefined" The bug was introduced in commit 0d7e36b (January 31, 2020) and has existed for ~5 years, but was only exposed in recent versions (3.2.0+) when file collection persistence logic started calling `invokePreSaveEvent` and fully relying on its return value to update the entry draft. Changed `invokeEvent` to return `_data.entry` (the complete entry object) instead of `_data.entry.get('data')` (just the data payload). This preserves all entry metadata through the event handler chain, ensuring that callers like `invokePreSaveEvent` receive the complete entry object with all properties intact. Verified fix resolves file collection publishing issues where: - Custom widgets modify entry data during save - File collections with single files rely on slug matching - Entry metadata must be preserved through preSave event handlers Fixes #[issue-number-if-exists] fix(core): test returning full entry when invoking preSave handler Co-authored-by: Yan <61414485+yanthomasdev@users.noreply.github.com> Co-authored-by: Martin Jagodic <jagodicmartin1@gmail.com>
1 parent 50986a0 commit 6ef2f9b

3 files changed

Lines changed: 69 additions & 4 deletions

File tree

packages/decap-cms-core/src/__tests__/backend.spec.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,68 @@ describe('Backend', () => {
391391
expect(backend.entryToRaw).toHaveBeenCalledTimes(1);
392392
expect(backend.entryToRaw).toHaveBeenCalledWith(collection, newEntry);
393393
});
394+
395+
it('should preserve slug when preSave event handler modifies file collection entry', async () => {
396+
const implementation = {
397+
init: jest.fn(() => implementation),
398+
persistEntry: jest.fn(() => implementation),
399+
};
400+
401+
const config = {
402+
backend: {
403+
commit_messages: 'commit-messages',
404+
},
405+
};
406+
407+
// File collection with a single file
408+
const collection = Map({
409+
name: 'settings',
410+
type: FILES,
411+
files: List([
412+
Map({
413+
name: 'config',
414+
file: 'data/config.json',
415+
fields: List([Map({ name: 'title', widget: 'string' })]),
416+
}),
417+
]),
418+
});
419+
420+
const originalEntry = Map({
421+
slug: 'config',
422+
path: 'data/config.json',
423+
data: Map({ title: 'original' }),
424+
meta: Map({ path: 'data/config.json' }),
425+
});
426+
427+
const entryDraft = Map({
428+
entry: originalEntry,
429+
});
430+
431+
const user = { login: 'login', name: 'name' };
432+
const backend = new Backend(implementation, { config, backendName: 'github' });
433+
434+
backend.currentUser = jest.fn().mockResolvedValue(user);
435+
backend.entryToRaw = jest.fn().mockReturnValue('content');
436+
437+
// Mock invokePreSaveEvent to simulate a preSave handler that modifies data
438+
// This is what happens when custom widgets or event handlers modify entry data
439+
// The key is that it returns the FULL entry with slug, not just the data
440+
backend.invokePreSaveEvent = jest.fn().mockImplementation(async entry => {
441+
// Simulate a preSave handler modifying the data field
442+
return entry.setIn(['data', 'title'], 'modified');
443+
});
444+
445+
await backend.persistEntry({ config, collection, entryDraft });
446+
447+
// Verify entryToRaw was called with an entry that has the slug
448+
expect(backend.entryToRaw).toHaveBeenCalledTimes(1);
449+
const entryPassedToRaw = backend.entryToRaw.mock.calls[0][1];
450+
451+
// Critical assertion: slug must be preserved
452+
expect(entryPassedToRaw.get('slug')).toBe('config');
453+
expect(entryPassedToRaw.get('path')).toBe('data/config.json');
454+
expect(entryPassedToRaw.getIn(['data', 'title'])).toBe('modified');
455+
});
394456
});
395457

396458
describe('persistMedia', () => {

packages/decap-cms-core/src/lib/__tests__/registry.spec.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ describe('registry', () => {
200200
});
201201
});
202202

203-
it(`should return an updated entry's DataMap`, async () => {
203+
it(`should return the complete updated entry object`, async () => {
204204
const { registerEventListener, invokeEvent } = require('../registry');
205205

206206
const event = 'preSave';
@@ -233,7 +233,7 @@ describe('registry', () => {
233233
expect(handler1).toHaveBeenCalledWith(data, options);
234234
expect(handler2).toHaveBeenCalledWith(dataAfterFirstHandlerExecution, options);
235235

236-
expect(result).toEqual(dataAfterSecondHandlerExecution.entry.get('data'));
236+
expect(result).toEqual(dataAfterSecondHandlerExecution.entry);
237237
});
238238

239239
it('should allow multiple events to not return a value', async () => {
@@ -254,7 +254,7 @@ describe('registry', () => {
254254

255255
expect(handler1).toHaveBeenCalledWith(data, options);
256256
expect(handler2).toHaveBeenCalledWith(data, options);
257-
expect(result).toEqual(data.entry.get('data'));
257+
expect(result).toEqual(data.entry);
258258
});
259259
});
260260
});

packages/decap-cms-core/src/lib/registry.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,10 @@ export async function invokeEvent({ name, data }) {
257257
_data = { ...data, entry };
258258
}
259259
}
260-
return _data.entry.get('data');
260+
// Return the full entry object with all metadata (slug, path, meta, etc.)
261+
// rather than just the data payload. Callers like invokePreSaveEvent expect
262+
// the complete entry object to be preserved through the event handler chain.
263+
return _data.entry;
261264
}
262265

263266
export function removeEventListener({ name, handler }) {

0 commit comments

Comments
 (0)