Skip to content

Commit

Permalink
feat(stark-core): implementation of a custom error handler
Browse files Browse the repository at this point in the history
  • Loading branch information
Mallikki committed Oct 15, 2018
1 parent 203eaf7 commit 74d98d0
Show file tree
Hide file tree
Showing 16 changed files with 286 additions and 16 deletions.
69 changes: 69 additions & 0 deletions docs/ERROR_HANDLING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Stark Default Error Handling

Stark provides its own error handler, StarkErrorHandler, that overrides [ Angular's default ErrorHandler](https://angular.io/api/core/ErrorHandler).

This handler will dispatch a StarkUnhandledError action to the application Store which you can treat as you want.

## StarkErrorHandlingModule

If you want to use this handler, you simply have to import StarkErrorHandlingModule from stark-core in your `app.module.ts` file.

```typescript
import {StarkErrorHandlingModule} from "@nationalbankbelgium/stark-core";

@NgModule({
bootstrap: ...
declarations: ...
imports: [
StarkErrorHandlingModule.forRoot(),
...
]
})
```

## Effects definition

To define what to do when an error happens, you can simply create a new effects file, like in the following example:

```typescript
...
import {StarkErrorHandlingActionTypes, StarkUnhandledError} from "@nationalbankbelgium/stark-core";

/**
* This class is used to determine what to do with an error
*/
@Injectable()
export class StarkErrorHandlingEffects {

public constructor(
private actions$: Actions
) {}

@Effect()
public doSomething(): Observable<void> {
return this.actions$.pipe(
ofType<StarkUnhandledError>(StarkErrorHandlingActionTypes.UNHANDLED_ERROR),
map((action: StarkUnhandledError) => {
//DO SOMETHING
})
);
}
}
```

If you do create that file, don't forget to import it in your `app.module.ts` file:

```typescript
...
import { StarkErrorHandlingEffects } from "./shared/effects/stark-error-handler.effects";

@NgModule({
bootstrap: ...
declarations: ...
imports: [
...
EffectsModule.forRoot([StarkErrorHandlingEffects]),
...
]
})
```
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { StarkLoggingState } from "../../modules/logging/reducers";
import { StarkSessionState } from "../../modules/session/reducers";
import { StarkSettingsState } from "../../modules/settings/reducers";

/**
* Interface defining the shape of the application state of Stark Core (i.e., what's stored in Redux by Stark)
*/
export interface StarkCoreApplicationState extends StarkLoggingState, StarkSessionState, StarkSettingsState {
// FIXME: still needed?
// starkApplicationMetadata: StarkApplicationMetadata;
}
1 change: 1 addition & 0 deletions packages/stark-core/src/modules.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from "./modules/http";
export * from "./modules/logging";
export * from "./modules/error-handling";
export * from "./modules/routing";
export * from "./modules/session";
export * from "./modules/settings";
Expand Down
3 changes: 3 additions & 0 deletions packages/stark-core/src/modules/error-handling.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./error-handling/actions";
export * from "./error-handling/handlers";
export * from "./error-handling/error-handling.module";
1 change: 1 addition & 0 deletions packages/stark-core/src/modules/error-handling/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./actions/error-handling.actions";
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Action } from "@ngrx/store";

/**
* All the StarkErrorHandling action types
*/
export enum StarkErrorHandlingActionTypes {
UNHANDLED_ERROR = "[StarkErrorHandling] Unhandled Error"
}

/**
* Action that requires to display an error message as a toast notification
* @returns The created action object
*/
export class StarkUnhandledError implements Action {
/**
* The type of action
* @link StarkErrorHandlingActionTypes
*/
public readonly type: StarkErrorHandlingActionTypes.UNHANDLED_ERROR = StarkErrorHandlingActionTypes.UNHANDLED_ERROR;

/**
* Class constructor
* @param error - the error to display
*/
public constructor(public error: any) {}
}

export type StarkErrorHandlingActions = StarkUnhandledError;
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { ErrorHandler, ModuleWithProviders, NgModule, Optional, SkipSelf } from "@angular/core";
import { StarkErrorHandler } from "./handlers/error-handler";

@NgModule({})
export class StarkErrorHandlingModule {
/**
* Instantiates the services only once since they should be singletons
* so the forRoot() should be called only by the AppModule
* @link https://angular.io/guide/singleton-services#forroot
* @returns a module with providers
*/
public static forRoot(): ModuleWithProviders {
return {
ngModule: StarkErrorHandlingModule,
providers: [
{
provide: ErrorHandler,
useClass: StarkErrorHandler
}
]
};
}

/**
* Prevents this module from being re-imported
* @link https://angular.io/guide/singleton-services#prevent-reimport-of-the-coremodule
* @param parentModule - the parent module
*/
public constructor(
@Optional()
@SkipSelf()
parentModule: StarkErrorHandlingModule
) {
if (parentModule) {
throw new Error("StarkErrorHandlingModule is already loaded. Import it in the AppModule only");
}
}
}
1 change: 1 addition & 0 deletions packages/stark-core/src/modules/error-handling/handlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./handlers/error-handler";
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { ErrorHandler, Injectable, Injector } from "@angular/core";
import { Store } from "@ngrx/store";
import { StarkCoreApplicationState } from "../../../common/store";
import { StarkUnhandledError } from "../actions";
import { STARK_LOGGING_SERVICE, StarkLoggingService } from "../../logging/services";

