Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 18 additions & 19 deletions packages/core-data/src/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ export const getEntityRecord = createSelector(
}
const context = query?.context ?? 'default';

if ( query === undefined ) {
if ( ! query || ! query._fields ) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new condition is the same as that used in the reducer.

// An item is considered complete if it is received without an associated
// fields query. Ideally, this would be implemented in such a way where the
// complete aggregate of all fields would satisfy completeness. Since the
// fields are not consistent across all entities, this would require
// introspection on the REST schema for each entity to know which fields
// compose a complete item for that entity.
const queryParts = query ? getQueryParts( query ) : {};
const isCompleteQuery =
! query || ! Array.isArray( queryParts.fields );

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this now make the && query._fields on the condition below redundant?

if ( item && query._fields ) {

Copy link
Member Author

@Mamaduka Mamaduka Sep 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would it?

It's good practice to check argument existence before iterating on them. I think we should leave the linked condition.

Copy link
Member

@aduth aduth Sep 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I mean is that line 378 can now never be reached if query._fields is falsey, so there's no need in checking that it's truthy.

Copy link
Member Author

@Mamaduka Mamaduka Sep 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. I don't have a strong opinion here, but it seems like a good check to keep in place "just in case" 😄

Updated in d1b7dca.

// If expecting a complete item, validate that completeness.
if ( ! queriedState.itemIsComplete[ context ]?.[ key ] ) {
return undefined;
Expand All @@ -375,30 +375,29 @@ export const getEntityRecord = createSelector(
}

const item = queriedState.items[ context ]?.[ key ];
if ( item && query._fields ) {
const filteredItem = {};
const fields = getNormalizedCommaSeparable( query._fields ) ?? [];
for ( let f = 0; f < fields.length; f++ ) {
const field = fields[ f ].split( '.' );
let value = item;
field.forEach( ( fieldName ) => {
value = value?.[ fieldName ];
} );
setNestedValue( filteredItem, field, value );
}
return filteredItem as EntityRecord;
if ( ! item ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the flattening with early return 👍

return item;
}

return item;
const filteredItem = {};
const fields = getNormalizedCommaSeparable( query._fields ) ?? [];
for ( let f = 0; f < fields.length; f++ ) {
const field = fields[ f ].split( '.' );
let value = item;
field.forEach( ( fieldName ) => {
value = value?.[ fieldName ];
} );
setNestedValue( filteredItem, field, value );
}
return filteredItem as EntityRecord;
} ) as GetEntityRecord,
( state: State, kind, name, recordId, query ) => {
const context = query?.context ?? 'default';
const queriedState =
state.entities.records?.[ kind ]?.[ name ]?.queriedData;
Comment on lines +396 to +397
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just extracted it into a reusable variable.

return [
state.entities.records?.[ kind ]?.[ name ]?.queriedData?.items[
context
]?.[ recordId ],
state.entities.records?.[ kind ]?.[ name ]?.queriedData
?.itemIsComplete[ context ]?.[ recordId ],
queriedState?.items[ context ]?.[ recordId ],
queriedState?.itemIsComplete[ context ]?.[ recordId ],
];
}
) as GetEntityRecord;
Expand Down
105 changes: 92 additions & 13 deletions packages/core-data/src/test/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import deepFreeze from 'deep-freeze';
*/
import {
getEntityRecord,
__experimentalGetEntityRecordNoResolver,
hasEntityRecords,
getEntityRecords,
getRawEntityRecord,
Expand All @@ -25,11 +24,7 @@ import {
getRevision,
} from '../selectors';

// getEntityRecord and __experimentalGetEntityRecordNoResolver selectors share the same tests.
describe.each( [
[ getEntityRecord ],
[ __experimentalGetEntityRecordNoResolver ],
] )( '%p', ( selector ) => {
Comment on lines -28 to -32
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed coverage for __experimentalGetEntityRecordNoResolver. It's a leftover from when the data package was introduced and hasn't been used in the core.

I think we should just remove or deprecate it.

describe( 'getEntityRecord', () => {
describe( 'normalizing Post ID passed as recordKey', () => {
it( 'normalizes any Post ID recordKey argument to a Number via `__unstableNormalizeArgs` method', async () => {
const normalized = getEntityRecord.__unstableNormalizeArgs( [
Expand Down Expand Up @@ -70,7 +65,7 @@ describe.each( [
},
},
} );
expect( selector( state, 'foo', 'bar', 'baz' ) ).toBeUndefined();
expect( getEntityRecord( state, 'foo', 'bar', 'baz' ) ).toBeUndefined();
} );

it( 'should return undefined for unknown record’s key', () => {
Expand All @@ -89,7 +84,9 @@ describe.each( [
},
},
} );
expect( selector( state, 'root', 'postType', 'post' ) ).toBeUndefined();
expect(
getEntityRecord( state, 'root', 'postType', 'post' )
).toBeUndefined();
} );

it( 'should return a record by key', () => {
Expand All @@ -116,16 +113,98 @@ describe.each( [
},
},
} );
expect( selector( state, 'root', 'postType', 'post' ) ).toEqual( {
slug: 'post',
expect( getEntityRecord( state, 'root', 'postType', 'post' ) ).toEqual(
{
slug: 'post',
}
);
} );

it( 'should return undefined if no item received, filtered item requested', () => {
const state = deepFreeze( {
entities: {
records: {
root: {
postType: {
queriedData: {
items: {},
itemIsComplete: {},
queries: {},
},
},
},
},
},
} );

expect(
getEntityRecord( state, 'root', 'postType', 'post', {
_fields: 'content',
} )
).toBeUndefined();
} );

it( 'should return null if no item received, filtered item requested', () => {} );
it( 'should return filtered item if incomplete item received, filtered item requested', () => {
const state = deepFreeze( {
entities: {
records: {
root: {
postType: {
queriedData: {
items: {
default: {
post: {
content: 'chicken',
author: 'bob',
},
},
},
itemIsComplete: {},
queries: {},
},
},
},
},
},
} );

it( 'should return filtered item if incomplete item received, filtered item requested', () => {} );
expect(
getEntityRecord( state, 'root', 'postType', 'post', {
_fields: 'content',
} )
).toEqual( { content: 'chicken' } );
} );

it( 'should return null if incomplete item received, complete item requested', () => {} );
it( 'should return undefined if incomplete item received, complete item requested', () => {
const state = deepFreeze( {
entities: {
records: {
root: {
postType: {
queriedData: {
items: {
default: {
post: {
content: 'chicken',
author: 'bob',
},
},
},
itemIsComplete: {},
queries: {},
},
},
},
},
},
} );

expect(
getEntityRecord( state, 'root', 'postType', 'post', {
context: 'default',
} )
).toBeUndefined();
} );

it( 'should return filtered item if complete item received, filtered item requested', () => {
const state = deepFreeze( {
Expand Down
Loading