reafactor(core): created dedicated `route.factory` for routing factorizing
MODIFY
packages/core/src/effects/effects.combiner.ts
packages/core/src/effects/effects.combiner.ts
import { from } from 'rxjs';
import { concatMap, last } from 'rxjs/operators';
-import { HttpRequest, HttpResponse } from '../http.interface';
import { Middleware } from './effects.interface';
-import { Observable } from 'rxjs/Observable';
export const combineMiddlewareEffects = (effects: Middleware[]): Middleware => {
const middlewaresObservable = from(middlewaresGuard(effects));
- return (req$: Observable<HttpRequest>, res: HttpResponse, metadata: any) => {
+ return (req$, res, metadata) => {
return middlewaresObservable.pipe(
concatMap(effect => effect(req$, res, metadata)),
last(),

MODIFY
packages/core/src/effects/effects.factory.ts
packages/core/src/effects/effects.factory.ts
import { HttpMethod } from '../http.interface';
import { Effect, EffectResponse } from './effects.interface';
-import { RouteConfig } from '../router/router.interface';
+import { RouteEffect } from '../router/router.interface';
export namespace EffectFactory {
export const matchPath = (path: string) => ({
matchType: (method: HttpMethod) => ({
- use: (effect: Effect<EffectResponse>): RouteConfig => ({
+ use: (effect: Effect<EffectResponse>): RouteEffect => ({
path, method, effect
}),
}),

MODIFY
packages/core/src/effects/effects.interface.ts
packages/core/src/effects/effects.interface.ts
@@ -9,6 +9,8 @@ export interface EffectResponse {
export type Middleware = Effect<HttpRequest>;
+export type ErrorMiddleware = Effect<EffectResponse, Error>;
+
export type Effect<T = EffectResponse, U = any> = (
request$: Observable<HttpRequest>,
response: HttpResponse,

MODIFY
packages/core/src/http.listener.spec.ts
packages/core/src/http.listener.spec.ts
-import { throwError } from 'rxjs';
-import { mapTo, switchMap } from 'rxjs/operators';
-import * as request from 'supertest';
-import { HttpError } from './error/error.model';
-import { httpListener } from './http.listener';
-import { EffectFactory } from './effects/effects.factory';
-
describe('Http listener', () => {
- it('reacts to attached effect', async () => {
- const effect$ = EffectFactory
- .matchPath('/test')
- .matchType('GET')
- .use(req$ => req$
- .pipe(
- mapTo({ status: 200 })
- )
- );
-
- const app = httpListener({ effects: [effect$] });
- return request(app)
- .get('/test')
- .expect(200);
+ test('@TODO', () => {
+ // @TODO
});
- it('reacts to throwed exception', async () => {
- const effect$ = EffectFactory
- .matchPath('/test')
- .matchType('GET')
- .use(req$ => req$
- .pipe(
- switchMap(() => throwError(new HttpError('test', 500))),
- mapTo({ status: 200 })
- )
- );
-
- const app = httpListener({ effects: [effect$] });
-
- return request(app)
- .get('/test')
- .expect(500);
- });
});

MODIFY
packages/core/src/http.listener.ts
packages/core/src/http.listener.ts
@@ -2,17 +2,18 @@ import { IncomingMessage, OutgoingMessage } from 'http';
import { of, Subject } from 'rxjs';
import { catchError, defaultIfEmpty, mergeMap, switchMap, tap } from 'rxjs/operators';
import { combineMiddlewareEffects } from './effects/effects.combiner';
-import { Effect, EffectResponse } from './effects/effects.interface';
+import { EffectResponse, Middleware, ErrorMiddleware } from './effects/effects.interface';
import { getErrorMiddleware } from './error/error.middleware';
import { Http, HttpRequest, HttpResponse, HttpStatus } from './http.interface';
import { handleResponse } from './response/response.handler';
-import { RouteEffects } from './router/router.interface';
-import { routingFactory, resolveRouting } from './router/router';
+import { RouteEffect, RouteEffectGroup } from './router/router.interface';
+import { resolveRouting } from './router/router';
+import { factorizeRouting } from './router/router.factory';
type HttpListenerConfig = {
- middlewares?: Effect<HttpRequest>[];
- effects: RouteEffects[];
- errorMiddleware?: Effect<EffectResponse, Error>;
+ middlewares?: Middleware[];
+ effects: (RouteEffect | RouteEffectGroup)[];
+ errorMiddleware?: ErrorMiddleware;
};
export const httpListener = ({
@@ -23,7 +24,7 @@ export const httpListener = ({
const request$ = new Subject<Http>();
const combinedMiddlewares = combineMiddlewareEffects(middlewares);
- const routerEffects = routingFactory(effects);
+ const routerEffects = factorizeRouting(effects);
const providedErrorMiddleware = getErrorMiddleware(errorMiddleware);
const defaultResponse = { status: HttpStatus.NOT_FOUND } as EffectResponse;

ADD
packages/core/src/router/router.factory.spec.ts
+import { mapTo } from 'rxjs/operators';
+import { Effect, Middleware } from '../effects/effects.interface';
+import { RouteEffect, RouteEffectGroup, Routing } from './router.interface';
+import { factorizeRouting } from './router.factory';
+
+describe('Router factory', () => {
+ let effectsCombiner;
+
+ beforeEach(() => {
+ jest.unmock('../effects/effects.combiner.ts');
+ effectsCombiner = require('../effects/effects.combiner');
+ });
+
+ test('#factorizeRouting factorizes routing with nested groups', () => {
+ // given
+ const m$: Middleware = req$ => req$;
+ const e1$: Effect = req$ => req$.pipe(mapTo({ body: 'test1' }));
+ const e2$: Effect = req$ => req$.pipe(mapTo({ body: 'test2' }));
+ const e3$: Effect = req$ => req$.pipe(mapTo({ body: 'test3' }));
+ const e4$: Effect = req$ => req$.pipe(mapTo({ body: 'test4' }));
+
+ const route1: RouteEffect = { path: '/', method: 'GET', effect: e1$ };
+ const route2: RouteEffect = { path: '/', method: 'GET', effect: e2$ };
+ const route3: RouteEffect = { path: '/', method: 'GET', effect: e3$ };
+ const route4: RouteEffect = { path: '/', method: 'POST', effect: e4$ };
+
+ const routeGroupNested: RouteEffectGroup = {
+ path: '/nested',
+ effects: [route3],
+ middlewares: [m$],
+ };
+
+ const routeGroup: RouteEffectGroup = {
+ path: '/group',
+ effects: [route2, routeGroupNested],
+ middlewares: [m$],
+ };
+
+ // when
+ effectsCombiner.combineMiddlewareEffects = jest.fn(() => m$);
+
+ const factorizedRouting = factorizeRouting([
+ route1,
+ routeGroup,
+ route4
+ ]);
+
+ // then
+ expect(factorizedRouting).toEqual([
+ {
+ regExp: /^\/?$/,
+ methods: { GET: { effect: e1$ }, POST: { effect: e4$ } },
+ },
+ {
+ regExp: /^\/group\/?$/,
+ methods: { GET: { middleware: m$, effect: e2$ } },
+ },
+ {
+ regExp: /^\/group\/nested\/?$/,
+ methods: { GET: { middleware: m$, effect: e3$ } },
+ },
+ ] as Routing);
+ });
+
+ test('#factorizeRouting throws error if route is redefined', () => {
+ // given
+ const e1$: Effect = req$ => req$.pipe(mapTo({ body: 'test1' }));
+ const e2$: Effect = req$ => req$.pipe(mapTo({ body: 'test2' }));
+ const e3$: Effect = req$ => req$.pipe(mapTo({ body: 'test3' }));
+
+ const route1: RouteEffect = { path: '/test', method: 'GET', effect: e1$ };
+ const route2: RouteEffect = { path: '/test', method: 'GET', effect: e2$ };
+ const route3: RouteEffect = { path: '/test/foo', method: 'GET', effect: e3$ };
+
+ // when
+ const error = () => factorizeRouting([
+ route1,
+ route2,
+ route3,
+ ]);
+
+ // then
+ expect(error).toThrowError('Redefinition of route at "GET: /test"');
+ });
+
+});

ADD
packages/core/src/router/router.factory.ts
+import { Effect } from '../effects/effects.interface';
+import { HttpRequest } from '../http.interface';
+import { isRouteEffectGroup } from './router.helpers';
+import { Routing, RoutingMethod, RoutingItem, RouteEffect, RouteEffectGroup } from './router.interface';
+import { factorizeRegExpWithParams } from './urlParams.factory';
+import { combineMiddlewareEffects } from '../effects/effects.combiner';
+
+export const factorizeRouting = (
+ routes: (RouteEffect | RouteEffectGroup)[],
+ middleware: Effect<HttpRequest>[] = [],
+ parentPath = '',
+): Routing => {
+ const routing: Routing = [];
+
+ routes.forEach(route => {
+ if (isRouteEffectGroup(route)) {
+ return routing.push(...factorizeRouting(
+ route.effects,
+ [...middleware, ...route.middlewares],
+ parentPath + route.path,
+ ));
+ }
+
+ const { regExp, parameters } = factorizeRegExpWithParams(parentPath + route.path);
+ const foundRoute = routing.find(route => route.regExp.source === regExp.source);
+ const method: RoutingMethod = {
+ effect: route.effect,
+ middleware: middleware.length > 0
+ ? middleware.length > 1 ? combineMiddlewareEffects(middleware) : middleware[0]
+ : undefined,
+ parameters,
+ };
+
+ if (foundRoute) {
+ if (foundRoute.methods[route.method]) {
+ throw new Error(`Redefinition of route at "${route.method}: ${parentPath + route.path}"`);
+ }
+
+ return foundRoute.methods[route.method] = method;
+ }
+
+ return routing.push({
+ regExp,
+ methods: { [route.method]: method },
+ } as RoutingItem);
+ });
+
+ return routing;
+};

MODIFY
packages/core/src/router/router.helper.spec.ts
packages/core/src/router/router.helper.spec.ts
-import { isRouteGroup, isRouteCombinerConfig } from './router.helpers';
+import { isRouteEffectGroup, isRouteCombinerConfig } from './router.helpers';
describe('Router helper', () => {
test('#isRouteGroup checks if provided argument is typeof RouteGroup', () => {
- expect(isRouteGroup({ path: '/', effects: [], middlewares: [] })).toEqual(true);
- expect(isRouteGroup({ path: '/', effects: [] })).toEqual(false);
- expect(isRouteGroup({ path: '/', method: 'GET', effect: req$ => req$ })).toEqual(false);
+ expect(isRouteEffectGroup({ path: '/', effects: [], middlewares: [] })).toEqual(true);
+ expect(isRouteEffectGroup({ path: '/', effects: [] })).toEqual(false);
+ expect(isRouteEffectGroup({ path: '/', method: 'GET', effect: req$ => req$ })).toEqual(false);
});
test('#isRouteCombinerConfig checks if provided argument is typeof RouteCombinerConfig', () => {

MODIFY
packages/core/src/router/router.helpers.ts
packages/core/src/router/router.helpers.ts
-import { RouteCombinerConfig, RouteGroup, Routing } from './router.interface';
+import { RouteCombinerConfig, RouteEffectGroup, Routing } from './router.interface';
export { Routing };
-export const isRouteGroup = (item): item is RouteGroup =>
+export const isRouteEffectGroup = (item): item is RouteEffectGroup =>
Array.isArray(item.effects) &&
Array.isArray(item.middlewares);

MODIFY
packages/core/src/router/router.interface.ts
packages/core/src/router/router.interface.ts
import { HttpMethod } from '../http.interface';
import { Effect, Middleware } from '../effects/effects.interface';
-export type RouteEffects = RouteConfig | RouteGroup;
-
-export interface RouteCombinerConfig {
- middlewares?: Middleware[];
- effects: RouteEffects[];
-}
-
-export interface RouteConfig {
+// Route
+export interface RouteEffect {
path: string;
method: HttpMethod;
effect: Effect;
}
-export interface RouteGroup {
+export interface RouteEffectGroup {
path: string;
- effects: RouteEffects[];
middlewares: Middleware[];
+ effects: (RouteEffect | RouteEffectGroup)[];
}
+// Combiner
+export interface RouteCombinerConfig {
+ middlewares?: Middleware[];
+ effects: (RouteEffect | RouteEffectGroup)[];
+}
+
+// Routing
export interface ParametricRegExp {
regExp: RegExp;
parameters?: string[] | undefined;
}
-
export interface RoutingMethod {
parameters?: string[] | undefined;
middleware?: Middleware | undefined;

MODIFY
packages/core/src/router/router.spec.ts
packages/core/src/router/router.spec.ts
import { mapTo } from 'rxjs/operators';
import { Effect } from '../effects/effects.interface';
-import { findRoute, resolveRouting, routingFactory } from './router';
+import { findRoute, resolveRouting } from './router';
import { HttpRequest, HttpResponse } from '../http.interface';
-import { RouteConfig, RouteGroup, RouteMatched, Routing } from './router.interface';
+import { RouteMatched, Routing } from './router.interface';
describe('Router', () => {
- test('#routingFactory factorizes routing with nested groups', () => {
- // given
- const m$: Effect<HttpRequest> = req$ => req$;
- const e1$: Effect = req$ => req$.pipe(mapTo({ body: 'test1' }));
- const e2$: Effect = req$ => req$.pipe(mapTo({ body: 'test2' }));
- const e3$: Effect = req$ => req$.pipe(mapTo({ body: 'test3' }));
- const e4$: Effect = req$ => req$.pipe(mapTo({ body: 'test4' }));
-
- const route1: RouteConfig = { path: '/', method: 'GET', effect: e1$ };
- const route2: RouteConfig = { path: '/', method: 'GET', effect: e2$ };
- const route3: RouteConfig = { path: '/', method: 'GET', effect: e3$ };
- const route4: RouteConfig = { path: '/', method: 'POST', effect: e4$ };
-
- const routeGroupNested: RouteGroup = {
- path: '/nested',
- effects: [route3],
- middlewares: [m$],
- };
-
- const routeGroup: RouteGroup = {
- path: '/group',
- effects: [route2, routeGroupNested],
- middlewares: [m$],
- };
-
- // when
- const factorizedRouting = routingFactory([
- route1,
- routeGroup,
- route4
- ]);
-
- // then
- expect(factorizedRouting).toEqual([
- { regExp: /^\/?$/, methods: { GET: { effect: e1$ }, POST: { effect: e4$ } } },
- { regExp: /^\/group\/?$/, methods: { GET: { middleware: m$, effect: e2$ } } },
- {
- regExp: /^\/group\/nested\/?$/,
- methods: {
- GET: { middleware: factorizedRouting[2].methods.GET!.middleware, effect: e3$ }
- },
- },
- ] as Routing);
- });
-
- test('#findRoute finds route inside collection', () => {
+ describe('#findRoute', () => {
+ test('finds route inside collection', () => {
// given
const e1$: Effect = req$ => req$.pipe(mapTo({ body: 'test' }));
const e2$: Effect = req$ => req$.pipe(mapTo({ body: 'test' }));
@@ -71,6 +27,7 @@ describe('Router', () => {
expect(route1).toEqual({ effect: e3$, middleware: undefined, params: {} });
expect(route2).toBeUndefined();
});
+ });
describe('#resolveRouting', () => {
let router;

MODIFY
packages/core/src/router/router.ts
packages/core/src/router/router.ts
import { EMPTY, Observable, of } from 'rxjs';
import { HttpMethod, HttpRequest, HttpResponse } from '../http.interface';
-import { Effect, EffectResponse } from '../effects/effects.interface';
-import { isRouteCombinerConfig, isRouteGroup } from './router.helpers';
-import {
- RouteCombinerConfig,
- RouteConfig,
- RouteEffects,
- RouteGroup,
- RouteMatched,
- Routing,
- RoutingMethod,
- RoutingItem
-} from './router.interface';
+import { EffectResponse } from '../effects/effects.interface';
+import { isRouteCombinerConfig } from './router.helpers';
+import { RouteCombinerConfig, RouteEffect, RouteEffectGroup, RouteMatched, Routing } from './router.interface';
import { queryParamsFactory } from '../router/queryParams.factory';
-import { combineMiddlewareEffects } from '../effects/effects.combiner';
-import { factorizeRegExpWithParams } from './urlParams.factory';
export const combineRoutes = (
path: string,
- configOrEffects: RouteCombinerConfig | RouteEffects[]
-): RouteGroup => ({
+ configOrEffects: RouteCombinerConfig | (RouteEffect | RouteEffectGroup)[]
+): RouteEffectGroup => ({
path,
effects: isRouteCombinerConfig(configOrEffects) ? configOrEffects.effects : configOrEffects,
middlewares: isRouteCombinerConfig(configOrEffects) ? (configOrEffects.middlewares || []) : [],
});
-export const routingFactory = (
- routes: (RouteGroup | RouteConfig)[],
- parentPath = '',
- middleware: Effect<HttpRequest>[] = []
-): Routing => {
- const routing: Routing = [];
- routes.forEach(route => {
- if (isRouteGroup(route)) {
- routing.push(...routingFactory(
- route.effects,
- parentPath + route.path,
- [...middleware, ...route.middlewares]
- ));
- } else {
- const { regExp, parameters } = factorizeRegExpWithParams(parentPath + route.path);
- const foundRoute = routing.find(route => route.regExp.source === regExp.source);
- const method: RoutingMethod = {
- effect: route.effect,
- middleware: middleware.length > 0
- ? middleware.length > 1 ? combineMiddlewareEffects(middleware) : middleware[0]
- : undefined,
- parameters,
- };
- if (foundRoute) {
- if (foundRoute.methods[route.method]) {
- throw new Error(`Redefinition of route at "${route.method}: ${parentPath + route.path}"`);
- }
- foundRoute.methods[route.method] = method;
- } else {
- routing.push({
- regExp,
- methods: { [route.method]: method },
- } as RoutingItem);
- }
- }
- });
- return routing;
-};
-
export const findRoute = (
routing: Routing,
url: string,

reafactor(core): created dedicated `route.factory` for routing factorizing