Skip to content
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

Feat : withLifecycleCallbacks support for wilcard * and array of callbacks + refacto #9577

Merged
merged 8 commits into from
Jan 29, 2024

Conversation

quentin-decre
Copy link
Contributor

Problem

Using withLifecycleCallbacks when you have many callbacks can be very verbose and hard to read / maintain. This can be improved a lot with two features :

1. Support for a wildcard resource : *

Imagine for example your API returns "identifier" and you want to rename it to "id" to make it work with react-admin. You will need to create a callback for each resource. But now you can do that :

const dataProvider = withLifecycleCallbacks(
  jsonServerProvider('http://localhost:3000'), [
  {
    resource: '*',
    afterRead: async (data, { dataProvider, resource }) => {
      // rename field to the record
      data.id = data.identifier
      return data
    },
  },
])

2. Support for arrays of callbacks

When you have a lot of callbacks / hooks (which is often the case when having a graphql API), you may want to have one callback per purpose, and name it after that purpose. So you could have many callbacks for the same resource / hook. This is already possible but quite verbose / difficult to read.
The idea is to allow to pass an array of callbacks, while still supporting passing directly one callback.

Currently :

      {
        resource: '*',
        beforeGetList: defaultSortById,
      },
      {
        resource: '*',
        beforeGetList: addNullsLastToSortOrders,
      },
      {
        resource: '*',
        beforeGetList: convertFilters,
      },

After :

      {
        resource: '*',
        beforeGetList: [defaultSortById, addNullsLastToSortOrders, convertFilters],
      },

@djhi
Copy link
Collaborator

djhi commented Jan 11, 2024

This looks great 👍 I like that it encourages good practices and better readability. Would you mind adding some tests and updating the documentation?

Copy link
Member

@fzaninotto fzaninotto left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this is a new feature, it must be PRed against next rather than master

@@ -67,13 +67,13 @@ import {
* [
* {
* resource: "posts",
* afterRead: async (data, dataProvider) => {
* afterRead: async (data, {dataProvider, resource}) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a breaking change. You can add the same feature in a backward compatible way, by adding a third parameter instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed


const afterUpdateHandlers = handlers.filter(
h => h.resource === resource && h.afterUpdate
newParams = await applyCallbacks(resource, 'beforeSave', newParams);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is another breaking change: beforeSave used to be applied on params.data, not on params

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed

}
const afterSaveHandlers = handlers.filter(
h => h.resource === resource && h.afterSave
newParams = await applyCallbacks(resource, 'beforeSave', newParams);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed

Comment on lines 405 to 406
| ResourceCallback<CreateResult<T>>[]
| ResourceCallback<CreateResult<T>>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should create a type for this union, to avoid repetition

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed

* @param {U} params The params / result to pass to the callbacks
* @returns {Promise<U>} The params / result after the callbacks have been applied
*/
const applyCallbacks = async function <U>(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest you extract that function, and unit test it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I extract this function, I see two options :

  1. either we have to pass the handlers + dataProvider each time, the drawback is that it's quite verbose and the code is less clear :
newParams = await applyCallbacks(
                dataProvider,
                handlers,
                resource,
                'beforeGetList',
                newParams
            );
  1. Or I could bind the the repeated parameters :
    const applyCallbacksWithContext = applyCallbacks.bind(
        null,
        dataProvider,
        handlers
    );

....

newParams = await applyCallbacksWithContext(
                resource,
                'beforeGetList',
                newParams
            );

I tend to prefer the first option because the code is less complex to understand even though it's very verbose.

What is your opinion on that ?

* The list of all possible data provider hooks
* @see https://marmelab.com/react-admin/DataProviders.html#data-provider-hooks
*/
export const dataProviderHooks = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you export it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I use it in my own code to apply a callback for every possible hook (for login purpose essentially).
I understand this could be in my own code though.

@quentin-decre
Copy link
Contributor Author

Regarding the documentation, I hesitated between added the new features in the main part of the documentation, or add an "advanced use" section, and explain the new possibilities, and how they are very useful, in which cases, best practices, ....
Also add some details about execution order and the possibility to add the same resource multiple times (it was already possible, but not very clear for me in the documentation).
What is your opinion on that ?

Can you tell me if the implementation is OK for you ? If yes, I will move on unit testing :)

Copy link
Collaborator

@djhi djhi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

Regarding the documentation, I hesitated between added the new features in the main part of the documentation, or add an "advanced use" section, and explain the new possibilities, and how they are very useful, in which cases, best practices, ....

I would probably add this in the Code Organization section.

Also add some details about execution order and the possibility to add the same resource multiple times (it was already possible, but not very clear for me in the documentation).

This is definitely worth adding in the Usage section

Comment on lines 560 to 586
/**
* The list of all possible data provider hooks
* @see https://marmelab.com/react-admin/DataProviders.html#data-provider-hooks
*/
export enum dataProviderHooks {
afterCreate = 'afterCreate',
afterDelete = 'afterDelete',
afterDeleteMany = 'afterDeleteMany',
afterGetList = 'afterGetList',
afterGetMany = 'afterGetMany',
afterGetManyReference = 'afterGetManyReference',
afterGetOne = 'afterGetOne',
afterUpdate = 'afterUpdate',
afterUpdateMany = 'afterUpdateMany',
beforeCreate = 'beforeCreate',
beforeDelete = 'beforeDelete',
beforeDeleteMany = 'beforeDeleteMany',
beforeGetList = 'beforeGetList',
beforeGetMany = 'beforeGetMany',
beforeGetManyReference = 'beforeGetManyReference',
beforeGetOne = 'beforeGetOne',
beforeUpdate = 'beforeUpdate',
beforeUpdateMany = 'beforeUpdateMany',
beforeSave = 'beforeSave',
afterRead = 'afterRead',
afterSave = 'afterSave',
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this is not used anywhere, I believe we should remove it

@quentin-decre
Copy link
Contributor Author

I have fixed + added some unit tests for the new features

@quentin-decre
Copy link
Contributor Author

Thanks!

Regarding the documentation, I hesitated between added the new features in the main part of the documentation, or add an "advanced use" section, and explain the new possibilities, and how they are very useful, in which cases, best practices, ....

I would probably add this in the Code Organization section.

Also add some details about execution order and the possibility to add the same resource multiple times (it was already possible, but not very clear for me in the documentation).

This is definitely worth adding in the Usage section

agreed, did that :)

Copy link
Collaborator

@djhi djhi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please remove the package-lock.json file? Besides, I don't get why there are changes in the yarn.lock file.

@quentin-decre
Copy link
Contributor Author

Can you please remove the package-lock.json file? Besides, I don't get why there are changes in the yarn.lock file.

deleted + reverted :)

@fzaninotto fzaninotto merged commit 9441614 into marmelab:next Jan 29, 2024
8 of 9 checks passed
@fzaninotto
Copy link
Member

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants