Skip to content

Commit

Permalink
Core Data: Implement _fields data reuse for entities
Browse files Browse the repository at this point in the history
  • Loading branch information
aduth authored and youknowriad committed Jul 29, 2020
1 parent 9594adb commit ce01c26
Show file tree
Hide file tree
Showing 17 changed files with 718 additions and 44 deletions.
23 changes: 21 additions & 2 deletions docs/designers-developers/developers/data/data-core.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,18 +152,21 @@ _Returns_

<a name="getEntityRecord" href="#getEntityRecord">#</a> **getEntityRecord**

Returns the Entity's record object by key.
Returns the Entity's record object by key. Returns `null` if the value is not
yet received, undefined if the value entity is known to not exist, or the
entity object if it exists and is received.

_Parameters_

- _state_ `Object`: State tree
- _kind_ `string`: Entity kind.
- _name_ `string`: Entity name.
- _key_ `number`: Record's key
- _query_ `?Object`: Optional query.

_Returns_

- `?Object`: Record.
- `(?Object|undefined)`: Record.

<a name="getEntityRecordEdits" href="#getEntityRecordEdits">#</a> **getEntityRecordEdits**

Expand Down Expand Up @@ -348,6 +351,22 @@ _Returns_

- `boolean`: Whether the entity record has edits or not.

<a name="hasEntityRecords" href="#hasEntityRecords">#</a> **hasEntityRecords**

Returns true if records have been received for the given set of parameters,
or false otherwise.

_Parameters_

- _state_ `Object`: State tree
- _kind_ `string`: Entity kind.
- _name_ `string`: Entity name.
- _query_ `?Object`: Optional terms query.

_Returns_

- `boolean`: Whether entity records have been received.

<a name="hasFetchedAutosaves" href="#hasFetchedAutosaves">#</a> **hasFetchedAutosaves**

Returns true if the REST request for autosaves has completed.
Expand Down
23 changes: 21 additions & 2 deletions packages/core-data/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -389,18 +389,21 @@ _Returns_

<a name="getEntityRecord" href="#getEntityRecord">#</a> **getEntityRecord**

Returns the Entity's record object by key.
Returns the Entity's record object by key. Returns `null` if the value is not
yet received, undefined if the value entity is known to not exist, or the
entity object if it exists and is received.

_Parameters_

- _state_ `Object`: State tree
- _kind_ `string`: Entity kind.
- _name_ `string`: Entity name.
- _key_ `number`: Record's key
- _query_ `?Object`: Optional query.

_Returns_

- `?Object`: Record.
- `(?Object|undefined)`: Record.

<a name="getEntityRecordEdits" href="#getEntityRecordEdits">#</a> **getEntityRecordEdits**

Expand Down Expand Up @@ -585,6 +588,22 @@ _Returns_

- `boolean`: Whether the entity record has edits or not.

<a name="hasEntityRecords" href="#hasEntityRecords">#</a> **hasEntityRecords**

Returns true if records have been received for the given set of parameters,
or false otherwise.

_Parameters_

- _state_ `Object`: State tree
- _kind_ `string`: Entity kind.
- _name_ `string`: Entity name.
- _query_ `?Object`: Optional terms query.

_Returns_

- `boolean`: Whether entity records have been received.

<a name="hasFetchedAutosaves" href="#hasFetchedAutosaves">#</a> **hasFetchedAutosaves**

Returns true if the REST request for autosaves has completed.
Expand Down
24 changes: 19 additions & 5 deletions packages/core-data/src/queried-data/get-query-parts.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,20 @@ import { addQueryArgs } from '@wordpress/url';
/**
* Internal dependencies
*/
import { withWeakMapCache } from '../utils';
import { withWeakMapCache, getNormalizedCommaSeparable } from '../utils';

/**
* An object of properties describing a specific query.
*
* @typedef {Object} WPQueriedDataQueryParts
*
* @property {number} page The query page (1-based index, default 1).
* @property {number} perPage Items per page for query (default 10).
* @property {string} stableKey An encoded stable string of all non-pagination
* query parameters.
* @property {number} page The query page (1-based index, default 1).
* @property {number} perPage Items per page for query (default 10).
* @property {string} stableKey An encoded stable string of all non-
* pagination, non-fields query parameters.
* @property {?(string[])} fields Target subset of fields to derive from
* item objects.
* @property {?(number[])} include Specific item IDs to include.
*/

