imp(core, middleware-joi, improved type-inference of combined middlewares
MODIFY
.gitignore
.gitignore
@@ -32,7 +32,6 @@ package-lock.json
# remove from npm package
# --------------------------------------------------
packages/**/*.spec.js
-packages/**/*.d.ts
# --------------------------------------------------
# Dependency directories

MODIFY
jest.config.js
jest.config.js
@@ -28,7 +28,10 @@ module.exports = {
],
globals: {
'ts-jest': {
- tsConfigFile: './tsconfig.json',
- }
+ tsConfig: './tsconfig.test.json',
+ diagnostics: {
+ ignoreCodes: [2300],
+ },
+ },
},
};

MODIFY
packages/@integration/package.json
packages/@integration/package.json
"@marblejs/core": "^1.2.1",
"@marblejs/middleware-body": "^1.2.1",
"@marblejs/middleware-logger": "^1.2.1",
+ "@marblejs/middleware-joi": "^1.2.1",
"@types/node": "^10.1.2",
"rxjs": "~6.2.2",
"typescript": "~3.0.3"

MODIFY
packages/@integration/src/effects/api.effects.ts
packages/@integration/src/effects/api.effects.ts
-import { EffectFactory, HttpError, HttpStatus, combineRoutes } from '@marblejs/core';
+import { EffectFactory, HttpError, HttpStatus, combineRoutes, use } from '@marblejs/core';
+import { validator$, Joi } from '@marblejs/middleware-joi';
import { throwError } from 'rxjs';
-import { map, switchMap } from 'rxjs/operators';
+import { map, mergeMap } from 'rxjs/operators';
import { user$ } from './user.effects';
import { static$ } from './static.effects';
+const rootValidator$ = validator$({
+ params: {
+ version: Joi.string().required(),
+ },
+}, { allowUnknown: true });
+
const root$ = EffectFactory
.matchPath('/')
.matchType('GET')
.use(req$ => req$.pipe(
+ use(rootValidator$),
map(req => req.params.version),
map(version => ({ body: `API version: ${version}` })),
));
@@ -15,21 +23,19 @@ const root$ = EffectFactory
const notImplemented$ = EffectFactory
.matchPath('/error')
.matchType('GET')
- .use(req$ => req$
- .pipe(
- switchMap(() => throwError(
+ .use(req$ => req$.pipe(
+ mergeMap(() => throwError(
new HttpError('Route not implemented', HttpStatus.NOT_IMPLEMENTED, { reason: 'Not implemented' })
)),
- )
- );
+ ));
const notFound$ = EffectFactory
.matchPath('*')
.matchType('*')
.use(req$ => req$.pipe(
- switchMap(() =>
- throwError(new HttpError('Route not found', HttpStatus.NOT_FOUND))
- )
+ mergeMap(() => throwError(
+ new HttpError('Route not found', HttpStatus.NOT_FOUND)
+ )),
));
export const api$ = combineRoutes(

MODIFY
packages/@integration/src/effects/static.effects.ts
packages/@integration/src/effects/static.effects.ts
import * as path from 'path';
-import { EffectFactory, combineRoutes } from '@marblejs/core';
+import { EffectFactory, combineRoutes, use } from '@marblejs/core';
+import { validator$, Joi } from '@marblejs/middleware-joi';
import { readFile } from '@marblejs/core/dist/+internal';
-import { map, switchMap } from 'rxjs/operators';
+import { map, mergeMap } from 'rxjs/operators';
const STATIC_PATH = path.resolve(__dirname, '../../../../assets');
+const getFileValidator$ = validator$({
+ params: {
+ dir: Joi.string().required(),
+ },
+}, { allowUnknown: true });
+
const getFile$ = EffectFactory
.matchPath('/:dir*')
.matchType('GET')
- .use(req$ => req$
- .pipe(
- map(req => req.params!.dir as string),
- switchMap(readFile(STATIC_PATH)),
+ .use(req$ => req$.pipe(
+ use(getFileValidator$),
+ map(req => req.params.dir),
+ mergeMap(readFile(STATIC_PATH)),
map(body => ({ body }))
));

MODIFY
packages/@integration/src/effects/user.effects.ts
packages/@integration/src/effects/user.effects.ts
-import { EffectFactory, combineRoutes, HttpError, HttpStatus } from '@marblejs/core';
-import { throwError } from 'rxjs';
-import { map, switchMap, catchError } from 'rxjs/operators';
+import { EffectFactory, combineRoutes, HttpError, HttpStatus, use } from '@marblejs/core';
+import { validator$, Joi } from '@marblejs/middleware-joi';
+import { throwError, of } from 'rxjs';
+import { map, switchMap, catchError, mergeMap } from 'rxjs/operators';
import { Dao } from '../fakes/dao.fake';
import { authorize$ } from '../middlewares/auth.middleware';
-const getUsers$ = EffectFactory
+const getUserValidator$ = validator$({
+ params: {
+ id: Joi.string(),
+ },
+}, { allowUnknown: true });
+
+const getUserList$ = EffectFactory
.matchPath('/')
.matchType('GET')
.use(req$ => req$.pipe(
- switchMap(Dao.getUsers),
+ mergeMap(Dao.getUsers),
map(users => ({ body: users })),
));
@@ -16,12 +23,15 @@ const getUser$ = EffectFactory
.matchPath('/:id')
.matchType('GET')
.use(req$ => req$.pipe(
+ use(getUserValidator$),
+ mergeMap(req => of(req).pipe(
map(req => req.params.id),
switchMap(Dao.getUserById),
map(user => ({ body: user })),
- catchError(() =>
- throwError(new HttpError('User does not exist', HttpStatus.NOT_FOUND))
- )
+ catchError(() => throwError(
+ new HttpError('User does not exist', HttpStatus.NOT_FOUND)
+ ))
+ )),
));
const postUser$ = EffectFactory
@@ -29,11 +39,11 @@ const postUser$ = EffectFactory
.matchType('POST')
.use(req$ => req$.pipe(
map(req => req.body),
- switchMap(Dao.postUser),
+ mergeMap(Dao.postUser),
map(response => ({ body: response })),
));
export const user$ = combineRoutes('/user', {
middlewares: [authorize$],
- effects: [getUsers$, getUser$, postUser$],
+ effects: [getUserList$, getUser$, postUser$],
});

MODIFY
packages/@integration/src/fakes/dao.fake.ts
packages/@integration/src/fakes/dao.fake.ts
-import { of, iif, throwError } from 'rxjs';
+import { of, throwError } from 'rxjs';
export namespace Dao {

MODIFY
packages/core/src/http.interface.ts
packages/core/src/http.interface.ts
@@ -3,9 +3,9 @@ import { Observable } from 'rxjs';
import { EffectHttpResponse } from './effects/effects.interface';
export interface HttpRequest<
- TBody = any,
- TParams = any,
- TQuery = any,
+ TBody = unknown,
+ TParams = unknown,
+ TQuery = unknown,
> extends http.IncomingMessage {
url: string;
method: HttpMethod;

MODIFY
packages/core/src/operators/use/use.operator.spec.ts
packages/core/src/operators/use/use.operator.spec.ts
-import { tap } from 'rxjs/operators';
+import { Observable } from 'rxjs';
+import { tap, map } from 'rxjs/operators';
import { Marbles } from '../../+internal';
-import { Middleware } from '../../effects/effects.interface';
+import { Middleware, Effect } from '../../effects/effects.interface';
import { HttpRequest } from '../../http.interface';
import { use } from './use.operator';
-const createMockReq = (test = 0) => ({ test } as any as HttpRequest);
+const createMockReq = (obj: Record<string, any>) => (obj as any as HttpRequest);
-const middleware$: Middleware = req$ =>
- req$.pipe(tap(req => req.test++));
+describe('use.operator', () => {
+ test('applies middlewares to the request pipeline', () => {
+ const middleware$: Middleware<HttpRequest<number>> = req$ =>
+ req$.pipe(tap(req => req.body!++ ));
-describe('Use operator', () => {
-
- it('applies middlewares to the request pipeline', () => {
const operators = [
use(middleware$),
use(middleware$)
];
Marbles.assert(operators, [
- ['-a---', { a: createMockReq() }],
- ['-a---', { a: createMockReq(2) }],
+ ['-a---', { a: createMockReq({ body: 0 }) }],
+ ['-a---', { a: createMockReq({ body: 2 }) }],
]);
});
+ test('infers types from composed middlewares', () => {
+ interface AuthorizedHttpRequest extends HttpRequest {
+ user: { id: string };
+ }
+
+ const m1$ = <_ extends HttpRequest>(req$: Observable<HttpRequest>):
+ Observable<HttpRequest<{ test: string }, unknown, unknown>> =>
+ req$.pipe(
+ tap(req => req.body = { test: 'test' }),
+ );
+
+ const m2$ = <_ extends HttpRequest>(req$: Observable<HttpRequest>):
+ Observable<HttpRequest<unknown, { test: boolean }, unknown>> =>
+ req$.pipe(
+ tap(req => req.params.test = true),
+ );
+
+ const m3$ = <_ extends HttpRequest>(req$: Observable<HttpRequest>):
+ Observable<HttpRequest<unknown, unknown, { test: number }>> =>
+ req$.pipe(
+ tap(req => req.query.test = 3),
+ );
+
+ const m4$ = <_ extends HttpRequest>(req$: Observable<HttpRequest>):
+ Observable<AuthorizedHttpRequest> =>
+ req$.pipe(
+ tap(req => req.user = { id: 'test_id' }),
+ );
+
+ const effect$: Effect = req$ => req$.pipe(
+ use(m1$),
+ use(m2$),
+ use(m3$),
+ use(m4$),
+ tap(req => req.body!.test as string),
+ tap(req => req.params.test as boolean),
+ tap(req => req.query.test as number),
+ tap(req => req.user.id as string),
+ map(req => ({ body: {
+ body: req.body,
+ params: req.params,
+ query: req.query,
+ user: req.user,
+ }})),
+ );
+
+ const expectedResult = {
+ body: {
+ body: { test: 'test' },
+ params: { test: true },
+ query: { test: 3 },
+ user: { id: 'test_id' },
+ }
+ };
+
+ Marbles.assertEffect(effect$, [
+ ['-a---', { a: createMockReq({ params: {}, query: {} }) }],
+ ['-a---', { a: expectedResult }],
+ ]);
+ });
});

MODIFY
packages/core/src/operators/use/use.operator.ts
packages/core/src/operators/use/use.operator.ts
@@ -5,7 +5,7 @@ import { HttpRequest, HttpResponse } from '../../http.interface';
export const use = <I extends HttpRequest, O extends HttpRequest>
(middleware: Middleware<I, O>, res?: HttpResponse) =>
- (source$: Observable<I>): Observable<O> =>
+ (source$: Observable<I>) =>
source$.pipe(
- switchMap(req => middleware(of(req), res!, {}))
+ switchMap(req => middleware(of(req), res!, {}) as Observable<I & O>)
);

MODIFY
packages/middleware-joi/package.json
packages/middleware-joi/package.json
"rxjs": "~6.2.2"
},
"dependencies": {
- "joi": "14.0.6",
+ "joi": "~14.3.0",
"@types/joi": "^14.0.0"
},
"devDependencies": {

RENAME
packages/middleware-joi/src/joi.types.ts
packages/middleware-joi/src/validator.interface.ts
-import 'joi';
+import * as Joi from 'joi';
+import { Schema } from './validator.schema';
+
+export type ExtractedSchema<SchemaToExtract extends Schema> = {
+ [K1 in keyof SchemaToExtract]:
+ SchemaToExtract[K1] extends undefined
+ ? unknown
+ : { [K2 in keyof SchemaToExtract[K1]]: Joi.ExtractType<SchemaToExtract[K1][K2]> }
+};
+
+export type ExtractedBody<T extends Schema> = ExtractedSchema<T>['body'];
+export type ExtractedParams<T extends Schema> = ExtractedSchema<T>['params'];
+export type ExtractedQuery<T extends Schema> = ExtractedSchema<T>['query'];
declare module 'joi' {
// Object Schema
@@ -23,5 +35,5 @@ declare module 'joi' {
U extends null ? Record<string, any> : { [K in keyof U]: ExtractType<U[K]> } :
T extends ArraySchema<infer U> ?
U extends null ? any[] : U[] :
- any;
+ unknown;
}

MODIFY
packages/middleware-joi/src/validator.middleware.ts
packages/middleware-joi/src/validator.middleware.ts
import * as Joi from 'joi';
-import './joi.types';
+import './validator.interface';
import { HttpRequest, HttpError, HttpStatus } from '@marblejs/core';
import { from, of, throwError, Observable } from 'rxjs';
import { mergeMap, flatMap, catchError, mapTo, switchMap, toArray, map } from 'rxjs/operators';
import { Schema, SchemaValidator } from './validator.schema';
+import { ExtractedBody, ExtractedParams, ExtractedQuery } from './validator.interface';
const validateSource = (rules: Map<string, any>, options: Joi.ValidationOptions) => (req: HttpRequest) =>
from(rules.keys()).pipe(
@@ -35,19 +36,12 @@ export const validator$ = <TBody = any, TParams = any, TQuery = any>
return throwError(result.error);
}
- type ExtractedSchema = {
- [K1 in keyof typeof schema]:
- typeof schema[K1] extends undefined
- ? any
- : { [K2 in keyof typeof schema[K1]]: Joi.ExtractType<typeof schema[K1][K2]> }
- };
-
- type ExtractedBody = ExtractedSchema['body'];
- type ExtractedParams = ExtractedSchema['params'];
- type ExtractedQuery = ExtractedSchema['query'];
-
return req$.pipe(
switchMap(validateSource(rules, options)),
- map(req => req as HttpRequest<ExtractedBody, ExtractedParams, ExtractedQuery>),
+ map(req => req as HttpRequest<
+ ExtractedBody<Required<typeof schema>>,
+ ExtractedParams<Required<typeof schema>>,
+ ExtractedQuery<Required<typeof schema>>
+ >),
);
};

MODIFY
tsconfig.json
tsconfig.json
],
"baseUrl": "."
},
- "exclude": [
- "**/*.d.ts"
+ "include": [
+ "./packages/**/*.ts",
+ "./packages/**/*.d.ts",
+ "./packages/**/*.json"
]
}

ADD
tsconfig.test.json
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "noEmitOnError": false
+ }
+}

MODIFY
yarn.lock
yarn.lock
@@ -3331,9 +3331,9 @@ jest@~23.6.0:
import-local "^1.0.0"
jest-cli "^23.6.0"
-joi@14.0.6:
- version "14.0.6"
- resolved "https://registry.yarnpkg.com/joi/-/joi-14.0.6.tgz#8c72314af6dba13ca48028a4f0af992ddc5a7472"
+joi@~14.3.0:
+ version "14.3.0"
+ resolved "https://registry.yarnpkg.com/joi/-/joi-14.3.0.tgz#55f7c5caa8256de74ccb12eb22ab1c19eea02db3"
dependencies:
hoek "6.x.x"
isemail "3.x.x"

imp(core, middleware-joi, @integration): improved type-inference of combined middlewares