Skip to content

Commit

Permalink
Core Data: Support entities in the 'canUser' selector (#63322)
Browse files Browse the repository at this point in the history
* Core Data: Support entities in the 'canUser' selector
* Cleanup 'canUser' unit tests
* Add unit tests
* Replace 'canUserEditEntityRecord' selector usages
* Make 'canUserEditEntityRecord' forwarded selector/resolver
* Add checks when the resource is an entity
* Return false when entity arg is malformed
* Update types and JSDoc
* Throw an error when an entity resource object is malformed
* Deprecate canUserEditEntityRecord

Co-authored-by: Mamaduka <mamaduka@git.wordpress.org>
Co-authored-by: ellatrix <ellatrix@git.wordpress.org>
Co-authored-by: jsnajdr <jsnajdr@git.wordpress.org>
Co-authored-by: tyxla <tyxla@git.wordpress.org>
Co-authored-by: SantosGuillamot <santosguillamot@git.wordpress.org>
Co-authored-by: adamziel <zieladam@git.wordpress.org>
Co-authored-by: TimothyBJacobs <timothyblynjacobs@git.wordpress.org>
  • Loading branch information
8 people authored Jul 11, 2024
1 parent dde48a1 commit 5ca43e8
Show file tree
Hide file tree
Showing 9 changed files with 292 additions and 124 deletions.
2 changes: 1 addition & 1 deletion docs/reference-guides/data/data-core.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ _Parameters_

- _state_ `State`: Data state.
- _action_ `string`: Action to check. One of: 'create', 'read', 'update', 'delete'.
- _resource_ `string`: REST resource to check, e.g. 'media' or 'posts'.
- _resource_ `string | EntityResource`: Entity resource to check. Accepts entity object `{ kind: 'root', name: 'media', id: 1 }` or REST base as a string - `media`.
- _id_ `EntityRecordKey`: Optional ID of the rest resource to check.

_Returns_
Expand Down
10 changes: 5 additions & 5 deletions packages/block-library/src/post-title/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ export default function PostTitleEdit( {
if ( isDescendentOfQueryLoop ) {
return false;
}
return select( coreStore ).canUserEditEntityRecord(
'postType',
postType,
postId
);
return select( coreStore ).canUser( 'update', {
kind: 'postType',
name: postType,
id: postId,
} );
},
[ isDescendentOfQueryLoop, postType, postId ]
);
Expand Down
6 changes: 5 additions & 1 deletion packages/block-library/src/utils/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ import { useViewportMatch } from '@wordpress/compose';
export function useCanEditEntity( kind, name, recordId ) {
return useSelect(
( select ) =>
select( coreStore ).canUserEditEntityRecord( kind, name, recordId ),
select( coreStore ).canUser( 'update', {
kind,
name,
id: recordId,
} ),
[ kind, name, recordId ]
);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/core-data/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ _Parameters_

- _state_ `State`: Data state.
- _action_ `string`: Action to check. One of: 'create', 'read', 'update', 'delete'.
- _resource_ `string`: REST resource to check, e.g. 'media' or 'posts'.
- _resource_ `string | EntityResource`: Entity resource to check. Accepts entity object `{ kind: 'root', name: 'media', id: 1 }` or REST base as a string - `media`.
- _id_ `EntityRecordKey`: Optional ID of the rest resource to check.

_Returns_
Expand Down
65 changes: 43 additions & 22 deletions packages/core-data/src/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -352,23 +352,48 @@ export const getEmbedPreview =
* 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} resource REST resource to check, e.g. 'media' or 'posts'.
* @param {?string} id ID of the rest resource to check.
* @param {string} requestedAction Action to check. One of: 'create', 'read', 'update',
* 'delete'.
* @param {string|Object} resource Entity resource to check. Accepts entity object `{ kind: 'root', name: 'media', 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 } ) => {
const { hasStartedResolution } = registry.select( STORE_NAME );

const resourcePath = id ? `${ resource }/${ id }` : resource;
const retrievedActions = [ 'create', 'read', 'update', 'delete' ];

if ( ! retrievedActions.includes( requestedAction ) ) {
throw new Error( `'${ requestedAction }' is not a valid action.` );
}

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 dispatch(
getOrLoadEntitiesConfig( resource.kind, resource.name )
);
const entityConfig = configs.find(
( config ) =>
config.name === resource.name &&
config.kind === resource.kind
);
if ( ! entityConfig ) {
return;
}

resourcePath =
entityConfig.baseURL + ( resource.id ? '/' + resource.id : '' );
} else {
// @todo: Maybe warn when detecting a legacy usage.
resourcePath = `/wp/v2/${ resource }` + ( id ? '/' + id : '' );
}

const { hasStartedResolution } = registry.select( STORE_NAME );

// Prevent resolving the same resource twice.
for ( const relatedAction of retrievedActions ) {
if ( relatedAction === requestedAction ) {
Expand All @@ -387,7 +412,7 @@ export const canUser =
let response;
try {
response = await apiFetch( {
path: `/wp/v2/${ resourcePath }`,
path: resourcePath,
method: 'OPTIONS',
parse: false,
} );
Expand Down Expand Up @@ -416,10 +441,15 @@ export const canUser =

registry.batch( () => {
for ( const action of retrievedActions ) {
dispatch.receiveUserPermission(
`${ action }/${ resourcePath }`,
permissions[ action ]
);
const key = (
typeof resource === 'object'
? [ action, resource.kind, resource.name, resource.id ]
: [ action, resource, id ]
)
.filter( Boolean )
.join( '/' );

dispatch.receiveUserPermission( key, permissions[ action ] );
}
} );
};
Expand All @@ -435,16 +465,7 @@ export const canUser =
export const canUserEditEntityRecord =
( kind, name, recordId ) =>
async ( { dispatch } ) => {
const configs = await dispatch( getOrLoadEntitiesConfig( kind, name ) );
const entityConfig = configs.find(
( config ) => config.name === name && config.kind === kind
);
if ( ! entityConfig ) {
return;
}

const resource = entityConfig.__unstable_rest_base;
await dispatch( canUser( 'update', resource, recordId ) );
await dispatch( canUser( 'update', { kind, name, id: recordId } ) );
};

/**
Expand Down
32 changes: 23 additions & 9 deletions packages/core-data/src/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ type EntityRecordArgs =
| [ string, string, EntityRecordKey ]
| [ string, string, EntityRecordKey, GetRecordsHttpQuery ];

type EntityResource = { kind: string; name: string; id?: EntityRecordKey };

/**
* Shared reference to an empty object for cases where it is important to avoid
* returning a new object reference on every invocation, as in a connected or
Expand Down Expand Up @@ -1136,7 +1138,8 @@ export function isPreviewEmbedFallback( state: State, url: string ): boolean {
*
* @param state Data state.
* @param action Action to check. One of: 'create', 'read', 'update', 'delete'.
* @param resource REST resource to check, e.g. 'media' or 'posts'.
* @param resource Entity resource to check. Accepts entity object `{ kind: 'root', name: 'media', id: 1 }`
* or REST base as a string - `media`.
* @param id Optional ID of the rest resource to check.
*
* @return Whether or not the user can perform the action,
Expand All @@ -1145,10 +1148,22 @@ export function isPreviewEmbedFallback( state: State, url: string ): boolean {
export function canUser(
state: State,
action: string,
resource: string,
resource: string | EntityResource,
id?: EntityRecordKey
): boolean | undefined {
const key = [ action, resource, id ].filter( Boolean ).join( '/' );
const isEntity = typeof resource === 'object';
if ( isEntity && ( ! resource.kind || ! resource.name ) ) {
return false;
}

const key = (
isEntity
? [ action, resource.kind, resource.name, resource.id ]
: [ action, resource, id ]
)
.filter( Boolean )
.join( '/' );

return state.userPermissions[ key ];
}

Expand All @@ -1173,13 +1188,12 @@ export function canUserEditEntityRecord(
name: string,
recordId: EntityRecordKey
): boolean | undefined {
const entityConfig = getEntityConfig( state, kind, name );
if ( ! entityConfig ) {
return false;
}
const resource = entityConfig.__unstable_rest_base;
deprecated( `wp.data.select( 'core' ).canUserEditEntityRecord()`, {
since: '6.7',
alternative: `wp.data.select( 'core' ).canUser( 'update', { kind, name, id } )`,
} );

return canUser( state, 'update', resource, recordId );
return canUser( state, 'update', { kind, name, id: recordId } );
}

/**
Expand Down
Loading

0 comments on commit 5ca43e8

Please sign in to comment.