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

Update / Create objects callback #158

Closed
Inexad opened this issue Sep 21, 2020 · 8 comments
Closed

Update / Create objects callback #158

Inexad opened this issue Sep 21, 2020 · 8 comments

Comments

@Inexad
Copy link

Inexad commented Sep 21, 2020

Hi,

It would be great to be able to add a callback functionality when deleting / updating objects.
Now you can use isDeleting$/isUpdating$, the problem is that it's hard to get the affected object.

Sometimes when removing / updating objects you need to perform actions after. Like removing it from another objects collection, call other function and so on.. The actions is needed to be performed after the actually server-side call.

Currently i'm doing it like this (which is not very clean):

        var objectToUpdate = new Order();
        objectToUpdate.id = 1;
        objectToUpdate.name = "Something"

        this.ordersFacade.update(objectToUpdate);
        this.ordersFacade.isSaving$.pipe(take(2)).subscribe((status) => {
          if (status) {
            doSomething(objectToUpdate);
          }
        });

Maybe i'm looking at this from wrong perspective ?

Regards.

@jrista
Copy link
Contributor

jrista commented Sep 21, 2020

Hi @Inexad! Thank you for the report!

Indeed, this is a bit of a rough area in NgRx Auto-Entity right now. It is also an area we are hoping to address in the near future. We do not yet have an ideal solution that would allow end developers to solve this problem easily, but we are working on it.

Now, we have made some small strides toward supporting this kind of functionality. You should be using custom effects, rather than the selections on the facades, to do the job. That said, it is a non-trivial problem to solve. I will have to get back to you with a more detailed explanation, however for now:

  1. You should create effects to "coordinate" multiple actions that must be taken when, say, an entity is created, updated, or deleted.
  2. You should be using the correlationId of each entity action to properly associate "Success/Failure" actions to the original Create/Update/Delete action.
  3. You should be using NgRx Auto-Entity 0.5.0 betas (soon to be released officially) in order to get all of the necessary functionality, including the entities that were created/updated/deleted (the actions in 0.5.x have been updated to include those entities now!)

With the above, and with some custom actions to assist in coordinating the rest of the process, it IS possible to perform other actions IN RESPONSE TO the success (or failure) of a create, update or delete of another entity.

We have some other plans for the future to support these scenarios as well. We may introduce another "stage" in the standard initiation/result (i.e. create / createsuccess|createfailure) flow. A third, intermediate stage, between the initiation (create), to handle the preliminary result (intermediate/preliminary result handler), and the final result (existing result handler). This three-stage approach has been discussed and is planned...there is an issue we created to track the idea:

#97

We also have had discussions about the addition of some kind of PatchEntities action that would support arbitrary updates to Auto-Entity state. Discussions are still ongoing, but such an action could potentially be used in a new "preliminary results" stage if we add one in the future.

We have also been considering the concept of "batched" processes. This would basically allow some kind of token to be created that can track the progress of multiple actions, and support easier coordination of multi-stage processes. We have an issue created to track that idea as well:

#152

In the mean time, using the 0.5.0 beta features to support the creation of "Correlated" and "Coordinated" workflows with NgRx Effects is the best way to solve the problem you have right now. I'll provide more detail with example code as soon as I get a chance.

@schuchard
Copy link
Contributor

