From 78c8bcef387957f45dc46c91da454ff0b5b94e24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 23 May 2022 13:38:23 +0200 Subject: [PATCH 1/9] Exhaustive type for getEntityRecord --- packages/core-data/src/entity-types/index.ts | 138 ++++++++++++++++--- packages/core-data/src/rememo.d.ts | 3 + packages/core-data/src/selectors.ts | 61 ++++++-- 3 files changed, 174 insertions(+), 28 deletions(-) create mode 100644 packages/core-data/src/rememo.d.ts diff --git a/packages/core-data/src/entity-types/index.ts b/packages/core-data/src/entity-types/index.ts index 0aca4e611814c3..1b5094f1a7cb48 100644 --- a/packages/core-data/src/entity-types/index.ts +++ b/packages/core-data/src/entity-types/index.ts @@ -20,6 +20,8 @@ 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 { EntityQuery, State } from '../selectors'; +import type { CoreEntities } from '../entities'; export type { EntityType } from './entities'; export type { BaseEntityRecords } from './base-entity-records'; @@ -47,24 +49,120 @@ 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; + +/** + * 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/rememo.d.ts b/packages/core-data/src/rememo.d.ts new file mode 100644 index 00000000000000..3b2b8bb5112d1c --- /dev/null +++ b/packages/core-data/src/rememo.d.ts @@ -0,0 +1,3 @@ +declare module 'rememo' { + export default function createSelector< T extends Function >( fn: T, ...any ) : T; +} diff --git a/packages/core-data/src/selectors.ts b/packages/core-data/src/selectors.ts index ee1b4df38416d5..f02a0a8fe336ea 100644 --- a/packages/core-data/src/selectors.ts +++ b/packages/core-data/src/selectors.ts @@ -19,6 +19,19 @@ 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 { + DefaultContextOf, + EntityRecordOf, + KeyOf, + Kind, + KindOf, + Name, + NameOf, +} from './entity-types'; + +// createSelector isn't properly typed if I don't explicitly import these files – ideally they would +// be merely ambient definitions that TS is aware of. +import type {} from './rememo'; // This is an incomplete, high-level approximation of the State type. // It makes the selectors slightly more safe, but is intended to evolve @@ -237,13 +250,28 @@ export function getEntityConfig( * @return Record. */ export const getEntityRecord = createSelector( - ( + function < + R extends EntityRecordOf< K, N >, + C extends Context = DefaultContextOf< R >, + K extends Kind = KindOf< R >, + N extends Name = NameOf< R >, + /** + * The requested fields. If specified, the REST API will remove from the response + * any fields not on that list. + */ + Fields extends undefined | string[] = undefined + >( state: State, - kind: string, - name: string, - key: RecordKey, - query?: EntityQuery< any > - ): EntityRecord | undefined => { + kind: K, + name: N, + key: KeyOf< R >, + query?: EntityQuery< C, Fields > + ): + | ( Fields extends undefined + ? EntityRecordOf< K, N, C > + : Partial< EntityRecordOf< K, N, C > > ) + | null + | undefined { const queriedState = get( state.entities.records, [ kind, name, @@ -265,14 +293,31 @@ export const getEntityRecord = createSelector( const item = queriedState.items[ context ]?.[ key ]; if ( item && query._fields ) { - const filteredItem = {}; + const filteredItem = {} as Partial< EntityRecordOf< K, N, C > >; const fields = getNormalizedCommaSeparable( query._fields ) ?? []; for ( let f = 0; f < fields.length; f++ ) { const field = fields[ f ].split( '.' ); const value = get( item, field ); set( filteredItem, field, value ); } - return filteredItem; + /** + * TypeScript limitation: + * + * Partial< EntityRecordOf< K, N, C > > + * + * Is not assignable to: + * + * Fields extends undefined + * ? EntityRecordOf< K, N, C > + * : Partial< EntityRecordOf< K, N, C > > + * + * At this point, even though TypeScript knows that + * Fields extends undefined. Unfortunately, this forces + * us to use `as any`. + * + * For more details, visit https://github.com/microsoft/TypeScript/issues/23132 + */ + return filteredItem as any; } return item; From d2d830e0329e9efdd160ebf349201e704eb3b18c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Thu, 26 May 2022 17:20:32 +0200 Subject: [PATCH 2/9] Type signatures for getEntityRecord and getEntityRecords --- packages/core-data/src/entity-types/index.ts | 6 +- packages/core-data/src/rememo.d.ts | 3 - packages/core-data/src/selectors.ts | 584 +++++++++++-------- packages/core-data/src/typed-rememo.ts | 27 + 4 files changed, 374 insertions(+), 246 deletions(-) delete mode 100644 packages/core-data/src/rememo.d.ts create mode 100644 packages/core-data/src/typed-rememo.ts diff --git a/packages/core-data/src/entity-types/index.ts b/packages/core-data/src/entity-types/index.ts index 1b5094f1a7cb48..ee5e52c6d85a2c 100644 --- a/packages/core-data/src/entity-types/index.ts +++ b/packages/core-data/src/entity-types/index.ts @@ -20,7 +20,6 @@ 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 { EntityQuery, State } from '../selectors'; import type { CoreEntities } from '../entities'; export type { EntityType } from './entities'; @@ -134,9 +133,10 @@ export type KeyOf< RecordOrKind extends EntityRecord | Kind, N extends Name = undefined, E extends EntityConfig = EntityConfigOf< RecordOrKind, N > -> = E[ 'key' ] extends keyof E[ 'record' ] +> = ( E[ 'key' ] extends keyof E[ 'record' ] ? E[ 'record' ][ E[ 'key' ] ] - : never; + : never ) & + ( number | string ); /** * Default context of the requested entity, sourced from PerPackageEntities. diff --git a/packages/core-data/src/rememo.d.ts b/packages/core-data/src/rememo.d.ts deleted file mode 100644 index 3b2b8bb5112d1c..00000000000000 --- a/packages/core-data/src/rememo.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -declare module 'rememo' { - export default function createSelector< T extends Function >( fn: T, ...any ) : T; -} diff --git a/packages/core-data/src/selectors.ts b/packages/core-data/src/selectors.ts index f02a0a8fe336ea..7e72f217dca057 100644 --- a/packages/core-data/src/selectors.ts +++ b/packages/core-data/src/selectors.ts @@ -1,7 +1,6 @@ /** * External dependencies */ -import createSelector from 'rememo'; import { set, map, find, get, filter, compact } from 'lodash'; /** @@ -18,7 +17,7 @@ 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, User, WpTemplate } from './entity-types'; import { DefaultContextOf, EntityRecordOf, @@ -28,10 +27,7 @@ import { Name, NameOf, } from './entity-types'; - -// createSelector isn't properly typed if I don't explicitly import these files – ideally they would -// be merely ambient definitions that TS is aware of. -import type {} from './rememo'; +import createSelector from './typed-rememo'; // This is an incomplete, high-level approximation of the State type. // It makes the selectors slightly more safe, but is intended to evolve @@ -42,7 +38,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; @@ -54,12 +50,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 > { @@ -68,27 +69,24 @@ 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; /** * HTTP Query parameters sent with the API request to fetch the entity records. */ -export type EntityQuery< - C extends Context, - Fields extends string[] | undefined = undefined -> = Record< string, any > & { +export type EntityQuery< C extends Context > = Record< string, any > & { context?: C; /** * The requested fields. If specified, the REST API will remove from the response * any fields not on that list. */ - _fields?: Fields; + _fields: string[] | undefined; }; /** @@ -181,7 +179,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()", @@ -197,7 +195,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 } ); } @@ -211,7 +209,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()", @@ -228,105 +226,122 @@ 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 } ); } /** - * 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. + * 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. * - * @param state State tree - * @param kind Entity kind. - * @param name Entity name. - * @param key Record's key - * @param query Optional query. + * More accurately, GetEntityRecord distinguishes between two different signatures + * the getEntityRecord selector has: * - * @return Record. + * 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 > > */ -export const getEntityRecord = createSelector( - function < +interface GetEntityRecord { + < R extends EntityRecordOf< K, N >, C extends Context = DefaultContextOf< R >, K extends Kind = KindOf< R >, - N extends Name = NameOf< R >, - /** - * The requested fields. If specified, the REST API will remove from the response - * any fields not on that list. - */ - Fields extends undefined | string[] = undefined + N extends Name = NameOf< R > >( state: State, kind: K, name: N, - key: KeyOf< R >, - query?: EntityQuery< C, Fields > - ): - | ( Fields extends undefined - ? EntityRecordOf< K, N, C > - : Partial< EntityRecordOf< K, N, C > > ) - | null - | undefined { - const queriedState = get( state.entities.records, [ - kind, - name, - 'queriedData', - ] ); - if ( ! queriedState ) { + key: KeyOf< K, N >, + query: EntityQuery< C > + ): 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?: Omit< EntityQuery< C >, '_fields' > + ): EntityRecordOf< K, N, C > | null | undefined; +} + +const getEntityRecordImplementation: 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< R >, + query?: EntityQuery< C > +) => { + const queriedState = get( state.entities.records, [ + kind, + name, + 'queriedData', + ] ); + if ( ! queriedState ) { + return undefined; + } + const context = query?.context ?? 'default'; + + if ( query === undefined ) { + // If expecting a complete item, validate that completeness. + if ( ! queriedState.itemIsComplete[ context ]?.[ key ] ) { return undefined; } - const context = query?.context ?? 'default'; - if ( query === undefined ) { - // If expecting a complete item, validate that completeness. - if ( ! queriedState.itemIsComplete[ context ]?.[ key ] ) { - return undefined; - } + return queriedState.items[ context ][ key ]; + } - return queriedState.items[ context ][ key ]; + 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( '.' ); + const value = get( item, field ); + set( filteredItem, field, value ); } + return filteredItem; + } - const item = queriedState.items[ context ]?.[ key ]; - if ( item && query._fields ) { - const filteredItem = {} as Partial< EntityRecordOf< K, N, C > >; - const fields = getNormalizedCommaSeparable( query._fields ) ?? []; - for ( let f = 0; f < fields.length; f++ ) { - const field = fields[ f ].split( '.' ); - const value = get( item, field ); - set( filteredItem, field, value ); - } - /** - * TypeScript limitation: - * - * Partial< EntityRecordOf< K, N, C > > - * - * Is not assignable to: - * - * Fields extends undefined - * ? EntityRecordOf< K, N, C > - * : Partial< EntityRecordOf< K, N, C > > - * - * At this point, even though TypeScript knows that - * Fields extends undefined. Unfortunately, this forces - * us to use `as any`. - * - * For more details, visit https://github.com/microsoft/TypeScript/issues/23132 - */ - return filteredItem as any; - } + return item; +}; - return item; - }, - ( +/** + * 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. + * + * @param state State tree + * @param kind Entity kind. + * @param name Entity name. + * @param key Record's key + * @param query Optional query. + * + * @return Record. + */ +export const getEntityRecord = createSelector( + getEntityRecordImplementation, + < K extends Kind, N extends Name >( state: State, - kind: string, - name: string, - recordId: RecordKey, + kind: K, + name: N, + recordId: KeyOf< K, N >, query?: EntityQuery< any > ) => { const context = query?.context ?? 'default'; @@ -361,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 ); } @@ -382,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 ( @@ -412,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'; @@ -451,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 > & { _fields: string[] } + ): 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?: Omit< EntityQuery< C >, '_fields' > + ): EntityRecordOf< K, N, C >[] | null | undefined; +} + /** * Returns the Entity's records. * @@ -470,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?: EntityQuery< C > +) => { // 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, [ @@ -487,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. @@ -508,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 any[] ).forEach( + < K extends Kind >( kind: K ) => { + ( Object.keys( records[ kind ] ) as any[] ).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; }, @@ -564,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 any[] ).forEach( + < K extends Kind >( kind: K ) => { + ( Object.keys( records[ kind ] ) as any[] ).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 ] @@ -612,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, + ] ); } /** @@ -636,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 ) || {}; @@ -654,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 ] ), ] @@ -671,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 ) || @@ -696,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'; @@ -748,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, @@ -770,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 ); } @@ -795,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, @@ -818,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, @@ -843,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, @@ -1006,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 ] ); @@ -1029,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 ) { @@ -1057,7 +1161,7 @@ export function canUserEditEntityRecord( export function getAutosaves( state: State, postType: string, - postId: RecordKey + postId: GenericRecordKey ): Array< any > | undefined { return state.autosaves[ postId ]; } @@ -1075,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; @@ -1099,7 +1203,7 @@ export const hasFetchedAutosaves = createRegistrySelector( ( select ) => ( state: State, postType: string, - postId: RecordKey + postId: GenericRecordKey ): boolean => { return select( STORE_NAME ).hasFinishedResolution( 'getAutosaves', [ postType, diff --git a/packages/core-data/src/typed-rememo.ts b/packages/core-data/src/typed-rememo.ts new file mode 100644 index 00000000000000..2c4fa0fe2b4396 --- /dev/null +++ b/packages/core-data/src/typed-rememo.ts @@ -0,0 +1,27 @@ +/** + * External dependencies + */ +import originalCreateSelector from 'rememo'; + +/** + * The same as the original rememo createSelector, only with a more complete + * TypeScript signature. See the original documentation below: + * + * Returns a memoized selector function. The getDependants function argument is + * called before the memoized selector and is expected to return an immutable + * reference or array of references on which the selector depends for computing + * its own return value. The memoize cache is preserved only as long as those + * dependant references remain the same. If getDependants returns a different + * reference(s), the cache is cleared and the selector value regenerated. + * + * @param selector Selector function. + * @param getDependants Dependant getter returning an array of + * references used in cache bust consideration. + * @return Memoized selector function. + */ +export default function createSelector< T extends ( ...args: any[] ) => any >( + selector: T, + getDependants: ( ...args: any[] ) => any +): T { + return originalCreateSelector( selector, getDependants ) as T; +} From 80fb58264b557c79167fc6ac367caa8dedae6599 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Thu, 26 May 2022 17:23:06 +0200 Subject: [PATCH 3/9] Inline the implementation of getEntityRecordImplementation --- packages/core-data/src/selectors.ts | 92 ++++++++++++++--------------- 1 file changed, 45 insertions(+), 47 deletions(-) diff --git a/packages/core-data/src/selectors.ts b/packages/core-data/src/selectors.ts index 7e72f217dca057..5c44728b10faae 100644 --- a/packages/core-data/src/selectors.ts +++ b/packages/core-data/src/selectors.ts @@ -276,52 +276,6 @@ interface GetEntityRecord { ): EntityRecordOf< K, N, C > | null | undefined; } -const getEntityRecordImplementation: 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< R >, - query?: EntityQuery< C > -) => { - const queriedState = get( state.entities.records, [ - kind, - name, - 'queriedData', - ] ); - if ( ! queriedState ) { - return undefined; - } - const context = query?.context ?? 'default'; - - if ( query === undefined ) { - // If expecting a complete item, validate that completeness. - if ( ! queriedState.itemIsComplete[ context ]?.[ key ] ) { - return undefined; - } - - return queriedState.items[ context ][ key ]; - } - - 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( '.' ); - const value = get( item, field ); - set( filteredItem, field, value ); - } - return filteredItem; - } - - return item; -}; - /** * 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 @@ -336,7 +290,51 @@ const getEntityRecordImplementation: GetEntityRecord = < * @return Record. */ export const getEntityRecord = createSelector( - getEntityRecordImplementation, + ( < + 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< R >, + query?: EntityQuery< C > + ) => { + const queriedState = get( state.entities.records, [ + kind, + name, + 'queriedData', + ] ); + if ( ! queriedState ) { + return undefined; + } + const context = query?.context ?? 'default'; + + if ( query === undefined ) { + // If expecting a complete item, validate that completeness. + if ( ! queriedState.itemIsComplete[ context ]?.[ key ] ) { + return undefined; + } + + return queriedState.items[ context ][ key ]; + } + + 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( '.' ); + const value = get( item, field ); + set( filteredItem, field, value ); + } + return filteredItem; + } + + return item; + } ) as GetEntityRecord, < K extends Kind, N extends Name >( state: State, kind: K, From dce539708e57d6785a5e23633d887695715a6bff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Thu, 26 May 2022 17:50:08 +0200 Subject: [PATCH 4/9] Explain the need for typed-rememo.ts --- packages/core-data/src/typed-rememo.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/core-data/src/typed-rememo.ts b/packages/core-data/src/typed-rememo.ts index 2c4fa0fe2b4396..2f11f41fdf4d9d 100644 --- a/packages/core-data/src/typed-rememo.ts +++ b/packages/core-data/src/typed-rememo.ts @@ -5,7 +5,11 @@ import originalCreateSelector from 'rememo'; /** * The same as the original rememo createSelector, only with a more complete - * TypeScript signature. See the original documentation below: + * TypeScript signature. A fix has been proposed in the following pull request: + * https://github.com/aduth/rememo/pull/7 + * Once it's merged, this file can be safely removed. + * + * And here's the original documentation: * * Returns a memoized selector function. The getDependants function argument is * called before the memoized selector and is expected to return an immutable From fc343d4b4b9c90da0860987ee63166ea45bf8c04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Thu, 2 Jun 2022 14:04:25 +0100 Subject: [PATCH 5/9] Remove typed-rememo.ts --- packages/core-data/src/selectors.ts | 2 +- packages/core-data/src/typed-rememo.ts | 31 -------------------------- 2 files changed, 1 insertion(+), 32 deletions(-) delete mode 100644 packages/core-data/src/typed-rememo.ts diff --git a/packages/core-data/src/selectors.ts b/packages/core-data/src/selectors.ts index 5c44728b10faae..52f31a8d2a73d7 100644 --- a/packages/core-data/src/selectors.ts +++ b/packages/core-data/src/selectors.ts @@ -1,6 +1,7 @@ /** * External dependencies */ +import createSelector from 'rememo'; import { set, map, find, get, filter, compact } from 'lodash'; /** @@ -27,7 +28,6 @@ import { Name, NameOf, } from './entity-types'; -import createSelector from './typed-rememo'; // This is an incomplete, high-level approximation of the State type. // It makes the selectors slightly more safe, but is intended to evolve diff --git a/packages/core-data/src/typed-rememo.ts b/packages/core-data/src/typed-rememo.ts deleted file mode 100644 index 2f11f41fdf4d9d..00000000000000 --- a/packages/core-data/src/typed-rememo.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * External dependencies - */ -import originalCreateSelector from 'rememo'; - -/** - * The same as the original rememo createSelector, only with a more complete - * TypeScript signature. A fix has been proposed in the following pull request: - * https://github.com/aduth/rememo/pull/7 - * Once it's merged, this file can be safely removed. - * - * And here's the original documentation: - * - * Returns a memoized selector function. The getDependants function argument is - * called before the memoized selector and is expected to return an immutable - * reference or array of references on which the selector depends for computing - * its own return value. The memoize cache is preserved only as long as those - * dependant references remain the same. If getDependants returns a different - * reference(s), the cache is cleared and the selector value regenerated. - * - * @param selector Selector function. - * @param getDependants Dependant getter returning an array of - * references used in cache bust consideration. - * @return Memoized selector function. - */ -export default function createSelector< T extends ( ...args: any[] ) => any >( - selector: T, - getDependants: ( ...args: any[] ) => any -): T { - return originalCreateSelector( selector, getDependants ) as T; -} From eb36773277f4f88baa0df00d0ca4e1a1f6ab5925 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 3 Jun 2022 00:20:43 +0100 Subject: [PATCH 6/9] Use correct type inference for Context in getEntityRecords by removing the use of Omit --- docs/reference-guides/data/data-core.md | 112 ++++++++++++------------ packages/core-data/README.md | 112 ++++++++++++------------ packages/core-data/src/selectors.ts | 52 ++++++----- 3 files changed, 141 insertions(+), 135 deletions(-) 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/selectors.ts b/packages/core-data/src/selectors.ts index 52f31a8d2a73d7..af0c0b46b725e0 100644 --- a/packages/core-data/src/selectors.ts +++ b/packages/core-data/src/selectors.ts @@ -28,6 +28,7 @@ import { Name, NameOf, } from './entity-types'; +import type { OmitNevers } from './entity-types/helpers'; // This is an incomplete, high-level approximation of the State type. // It makes the selectors slightly more safe, but is intended to evolve @@ -80,14 +81,20 @@ type Optional< T > = T | undefined; /** * HTTP Query parameters sent with the API request to fetch the entity records. */ -export type EntityQuery< C extends Context > = Record< string, any > & { +export type EntityQuery< + C extends Context, + 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: string[] | undefined; -}; +} & ( 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 @@ -259,7 +266,7 @@ interface GetEntityRecord { kind: K, name: N, key: KeyOf< K, N >, - query: EntityQuery< C > + query: EntityQuery< C, true > ): Partial< EntityRecordOf< K, N, C > > | null | undefined; < @@ -272,7 +279,7 @@ interface GetEntityRecord { kind: K, name: N, key: KeyOf< K, N >, - query?: Omit< EntityQuery< C >, '_fields' > + query?: EntityQuery< C, false > ): EntityRecordOf< K, N, C > | null | undefined; } @@ -289,8 +296,8 @@ interface GetEntityRecord { * * @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 >, @@ -300,7 +307,7 @@ export const getEntityRecord = createSelector( kind: K, name: N, key: KeyOf< R >, - query?: EntityQuery< C > + query ) => { const queriedState = get( state.entities.records, [ kind, @@ -334,14 +341,8 @@ export const getEntityRecord = createSelector( } return item; - } ) as GetEntityRecord, - < K extends Kind, N extends Name >( - state: State, - kind: K, - name: N, - recordId: KeyOf< K, N >, - query?: EntityQuery< any > - ) => { + }, + ( state: State, kind, name, recordId, query ) => { const context = query?.context ?? 'default'; return [ get( state.entities.records, [ @@ -363,7 +364,12 @@ export const getEntityRecord = createSelector( ]; } ); +const commentDefault = getEntityRecord( {} as State, 'root', 'comment', 15 ); +// commentDefault is Comment<'edit'> +const commentView = getEntityRecord( {} as State, 'root', 'comment', 15, { + context: 'view', +} ); /** * Returns the Entity's record object by key. Doesn't trigger a resolver nor requests the entity records from the API if the entity record isn't available in the local state. * @@ -499,7 +505,7 @@ interface GetEntityRecords { state: State, kind: K, name: N, - query: EntityQuery< C > & { _fields: string[] } + query: EntityQuery< C, true > ): Partial< EntityRecordOf< K, N, C > >[] | null | undefined; < @@ -511,7 +517,7 @@ interface GetEntityRecords { state: State, kind: K, name: N, - query?: Omit< EntityQuery< C >, '_fields' > + query?: EntityQuery< C, false > ): EntityRecordOf< K, N, C >[] | null | undefined; } @@ -534,7 +540,7 @@ export const getEntityRecords: GetEntityRecords = < state: State, kind: K, name: N, - query?: EntityQuery< C > + 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. From 40c330edafc9a8899044e2977ab446964ed56caf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 3 Jun 2022 00:22:47 +0100 Subject: [PATCH 7/9] Remove as any with as Kind[] and as Name[] --- packages/core-data/src/selectors.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core-data/src/selectors.ts b/packages/core-data/src/selectors.ts index af0c0b46b725e0..3cf2caafe0f104 100644 --- a/packages/core-data/src/selectors.ts +++ b/packages/core-data/src/selectors.ts @@ -574,9 +574,9 @@ export const __experimentalGetDirtyEntityRecords = createSelector( entities: { records }, } = state; const dirtyRecords = []; - ( Object.keys( records ) as any[] ).forEach( + ( Object.keys( records ) as Kind[] ).forEach( < K extends Kind >( kind: K ) => { - ( Object.keys( records[ kind ] ) as any[] ).forEach( + ( Object.keys( records[ kind ] ) as Name[] ).forEach( < N extends Name >( name: N ) => { const primaryKeys = ( Object.keys( records[ kind ][ name ].edits @@ -651,9 +651,9 @@ export const __experimentalGetEntitiesBeingSaved = createSelector( entities: { records }, } = state; const recordsBeingSaved = []; - ( Object.keys( records ) as any[] ).forEach( + ( Object.keys( records ) as Kind[] ).forEach( < K extends Kind >( kind: K ) => { - ( Object.keys( records[ kind ] ) as any[] ).forEach( + ( Object.keys( records[ kind ] ) as Name[] ).forEach( < N extends Name >( name: N ) => { const primaryKeys = ( Object.keys( records[ kind ][ name ].saving From a96e71109938f441ab6c33fa5d143a53640d7283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 3 Jun 2022 00:25:13 +0100 Subject: [PATCH 8/9] Clean up imports --- packages/core-data/src/selectors.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/core-data/src/selectors.ts b/packages/core-data/src/selectors.ts index 3cf2caafe0f104..e008a11c189c04 100644 --- a/packages/core-data/src/selectors.ts +++ b/packages/core-data/src/selectors.ts @@ -18,8 +18,8 @@ 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, WpTemplate } from './entity-types'; -import { +import type { + Context, DefaultContextOf, EntityRecordOf, KeyOf, @@ -27,8 +27,9 @@ import { KindOf, Name, NameOf, + User, + WpTemplate, } from './entity-types'; -import type { OmitNevers } from './entity-types/helpers'; // This is an incomplete, high-level approximation of the State type. // It makes the selectors slightly more safe, but is intended to evolve From 5d81376d88c500329f3827209fb1f4eef4b421a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 3 Jun 2022 23:57:01 +0100 Subject: [PATCH 9/9] Remove dev artifacts --- packages/core-data/src/selectors.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/core-data/src/selectors.ts b/packages/core-data/src/selectors.ts index e008a11c189c04..c039f75aaf4217 100644 --- a/packages/core-data/src/selectors.ts +++ b/packages/core-data/src/selectors.ts @@ -365,12 +365,7 @@ export const getEntityRecord: GetEntityRecord = createSelector( ]; } ); -const commentDefault = getEntityRecord( {} as State, 'root', 'comment', 15 ); -// commentDefault is Comment<'edit'> -const commentView = getEntityRecord( {} as State, 'root', 'comment', 15, { - context: 'view', -} ); /** * Returns the Entity's record object by key. Doesn't trigger a resolver nor requests the entity records from the API if the entity record isn't available in the local state. *