@wordpress/core-data
Version:
Access to and manipulation of core WordPress entities.
1,134 lines (1,030 loc) • 30.2 kB
JavaScript
/**
* External dependencies
*/
import { camelCase } from 'change-case';
/**
* WordPress dependencies
*/
import { addQueryArgs } from '@wordpress/url';
import { decodeEntities } from '@wordpress/html-entities';
import apiFetch from '@wordpress/api-fetch';
/**
* Internal dependencies
*/
import { STORE_NAME } from './name';
import { additionalEntityConfigLoaders, DEFAULT_ENTITY_KEY } from './entities';
import {
forwardResolver,
getNormalizedCommaSeparable,
getUserPermissionCacheKey,
getUserPermissionsFromAllowHeader,
ALLOWED_RESOURCE_ACTIONS,
RECEIVE_INTERMEDIATE_RESULTS,
} from './utils';
import { getSyncProvider } from './sync';
import { fetchBlockPatterns } from './fetch';
/**
* Requests authors from the REST API.
*
* @param {Object|undefined} query Optional object of query parameters to
* include with request.
*/
export const getAuthors =
( query ) =>
async ( { dispatch } ) => {
const path = addQueryArgs(
'/wp/v2/users/?who=authors&per_page=100',
query
);
const users = await apiFetch( { path } );
dispatch.receiveUserQuery( path, users );
};
/**
* Requests the current user from the REST API.
*/
export const getCurrentUser =
() =>
async ( { dispatch } ) => {
const currentUser = await apiFetch( { path: '/wp/v2/users/me' } );
dispatch.receiveCurrentUser( currentUser );
};
/**
* Requests an entity's record from the REST API.
*
* @param {string} kind Entity kind.
* @param {string} name Entity name.
* @param {number|string} key Record's key
* @param {Object|undefined} query Optional object of query parameters to
* include with request. If requesting specific
* fields, fields must always include the ID.
*/
export const getEntityRecord =
( kind, name, key = '', query ) =>
async ( { select, dispatch, registry, resolveSelect } ) => {
// For back-compat, we allow querying for static templates through
// wp_template.
if (
kind === 'postType' &&
name === 'wp_template' &&
typeof key === 'string' &&
// __experimentalGetDirtyEntityRecords always calls getEntityRecord
// with a string key, so we need that it's not a numeric ID.
! /^\d+$/.test( key )
) {
name = 'wp_registered_template';
}
const configs = await resolveSelect.getEntitiesConfig( kind );
const entityConfig = configs.find(
( config ) => config.name === name && config.kind === kind
);
if ( ! entityConfig ) {
return;
}
const lock = await dispatch.__unstableAcquireStoreLock(
STORE_NAME,
[ 'entities', 'records', kind, name, key ],
{ exclusive: false }
);
try {
// Entity supports configs,
// use the sync algorithm instead of the old fetch behavior.
if (
window.__experimentalEnableSync &&
entityConfig.syncConfig &&
! query
) {
if ( globalThis.IS_GUTENBERG_PLUGIN ) {
const objectId = entityConfig.getSyncObjectId( key );
// Loads the persisted document.
await getSyncProvider().bootstrap(
entityConfig.syncObjectType,
objectId,
( record ) => {
dispatch.receiveEntityRecords(
kind,
name,
record,
query
);
}
);
// Bootstraps the edited document as well (and load from peers).
await getSyncProvider().bootstrap(
entityConfig.syncObjectType + '--edit',
objectId,
( record ) => {
dispatch( {
type: 'EDIT_ENTITY_RECORD',
kind,
name,
recordId: key,
edits: record,
meta: {
undo: undefined,
},
} );
}
);
}
} else {
if ( query !== undefined && query._fields ) {
// If requesting specific fields, items and query association to said
// records are stored by ID reference. Thus, fields must always include
// the ID.
query = {
...query,
_fields: [
...new Set( [
...( getNormalizedCommaSeparable(
query._fields
) || [] ),
entityConfig.key || DEFAULT_ENTITY_KEY,
] ),
].join(),
};
}
if ( query !== undefined && query._fields ) {
// The resolution cache won't consider query as reusable based on the
// fields, so it's tested here, prior to initiating the REST request,
// and without causing `getEntityRecord` resolution to occur.
const hasRecord = select.hasEntityRecord(
kind,
name,
key,
query
);
if ( hasRecord ) {
return;
}
}
const path = addQueryArgs(
entityConfig.baseURL + ( key ? '/' + key : '' ),
{
...entityConfig.baseURLParams,
...query,
}
);
const response = await apiFetch( { path, parse: false } );
const record = await response.json();
const permissions = getUserPermissionsFromAllowHeader(
response.headers?.get( 'allow' )
);
const canUserResolutionsArgs = [];
const receiveUserPermissionArgs = {};
for ( const action of ALLOWED_RESOURCE_ACTIONS ) {
receiveUserPermissionArgs[
getUserPermissionCacheKey( action, {
kind,
name,
id: key,
} )
] = permissions[ action ];
canUserResolutionsArgs.push( [
action,
{ kind, name, id: key },
] );
}
registry.batch( () => {
dispatch.receiveEntityRecords( kind, name, record, query );
dispatch.receiveUserPermissions(
receiveUserPermissionArgs
);
dispatch.finishResolutions(
'canUser',
canUserResolutionsArgs
);
} );
}
} finally {
dispatch.__unstableReleaseStoreLock( lock );
}
};
export const getTemplateAutoDraftId =
( staticTemplateId ) =>
async ( { resolveSelect, dispatch } ) => {
const record = await resolveSelect.getEntityRecord(
'postType',
'wp_registered_template',
staticTemplateId
);
const autoDraft = await dispatch.saveEntityRecord(
'postType',
'wp_template',
{
...record,
id: undefined,
type: 'wp_template',
status: 'auto-draft',
}
);
await dispatch.receiveTemplateAutoDraftId(
staticTemplateId,
autoDraft.id
);
};
/**
* Requests an entity's record from the REST API.
*/
export const getRawEntityRecord = forwardResolver( 'getEntityRecord' );
/**
* Requests an entity's record from the REST API.
*/
export const getEditedEntityRecord = forwardResolver( 'getEntityRecord' );
/**
* Requests the entity's records from the REST API.
*
* @param {string} kind Entity kind.
* @param {string} name Entity name.
* @param {?Object} query Query Object. If requesting specific fields, fields
* must always include the ID.
*/
export const getEntityRecords =
( kind, name, query = {} ) =>
async ( { dispatch, registry, resolveSelect } ) => {
const configs = await resolveSelect.getEntitiesConfig( kind );
const entityConfig = configs.find(
( config ) => config.name === name && config.kind === kind
);
if ( ! entityConfig ) {
return;
}
const lock = await dispatch.__unstableAcquireStoreLock(
STORE_NAME,
[ 'entities', 'records', kind, name ],
{ exclusive: false }
);
// Keep a copy of the original query for later use in getResolutionsArgs.
// The query object may be modified below (for example, when _fields is
// specified), but we want to use the original query when marking
// resolutions as finished.
const rawQuery = { ...query };
const key = entityConfig.key || DEFAULT_ENTITY_KEY;
function getResolutionsArgs( records, recordsQuery ) {
const queryArgs = Object.fromEntries(
Object.entries( recordsQuery ).filter( ( [ k, v ] ) => {
return [ 'context', '_fields' ].includes( k ) && !! v;
} )
);
return records
.filter( ( record ) => record?.[ key ] )
.map( ( record ) => [
kind,
name,
record[ key ],
Object.keys( queryArgs ).length > 0 ? queryArgs : undefined,
] );
}
try {
if ( query._fields ) {
// If requesting specific fields, items and query association to said
// records are stored by ID reference. Thus, fields must always include
// the ID.
query = {
...query,
_fields: [
...new Set( [
...( getNormalizedCommaSeparable( query._fields ) ||
[] ),
key,
] ),
].join(),
};
}
const path = addQueryArgs( entityConfig.baseURL, {
...entityConfig.baseURLParams,
...query,
} );
let records = [],
meta;
if ( entityConfig.supportsPagination && query.per_page !== -1 ) {
const response = await apiFetch( { path, parse: false } );
records = Object.values( await response.json() );
meta = {
totalItems: parseInt(
response.headers.get( 'X-WP-Total' )
),
totalPages: parseInt(
response.headers.get( 'X-WP-TotalPages' )
),
};
} else if (
query.per_page === -1 &&
query[ RECEIVE_INTERMEDIATE_RESULTS ] === true
) {
let page = 1;
let totalPages;
do {
const response = await apiFetch( {
path: addQueryArgs( path, { page, per_page: 100 } ),
parse: false,
} );
const pageRecords = Object.values( await response.json() );
totalPages = parseInt(
response.headers.get( 'X-WP-TotalPages' )
);
if ( ! meta ) {
meta = {
totalItems: parseInt(
response.headers.get( 'X-WP-Total' )
),
totalPages: 1,
};
}
records.push( ...pageRecords );
registry.batch( () => {
dispatch.receiveEntityRecords(
kind,
name,
records,
query,
false,
undefined,
meta
);
dispatch.finishResolutions(
'getEntityRecord',
getResolutionsArgs( pageRecords, rawQuery )
);
} );
page++;
} while ( page <= totalPages );
} else {
records = Object.values( await apiFetch( { path } ) );
meta = {
totalItems: records.length,
totalPages: 1,
};
}
// If we request fields but the result doesn't contain the fields,
// explicitly set these fields as "undefined"
// that way we consider the query "fulfilled".
if ( query._fields ) {
records = records.map( ( record ) => {
query._fields.split( ',' ).forEach( ( field ) => {
if ( ! record.hasOwnProperty( field ) ) {
record[ field ] = undefined;
}
} );
return record;
} );
}
registry.batch( () => {
dispatch.receiveEntityRecords(
kind,
name,
records,
query,
false,
undefined,
meta
);
const targetHints = records
.filter(
( record ) =>
!! record?.[ key ] &&
!! record?._links?.self?.[ 0 ]?.targetHints?.allow
)
.map( ( record ) => ( {
id: record[ key ],
permissions: getUserPermissionsFromAllowHeader(
record._links.self[ 0 ].targetHints.allow
),
} ) );
const canUserResolutionsArgs = [];
const receiveUserPermissionArgs = {};
for ( const targetHint of targetHints ) {
for ( const action of ALLOWED_RESOURCE_ACTIONS ) {
canUserResolutionsArgs.push( [
action,
{ kind, name, id: targetHint.id },
] );
receiveUserPermissionArgs[
getUserPermissionCacheKey( action, {
kind,
name,
id: targetHint.id,
} )
] = targetHint.permissions[ action ];
}
}
if ( targetHints.length > 0 ) {
dispatch.receiveUserPermissions(
receiveUserPermissionArgs
);
dispatch.finishResolutions(
'canUser',
canUserResolutionsArgs
);
}
dispatch.finishResolutions(
'getEntityRecord',
getResolutionsArgs( records, rawQuery )
);
dispatch.__unstableReleaseStoreLock( lock );
} );
} catch ( e ) {
dispatch.__unstableReleaseStoreLock( lock );
}
};
getEntityRecords.shouldInvalidate = ( action, kind, name ) => {
return (
( action.type === 'RECEIVE_ITEMS' || action.type === 'REMOVE_ITEMS' ) &&
action.invalidateCache &&
kind === action.kind &&
name === action.name
);
};
/**
* Requests the total number of entity records.
*/
export const getEntityRecordsTotalItems = forwardResolver( 'getEntityRecords' );
/**
* Requests the number of available pages for the given query.
*/
export const getEntityRecordsTotalPages = forwardResolver( 'getEntityRecords' );
/**
* Requests the current theme.
*/
export const getCurrentTheme =
() =>
async ( { dispatch, resolveSelect } ) => {
const activeThemes = await resolveSelect.getEntityRecords(
'root',
'theme',
{ status: 'active' }
);
dispatch.receiveCurrentTheme( activeThemes[ 0 ] );
};
/**
* Requests theme supports data from the index.
*/
export const getThemeSupports = forwardResolver( 'getCurrentTheme' );
/**
* Requests a preview from the Embed API.
*
* @param {string} url URL to get the preview for.
*/
export const getEmbedPreview =
( url ) =>
async ( { dispatch } ) => {
try {
const embedProxyResponse = await apiFetch( {
path: addQueryArgs( '/oembed/1.0/proxy', { url } ),
} );
dispatch.receiveEmbedPreview( url, embedProxyResponse );
} catch ( error ) {
// Embed API 404s if the URL cannot be embedded, so we have to catch the error from the apiRequest here.
dispatch.receiveEmbedPreview( url, false );
}
};
/**
* Checks whether the current user can perform the given action on the given
* REST resource.
*
* @param {string} requestedAction Action to check. One of: 'create', 'read', 'update',
* 'delete'.
* @param {string|Object} resource Entity resource to check. Accepts entity object `{ kind: 'postType', name: 'attachment', id: 1 }`
* or REST base as a string - `media`.
* @param {?string} id ID of the rest resource to check.
*/
export const canUser =
( requestedAction, resource, id ) =>
async ( { dispatch, registry, resolveSelect } ) => {
if ( ! ALLOWED_RESOURCE_ACTIONS.includes( requestedAction ) ) {
throw new Error( `'${ requestedAction }' is not a valid action.` );
}
const { hasStartedResolution } = registry.select( STORE_NAME );
// Prevent resolving the same resource twice.
for ( const relatedAction of ALLOWED_RESOURCE_ACTIONS ) {
if ( relatedAction === requestedAction ) {
continue;
}
const isAlreadyResolving = hasStartedResolution( 'canUser', [
relatedAction,
resource,
id,
] );
if ( isAlreadyResolving ) {
return;
}
}
let resourcePath = null;
if ( typeof resource === 'object' ) {
if ( ! resource.kind || ! resource.name ) {
throw new Error( 'The entity resource object is not valid.' );
}
const configs = await resolveSelect.getEntitiesConfig(
resource.kind
);
const entityConfig = configs.find(
( config ) =>
config.name === resource.name &&
config.kind === resource.kind
);
if ( ! entityConfig ) {
return;
}
resourcePath =
entityConfig.baseURL + ( resource.id ? '/' + resource.id : '' );
} else {
resourcePath = `/wp/v2/${ resource }` + ( id ? '/' + id : '' );
}
let response;
try {
response = await apiFetch( {
path: resourcePath,
method: 'OPTIONS',
parse: false,
} );
} catch ( error ) {
// Do nothing if our OPTIONS request comes back with an API error (4xx or
// 5xx). The previously determined isAllowed value will remain in the store.
return;
}
// Optional chaining operator is used here because the API requests don't
// return the expected result in the React native version. Instead, API requests
// only return the result, without including response properties like the headers.
const permissions = getUserPermissionsFromAllowHeader(
response.headers?.get( 'allow' )
);
registry.batch( () => {
for ( const action of ALLOWED_RESOURCE_ACTIONS ) {
const key = getUserPermissionCacheKey( action, resource, id );
dispatch.receiveUserPermission( key, permissions[ action ] );
// Mark related action resolutions as finished.
if ( action !== requestedAction ) {
dispatch.finishResolution( 'canUser', [
action,
resource,
id,
] );
}
}
} );
};
/**
* Checks whether the current user can perform the given action on the given
* REST resource.
*
* @param {string} kind Entity kind.
* @param {string} name Entity name.
* @param {number|string} recordId Record's id.
*/
export const canUserEditEntityRecord =
( kind, name, recordId ) =>
async ( { dispatch } ) => {
await dispatch( canUser( 'update', { kind, name, id: recordId } ) );
};
/**
* Request autosave data from the REST API.
*
* @param {string} postType The type of the parent post.
* @param {number} postId The id of the parent post.
*/
export const getAutosaves =
( postType, postId ) =>
async ( { dispatch, resolveSelect } ) => {
const {
rest_base: restBase,
rest_namespace: restNamespace = 'wp/v2',
supports,
} = await resolveSelect.getPostType( postType );
if ( ! supports?.autosave ) {
return;
}
const autosaves = await apiFetch( {
path: `/${ restNamespace }/${ restBase }/${ postId }/autosaves?context=edit`,
} );
if ( autosaves && autosaves.length ) {
dispatch.receiveAutosaves( postId, autosaves );
}
};
/**
* Request autosave data from the REST API.
*
* This resolver exists to ensure the underlying autosaves are fetched via
* `getAutosaves` when a call to the `getAutosave` selector is made.
*
* @param {string} postType The type of the parent post.
* @param {number} postId The id of the parent post.
*/
export const getAutosave =
( postType, postId ) =>
async ( { resolveSelect } ) => {
await resolveSelect.getAutosaves( postType, postId );
};
export const __experimentalGetCurrentGlobalStylesId =
() =>
async ( { dispatch, resolveSelect } ) => {
const activeThemes = await resolveSelect.getEntityRecords(
'root',
'theme',
{ status: 'active' }
);
const globalStylesURL =
activeThemes?.[ 0 ]?._links?.[ 'wp:user-global-styles' ]?.[ 0 ]
?.href;
if ( ! globalStylesURL ) {
return;
}
// Regex matches the ID at the end of a URL or immediately before
// the query string.
const matches = globalStylesURL.match( /\/(\d+)(?:\?|$)/ );
const id = matches ? Number( matches[ 1 ] ) : null;
if ( id ) {
dispatch.__experimentalReceiveCurrentGlobalStylesId( id );
}
};
export const __experimentalGetCurrentThemeBaseGlobalStyles =
() =>
async ( { resolveSelect, dispatch } ) => {
const currentTheme = await resolveSelect.getCurrentTheme();
// Please adjust the preloaded requests if this changes!
const themeGlobalStyles = await apiFetch( {
path: `/wp/v2/global-styles/themes/${ currentTheme.stylesheet }?context=view`,
} );
dispatch.__experimentalReceiveThemeBaseGlobalStyles(
currentTheme.stylesheet,
themeGlobalStyles
);
};
export const __experimentalGetCurrentThemeGlobalStylesVariations =
() =>
async ( { resolveSelect, dispatch } ) => {
const currentTheme = await resolveSelect.getCurrentTheme();
// Please adjust the preloaded requests if this changes!
const variations = await apiFetch( {
path: `/wp/v2/global-styles/themes/${ currentTheme.stylesheet }/variations?context=view`,
} );
dispatch.__experimentalReceiveThemeGlobalStyleVariations(
currentTheme.stylesheet,
variations
);
};
/**
* Fetches and returns the revisions of the current global styles theme.
*/
export const getCurrentThemeGlobalStylesRevisions =
() =>
async ( { resolveSelect, dispatch } ) => {
const globalStylesId =
await resolveSelect.__experimentalGetCurrentGlobalStylesId();
const record = globalStylesId
? await resolveSelect.getEntityRecord(
'root',
'globalStyles',
globalStylesId
)
: undefined;
const revisionsURL = record?._links?.[ 'version-history' ]?.[ 0 ]?.href;
if ( revisionsURL ) {
const resetRevisions = await apiFetch( {
url: revisionsURL,
} );
const revisions = resetRevisions?.map( ( revision ) =>
Object.fromEntries(
Object.entries( revision ).map( ( [ key, value ] ) => [
camelCase( key ),
value,
] )
)
);
dispatch.receiveThemeGlobalStyleRevisions(
globalStylesId,
revisions
);
}
};
getCurrentThemeGlobalStylesRevisions.shouldInvalidate = ( action ) => {
return (
action.type === 'SAVE_ENTITY_RECORD_FINISH' &&
action.kind === 'root' &&
! action.error &&
action.name === 'globalStyles'
);
};
export const getBlockPatterns =
() =>
async ( { dispatch } ) => {
const patterns = await fetchBlockPatterns();
dispatch( { type: 'RECEIVE_BLOCK_PATTERNS', patterns } );
};
export const getBlockPatternCategories =
() =>
async ( { dispatch } ) => {
const categories = await apiFetch( {
path: '/wp/v2/block-patterns/categories',
} );
dispatch( { type: 'RECEIVE_BLOCK_PATTERN_CATEGORIES', categories } );
};
export const getUserPatternCategories =
() =>
async ( { dispatch, resolveSelect } ) => {
const patternCategories = await resolveSelect.getEntityRecords(
'taxonomy',
'wp_pattern_category',
{
per_page: -1,
_fields: 'id,name,description,slug',
context: 'view',
}
);
const mappedPatternCategories =
patternCategories?.map( ( userCategory ) => ( {
...userCategory,
label: decodeEntities( userCategory.name ),
name: userCategory.slug,
} ) ) || [];
dispatch( {
type: 'RECEIVE_USER_PATTERN_CATEGORIES',
patternCategories: mappedPatternCategories,
} );
};
export const getNavigationFallbackId =
() =>
async ( { dispatch, select, registry } ) => {
const fallback = await apiFetch( {
path: addQueryArgs( '/wp-block-editor/v1/navigation-fallback', {
_embed: true,
} ),
} );
const record = fallback?._embedded?.self;
registry.batch( () => {
dispatch.receiveNavigationFallbackId( fallback?.id );
if ( ! record ) {
return;
}
// If the fallback is already in the store, don't invalidate navigation queries.
// Otherwise, invalidate the cache for the scenario where there were no Navigation
// posts in the state and the fallback created one.
const existingFallbackEntityRecord = select.getEntityRecord(
'postType',
'wp_navigation',
fallback.id
);
const invalidateNavigationQueries = ! existingFallbackEntityRecord;
dispatch.receiveEntityRecords(
'postType',
'wp_navigation',
record,
undefined,
invalidateNavigationQueries
);
// Resolve to avoid further network requests.
dispatch.finishResolution( 'getEntityRecord', [
'postType',
'wp_navigation',
fallback.id,
] );
} );
};
export const getDefaultTemplateId =
( query ) =>
async ( { dispatch, registry, resolveSelect } ) => {
const template = await apiFetch( {
path: addQueryArgs( '/wp/v2/templates/lookup', query ),
} );
// Wait for the the entities config to be loaded, otherwise receiving
// the template as an entity will not work.
await resolveSelect.getEntitiesConfig( 'postType' );
const id = template?.wp_id || template?.id;
// Endpoint may return an empty object if no template is found.
if ( id ) {
template.id = id;
template.type =
typeof id === 'string'
? 'wp_registered_template'
: 'wp_template';
registry.batch( () => {
dispatch.receiveDefaultTemplateId( query, id );
dispatch.receiveEntityRecords( 'postType', template.type, [
template,
] );
// Avoid further network requests.
dispatch.finishResolution( 'getEntityRecord', [
'postType',
template.type,
id,
] );
} );
}
};
getDefaultTemplateId.shouldInvalidate = ( action ) => {
return (
action.type === 'EDIT_ENTITY_RECORD' &&
action.kind === 'root' &&
action.name === 'site'
);
};
/**
* Requests an entity's revisions from the REST API.
*
* @param {string} kind Entity kind.
* @param {string} name Entity name.
* @param {number|string} recordKey The key of the entity record whose revisions you want to fetch.
* @param {Object|undefined} query Optional object of query parameters to
* include with request. If requesting specific
* fields, fields must always include the ID.
*/
export const getRevisions =
( kind, name, recordKey, query = {} ) =>
async ( { dispatch, registry, resolveSelect } ) => {
const configs = await resolveSelect.getEntitiesConfig( kind );
const entityConfig = configs.find(
( config ) => config.name === name && config.kind === kind
);
if ( ! entityConfig ) {
return;
}
if ( query._fields ) {
// If requesting specific fields, items and query association to said
// records are stored by ID reference. Thus, fields must always include
// the ID.
query = {
...query,
_fields: [
...new Set( [
...( getNormalizedCommaSeparable( query._fields ) ||
[] ),
entityConfig.revisionKey || DEFAULT_ENTITY_KEY,
] ),
].join(),
};
}
const path = addQueryArgs(
entityConfig.getRevisionsUrl( recordKey ),
query
);
let records, response;
const meta = {};
const isPaginated =
entityConfig.supportsPagination && query.per_page !== -1;
try {
response = await apiFetch( { path, parse: ! isPaginated } );
} catch ( error ) {
// Do nothing if our request comes back with an API error.
return;
}
if ( response ) {
if ( isPaginated ) {
records = Object.values( await response.json() );
meta.totalItems = parseInt(
response.headers.get( 'X-WP-Total' )
);
} else {
records = Object.values( response );
}
// If we request fields but the result doesn't contain the fields,
// explicitly set these fields as "undefined"
// that way we consider the query "fulfilled".
if ( query._fields ) {
records = records.map( ( record ) => {
query._fields.split( ',' ).forEach( ( field ) => {
if ( ! record.hasOwnProperty( field ) ) {
record[ field ] = undefined;
}
} );
return record;
} );
}
registry.batch( () => {
dispatch.receiveRevisions(
kind,
name,
recordKey,
records,
query,
false,
meta
);
// When requesting all fields, the list of results can be used to
// resolve the `getRevision` selector in addition to `getRevisions`.
if ( ! query?._fields && ! query.context ) {
const key = entityConfig.key || DEFAULT_ENTITY_KEY;
const resolutionsArgs = records
.filter( ( record ) => record[ key ] )
.map( ( record ) => [
kind,
name,
recordKey,
record[ key ],
] );
dispatch.finishResolutions(
'getRevision',
resolutionsArgs
);
}
} );
}
};
// Invalidate cache when a new revision is created.
getRevisions.shouldInvalidate = ( action, kind, name, recordKey ) =>
action.type === 'SAVE_ENTITY_RECORD_FINISH' &&
name === action.name &&
kind === action.kind &&
! action.error &&
recordKey === action.recordId;
/**
* Requests a specific Entity revision from the REST API.
*
* @param {string} kind Entity kind.
* @param {string} name Entity name.
* @param {number|string} recordKey The key of the entity record whose revisions you want to fetch.
* @param {number|string} revisionKey The revision's key.
* @param {Object|undefined} query Optional object of query parameters to
* include with request. If requesting specific
* fields, fields must always include the ID.
*/
export const getRevision =
( kind, name, recordKey, revisionKey, query ) =>
async ( { dispatch, resolveSelect } ) => {
const configs = await resolveSelect.getEntitiesConfig( kind );
const entityConfig = configs.find(
( config ) => config.name === name && config.kind === kind
);
if ( ! entityConfig ) {
return;
}
if ( query !== undefined && query._fields ) {
// If requesting specific fields, items and query association to said
// records are stored by ID reference. Thus, fields must always include
// the ID.
query = {
...query,
_fields: [
...new Set( [
...( getNormalizedCommaSeparable( query._fields ) ||
[] ),
entityConfig.revisionKey || DEFAULT_ENTITY_KEY,
] ),
].join(),
};
}
const path = addQueryArgs(
entityConfig.getRevisionsUrl( recordKey, revisionKey ),
query
);
let record;
try {
record = await apiFetch( { path } );
} catch ( error ) {
// Do nothing if our request comes back with an API error.
return;
}
if ( record ) {
dispatch.receiveRevisions( kind, name, recordKey, record, query );
}
};
/**
* Requests a specific post type options from the REST API.
*
* @param {string} postType Post type slug.
*/
export const getRegisteredPostMeta =
( postType ) =>
async ( { dispatch, resolveSelect } ) => {
let options;
try {
const {
rest_namespace: restNamespace = 'wp/v2',
rest_base: restBase,
} = ( await resolveSelect.getPostType( postType ) ) || {};
options = await apiFetch( {
path: `${ restNamespace }/${ restBase }/?context=edit`,
method: 'OPTIONS',
} );
} catch ( error ) {
// Do nothing if the request comes back with an API error.
return;
}
if ( options ) {
dispatch.receiveRegisteredPostMeta(
postType,
options?.schema?.properties?.meta?.properties
);
}
};
/**
* Requests entity configs for the given kind from the REST API.
*
* @param {string} kind Entity kind.
*/
export const getEntitiesConfig =
( kind ) =>
async ( { dispatch } ) => {
const loader = additionalEntityConfigLoaders.find(
( l ) => l.kind === kind
);
if ( ! loader ) {
return;
}
try {
const configs = await loader.loadEntities();
if ( ! configs.length ) {
return;
}
dispatch.addEntities( configs );
} catch {
// Do nothing if the request comes back with an API error.
}
};