@Injectable()
export class StarkErrorHandler implements ErrorHandler {
private _starkLoggingService: StarkLoggingService;
private _applicationStore: Store<StarkCoreApplicationState>;

public constructor(private injector: Injector) {}

/**
* Thie method will dispatch an error method, which the user can then handle
* @param error the encountered error
*/
public handleError(error: any): void {
this.starkLoggingService.error("StarkErrorHandler: an error has occurred : ", error);
this.applicationStore.dispatch(new StarkUnhandledError(error));
}

/**
* Gets the StarkLoggingService from the Injector.
* @throws When the service is not found (the StarkLoggingService is not provided in the app).
*/
private get starkLoggingService(): StarkLoggingService {
if (typeof this._starkLoggingService === "undefined") {
this._starkLoggingService = this.injector.get<StarkLoggingService>(STARK_LOGGING_SERVICE);
return this._starkLoggingService;
}

return this._starkLoggingService;
}

/**
* Gets the Application Store from the Injector.
* @throws When the Store is not found (the NGRX Store module is not imported in the app).
*/
private get applicationStore(): Store<StarkCoreApplicationState> {
if (typeof this._applicationStore === "undefined") {
this._applicationStore = this.injector.get<Store<StarkCoreApplicationState>>(Store);
return this._applicationStore;
}

return this._applicationStore;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*tslint:disable:completed-docs no-identical-functions*/
import { Component, NgModuleFactoryLoader, NO_ERRORS_SCHEMA, SystemJsNgModuleLoader } from "@angular/core";
import { Component, Injector, NgModuleFactoryLoader, NO_ERRORS_SCHEMA, SystemJsNgModuleLoader } from "@angular/core";
import { fakeAsync, inject, TestBed, tick } from "@angular/core/testing";
import { Ng2StateDeclaration, UIRouterModule } from "@uirouter/angular";
import { StateDeclaration, StateObject, StateService, TransitionService, UIRouter } from "@uirouter/core";
Expand All @@ -18,6 +18,9 @@ import { StarkCoreApplicationState } from "../../../common/store";
import CallInfo = jasmine.CallInfo;
import Spy = jasmine.Spy;
import SpyObj = jasmine.SpyObj;
import { StarkErrorHandler } from "../../error-handling";
import { StarkXSRFService } from "../../xsrf/services";
import { MockStarkXsrfService } from "../../xsrf/testing/xsrf.mock";

@Component({ selector: "test-home", template: "HOME" })
export class HomeComponent {}
Expand All @@ -29,7 +32,9 @@ export class LogoutPageComponent {}
describe("Service: StarkRoutingService", () => {
let $state: StateService;
let router: UIRouter;

let mockInjectorService: SpyObj<Injector>;
let mockXSRFService: StarkXSRFService;
let errorHandler: StarkErrorHandler;
let routingService: StarkRoutingServiceImpl;
let mockLogger: StarkLoggingService;
let appConfig: StarkApplicationConfig;
Expand Down Expand Up @@ -376,13 +381,21 @@ describe("Service: StarkRoutingService", () => {
deferIntercept: true // FIXME: this option shouldn't be used but is needed for Chrome and HeadlessChrome otherwise it doesn't work. Why?
});

beforeEach(() => {
mockInjectorService = jasmine.createSpyObj<Injector>("injector,", ["get"]);
mockXSRFService = new MockStarkXsrfService();
/* tslint:disable-next-line:deprecation */
(<Spy>mockInjectorService.get).and.returnValue(mockXSRFService);
});

const starkRoutingServiceFactory: Function = (state: StateService, transitions: TransitionService) => {
appConfig = new StarkApplicationConfigImpl();
appConfig.homeStateName = "homepage";

mockLogger = new MockStarkLoggingService(mockCorrelationId);
errorHandler = new StarkErrorHandler(mockInjectorService);

return new StarkRoutingServiceImpl(mockLogger, appConfig, mockStore, state, transitions);
return new StarkRoutingServiceImpl(mockLogger, appConfig, errorHandler, mockStore, state, transitions);
};

/**
Expand Down Expand Up @@ -427,9 +440,9 @@ describe("Service: StarkRoutingService", () => {
const modifiedAppConfig: StarkApplicationConfig = new StarkApplicationConfigImpl();
modifiedAppConfig.homeStateName = <any>undefined;

expect(() => new StarkRoutingServiceImpl(mockLogger, modifiedAppConfig, mockStore, <any>{}, <any>{})).toThrowError(
/homeStateName/
);
expect(
() => new StarkRoutingServiceImpl(mockLogger, modifiedAppConfig, errorHandler, mockStore, <any>{}, <any>{})
).toThrowError(/homeStateName/);
});
});

Expand Down Expand Up @@ -950,7 +963,7 @@ describe("Service: StarkRoutingService", () => {
.navigateTo("page-01")
.pipe(
catchError((error: any) => {
expect(mockLogger.error).toHaveBeenCalledTimes(2);
expect(mockLogger.error).toHaveBeenCalledTimes(1);
const message: string = (<Spy>mockLogger.error).calls.argsFor(0)[0];
expect(message).toMatch(/Error during route transition/);
return throwError(errorPrefix + error);
Expand Down Expand Up @@ -986,7 +999,7 @@ describe("Service: StarkRoutingService", () => {
.navigateTo("page-01")
.pipe(
catchError((error: any) => {
expect(mockLogger.error).toHaveBeenCalledTimes(2);
expect(mockLogger.error).toHaveBeenCalledTimes(1);
const message: string = (<Spy>mockLogger.error).calls.argsFor(0)[0];
expect(message).toMatch(/An error occurred with a resolve in the new state/);
return throwError(errorPrefix + error);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* tslint:disable:completed-docs*/
import { Action, Store } from "@ngrx/store";
import { EMPTY, from, Observable } from "rxjs";
import { Inject, Injectable } from "@angular/core";
import { ErrorHandler, Inject, Injectable } from "@angular/core";
import {
HookMatchCriteria,
HookRegOptions,
Expand Down Expand Up @@ -75,6 +75,7 @@ export class StarkRoutingServiceImpl implements StarkRoutingService {
public constructor(
@Inject(STARK_LOGGING_SERVICE) private logger: StarkLoggingService,
@Inject(STARK_APP_CONFIG) private appConfig: StarkApplicationConfig,
private defaultErrorHandler: ErrorHandler,
private store: Store<StarkCoreApplicationState>,
private $state: StateService,
private $transitions: TransitionService
Expand Down Expand Up @@ -534,7 +535,7 @@ export class StarkRoutingServiceImpl implements StarkRoutingService {
}

if (!this.knownRejectionCausesRegex.test(stringError)) {
this.logger.error(starkRoutingServiceName + ": defaultErrorHandler => ", new Error(stringError));
this.defaultErrorHandler.handleError(Error(stringError));
}
}
);
Expand Down
5 changes: 4 additions & 1 deletion packages/stark-core/src/modules/settings/settings.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import { STARK_SETTINGS_SERVICE, StarkSettingsServiceImpl } from "./services";
import { StarkSettingsEffects } from "./effects";

@NgModule({
imports: [StoreModule.forFeature("StarkSettings", starkSettingsReducers), EffectsModule.forFeature([StarkSettingsEffects])]
imports: [
StoreModule.forFeature("StarkSettings", starkSettingsReducers),
EffectsModule.forFeature([StarkSettingsEffects])
]
})
export class StarkSettingsModule {
/**
Expand Down
7 changes: 5 additions & 2 deletions showcase/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { MatListModule } from "@angular/material/list";
import { MatSidenavModule } from "@angular/material/sidenav";
import { MatTooltipModule } from "@angular/material/tooltip";
import { DateAdapter } from "@angular/material/core";
import { SharedModule } from "./shared/shared.module";
import { SharedModule } from "./shared";
import { Observable, of } from "rxjs";
import { filter, map } from "rxjs/operators";

Expand All @@ -34,6 +34,7 @@ import {
StarkApplicationConfigImpl,
StarkApplicationMetadata,
StarkApplicationMetadataImpl,
StarkErrorHandlingModule,
StarkHttpModule,
StarkLoggingActionTypes,
StarkLoggingModule,
Expand Down Expand Up @@ -89,6 +90,7 @@ import { NewsModule } from "./news";
import "../styles/styles.pcss";
// load SASS styles
import "../styles/styles.scss";
import { StarkErrorHandlingEffects } from "./shared/effects/stark-error-handling.effects";
/* tslint:enable */

// TODO: where to put this factory function?
Expand Down Expand Up @@ -195,7 +197,7 @@ export const metaReducers: MetaReducer<State>[] = ENV !== "production" ? [logger
name: "Stark Showcase - NgRx Store DevTools", // shown in the monitor page
logOnly: environment.production // restrict extension to log-only mode (setting it to false enables all extension features)
}),
EffectsModule.forRoot([]), // needed to set up the providers required for effects
EffectsModule.forRoot([StarkErrorHandlingEffects]), // needed to set up the providers required for effects
UIRouterModule.forRoot({
states: APP_STATES,
useHash: !Boolean(history.pushState),
Expand All @@ -208,6 +210,7 @@ export const metaReducers: MetaReducer<State>[] = ENV !== "production" ? [logger
StarkHttpModule.forRoot(),
StarkLoggingModule.forRoot(),
StarkSessionModule.forRoot(),
StarkErrorHandlingModule.forRoot(),
StarkSettingsModule.forRoot(),
StarkRoutingModule.forRoot(),
StarkUserModule.forRoot(),
Expand Down
Loading

0 comments on commit 74d98d0

Please sign in to comment.