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(Entity): add updateMany by predicate #907

Merged
merged 11 commits into from
Oct 26, 2018

Conversation

timdeschryver
Copy link
Member

@timdeschryver timdeschryver commented Mar 13, 2018

This PR allows to edit entities based on a predicate.
See #672, in the issue it was mentioned to update the entities via multiple predicates - see my comment as why I don't think thats a good idea. If needed I can implement it that way tho.

I do got one question about the implementation of the sorted state adapter.
Why does this have an own implementation?
In my head the sorted adapter should call the unsorted adapter and do the sorting afterwards, but I must be missing someting 😅

@coveralls
Copy link

coveralls commented Mar 13, 2018

Coverage Status

Coverage increased (+0.07%) to 88.437% when pulling 3680a79 on timdeschryver:pr/update-many-predicate into a9bc070 on ngrx:master.

@MikeRyanDev
Copy link
Member

Implementation seems reasonable. What are your opinions on this:

Instead of a predicate with a list of changes the signature is updated to use a filter/map function:

 // Name is bad but it illustrated the interface
interface Updater<T> {
  (entity: T): false | Change<T>;
}
function updateMany(updater: Updater<T>, state: EntityState<T>);

@timdeschryver
Copy link
Member Author

I don't think I get the idea.
Would this be a map implementation where the user implements map?
For example:

adapter.updateMany(
      entity => entity.id.startsWith('a') ? {title: 'New title'} : false, // maybe null or an empty object instead of false?
      withMany
);

@MikeRyanDev
Copy link
Member

Yes, that's right.

@timdeschryver
Copy link
Member Author

timdeschryver commented Mar 15, 2018

I must say your proposal feels a bit weird at first but after seeing it in action, it is better in my opinion.
I did change the signature to return a Partial<T>, this brings the flexibility to do a second change - if no change is required we can return an empty object or the entitity object itself.
Thoughts?
See the unsorted state test which illustrates an additional change: book => (book.id.startsWith('a') ? firstChange : secondChange),

If someone got a better name than Change please feel free to speak up 😄

EDIT: I just realized that with your signature, it is also possible to have a second change.
Matter of fact, it is possible to introduce as many changes as needed.
I'll change the code so it has a false | Partial<T> signature.

@@ -38,6 +38,10 @@ export type UpdateNum<T> = {

export type Update<T> = UpdateStr<T> | UpdateNum<T>;

export type Map<T> = {
Copy link
Member

Choose a reason for hiding this comment

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

Recommend renaming this to something more specific. Maybe UpdateMap?

Copy link
Member Author

Choose a reason for hiding this comment

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

LGTM!

const updates: Update<T>[] =
updatesOrMap instanceof Array
? updatesOrMap
: state.ids.map((id: string | number) => ({
Copy link
Member

Choose a reason for hiding this comment

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

I think that creating an Update object for each entity means even entities that shouldn't change will lose referential equality. I think this map should be a reduce instead that shrinks the ids array into a smaller array of Updates for only changed entities.

: state.ids.reduce((changes: Update<T>[], id: string | number) => {
const change = updatesOrMap(state.entities[id]);
if (change && Object.keys(change).length > 0) {
return [
Copy link
Member

Choose a reason for hiding this comment

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

Recommend changing this line to:

changes.push({ id, changes: change });

return changes;

That way we don't allocate and deallocate an array for every item we are upserting.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done, thanks for the feedback/reviews!

Copy link
Member

Choose a reason for hiding this comment

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

@tdeschryver Thanks for all of the hard work! We really appreciate it! 😄

@MikeRyanDev
Copy link
Member

One minor change. Otherwise this is looking good to me!

@sandangel
Copy link
Contributor

sandangel commented Apr 11, 2018

Hi, how about adding this feature to upsert too? When predicate function is provided, upsert will call update with predicate under the hood

@brandonroberts
Copy link
Member

@timdeschryver will you rebase this so it can land?

@timdeschryver
Copy link
Member Author

✔️

@MikeRyanDev
Copy link
Member

I wonder if we could just have entityAdapter.map(...) instead.

@timdeschryver
Copy link
Member Author

That would make sense, since this is what we're actually doing.
But I think having a updateMany would have its value if we would change the API to the following ?

{
  predicate: entity => boolean,
  changes: Partial<Entity>
}

@timdeschryver
Copy link
Member Author

timdeschryver commented Sep 14, 2018

Ha, now I see we started with the above API 😄 - 149042b

@timdeschryver
Copy link
Member Author

timdeschryver commented Sep 22, 2018

I've updated the PR to be a map function.
While doing this I also changed the signature to actually return an entity object.

Let me know your thoughts about this one.
I could also submit a PR if you agree with #907 (comment).

@@ -38,6 +38,10 @@ export type UpdateNum<T> = {

export type Update<T> = UpdateStr<T> | UpdateNum<T>;

export type EntityMap<T> = {
Copy link
Member

Choose a reason for hiding this comment

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

Update type to match export type ComparerStr<T> = (a: T, b: T) => string;

@brandonroberts
Copy link
Member

Minor nit, otherwise changes LGTM

@timdeschryver
Copy link
Member Author

There we go 😄

@brandonroberts
Copy link
Member

@timdeschryver rebase pls

EntityState,
EntityAdapter,
Update,
EntityMap,
Copy link
Member Author

@timdeschryver timdeschryver Oct 26, 2018

Choose a reason for hiding this comment

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

I did also add EntityMap and Predicate here.

@@ -19,9 +19,6 @@ export class Store<T> extends Observable<T> implements Observer<Action> {
this.source = state$;
}

/**
Copy link
Member

Choose a reason for hiding this comment

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

Revert this change because its in a separate PR

Copy link
Member Author

Choose a reason for hiding this comment

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

Doh, I was using 2 VSCode instances and was wondering what was happening.
Sorry... 😅

@brandonroberts brandonroberts merged commit 4e4c50f into ngrx:master Oct 26, 2018
@timdeschryver timdeschryver deleted the pr/update-many-predicate branch October 26, 2018 18:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants