Conversation
core-data/resolvers.js
Outdated
| yield receivePostTypes( postType ); | ||
| export async function* getModelRecord( state, kind, name, primaryKey ) { | ||
| const modelConfig = getModel( kind, name ); | ||
| const record = await apiRequest( { path: `${ modelConfig.baseUrl }/${ primaryKey }?context=edit` } ); |
There was a problem hiding this comment.
Maybe we should allow model to configure the request as some models probably don't require context=edit.
There was a problem hiding this comment.
I tend to think we should always use the edit context (or remove the context concept entirely) but yeah let's keep this way and see if we need to make it configurable as we add models.
There was a problem hiding this comment.
I'd also like to remove it entirely. At least from this abstraction.
|
Regarding the functional point of view, these changes test well, awesome work 👍 |
Yes exactly, mutation, pagination, query fetching...
General request => yes, this just sets the baseline but I don't think we should use it to retrieve a specific record because of pagination. |
core-data/reducer.js
Outdated
| }; | ||
| } | ||
| function model( modelConfig ) { | ||
| return ( state = { byPK: {} }, action ) => { |
There was a problem hiding this comment.
Can we avoid using PK abbreviation and use consistently PrimaryKey? :D
core-data/models.js
Outdated
| import { find } from 'lodash'; | ||
|
|
||
| const models = [ | ||
| { name: 'postType', kind: 'root', pk: 'slug', baseUrl: '/wp/v2/types' }, |
There was a problem hiding this comment.
It should be type rather than kind for consistency. I guess this is because of the fact that type is a reserved word in actions, right?
There was a problem hiding this comment.
Yes it's one of the reasons. Also, type is overused :) so we could have a postype of type type :)
There was a problem hiding this comment.
Can we have a post type of kind type? :D
|
This is cool 👍 |
|
How can we move forward with this, I'd love to merge it to unblock #6384 |
|
@aduth I'd appreciate your thoughts on this one |
|
What exactly is |
core-data/reducer.js
Outdated
| } | ||
|
|
||
| const modelsByKind = groupBy( modelsConfig, 'kind' ); | ||
| export const models = combineReducers( Object.entries( modelsByKind ).reduce( ( memo, [ kind, subModels ] ) => { |
There was a problem hiding this comment.
This whole sequence of logic could do for some explaining variables, newlines, comments. Quite difficult to interpret.
| * @return {Object?} Post Type object. | ||
| * @return {Object?} Record. | ||
| */ | ||
| export function getPostType( state, slug ) { |
There was a problem hiding this comment.
I assume we have some using the existing selector to be updated (work-in-progress) ?
There was a problem hiding this comment.
This PR now generates dynamically the selector in core-data/model-selectors.js. So everything keeps working as before :)
core-data/models.js
Outdated
| { name: 'postType', kind: 'root', primaryKey: 'slug', baseUrl: '/wp/v2/types' }, | ||
| ]; | ||
|
|
||
| export function getModel( kind, name ) { |
There was a problem hiding this comment.
I think you'd at one point floated the idea to me that this configuration could live in state, which would enable others to more easily extend with their own models, and might make it align better with existing concepts like getting via selector, not the standalone utility function here.
There was a problem hiding this comment.
Yes true, I didn't want to build a huge PR directly on the first iteration, I do think we need dynamic models, I think it's needed in #6384 but was thinking of adding those in another iteration.
core-data/models.js
Outdated
| import { find } from 'lodash'; | ||
|
|
||
| const models = [ | ||
| { name: 'postType', kind: 'root', primaryKey: 'slug', baseUrl: '/wp/v2/types' }, |
There was a problem hiding this comment.
I don't know that it's an issue, but I could foresee some confusion on primaryKey naming with how it might be confused or misinterpreted as the database concept, where in the case of post types there is no database entity, and for others we might choose a key which isn't actually the primary key for the database entity. Maybe just me?
There was a problem hiding this comment.
I don't know, I've always used primaryKey personally but I can update to identifier if you think it's better.
core-data/model-resolvers.js
Outdated
| @@ -0,0 +1,19 @@ | |||
| /** | |||
There was a problem hiding this comment.
Should we organize models-related files into their own folder if there's several related ones which stand apart from the rest of the files?
There was a problem hiding this comment.
Move those to the index.js. It's not easy to find a good location without creating cycle dependencies.
core-data/model-resolvers.js
Outdated
| import modelsConfig from './models'; | ||
| import { getModelRecord } from './resolvers'; | ||
|
|
||
| export default modelsConfig.reduce( ( memo, { kind, name } ) => { |
There was a problem hiding this comment.
Minor: Just noting that this is a runtime evaluation which could theoretically be pre-evaluated (the sort of thing prepack would love to optimize for).
There was a problem hiding this comment.
Haha, are you suggesting we try prepack :) I haven't played with it yet
There was a problem hiding this comment.
Haha, are you suggesting we try prepack :)
Nah, not necessarily. Just remarking that it's wasted work that could be pregenerated.
There's also this, which might be more of a viable option if it's really a concern: https://github.com/kentcdodds/babel-plugin-preval
core-data/reducer.js
Outdated
| }; | ||
| } | ||
| function model( modelConfig ) { | ||
| return ( state = { byPrimaryKey: {} }, action ) => { |
There was a problem hiding this comment.
What other properties do you foresee living in this state object?
There was a problem hiding this comment.
I'm thinking queries maybe, but it could be added later, I can update this PR to remove this key at the moment.
Kind is just a way to group models to avoid duplication, I see these as possible values: This will avoid ambiguity in the reducers and at the selectors level.
I believe there a conflict in the implementation details but they share similar goes. Instead of having helpers to create reducers/selectors/actions... the models abstraction generate them. #6395 adds the queries data though, and I think this could be refactored to be included in the models abstraction. (each model will generate its |
| const postType = await apiRequest( { path: `/wp/v2/types/${ slug }?context=edit` } ); | ||
| yield receivePostTypes( postType ); | ||
| export async function* getEntityRecord( state, kind, name, key ) { | ||
| const entity = getEntity( kind, name ); |
There was a problem hiding this comment.
What does entity contain, is it config? It looks like it. Should we rename to:
const entityConfig = getEntityConfig( kind, name );to make it easier to follow?
There was a problem hiding this comment.
If we use entityConfig it means we need an entity too, but we don't have such a thing.
I think it's just entity. I've used entityConfig in the reducer to avoid variable shadowing.
|
In the last commit 9609b27 I've updated the media to use the abstraction. |
|
I feel it's pretty solid for a V1, let me know how to move this forward. |
core-data/index.js
Outdated
| import entities from './entities'; | ||
|
|
||
| const entityResolvers = entities.reduce( ( memo, { kind, name } ) => { | ||
| const kindPrefix = kind === 'root' ? '' : upperFirst( camelCase( kind ) ); |
There was a problem hiding this comment.
I would love to see the following method to avoid code duplication and ensure that both selector and resolver names always match:
const getMethodName = ( kind, name ) => {
const kindPrefix = kind === 'root' ? '' : upperFirst( camelCase( kind ) );
const nameSuffix = upperFirst( camelCase( name ) );
return `get${ kindPrefix }${ nameSuffix }`;
};This logic is quite complicated and essential to have it working properly for all selectors so it would be great to have a simple unit test which ensures it never regresses.
There was a problem hiding this comment.
Good suggestion, updating.
There was a problem hiding this comment.
or:
const generateEntityMethods = ( source) => {
const entityResolvers = entities.reduce( ( memo, { kind, name } ) => {
const kindPrefix = kind === 'root' ? '' : upperFirst( camelCase( kind ) );
const nameSuffix = upperFirst( camelCase( name ) );
return {
...memo,
[ `get${ kindPrefix }${ nameSuffix }` ]: ( state, key ) => source.getEntityRecord( state, kind, name, key ),
};
}, {} );
const entityResolvesr = generateEntityMethods( resolvers );
const entitySelectors = generateEntityMethods( selectors );
gziolo
left a comment
There was a problem hiding this comment.
I'm giving 👍 to the implementation. I recommend waiting for another opinion before proceeding given the impact this PR is going to have. I personally love the direction and flexibility this PR introduces. In particular, this will make it very easy to have those entities extensible because of how those definitions are stored in one file: https://github.com/WordPress/gutenberg/blob/9609b27b885598deb495369f28bd19c11c7023a2/core-data/entities.js.
| import { find, upperFirst, camelCase } from 'lodash'; | ||
|
|
||
| const entities = [ | ||
| { name: 'postType', kind: 'root', key: 'slug', baseUrl: '/wp/v2/types' }, |
There was a problem hiding this comment.
Could kind just be optional rather than having an explicit 'root' value? Or does that make the implementation more complex to handle the fact it's optional?
There was a problem hiding this comment.
In several methods, the kind is the first parameter before the name and other optional parameters (think query, for instance). I'd love to get rid of the kind but I fear these methods arguments won't be that easy to handle in that case. getEntityRecords( null, 'postType', query )
core-data/entities.js
Outdated
| * @return {Object} Entity | ||
| */ | ||
| export function getEntity( kind, name ) { | ||
| return find( entities, ( entity ) => entity.kind === kind && entity.name === name ); |
There was a problem hiding this comment.
Nit: Object predicate could make this nice:
return find( entities, { kind, name } );There was a problem hiding this comment.
I though these shortcuts were getting deprecated from lodash?
There was a problem hiding this comment.
As I understand it, they are on the roadmap to be excluded by default in 5.0.0 .
Remove shorthand support by default
https://github.com/lodash/lodash/wiki/Roadmap
Doesn't mean they won't exist as an option. I'm personally a fan, and would opt for it.
| * @return {string} Method name | ||
| */ | ||
| export const getMethodName = ( kind, name ) => { | ||
| const kindPrefix = kind === 'root' ? '' : upperFirst( camelCase( kind ) ); |
There was a problem hiding this comment.
root optional might actually make this simpler.
core-data/index.js
Outdated
| import * as resolvers from './resolvers'; | ||
| import { default as entities, getMethodName } from './entities'; | ||
|
|
||
| const entityResolvers = entities.reduce( ( memo, { kind, name } ) => { |
There was a problem hiding this comment.
Personally I've stopped using the name memo because it seems to be common for it to be unfamiliar. Not sure if you feel strongly about it one way or the other.
There was a problem hiding this comment.
What's your current way of doing this? do you use a specific name, like resolvers here for instance?
There was a problem hiding this comment.
I tend toward result, though I'm not strongly committed to it.
core-data/index.js
Outdated
| const entityResolvers = entities.reduce( ( memo, { kind, name } ) => { | ||
| const methodName = getMethodName( kind, name ); | ||
| return { | ||
| ...memo, |
There was a problem hiding this comment.
Minor (Perf): As implemented, we're creating a shallow clone on each iteration, when we could be mutating directly.
core-data/index.js
Outdated
| }; | ||
| }, {} ); | ||
|
|
||
| const entitySelectors = entities.reduce( ( memo, { kind, name } ) => { |
There was a problem hiding this comment.
My DRY senses are tingling 😄
Maybe:
const createEntityRecordGetter = ( source ) => entities.reduce( ( result, entity ) => {
const { kind, name } = entity;
const methodName = getMethodName( kind, name );
result[ methodName ] = ( state, key ) => source.getEntityRecord( state, kind, name, key );
return result;
} );There was a problem hiding this comment.
I know me too, but at some point, I thought they could diverge (have more selectors than resolvers ). Of course, we can update later when they diverge.
There was a problem hiding this comment.
I think I saw a similar comment earlier today 😄
There was a problem hiding this comment.
Oof, the "Show outdated" toggles in GitHub are really easy to miss. 😬
There was a problem hiding this comment.
Since you both asked for it, I executed :)
core-data/reducer.js
Outdated
| } | ||
|
|
||
| return state; | ||
| const key = entityConfig.key || 'id'; |
There was a problem hiding this comment.
Minor (Perf): We don't need to derive this on every dispatch. It can be in the surrounding function scope.
|
Thanks all for your help on this PR |
This PR tries to implement the idea of an entities abstraction. An entity is an abstraction of a WP entity (post, taxonomy, postType, category...). It allows to automatically generate selectors, resolvers for the said entity in the core data module.
In this PR, it's a basic implementation:
This PR is blocking #6384 and is related to #6395