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(data): add provideEntityData and provideEntityDataWithoutEffects #3647

Merged
merged 13 commits into from
Nov 7, 2022
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
Logger,
} from '../..';

// TODO fix these tests
markostanimirovic marked this conversation as resolved.
Show resolved Hide resolved
describe('EntityCollectionService', () => {
describe('Command dispatching', () => {
// Borrowing the dispatcher tests from entity-dispatcher.spec.
Expand Down Expand Up @@ -133,9 +134,16 @@ describe('EntityCollectionService', () => {
const httpError = { error: new Error('Test Failure'), status: 501 };
const error = makeDataServiceError('GET', httpError);
dataService.setErrorResponse('getWithQuery', error);
heroCollectionService
.getWithQuery({ name: 'foo' })
.subscribe(expectErrorToBe(error, done));
heroCollectionService.getWithQuery({ name: 'foo' }).subscribe(
() => {
console.log('expected error', error);
expectErrorToBe(error, done);
},
(err) => {
console.log('actual error', err);
expectErrorToBe(error, done);
}
);
markostanimirovic marked this conversation as resolved.
Show resolved Hide resolved
});

it('load observable should emit heroes on success', (done: any) => {
Expand Down
18 changes: 18 additions & 0 deletions modules/data/src/entity-data-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { InjectionToken } from '@angular/core';
import { MetaReducer } from '@ngrx/store';
import { EntityCache } from './reducers/entity-cache';
import { EntityAction } from './actions/entity-action';
import { EntityMetadataMap } from './entity-metadata/entity-metadata';
import { EntityCollection } from './reducers/entity-collection';

export interface EntityDataModuleConfig {
entityMetadata?: EntityMetadataMap;
entityCacheMetaReducers?: (
| MetaReducer<EntityCache>
| InjectionToken<MetaReducer<EntityCache>>
)[];
entityCollectionMetaReducers?: MetaReducer<EntityCollection, EntityAction>[];
// Initial EntityCache state or a function that returns that state
initialEntityCacheState?: EntityCache | (() => EntityCache);
pluralNames?: { [name: string]: string };
}
164 changes: 11 additions & 153 deletions modules/data/src/entity-data-without-effects.module.ts
Original file line number Diff line number Diff line change
@@ -1,66 +1,10 @@
import { ModuleWithProviders, NgModule } from '@angular/core';
import { EntityDataModuleConfig } from './entity-data-config';
import {
ModuleWithProviders,
NgModule,
Inject,
Injector,
InjectionToken,
Optional,
OnDestroy,
} from '@angular/core';

import {
Action,
combineReducers,
MetaReducer,
ReducerManager,
StoreModule,
} from '@ngrx/store';

import { CorrelationIdGenerator } from './utils/correlation-id-generator';
import { EntityDispatcherDefaultOptions } from './dispatchers/entity-dispatcher-default-options';
import { EntityAction } from './actions/entity-action';
import { EntityActionFactory } from './actions/entity-action-factory';
import { EntityCache } from './reducers/entity-cache';
import { EntityCacheDispatcher } from './dispatchers/entity-cache-dispatcher';
import { entityCacheSelectorProvider } from './selectors/entity-cache-selector';
import { EntityCollectionServiceElementsFactory } from './entity-services/entity-collection-service-elements-factory';
import { EntityCollectionServiceFactory } from './entity-services/entity-collection-service-factory';
import { EntityServices } from './entity-services/entity-services';
import { EntityCollection } from './reducers/entity-collection';
import { EntityCollectionCreator } from './reducers/entity-collection-creator';
import { EntityCollectionReducerFactory } from './reducers/entity-collection-reducer';
import { EntityCollectionReducerMethodsFactory } from './reducers/entity-collection-reducer-methods';
import { EntityCollectionReducerRegistry } from './reducers/entity-collection-reducer-registry';
import { EntityDispatcherFactory } from './dispatchers/entity-dispatcher-factory';
import { EntityDefinitionService } from './entity-metadata/entity-definition.service';
import { EntityMetadataMap } from './entity-metadata/entity-metadata';
import { EntityCacheReducerFactory } from './reducers/entity-cache-reducer';
import {
ENTITY_CACHE_NAME,
ENTITY_CACHE_NAME_TOKEN,
ENTITY_CACHE_META_REDUCERS,
ENTITY_COLLECTION_META_REDUCERS,
INITIAL_ENTITY_CACHE_STATE,
} from './reducers/constants';

import { DefaultLogger } from './utils/default-logger';
import { EntitySelectorsFactory } from './selectors/entity-selectors';
import { EntitySelectors$Factory } from './selectors/entity-selectors$';
import { EntityServicesBase } from './entity-services/entity-services-base';
import { EntityServicesElements } from './entity-services/entity-services-elements';
import { Logger, PLURAL_NAMES_TOKEN } from './utils/interfaces';

export interface EntityDataModuleConfig {
entityMetadata?: EntityMetadataMap;
entityCacheMetaReducers?: (
| MetaReducer<EntityCache, Action>
| InjectionToken<MetaReducer<EntityCache, Action>>
)[];
entityCollectionMetaReducers?: MetaReducer<EntityCollection, EntityAction>[];
// Initial EntityCache state or a function that returns that state
initialEntityCacheState?: EntityCache | (() => EntityCache);
pluralNames?: { [name: string]: string };
}
provideRootEntityDataWithoutEffects,
ENTITY_DATA_WITHOUT_EFFECTS_PROVIDERS,
initializeEntityDataWithoutEffects,
} from './provide-entity-data';

/**
* Module without effects or dataservices which means no HTTP calls
Expand All @@ -69,105 +13,19 @@ export interface EntityDataModuleConfig {
* therefore opt-out of @ngrx/effects for entities
*/
@NgModule({
imports: [
StoreModule, // rely on Store feature providers rather than Store.forFeature()
],
providers: [
CorrelationIdGenerator,
EntityDispatcherDefaultOptions,
EntityActionFactory,
EntityCacheDispatcher,
EntityCacheReducerFactory,
entityCacheSelectorProvider,
EntityCollectionCreator,
EntityCollectionReducerFactory,
EntityCollectionReducerMethodsFactory,
EntityCollectionReducerRegistry,
EntityCollectionServiceElementsFactory,
EntityCollectionServiceFactory,
EntityDefinitionService,
EntityDispatcherFactory,
EntitySelectorsFactory,
EntitySelectors$Factory,
EntityServicesElements,
{ provide: ENTITY_CACHE_NAME_TOKEN, useValue: ENTITY_CACHE_NAME },
{ provide: EntityServices, useClass: EntityServicesBase },
{ provide: Logger, useClass: DefaultLogger },
],
providers: [ENTITY_DATA_WITHOUT_EFFECTS_PROVIDERS],
})
export class EntityDataModuleWithoutEffects implements OnDestroy {
private entityCacheFeature: any;

export class EntityDataModuleWithoutEffects {
static forRoot(
config: EntityDataModuleConfig
): ModuleWithProviders<EntityDataModuleWithoutEffects> {
return {
ngModule: EntityDataModuleWithoutEffects,
providers: [
{
provide: ENTITY_CACHE_META_REDUCERS,
useValue: config.entityCacheMetaReducers
? config.entityCacheMetaReducers
: [],
},
{
provide: ENTITY_COLLECTION_META_REDUCERS,
useValue: config.entityCollectionMetaReducers
? config.entityCollectionMetaReducers
: [],
},
{
provide: PLURAL_NAMES_TOKEN,
multi: true,
useValue: config.pluralNames ? config.pluralNames : {},
},
],
};
}

constructor(
private reducerManager: ReducerManager,
entityCacheReducerFactory: EntityCacheReducerFactory,
private injector: Injector,
// optional params
@Optional()
@Inject(ENTITY_CACHE_NAME_TOKEN)
private entityCacheName: string,
@Optional()
@Inject(INITIAL_ENTITY_CACHE_STATE)
private initialState: any,
@Optional()
@Inject(ENTITY_CACHE_META_REDUCERS)
private metaReducers: (
| MetaReducer<EntityCache, Action>
| InjectionToken<MetaReducer<EntityCache, Action>>
)[]
) {
// Add the @ngrx/data feature to the Store's features
// as Store.forFeature does for StoreFeatureModule
const key = entityCacheName || ENTITY_CACHE_NAME;

initialState =
typeof initialState === 'function' ? initialState() : initialState;

const reducers: MetaReducer<EntityCache, Action>[] = (
metaReducers || []
).map((mr) => {
return mr instanceof InjectionToken ? injector.get(mr) : mr;
});

this.entityCacheFeature = {
key,
reducers: entityCacheReducerFactory.create(),
reducerFactory: combineReducers,
initialState: initialState || {},
metaReducers: reducers,
providers: [provideRootEntityDataWithoutEffects(config)],
};
reducerManager.addFeature(this.entityCacheFeature);
}

// eslint-disable-next-line @angular-eslint/contextual-lifecycle
ngOnDestroy() {
this.reducerManager.removeFeature(this.entityCacheFeature);
constructor() {
initializeEntityDataWithoutEffects();
}
}
111 changes: 13 additions & 98 deletions modules/data/src/entity-data.module.ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,22 @@
import { ModuleWithProviders, NgModule } from '@angular/core';

import { EffectsModule, EffectSources } from '@ngrx/effects';

import { DefaultDataServiceFactory } from './dataservices/default-data.service';

import {
DefaultPersistenceResultHandler,
PersistenceResultHandler,
} from './dataservices/persistence-result-handler.service';

import {
DefaultHttpUrlGenerator,
HttpUrlGenerator,
} from './dataservices/http-url-generator';

import { EntityCacheDataService } from './dataservices/entity-cache-data.service';
import { EntityCacheEffects } from './effects/entity-cache-effects';
import { EntityDataService } from './dataservices/entity-data.service';
import { EntityEffects } from './effects/entity-effects';

import { ENTITY_METADATA_TOKEN } from './entity-metadata/entity-metadata';

import {
ENTITY_CACHE_META_REDUCERS,
ENTITY_COLLECTION_META_REDUCERS,
} from './reducers/constants';
import { Pluralizer, PLURAL_NAMES_TOKEN } from './utils/interfaces';
import { DefaultPluralizer } from './utils/default-pluralizer';

import { EntityDataModuleConfig } from './entity-data-config';
import { EntityDataModuleWithoutEffects } from './entity-data-without-effects.module';
import {
EntityDataModuleConfig,
EntityDataModuleWithoutEffects,
} from './entity-data-without-effects.module';
ENTITY_DATA_PROVIDERS,
initializeEntityData,
provideRootEntityData,
provideRootEntityDataWithoutEffects,
} from './provide-entity-data';

/**
* entity-data main module includes effects and HTTP data services
* Configure with `forRoot`.
* No `forFeature` yet.
*/
@NgModule({
imports: [
EntityDataModuleWithoutEffects,
EffectsModule, // do not supply effects because can't replace later
],
providers: [
DefaultDataServiceFactory,
EntityCacheDataService,
EntityDataService,
EntityCacheEffects,
EntityEffects,
{ provide: HttpUrlGenerator, useClass: DefaultHttpUrlGenerator },
{
provide: PersistenceResultHandler,
useClass: DefaultPersistenceResultHandler,
},
{ provide: Pluralizer, useClass: DefaultPluralizer },
],
imports: [EntityDataModuleWithoutEffects],
providers: [ENTITY_DATA_PROVIDERS],
})
export class EntityDataModule {
static forRoot(
Expand All @@ -64,59 +25,13 @@ export class EntityDataModule {
return {
ngModule: EntityDataModule,
providers: [
// TODO: Moved these effects classes up to EntityDataModule itself
// Remove this comment if that was a mistake.
// EntityCacheEffects,
// EntityEffects,
{
provide: ENTITY_METADATA_TOKEN,
multi: true,
useValue: config.entityMetadata ? config.entityMetadata : [],
},
{
provide: ENTITY_CACHE_META_REDUCERS,
useValue: config.entityCacheMetaReducers
? config.entityCacheMetaReducers
: [],
},
{
provide: ENTITY_COLLECTION_META_REDUCERS,
useValue: config.entityCollectionMetaReducers
? config.entityCollectionMetaReducers
: [],
},
{
provide: PLURAL_NAMES_TOKEN,
multi: true,
useValue: config.pluralNames ? config.pluralNames : {},
},
provideRootEntityDataWithoutEffects(config),
provideRootEntityData(config),
],
};
}

constructor(
private effectSources: EffectSources,
entityCacheEffects: EntityCacheEffects,
entityEffects: EntityEffects
) {
// We can't use `forFeature()` because, if we did, the developer could not
// replace the entity-data `EntityEffects` with a custom alternative.
// Replacing that class is an extensibility point we need.
//
// The FEATURE_EFFECTS token is not exposed, so can't use that technique.
// Warning: this alternative approach relies on an undocumented API
// to add effect directly rather than through `forFeature()`.
// The danger is that EffectsModule.forFeature evolves and we no longer perform a crucial step.
this.addEffects(entityCacheEffects);
this.addEffects(entityEffects);
}

/**
* Add another class instance that contains effects.
* @param effectSourceInstance a class instance that implements effects.
* Warning: undocumented @ngrx/effects API
*/
addEffects(effectSourceInstance: any) {
this.effectSources.addEffects(effectSourceInstance);
constructor() {
initializeEntityData();
}
}
14 changes: 10 additions & 4 deletions modules/data/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,9 +190,15 @@ export {
toUpdateFactory,
} from './utils/utilities';

// // EntityDataConfig
export { EntityDataModuleConfig } from './entity-data-config';

// // EntityDataModule
export {
EntityDataModuleConfig,
EntityDataModuleWithoutEffects,
} from './entity-data-without-effects.module';
export { EntityDataModuleWithoutEffects } from './entity-data-without-effects.module';
export { EntityDataModule } from './entity-data.module';

// // Standalone APIs
export {
provideEntityData,
provideEntityDataWithoutEffects,
} from './provide-entity-data';
Loading