Skip to content

Commit

Permalink
feat: transformation to anything
Browse files Browse the repository at this point in the history
closes #7
  • Loading branch information
satanTime committed Apr 12, 2020
1 parent cd3bc39 commit 8bd859c
Show file tree
Hide file tree
Showing 15 changed files with 156 additions and 47 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,12 +238,16 @@ const selector3 = {
`selector` is a selector that works with a root entity.

`transformer` is an optional function that can be useful when we need a
post processing transformation, for example to a call instance.
post processing transformation, for example to a class instance, actually an entity can be transformed to anything.
```typescript
rootEntity(
const userClassInstance = rootEntity(
selector,
entity => plainToClass(UserClass, entity),
);
const userJsonString = rootEntity(
selector,
entity => JSON.stringify(entity),
);
```

`relationships` is an optional argument that is produced by a relationship function.
Expand Down
1 change: 1 addition & 0 deletions e2e/angular8/src/app/data/selector.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export class SelectorService {

public readonly selectHeroWithVillainShort = rootEntity(
this.hero,
() => 'transformed:hero',
relatedEntity(this.serviceFactory.create<Villain>('Villain'), 'villainId', 'villain'),
);

Expand Down
4 changes: 2 additions & 2 deletions e2e/angular8/src/app/store/user/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const transformedUser = rootEntitySelector({collection: selectUserState, id: ada
cloned: true,
}));
const transformedUserCompany = relatedEntitySelector(selectCompanyState, 'companyId', 'company');
const transformedCompany = rootEntitySelector(selectCompanyState, entity => ({...entity, cloned: true}));
const transformedCompany = rootEntitySelector(selectCompanyState, () => 'transformed:company');
const transformedCompanyStaff = childrenEntitiesSelector(selectUserState, 'companyId', 'staff');
const transformedCompanyAdmin = relatedEntitySelector(selectUserState, 'adminId', 'admin');
const transformedCompanyAddress = relatedEntitySelector(selectAddressState, 'addressId', 'address');
Expand Down Expand Up @@ -69,7 +69,7 @@ export const selectCompleteUser = rootEntity(

export const selectTransformedUser = rootEntity(
selectUserState,
entity => ({...entity, cloned: true}),
() => 'transformed:user',
relatedEntity(
selectCompanyState,
'companyId',
Expand Down
1 change: 1 addition & 0 deletions e2e/angular9/src/app/data/selector.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export class SelectorService {

public readonly selectHeroWithVillainShort = rootEntity(
this.hero,
() => 'transformed:hero',
relatedEntity(this.serviceFactory.create<Villain>('Villain'), 'villainId', 'villain'),
);

Expand Down
4 changes: 2 additions & 2 deletions e2e/angular9/src/app/store/user/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const transformedUser = rootEntitySelector({collection: selectUserState, id: ada
cloned: true,
}));
const transformedUserCompany = relatedEntitySelector(selectCompanyState, 'companyId', 'company');
const transformedCompany = rootEntitySelector(selectCompanyState, entity => ({...entity, cloned: true}));
const transformedCompany = rootEntitySelector(selectCompanyState, () => 'transformed:company');
const transformedCompanyStaff = childrenEntitiesSelector(selectUserState, 'companyId', 'staff');
const transformedCompanyAdmin = relatedEntitySelector(selectUserState, 'adminId', 'admin');
const transformedCompanyAddress = relatedEntitySelector(selectAddressState, 'addressId', 'address');
Expand Down Expand Up @@ -69,7 +69,7 @@ export const selectCompleteUser = rootEntity(

export const selectTransformedUser = rootEntity(
selectUserState,
entity => ({...entity, cloned: true}),
() => 'transformed:user',
relatedEntity(
selectCompanyState,
'companyId',
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"e2e:a9": "cd e2e/angular9 && npm run e2e",
"e2e:a": "npm run e2e:a6 && npm run e2e:a7 && npm run e2e:a8 && npm run e2e:a9",
"e2e": "npm run i:a && npm run s:a && npm run e2e:a",
"lint": "tslint --config ./tslint.yaml -p ./tsconfig.json",
"lint": "tsc -p ./tsconfig.json --noEmit && tsc -p ./tsconfig.spec.json --noEmit && tslint --config ./tslint.yaml -p ./tsconfig.json",
"release": "standard-version",
"test": "karma start",
"test:debug": "npm test -- --no-single-run --auto-watch --browsers=Chrome"
Expand Down
20 changes: 15 additions & 5 deletions src/operators/relationships.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,28 @@ import {HANDLER_ROOT_ENTITIES, HANDLER_ROOT_ENTITY, ID_TYPES, STORE_INSTANCE} fr

export function relationships<STORE, ENTITY>(
store: STORE_INSTANCE<STORE>,
selector: HANDLER_ROOT_ENTITIES<STORE, ENTITY, ID_TYPES>,
selector: HANDLER_ROOT_ENTITIES<STORE, ENTITY, ENTITY, ID_TYPES>,
): (next: Observable<Array<ENTITY>>) => Observable<Array<ENTITY>>;

export function relationships<STORE, ENTITY, TRANSFORMED>(
store: STORE_INSTANCE<STORE>,
selector: HANDLER_ROOT_ENTITIES<STORE, ENTITY, TRANSFORMED, ID_TYPES>,
): (next: Observable<Array<ENTITY>>) => Observable<Array<TRANSFORMED>>;

export function relationships<STORE, ENTITY>(
store: STORE_INSTANCE<STORE>,
selector: HANDLER_ROOT_ENTITY<STORE, ENTITY, ID_TYPES>,
selector: HANDLER_ROOT_ENTITY<STORE, ENTITY, ENTITY, ID_TYPES>,
): (next: Observable<ENTITY>) => Observable<ENTITY>;

export function relationships<STORE, SET, TYPES>(
export function relationships<STORE, ENTITY, TRANSFORMED>(
store: STORE_INSTANCE<STORE>,
selector: HANDLER_ROOT_ENTITY<STORE, ENTITY, TRANSFORMED, ID_TYPES>,
): (next: Observable<ENTITY>) => Observable<TRANSFORMED>;

export function relationships<STORE, SET, TRANSFORMED, TYPES>(
store: STORE_INSTANCE<STORE>,
selector: HANDLER_ROOT_ENTITY<STORE, SET, TYPES>,
): (next: Observable<SET>) => Observable<undefined | SET> {
selector: HANDLER_ROOT_ENTITY<STORE, SET, SET | TRANSFORMED, TYPES>,
): (next: Observable<SET>) => Observable<undefined | SET | TRANSFORMED> {
return next =>
next.pipe(
switchMap(input => {
Expand Down
18 changes: 13 additions & 5 deletions src/rootEntities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,18 @@ import {rootEntityFlags} from './rootEntityFlags';
import {HANDLER_ROOT_ENTITIES, HANDLER_ROOT_ENTITY, ID_TYPES} from './types';

export function rootEntities<STORE, ENTITY>(
rootSelector: HANDLER_ROOT_ENTITY<STORE, ENTITY, ID_TYPES>,
): HANDLER_ROOT_ENTITIES<STORE, ENTITY, ID_TYPES> {
const cacheMap = new Map<string, Array<ENTITY>>();
const emptyResult: Array<ENTITY> = [];
rootSelector: HANDLER_ROOT_ENTITY<STORE, ENTITY, ENTITY, ID_TYPES>,
): HANDLER_ROOT_ENTITIES<STORE, ENTITY, ENTITY, ID_TYPES>;

export function rootEntities<STORE, ENTITY, TRANSFORMED>(
rootSelector: HANDLER_ROOT_ENTITY<STORE, ENTITY, TRANSFORMED, ID_TYPES>,
): HANDLER_ROOT_ENTITIES<STORE, ENTITY, TRANSFORMED, ID_TYPES>;

export function rootEntities<STORE, ENTITY, TRANSFORMED>(
rootSelector: HANDLER_ROOT_ENTITY<STORE, ENTITY, TRANSFORMED, ID_TYPES>,
): HANDLER_ROOT_ENTITIES<STORE, ENTITY, ENTITY | TRANSFORMED, ID_TYPES> {
const cacheMap = new Map<string, Array<ENTITY | TRANSFORMED>>();
const emptyResult: Array<ENTITY | TRANSFORMED> = [];

const callback = (state: STORE, ids: undefined | Array<ID_TYPES>) => {
if (!ids) {
Expand All @@ -19,7 +27,7 @@ export function rootEntities<STORE, ENTITY>(
return cacheValue;
}

const value: Array<ENTITY> = [];
const value: Array<ENTITY | TRANSFORMED> = [];
for (const itemId of ids) {
const item = rootSelector(state, itemId);
if (!item) {
Expand Down
18 changes: 9 additions & 9 deletions src/rootEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,18 @@ import {normalizeSelector} from './utils';
export function rootEntity<STORE, ENTITY>(
featureSelector: FEATURE_SELECTOR<STORE, ENTITY>,
...relations: Array<HANDLER_RELATED_ENTITY<STORE, ENTITY>>
): HANDLER_ROOT_ENTITY<STORE, ENTITY, ID_TYPES>;
export function rootEntity<STORE, ENTITY>(
): HANDLER_ROOT_ENTITY<STORE, ENTITY, ENTITY, ID_TYPES>;
export function rootEntity<STORE, ENTITY, TRANSFORMED>(
featureSelector: FEATURE_SELECTOR<STORE, ENTITY>,
transformer: TRANSFORMER<ENTITY>,
transformer: TRANSFORMER<ENTITY, TRANSFORMED>,
...relations: Array<HANDLER_RELATED_ENTITY<STORE, ENTITY>>
): HANDLER_ROOT_ENTITY<STORE, ENTITY, ID_TYPES>;
export function rootEntity<STORE, ENTITY>(
): HANDLER_ROOT_ENTITY<STORE, ENTITY, TRANSFORMED, ID_TYPES>;
export function rootEntity<STORE, ENTITY, TRANSFORMED>(
featureSelector: FEATURE_SELECTOR<STORE, ENTITY>,
deside?: TRANSFORMER<ENTITY> | HANDLER_RELATED_ENTITY<STORE, ENTITY>,
deside?: TRANSFORMER<ENTITY, TRANSFORMED> | HANDLER_RELATED_ENTITY<STORE, ENTITY>,
...relationships: Array<HANDLER_RELATED_ENTITY<STORE, ENTITY>>
): HANDLER_ROOT_ENTITY<STORE, ENTITY, ID_TYPES> {
let transformer: undefined | TRANSFORMER<ENTITY>;
): HANDLER_ROOT_ENTITY<STORE, ENTITY, ENTITY | TRANSFORMED, ID_TYPES> {
let transformer: undefined | TRANSFORMER<ENTITY, TRANSFORMED>;
if (isBuiltInSelector<STORE, ENTITY>(deside)) {
relationships = [deside, ...relationships];
} else {
Expand All @@ -38,7 +38,7 @@ export function rootEntity<STORE, ENTITY>(
const callback = (state: STORE, id: ID_TYPES) => {
const cacheData = cacheMap.get(id);
let cacheRefs: HANDLER_CACHE<STORE, UNKNOWN> = [];
let cacheValue: undefined | ENTITY;
let cacheValue: undefined | TRANSFORMED | ENTITY;
if (cacheData && cacheData[0]) {
cacheRefs = cacheData[0];
}
Expand Down
17 changes: 15 additions & 2 deletions src/rootEntitySelector.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
import {rootEntity} from './rootEntity';
import {FEATURE_SELECTOR, HANDLER_RELATED_ENTITY, HANDLER_ROOT_ENTITY, ID_TYPES, TRANSFORMER} from './types';

export function rootEntitySelector<STORE, ENTITY, TRANSFORMED>(
featureSelector: FEATURE_SELECTOR<STORE, ENTITY>,
transformer: TRANSFORMER<ENTITY, TRANSFORMED>,
): (
...relations: Array<HANDLER_RELATED_ENTITY<STORE, ENTITY>>
) => HANDLER_ROOT_ENTITY<STORE, ENTITY, TRANSFORMED, ID_TYPES>;

export function rootEntitySelector<STORE, ENTITY>(
featureSelector: FEATURE_SELECTOR<STORE, ENTITY>,
transformer?: TRANSFORMER<ENTITY>,
): (...relations: Array<HANDLER_RELATED_ENTITY<STORE, ENTITY>>) => HANDLER_ROOT_ENTITY<STORE, ENTITY, ID_TYPES> {
): (...relations: Array<HANDLER_RELATED_ENTITY<STORE, ENTITY>>) => HANDLER_ROOT_ENTITY<STORE, ENTITY, ENTITY, ID_TYPES>;

export function rootEntitySelector<STORE, ENTITY, TRANSFORMED>(
featureSelector: FEATURE_SELECTOR<STORE, ENTITY>,
transformer?: TRANSFORMER<ENTITY, TRANSFORMED>,
): (
...relations: Array<HANDLER_RELATED_ENTITY<STORE, ENTITY>>
) => HANDLER_ROOT_ENTITY<STORE, ENTITY, ENTITY | TRANSFORMED, ID_TYPES> {
const callback = (...relations: Array<HANDLER_RELATED_ENTITY<STORE, ENTITY>>) =>
transformer
? rootEntity(featureSelector, transformer, ...relations)
Expand Down
14 changes: 7 additions & 7 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,16 @@ export type HANDLER_CACHE<S, E> = Array<
| [string, STORE_SELECTOR<S, EntityState<E>>, ID_TYPES | null, E, E]
>;

export type HANDLER_ROOT_ENTITY<S, E, I> = {
(state: S, id: I): undefined | E;
export type HANDLER_ROOT_ENTITY<S, F, T, I> = {
(state: S, id: I): undefined | T;
ngrxEntityRelationship: string;
idSelector: ID_SELECTOR<E>;
idSelector: ID_SELECTOR<F>;
};

export type HANDLER_ROOT_ENTITIES<S, E, I> = {
(state: S, id: Array<I>): Array<E>;
export type HANDLER_ROOT_ENTITIES<S, F, T, I> = {
(state: S, id: Array<I>): Array<T>;
ngrxEntityRelationship: string;
idSelector: ID_SELECTOR<E>;
idSelector: ID_SELECTOR<F>;
};

export type HANDLER_RELATED_ENTITY<S, E> = {
Expand All @@ -73,7 +73,7 @@ export type VALUES_FILTER_PROPS<PARENT_ENTITY, RELATED_ENTITY> = NonNullable<
FILTER_PROPS<PARENT_ENTITY, RELATED_ENTITY | EMPTY_TYPES>
>;

export type TRANSFORMER<T> = (entity: T) => T;
export type TRANSFORMER<F, T> = (entity: F) => T;

export const isBuiltInSelector = <S, E>(value: UNKNOWN): value is HANDLER_RELATED_ENTITY<S, E> => {
return value && value.ngrxEntityRelationship;
Expand Down
6 changes: 3 additions & 3 deletions test/operators/relationships.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe('operators/relationships', () => {
const store = {
select: jasmine.createSpy().and.returnValue(store$),
};
const selector: HANDLER_ROOT_ENTITY<UNKNOWN, Entity, UNKNOWN> = <any>jasmine.createSpy();
const selector: HANDLER_ROOT_ENTITY<UNKNOWN, Entity, Entity, UNKNOWN> = <any>jasmine.createSpy();
selector.ngrxEntityRelationship = 'spy';

of(undefined)
Expand All @@ -32,7 +32,7 @@ describe('operators/relationships', () => {
const store = {
select: jasmine.createSpy().and.returnValue(store$),
};
const selector: HANDLER_ROOT_ENTITY<UNKNOWN, Entity, UNKNOWN> = <any>jasmine.createSpy();
const selector: HANDLER_ROOT_ENTITY<UNKNOWN, Entity, Entity, UNKNOWN> = <any>jasmine.createSpy();
selector.ngrxEntityRelationship = 'spy';
selector.idSelector = jasmine.createSpy('idSelector').and.returnValue('hello');

Expand Down Expand Up @@ -60,7 +60,7 @@ describe('operators/relationships', () => {
const store = {
select: jasmine.createSpy().and.returnValue(store$),
};
const selector: HANDLER_ROOT_ENTITIES<UNKNOWN, Entity, UNKNOWN> = <any>jasmine.createSpy();
const selector: HANDLER_ROOT_ENTITIES<UNKNOWN, Entity, Entity, UNKNOWN> = <any>jasmine.createSpy();
selector.ngrxEntityRelationship = 'spy';
selector.idSelector = jasmine.createSpy('idSelector').and.returnValues('hello1', 'hello2');

Expand Down
32 changes: 26 additions & 6 deletions test/rootEntities.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {rootEntities, rootEntityFlags} from '../src';
import {HANDLER_ROOT_ENTITY, ID_TYPES} from '../src/types';
import {rootEntity} from '../src/rootEntity';
import {FEATURE_SELECTOR, HANDLER_ROOT_ENTITY, ID_TYPES} from '../src/types';

describe('rootEntities', () => {
type Entity = {
Expand All @@ -25,7 +26,7 @@ describe('rootEntities', () => {
});

it('returns the same empty array on no ids', () => {
const selectorRoot: HANDLER_ROOT_ENTITY<{}, Entity, ID_TYPES> & jasmine.Spy = <any>jasmine.createSpy();
const selectorRoot: HANDLER_ROOT_ENTITY<{}, Entity, Entity, ID_TYPES> & jasmine.Spy = <any>jasmine.createSpy();
const selector = rootEntities(selectorRoot);

const actual = selector({}, undefined);
Expand All @@ -34,7 +35,7 @@ describe('rootEntities', () => {
});

it('returns the same idSelector as rootEntity', () => {
const selectorRoot: HANDLER_ROOT_ENTITY<{}, Entity, ID_TYPES> & jasmine.Spy = <any>jasmine.createSpy();
const selectorRoot: HANDLER_ROOT_ENTITY<{}, Entity, Entity, ID_TYPES> & jasmine.Spy = <any>jasmine.createSpy();
selectorRoot.idSelector = <any>jasmine.createSpy();
const selector = rootEntities(selectorRoot);
expect(selector.idSelector).toBe(selectorRoot.idSelector);
Expand All @@ -46,7 +47,7 @@ describe('rootEntities', () => {

it('returns the cached value when rootEntityFlags.disabled is true', () => {
const state = {};
const selectorRoot: HANDLER_ROOT_ENTITY<typeof state, Entity, ID_TYPES> & jasmine.Spy = <any>(
const selectorRoot: HANDLER_ROOT_ENTITY<typeof state, Entity, Entity, ID_TYPES> & jasmine.Spy = <any>(
jasmine.createSpy()
);
const selector = rootEntities(selectorRoot);
Expand Down Expand Up @@ -103,7 +104,7 @@ describe('rootEntities', () => {

it('collects only existing entities', () => {
const state = {};
const selectorRoot: HANDLER_ROOT_ENTITY<typeof state, Entity, ID_TYPES> & jasmine.Spy = <any>(
const selectorRoot: HANDLER_ROOT_ENTITY<typeof state, Entity, Entity, ID_TYPES> & jasmine.Spy = <any>(
jasmine.createSpy()
);
const selector = rootEntities(selectorRoot);
Expand Down Expand Up @@ -139,7 +140,7 @@ describe('rootEntities', () => {

it('returns the cached value when entities have not been changed', () => {
const state = {};
const selectorRoot: HANDLER_ROOT_ENTITY<typeof state, Entity, ID_TYPES> & jasmine.Spy = <any>(
const selectorRoot: HANDLER_ROOT_ENTITY<typeof state, Entity, Entity, ID_TYPES> & jasmine.Spy = <any>(
jasmine.createSpy()
);
const selector = rootEntities(selectorRoot);
Expand All @@ -160,4 +161,23 @@ describe('rootEntities', () => {
const actual2 = selector(state, [1, 2]);
expect(actual2).toBe(actual1);
});

it('returns an array of transformed entities', () => {
const state = {};
const funcSelector: FEATURE_SELECTOR<typeof state, Entity> & jasmine.Spy = <any>jasmine.createSpy();
const selectorRoot = rootEntity(funcSelector, () => 'transformed');
const selector = rootEntities(selectorRoot);

const entity1 = Symbol();
const entity2 = Symbol();
funcSelector.and.returnValue({
entities: {
1: entity1,
2: entity2,
},
});

const actual = selector(state, [1, 2]);
expect(actual).toEqual(['transformed', 'transformed']);
});
});
26 changes: 24 additions & 2 deletions test/rootEntity.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ describe('rootEntity', () => {
const state = {
feature: createEntityAdapter<Entity>().getInitialState(),
};
const selector = rootEntity<typeof state, Entity>(
const selector = rootEntity<typeof state, Entity, Entity>(
v => v.feature,
entity => ({...entity, transformed: true}),
);
Expand Down Expand Up @@ -294,7 +294,7 @@ describe('rootEntity', () => {
rel.ngrxEntityRelationship = 'spy';
rel.and.callFake((_1, _2, _3, v) => (v.processed = true));

const selector = rootEntity<typeof state, Entity>(v => v.feature, transformer, rel);
const selector = rootEntity<typeof state, Entity, Entity>(v => v.feature, transformer, rel);

state.feature.entities = {
...state.feature.entities,
Expand All @@ -311,6 +311,28 @@ describe('rootEntity', () => {
);
});

it('uses transformer to a different type', () => {
const state = {
feature: createEntityAdapter<Entity>().getInitialState(),
};
const transformer = () => 'transformed';
const rel = <jasmine.Spy & HANDLER_RELATED_ENTITY<typeof state, Entity>>(<any>jasmine.createSpy());
rel.ngrxEntityRelationship = 'spy';
rel.and.callFake((_1, _2, _3, v) => (v.processed = true));

const selector = rootEntity<typeof state, Entity, string>(v => v.feature, transformer, rel);

state.feature.entities = {
...state.feature.entities,
id1: {
id: 'id1',
name: 'name1',
},
};
const actual = selector(state, 'id1');
expect(actual).toBe('transformed');
});

it('supports EntityCollectionService as a selector', () => {
const state = {
feature: createEntityAdapter<Entity>().getInitialState(),
Expand Down
Loading

0 comments on commit 8bd859c

Please sign in to comment.