Skip to content

Commit 8eb9a67

Browse files
committed
Fix validation for nested object under single subfield list field
1 parent 319f028 commit 8eb9a67

2 files changed

Lines changed: 160 additions & 2 deletions

File tree

src/lib/services/contents/entry/fields.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { isMultiple } from '$lib/services/integrations/media-libraries/shared';
3131
* MediaField,
3232
* MultiValueField,
3333
* NumberField,
34+
* ObjectField,
3435
* RelationField,
3536
* SelectField,
3637
* } from '$lib/types/public';
@@ -142,8 +143,18 @@ export const getField = (args) => {
142143
const subFieldName = isNumericKey ? keyPathArray[index + 1] : undefined;
143144

144145
// It’s possible to get a single-subfield List field with or without a subfield name (e.g.
145-
// `image.0` or `image.0.src`), but when a subfield name is specified, check if it’s valid
146-
field = !subFieldName || subField.name === subFieldName ? subField : undefined;
146+
// `image.0` or `image.0.src`), but when a subfield name is specified, check if it’s valid.
147+
// The field could be nested (object inside object), so check recursively.
148+
if (
149+
!subFieldName ||
150+
subField.name === subFieldName ||
151+
(subField.widget === 'object' &&
152+
/** @type {ObjectField} */ (subField).fields?.some((f) => f.name === subFieldName))
153+
) {
154+
field = subField;
155+
} else {
156+
field = undefined;
157+
}
147158
} else if (subFields && isNumericKey) {
148159
// For list widgets with multiple fields, numeric keys (like "0") should be skipped
149160
// Keep the current field (the list widget) and continue to the next part of the path field

src/lib/services/contents/entry/fields.test.js

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,38 @@ describe('Test getField()', () => {
138138
widget: 'list',
139139
// Missing field, fields, or types
140140
},
141+
{
142+
name: 'objectList',
143+
widget: 'list',
144+
field: {
145+
name: 'item',
146+
widget: 'object',
147+
fields: [
148+
{ name: 'title', widget: 'string' },
149+
{ name: 'description', widget: 'text' },
150+
{ name: 'active', widget: 'boolean' },
151+
],
152+
},
153+
},
154+
{
155+
name: 'nestedObjectList',
156+
widget: 'list',
157+
field: {
158+
name: 'section',
159+
widget: 'object',
160+
fields: [
161+
{ name: 'header', widget: 'string' },
162+
{
163+
name: 'content',
164+
widget: 'object',
165+
fields: [
166+
{ name: 'body', widget: 'text' },
167+
{ name: 'footnote', widget: 'string' },
168+
],
169+
},
170+
],
171+
},
172+
},
141173
{
142174
name: 'category',
143175
widget: 'select',
@@ -347,6 +379,121 @@ describe('Test getField()', () => {
347379

348380
expect(result).toBeUndefined();
349381
});
382+
383+
test('should handle list field with single object subfield', () => {
384+
// @ts-expect-error - Simplified mock for testing
385+
mockGetCollection.mockReturnValue(mockCollection);
386+
387+
// Access the object subfield itself
388+
const objectResult = getField({
389+
collectionName: 'posts',
390+
keyPath: 'objectList.0',
391+
valueMap: {},
392+
});
393+
394+
expect(objectResult).toEqual({
395+
name: 'item',
396+
widget: 'object',
397+
fields: [
398+
{ name: 'title', widget: 'string' },
399+
{ name: 'description', widget: 'text' },
400+
{ name: 'active', widget: 'boolean' },
401+
],
402+
});
403+
404+
// Access a field within the object subfield - this tests the specific condition
405+
const titleResult = getField({
406+
collectionName: 'posts',
407+
keyPath: 'objectList.0.title',
408+
valueMap: {},
409+
});
410+
411+
expect(titleResult).toEqual({ name: 'title', widget: 'string' });
412+
413+
// Access another field within the object subfield
414+
const descriptionResult = getField({
415+
collectionName: 'posts',
416+
keyPath: 'objectList.0.description',
417+
valueMap: {},
418+
});
419+
420+
expect(descriptionResult).toEqual({ name: 'description', widget: 'text' });
421+
422+
// Access a boolean field within the object subfield
423+
const activeResult = getField({
424+
collectionName: 'posts',
425+
keyPath: 'objectList.0.active',
426+
valueMap: {},
427+
});
428+
429+
expect(activeResult).toEqual({ name: 'active', widget: 'boolean' });
430+
431+
// Access a non-existent field within the object subfield
432+
const invalidResult = getField({
433+
collectionName: 'posts',
434+
keyPath: 'objectList.0.nonexistent',
435+
valueMap: {},
436+
});
437+
438+
expect(invalidResult).toBeUndefined();
439+
});
440+
441+
test('should handle list field with single object subfield - edge cases', () => {
442+
// @ts-expect-error - Simplified mock for testing
443+
mockGetCollection.mockReturnValue(mockCollection);
444+
445+
// Test when the object subfield exists but the requested field doesn't exist in it
446+
// This should trigger the condition where subField.widget === 'object' but
447+
// the field is not found in subField.fields, causing field to be set to undefined
448+
const result = getField({
449+
collectionName: 'posts',
450+
keyPath: 'objectList.0.invalidField',
451+
valueMap: {},
452+
});
453+
454+
expect(result).toBeUndefined();
455+
456+
// Test accessing deeper nested path that doesn't exist
457+
const deepResult = getField({
458+
collectionName: 'posts',
459+
keyPath: 'objectList.0.title.nested.field',
460+
valueMap: {},
461+
});
462+
463+
expect(deepResult).toBeUndefined();
464+
});
465+
466+
test('should handle nested object subfields recursively', () => {
467+
// @ts-expect-error - Simplified mock for testing
468+
mockGetCollection.mockReturnValue(mockCollection);
469+
470+
// Test accessing nested object field within list item
471+
// This specifically tests the recursive condition:
472+
// (subField.widget === 'object' && subField.fields?.some((f) => f.name === subFieldName))
473+
const nestedResult = getField({
474+
collectionName: 'posts',
475+
keyPath: 'nestedObjectList.0.content',
476+
valueMap: {},
477+
});
478+
479+
expect(nestedResult).toEqual({
480+
name: 'content',
481+
widget: 'object',
482+
fields: [
483+
{ name: 'body', widget: 'text' },
484+
{ name: 'footnote', widget: 'string' },
485+
],
486+
});
487+
488+
// Test accessing field within the nested object
489+
const deepFieldResult = getField({
490+
collectionName: 'posts',
491+
keyPath: 'nestedObjectList.0.content.body',
492+
valueMap: {},
493+
});
494+
495+
expect(deepFieldResult).toEqual({ name: 'body', widget: 'text' });
496+
});
350497
});
351498

352499
describe('Variable type fields with valueMap', () => {

0 commit comments

Comments
 (0)