Skip to content

Commit

Permalink
feat(entities): 🔥 add events
Browse files Browse the repository at this point in the history
BREAKING CHANGE: 🧨 the store peer dependency is now v2.5.0
  • Loading branch information
NetanelBasal committed Jan 19, 2024
1 parent c2ef549 commit e05d8ff
Show file tree
Hide file tree
Showing 10 changed files with 211 additions and 78 deletions.
18 changes: 18 additions & 0 deletions docs/docs/miscellaneous/entity-events.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Entity Events

You can listen to store entity events using the `store.events$` observable:

```ts
store.events$.subscribe((event: StoreEvent) => {
if (event.type === 'add') {
console.log(event.ids);
}
});

interface StoreEvent {
type: 'add' | 'delete' | 'update' | 'set';
ids: any[];
}
```

Delete all - `event.type === 'delete' && !event.ids.length`
3 changes: 3 additions & 0 deletions packages/entities/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
"email": "netanel7799@gmail.com",
"url": "https://netbasal.com"
},
"peerDependencies": {
"@ngneat/elf": ">=2.5.0"
},
"sideEffects": false,
"license": "MIT"
}
25 changes: 17 additions & 8 deletions packages/entities/src/lib/add.mutation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from '@ngneat/elf-mocks';
import { addEntities, addEntitiesFifo } from './add.mutation';
import { UIEntitiesRef, withEntities, withUIEntities } from './entity.state';
import { take } from 'rxjs';

