feat(middleware-joi): type inference for basic types (string, boolean, number, object)
MODIFY
packages/middleware-joi/src/index.ts
packages/middleware-joi/src/index.ts
import * as Joi from 'joi';
-import { Schema, SchemaValidator } from './schema';
-import { HttpRequest, HttpError, HttpStatus, Effect } from '@marblejs/core';
-import { from, of, throwError } from 'rxjs';
-import { mergeMap, flatMap, catchError, mapTo, switchMap, toArray, map } from 'rxjs/operators';
-const validateSource = (rules: Map<string, any>, options: Joi.ValidationOptions) => (req: HttpRequest) =>
- from(rules.keys()).pipe(
- mergeMap(rule =>
- of(req[rule]).pipe(
- flatMap(item =>
- from(Joi.validate(item || {}, rules.get(rule), options)).pipe(
- catchError((err: Joi.ValidationError) => {
- const message = err.details[0].message;
- return throwError(new HttpError(message, HttpStatus.BAD_REQUEST));
- })
- )
- ),
- map(result => (req[rule] = result))
- )
- ),
- toArray(),
- mapTo(req)
- );
-
-const validator$ = (schema: Schema, options: Joi.ValidationOptions = {}): Effect<HttpRequest> => req$ => {
- const result = Joi.validate(schema, SchemaValidator);
- const rules = Object.keys(schema).reduce(
- (acc, value) => acc.set(value, Joi.compile(schema[value])),
- new Map()
- );
-
- if (result && result.error) {
- return throwError(result.error);
- }
-
- return req$.pipe(
- switchMap(validateSource(rules, options))
- );
-};
-
-export { Joi, validator$ };
+// package public API
+export { validator$ } from './validator.middleware';
+export { Joi };

MODIFY
packages/middleware-joi/src/schema.ts
packages/middleware-joi/src/schema.ts
-import { object, ObjectSchema } from 'joi';
+import * as Joi from 'joi';
-export const SchemaValidator: ObjectSchema = object().keys({
- headers: object(),
- params: object(),
- query: object(),
- body: object(),
+export const SchemaValidator = Joi.object().keys({
+ headers: Joi.object(),
+ params: Joi.object(),
+ query: Joi.object(),
+ body: Joi.object(),
}).min(1);
-export interface Schema {
- headers?: any;
- params?: any;
- query?: any;
- body?: any;
+export type ExtractType<T extends Joi.SchemaLike> =
+ T extends Joi.NumberSchema ? number :
+ T extends Joi.BooleanSchema ? boolean :
+ T extends Joi.StringSchema ? string :
+ T extends Joi.ObjectSchema ? object :
+ any;
+
+export interface Schema<
+ TBody = any,
+ TParams = any,
+ TQuery = any,
+ THeaders = any,
+> {
+ body: TBody;
+ params: TParams;
+ query: TQuery;
+ headers: THeaders;
}

ADD
packages/middleware-joi/src/validator.middleware.ts
+import * as Joi from 'joi';
+import { Schema, SchemaValidator, ExtractType } from './schema';
+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';
+
+const validateSource = (rules: Map<string, any>, options: Joi.ValidationOptions) => (req: HttpRequest) =>
+ from(rules.keys()).pipe(
+ mergeMap(rule => of(req[rule]).pipe(
+ flatMap(item =>
+ from(Joi.validate(item || {}, rules.get(rule), options)).pipe(
+ catchError((err: Joi.ValidationError) => {
+ const message = err.details[0].message;
+ return throwError(new HttpError(message, HttpStatus.BAD_REQUEST));
+ })
+ )
+ ),
+ map(result => (req[rule] = result))
+ )),
+ toArray(),
+ mapTo(req)
+ );
+
+export const validator$ = <TBody = any, TParams = any, TQuery = any>
+ (schema: Partial<Schema<TBody, TParams, TQuery>>, options: Joi.ValidationOptions = {}) =>
+ (req$: Observable<HttpRequest>) => {
+ const result = Joi.validate(schema, SchemaValidator);
+ const rules = Object.keys(schema).reduce(
+ (acc, value) => acc.set(value, Joi.compile(schema[value])),
+ new Map()
+ );
+
+ if (result && result.error) {
+ return throwError(result.error);
+ }
+
+ type ValidatorSchema = Schema<TBody, TParams, TQuery>;
+ type ExtractedSchema = {
+ [K1 in keyof ValidatorSchema]: {
+ [K2 in keyof ValidatorSchema[K1]]: ExtractType<ValidatorSchema[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>),
+ );
+ };

feat(middleware-joi): type inference for basic types (string, boolean, number, object)