diff --git a/docs/reference-guides/data/data-core.md b/docs/reference-guides/data/data-core.md index 455808c6fda651..179f23a986c340 100644 --- a/docs/reference-guides/data/data-core.md +++ b/docs/reference-guides/data/data-core.md @@ -21,7 +21,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'. -- _id_ `RecordKey`: Optional ID of the rest resource to check. +- _id_ `GenericRecordKey`: Optional ID of the rest resource to check. _Returns_ @@ -39,9 +39,9 @@ Calling this may trigger an OPTIONS request to the REST API via the _Parameters_ - _state_ `State`: Data state. -- _kind_ `string`: Entity kind. -- _name_ `string`: Entity name. -- _recordId_ `RecordKey`: Record's id. +- _kind_ `Kind`: Entity kind. +- _name_ `Name`: Entity name. +- _recordId_ `GenericRecordKey`: Record's id. _Returns_ @@ -70,8 +70,8 @@ _Parameters_ - _state_ `State`: State tree. - _postType_ `string`: The type of the parent post. -- _postId_ `RecordKey`: The id of the parent post. -- _authorId_ `RecordKey`: The id of the author. +- _postId_ `GenericRecordKey`: The id of the parent post. +- _authorId_ `GenericRecordKey`: The id of the author. _Returns_ @@ -88,7 +88,7 @@ _Parameters_ - _state_ `State`: State tree. - _postType_ `string`: The type of the parent post. -- _postId_ `RecordKey`: The id of the parent post. +- _postId_ `GenericRecordKey`: The id of the parent post. _Returns_ @@ -149,9 +149,9 @@ Returns the specified entity record, merged with its edits. _Parameters_ - _state_ `State`: State tree. -- _kind_ `string`: Entity kind. -- _name_ `string`: Entity name. -- _recordId_ `RecordKey`: Record ID. +- _kind_ `K`: Entity kind. +- _name_ `N`: Entity name. +- _recordId_ `KeyOf< K, N >`: Record ID. _Returns_ @@ -179,7 +179,7 @@ Returns the loaded entities for the given kind. _Parameters_ - _state_ `State`: Data state. -- _kind_ `string`: Entity kind. +- _kind_ `Kind`: Entity kind. _Returns_ @@ -192,7 +192,7 @@ Returns the loaded entities for the given kind. _Parameters_ - _state_ `State`: Data state. -- _kind_ `string`: Entity kind. +- _kind_ `Kind`: Entity kind. _Returns_ @@ -207,8 +207,8 @@ Returns the entity config given its kind and name. _Parameters_ - _state_ `State`: Data state. -- _kind_ `string`: Entity kind. -- _name_ `string`: Entity name. +- _kind_ `Kind`: Entity kind. +- _name_ `Name`: Entity name. _Returns_ @@ -221,8 +221,8 @@ Returns the entity config given its kind and name. _Parameters_ - _state_ `State`: Data state. -- _kind_ `string`: Entity kind. -- _name_ `string`: Entity name. +- _kind_ `Kind`: Entity kind. +- _name_ `Name`: Entity name. _Returns_ @@ -237,14 +237,14 @@ entity object if it exists and is received. _Parameters_ - _state_ `State`: State tree -- _kind_ `string`: Entity kind. -- _name_ `string`: Entity name. -- _key_ `RecordKey`: Record's key -- _query_ `EntityQuery< any >`: Optional query. +- _kind_ `K`: Entity kind. +- _name_ `N`: Entity name. +- _key_ `KeyOf< R >`: Record's key +- _query_ Optional query. _Returns_ -- `EntityRecord | undefined`: Record. +- Record. ### getEntityRecordEdits @@ -253,9 +253,9 @@ Returns the specified entity record's edits. _Parameters_ - _state_ `State`: State tree. -- _kind_ `string`: Entity kind. -- _name_ `string`: Entity name. -- _recordId_ `RecordKey`: Record ID. +- _kind_ `K`: Entity kind. +- _name_ `N`: Entity name. +- _recordId_ `KeyOf< K, N >`: Record ID. _Returns_ @@ -272,9 +272,9 @@ They are defined in the entity's config. _Parameters_ - _state_ `State`: State tree. -- _kind_ `string`: Entity kind. -- _name_ `string`: Entity name. -- _recordId_ `RecordKey`: Record ID. +- _kind_ `K`: Entity kind. +- _name_ `N`: Entity name. +- _recordId_ `KeyOf< K, N >`: Record ID. _Returns_ @@ -287,13 +287,13 @@ Returns the Entity's records. _Parameters_ - _state_ `State`: State tree -- _kind_ `string`: Entity kind. -- _name_ `string`: Entity name. -- _query_ `EntityQuery< any >`: Optional terms query. +- _kind_ `K`: Entity kind. +- _name_ `N`: Entity name. +- _query_ Optional terms query. _Returns_ -- `Array< EntityRecord > | undefined`: Records. +- Records. ### getLastEntityDeleteError @@ -302,9 +302,9 @@ Returns the specified entity record's last delete error. _Parameters_ - _state_ `State`: State tree. -- _kind_ `string`: Entity kind. -- _name_ `string`: Entity name. -- _recordId_ `RecordKey`: Record ID. +- _kind_ `Kind`: Entity kind. +- _name_ `Name`: Entity name. +- _recordId_ `GenericRecordKey`: Record ID. _Returns_ @@ -317,9 +317,9 @@ Returns the specified entity record's last save error. _Parameters_ - _state_ `State`: State tree. -- _kind_ `string`: Entity kind. -- _name_ `string`: Entity name. -- _recordId_ `RecordKey`: Record ID. +- _kind_ `Kind`: Entity kind. +- _name_ `Name`: Entity name. +- _recordId_ `GenericRecordKey`: Record ID. _Returns_ @@ -333,9 +333,9 @@ with its attributes mapped to their raw values. _Parameters_ - _state_ `State`: State tree. -- _kind_ `string`: Entity kind. -- _name_ `string`: Entity name. -- _key_ `RecordKey`: Record's key. +- _kind_ `K`: Entity kind. +- _name_ `N`: Entity name. +- _key_ `KeyOf< K, N >`: Record's key. _Returns_ @@ -421,9 +421,9 @@ and false otherwise. _Parameters_ - _state_ `State`: State tree. -- _kind_ `string`: Entity kind. -- _name_ `string`: Entity name. -- _recordId_ `RecordKey`: Record ID. +- _kind_ `K`: Entity kind. +- _name_ `N`: Entity name. +- _recordId_ `KeyOf< K, N >`: Record ID. _Returns_ @@ -437,9 +437,9 @@ or false otherwise. _Parameters_ - _state_ `State`: State tree -- _kind_ `string`: Entity kind. -- _name_ `string`: Entity name. -- _query_ `EntityQuery< any >`: Optional terms query. +- _kind_ `K`: Entity kind. +- _name_ `N`: Entity name. +- _query_ `EntityQuery< C >`: Optional terms query. _Returns_ @@ -453,7 +453,7 @@ _Parameters_ - _state_ `State`: State tree. - _postType_ `string`: The type of the parent post. -- _postId_ `RecordKey`: The id of the parent post. +- _postId_ `GenericRecordKey`: The id of the parent post. _Returns_ @@ -492,9 +492,9 @@ Returns true if the specified entity record is autosaving, and false otherwise. _Parameters_ - _state_ `State`: State tree. -- _kind_ `string`: Entity kind. -- _name_ `string`: Entity name. -- _recordId_ `RecordKey`: Record ID. +- _kind_ `Kind`: Entity kind. +- _name_ `Name`: Entity name. +- _recordId_ `GenericRecordKey`: Record ID. _Returns_ @@ -507,9 +507,9 @@ Returns true if the specified entity record is deleting, and false otherwise. _Parameters_ - _state_ `State`: State tree. -- _kind_ `string`: Entity kind. -- _name_ `string`: Entity name. -- _recordId_ `RecordKey`: Record ID. +- _kind_ `Kind`: Entity kind. +- _name_ `Name`: Entity name. +- _recordId_ `GenericRecordKey`: Record ID. _Returns_ @@ -553,9 +553,9 @@ Returns true if the specified entity record is saving, and false otherwise. _Parameters_ - _state_ `State`: State tree. -- _kind_ `string`: Entity kind. -- _name_ `string`: Entity name. -- _recordId_ `RecordKey`: Record ID. +- _kind_ `K`: Entity kind. +- _name_ `N`: Entity name. +- _recordId_ `KeyOf< K, N >`: Record ID. _Returns_ diff --git a/packages/core-data/README.md b/packages/core-data/README.md index bfe9535dd2fe7b..2f6c845a7d933e 100644 --- a/packages/core-data/README.md +++ b/packages/core-data/README.md @@ -268,7 +268,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'. -- _id_ `RecordKey`: Optional ID of the rest resource to check. +- _id_ `GenericRecordKey`: Optional ID of the rest resource to check. _Returns_ @@ -286,9 +286,9 @@ Calling this may trigger an OPTIONS request to the REST API via the _Parameters_ - _state_ `State`: Data state. -- _kind_ `string`: Entity kind. -- _name_ `string`: Entity name. -- _recordId_ `RecordKey`: Record's id. +- _kind_ `Kind`: Entity kind. +- _name_ `Name`: Entity name. +- _recordId_ `GenericRecordKey`: Record's id. _Returns_ @@ -317,8 +317,8 @@ _Parameters_ - _state_ `State`: State tree. - _postType_ `string`: The type of the parent post. -- _postId_ `RecordKey`: The id of the parent post. -- _authorId_ `RecordKey`: The id of the author. +- _postId_ `GenericRecordKey`: The id of the parent post. +- _authorId_ `GenericRecordKey`: The id of the author. _Returns_ @@ -335,7 +335,7 @@ _Parameters_ - _state_ `State`: State tree. - _postType_ `string`: The type of the parent post. -- _postId_ `RecordKey`: The id of the parent post. +- _postId_ `GenericRecordKey`: The id of the parent post. _Returns_ @@ -396,9 +396,9 @@ Returns the specified entity record, merged with its edits. _Parameters_ - _state_ `State`: State tree. -- _kind_ `string`: Entity kind. -- _name_ `string`: Entity name. -- _recordId_ `RecordKey`: Record ID. +- _kind_ `K`: Entity kind. +- _name_ `N`: Entity name. +- _recordId_ `KeyOf< K, N >`: Record ID. _Returns_ @@ -426,7 +426,7 @@ Returns the loaded entities for the given kind. _Parameters_ - _state_ `State`: Data state. -- _kind_ `string`: Entity kind. +- _kind_ `Kind`: Entity kind. _Returns_ @@ -439,7 +439,7 @@ Returns the loaded entities for the given kind. _Parameters_ - _state_ `State`: Data state. -- _kind_ `string`: Entity kind. +- _kind_ `Kind`: Entity kind. _Returns_ @@ -454,8 +454,8 @@ Returns the entity config given its kind and name. _Parameters_ - _state_ `State`: Data state. -- _kind_ `string`: Entity kind. -- _name_ `string`: Entity name. +- _kind_ `Kind`: Entity kind. +- _name_ `Name`: Entity name. _Returns_ @@ -468,8 +468,8 @@ Returns the entity config given its kind and name. _Parameters_ - _state_ `State`: Data state. -- _kind_ `string`: Entity kind. -- _name_ `string`: Entity name. +- _kind_ `Kind`: Entity kind. +- _name_ `Name`: Entity name. _Returns_ @@ -484,14 +484,14 @@ entity object if it exists and is received. _Parameters_ - _state_ `State`: State tree -- _kind_ `string`: Entity kind. -- _name_ `string`: Entity name. -- _key_ `RecordKey`: Record's key -- _query_ `EntityQuery< any >`: Optional query. +- _kind_ `K`: Entity kind. +- _name_ `N`: Entity name. +- _key_ `KeyOf< R >`: Record's key +- _query_ Optional query. _Returns_ -- `EntityRecord | undefined`: Record. +- Record. ### getEntityRecordEdits @@ -500,9 +500,9 @@ Returns the specified entity record's edits. _Parameters_ - _state_ `State`: State tree. -- _kind_ `string`: Entity kind. -- _name_ `string`: Entity name. -- _recordId_ `RecordKey`: Record ID. +- _kind_ `K`: Entity kind. +- _name_ `N`: Entity name. +- _recordId_ `KeyOf< K, N >`: Record ID. _Returns_ @@ -519,9 +519,9 @@ They are defined in the entity's config. _Parameters_ - _state_ `State`: State tree. -- _kind_ `string`: Entity kind. -- _name_ `string`: Entity name. -- _recordId_ `RecordKey`: Record ID. +- _kind_ `K`: Entity kind. +- _name_ `N`: Entity name. +- _recordId_ `KeyOf< K, N >`: Record ID. _Returns_ @@ -534,13 +534,13 @@ Returns the Entity's records. _Parameters_ - _state_ `State`: State tree -- _kind_ `string`: Entity kind. -- _name_ `string`: Entity name. -- _query_ `EntityQuery< any >`: Optional terms query. +- _kind_ `K`: Entity kind. +- _name_ `N`: Entity name. +- _query_ Optional terms query. _Returns_ -- `Array< EntityRecord > | undefined`: Records. +- Records. ### getLastEntityDeleteError @@ -549,9 +549,9 @@ Returns the specified entity record's last delete error. _Parameters_ - _state_ `State`: State tree. -- _kind_ `string`: Entity kind. -- _name_ `string`: Entity name. -- _recordId_ `RecordKey`: Record ID. +- _kind_ `Kind`: Entity kind. +- _name_ `Name`: Entity name. +- _recordId_ `GenericRecordKey`: Record ID. _Returns_ @@ -564,9 +564,9 @@ Returns the specified entity record's last save error. _Parameters_ - _state_ `State`: State tree. -- _kind_ `string`: Entity kind. -- _name_ `string`: Entity name. -- _recordId_ `RecordKey`: Record ID. +- _kind_ `Kind`: Entity kind. +- _name_ `Name`: Entity name. +- _recordId_ `GenericRecordKey`: Record ID. _Returns_ @@ -580,9 +580,9 @@ with its attributes mapped to their raw values. _Parameters_ - _state_ `State`: State tree. -- _kind_ `string`: Entity kind. -- _name_ `string`: Entity name. -- _key_ `RecordKey`: Record's key. +- _kind_ `K`: Entity kind. +- _name_ `N`: Entity name. +- _key_ `KeyOf< K, N >`: Record's key. _Returns_ @@ -668,9 +668,9 @@ and false otherwise. _Parameters_ - _state_ `State`: State tree. -- _kind_ `string`: Entity kind. -- _name_ `string`: Entity name. -- _recordId_ `RecordKey`: Record ID. +- _kind_ `K`: Entity kind. +- _name_ `N`: Entity name. +- _recordId_ `KeyOf< K, N >`: Record ID. _Returns_ @@ -684,9 +684,9 @@ or false otherwise. _Parameters_ - _state_ `State`: State tree -- _kind_ `string`: Entity kind. -- _name_ `string`: Entity name. -- _query_ `EntityQuery< any >`: Optional terms query. +- _kind_ `K`: Entity kind. +- _name_ `N`: Entity name. +- _query_ `EntityQuery< C >`: Optional terms query. _Returns_ @@ -700,7 +700,7 @@ _Parameters_ - _state_ `State`: State tree. - _postType_ `string`: The type of the parent post. -- _postId_ `RecordKey`: The id of the parent post. +- _postId_ `GenericRecordKey`: The id of the parent post. _Returns_ @@ -739,9 +739,9 @@ Returns true if the specified entity record is autosaving, and false otherwise. _Parameters_ - _state_ `State`: State tree. -- _kind_ `string`: Entity kind. -- _name_ `string`: Entity name. -- _recordId_ `RecordKey`: Record ID. +- _kind_ `Kind`: Entity kind. +- _name_ `Name`: Entity name. +- _recordId_ `GenericRecordKey`: Record ID. _Returns_ @@ -754,9 +754,9 @@ Returns true if the specified entity record is deleting, and false otherwise. _Parameters_ - _state_ `State`: State tree. -- _kind_ `string`: Entity kind. -- _name_ `string`: Entity name. -- _recordId_ `RecordKey`: Record ID. +- _kind_ `Kind`: Entity kind. +- _name_ `Name`: Entity name. +- _recordId_ `GenericRecordKey`: Record ID. _Returns_ @@ -800,9 +800,9 @@ Returns true if the specified entity record is saving, and false otherwise. _Parameters_ - _state_ `State`: State tree. -- _kind_ `string`: Entity kind. -- _name_ `string`: Entity name. -- _recordId_ `RecordKey`: Record ID. +- _kind_ `K`: Entity kind. +- _name_ `N`: Entity name. +- _recordId_ `KeyOf< K, N >`: Record ID. _Returns_ diff --git a/packages/core-data/src/entity-types/index.ts b/packages/core-data/src/entity-types/index.ts index 0aca4e611814c3..ee5e52c6d85a2c 100644 --- a/packages/core-data/src/entity-types/index.ts +++ b/packages/core-data/src/entity-types/index.ts @@ -20,6 +20,7 @@ import type { Widget } from './widget'; import type { WidgetType } from './widget-type'; import type { WpTemplate } from './wp-template'; import type { WpTemplatePart } from './wp-template-part'; +import type { CoreEntities } from '../entities'; export type { EntityType } from './entities'; export type { BaseEntityRecords } from './base-entity-records'; @@ -47,24 +48,121 @@ export type { WpTemplatePart, }; -export type EntityRecord< C extends Context > = - | Attachment< C > - | Comment< C > - | MenuLocation< C > - | NavMenu< C > - | NavMenuItem< C > - | Page< C > - | Plugin< C > - | Post< C > - | Settings< C > - | Sidebar< C > - | Taxonomy< C > - | Theme< C > - | Type< C > - | User< C > - | Widget< C > - | WidgetType< C > - | WpTemplate< C > - | WpTemplatePart< C >; - export type UpdatableEntityRecord = Updatable< EntityRecord< 'edit' > >; + +/** + * An interface that may be extended to add types for new entities. Each entry + * must be a union of entity definitions adhering to the EntityInterface type. + * + * Example: + * + * ```ts + * import type { Context } from '@wordpress/core-data'; + * // ... + * + * interface Order { + * id: number; + * clientId: number; + * // ... + * } + * + * type OrderEntity = { + * kind: 'myPlugin'; + * name: 'order'; + * recordType: Order; + * } + * + * declare module '@wordpress/core-data' { + * export interface PerPackageEntities< C extends Context > { + * myPlugin: OrderEntity | ClientEntity + * } + * } + * + * const c = getEntityRecord( 'myPlugin', 'order', 15 ); + * // c is of the type Order + * ``` + */ +export interface PerPackageEntityConfig< C extends Context > { + core: CoreEntities< C >; +} + +/** + * A union of all the registered entities. + */ +type EntityConfig< + C extends Context = any +> = PerPackageEntityConfig< C >[ keyof PerPackageEntityConfig< C > ]; + +/** + * A union of all known record types. + */ +export type EntityRecord< + C extends Context = any +> = EntityConfig< C >[ 'record' ]; + +/** + * An entity corresponding to a specified record type. + */ +export type EntityConfigOf< + RecordOrKind extends EntityRecord | Kind, + N extends Name = undefined +> = RecordOrKind extends EntityRecord + ? Extract< EntityConfig, { record: RecordOrKind } > + : Extract< EntityConfig, { config: { kind: RecordOrKind; name: N } } >; + +/** + * Name of the requested entity. + */ +export type NameOf< + R extends EntityRecord +> = EntityConfigOf< R >[ 'config' ][ 'name' ]; + +/** + * Kind of the requested entity. + */ +export type KindOf< + R extends EntityRecord +> = EntityConfigOf< R >[ 'config' ][ 'kind' ]; + +/** + * Primary key type of the requested entity, sourced from PerPackageEntities. + * + * For core entities, the key type is computed using the entity configuration in entities.js. + */ +export type KeyOf< + RecordOrKind extends EntityRecord | Kind, + N extends Name = undefined, + E extends EntityConfig = EntityConfigOf< RecordOrKind, N > +> = ( E[ 'key' ] extends keyof E[ 'record' ] + ? E[ 'record' ][ E[ 'key' ] ] + : never ) & + ( number | string ); + +/** + * Default context of the requested entity, sourced from PerPackageEntities. + * + * For core entities, the default context is extracted from the entity configuration + * in entities.js. + */ +export type DefaultContextOf< + RecordOrKind extends EntityRecord | Kind, + N extends Name = undefined +> = EntityConfigOf< RecordOrKind, N >[ 'defaultContext' ]; + +/** + * An entity record type associated with specified kind and name, sourced from PerPackageEntities. + */ +export type EntityRecordOf< + K extends Kind, + N extends Name, + C extends Context = DefaultContextOf< K, N > +> = Extract< EntityConfig< C >, { config: { kind: K; name: N } } >[ 'record' ]; + +/** + * A union of all known entity kinds. + */ +export type Kind = EntityConfig[ 'config' ][ 'kind' ]; +/** + * A union of all known entity names. + */ +export type Name = EntityConfig[ 'config' ][ 'name' ]; diff --git a/packages/core-data/src/selectors.ts b/packages/core-data/src/selectors.ts index ee1b4df38416d5..c039f75aaf4217 100644 --- a/packages/core-data/src/selectors.ts +++ b/packages/core-data/src/selectors.ts @@ -18,7 +18,18 @@ import { STORE_NAME } from './name'; import { getQueriedItems } from './queried-data'; import { DEFAULT_ENTITY_KEY } from './entities'; import { getNormalizedCommaSeparable, isRawAttribute } from './utils'; -import type { Context, User, Theme, WpTemplate } from './entity-types'; +import type { + Context, + DefaultContextOf, + EntityRecordOf, + KeyOf, + Kind, + KindOf, + Name, + NameOf, + User, + WpTemplate, +} from './entity-types'; // This is an incomplete, high-level approximation of the State type. // It makes the selectors slightly more safe, but is intended to evolve @@ -29,7 +40,7 @@ interface State { blockPatterns: Array< unknown >; blockPatternCategories: Array< unknown >; currentGlobalStylesId: string; - currentTheme: Theme< 'edit' >; + currentTheme: string; currentUser: User< 'edit' >; embedPreviews: Record< string, { html: string } >; entities: EntitiesState; @@ -41,12 +52,17 @@ interface State { interface EntitiesState { config: EntityConfig[]; - records: Record< string, unknown >; + records: Record< Kind, Record< Name, EntityState< Kind, Name > > >; +} + +interface EntityState< K extends Kind, N extends Name > { + edits: Record< KeyOf< K, N >, Partial< EntityRecordOf< K, N > > >; + saving: Record< KeyOf< K, N >, { pending: boolean } >; } interface EntityConfig { - name: string; - kind: string; + name: Name; + kind: Kind; } interface UndoState extends Array< Object > { @@ -55,11 +71,11 @@ interface UndoState extends Array< Object > { } interface UserState { - queries: Record< string, RecordKey[] >; - byId: Record< RecordKey, User< 'edit' > >; + queries: Record< string, GenericRecordKey[] >; + byId: Record< GenericRecordKey, User< 'edit' > >; } -type RecordKey = number | string; +type GenericRecordKey = number | string; type EntityRecord = any; type Optional< T > = T | undefined; @@ -68,15 +84,18 @@ type Optional< T > = T | undefined; */ export type EntityQuery< C extends Context, - Fields extends string[] | undefined = undefined -> = Record< string, any > & { + WithFields extends boolean = true +> = Omit< Record< string, any >, '_fields' > & { context?: C; - /** - * The requested fields. If specified, the REST API will remove from the response - * any fields not on that list. - */ - _fields?: Fields; -}; +} & ( WithFields extends true + ? { + /** + * The requested fields. If specified, the REST API will remove from the response + * any fields not on that list. + */ + _fields: string[]; + } + : {} ); /** * Shared reference to an empty object for cases where it is important to avoid @@ -168,7 +187,7 @@ export const getUserQueryResults = createSelector( * * @return Array of entities with config matching kind. */ -export function getEntitiesByKind( state: State, kind: string ): Array< any > { +export function getEntitiesByKind( state: State, kind: Kind ): Array< any > { deprecated( "wp.data.select( 'core' ).getEntitiesByKind()", { since: '6.0', alternative: "wp.data.select( 'core' ).getEntitiesConfig()", @@ -184,7 +203,7 @@ export function getEntitiesByKind( state: State, kind: string ): Array< any > { * * @return Array of entities with config matching kind. */ -export function getEntitiesConfig( state: State, kind: string ): Array< any > { +export function getEntitiesConfig( state: State, kind: Kind ): Array< any > { return filter( state.entities.config, { kind } ); } @@ -198,7 +217,7 @@ export function getEntitiesConfig( state: State, kind: string ): Array< any > { * * @return Entity config */ -export function getEntity( state: State, kind: string, name: string ): any { +export function getEntity( state: State, kind: Kind, name: Name ): any { deprecated( "wp.data.select( 'core' ).getEntity()", { since: '6.0', alternative: "wp.data.select( 'core' ).getEntityConfig()", @@ -215,14 +234,56 @@ export function getEntity( state: State, kind: string, name: string ): any { * * @return Entity config */ -export function getEntityConfig( - state: State, - kind: string, - name: string -): any { +export function getEntityConfig( state: State, kind: Kind, name: Name ): any { return find( state.entities.config, { kind, name } ); } +/** + * GetEntityRecord is declared as an *interface*, but it actually describes + * the specifies the getEntityRecord *function* signature. It may seem unusual, + * but it's just how TypeScript implements function overloading. + * + * More accurately, GetEntityRecord distinguishes between two different signatures + * the getEntityRecord selector has: + * + * 1. When query._fields is not given, the returned type is EntityRecordOf< K, N, C > + * 2. When query._fields is given, the returned type is Partial> + * + * Unfortunately, due to a TypeScript limitation (https://github.com/microsoft/TypeScript/issues/23132) + * we can't use a single function signature with a return type such as: + * + * Fields extends undefined + * ? EntityRecordOf< K, N, C > + * : Partial< EntityRecordOf< K, N, C > > + */ +interface GetEntityRecord { + < + R extends EntityRecordOf< K, N >, + C extends Context = DefaultContextOf< R >, + K extends Kind = KindOf< R >, + N extends Name = NameOf< R > + >( + state: State, + kind: K, + name: N, + key: KeyOf< K, N >, + query: EntityQuery< C, true > + ): Partial< EntityRecordOf< K, N, C > > | null | undefined; + + < + R extends EntityRecordOf< K, N >, + C extends Context = DefaultContextOf< R >, + K extends Kind = KindOf< R >, + N extends Name = NameOf< R > + >( + state: State, + kind: K, + name: N, + key: KeyOf< K, N >, + query?: EntityQuery< C, false > + ): EntityRecordOf< K, N, C > | null | undefined; +} + /** * 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 @@ -236,14 +297,19 @@ export function getEntityConfig( * * @return Record. */ -export const getEntityRecord = createSelector( - ( +export const getEntityRecord: GetEntityRecord = createSelector( + < + R extends EntityRecordOf< K, N >, + C extends Context = DefaultContextOf< R >, + K extends Kind = KindOf< R >, + N extends Name = NameOf< R > + >( state: State, - kind: string, - name: string, - key: RecordKey, - query?: EntityQuery< any > - ): EntityRecord | undefined => { + kind: K, + name: N, + key: KeyOf< R >, + query + ) => { const queriedState = get( state.entities.records, [ kind, name, @@ -277,13 +343,7 @@ export const getEntityRecord = createSelector( return item; }, - ( - state: State, - kind: string, - name: string, - recordId: RecordKey, - query?: EntityQuery< any > - ) => { + ( state: State, kind, name, recordId, query ) => { const context = query?.context ?? 'default'; return [ get( state.entities.records, [ @@ -316,12 +376,10 @@ export const getEntityRecord = createSelector( * * @return Record. */ -export function __experimentalGetEntityRecordNoResolver( - state: State, - kind: string, - name: string, - key: RecordKey -): EntityRecord | null { +export function __experimentalGetEntityRecordNoResolver< + K extends Kind, + N extends Name +>( state: State, kind: K, name: N, key: KeyOf< K, N > ) { return getEntityRecord( state, kind, name, key ); } @@ -337,11 +395,11 @@ export function __experimentalGetEntityRecordNoResolver( * @return Object with the entity's raw attributes. */ export const getRawEntityRecord = createSelector( - ( + < K extends Kind, N extends Name >( state: State, - kind: string, - name: string, - key: RecordKey + kind: K, + name: N, + key: KeyOf< K, N > ): EntityRecord | undefined => { const record = getEntityRecord( state, kind, name, key ); return ( @@ -367,9 +425,9 @@ export const getRawEntityRecord = createSelector( }, ( state: State, - kind: string, - name: string, - recordId: RecordKey, + kind: Kind, + name: Name, + recordId: GenericRecordKey, query?: EntityQuery< any > ) => { const context = query?.context ?? 'default'; @@ -406,15 +464,59 @@ export const getRawEntityRecord = createSelector( * * @return Whether entity records have been received. */ -export function hasEntityRecords( - state: State, - kind: string, - name: string, - query?: EntityQuery< any > -): boolean { +export function hasEntityRecords< + R extends EntityRecordOf< K, N >, + C extends Context = DefaultContextOf< R >, + K extends Kind = KindOf< R >, + N extends Name = NameOf< R > +>( state: State, kind: K, name: N, query?: EntityQuery< C > ): boolean { return Array.isArray( getEntityRecords( state, kind, name, query ) ); } +/** + * GetEntityRecord is declared as an *interface*, but it actually describes + * the specifies the getEntityRecord *function* signature. It may seem unusual, + * but it's just how TypeScript implements function overloading. + * + * More accurately, GetEntityRecord distinguishes between two different signatures + * the getEntityRecord selector has: + * + * 1. When query._fields is not given, the returned type is EntityRecordOf< K, N, C >[] + * 2. When query._fields is given, the returned type is Partial>[] + * + * Unfortunately, due to a TypeScript limitation (https://github.com/microsoft/TypeScript/issues/23132) + * we can't use a single function signature with a return type such as: + * + * Fields extends undefined + * ? EntityRecordOf< K, N, C >[] + * : Partial< EntityRecordOf< K, N, C > >[] + */ +interface GetEntityRecords { + < + R extends EntityRecordOf< K, N >, + C extends Context = DefaultContextOf< R >, + K extends Kind = KindOf< R >, + N extends Name = NameOf< R > + >( + state: State, + kind: K, + name: N, + query: EntityQuery< C, true > + ): Partial< EntityRecordOf< K, N, C > >[] | null | undefined; + + < + R extends EntityRecordOf< K, N >, + C extends Context = DefaultContextOf< R >, + K extends Kind = KindOf< R >, + N extends Name = NameOf< R > + >( + state: State, + kind: K, + name: N, + query?: EntityQuery< C, false > + ): EntityRecordOf< K, N, C >[] | null | undefined; +} + /** * Returns the Entity's records. * @@ -425,12 +527,17 @@ export function hasEntityRecords( * * @return Records. */ -export function getEntityRecords( +export const getEntityRecords: GetEntityRecords = < + R extends EntityRecordOf< K, N >, + C extends Context = DefaultContextOf< R >, + K extends Kind = KindOf< R >, + N extends Name = NameOf< R > +>( state: State, - kind: string, - name: string, - query?: EntityQuery< any > -): Array< EntityRecord > | undefined { + kind: K, + name: N, + query +) => { // Queried data state is prepopulated for all known entities. If this is not // assigned for the given parameters, then it is known to not exist. const queriedState = get( state.entities.records, [ @@ -442,13 +549,13 @@ export function getEntityRecords( return null; } return getQueriedItems( queriedState, query ); -} +}; type DirtyEntityRecord = { title: string; - key: RecordKey; - name: string; - kind: string; + key: GenericRecordKey; + name: Name; + kind: Kind; }; /** * Returns the list of dirty entity records. @@ -463,43 +570,64 @@ export const __experimentalGetDirtyEntityRecords = createSelector( entities: { records }, } = state; const dirtyRecords = []; - Object.keys( records ).forEach( ( kind ) => { - Object.keys( records[ kind ] ).forEach( ( name ) => { - const primaryKeys = Object.keys( - records[ kind ][ name ].edits - ).filter( - ( primaryKey: RecordKey ) => - // The entity record must exist (not be deleted), - // and it must have edits. - getEntityRecord( state, kind, name, primaryKey ) && - hasEditsForEntityRecord( state, kind, name, primaryKey ) - ); - - if ( primaryKeys.length ) { - const entityConfig = getEntityConfig( state, kind, name ); - primaryKeys.forEach( ( primaryKey ) => { - const entityRecord = getEditedEntityRecord( - state, - kind, - name, - primaryKey + ( Object.keys( records ) as Kind[] ).forEach( + < K extends Kind >( kind: K ) => { + ( Object.keys( records[ kind ] ) as Name[] ).forEach( + < N extends Name >( name: N ) => { + const primaryKeys = ( Object.keys( + records[ kind ][ name ].edits + ) as KeyOf< K, N >[] ).filter( + ( primaryKey ) => + // The entity record must exist (not be deleted), + // and it must have edits. + getEntityRecord( + state, + kind, + name, + primaryKey + ) && + hasEditsForEntityRecord( + state, + kind, + name, + primaryKey + ) ); - dirtyRecords.push( { - // We avoid using primaryKey because it's transformed into a string - // when it's used as an object key. - key: - entityRecord[ - entityConfig.key || DEFAULT_ENTITY_KEY - ], - title: - entityConfig?.getTitle?.( entityRecord ) || '', - name, - kind, - } ); - } ); - } - } ); - } ); + + if ( primaryKeys.length ) { + const entityConfig = getEntityConfig( + state, + kind, + name + ); + primaryKeys.forEach( ( primaryKey ) => { + const entityRecord = getEditedEntityRecord( + state, + kind, + name, + primaryKey + ); + dirtyRecords.push( { + // We avoid using primaryKey because it's transformed into a string + // when it's used as an object key. + key: + entityRecord[ + entityConfig.key || + DEFAULT_ENTITY_KEY + ], + title: + entityConfig?.getTitle?.( + entityRecord + ) || '', + name, + kind, + } ); + } ); + } + } + ); + } + ); return dirtyRecords; }, @@ -519,39 +647,55 @@ export const __experimentalGetEntitiesBeingSaved = createSelector( entities: { records }, } = state; const recordsBeingSaved = []; - Object.keys( records ).forEach( ( kind ) => { - Object.keys( records[ kind ] ).forEach( ( name ) => { - const primaryKeys = Object.keys( - records[ kind ][ name ].saving - ).filter( ( primaryKey ) => - isSavingEntityRecord( state, kind, name, primaryKey ) - ); - - if ( primaryKeys.length ) { - const entityConfig = getEntityConfig( state, kind, name ); - primaryKeys.forEach( ( primaryKey ) => { - const entityRecord = getEditedEntityRecord( - state, - kind, - name, - primaryKey + ( Object.keys( records ) as Kind[] ).forEach( + < K extends Kind >( kind: K ) => { + ( Object.keys( records[ kind ] ) as Name[] ).forEach( + < N extends Name >( name: N ) => { + const primaryKeys = ( Object.keys( + records[ kind ][ name ].saving + ) as KeyOf< K, N >[] ).filter( ( primaryKey ) => + isSavingEntityRecord( + state, + kind, + name, + primaryKey + ) ); - recordsBeingSaved.push( { - // We avoid using primaryKey because it's transformed into a string - // when it's used as an object key. - key: - entityRecord[ - entityConfig.key || DEFAULT_ENTITY_KEY - ], - title: - entityConfig?.getTitle?.( entityRecord ) || '', - name, - kind, - } ); - } ); - } - } ); - } ); + + if ( primaryKeys.length ) { + const entityConfig = getEntityConfig( + state, + kind, + name + ); + primaryKeys.forEach( ( primaryKey ) => { + const entityRecord = getEditedEntityRecord( + state, + kind, + name, + primaryKey + ); + recordsBeingSaved.push( { + // We avoid using primaryKey because it's transformed into a string + // when it's used as an object key. + key: + entityRecord[ + entityConfig.key || + DEFAULT_ENTITY_KEY + ], + title: + entityConfig?.getTitle?.( + entityRecord + ) || '', + name, + kind, + } ); + } ); + } + } + ); + } + ); return recordsBeingSaved; }, ( state ) => [ state.entities.records ] @@ -567,13 +711,18 @@ export const __experimentalGetEntitiesBeingSaved = createSelector( * * @return The entity record's edits. */ -export function getEntityRecordEdits( +export function getEntityRecordEdits< K extends Kind, N extends Name >( state: State, - kind: string, - name: string, - recordId: RecordKey + kind: K, + name: N, + recordId: KeyOf< K, N > ): Optional< any > { - return get( state.entities.records, [ kind, name, 'edits', recordId ] ); + return get( state.entities.records, [ + kind, + name, + 'edits', + recordId as string | number, + ] ); } /** @@ -591,11 +740,11 @@ export function getEntityRecordEdits( * @return The entity record's non transient edits. */ export const getEntityRecordNonTransientEdits = createSelector( - ( + < K extends Kind, N extends Name >( state: State, - kind: string, - name: string, - recordId: RecordKey + kind: K, + name: N, + recordId: KeyOf< K, N > ): Optional< any > => { const { transientEdits } = getEntityConfig( state, kind, name ) || {}; const edits = getEntityRecordEdits( state, kind, name, recordId ) || {}; @@ -609,7 +758,7 @@ export const getEntityRecordNonTransientEdits = createSelector( return acc; }, {} ); }, - ( state: State, kind: string, name: string, recordId: RecordKey ) => [ + ( state: State, kind: Kind, name: Name, recordId: GenericRecordKey ) => [ state.entities.config, get( state.entities.records, [ kind, name, 'edits', recordId ] ), ] @@ -626,11 +775,11 @@ export const getEntityRecordNonTransientEdits = createSelector( * * @return Whether the entity record has edits or not. */ -export function hasEditsForEntityRecord( +export function hasEditsForEntityRecord< K extends Kind, N extends Name >( state: State, - kind: string, - name: string, - recordId: RecordKey + kind: K, + name: N, + recordId: KeyOf< K, N > ): boolean { return ( isSavingEntityRecord( state, kind, name, recordId ) || @@ -651,20 +800,20 @@ export function hasEditsForEntityRecord( * @return The entity record, merged with its edits. */ export const getEditedEntityRecord = createSelector( - ( + < K extends Kind, N extends Name >( state: State, - kind: string, - name: string, - recordId: RecordKey + kind: K, + name: N, + recordId: KeyOf< K, N > ): EntityRecord | undefined => ( { ...getRawEntityRecord( state, kind, name, recordId ), ...getEntityRecordEdits( state, kind, name, recordId ), } ), ( state: State, - kind: string, - name: string, - recordId: RecordKey, + kind: Kind, + name: Name, + recordId: GenericRecordKey, query?: EntityQuery< any > ) => { const context = query?.context ?? 'default'; @@ -703,9 +852,9 @@ export const getEditedEntityRecord = createSelector( */ export function isAutosavingEntityRecord( state: State, - kind: string, - name: string, - recordId: RecordKey + kind: Kind, + name: Name, + recordId: GenericRecordKey ): boolean { const { pending, isAutosave } = get( state.entities.records, @@ -725,15 +874,15 @@ export function isAutosavingEntityRecord( * * @return Whether the entity record is saving or not. */ -export function isSavingEntityRecord( +export function isSavingEntityRecord< K extends Kind, N extends Name >( state: State, - kind: string, - name: string, - recordId: RecordKey + kind: K, + name: N, + recordId: KeyOf< K, N > ): boolean { return get( state.entities.records, - [ kind, name, 'saving', recordId, 'pending' ], + [ kind, name, 'saving', recordId as GenericRecordKey, 'pending' ], false ); } @@ -750,9 +899,9 @@ export function isSavingEntityRecord( */ export function isDeletingEntityRecord( state: State, - kind: string, - name: string, - recordId: RecordKey + kind: Kind, + name: Name, + recordId: GenericRecordKey ): boolean { return get( state.entities.records, @@ -773,9 +922,9 @@ export function isDeletingEntityRecord( */ export function getLastEntitySaveError( state: State, - kind: string, - name: string, - recordId: RecordKey + kind: Kind, + name: Name, + recordId: GenericRecordKey ): any { return get( state.entities.records, [ kind, @@ -798,9 +947,9 @@ export function getLastEntitySaveError( */ export function getLastEntityDeleteError( state: State, - kind: string, - name: string, - recordId: RecordKey + kind: Kind, + name: Name, + recordId: GenericRecordKey ): any { return get( state.entities.records, [ kind, @@ -961,7 +1110,7 @@ export function canUser( state: State, action: string, resource: string, - id?: RecordKey + id?: GenericRecordKey ): boolean | undefined { const key = compact( [ action, resource, id ] ).join( '/' ); return get( state, [ 'userPermissions', key ] ); @@ -984,9 +1133,9 @@ export function canUser( */ export function canUserEditEntityRecord( state: State, - kind: string, - name: string, - recordId: RecordKey + kind: Kind, + name: Name, + recordId: GenericRecordKey ): boolean | undefined { const entityConfig = getEntityConfig( state, kind, name ); if ( ! entityConfig ) { @@ -1012,7 +1161,7 @@ export function canUserEditEntityRecord( export function getAutosaves( state: State, postType: string, - postId: RecordKey + postId: GenericRecordKey ): Array< any > | undefined { return state.autosaves[ postId ]; } @@ -1030,8 +1179,8 @@ export function getAutosaves( export function getAutosave( state: State, postType: string, - postId: RecordKey, - authorId: RecordKey + postId: GenericRecordKey, + authorId: GenericRecordKey ): EntityRecord | undefined { if ( authorId === undefined ) { return; @@ -1054,7 +1203,7 @@ export const hasFetchedAutosaves = createRegistrySelector( ( select ) => ( state: State, postType: string, - postId: RecordKey + postId: GenericRecordKey ): boolean => { return select( STORE_NAME ).hasFinishedResolution( 'getAutosaves', [ postType,