fix(core): allow to override routes (eg. default falback one)
MODIFY
packages/core/src/listener/http.listener.ts
packages/core/src/listener/http.listener.ts
import { IncomingMessage, OutgoingMessage } from 'http';
import { pipe } from 'fp-ts/lib/pipeable';
import * as R from 'fp-ts/lib/Reader';
-import { combineMiddlewares } from '../effects/effects.combiner';
import {
HttpMiddlewareEffect,
HttpErrorEffect,
@@ -41,9 +40,8 @@ export const httpListener = ({
const ask = lookup(ctx);
const client = useContext(ServerClientToken)(ask);
const effectContext = createEffectContext({ ask, client });
- const middleware$ = combineMiddlewares(...middlewares);
- const routing = factorizeRoutingWithDefaults(effects);
- const { resolve, errorSubject } = resolveRouting(routing, effectContext)(middleware$, output$, error$);
+ const routing = factorizeRoutingWithDefaults(effects, middlewares);
+ const { resolve, errorSubject } = resolveRouting(routing, effectContext)(output$, error$);
const httpServer = (req: IncomingMessage, res: OutgoingMessage) => {
const marbleReq = req as HttpRequest;

MODIFY
packages/core/src/router/router.effects.ts
packages/core/src/router/router.effects.ts
import { throwError } from 'rxjs';
-import { mergeMap } from 'rxjs/operators';
+import { mergeMapTo } from 'rxjs/operators';
import { r } from './router.ixbuilder';
import { HttpError } from '../error/error.model';
import { HttpStatus } from '../http.interface';
@@ -9,6 +9,5 @@ export const ROUTE_NOT_FOUND_ERROR = new HttpError('Route not found', HttpStatus
export const notFound$ = r.pipe(
r.matchPath('*'),
r.matchType('*'),
- r.useEffect(req$ => req$.pipe(
- mergeMap(() => throwError(ROUTE_NOT_FOUND_ERROR)),
- )));
+ r.useEffect(req$ => req$.pipe(mergeMapTo(throwError(ROUTE_NOT_FOUND_ERROR)))),
+ r.applyMeta({ overridable: true }));

MODIFY
packages/core/src/router/router.factory.ts
packages/core/src/router/router.factory.ts
@@ -9,9 +9,11 @@ import {
RouteEffectGroup,
} from './router.interface';
import { factorizeRegExpWithParams } from './router.params.factory';
-import { isNonNullable } from '../+internal/utils';
import { notFound$ } from './router.effects';
+const canOverrideRoute = (route: RouteEffect) =>
+ route.meta?.overridable === true;
+
export const factorizeRouting = (
routes: (RouteEffect | RouteEffectGroup)[],
middlewares: HttpMiddlewareEffect[] = [],
@@ -36,15 +38,19 @@ export const factorizeRouting = (
parameters,
effect: route.effect,
middleware: middlewares.length
- ? combineMiddlewares(...[...middlewares, route.middleware].filter(isNonNullable))
+ ? combineMiddlewares(...middlewares, route.middleware)
: route.middleware,
};
if (foundRoute) {
- if (foundRoute.methods[route.method]) {
+ if (!canOverrideRoute(route) && foundRoute.methods[route.method]) {
throw new Error(`Redefinition of route at "${route.method}: ${parentPath + route.path}"`);
}
+ if (canOverrideRoute(route) && foundRoute.methods[route.method]) {
+ return;
+ }
+
return foundRoute.methods[route.method] = method;
}
@@ -58,5 +64,8 @@ export const factorizeRouting = (
return routing;
};
-export const factorizeRoutingWithDefaults = (routes: (RouteEffect | RouteEffectGroup)[]): Routing =>
- factorizeRouting([...routes, notFound$]);
+export const factorizeRoutingWithDefaults = (
+ routes: (RouteEffect | RouteEffectGroup)[],
+ middlewares: HttpMiddlewareEffect[] = [],
+): Routing =>
+ factorizeRouting([...routes, notFound$], middlewares);

MODIFY
packages/core/src/router/router.interface.ts
packages/core/src/router/router.interface.ts
@@ -3,12 +3,16 @@ import { HttpMethod, HttpRequest } from '../http.interface';
import { HttpEffect, HttpMiddlewareEffect, HttpEffectResponse } from '../effects/http-effects.interface';
// Route
+export interface RouteMeta extends Record<string, any> {
+ overridable?: boolean;
+}
+
export interface RouteEffect<T extends HttpRequest = HttpRequest> {
path: string;
method: HttpMethod;
effect: HttpEffect<T, HttpEffectResponse>;
middleware?: HttpMiddlewareEffect;
- meta?: any;
+ meta?: RouteMeta;
}
export interface RouteEffectGroup {

MODIFY
packages/core/src/router/router.ixbuilder.ts
packages/core/src/router/router.ixbuilder.ts
import { IxBuilder, ichainCurry } from '../+internal/fp/IxBuilder';
-import { RouteEffect } from './router.interface';
+import { RouteEffect, RouteMeta } from './router.interface';
import { HttpMethod } from '../http.interface';
import { HttpMiddlewareEffect, HttpEffect } from '../effects/http-effects.interface';
import { combineMiddlewares } from '../effects/effects.combiner';
@@ -36,7 +36,7 @@ const use = (middleware: HttpMiddlewareEffect) => ichainCurry((spec: RouteEffect
const useEffect = (effect: HttpEffect) => ichainCurry((spec: RouteEffectSpec) =>
new IxBuilder<TypeApplied | MiddlewareApplied, EffectApplied, RouteEffectSpec>({ ...spec, effect }));
-const applyMeta = (meta: Record<string, any>) => ichainCurry((spec: RouteEffectSpec) =>
+const applyMeta = (meta: RouteMeta) => ichainCurry((spec: RouteEffectSpec) =>
new IxBuilder<EffectApplied | MetaApplied, MetaApplied, RouteEffectSpec>({
...spec,
meta: { ...spec.meta, ...meta },

MODIFY
packages/core/src/router/router.resolver.v2.ts
packages/core/src/router/router.resolver.v2.ts
import { Subject, zip, OperatorFunction, of, fromEvent, Observable } from 'rxjs';
-import { publish, catchError, map, filter, takeUntil} from 'rxjs/operators';
+import { publish, catchError, map, filter, takeUntil, share, take} from 'rxjs/operators';
import { Routing, BootstrappedRoutingItem } from './router.interface';
import { EffectContext } from '../effects/effects.interface';
import { HttpServer, HttpRequest } from '../http.interface';
import { queryParamsFactory } from './router.query.factory';
import { defaultError$ } from '../error/error.effect';
-import { HttpMiddlewareEffect, HttpEffectResponse, HttpErrorEffect, HttpOutputEffect } from '../effects/http-effects.interface';
-import { combineMiddlewares } from '../effects/effects.combiner';
+import { HttpEffectResponse, HttpErrorEffect, HttpOutputEffect } from '../effects/http-effects.interface';
import { matchRoute } from './router.matcher';
import { isError } from '../+internal/utils';
@@ -14,14 +13,10 @@ export const resolveRouting = (
routing: Routing,
ctx: EffectContext<HttpServer>,
) => (
- globalMiddleware$?: HttpMiddlewareEffect,
output$?: HttpOutputEffect,
error$?: HttpErrorEffect,
) => {
- const close$ = new Subject();
-
- fromEvent(ctx.client, 'close')
- .subscribe(() => close$.next());
+ const close$ = fromEvent(ctx.client, 'close').pipe(take(1), share());
const outputSubject = new Subject<{ res: HttpEffectResponse; req: HttpRequest}>();
const outputStream$ = outputSubject.asObservable().pipe(takeUntil(close$));
@@ -84,8 +79,6 @@ export const resolveRouting = (
const inputSubject = new Subject<HttpRequest>();
const inputStream$ = inputSubject.asObservable().pipe(takeUntil(close$));
- const middleware$ = combineMiddlewares(r$ => r$, globalMiddleware$, middleware);
-
const subscribe = (stream$: Observable<any>) =>
stream$.subscribe(
([res, req]) => outputSubject.next({ req, res }),
@@ -95,7 +88,7 @@ export const resolveRouting = (
const flow$ = zip(
inputStream$.pipe(
- publish(req$ => middleware$(req$, ctx)),
+ publish(req$ => middleware ? middleware(req$, ctx) : req$),
publish(req$ => effect(req$, ctx)),
catchError(error => of(error)),
),

MODIFY
packages/core/src/router/specs/router.factory.spec.ts
packages/core/src/router/specs/router.factory.spec.ts
import { of } from 'rxjs';
import { mapTo, tap } from 'rxjs/operators';
import { RouteEffect, RouteEffectGroup, Routing } from '../router.interface';
-import { factorizeRouting } from '../router.factory';
+import { factorizeRouting, factorizeRoutingWithDefaults } from '../router.factory';
import { createHttpRequest, createMockEffectContext } from '../../+internal';
import { HttpEffect, HttpMiddlewareEffect } from '../../effects/http-effects.interface';
import { factorizeRegExpWithParams } from '../router.params.factory';
+import { r } from '../router.ixbuilder';
const getRegExp = (path: string) => factorizeRegExpWithParams(path).regExp;
@@ -175,3 +176,46 @@ describe('#factorizeRouting', () => {
expect(error).toThrowError('Redefinition of route at "GET: /test"');
});
});
+
+describe('#factorizeRoutingWithDefaults', () => {
+ test('adds fallback route', () => {
+ const factorizedRouting = factorizeRoutingWithDefaults([]);
+
+ expect(factorizedRouting).toEqual([
+ {
+ regExp: getRegExp('/*'),
+ path: '/(.*)',
+ methods: {
+ '*': {
+ effect: expect.any(Function),
+ middleware: undefined,
+ parameters: undefined,
+ },
+ },
+ },
+ ] as Routing);
+ });
+
+ test('allows to redefine fallback route', () => {
+ const fallback$ = r.pipe(
+ r.matchPath('*'),
+ r.matchType('*'),
+ r.useEffect(req$ => req$.pipe(mapTo({ body: 'fallback' }))));
+
+ const factorizedRouting = factorizeRoutingWithDefaults([ fallback$ ]);
+
+ expect(factorizedRouting).toEqual([
+ {
+ regExp: getRegExp('/*'),
+ path: '/(.*)',
+ methods: {
+ [fallback$.method]: {
+ effect: fallback$.effect,
+ middleware: undefined,
+ parameters: undefined,
+ },
+ },
+ },
+ ] as Routing);
+ });
+});

fix(core): allow to override routes (eg. default falback one)