-
Notifications
You must be signed in to change notification settings - Fork 4.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Initial entities implementation #6462
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This whole sequence of logic could do for some explaining variables, newlines, comments. Quite difficult to interpret.
*/ | ||
export function getPostType( state, slug ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume we have some using the existing selector to be updated (work-in-progress) ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know, I've always used primaryKey
personally but I can update to identifier
if you think it's better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe just key
? Or keyBy
(#) ?
core-data/model-resolvers.js
Outdated
@@ -0,0 +1,19 @@ | |||
/** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haha, are you suggesting we try prepack :) I haven't played with it yet
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What other properties do you foresee living in this state object?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good suggestion, updating.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 );
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Object predicate could make this nice:
return find( entities, { kind, name } );
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I though these shortcuts were getting deprecated from lodash?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
root
optional might actually make this simpler.
core-data/index.js
Outdated
@@ -10,12 +10,29 @@ import reducer from './reducer'; | |||
import * as selectors from './selectors'; | |||
import * as actions from './actions'; | |||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I saw a similar comment earlier today 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oof, the "Show outdated" toggles in GitHub are really easy to miss. 😬
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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