/**
Expand All @@ -36,6 +39,8 @@ export function getQueryParts( query ) {
stableKey: '',
page: 1,
perPage: 10,
fields: null,
include: null,
};

// Ensure stable key by sorting keys. Also more efficient for iterating.
Expand All @@ -49,10 +54,19 @@ export function getQueryParts( query ) {
case 'page':
parts[ key ] = Number( value );
break;

case 'per_page':
parts.perPage = Number( value );
break;

case 'include':
parts.include = getNormalizedCommaSeparable( value ).map( Number );
break;

case '_fields':
parts.fields = getNormalizedCommaSeparable( value );
break;

default:
// While it could be any deterministic string, for simplicity's
// sake mimic querystring encoding for stable key.
Expand Down
41 changes: 41 additions & 0 deletions packages/core-data/src/queried-data/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,46 @@ function items( state = {}, action ) {
return state;
}

/**
* Reducer tracking item completeness, keyed by ID. A complete item is one for
* which all fields are known. This is used in supporting `_fields` queries,
* where not all properties associated with an entity are necessarily returned.
* In such cases, completeness is used as an indication of whether it would be
* safe to use queried data for a non-`_fields`-limited request.
*
* @param {Object<string,boolean>} state Current state.
* @param {Object} action Dispatched action.
*
* @return {Object<string,boolean>} Next state.
*/
export function itemIsComplete( state = {}, action ) {
const { type, query, key = DEFAULT_ENTITY_KEY } = action;
if ( type !== 'RECEIVE_ITEMS' ) {
return state;
}

// 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 entity types, this would require
// introspection on the REST schema for each entity to know which fields
// compose a complete item for that entity.
const isCompleteQuery = ! query || ! Array.isArray( getQueryParts( query ).fields );

return {
...state,
...action.items.reduce( ( result, item ) => {
const itemId = item[ key ];

// Defer to completeness if already assigned. Technically the
// data may be outdated if receiving items for a field subset.
result[ itemId ] = state[ itemId ] || isCompleteQuery;

return result;
}, {} ),
};
}

/**
* Reducer tracking queries state, keyed by stable query key. Each reducer
* query object includes `itemIds` and `requestingPageByPerPage`.
Expand Down Expand Up @@ -171,5 +211,6 @@ const queries = ( state = {}, action ) => {

export default combineReducers( {
items,
itemIsComplete,
queries,
} );
54 changes: 49 additions & 5 deletions packages/core-data/src/queried-data/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,21 @@ const queriedItemsCacheByState = new WeakMap();
* @return {?Array} Query items.
*/
function getQueriedItemsUncached( state, query ) {
const { stableKey, page, perPage } = getQueryParts( query );
const { stableKey, page, perPage, include, fields } = getQueryParts( query );

if ( ! state.queries[ stableKey ] ) {
return null;
let itemIds;
if ( Array.isArray( include ) && ! stableKey ) {
// If the parsed query yields a set of IDs, but otherwise no filtering,
// it's safe to consider targeted item IDs as the include set. This
// doesn't guarantee that those objects have been queried, which is
// accounted for below in the loop `null` return.
itemIds = include;
// TODO: Avoid storing the empty stable string in reducer, since it
// can be computed dynamically here always.
} else if ( state.queries[ stableKey ] ) {
itemIds = state.queries[ stableKey ];
}

const itemIds = state.queries[ stableKey ];
if ( ! itemIds ) {
return null;
}
Expand All @@ -47,7 +55,43 @@ function getQueriedItemsUncached( state, query ) {
const items = [];
for ( let i = startOffset; i < endOffset; i++ ) {
const itemId = itemIds[ i ];
items.push( state.items[ itemId ] );
if ( Array.isArray( include ) && ! include.includes( itemId ) ) {
continue;
}

if ( ! state.items.hasOwnProperty( itemId ) ) {
return null;
}

const item = state.items[ itemId ];

let filteredItem;
if ( Array.isArray( fields ) ) {
filteredItem = {};

for ( let f = 0; f < fields.length; f++ ) {
// Abort the entire request if a field is missing from the item.
// This accounts for the fact that queried items are stored by
// stable key without an associated fields query. Other requests
// may have included fewer fields properties.
const field = fields[ f ];
if ( ! item.hasOwnProperty( field ) ) {
return null;
}

filteredItem[ field ] = item[ field ];
}
} else {
// If expecting a complete item, validate that completeness, or
// otherwise abort.
if ( ! state.itemIsComplete[ itemId ] ) {
return null;
}

filteredItem = item;
}

items.push( filteredItem );
}

return items;
Expand Down
34 changes: 34 additions & 0 deletions packages/core-data/src/queried-data/test/get-query-parts.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,32 @@ describe( 'getQueryParts', () => {
page: 2,
perPage: 2,
stableKey: '',
fields: null,
include: null,
} );
} );

it( 'parses out `include` ID filtering', () => {
const parts = getQueryParts( { include: [ 1 ] } );

expect( parts ).toEqual( {
page: 1,
perPage: 10,
stableKey: '',
fields: null,
include: [ 1 ],
} );
} );

it( 'parses out `_fields` property filtering', () => {
const parts = getQueryParts( { _fields: 'content', a: 1 } );

expect( parts ).toEqual( {
page: 1,
perPage: 10,
stableKey: 'a=1',
fields: [ 'content' ],
include: null,
} );
} );

Expand All @@ -23,6 +49,8 @@ describe( 'getQueryParts', () => {
page: 1,
perPage: 10,
stableKey: '%3F=%26&b=2',
fields: null,
include: null,
} );
} );

Expand All @@ -33,6 +61,8 @@ describe( 'getQueryParts', () => {
page: 1,
perPage: 10,
stableKey: 'a%5B0%5D=1&a%5B1%5D=2',
fields: null,
include: null,
} );
} );

Expand All @@ -45,6 +75,8 @@ describe( 'getQueryParts', () => {
page: 1,
perPage: 10,
stableKey: 'b=2',
fields: null,
include: null,
} );
} );

Expand All @@ -55,6 +87,8 @@ describe( 'getQueryParts', () => {
page: 1,
perPage: -1,
stableKey: 'b=2',
fields: null,
include: null,
} );
} );
} );
Loading

0 comments on commit ce01c26

Please sign in to comment.