Also within the 0.5.0 betas, the actionable facade methods return the correlationId (you can also create your own and pass it to the method if you'd like).

const correlationId = this.ordersFacade.update(objectToUpdate);

@Inexad
Copy link
Author

Inexad commented Sep 21, 2020

@jrista @schuchard
Thanks once again for a throughout explanation and status update!

I will use correlationId for now and i'm very excited for upcoming updates of this great package! I'm currently migrating some of our business system using this package, and it works good so far !

Regards.

@Inexad Inexad closed this as completed Sep 21, 2020
@jrista
Copy link
Contributor

jrista commented Sep 21, 2020

Let us know if you run into any other issues. I'll also probably reply to this issue with some additional code examples later this evening, as I want to make sure you understand all the little nuances of "correlating" actions.

@jrista
Copy link
Contributor

jrista commented Sep 30, 2020

@Inexad Correlation is a more complex topic, and since it is taking some time to get the details together, I've decided to just add it all to our git book docs. Keep an eye out there for the info on how to coordinate.

@Inexad
Copy link
Author

Inexad commented Oct 1, 2020

@jrista Thanks! I will keep checking the git book docs.

Regarding callback functionality, wouldn't it be possible to pass in a callback function for each function that is invoked after completion ?

like:

orderFacade.loadAll({customerId: 123}, null, loadedCallback);
orderFacade.update(orderObject, updatedCallback)

loadedCallback(result) {

}

updatedCallback(updatedOrderObject) {

}

Regards

@Inexad Inexad reopened this Oct 1, 2020
@jrista
Copy link
Contributor

jrista commented Oct 1, 2020

So the facades are simply there to facilitate using NgRx. That means selecting data and dispatching actions.

Dispatching an action initiates an asynchronous process...and this is explicitly by design. Calling load on a facade does not mean that we make an http call directly, and can then just "wait" for its response. Calling load on a facade simply dispatches an ngrx action that ultimately results in data being loaded.

The fundamental design of NgRx is to EXPLICITLY decouple things. One of the things decoupled when using NgRx is requesting that data be loaded, and getting the data once it is available. It is an express design decision and therefor also an express choice to use this decoupled process when using NgRx...in general. One of our goals with NgRx Auto-Entity is to use NgRx as it was designed as well, and not try to change those aspects of the framework.

So, when you use an NgRx Auto-Entity facade...the express and intended design is that if you want to load data, which is intended to get that data into state (not necessarily anywhere else), then you are then intending to also use the appropriate selection (i.e. all$, or sorted$, etc.) to retrieve the data from state (note...from state, not from a server or anywhere else) for display in your UI, or use in effects, etc.

We strongly believe in the benefits that NgRx brings to the table by decoupling data retrieval and state...from data selection and use.

Further, when it comes to synchronizing behavior. This should also be done primarily through effects. There is a recommended design here when using NgRx Auto-Entity that I'll be outlining in some of the upcoming documentation. Generally speaking, with an Auto-Entity app, you would have several "layers" of code, and generally speaking, layers communicate "down" to the layers below, but not "up" to layers above:

==============================
   UI    UI   UI   UI   UI
---v-----v----v----v----v-----
   Facades        Facades  
------v--------------v--------
        Effects/Selectors
----v---------v----------v----
    |  State    State    |
----v--------------------v----
       Entity Services
==============================
              |
              v
==============================
     API     API     API
==============================

The vast majority of your code should ultimately reside in effects and selectors when using NgRx in general. A facade layer is largely just there to bridge the gap between UI and the rest of the application, and facilitate reuse.

So when it comes to coordinating behavior, that would largely be done in the effects. So in your original post, you chose to update some object, then you needed to do something once it was updated. That would all be effects:

export const saveOrderAndToast = createAction('[Orders] Save and Toast', props<{ order: Order, correlationId: string }>());

export class OrderFacade extends OrderFacadeBase {
  // ...standard facade setup...

  save(order: Order): void {
    this.store.dispatch(saveOrderAndToast({order, correlationId: /* generate random correlation id */});
  }
}

export class OrderEffects {
  constructor(private actions$: Actions, private toasts: ToastService) {}

  updateOrder$ = createEffect(
    () => this.actions$.pipe(
      ofType(saveOrderAndToast),
      map(({order, correlationId}) => new Update(Order, order, correlationId))
	)
  );

  toastOnOrderSaveSuccess$ = createEffect(
    () => combineLatest([
      this.actions$.pipe(ofType(saveOrderAndToast)),
      this.actions$.pipe(
        ofEntityType(Order, EntityActionTypes.UpdateSucceeded), 
        timeout(30000), // If you don't want to wait indefinitely, set up a timeout...
        catchError(() => null) // ... then convert any error to null (timeout throws if it times out!)
      ),
    ]).pipe(
      // When we get an update success action dispatched where the correlationId matches our initiating
      // action 'saveOrderAndToast', we continue:
      filter(([{correlationId}, success]) => !!success && success.correlationId === correlationId),
      switchMap(([, {entity}]) => 
        // If you use services that return promises, you can use `switchMap` or `from` to turn them into observables!
        from(this.toasts.showToast('Order Saved', `Successfully updated order ${getKeyFromModel(Order, order)}`))
      )
	)
  );
}

So the high level, simple approach here is:

  1. Create an action to initiate a process (a workflow, for lack of a better term)
  2. Dispatch that action with any relevant data, and include a correlationId
  3. Handle your initiating action in an effect, pass any data and correlationId to Auto-Entity actions
  4. Create another effect, combine latest on TWO action streams:
    a) Your initiating action
    b) The appropriate result action for your Auto-Entity
  5. Filter this second effect so that only when the correlationId of the result (success/failure) matches that of the initiating action
  6. Complete your process

In my above example, I have only handled success. You could also handle failure, or only failure, with another effect. You could handle success and failure in a single action. You can do a lot of things once you start using effects, as that is, generally, how NgRx works. Effects observe action streams, filter actions, join actions and data from other streams (i.e. selectors), and dispatch other actions. This, ultimately, creates "workflows" that encapsulate the vast majority of your business logic.

Auto-Entity is just here to take care of most of the entity state creation boilerplate. Instead of having to write dozens to hundreds of lines of code per entity, you can write about 10 or so. Once you have created an auto entity, however, you should still be using NgRx largely how you would have used it before.

@Inexad
Copy link
Author

Inexad commented Oct 3, 2020

@jrista Thanks so much for this throughout explanation!

Regards!

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

No branches or pull requests

3 participants