describe('add', () => {
let store: ReturnType<typeof createEntitiesStore>;
Expand All @@ -17,7 +18,15 @@ describe('add', () => {
});

it('should add entity', () => {
store.events$.pipe(take(1)).subscribe((action) => {
expect(action).toMatchObject({
type: 'add',
ids: [1],
});
});

store.update(addEntities(createTodo(1)));

toMatchSnapshot(expect, store, 'add one');
});

Expand All @@ -29,15 +38,15 @@ describe('add', () => {
it('should prepend entities', () => {
store.update(addEntities([createTodo(1), createTodo(2)]));
store.update(
addEntities([createTodo(3), createTodo(4)], { prepend: true })
addEntities([createTodo(3), createTodo(4)], { prepend: true }),
);
toMatchSnapshot(expect, store, 'prepend');
});

it('should work with ref', () => {
const store = createUIEntityStore();
store.update(
addEntities([createUITodo(1), createUITodo(2)], { ref: UIEntitiesRef })
addEntities([createUITodo(1), createUITodo(2)], { ref: UIEntitiesRef }),
);
toMatchSnapshot(expect, store, 'ref');
});
Expand All @@ -46,10 +55,10 @@ describe('add', () => {
const store = createStore(
{ name: '' },
withEntities<{ id: number }>(),
withUIEntities<{ _id: string; name: string }, '_id'>({ idKey: '_id' })
withUIEntities<{ _id: string; name: string }, '_id'>({ idKey: '_id' }),
);
store.update(
addEntities({ _id: '1', name: 'foo' }, { ref: UIEntitiesRef })
addEntities({ _id: '1', name: 'foo' }, { ref: UIEntitiesRef }),
);
store.update(addEntities({ id: 1 }));
expect(store.getValue()).toMatchSnapshot();
Expand All @@ -61,7 +70,7 @@ describe('add', () => {
store.update(
addEntitiesFifo([createTodo(1), createTodo(2), createTodo(3)], {
limit,
})
}),
);
store.update(addEntitiesFifo([createTodo(4)], { limit }));

Expand All @@ -73,15 +82,15 @@ describe('add', () => {
store.update(
addEntitiesFifo([createTodo(1), createTodo(2), createTodo(3)], {
limit,
})
}),
);
expect(store.getValue()).toMatchSnapshot('should be 3 2 1');

store.update(
addEntitiesFifo(
[createTodo(4), createTodo(5), createTodo(6), createTodo(7)],
{ limit }
)
{ limit },
),
);
expect(store.getValue()).toMatchSnapshot('should be 7 6 5');

Expand Down
26 changes: 15 additions & 11 deletions packages/entities/src/lib/add.mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,16 @@ export interface AddEntitiesOptions {
*/
export function addEntities<
S extends EntitiesState<Ref>,
Ref extends EntitiesRef = DefaultEntitiesRef
Ref extends EntitiesRef = DefaultEntitiesRef,
>(
entities: OrArray<getEntityType<S, Ref>>,
options: AddEntitiesOptions & BaseEntityOptions<Ref> = {}
options: AddEntitiesOptions & BaseEntityOptions<Ref> = {},
): Reducer<S> {
return function (state, context) {
return function (state, ctx) {
const { prepend = false, ref = defaultEntitiesRef } = options;

const { entitiesKey, idsKey } = ref!;
const idKey = getIdKey<any>(context, ref);
const idKey = getIdKey<any>(ctx, ref);

const asArray = coerceArray(entities);

Expand All @@ -53,6 +53,8 @@ export function addEntities<

const { ids, asObject } = buildEntities<S, Ref>(asArray, idKey);

ctx.setEvent({ type: 'add', ids });

return {
...state,
[entitiesKey]: { ...state[entitiesKey], ...asObject },
Expand All @@ -75,14 +77,14 @@ export function addEntities<
*/
export function addEntitiesFifo<
S extends EntitiesState<Ref>,
Ref extends EntitiesRef = DefaultEntitiesRef
Ref extends EntitiesRef = DefaultEntitiesRef,
>(
entities: OrArray<getEntityType<S, Ref>>,
options: {
limit: number;
} & BaseEntityOptions<Ref>
} & BaseEntityOptions<Ref>,
): Reducer<S> {
return function (state, context) {
return function (state, ctx) {
const { ref = defaultEntitiesRef, limit } = options;

const { entitiesKey, idsKey } = ref!;
Expand All @@ -94,7 +96,7 @@ export function addEntitiesFifo<
if (normalizedEntities.length > limit) {
// Remove new entities that pass the limit
normalizedEntities = normalizedEntities.slice(
normalizedEntities.length - limit
normalizedEntities.length - limit,
);
}

Expand All @@ -103,14 +105,16 @@ export function addEntitiesFifo<
// Remove exiting entities that passes the limit
if (total > limit) {
const idsRemove = currentIds.slice(0, total - limit);
newState = deleteEntities<S, Ref>(idsRemove)(state, context);
newState = deleteEntities<S, Ref>(idsRemove)(state, ctx);
}

const { ids, asObject } = buildEntities<S, Ref>(
normalizedEntities,
getIdKey(context, ref)
getIdKey(ctx, ref),
);

ctx.setEvent({ type: 'add', ids });

return {
...state,
[entitiesKey]: { ...newState[entitiesKey], ...asObject },
Expand All @@ -123,7 +127,7 @@ function throwIfEntityExists(
entities: any[],
idKey: string,
state: Record<any, any>,
entitiesKey: string
entitiesKey: string,
) {
entities.forEach((entity) => {
const id = entity[idKey];
Expand Down
11 changes: 9 additions & 2 deletions packages/entities/src/lib/delete.mutation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
deleteEntitiesByPredicate,
} from './delete.mutation';
import { UIEntitiesRef } from './entity.state';
import { take } from 'rxjs';

describe('delete', () => {
let store: ReturnType<typeof createEntitiesStore>;
Expand All @@ -23,6 +24,12 @@ describe('delete', () => {
it('should delete entity', () => {
store.update(addEntities([createTodo(1), createTodo(2)]));
toMatchSnapshot(expect, store, 'should have two entities');
store.events$.pipe(take(1)).subscribe((action) => {
expect(action).toMatchObject({
type: 'delete',
ids: [1],
});
});
store.update(deleteEntities(1));
toMatchSnapshot(expect, store, 'should delete one entity');
});
Expand Down Expand Up @@ -51,13 +58,13 @@ describe('delete', () => {
it('should work with ref', () => {
const store = createUIEntityStore();
store.update(
addEntities([createUITodo(1), createUITodo(2)], { ref: UIEntitiesRef })
addEntities([createUITodo(1), createUITodo(2)], { ref: UIEntitiesRef }),
);
toMatchSnapshot(expect, store, 'should have three ui entities');
store.update(
deleteEntitiesByPredicate((entity) => entity.id === 1, {
ref: UIEntitiesRef,
})
}),
);
toMatchSnapshot(expect, store, 'should delete the ui entity with id of 1');
});
Expand Down
27 changes: 16 additions & 11 deletions packages/entities/src/lib/delete.mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,26 @@ import { findIdsByPredicate } from './entity.utils';
*/
export function deleteEntities<
S extends EntitiesState<Ref>,
Ref extends EntitiesRef = DefaultEntitiesRef
Ref extends EntitiesRef = DefaultEntitiesRef,
>(
ids: OrArray<getIdType<S, Ref>>,
options: BaseEntityOptions<Ref> = {}
options: BaseEntityOptions<Ref> = {},
): Reducer<S> {
return function (state) {
return function (state, ctx) {
const { ref: { idsKey, entitiesKey } = defaultEntitiesRef } = options;

const idsToRemove = coerceArray(ids);
const newEntities = { ...state[entitiesKey] };
const newIds = state[idsKey].filter(
(id: getIdType<S, Ref>) => !idsToRemove.includes(id)
(id: getIdType<S, Ref>) => !idsToRemove.includes(id),
);

for (const id of idsToRemove) {
Reflect.deleteProperty(newEntities, id);
}

ctx.setEvent({ type: 'delete', ids: idsToRemove });

return {
...state,
[entitiesKey]: newEntities,
Expand All @@ -61,20 +63,21 @@ export function deleteEntities<
*/
export function deleteEntitiesByPredicate<
S extends EntitiesState<Ref>,
Ref extends EntitiesRef = DefaultEntitiesRef
Ref extends EntitiesRef = DefaultEntitiesRef,
>(
predicate: ItemPredicate<getEntityType<S, Ref>>,
options: BaseEntityOptions<Ref> = {}
options: BaseEntityOptions<Ref> = {},
): Reducer<S> {
return function reducer(state, context) {
return function reducer(state, ctx) {
const ids = findIdsByPredicate(
state,
options.ref || (defaultEntitiesRef as Ref),
predicate
predicate,
);

if (ids.length) {
return deleteEntities(ids, options)(state, context) as S;
ctx.setEvent({ type: 'delete', ids });
return deleteEntities(ids, options)(state, ctx) as S;
}

return state;
Expand All @@ -92,11 +95,13 @@ export function deleteEntitiesByPredicate<
*/
export function deleteAllEntities<
S extends EntitiesState<Ref>,
Ref extends EntitiesRef = DefaultEntitiesRef
Ref extends EntitiesRef = DefaultEntitiesRef,
>(options: BaseEntityOptions<Ref> = {}): Reducer<S> {
return function reducer(state: S) {
return function reducer(state: S, ctx) {
const { ref: { idsKey, entitiesKey } = defaultEntitiesRef } = options;

ctx.setEvent({ type: 'delete', ids: [] });

return {
...state,
[entitiesKey]: {},
Expand Down
12 changes: 9 additions & 3 deletions packages/entities/src/lib/set.mutation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from '@ngneat/elf-mocks';
import { setEntities, setEntitiesMap } from './set.mutation';
import { UIEntitiesRef } from './entity.state';
import { take } from 'rxjs';

describe('set', () => {
let store: ReturnType<typeof createEntitiesStore>;
Expand All @@ -18,7 +19,12 @@ describe('set', () => {
it('should set entities', () => {
store.update(setEntities([createTodo(1)]));
toMatchSnapshot(expect, store, 'set one');

store.events$.pipe(take(1)).subscribe((action) => {
expect(action).toMatchObject({
type: 'set',
ids: [2],
});
});
store.update(setEntities([createTodo(2)]));
toMatchSnapshot(expect, store, 'set one');
});
Expand All @@ -27,14 +33,14 @@ describe('set', () => {
store.update(
setEntitiesMap({
1: createTodo(1),
})
}),
);
toMatchSnapshot(expect, store, 'set one');

store.update(
setEntitiesMap({
2: createTodo(2),
})
}),
);
toMatchSnapshot(expect, store, 'set one');
});
Expand Down
14 changes: 8 additions & 6 deletions packages/entities/src/lib/set.mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,21 @@ import { buildEntities } from './entity.utils';
*/
export function setEntities<
S extends EntitiesState<Ref>,
Ref extends EntitiesRef = DefaultEntitiesRef
Ref extends EntitiesRef = DefaultEntitiesRef,
>(
entities: getEntityType<S, Ref>[],
options: BaseEntityOptions<Ref> = {}
options: BaseEntityOptions<Ref> = {},
): Reducer<S> {
return function (state, context) {
return function (state, ctx) {
const { ref = defaultEntitiesRef } = options;
const { entitiesKey, idsKey } = ref!;
const { ids, asObject } = buildEntities<S, Ref>(
entities,
getIdKey<getIdType<S, Ref>>(context, ref)
getIdKey<getIdType<S, Ref>>(ctx, ref),
);

ctx.setEvent({ type: 'set', ids });

return {
...state,
[entitiesKey]: asObject,
Expand All @@ -45,10 +47,10 @@ export function setEntities<

export function setEntitiesMap<
S extends EntitiesState<Ref>,
Ref extends EntitiesRef = DefaultEntitiesRef
Ref extends EntitiesRef = DefaultEntitiesRef,
>(
entities: Record<any, getEntityType<S, Ref>>,
options: BaseEntityOptions<Ref> = {}
options: BaseEntityOptions<Ref> = {},
) {
return setEntities(Object.values(entities), options);
}
Loading

0 comments on commit e05d8ff

Please sign in to comment.