Last active
December 22, 2023 21:34
-
-
Save crutch12/fa367767ef30b2be0bea2d105fcc71f0 to your computer and use it in GitHub Desktop.
Msw.js Rest class typed with Taxios scheme (msw@1)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// @NOTE: ONLY msw@1 (!) | |
// msw@2 is not supported | |
import { MockedResponse, ResponseComposition, rest, RestContext, RestRequest } from 'msw'; | |
import { DefaultRequestBody, ResponseResolver } from 'msw/lib/types/handlers/RequestHandler'; | |
import { PathParams } from 'msw/lib/types/utils/matching/matchRequestUrl'; | |
import qs from 'qs'; | |
import { ConditionalPick } from 'type-fest'; | |
import urljoin from 'url-join'; | |
/** | |
* Метод, который конвертирует параметры пути к формату :ids, так как изначально приходит путь формата {ids} | |
* @param swaggerPath Исходный сваггер путь | |
* @example | |
* convertPath(/contractors/byIds/{ids}); | |
* return /contractors/byIds/:ids | |
*/ | |
const convertPath = (swaggerPath: string) => { | |
const mswPath = swaggerPath.replace(/}/gi, '').replace(/{/gi, ':'); | |
return mswPath; | |
}; | |
type Method = 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'OPTIONS' | 'PATCH'; | |
type Field = 'query' | 'params' | 'body' | 'response'; | |
type Scheme = { | |
version: '1'; | |
routes: { | |
[route in string]: { | |
[method in Method]?: { | |
[field in Field]?: unknown; | |
}; | |
}; | |
}; | |
}; | |
type Routes<T extends Scheme, M extends Method> = keyof ConditionalPick<T['routes'], Partial<Record<M, unknown>>>; | |
type FieldType<T extends Scheme, M extends Method, Route extends Routes<T, M>, F extends Field> = Required< | |
T['routes'][Route] | |
>[M] extends Partial<Record<F, any>> | |
? Required<Required<T['routes'][Route]>[M]>[F] | |
: never; | |
/** | |
* msw хранит параметры как строки или массив строк, поэтому нужно привести все значения params из Taxios к строке или массиву строк | |
*/ | |
type Params<T extends Scheme, M extends Method, Route extends Routes<T, M>> = { | |
[K in keyof FieldType<T, M, Route, 'params'>]: FieldType<T, M, Route, 'params'>[K] extends unknown[] | |
? string[] | |
: string; | |
}; | |
/** | |
* Расширяем тип RestRequest, добавляя поле query, который берем из taxios`а, а типы приводим к строке (т.к. это query запроса). | |
*/ | |
interface RestRequestWithQuery< | |
BodyType extends DefaultRequestBody = DefaultRequestBody, | |
ParamsType extends PathParams = PathParams, | |
T extends Scheme = Scheme, | |
M extends Method = Method, | |
Route extends Routes<T, M> = Routes<T, M>, | |
Q = Required<FieldType<T, M, Route, 'query'>>, | |
> extends RestRequest<BodyType, ParamsType> { | |
query: { | |
[K in keyof Q]: | |
| (FieldType<T, M, Route, 'query'>[K] extends undefined ? undefined : never) | |
| (Q[K] extends unknown[] ? string[] : string); | |
}; | |
} | |
type ReqType<T extends Scheme, M extends Method, K extends Routes<T, M>> = Record<M, unknown> extends | |
| { GET: unknown } | |
| { HEAD: unknown } | |
? RestRequestWithQuery<never, Params<T, M, K>, T, M, K> | |
: RestRequestWithQuery<FieldType<T, M, K, 'body'>, Params<T, M, K>, T, M, K>; | |
type HandlerType<T extends Scheme, M extends Method, K extends Routes<T, M>> = ( | |
req: ReqType<T, M, K>, | |
res: ResponseComposition<FieldType<T, M, K, 'response'>>, | |
ctx: RestContext, | |
) => MockedResponse<FieldType<T, M, K, 'response'>> | Promise<MockedResponse<FieldType<T, M, K, 'response'>>>; | |
/** | |
* Класс, который типизирует методы msw REST с помощью taxios схемы | |
* @see https://github.com/simplesmiler/taxios | |
* @see https://mswjs.io/ | |
*/ | |
export class TypedRest<T extends Scheme> { | |
private rest: typeof rest; | |
private readonly baseURL?: string; | |
/** | |
* @param _rest Объект, который содержит набор обработчиков запросов, предназначенных для удобного имитации запросов REST API. | |
* @param baseURL базовый URL, будет добавлен ко всем путям | |
*/ | |
constructor(_rest: typeof rest, baseURL?: string) { | |
this.rest = _rest; | |
this.baseURL = baseURL; | |
} | |
/** | |
* Обертка над обработчиком, чтобы прокинуть query в объект RestRequest | |
*/ | |
private prepareHandler<M extends Method, K extends Routes<T, M>>(handler: HandlerType<T, M, K>) { | |
return ( | |
req: Record<M, unknown> extends { GET: unknown } | { HEAD: unknown } | |
? RestRequest<never, Params<T, M, K>> | |
: RestRequest<FieldType<T, M, K, 'body'>, Params<T, M, K>>, | |
res: ResponseComposition<FieldType<T, M, K, 'response'>>, | |
ctx: RestContext, | |
) => { | |
const reqWithQuery = req as ReqType<T, M, K>; | |
const query = qs.parse(req.url.search, { ignoreQueryPrefix: true }); | |
reqWithQuery.query = query as typeof reqWithQuery.query; | |
return handler(reqWithQuery, res, ctx); | |
}; | |
} | |
private preparePath(path: string) { | |
return convertPath(this.baseURL ? urljoin(this.baseURL, path) : path); | |
} | |
public get<K extends Routes<T, 'GET'>>(path: K, handler: HandlerType<T, 'GET', K>) { | |
return this.rest.get<never, Params<T, 'GET', K>, FieldType<T, 'GET', K, 'response'>>( | |
this.preparePath(path as string), | |
this.prepareHandler(handler), | |
); | |
} | |
public post<K extends Routes<T, 'POST'>>(path: K, handler: HandlerType<T, 'POST', K>) { | |
return this.rest.post<FieldType<T, 'POST', K, 'body'>, Params<T, 'POST', K>, FieldType<T, 'POST', K, 'response'>>( | |
this.preparePath(path as string), | |
this.prepareHandler(handler), | |
); | |
} | |
public put<K extends Routes<T, 'PUT'>>(path: K, handler: HandlerType<T, 'PUT', K>) { | |
return this.rest.put<FieldType<T, 'PUT', K, 'body'>, Params<T, 'PUT', K>, FieldType<T, 'PUT', K, 'response'>>( | |
this.preparePath(path as string), | |
this.prepareHandler(handler), | |
); | |
} | |
public delete<K extends Routes<T, 'DELETE'>>(path: K, handler: HandlerType<T, 'DELETE', K>) { | |
return this.rest.delete< | |
FieldType<T, 'DELETE', K, 'body'>, | |
Params<T, 'DELETE', K>, | |
FieldType<T, 'DELETE', K, 'response'> | |
>(this.preparePath(path as string), this.prepareHandler(handler)); | |
} | |
public head<K extends Routes<T, 'HEAD'>>(path: K, handler: HandlerType<T, 'HEAD', K>) { | |
return this.rest.head<never, Params<T, 'HEAD', K>, FieldType<T, 'HEAD', K, 'response'>>( | |
this.preparePath(path as string), | |
this.prepareHandler(handler), | |
); | |
} | |
public patch<K extends Routes<T, 'PATCH'>>(path: K, handler: HandlerType<T, 'PATCH', K>) { | |
return this.rest.patch< | |
FieldType<T, 'PATCH', K, 'body'>, | |
Params<T, 'PATCH', K>, | |
FieldType<T, 'PATCH', K, 'response'> | |
>(this.preparePath(path as string), this.prepareHandler(handler)); | |
} | |
public options<K extends Routes<T, 'OPTIONS'>>(path: K, handler: HandlerType<T, 'OPTIONS', K>) { | |
return this.rest.options< | |
FieldType<T, 'OPTIONS', K, 'body'>, | |
Params<T, 'OPTIONS', K>, | |
FieldType<T, 'OPTIONS', K, 'response'> | |
>(this.preparePath(path as string), this.prepareHandler(handler)); | |
} | |
public all<K extends keyof T['routes']>( | |
path: K, | |
handler: ResponseResolver<RestRequest<DefaultRequestBody, PathParams>, RestContext, DefaultRequestBody>, | |
) { | |
return this.rest.all(this.preparePath(path as string), handler); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment