diff --git a/docs/entity/README.md b/docs/entity/README.md new file mode 100644 index 0000000000..1780c537fd --- /dev/null +++ b/docs/entity/README.md @@ -0,0 +1,24 @@ +# @ngrx/entity + +Entity State adapter for managing record collections. + +@ngrx/entity provides an API to manipulate and query entity collections. + +- Reduces boilerplate for managing common datasets +- Provides performant operations for managing entity collections +- Extensible type-safe adapters for selecting entity information + +### Installation +Install @ngrx/entity from npm: + +`npm install @ngrx/entity --save` OR `yarn add @ngrx/entity` + + +### Nightly builds + +`npm install github:ngrx/entity-builds` OR `yarn add github:ngrx/entity-builds` + +## API Documentation +- [Interfaces](./interfaces.md) +- [Entity Adapter](./adapter.md) +- [Selectors]('./adapter.md#entity-selectors) diff --git a/docs/entity/adapter.md b/docs/entity/adapter.md new file mode 100644 index 0000000000..2a247bbbab --- /dev/null +++ b/docs/entity/adapter.md @@ -0,0 +1,297 @@ +# Entity Adapter + +## createEntityAdapter + +A method for returning a generic entity adapter for a single entity state collection. The +returned adapter provides many [methods](#adapter-methods) for performing operations +against the collection type. The method takes an object for configuration with 2 properties. + + - `selectId`: A `method` for selecting the primary id for the collection + - `sort`: A [sort function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) for sorting the collection. Set to `false` to leave collection unsorted. + +Usage: + +```ts +import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity'; + +export interface User { + id: number; + name: string; + description: string; +} + +export interface State extends EntityState { + // additional entities state properties + selectedUserId: number; +} + +export function sortByName(a: User, b: User): number { + return a.name.localeCompare(b.name); +} + +export const adapter: EntityAdapter = createEntityAdapter({ + selectId: (user: User) => user.id, + sort: sortByName, +}); +``` + +## Adapter Methods + +These methods are provided by the adapter object returned +when using [createEntityAdapter](#createEntityAdapter). The methods are used inside your reducer function to manage +the entity collection based on your provided actions. + +### getInitialState + +Returns the `initialState` for entity state based on the provided type. Additional state is also provided through the provided configuration object. The initialState is provided to your reducer function. + +Usage: + +```ts +import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity'; + +export interface User { + id: number; + name: string; + description: string; +} + +export interface State extends EntityState { + // additional entities state properties + selectedUserId: number | null; +} + +export const initialState: State = adapter.getInitialState({ + // additional entity state properties + selectedUserId: null +}); + +export function reducer(state = initialState, action): State { + switch(action.type) { + default: { + return state; + } + } +} +``` + +## Adapter Collection Methods + +The entity adapter also provides methods for operations against an entity. These methods can change +one to many records at a time. Each method returns the newly modified state if changes were made and the same +state if no changes were made. + +* `addOne`: Add one entity to the collection +* `addMany`: Add multiple entities to the collection +* `addAll`: Replace current collection with provided collection +* `removeOne`: Remove one entity to the collection +* `removeMany`: Remove multiple entities to the collection +* `removeAll`: Clear entity collection +* `updateOne`: Update one entity in the collection +* `updateMany`: Update multiple entities in the collection + +Usage: + +`user.model.ts` + +```ts +export interface User { + id: number; + name: string; + description: string; +} +``` + +`user.actions.ts` + +```ts +import { Action } from '@ngrx/action'; +import { User } from './user.model'; + +export const LOAD_USERS = '[User] Load Users'; +export const ADD_USER = '[User] Add User'; +export const ADD_USERS = '[User] Add Users'; +export const UPDATE_USER = '[User] Update User'; +export const UPDATE_USERS = '[User] Update Users'; +export const DELETE_USER = '[User] Delete User'; +export const DELETE_USERS = '[User] Delete Users'; +export const CLEAR_USERS = '[User] Clear Users'; + +export class LoadUsers implements Action { + readonly type = LOAD_USERS; + + constructor(public payload: { users: User[] }) {} +} + +export class AddUser implements Action { + readonly type = ADD_USER; + + constructor(public payload: { user: User }) {} +} + +export class AddUsers implements Action { + readonly type = ADD_USERS; + + constructor(public payload: { users: User[] }) {} +} + +export class UpdateUser implements Action { + readonly type = UPDATE_USER; + + constructor(public payload: { user: User }) {} +} + +export class UpdateUsers implements Action { + readonly type = UPDATE_USERS; + + constructor(public payload: { users: User[] }) {} +} + +export class DeleteUser implements Action { + readonly type = DELETE_USER; + + constructor(public payload: { user: User }) {} +} + +export class DeleteUsers implements Action { + readonly type = DELETE_USERS; + + constructor(public payload: { users: User[] }) {} +} + +export class ClearUsers implements Action { + readonly type = CLEAR_USERS; +} + +export type All = + LoadUsers + | AddUser + | AddUsers + | UpdateUser + | UpdateUsers + | DeleteUser + | DeleteUsers + | ClearUsers; +``` + +`user.reducer.ts` +```ts +import * as user from './user.actions'; + +export interface State extends EntityState { + // additional entities state properties + selectedUserId: number | null; +} + +export const adapter: EntityAdapter = createEntityAdapter({ + selectId: (user: User) => user.id, + sort: true, +}); + +export const initialState: State = adapter.getInitialState({ + // additional entity state properties + selectedUserId: null +}); + +export function reducer( + state = initialState, + action: UserActions.All +): State { + switch (action.type) { + case user.ADD_USER: { + return { + ...state, + ...adapter.addOne(action.payload.user, state), + }; + } + + case user.ADD_USERS: { + return { + ...state, + ...adapter.addMany(action.payload.users, state), + }; + } + + case user.UPDATE_USER: { + return { + ...state, + ...adapter.updateOne(action.payload.user, state), + }; + } + + case user.UPDATE_USERS: { + return { + ...state, + ...adapter.updateMany(action.payload.users, state), + }; + } + + case user.LOAD_USERS: { + return { + ...state, + ...adapter.addAll(action.payload.users, state), + }; + } + + case user.CLEAR_USERS: { + return { + ...adapter.removeAll(state), + selectedUserId: null + }; + } + + default: { + return state; + } + } +} + +export const getSelectedUserId = (state: State) => state.selectedUserId; +``` + +### Entity Selectors + +The `getSelectors` method returned by the created entity adapter provides functions for selecting information from the entity. + +The `getSelectors` method takes a selector function +as its only argument to select the piece of state for a defined entity. + +Usage: + +`reducers/index.ts` + +```ts +import { createSelector, createFeatureSelector } from '@ngrx/store'; +import * as fromUser from './user.reducer'; + +export interface State { + users: fromUser.State; +} + +export const selectUserState = createFeatureSelector('users'); + +export const { + // select the array of user ids + selectIds: getUserIds, + + // select the dictionary of user entities + selectEntities: getUserEntities, + + // select the array of users + selectAll: getAllUsers, + + // select the total user count + selectTotal: getUserTotal +} = fromUser.adapter.getSelectors(selectUserState); + +export const selectUserIds = createSelector(selectUserState, getUserIds); +export const selectUserEntities = createSelector(selectUserState, getUserEntities); +export const selectAllUsers = createSelector(selectUserState, getAllUsers); +export const selectUserCount = createSelector(selectUserState, getUserTotal); +export const selectCurrentUserId = createSelector(selectUserState, fromUser.getSelectedUserId); +export const selectCurrentUser = createSelector( + selectUserEntities, + selectCurrentUserId, + (userEntities, userId) => userEntities[userId] +); +``` diff --git a/docs/entity/interfaces.md b/docs/entity/interfaces.md new file mode 100644 index 0000000000..55294543ea --- /dev/null +++ b/docs/entity/interfaces.md @@ -0,0 +1,37 @@ +# Entity Interfaces + +## EntityState + +The Entity State is a predefined generic interface for a given entity collection with the following properties: + + * `ids`: An array of all the primary ids in the collection + * `entities`: A dictionary of entities in the collection indexed by the primary id + + Extend this interface to provided any additional properties for the entity state. + + Usage: + + ```ts + export interface User { + id: number; + name: string; + description: string; +} + +export interface State extends EntityState { + // additional entity state properties + selectedUserId: string | null; +} +``` + +## EntityAdapter + +Provides a generic type interface for the provided [entity adapter](./adapter#createEntityAdapter). The entity adapter provides many [collection methods](./adapter.md#adapter-collection-methods) for managing the entity state. + +Usage: + +```ts +export const adapter: EntityAdapter = createEntityAdapter({ + selectId: (user: User) => user.id +}); +``` diff --git a/example-app/app/books/models/book.ts b/example-app/app/books/models/book.ts index 70cf4e623a..c8b067ac12 100644 --- a/example-app/app/books/models/book.ts +++ b/example-app/app/books/models/book.ts @@ -15,3 +15,23 @@ export interface Book { }; }; } + +export function generateMockBook(): Book { + return { + id: '1', + volumeInfo: { + title: 'title', + subtitle: 'subtitle', + authors: ['author'], + publisher: 'publisher', + publishDate: '', + description: 'description', + averageRating: 3, + ratingsCount: 5, + imageLinks: { + thumbnail: 'string', + smallThumbnail: 'string', + }, + }, + }; +} diff --git a/example-app/app/books/reducers/book.spec.ts b/example-app/app/books/reducers/book.spec.ts deleted file mode 100644 index 45650f4119..0000000000 --- a/example-app/app/books/reducers/book.spec.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { reducer } from './books'; -import * as fromBooks from './books'; -import { SearchComplete, Load, Select } from '../actions/book'; -import { Book } from '../models/book'; -import { LoadSuccess } from '../actions/collection'; - -describe('BooksReducer', () => { - describe('undefined action', () => { - it('should return the default state', () => { - const action = {} as any; - - const result = reducer(undefined, action); - expect(result).toEqual(fromBooks.initialState); - }); - }); - - describe('SEARCH_COMPLETE & LOAD_SUCCESS', () => { - function noExistingBooks(action: any) { - const book1 = { id: '111' } as Book; - const book2 = { id: '222' } as Book; - const createAction = new action([book1, book2]); - - const expectedResult = { - ids: ['111', '222'], - entities: { - '111': book1, - '222': book2, - }, - selectedBookId: null, - }; - - const result = reducer(fromBooks.initialState, createAction); - expect(result).toEqual(expectedResult); - } - - function existingBooks(action: any) { - const book1 = { id: '111' } as Book; - const book2 = { id: '222' } as Book; - const initialState = { - ids: ['111', '222'], - entities: { - '111': book1, - '222': book2, - }, - selectedBookId: null, - } as any; - // should not replace existing books - const differentBook2 = { id: '222', foo: 'bar' } as any; - const book3 = { id: '333' } as Book; - const createAction = new action([book3, differentBook2]); - - const expectedResult = { - ids: ['111', '222', '333'], - entities: { - '111': book1, - '222': book2, - '333': book3, - }, - selectedBookId: null, - }; - - const result = reducer(initialState, createAction); - expect(result).toEqual(expectedResult); - } - - it('should add all books in the payload when none exist', () => { - noExistingBooks(SearchComplete); - noExistingBooks(LoadSuccess); - }); - - it('should add only new books when books already exist', () => { - existingBooks(SearchComplete); - existingBooks(LoadSuccess); - }); - }); - - describe('LOAD', () => { - it('should add a single book, if the book does not exist', () => { - const book = { id: '888' } as Book; - const action = new Load(book); - - const expectedResult = { - ids: ['888'], - entities: { - '888': book, - }, - selectedBookId: null, - }; - - const result = reducer(fromBooks.initialState, action); - expect(result).toEqual(expectedResult); - }); - - it('should return the existing state if the book exists', () => { - const initialState = { - ids: ['999'], - entities: { - '999': { id: '999' }, - }, - } as any; - const book = { id: '999', foo: 'baz' } as any; - const action = new Load(book); - - const result = reducer(initialState, action); - expect(result).toEqual(initialState); - }); - }); - - describe('SELECT', () => { - it('should set the selected book id on the state', () => { - const action = new Select('1'); - - const result = reducer(fromBooks.initialState, action); - expect(result.selectedBookId).toBe('1'); - }); - }); - - describe('Selections', () => { - const book1 = { id: '111' } as Book; - const book2 = { id: '222' } as Book; - const state: fromBooks.State = { - ids: ['111', '222'], - entities: { - '111': book1, - '222': book2, - }, - selectedBookId: '111', - }; - - describe('getEntities', () => { - it('should return entities', () => { - const result = fromBooks.getEntities(state); - expect(result).toBe(state.entities); - }); - }); - - describe('getIds', () => { - it('should return ids', () => { - const result = fromBooks.getIds(state); - expect(result).toBe(state.ids); - }); - }); - - describe('getSelectedId', () => { - it('should return the selected id', () => { - const result = fromBooks.getSelectedId(state); - expect(result).toBe('111'); - }); - }); - - describe('getSelected', () => { - it('should return the selected book', () => { - const result = fromBooks.getSelected(state); - expect(result).toBe(book1); - }); - }); - - describe('getAll', () => { - it('should return all books as an array ', () => { - const result = fromBooks.getAll(state); - expect(result).toEqual([book1, book2]); - }); - }); - }); -}); diff --git a/example-app/app/books/reducers/books.spec.ts b/example-app/app/books/reducers/books.spec.ts new file mode 100644 index 0000000000..47560daa80 --- /dev/null +++ b/example-app/app/books/reducers/books.spec.ts @@ -0,0 +1,127 @@ +import { reducer } from './books'; +import * as fromBooks from './books'; +import { SearchComplete, Load, Select } from '../actions/book'; +import { Book, generateMockBook } from '../models/book'; +import { LoadSuccess } from '../actions/collection'; + +describe('BooksReducer', () => { + const book1 = generateMockBook(); + const book2 = { ...book1, id: '222' }; + const book3 = { ...book1, id: '333' }; + const initialState: fromBooks.State = { + ids: [book1.id, book2.id], + entities: { + [book1.id]: book1, + [book2.id]: book2, + }, + selectedBookId: null, + }; + + describe('undefined action', () => { + it('should return the default state', () => { + const result = reducer(undefined, {} as any); + + expect(result).toEqual(fromBooks.initialState); + }); + }); + + describe('SEARCH_COMPLETE & LOAD_SUCCESS', () => { + function noExistingBooks( + action: any, + booksInitialState: any, + initialState: any, + books: Book[] + ) { + const createAction = new action(books); + + const result = reducer(booksInitialState, createAction); + + expect(result).toEqual(initialState); + } + + function existingBooks(action: any, initialState: any, books: Book[]) { + // should not replace existing books + const differentBook2 = { ...books[0], foo: 'bar' }; + const createAction = new action([books[1], differentBook2]); + + const expectedResult = { + ids: [...initialState.ids, books[1].id], + entities: { + ...initialState.entities, + [books[1].id]: books[1], + }, + selectedBookId: null, + }; + + const result = reducer(initialState, createAction); + expect(result).toEqual(expectedResult); + } + + it('should add all books in the payload when none exist', () => { + noExistingBooks(SearchComplete, fromBooks.initialState, initialState, [ + book1, + book2, + ]); + + noExistingBooks(LoadSuccess, fromBooks.initialState, initialState, [ + book1, + book2, + ]); + }); + + it('should add only new books when books already exist', () => { + existingBooks(SearchComplete, initialState, [book2, book3]); + + existingBooks(LoadSuccess, initialState, [book2, book3]); + }); + }); + + describe('LOAD', () => { + const expectedResult = { + ids: [book1.id], + entities: { + [book1.id]: book1, + }, + selectedBookId: null, + }; + + it('should add a single book, if the book does not exist', () => { + const action = new Load(book1); + + const result = reducer(fromBooks.initialState, action); + + expect(result).toEqual(expectedResult); + }); + + it('should return the existing state if the book exists', () => { + const action = new Load(book1); + + const result = reducer(expectedResult, action); + + expect(result).toEqual(expectedResult); + }); + }); + + describe('SELECT', () => { + it('should set the selected book id on the state', () => { + const action = new Select(book1.id); + + const result = reducer(initialState, action); + + expect(result.selectedBookId).toBe(book1.id); + }); + }); + + describe('Selectors', () => { + describe('getSelectedId', () => { + it('should return the selected id', () => { + const result = fromBooks.getSelectedId({ + ...initialState, + selectedBookId: book1.id, + }); + + expect(result).toBe(book1.id); + }); + }); + }); +}); diff --git a/example-app/app/books/reducers/books.ts b/example-app/app/books/reducers/books.ts index b1a91c9fea..589ba961fd 100644 --- a/example-app/app/books/reducers/books.ts +++ b/example-app/app/books/reducers/books.ts @@ -1,19 +1,40 @@ import { createSelector } from '@ngrx/store'; +import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity'; import { Book } from '../models/book'; import * as book from '../actions/book'; import * as collection from '../actions/collection'; -export interface State { - ids: string[]; - entities: { [id: string]: Book }; +/** + * @ngrx/entity provides a predefined interface for handling + * a structured dictionary of records. This interface + * includes an array of ids, and a dictionary of the provided + * model type by id. This interface is extended to include + * any additional interface properties. + */ +export interface State extends EntityState { selectedBookId: string | null; } -export const initialState: State = { - ids: [], - entities: {}, +/** + * createEntityAdapter creates many an object of helper + * functions for single or multiple operations + * against the dictionary of records. The configuration + * object takes a record id selector function and + * a sort option whether to sort the records when performing + * operations + */ +export const adapter: EntityAdapter = createEntityAdapter({ + selectId: (book: Book) => book.id, + sort: false, +}); + +/** getInitialState returns the default initial state + * for the generated entity state. Initial state + * additional properties can also be defined. +*/ +export const initialState: State = adapter.getInitialState({ selectedBookId: null, -}; +}); export function reducer( state = initialState, @@ -22,46 +43,36 @@ export function reducer( switch (action.type) { case book.SEARCH_COMPLETE: case collection.LOAD_SUCCESS: { - const books = action.payload; - const newBooks = books.filter(book => !state.entities[book.id]); - - const newBookIds = newBooks.map(book => book.id); - const newBookEntities = newBooks.reduce( - (entities: { [id: string]: Book }, book: Book) => { - return Object.assign(entities, { - [book.id]: book, - }); - }, - {} - ); - return { - ids: [...state.ids, ...newBookIds], - entities: Object.assign({}, state.entities, newBookEntities), + /** + * The addMany function provided by the created adapter + * adds many records to the entity dictionary + * and returns a new state including those records. If + * the collection is to be sorted, the adapter will + * sort each record upon entry into the sorted array. + */ + ...adapter.addMany(action.payload, state), selectedBookId: state.selectedBookId, }; } case book.LOAD: { - const book = action.payload; - - if (state.ids.indexOf(book.id) > -1) { - return state; - } - return { - ids: [...state.ids, book.id], - entities: Object.assign({}, state.entities, { - [book.id]: book, - }), + /** + * The addOne function provided by the created adapter + * adds one record to the entity dictionary + * and returns a new state including that records if it doesn't + * exist already. If the collection is to be sorted, the adapter will + * insert the new record into the sorted array. + */ + ...adapter.addOne(action.payload, state), selectedBookId: state.selectedBookId, }; } case book.SELECT: { return { - ids: state.ids, - entities: state.entities, + ...state, selectedBookId: action.payload, }; } @@ -81,20 +92,4 @@ export function reducer( * use-case. */ -export const getEntities = (state: State) => state.entities; - -export const getIds = (state: State) => state.ids; - export const getSelectedId = (state: State) => state.selectedBookId; - -export const getSelected = createSelector( - getEntities, - getSelectedId, - (entities, selectedId) => { - return entities[selectedId]; - } -); - -export const getAll = createSelector(getEntities, getIds, (entities, ids) => { - return ids.map(id => entities[id]); -}); diff --git a/example-app/app/books/reducers/collection.ts b/example-app/app/books/reducers/collection.ts index f8f65f74b1..cb2d64eb4c 100644 --- a/example-app/app/books/reducers/collection.ts +++ b/example-app/app/books/reducers/collection.ts @@ -18,41 +18,38 @@ export function reducer( ): State { switch (action.type) { case collection.LOAD: { - return Object.assign({}, state, { + return { + ...state, loading: true, - }); + }; } case collection.LOAD_SUCCESS: { - const books = action.payload; - return { loaded: true, loading: false, - ids: books.map(book => book.id), + ids: action.payload.map(book => book.id), }; } case collection.ADD_BOOK_SUCCESS: case collection.REMOVE_BOOK_FAIL: { - const book = action.payload; - - if (state.ids.indexOf(book.id) > -1) { + if (state.ids.indexOf(action.payload.id) > -1) { return state; } - return Object.assign({}, state, { - ids: [...state.ids, book.id], - }); + return { + ...state, + ids: [...state.ids, action.payload.id], + }; } case collection.REMOVE_BOOK_SUCCESS: case collection.ADD_BOOK_FAIL: { - const book = action.payload; - - return Object.assign({}, state, { - ids: state.ids.filter(id => id !== book.id), - }); + return { + ...state, + ids: state.ids.filter(id => id !== action.payload.id), + }; } default: { diff --git a/example-app/app/books/reducers/index.ts b/example-app/app/books/reducers/index.ts index c273977c6b..3ca7970199 100644 --- a/example-app/app/books/reducers/index.ts +++ b/example-app/app/books/reducers/index.ts @@ -53,23 +53,35 @@ export const getBooksState = createFeatureSelector('books'); */ export const getBookEntitiesState = createSelector( getBooksState, - (state: BooksState) => state.books -); -export const getBookEntities = createSelector( - getBookEntitiesState, - fromBooks.getEntities -); -export const getBookIds = createSelector( - getBookEntitiesState, - fromBooks.getIds + state => state.books ); + export const getSelectedBookId = createSelector( getBookEntitiesState, fromBooks.getSelectedId ); + +/** + * Adapters created with @ngrx/entity generate + * commonly used selector functions including + * getting all ids in the record set, a dictionary + * of the records by id, an array of records and + * the total number of records. This reducers boilerplate + * in selecting records from the entity state. + */ +export const { + selectIds: getBookIds, + selectEntities: getBookEntities, + selectAll: getAllBooks, + selectTotal: getTotalBooks, +} = fromBooks.adapter.getSelectors(getBookEntitiesState); + export const getSelectedBook = createSelector( - getBookEntitiesState, - fromBooks.getSelected + getBookEntities, + getSelectedBookId, + (entities, selectedId) => { + return selectedId && entities[selectedId]; + } ); /** diff --git a/example-app/app/books/reducers/search.ts b/example-app/app/books/reducers/search.ts index 3840bde324..381285fd27 100644 --- a/example-app/app/books/reducers/search.ts +++ b/example-app/app/books/reducers/search.ts @@ -25,17 +25,16 @@ export function reducer(state = initialState, action: book.Actions): State { }; } - return Object.assign({}, state, { + return { + ...state, query, loading: true, - }); + }; } case book.SEARCH_COMPLETE: { - const books = action.payload; - return { - ids: books.map(book => book.id), + ids: action.payload.map(book => book.id), loading: false, query: state.query, }; diff --git a/example-app/tsconfig.app.json b/example-app/tsconfig.app.json index ee93846bfe..ef30cf72c9 100644 --- a/example-app/tsconfig.app.json +++ b/example-app/tsconfig.app.json @@ -17,9 +17,11 @@ "rootDir": "../", "paths": { "@ngrx/effects": ["../dist/effects"], + "@ngrx/effects/testing": ["../dist/effects/testing"], "@ngrx/store": ["../dist/store"], "@ngrx/router-store": ["../dist/router-store"], - "@ngrx/store-devtools": ["../dist/store-devtools"] + "@ngrx/store-devtools": ["../dist/store-devtools"], + "@ngrx/entity": ["../dist/entity"] } }, "exclude": [ diff --git a/example-app/tsconfig.spec.json b/example-app/tsconfig.spec.json index e4753f0407..19a8a96901 100644 --- a/example-app/tsconfig.spec.json +++ b/example-app/tsconfig.spec.json @@ -20,9 +20,11 @@ "rootDir": "../", "paths": { "@ngrx/effects": ["../modules/effects"], + "@ngrx/effects/testing": ["../modules/effects/testing"], "@ngrx/store": ["../modules/store"], "@ngrx/router-store": ["../modules/router-store"], - "@ngrx/store-devtools": ["../modules/store-devtools"] + "@ngrx/store-devtools": ["../modules/store-devtools"], + "@ngrx/entity": ["../modules/entity"] } }, "files": [ diff --git a/tsconfig.json b/tsconfig.json index 5bb3e217cd..b10bbc7884 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,6 +28,9 @@ ], "@ngrx/router-store": [ "./modules/router-store" + ], + "@ngrx/entity": [ + "./modules/entity" ] } },