feature - route combiner
MODIFY
example/app.ts
example/app.ts
import { bodyParser$, httpListener, logger$ } from '../src';
-import { hello$, root$ } from './controllers';
+import { apiV1$, hello$, root$ } from './controllers';
const middlewares = [
logger$,
@@ -9,6 +9,7 @@ const middlewares = [
const effects = [
root$,
hello$,
+ apiV1$,
];
export const app = httpListener({ middlewares, effects });

MODIFY
example/controllers.ts
example/controllers.ts
import { map } from 'rxjs/operators';
-import { Effect, matchPath, matchType } from '../src';
+import { Effect, combineRoutes, matchPath, matchType } from '../src';
export const root$: Effect = request$ => request$
.pipe(
@@ -18,3 +18,18 @@ export const hello$: Effect = request$ => request$
body: { data: `Hello, ${req.body.data}! @ ${req.url}` },
}))
);
+
+
+const route1$: Effect = request$ => request$
+ .pipe(
+ matchPath('/route1'),
+ matchType('GET'),
+ map(req => ({
+ body: { data: `1 @ ${req.url}` },
+ }))
+ );
+
+export const apiV1$ = combineRoutes(
+ '/api/v1',
+ [ route1$ ],
+);

MODIFY
src/effects/effects.combiner.ts
src/effects/effects.combiner.ts
import { merge, of } from 'rxjs';
-import { switchMap } from 'rxjs/operators';
+import { mergeMap, switchMap } from 'rxjs/operators';
import { HttpRequest } from '../http.interface';
-import { EffectCombiner, MiddlewareCombiner } from './effects.interface';
+import { matchPath } from '../operators/matchPath.operator';
+import { EffectCombiner, MiddlewareCombiner, RoutesCombiner, isGroup } from './effects.interface';
export const combineEffects: EffectCombiner = effects => res => req => {
const req$ = of(req);
- const mappedEffects = effects.map(effect => effect(req$, res, undefined));
+ const mappedEffects = effects.map(effect => isGroup(effect)
+ ? req$.pipe(
+ matchPath(effect.path, '/:foo*'),
+ mergeMap(combineEffects(effect.effects)(res))
+ )
+ : effect(req$, res, undefined)
+ );
+
return merge(...mappedEffects);
};
+export const combineRoutes: RoutesCombiner = (path, effects) => ({
+ path,
+ effects
+});
+
export const combineMiddlewareEffects: MiddlewareCombiner = effects => res => req => {
const req$ = of(req);
const mappedEffects = effects.map(effect =>

MODIFY
src/effects/effects.interface.ts
src/effects/effects.interface.ts
@@ -10,12 +10,29 @@ export interface EffectResponse {
export type Effect<T = EffectResponse, U = any> = (
request$: Observable<HttpRequest>,
response: HttpResponse,
- metadata: U,
+ metadata: U
) => Observable<T>;
+export type EffectCombiner = (
+ effects: (Effect<EffectResponse> | GroupedEffects)[]
+) => (res: HttpResponse) => (req: HttpRequest) => Observable<EffectResponse>;
-export type EffectCombiner = (effects: Effect<EffectResponse>[]) => (res: HttpResponse) => (req: HttpRequest) =>
- Observable<EffectResponse>;
+export type MiddlewareCombiner = (
+ effects: Effect<HttpRequest>[]
+) => (res: HttpResponse) => (req: HttpRequest) => Observable<HttpRequest>;
-export type MiddlewareCombiner = (effects: Effect<HttpRequest>[]) => (res: HttpResponse) => (req: HttpRequest) =>
- Observable<HttpRequest>;
+export type RoutesCombiner = (
+ path: string,
+ effects: (Effect<EffectResponse> | GroupedEffects)[]
+) => GroupedEffects;
+
+export interface GroupedEffects {
+ path: string;
+ effects: (Effect<EffectResponse> | GroupedEffects)[];
+}
+
+export const isGroup = (item): item is GroupedEffects =>
+ typeof item === 'object' && !!item.path && !!item.effects;
+
+export const isEffect = (item): item is Effect<EffectResponse> =>
+ !isGroup(item);

MODIFY
src/http.interface.ts
src/http.interface.ts
@@ -2,6 +2,7 @@ import * as http from 'http';
export interface HttpRequest extends http.IncomingMessage {
body?: any;
+ matchers?: string[];
[key: string]: any;
}

MODIFY
src/http.listener.ts
src/http.listener.ts
import { Subject, of } from 'rxjs';
import { catchError, defaultIfEmpty, switchMap, tap } from 'rxjs/operators';
import { combineEffects, combineMiddlewareEffects } from './effects/effects.combiner';
-import { Effect, EffectResponse } from './effects/effects.interface';
+import { Effect, EffectResponse, GroupedEffects } from './effects/effects.interface';
import { Http, HttpRequest, HttpResponse, HttpStatus } from './http.interface';
import { getErrorMiddleware } from './middlewares/error.middleware';
import { handleResponse } from './response/response.handler';
type HttpListenerConfig = {
middlewares: Effect<HttpRequest>[],
- effects: Effect[],
+ effects: (Effect | GroupedEffects)[],
errorMiddleware?: Effect<EffectResponse, Error>,
};

MODIFY
src/index.ts
src/index.ts
export * from './effects/effects.interface';
+export { combineRoutes } from './effects/effects.combiner';
export * from './middlewares/bodyParser.middleware';
export * from './middlewares/error.middleware';
export * from './middlewares/logger.middleware';

MODIFY
src/operators/matchPath.operator.ts
src/operators/matchPath.operator.ts
import * as pathToRegexp from 'path-to-regexp';
import { Observable } from 'rxjs';
-import { filter } from 'rxjs/operators';
+import { filter, tap } from 'rxjs/operators';
import { HttpRequest } from '../http.interface';
-export const matchPath = (path: string) => (source$: Observable<HttpRequest>) =>
+const removeTrailingSlash = (path: string) =>
+ path.replace(/\/$/, '');
+
+const pathFactory = (matchers: string[], path: string, suffix?: string) =>
+ matchers.reduce((prev, cur) => prev + cur, '') + path + (suffix || '');
+
+export const matchPath = (path: string, suffix?: string) => (source$: Observable<HttpRequest>) =>
source$.pipe(
- filter(req => pathToRegexp(path).test(req.url!))
+ tap(req => req.matchers = req.matchers || []),
+ filter(req => {
+ const mappedPaths = removeTrailingSlash(pathFactory(req.matchers!, path, suffix));
+ return pathToRegexp(mappedPaths).test(req.url!);
+ }),
+ tap(req => (path !== '/') && req.matchers!.push(path))
);

feature - route combiner