diff --git a/.changeset/chilled-trains-move.md b/.changeset/chilled-trains-move.md new file mode 100644 index 0000000..5e9526b --- /dev/null +++ b/.changeset/chilled-trains-move.md @@ -0,0 +1,15 @@ +--- +"@acquirejs/mocks": minor +"@acquirejs/core": minor +--- + +Refactored core package (BREAKING CHANGE) + +- Added AcquireBase class to deal with shared middleware. +- Renamed AcquireRequestExecutor class to AcquireRequestHandler. +- Reworked AcquireRequestHandlers call signature. +- Reworked Acquire class to generate request handlers through the createRequestHandler method. +- Added AcquireRequestHandlerFactory class to deal with request handler creation. +- Renamed AcquireRequestLogger middleware to RequestLogger. +- Added DelaySimulator middleware. +- Updated README. diff --git a/README.md b/README.md index a2f3269..c822c4d 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,9 @@ AcquireJS is a TypeScript library designed to streamline the process of working with REST APIs. The library aims to solve three common pain-points in the data fetching/mutation process: -- πŸ”’ Type safety - Ensure that data going into and out of your application from external APIs is type safe. +- πŸ”’ Type safety - Ensure that data going into and out of your application from REST APIs is type safe. -- πŸ—ΊοΈ Data mapping - Decide how data going into and out of your application from external APIs should be mapped, using a declarative approach. +- πŸ—ΊοΈ Data mapping - Decide how data going into and out of your application from REST APIs should be mapped, using a declarative approach. - 🎭 Mocking and testing - Easily mock data and API responses in order to test your code. Mock Data Transfer Objects (DTOs) to write unit tests at the function and component level, mock API calls to write integration tests at the page level or mock API calls with relational data to write End-to-End (E2E) tests at the application level. @@ -55,6 +55,18 @@ yarn add @acquirejs/core reflect-metadata ``` > πŸ’‘ Tip: AcquireJS is built on [axios](https://axios-http.com/docs/intro). If you want to specify an axios config, you should also install axios. +> +> Using npm: +> +> ```bash +> npm install axios +> ``` +> +> Using yarn: +> +> ```bash +> yarn add axios +> ``` --- @@ -122,7 +134,7 @@ This will allow multiple requests to share the same default settings, like base A key concept in AcquireJS is to use two sets of classes for each endpoint: A DTO class and a Model class. The DTO (Data Transfer Object) is a class representing the data as delivered from or to the server. It should only contain JSON primitive values. -Imagine an example endpoint (`http://api.example.com/users/1`) that returns a user JSON response that looks like this: +Imagine an example endpoint (`http://api.example.com/users/1`) that returns a user JSON response in the following format: ```json { @@ -200,29 +212,23 @@ import acquire from "../../acquire.ts"; import UserDTO from "./dtos/UserDTO.ts"; import UserModel from "./models/UserModel.ts"; -export const getUser = acquire({ - request: { +export const getUser = acquire + .createRequestHandler() + .withResponseMapping(UserModel, UserDTO) + .get({ url: "http://api.example.com/users/1" - }, - responseMapping: { - DTO: UserDTO, - Model: UserModel - } -}); + }); ``` > πŸ’‘ Tip: If you instantiated `Acquire` using an `axios` instance with a `baseURL` of `http://api.example.com/`, you could instead just set the `path`: > > ```typescript -> export const getUser = acquire({ -> request: { +> export const getUser = acquire +> .createRequestHandler() +> .withResponseMapping(UserModel, UserDTO) +> .get({ > path: "/users/1" -> }, -> responseMapping: { -> DTO: UserDTO, -> Model: UserModel -> } -> }); +> }); > ``` ### Calling the request executor @@ -230,7 +236,7 @@ export const getUser = acquire({ The request can now be executed: ```typescript -import { getUser } from "path/to/getUser"; +import { getUser } from "path-to-getUser"; const user = await getUser(); @@ -247,28 +253,25 @@ Here, `user.model` is typed and mapped according to the `UserModel` class! πŸŽ‰ ### Using dynamic request arguments -In the previous example, the ID of the user was hard-coded into the url, causing `getUser` to always return the user with ID 1. This was only shown as a simplistic example, but in general, the ID should be passed as an argument to the `getUser` method. This can be done by passing an object with a `userId` key to the `getUser` method. All properties on the `request` object can be set as values or callbacks that take `callArgs` as the argument. The type of `callArgs` can be set by binding `callArgs` to the method using the `withCallArgs` method on the `Acquire` instance: +In the previous example, the ID of the user was hard-coded into the url, causing `getUser` to always return the user with ID 1. This was only shown as a simplistic example, but in general, the ID should be passed as an argument to the `getUser` method. + +Setting dynamic request parameters can be done by passing a generic type (`TCallArgs`) to the `get` method. The generic type must extend an object and will be required when calling the `getUser` function. All request configuration properties can be set as values or callbacks that take `TCallArgs` as the argument. In the following example, the `userId` is injected into the path of the url: ```typescript -export const getUser = acquire.withCallArgs<{ userId: number }>()({ - request: { - url: (callArgs) => `http://api.example.com/users/${callArgs?.userId}` - }, - responseMapping: { - DTO: UserDTO, - Model: UserModel - } -}); +export const getUser = acquire + .createRequestHandler() + .withResponseMapping(UserModel, UserDTO) + .get<{ userId: number }>({ + url: ({ userId }) => `http://api.example.com/users/${userId}` + }); ``` -And calling the method like so: +`getUser` is then called like so: ```typescript -const user = await getUser({ userId: 10 }); +const { model: user } = await getUser({ userId: 10 }); ``` -> πŸ” Caveat: Note that when using `withCallArgs`, the function is curried. Don't forget the additional set of parenthesis! - --- ### Requests that return arrays @@ -276,15 +279,12 @@ const user = await getUser({ userId: 10 }); Endpoints that return lists of items typically return a JSON array response. When working with endpoints that directly return arrays, the DTO and Model can be wrapped in an array: ```typescript -export const getUsers = acquire({ - request: { +export const getUsers = acquire + .createRequestHandler() + .withResponseMapping([UserModel], [UserDTO]) // πŸ‘ˆ notice the square brackets! + .get({ url: "http://api.example.com/users" - }, - responseMapping: { - DTO: [UserDTO], - Model: [UserModel] - } -}); + }); ``` Now, the return type of `getUsers` has `model` typed as a `UserModel[]` and `dto` as `UserDTO[]`. @@ -293,7 +293,7 @@ Now, the return type of `getUsers` has `model` typed as a `UserModel[]` and `dto ### Mutations -AcquireJS can also perform mutations. In this case, the request `method` can be specified in the `request` argument. Additionally, a `requestMapping` can be provided, similar to the `responseMapping`. In general, the DTO used for queries and mutations may differ. For instance, the `UserDTO` in the previous example has information about the ID of the user, if the user is currently active, as well as when the user was created, last updated and last active. While this information is not included in the body of a `POST` request, it may appear in the response of the request. Hence, separate `CreateUserDTO` and `CreateUserModel` classes can be created to deal with the outgoing data: +AcquireJS can also perform mutations. In this case, a request method other than `get` (e.g., `put`, `post`, `delete`) can be used as the final function to end the chaining. Additionally, a `withRequestMapping` can be provided, similar to the `withResponseMapping`. In general, the DTO used for queries and mutations may differ. For instance, the `UserDTO` in the previous example has information about the ID of the user, if the user is currently active, as well as when the user was created, last updated and last active. While this information is not included in the body of a `post` request, it may appear in the response of the same request. Hence, separate `CreateUserDTO` and `CreateUserModel` classes can be created to deal with the outgoing data: ```typescript // src/api/users/dtos/CreateUserDTO.ts @@ -338,22 +338,22 @@ import CreateUserDTO from "./dtos/CreateUserDTO.ts"; import UserModel from "./models/UserModel.ts"; import CreateUserModel from "./models/CreateUserModel.ts"; -export const getUser = acquire(/* From the previous example...*/); - -export const createUser = acquire({ - request: { - url: "http://api.example.com/users", - method: "POST" - }, - requestMapping: { - DTO: CreateUserDTO, - Model: CreateUserModel - }, - responseMapping: { - DTO: UserDTO, - Model: UserModel - } -}); +// From the previous example... +export const getUser = acquire + .createRequestHandler() + .withResponseMapping(UserModel, UserDTO) + .get<{ userId: number }>({ + url: ({ userId }) => `http://api.example.com/users/${userId}` + }); + +// Adding a mutation +export const createUser = acquire + .createRequestHandler() + .withRequestMapping(CreateUserModel, CreateUserDTO) + .withResponseMapping(UserModel, UserDTO) + .post({ + url: "http://api.example.com/users" + }); ``` To pass this data to the `createUser` method, `data` can be set in the argument: @@ -366,7 +366,7 @@ const user = await createUser({ email: "janedoe@example.com", phoneNumber: "+1987654321", role: "basic-user" - } + } // πŸ‘ˆ The type of `data` is dictated by `CreateUserModel` }); ``` @@ -385,9 +385,9 @@ In the examples above, the `UserDTO` class was always specified, but was not rea > πŸ’‘ Tip: To see if a request is executed or mocked, and to see if the mocking is on demand or from interceptors, you can attach a logger to the `Acquire` instance: > > ```typescript -> import { Acquire, AcquireRequestLogger } from "@acquirejs/core"; +> import { Acquire, RequestLogger } from "@acquirejs/core"; > -> const acquire = new Acquire().use(new AcquireRequestLogger()); +> const acquire = new Acquire().use(new RequestLogger()); > > export default acquire; > ``` @@ -500,10 +500,10 @@ When mocking an AcquireJS request, no actual network request is executed. Instea This is mostly useful when writing simple unit tests, but is not that suited for testing components that fetch data, as it requires us to modify how the function is called. - > πŸ’‘ Tip: When calling a request executor function using `.mock()`, you can (in addition to the `callArgs`) provide a `$count` argument, i.e., + > πŸ’‘ Tip: When calling a request executor function using `.mock()`, the final argument passed to the function is the generated data count: > > ```typescript - > const users = await getUsers.mock({ $count: 100 }); + > const users = await getUsers.mock(100); > ``` > > This will decide how many objects are returned for functions that return arrays (in this case, 100 mock users are generated). The default count is 10. @@ -511,7 +511,7 @@ When mocking an AcquireJS request, no actual network request is executed. Instea 2. Enable mocking globally: ```typescript - import acquire from "path/to/acquire"; + import acquire from "path-to-acquire"; acquire.enableMocking(); // or @@ -640,28 +640,19 @@ import acquire from "../../acquire.ts"; import PostDTO from "./dtos/PostDTO.ts"; import PostModel from "./models/PostModel.ts"; -export const getPosts = acquire.withCallArgs<{ - createdByUserId?: number; - page?: number; - pageSize?: number; - sortBy?: keyof PostDTO; - sortByDescending?: boolean; -}>()({ - request: { +export const getPosts = acquire + .createRequestHandler() + .withResponseMapping(UserModel, UserDTO) + .get<{ + createdByUserId?: number; + page?: number; + pageSize?: number; + sortBy?: keyof PostDTO; + sortByDescending?: boolean; + }>({ url: "http://api.example.com/posts", - params: (callArgs) => ({ - createdByUserId: callArgs?.createdByUserId, - page: callArgs?.page, - pageSize: callArgs?.pageSize, - sortBy: callArgs?.sortBy, - sortByDescending: callArgs?.sortByDescending - }) - }, - responseMapping: { - DTO: UserDTO, - Model: UserModel - } -}); + params: (callArgs) => callArgs + }); ``` Here, it is assumed that the `/posts` endpoint can accept additional parameters which can be used to filter the returned posts. @@ -671,11 +662,11 @@ As the DTOs now have relations, it is necessary to store all the mocked data som ```typescript // src/api/acquire.ts -import { Acquire, AcquireLogger, AcquireMockCache } from "@acquirejs/core"; +import { Acquire, AcquireMockCache, RequestLogger } from "@acquirejs/core"; const acquire = new Acquire() .useMockCache(new AcquireMockCache()) - .use(new AcquireRequestLogger()); + .use(new RequestLogger()); export default acquire; ``` diff --git a/demos/vite-demo/src/api/acquire.ts b/demos/vite-demo/src/api/acquire.ts index d35c82c..657f568 100644 --- a/demos/vite-demo/src/api/acquire.ts +++ b/demos/vite-demo/src/api/acquire.ts @@ -1,18 +1,14 @@ -import { - Acquire, - AcquireMockCache, - AcquireRequestLogger -} from "@acquirejs/core"; +import { Acquire, AcquireMockCache, RequestLogger } from "@acquirejs/core"; import axios from "axios"; -const axiosInstance = axios.create({ - baseURL: "https://jsonplaceholder.typicode.com" -}); - export const mockCache = new AcquireMockCache(); -const acquire = new Acquire(axiosInstance) +const acquire = new Acquire( + axios.create({ + baseURL: "https://jsonplaceholder.typicode.com" + }) +) .useMockCache(mockCache) - .use(new AcquireRequestLogger()); + .use(new RequestLogger()); export default acquire; diff --git a/demos/vite-demo/src/api/comment/commentApi.ts b/demos/vite-demo/src/api/comment/commentApi.ts index 66aa228..d59a39c 100644 --- a/demos/vite-demo/src/api/comment/commentApi.ts +++ b/demos/vite-demo/src/api/comment/commentApi.ts @@ -4,36 +4,26 @@ import { CreateCommentDTO } from "./dtos/CreateCommentDTO"; import { CommentModel } from "./models/CommentModel"; import { CreateCommentModel } from "./models/CreateCommentModel"; -export const getComments = acquire.withCallArgs<{ - postId?: number; - sort?: keyof CommentDTO; - order?: "asc" | "desc"; -}>()({ - request: { +export const getComments = acquire + .createRequestHandler() + .withResponseMapping([CommentModel], [CommentDTO]) + .get<{ + postId?: number; + sort?: keyof CommentDTO; + order?: "asc" | "desc"; + }>({ path: "/comments", - params: (args) => ({ - postId: args?.postId, - _sort: args?.sort, - _order: args?.order + params: ({ postId, sort, order }) => ({ + postId: postId, + _sort: sort, + _order: order }) - }, - responseMapping: { - DTO: [CommentDTO], - Model: [CommentModel] - } -}); + }); -export const createComment = acquire({ - request: { - path: "/comments", - method: "POST" - }, - requestMapping: { - DTO: CreateCommentDTO, - Model: CreateCommentModel - }, - responseMapping: { - DTO: CommentDTO, - Model: CommentModel - } -}); +export const createComment = acquire + .createRequestHandler() + .withRequestMapping(CreateCommentModel, CreateCommentDTO) + .withResponseMapping(CommentModel, CommentDTO) + .post({ + path: "/comments" + }); diff --git a/demos/vite-demo/src/api/post/postApi.ts b/demos/vite-demo/src/api/post/postApi.ts index f235bc2..f9329c2 100644 --- a/demos/vite-demo/src/api/post/postApi.ts +++ b/demos/vite-demo/src/api/post/postApi.ts @@ -4,40 +4,30 @@ import { PostDTO } from "./dtos/PostDTO"; import { CreatePostModel } from "./models/CreatePostModel"; import { PostModel } from "./models/PostModel"; -export const getPosts = acquire.withCallArgs<{ - page?: number; - limit?: number; - sort?: keyof PostDTO; - order?: "asc" | "desc"; - userId?: number; -}>()({ - request: { +export const getPosts = acquire + .createRequestHandler() + .withResponseMapping([PostModel], [PostDTO]) + .get<{ + page?: number; + limit?: number; + sort?: keyof PostDTO; + order?: "asc" | "desc"; + userId?: number; + }>({ path: "/posts", - params: (args) => ({ - _page: args?.page, - _limit: args?.limit, - _sort: args?.sort, - _order: args?.order, - userId: args?.userId + params: ({ page, limit, sort, order, userId }) => ({ + _page: page, + _limit: limit, + _sort: sort, + _order: order, + userId: userId }) - }, - responseMapping: { - DTO: [PostDTO], - Model: [PostModel] - } -}); + }); -export const createPost = acquire({ - request: { - path: "/posts", - method: "POST" - }, - requestMapping: { - DTO: CreatePostDTO, - Model: CreatePostModel - }, - responseMapping: { - DTO: PostDTO, - Model: PostModel - } -}); +export const createPost = acquire + .createRequestHandler() + .withRequestMapping(CreatePostModel, CreatePostDTO) + .withResponseMapping(PostModel, PostDTO) + .post({ + path: "/posts" + }); diff --git a/demos/vite-demo/src/api/post/postApiMocking.ts b/demos/vite-demo/src/api/post/postApiMocking.ts index 6952bb3..68f3594 100644 --- a/demos/vite-demo/src/api/post/postApiMocking.ts +++ b/demos/vite-demo/src/api/post/postApiMocking.ts @@ -5,9 +5,9 @@ import { createPost, getPosts } from "./postApi"; getPosts.useOnMocking(({ response, mockCache, callArgs }) => { const { userId, page, limit, order, sort } = callArgs ?? {}; - const dbSimulator = mockCache?.createDatabaseSimulator(PostDTO); + const dbSimulator = mockCache!.createDatabaseSimulator(PostDTO); const data = dbSimulator - ?.filter(userId ? (post) => post.userId === userId : undefined) + .filter(userId ? (post) => post.userId === userId : undefined) .sort(sort, order) .paginate(page, limit, 1) .get(); @@ -15,16 +15,16 @@ getPosts.useOnMocking(({ response, mockCache, callArgs }) => { response.data = data; response.headers = { ...response.headers, - ["x-total-count"]: dbSimulator?.count() + ["x-total-count"]: dbSimulator.count() }; }); createPost.useOnMocking(({ response, mockCache }) => { - const dbSimulator = mockCache?.createDatabaseSimulator(PostDTO); - const id = dbSimulator?.generateNextID(); + const dbSimulator = mockCache!.createDatabaseSimulator(PostDTO); + const id = dbSimulator.generateNextID(); const newPost: PostDTO = { - ...response.config?.data, + ...response.config.data, id, userId: demoUser.id }; diff --git a/demos/vite-demo/src/api/user/userApi.ts b/demos/vite-demo/src/api/user/userApi.ts index e8c6ac2..c605310 100644 --- a/demos/vite-demo/src/api/user/userApi.ts +++ b/demos/vite-demo/src/api/user/userApi.ts @@ -2,22 +2,16 @@ import acquire from "../acquire"; import { UserDTO } from "./dtos/UserDTO"; import { UserModel } from "./models/UserModel"; -export const getUsers = acquire({ - request: { +export const getUsers = acquire + .createRequestHandler() + .withResponseMapping([UserModel], [UserDTO]) + .get({ path: "/users" - }, - responseMapping: { - DTO: [UserDTO], - Model: [UserModel] - } -}); + }); -export const getUser = acquire.withCallArgs<{ userId: number }>()({ - request: { - path: (args) => `/users/${args?.userId}` - }, - responseMapping: { - DTO: UserDTO, - Model: UserModel - } -}); +export const getUser = acquire + .createRequestHandler() + .withResponseMapping(UserModel, UserDTO) + .get<{ userId: number }>({ + path: ({ userId }) => `/users/${userId}` + }); diff --git a/packages/core/src/classes/Acquire.class.ts b/packages/core/src/classes/Acquire.class.ts index 6ae5887..6b009e5 100644 --- a/packages/core/src/classes/Acquire.class.ts +++ b/packages/core/src/classes/Acquire.class.ts @@ -1,162 +1,15 @@ -import { AcquireArgs } from "@/interfaces/AcquireArgs.interface"; -import { AcquireCallArgs } from "@/interfaces/AcquireCallArgs.interface"; -import { - AcquireMiddleware, - AcquireMiddlewareWithOrder -} from "@/interfaces/AcquireMiddleware.interface"; -import { ClassOrClassArray } from "@/interfaces/ClassOrClassArray.interface"; -import { OmitFirstArg } from "@/interfaces/OmitFirstArg.interface"; import axios, { AxiosInstance } from "axios"; +import AcquireBase from "./AcquireBase.class"; import AcquireMockCache from "./AcquireMockCache.class"; -import { - AcquireRequestExecutor, - AcquireRequestExecutorGetConfig -} from "./AcquireRequestExecutor.class"; -import { CallableInstance } from "./CallableInstance.class"; - -interface BoundAcquireRequestExecutor< - TCallArgs extends AcquireCallArgs = object, - TResponseDTO extends ClassOrClassArray = any, - TResponseModel extends ClassOrClassArray = any, - TRequestDTO extends ClassOrClassArray = any, - TRequestModel extends ClassOrClassArray = any -> { - /* - Note: All methods that should be exposed from AcquireRequestExecutor - to the callable Acquire instance should be specified explicitly here. - */ - ( - ...callArgs: Parameters< - OmitFirstArg< - AcquireRequestExecutor< - TCallArgs, - TResponseDTO, - TResponseModel, - TRequestDTO, - TRequestModel - >["execute"] - > - > - ): ReturnType< - AcquireRequestExecutor< - TCallArgs, - TResponseDTO, - TResponseModel, - TRequestDTO, - TRequestModel - >["execute"] - >; - - execute: OmitFirstArg< - AcquireRequestExecutor< - TCallArgs, - TResponseDTO, - TResponseModel, - TRequestDTO, - TRequestModel - >["execute"] - >; - mock: OmitFirstArg< - AcquireRequestExecutor< - TCallArgs, - TResponseDTO, - TResponseModel, - TRequestDTO, - TRequestModel - >["mock"] - >; - use: AcquireRequestExecutor< - TCallArgs, - TResponseDTO, - TResponseModel, - TRequestDTO, - TRequestModel - >["use"]; - useOnExecution: AcquireRequestExecutor< - TCallArgs, - TResponseDTO, - TResponseModel, - TRequestDTO, - TRequestModel - >["useOnExecution"]; - useOnMocking: AcquireRequestExecutor< - TCallArgs, - TResponseDTO, - TResponseModel, - TRequestDTO, - TRequestModel - >["useOnMocking"]; - clearMiddlewares: AcquireRequestExecutor< - TCallArgs, - TResponseDTO, - TResponseModel, - TRequestDTO, - TRequestModel - >["clearMiddlewares"]; -} - -export interface Acquire { - < - TCallArgs extends AcquireCallArgs = object, - TResponseDTO extends ClassOrClassArray = any, - TResponseModel extends ClassOrClassArray = any, - TRequestDTO extends ClassOrClassArray = any, - TRequestModel extends ClassOrClassArray = any - >( - acquireArgs: AcquireArgs< - TCallArgs, - TResponseDTO, - TResponseModel, - TRequestDTO, - TRequestModel, - false - > - ): BoundAcquireRequestExecutor< - TCallArgs, - TResponseDTO, - TResponseModel, - TRequestDTO, - TRequestModel - >; -} - -export class Acquire extends CallableInstance { - private __isAcquireInstance = true; - private executionMiddlewares: AcquireMiddlewareWithOrder[] = []; - private mockingMiddlewares: AcquireMiddlewareWithOrder[] = []; +import AcquireRequestFactory from "./AcquireRequestHandlerFactory.class"; +export class Acquire extends AcquireBase { constructor( private axiosInstance: AxiosInstance = axios, - private mockCache?: AcquireMockCache, + private mockCache: AcquireMockCache | undefined = undefined, private isMockingEnabled = false ) { - super((acquireArgs: AcquireArgs) => this.acquire(acquireArgs)); - } - - static isAcquireInstance(instance: unknown): instance is Acquire { - return !!(instance as Acquire)?.__isAcquireInstance; - } - - public use(middleware: AcquireMiddleware, order = 0): this { - this.executionMiddlewares.push([middleware, order]); - this.mockingMiddlewares.push([middleware, order]); - return this; - } - - public useOnExecution(middleware: AcquireMiddleware, order = 0): this { - this.executionMiddlewares.push([middleware, order]); - return this; - } - - public useOnMocking(middleware: AcquireMiddleware, order = 0): this { - this.mockingMiddlewares.push([middleware, order]); - return this; - } - - public clearMiddlewares(): this { - this.executionMiddlewares = []; - this.mockingMiddlewares = []; - return this; + super(); } public setAxiosInstance(axiosInstance: AxiosInstance): this { @@ -196,90 +49,13 @@ export class Acquire extends CallableInstance { return this.isMockingEnabled; } - public acquire< - TCallArgs extends AcquireCallArgs = object, - TResponseDTO extends ClassOrClassArray = any, - TResponseModel extends ClassOrClassArray = any, - TRequestDTO extends ClassOrClassArray = any, - TRequestModel extends ClassOrClassArray = any - >( - acquireArgs: AcquireArgs< - TCallArgs, - TResponseDTO, - TResponseModel, - TRequestDTO, - TRequestModel, - false - > - ): BoundAcquireRequestExecutor< - TCallArgs, - TResponseDTO, - TResponseModel, - TRequestDTO, - TRequestModel - > { - const { request } = acquireArgs; - - if (!request.path && !request.url && !request.baseURL) { - console.warn("'baseURL', 'url' or 'path' is missing."); - } - - const getConfig: AcquireRequestExecutorGetConfig = () => ({ - axiosInstance: this.axiosInstance, - executionMiddlewares: this.executionMiddlewares, - mockingMiddlewares: this.mockingMiddlewares, - mockCache: this.mockCache, - isMockingEnabled: this.isMockingEnabled - }); - - const requestExecutor = new AcquireRequestExecutor< - any, - TResponseDTO, - TResponseModel, - TRequestDTO, - TRequestModel - >(getConfig); - - requestExecutor.execute = requestExecutor.execute.bind( - requestExecutor, - acquireArgs - ); - - requestExecutor.mock = requestExecutor.mock.bind( - requestExecutor, - acquireArgs + public createRequestHandler(): AcquireRequestFactory { + return this.cloneBase().into( + new AcquireRequestFactory(() => ({ + axiosInstance: this.axiosInstance, + isMockingEnabled: this.isMockingEnabled, + mockCache: this.mockCache + })) ); - - return requestExecutor as unknown as BoundAcquireRequestExecutor< - TCallArgs, - TResponseDTO, - TResponseModel, - TRequestDTO, - TRequestModel - >; - } - - public withCallArgs(): < - TResponseDTO extends ClassOrClassArray = any, - TResponseModel extends ClassOrClassArray = any, - TRequestDTO extends ClassOrClassArray = any, - TRequestModel extends ClassOrClassArray = any - >( - acquireArgs: AcquireArgs< - TCallArgs, - TResponseDTO, - TResponseModel, - TRequestDTO, - TRequestModel, - false - > - ) => BoundAcquireRequestExecutor< - TCallArgs, - TResponseDTO, - TResponseModel, - TRequestDTO, - TRequestModel - > { - return (acquireArgs) => this.acquire(acquireArgs); } } diff --git a/packages/core/src/classes/AcquireBase.class.ts b/packages/core/src/classes/AcquireBase.class.ts new file mode 100644 index 0000000..b3feccd --- /dev/null +++ b/packages/core/src/classes/AcquireBase.class.ts @@ -0,0 +1,231 @@ +import isAcquireMiddlewareClass from "@/guards/isAcquireMiddleware.guard"; +import { AcquireCallArgs } from "@/interfaces/AcquireCallArgs.interface"; +import { AcquireContext } from "@/interfaces/AcquireContext.interface"; +import { ClassOrClassArray } from "@/interfaces/ClassOrClassArray.interface"; + +export type AcquireMiddlewareFn< + TCallArgs extends AcquireCallArgs = never, + TResponseModel extends ClassOrClassArray | unknown = unknown, + TResponseDTO extends ClassOrClassArray | unknown = unknown, + TRequestModel extends ClassOrClassArray = never, + TRequestDTO extends ClassOrClassArray = never +> = ( + context: AcquireContext< + TCallArgs, + TResponseModel, + TResponseDTO, + TRequestModel, + TRequestDTO + > +) => void | Promise; + +export interface AcquireMiddlewareClass< + TCallArgs extends AcquireCallArgs = never, + TResponseModel extends ClassOrClassArray | unknown = unknown, + TResponseDTO extends ClassOrClassArray | unknown = unknown, + TRequestModel extends ClassOrClassArray = never, + TRequestDTO extends ClassOrClassArray = never +> { + order?: number; + handle: AcquireMiddlewareFn< + TCallArgs, + TResponseModel, + TResponseDTO, + TRequestModel, + TRequestDTO + >; +} + +export type AcquireMiddleware< + TCallArgs extends AcquireCallArgs = never, + TResponseModel extends ClassOrClassArray | unknown = unknown, + TResponseDTO extends ClassOrClassArray | unknown = unknown, + TRequestModel extends ClassOrClassArray = never, + TRequestDTO extends ClassOrClassArray = never +> = + | AcquireMiddlewareFn< + TCallArgs, + TResponseModel, + TResponseDTO, + TRequestModel, + TRequestDTO + > + | AcquireMiddlewareClass< + TCallArgs, + TResponseModel, + TResponseDTO, + TRequestModel, + TRequestDTO + >; + +export type AcquireMiddlewareWithOrder< + TCallArgs extends AcquireCallArgs = never, + TResponseModel extends ClassOrClassArray | unknown = unknown, + TResponseDTO extends ClassOrClassArray | unknown = unknown, + TRequestModel extends ClassOrClassArray = never, + TRequestDTO extends ClassOrClassArray = never +> = [ + middleware: AcquireMiddleware< + TCallArgs, + TResponseModel, + TResponseDTO, + TRequestModel, + TRequestDTO + >, + order: number +]; + +export default abstract class AcquireBase< + TCallArgs extends AcquireCallArgs = never, + TResponseModel extends ClassOrClassArray | unknown = unknown, + TResponseDTO extends ClassOrClassArray | unknown = unknown, + TRequestModel extends ClassOrClassArray = never, + TRequestDTO extends ClassOrClassArray = never +> extends Function { + private executionMiddlewares: AcquireMiddlewareWithOrder< + TCallArgs, + TResponseModel, + TResponseDTO, + TRequestModel, + TRequestDTO + >[] = []; + private mockingMiddlewares: AcquireMiddlewareWithOrder< + TCallArgs, + TResponseModel, + TResponseDTO, + TRequestModel, + TRequestDTO + >[] = []; + + protected cloneBase< + TCallArgsOverload extends AcquireCallArgs = never, + TResponseModelOverload extends ClassOrClassArray | unknown = unknown, + TResponseDTOOverload extends ClassOrClassArray | unknown = unknown, + TRequestModelOverload extends ClassOrClassArray = never, + TRequestDTOOverload extends ClassOrClassArray = never + >(): { + into: < + TTarget extends AcquireBase< + TCallArgsOverload, + TResponseModelOverload, + TResponseDTOOverload, + TRequestModelOverload, + TRequestDTOOverload + > + >( + target: TTarget + ) => TTarget; + } { + return { + into: < + TTarget extends AcquireBase< + TCallArgsOverload, + TResponseModelOverload, + TResponseDTOOverload, + TRequestModelOverload, + TRequestDTOOverload + > + >( + target: TTarget + ): TTarget => { + target.executionMiddlewares = [...this.executionMiddlewares] as any; + target.mockingMiddlewares = [...this.mockingMiddlewares] as any; + return target; + } + }; + } + + protected resolveMiddlewares( + middlewaresWithOrder: AcquireMiddlewareWithOrder[] + ): AcquireMiddlewareFn[] { + const middlewareFnsWithOrder: [ + middlewareFn: AcquireMiddlewareFn, + order: number + ][] = []; + + for (const orderedMiddleware of middlewaresWithOrder) { + const [middleware, order] = orderedMiddleware; + if (isAcquireMiddlewareClass(middleware)) { + middlewareFnsWithOrder.push([ + middleware.handle, + middleware.order ?? order + ]); + } else { + middlewareFnsWithOrder.push([middleware, order]); + } + } + + return middlewareFnsWithOrder + .sort((a, b) => a[1] - b[1]) + .map(([middlewareFn]) => middlewareFn); + } + + protected getMiddlewares< + TCallArgsOverload extends AcquireCallArgs = never, + TResponseModelOverload extends ClassOrClassArray | unknown = unknown, + TResponseDTOOverload extends ClassOrClassArray | unknown = unknown, + TRequestModelOverload extends ClassOrClassArray = never, + TRequestDTOOverload extends ClassOrClassArray = never + >( + mode: AcquireContext["type"] + ): AcquireMiddlewareFn< + TCallArgsOverload, + TResponseModelOverload, + TResponseDTOOverload, + TRequestModelOverload, + TRequestDTOOverload + >[] { + return this.resolveMiddlewares( + mode === "execution" ? this.executionMiddlewares : this.mockingMiddlewares + ); + } + + public use( + middleware: AcquireMiddleware< + TCallArgs, + TResponseModel, + TResponseDTO, + TRequestModel, + TRequestDTO + >, + order = 0 + ): this { + this.executionMiddlewares.push([middleware, order]); + this.mockingMiddlewares.push([middleware, order]); + return this; + } + + public useOnExecution( + middleware: AcquireMiddleware< + TCallArgs, + TResponseModel, + TResponseDTO, + TRequestModel, + TRequestDTO + >, + order = 0 + ): this { + this.executionMiddlewares.push([middleware, order]); + return this; + } + + public useOnMocking( + middleware: AcquireMiddleware< + TCallArgs, + TResponseModel, + TResponseDTO, + TRequestModel, + TRequestDTO + >, + order = 0 + ): this { + this.mockingMiddlewares.push([middleware, order]); + return this; + } + + public clearMiddlewares(): this { + this.executionMiddlewares = []; + this.mockingMiddlewares = []; + return this; + } +} diff --git a/packages/core/src/classes/AcquireRequestExecutor.class.ts b/packages/core/src/classes/AcquireRequestExecutor.class.ts deleted file mode 100644 index d86b97d..0000000 --- a/packages/core/src/classes/AcquireRequestExecutor.class.ts +++ /dev/null @@ -1,430 +0,0 @@ -import { RequestMethodType } from "@/constants/RequestMethod.const"; -import generateMock from "@/functions/generateMock.function"; -import resolveValueOrCallback from "@/functions/resolveValueOrCallback.function"; -import transform from "@/functions/transform.function"; -import unwrapClassOrClassArray from "@/functions/unwrapClassArray.function"; -import isAcquireMiddlewareClass from "@/guards/isAcquireMiddleware.guard"; -import { AcquireArgs } from "@/interfaces/AcquireArgs.interface"; -import { AcquireCallArgs } from "@/interfaces/AcquireCallArgs.interface"; -import { AcquireContext } from "@/interfaces/AcquireContext.interface"; -import { - AcquireMiddleware, - AcquireMiddlewareFn, - AcquireMiddlewareWithOrder -} from "@/interfaces/AcquireMiddleware.interface"; -import { AcquireRequestOptions } from "@/interfaces/AcquireRequestOptions.interface"; -import { ClassOrClassArray } from "@/interfaces/ClassOrClassArray.interface"; -import { InstanceOrInstanceArray } from "@/interfaces/InstanceOrInstanceArray.interface"; -import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios"; -import { instanceToPlain } from "class-transformer"; -import AcquireMockCache from "./AcquireMockCache.class"; -import { CallableInstance } from "./CallableInstance.class"; - -export interface AcquireRequestExecutor< - TCallArgs extends AcquireCallArgs = object, - TResponseDTO extends ClassOrClassArray = any, - TResponseModel extends ClassOrClassArray = any, - TRequestDTO extends ClassOrClassArray = any, - TRequestModel extends ClassOrClassArray = any -> { - ( - acquireArgs: AcquireArgs< - TCallArgs, - TResponseDTO, - TResponseModel, - TRequestDTO, - TRequestModel, - false - >, - callArgs?: TCallArgs & { data?: InstanceOrInstanceArray } - ): Promise>; -} - -export type AcquireRequestExecutorGetConfig = () => { - axiosInstance: AxiosInstance; - executionMiddlewares?: AcquireMiddlewareWithOrder[]; - mockingMiddlewares?: AcquireMiddlewareWithOrder[]; - mockCache?: AcquireMockCache; - isMockingEnabled?: boolean; -}; - -export interface AcquireResult< - TResponseDTO extends ClassOrClassArray | undefined, - TResponseModel extends ClassOrClassArray | undefined -> { - response: AxiosResponse; - dto: InstanceOrInstanceArray; - model: InstanceOrInstanceArray; -} - -export class AcquireRequestExecutor< - TCallArgs extends AcquireCallArgs = object, - TResponseDTO extends ClassOrClassArray = any, - TResponseModel extends ClassOrClassArray = any, - TRequestDTO extends ClassOrClassArray = any, - TRequestModel extends ClassOrClassArray = any -> extends CallableInstance { - private __isAcquireRequestExecutorInstance = true; - private executionMiddlewares: AcquireMiddlewareWithOrder[] = []; - private mockingMiddlewares: AcquireMiddlewareWithOrder[] = []; - - constructor(private getConfig: AcquireRequestExecutorGetConfig) { - super((...args: any[]) => { - if (getConfig().isMockingEnabled) { - return (this.mock as any)(...args); - } - return (this.execute as any)(...args); - }); - this.mock = this.mock.bind(this); - } - - static isAcquireRequestExecutorInstance( - instance: unknown - ): instance is AcquireRequestExecutor { - return !!(instance as AcquireRequestExecutor) - ?.__isAcquireRequestExecutorInstance; - } - - public use(middleware: AcquireMiddleware, order = 0): this { - this.executionMiddlewares.push([middleware, order]); - this.mockingMiddlewares.push([middleware, order]); - return this; - } - - public useOnExecution(middleware: AcquireMiddleware, order = 0): this { - this.executionMiddlewares.push([middleware, order]); - return this; - } - - public useOnMocking(middleware: AcquireMiddleware, order = 0): this { - this.mockingMiddlewares.push([middleware, order]); - return this; - } - - public clearMiddlewares(): this { - this.executionMiddlewares = []; - this.mockingMiddlewares = []; - return this; - } - - public async execute( - acquireArgs: AcquireArgs< - TCallArgs, - TResponseDTO, - TResponseModel, - TRequestDTO, - TRequestModel, - false - >, - callArgs?: TCallArgs & { data?: InstanceOrInstanceArray } - ): Promise> { - const { - request, - responseMapping = {}, - requestMapping = {}, - axiosConfig = {} - } = acquireArgs; - const { axiosInstance, mockCache, isMockingEnabled } = this.getConfig(); - - if (isMockingEnabled) { - return this.mock(acquireArgs, callArgs); - } - - let requestData = callArgs?.data; - if (requestMapping.DTO && callArgs?.data) { - const plainData = instanceToPlain(callArgs.data); - - requestData = transform(plainData, requestMapping.DTO, { - excludeExtraneousValues: true - }); - } - - const { request: _request, ...rest } = acquireArgs ?? {}; - const requestConfig = ( - request ? this.resolveRequestConfig(request, callArgs, axiosConfig) : {} - ) as AxiosRequestConfig & { method: RequestMethodType }; - - let response = null; - - function preventMockDataGeneration(): void {} - function mockDataGenerationPrevented(): boolean { - return false; - } - - const context: AcquireContext< - TCallArgs, - TResponseDTO, - TResponseModel, - TRequestDTO, - TRequestModel - > = { - acquireArgs: { - ...rest, - request: requestConfig - }, - type: "execution", - method: requestConfig.method, - callArgs, - mockCache, - preventMockDataGeneration, - mockDataGenerationPrevented, - response: {} as AxiosResponse // The response context is handled below - }; - - try { - response = await axiosInstance.request({ - ...requestConfig, - data: requestData - }); - - context.response = response; - } catch (error) { - if (axios.isAxiosError(error)) { - context.error = error; - if (error.response) { - context.response = error.response; - } - } - throw error; - } finally { - for (const middleware of this.getMiddlewares("execution")) { - middleware(context); - } - } - - const dto = transform(response.data, responseMapping.DTO); - const model = transform(response.data, responseMapping.Model, { - excludeExtraneousValues: true - }); - - return { - response, - dto, - model - }; - } - - public async mock( - acquireArgs: AcquireArgs< - TCallArgs, - TResponseDTO, - TResponseModel, - TRequestDTO, - TRequestModel, - false - >, - callArgs?: TCallArgs & { - data?: InstanceOrInstanceArray; - $count?: number; - } - ): Promise> { - const { - request, - responseMapping = {}, - requestMapping = {}, - axiosConfig = {} - } = acquireArgs; - const { axiosInstance, mockCache } = this.getConfig(); - - const requestConfig = ( - request ? this.resolveRequestConfig(request, callArgs, axiosConfig) : {} - ) as AxiosRequestConfig & { method: RequestMethodType }; - - let requestData = callArgs?.data; - if (requestMapping.DTO && callArgs?.data) { - const plainData = instanceToPlain(callArgs.data); - - requestData = transform(plainData, requestMapping.DTO, { - excludeExtraneousValues: true - }); - } - - function mergeConfigObjects( - ...args: object[] - ): Partial { - const config: Partial = {}; - - args.forEach((arg) => { - for (const _key in arg) { - const key = _key as keyof typeof arg; - if (arg[key] != null) { - config[key] = arg[key]; - } - } - }); - - return config; - } - - const mockResponse = { - config: mergeConfigObjects(axiosInstance.defaults, requestConfig, { - data: requestData - }), - data: undefined, - status: 200, - statusText: "OK" - } as Partial; - - const { request: _request, ...rest } = acquireArgs ?? {}; - - let isMockDataGenerationPrevented = false; - - function preventMockDataGeneration(): void { - isMockDataGenerationPrevented = true; - } - function mockDataGenerationPrevented(): boolean { - return isMockDataGenerationPrevented; - } - - const context: AcquireContext< - TCallArgs, - TResponseDTO, - TResponseModel, - TRequestDTO, - TRequestModel - > = { - acquireArgs: { - ...rest, - request: requestConfig - }, - type: "mocking", - method: requestConfig.method, - callArgs, - mockCache, - mockDataGenerationPrevented, - preventMockDataGeneration, - response: mockResponse as AxiosResponse - }; - - for (const middleware of this.getMiddlewares("mocking")) { - try { - middleware(context); - } catch (error) { - if (axios.isAxiosError(error)) { - context.error = error; - if (error.response) { - context.response = error.response; - } - } - } - } - - if (context.error) { - throw context.error; - } - - if ( - !isMockDataGenerationPrevented && - !context.response.data && - responseMapping.DTO - ) { - const { ClassUnwrapped: DTOUnwrapped, isClassArray: isDTOArray } = - unwrapClassOrClassArray(responseMapping.DTO); - - const mockResult = await (isDTOArray - ? generateMock(DTOUnwrapped, callArgs?.$count ?? 10, mockCache, context) - : generateMock(DTOUnwrapped, undefined, mockCache, context)); - - mockResponse.data = instanceToPlain(mockResult); - } - - const mockDto = transform(mockResponse?.data, responseMapping.DTO); - const mockModel = transform(mockResponse?.data, responseMapping.Model, { - excludeExtraneousValues: true - }); - - return { - response: mockResponse as AxiosResponse, - dto: mockDto, - model: mockModel - }; - } - - private resolveRequestConfig( - request: AcquireRequestOptions, - callArgs: any, - axiosConfig?: AxiosRequestConfig - ): AxiosRequestConfig { - const { - method = "GET", - url, - path, - baseURL, - headers, - params, - responseEncoding, - responseType, - timeout, - timeoutErrorMessage - } = request; - - const methodValue = resolveValueOrCallback(method, callArgs); - const baseURLValue = resolveValueOrCallback(baseURL, callArgs); - const headersValue = resolveValueOrCallback(headers, callArgs); - const paramsValue = resolveValueOrCallback(params, callArgs); - const responseEncodingValue = resolveValueOrCallback( - responseEncoding, - callArgs - ); - const responseTypeValue = resolveValueOrCallback(responseType, callArgs); - const timeoutValue = resolveValueOrCallback(timeout, callArgs); - const timeoutErrorMessageValue = resolveValueOrCallback( - timeoutErrorMessage, - callArgs - ); - const urlValue = resolveValueOrCallback(url, callArgs); - const pathValue = resolveValueOrCallback(path, callArgs); - - return { - method: methodValue, - baseURL: baseURLValue, - headers: headersValue, - params: paramsValue, - responseEncoding: responseEncodingValue, - responseType: responseTypeValue, - timeout: timeoutValue, - timeoutErrorMessage: timeoutErrorMessageValue, - url: urlValue ?? pathValue, - ...axiosConfig - }; - } - - private resolveMiddlewares( - orderedMiddlewares: AcquireMiddlewareWithOrder[] - ): AcquireMiddlewareFn[] { - const orderedMiddlewareFns: [ - middlewareFn: AcquireMiddlewareFn, - order: number - ][] = []; - - for (const orderedMiddleware of orderedMiddlewares) { - const [middleware, order] = orderedMiddleware; - if (isAcquireMiddlewareClass(middleware)) { - orderedMiddlewareFns.push([ - middleware.handle, - middleware.order ?? order - ]); - } else { - orderedMiddlewareFns.push([middleware, order]); - } - } - - return orderedMiddlewareFns - .sort((a, b) => a[1] - b[1]) - .map(([middlewareFn]) => middlewareFn); - } - - private getMiddlewares(mode: AcquireContext["type"]): AcquireMiddlewareFn[] { - if (mode === "execution") { - return this.resolveMiddlewares([ - ...(this.getConfig().executionMiddlewares ?? []), - ...this.executionMiddlewares - ]); - } - - return this.resolveMiddlewares([ - ...(this.getConfig().mockingMiddlewares ?? []), - ...this.mockingMiddlewares - ]); - } -} diff --git a/packages/core/src/classes/AcquireRequestHandler.class.ts b/packages/core/src/classes/AcquireRequestHandler.class.ts new file mode 100644 index 0000000..8bc4005 --- /dev/null +++ b/packages/core/src/classes/AcquireRequestHandler.class.ts @@ -0,0 +1,353 @@ +import { RequestMethodType } from "@/constants/RequestMethod.const"; +import generateMock from "@/functions/generateMock.function"; +import resolveValueOrCallback from "@/functions/resolveValueOrCallback.function"; +import transform from "@/functions/transform.function"; +import unwrapClassOrClassArray from "@/functions/unwrapClassArray.function"; +import isClassOrClassArray from "@/guards/isClassOrClassArray.guard"; +import { AcquireCallArgs } from "@/interfaces/AcquireCallArgs.interface"; +import { AcquireContext } from "@/interfaces/AcquireContext.interface"; +import { AcquireRequestConfig } from "@/interfaces/AcquireRequestConfig.interface"; +import { ClassOrClassArray } from "@/interfaces/ClassOrClassArray.interface"; +import { InstanceOrInstanceArray } from "@/interfaces/InstanceOrInstanceArray.interface"; +import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios"; +import { instanceToPlain } from "class-transformer"; +import AcquireBase from "./AcquireBase.class"; +import AcquireMockCache from "./AcquireMockCache.class"; + +export interface AcquireRequestHandlerConfig { + axiosInstance: AxiosInstance; + mockCache?: AcquireMockCache; + isMockingEnabled?: boolean; +} +export interface AcquireResult< + TResponseModel extends ClassOrClassArray | unknown, + TResponseDTO extends ClassOrClassArray | unknown +> { + response: AxiosResponse; + dto: InstanceOrInstanceArray; + model: InstanceOrInstanceArray; +} + +type RequestArgs = [TRequestModel] extends [never] + ? [TCallArgs] extends [never] + ? [] + : [TCallArgs] + : [TCallArgs] extends [never] + ? [{ data: InstanceOrInstanceArray }] + : [{ data: InstanceOrInstanceArray } & TCallArgs]; + +export interface AcquireRequestHandler< + TCallArgs extends AcquireCallArgs = never, + TResponseModel extends ClassOrClassArray | unknown = unknown, + TResponseDTO extends ClassOrClassArray | unknown = unknown, + TRequestModel extends ClassOrClassArray = never, + TRequestDTO extends ClassOrClassArray = never +> { + (...args: RequestArgs): Promise< + AcquireResult + >; +} + +export class AcquireRequestHandler< + TCallArgs extends AcquireCallArgs = never, + TResponseModel extends ClassOrClassArray | unknown = unknown, + TResponseDTO extends ClassOrClassArray | unknown = unknown, + TRequestModel extends ClassOrClassArray = never, + TRequestDTO extends ClassOrClassArray = never +> extends AcquireBase< + TCallArgs, + TResponseModel, + TResponseDTO, + TRequestModel, + TRequestDTO +> { + constructor( + private requestConfig: AcquireRequestConfig, + private getConfig: () => AcquireRequestHandlerConfig, + private responseModel?: TResponseModel, + private responseDTO?: TResponseDTO, + private requestModel?: TRequestModel, + private requestDTO?: TRequestDTO + ) { + super(); + + const proxy = new Proxy(this, { + apply: function ( + target, + _thisArg, + argumentsList: any + ): ReturnType { + if (target.getConfig().isMockingEnabled) { + return target.mock(...argumentsList); + } else { + return target.execute(...argumentsList); + } + } + }); + + return proxy; + } + + private resolveRequestConfig( + request: AcquireRequestConfig, + callArgs: any + ): AxiosRequestConfig & { method: RequestMethodType } { + const { + method = "GET", + url, + path, + baseURL, + headers, + params, + responseEncoding, + responseType, + timeout, + timeoutErrorMessage, + axiosConfig + } = request; + + const { method: configMethod, ...config } = axiosConfig ?? {}; + + const methodValue = resolveValueOrCallback(method ?? method, callArgs); + const baseURLValue = resolveValueOrCallback(baseURL, callArgs); + const headersValue = resolveValueOrCallback(headers, callArgs); + const paramsValue = resolveValueOrCallback(params, callArgs); + const responseEncodingValue = resolveValueOrCallback( + responseEncoding, + callArgs + ); + const responseTypeValue = resolveValueOrCallback(responseType, callArgs); + const timeoutValue = resolveValueOrCallback(timeout, callArgs); + const timeoutErrorMessageValue = resolveValueOrCallback( + timeoutErrorMessage, + callArgs + ); + const urlValue = resolveValueOrCallback(url, callArgs); + const pathValue = resolveValueOrCallback(path, callArgs); + + return { + method: methodValue, + baseURL: baseURLValue, + headers: headersValue, + params: paramsValue, + responseEncoding: responseEncodingValue, + responseType: responseTypeValue, + timeout: timeoutValue, + timeoutErrorMessage: timeoutErrorMessageValue, + url: urlValue ?? pathValue, + ...config + }; + } + + public async execute( + ...args: RequestArgs + ): Promise> { + const { axiosInstance, mockCache, isMockingEnabled } = this.getConfig(); + const callArgs: AcquireCallArgs | undefined = args[0]; + + if (isMockingEnabled) { + return this.mock(...args); + } + + let requestData = callArgs?.data; + if (this.requestDTO && callArgs?.data) { + const plainData = instanceToPlain(callArgs.data); + + requestData = transform(plainData, this.requestDTO, { + excludeExtraneousValues: true + }); + } + + const resolvedRequestConfig = this.resolveRequestConfig( + this.requestConfig, + callArgs + ); + let response = null; + + const context: AcquireContext< + TCallArgs, + TResponseModel, + TResponseDTO, + TRequestModel, + TRequestDTO + > = { + requestConfig: resolvedRequestConfig, + responseModel: this.responseModel, + responseDTO: this.responseDTO, + requestDTO: this.requestDTO, + requestModel: this.requestModel, + type: "execution", + method: resolvedRequestConfig.method, + callArgs: callArgs as TCallArgs, + mockCache, + response: {} as AxiosResponse, // The response context is handled below + error: undefined + }; + + try { + response = await axiosInstance.request({ + ...resolvedRequestConfig, + data: requestData + }); + + context.response = response; + } catch (error) { + if (axios.isAxiosError(error)) { + context.error = error; + if (error.response) { + context.response = error.response; + } + } + throw error; + } finally { + for await (const middleware of this.getMiddlewares< + TCallArgs, + TResponseModel, + TResponseDTO, + TRequestModel, + TRequestDTO + >("execution")) { + await middleware(context); + } + } + + const dto = transform(response.data, this.responseDTO); + const model = transform(response.data, this.responseModel, { + excludeExtraneousValues: true + }); + + return { + response, + dto, + model + }; + } + + public async mock( + ...args: [ + ...callArgs: RequestArgs, + count?: number + ] + ): Promise> { + const { axiosInstance, mockCache } = this.getConfig(); + + let count = 10; + + if (typeof args[args.length - 1] === "number") { + count = args.pop() as number; + } + const callArgs = args[0] as RequestArgs[0]; + + const resolvedRequestConfig = this.resolveRequestConfig( + this.requestConfig, + callArgs + ); + + let requestData = callArgs?.data; + if (this.requestDTO && callArgs?.data) { + const plainData = instanceToPlain(callArgs.data); + + requestData = transform(plainData, this.requestDTO, { + excludeExtraneousValues: true + }); + } + + function mergeConfigObjects( + ...args: object[] + ): Partial { + const config: Partial = {}; + + args.forEach((arg) => { + for (const _key in arg) { + const key = _key as keyof typeof arg; + if (arg[key] != null) { + config[key] = arg[key]; + } + } + }); + + return config; + } + + let mockResponse = { + config: mergeConfigObjects( + axiosInstance.defaults, + resolvedRequestConfig, + { + data: requestData + } + ), + data: undefined, + status: 200, + statusText: "OK" + } as Partial; + + const context: AcquireContext< + TCallArgs, + TResponseModel, + TResponseDTO, + TRequestModel, + TRequestDTO + > = { + requestConfig: resolvedRequestConfig, + responseModel: this.responseModel, + responseDTO: this.responseDTO, + requestDTO: this.requestDTO, + requestModel: this.requestModel, + type: "mocking", + method: resolvedRequestConfig.method, + callArgs: callArgs as TCallArgs, + mockCache, + response: mockResponse as AxiosResponse, + error: undefined + }; + + for await (const middleware of this.getMiddlewares< + TCallArgs, + TResponseModel, + TResponseDTO, + TRequestModel, + TRequestDTO + >("mocking")) { + try { + await middleware(context); + } catch (error) { + if (axios.isAxiosError(error)) { + context.error = error; + if (error.response) { + context.response = error.response; + } + } + } + } + + if (context.error) { + throw context.error; + } + + mockResponse = { ...mockResponse, ...context.response }; + + if (mockResponse.data == null && isClassOrClassArray(this.responseDTO)) { + const { ClassUnwrapped: DTOUnwrapped, isClassArray: isDTOArray } = + unwrapClassOrClassArray(this.responseDTO); + const genericContext = context as AcquireContext; + + const mockResult = await (isDTOArray + ? generateMock(DTOUnwrapped, count, mockCache, genericContext) + : generateMock(DTOUnwrapped, undefined, mockCache, genericContext)); + + mockResponse.data = instanceToPlain(mockResult); + } + + const mockDto = transform(mockResponse?.data, this.responseDTO); + const mockModel = transform(mockResponse?.data, this.responseModel, { + excludeExtraneousValues: true + }); + + return { + response: mockResponse as AxiosResponse, + dto: mockDto, + model: mockModel + }; + } +} diff --git a/packages/core/src/classes/AcquireRequestHandlerFactory.class.ts b/packages/core/src/classes/AcquireRequestHandlerFactory.class.ts new file mode 100644 index 0000000..a2e35de --- /dev/null +++ b/packages/core/src/classes/AcquireRequestHandlerFactory.class.ts @@ -0,0 +1,242 @@ +import { AcquireCallArgs } from "@/interfaces/AcquireCallArgs.interface"; +import { AcquireRequestConfig } from "@/interfaces/AcquireRequestConfig.interface"; +import { ClassOrClassArray } from "@/interfaces/ClassOrClassArray.interface"; +import AcquireBase from "./AcquireBase.class"; +import { + AcquireRequestHandler, + AcquireRequestHandlerConfig +} from "./AcquireRequestHandler.class"; + +export type AcquireRequestMethodConfig = + Omit, "method">; + +export default class AcquireRequestHandlerFactory< + TResponseModel extends ClassOrClassArray | unknown = unknown, + TResponseDTO extends ClassOrClassArray | unknown = unknown, + TRequestModel extends ClassOrClassArray = never, + TRequestDTO extends ClassOrClassArray = never +> extends AcquireBase< + never, + TResponseModel, + TResponseDTO, + TRequestModel, + TRequestDTO +> { + private responseModel: TResponseModel | undefined; + private responseDTO: TResponseDTO | undefined; + private requestModel: TRequestModel | undefined; + private requestDTO: TRequestDTO | undefined; + + constructor(private getConfig: () => AcquireRequestHandlerConfig) { + super(); + } + + withResponseMapping< + TResponseModelOverload extends ClassOrClassArray, + TResponseDTOOverload extends ClassOrClassArray + >( + model?: TResponseModelOverload, + dto?: TResponseDTOOverload + ): AcquireRequestHandlerFactory< + TResponseModelOverload, + TResponseDTOOverload, + TRequestModel, + TRequestDTO + > { + const factory = this.cloneBase< + never, + TResponseModelOverload, + TResponseDTOOverload, + TRequestModel, + TRequestDTO + >().into( + new AcquireRequestHandlerFactory< + TResponseModelOverload, + TResponseDTOOverload, + TRequestModel, + TRequestDTO + >(this.getConfig) + ); + factory.responseModel = model; + factory.responseDTO = dto; + return factory; + } + + withRequestMapping< + TRequestModelOverload extends ClassOrClassArray, + TRequestDTOOverload extends ClassOrClassArray + >( + model?: TRequestModelOverload, + dto?: TRequestDTOOverload + ): AcquireRequestHandlerFactory< + TResponseModel, + TResponseDTO, + TRequestModelOverload, + TRequestDTOOverload + > { + const factory = this.cloneBase< + never, + TResponseModel, + TResponseDTO, + TRequestModelOverload, + TRequestDTOOverload + >().into( + new AcquireRequestHandlerFactory< + TResponseModel, + TResponseDTO, + TRequestModelOverload, + TRequestDTOOverload + >(this.getConfig) + ); + factory.requestModel = model; + factory.requestDTO = dto; + return factory; + } + + request( + requestConfig: AcquireRequestConfig + ): AcquireRequestHandler< + TCallArgs, + TResponseModel, + TResponseDTO, + TRequestModel, + TRequestDTO + > { + const executor = new AcquireRequestHandler( + requestConfig, + this.getConfig, + this.responseModel, + this.responseDTO, + this.requestModel, + this.requestDTO + ); + + return this.cloneBase< + TCallArgs, + TResponseModel, + TResponseDTO, + TRequestModel, + TRequestDTO + >().into(executor); + } + + get( + requestConfig: AcquireRequestMethodConfig + ): AcquireRequestHandler< + TCallArgs, + TResponseModel, + TResponseDTO, + TRequestModel, + TRequestDTO + > { + return this.request({ ...requestConfig, method: "GET" }); + } + + post( + requestConfig: AcquireRequestMethodConfig + ): AcquireRequestHandler< + TCallArgs, + TResponseModel, + TResponseDTO, + TRequestModel, + TRequestDTO + > { + return this.request({ ...requestConfig, method: "POST" }); + } + + put( + requestConfig: AcquireRequestMethodConfig + ): AcquireRequestHandler< + TCallArgs, + TResponseModel, + TResponseDTO, + TRequestModel, + TRequestDTO + > { + return this.request({ ...requestConfig, method: "PUT" }); + } + + patch( + requestConfig: AcquireRequestMethodConfig + ): AcquireRequestHandler< + TCallArgs, + TResponseModel, + TResponseDTO, + TRequestModel, + TRequestDTO + > { + return this.request({ ...requestConfig, method: "PATCH" }); + } + + delete( + requestConfig: AcquireRequestMethodConfig + ): AcquireRequestHandler< + TCallArgs, + TResponseModel, + TResponseDTO, + TRequestModel, + TRequestDTO + > { + return this.request({ ...requestConfig, method: "DELETE" }); + } + + head( + requestConfig: AcquireRequestMethodConfig + ): AcquireRequestHandler< + TCallArgs, + TResponseModel, + TResponseDTO, + TRequestModel, + TRequestDTO + > { + return this.request({ ...requestConfig, method: "HEAD" }); + } + + options( + requestConfig: AcquireRequestMethodConfig + ): AcquireRequestHandler< + TCallArgs, + TResponseModel, + TResponseDTO, + TRequestModel, + TRequestDTO + > { + return this.request({ ...requestConfig, method: "OPTIONS" }); + } + + purge( + requestConfig: AcquireRequestMethodConfig + ): AcquireRequestHandler< + TCallArgs, + TResponseModel, + TResponseDTO, + TRequestModel, + TRequestDTO + > { + return this.request({ ...requestConfig, method: "PURGE" }); + } + + link( + requestConfig: AcquireRequestMethodConfig + ): AcquireRequestHandler< + TCallArgs, + TResponseModel, + TResponseDTO, + TRequestModel, + TRequestDTO + > { + return this.request({ ...requestConfig, method: "LINK" }); + } + + unlink( + requestConfig: AcquireRequestMethodConfig + ): AcquireRequestHandler< + TCallArgs, + TResponseModel, + TResponseDTO, + TRequestModel, + TRequestDTO + > { + return this.request({ ...requestConfig, method: "UNLINK" }); + } +} diff --git a/packages/core/src/classes/CallableInstance.class.ts b/packages/core/src/classes/CallableInstance.class.ts deleted file mode 100644 index d3a189a..0000000 --- a/packages/core/src/classes/CallableInstance.class.ts +++ /dev/null @@ -1,14 +0,0 @@ -export abstract class CallableInstance { - constructor(callback: (...args: any) => any) { - return this.createCallableInstance(callback); - } - - private createCallableInstance(callback: (...args: any) => any): this { - const callableInstance = ((...args: any[]) => - callback(...args)) as unknown as this; - Object.setPrototypeOf(callableInstance, Object.getPrototypeOf(this)); - Object.assign(callableInstance, this); - - return callableInstance; - } -} diff --git a/packages/core/src/functions/transform.function.ts b/packages/core/src/functions/transform.function.ts index 254dd93..72946c2 100644 --- a/packages/core/src/functions/transform.function.ts +++ b/packages/core/src/functions/transform.function.ts @@ -8,7 +8,7 @@ export default function transform( Model?: TModel, options?: ClassTransformOptions ): InstanceOrInstanceArray { - if (!Model) { + if (Model == null) { return data as InstanceOrInstanceArray; } diff --git a/packages/core/src/guards/isAcquireMiddleware.guard.ts b/packages/core/src/guards/isAcquireMiddleware.guard.ts index e94c33e..fd7087b 100644 --- a/packages/core/src/guards/isAcquireMiddleware.guard.ts +++ b/packages/core/src/guards/isAcquireMiddleware.guard.ts @@ -1,4 +1,4 @@ -import { AcquireMiddlewareClass } from "@/interfaces/AcquireMiddleware.interface"; +import { AcquireMiddlewareClass } from "@/classes/AcquireBase.class"; export default function isAcquireMiddlewareClass( obj: any diff --git a/packages/core/src/guards/isClassOrClassArray.guard.ts b/packages/core/src/guards/isClassOrClassArray.guard.ts new file mode 100644 index 0000000..fd0f3a8 --- /dev/null +++ b/packages/core/src/guards/isClassOrClassArray.guard.ts @@ -0,0 +1,10 @@ +import { ClassOrClassArray } from "@/interfaces/ClassOrClassArray.interface"; + +export default function isClassOrClassArray( + input: unknown +): input is ClassOrClassArray { + return ( + typeof input === "function" || + (Array.isArray(input) && typeof input[0] === "function") + ); +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 134fcb6..a7c9653 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -8,9 +8,22 @@ export * from "class-transformer"; /*/ /* --------------------------------- Classes -------------------------------- */ export { Acquire } from "./classes/Acquire.class"; -export { default as AcquireLogger } from "./classes/AcquireLogger.class"; +export { + type AcquireMiddleware, + type AcquireMiddlewareClass, + type AcquireMiddlewareFn +} from "./classes/AcquireBase.class"; +export { + default as AcquireLogger, + type AcquireLogColor, + type AcquireLoggerOptions +} from "./classes/AcquireLogger.class"; export { default as AcquireMockCache } from "./classes/AcquireMockCache.class"; export { default as acquireMockDataStorage } from "./classes/AcquireMockDataStorage.class"; +export { + type AcquireRequestHandlerConfig, + type AcquireResult +} from "./classes/AcquireRequestHandler.class"; /* -------------------------------------------------------------------------- */ /*/ /* -------------------------------- Constants ------------------------------- */ @@ -74,31 +87,35 @@ export { default as transform } from "./functions/transform.function"; /* -------------------------------------------------------------------------- */ /*/ /* ------------------------------- Interfaces ------------------------------- */ -export type { AcquireArgs } from "./interfaces/AcquireArgs.interface"; -export type { AcquireCallArgs } from "./interfaces/AcquireCallArgs.interface"; -export type { AcquireContext } from "./interfaces/AcquireContext.interface"; -export type { - AcquireMiddleware, - AcquireMiddlewareClass, - AcquireMiddlewareFn, - AcquireMiddlewareWithOrder -} from "./interfaces/AcquireMiddleware.interface"; -export type { AcquireMockGenerator } from "./interfaces/AcquireMockGenerator.interface"; -export type { AcquireRequestOptions } from "./interfaces/AcquireRequestOptions.interface"; -export type { AcquireTransformerOptions } from "./interfaces/AcquireTransformerOptions.interface"; -export type { ClassConstructor } from "./interfaces/ClassConstructor.interface"; -export type { ClassOrClassArray } from "./interfaces/ClassOrClassArray.interface"; -export type { InstanceOrInstanceArray } from "./interfaces/InstanceOrInstanceArray.interface"; -export type { - JSONArray, - JSONObject, - JSONValue +export { type AcquireCallArgs } from "./interfaces/AcquireCallArgs.interface"; +export { type AcquireContext } from "./interfaces/AcquireContext.interface"; +export { type AcquireMockGenerator } from "./interfaces/AcquireMockGenerator.interface"; +export { type AcquireRequestConfig } from "./interfaces/AcquireRequestConfig.interface"; +export { type AcquireTransformerOptions } from "./interfaces/AcquireTransformerOptions.interface"; +export { type ClassConstructor } from "./interfaces/ClassConstructor.interface"; +export { type ClassOrClassArray } from "./interfaces/ClassOrClassArray.interface"; +export { type InstanceOrInstanceArray } from "./interfaces/InstanceOrInstanceArray.interface"; +export { + type JSONArray, + type JSONObject, + type JSONValue } from "./interfaces/JSON.interface"; -export type { LogLevel, Logger, LoggerFn } from "./interfaces/Logger.interface"; -export type { OmitFirstArg } from "./interfaces/OmitFirstArg.interface"; -export type { ValueOrCallback } from "./interfaces/ValueOrCallback.interface"; +export { + type LogLevel, + type Logger, + type LoggerFn +} from "./interfaces/Logger.interface"; +export { type ValueOrCallback } from "./interfaces/ValueOrCallback.interface"; /* -------------------------------------------------------------------------- */ /*/ /* ------------------------------- Middleware ------------------------------- */ -export { default as AcquireRequestLogger } from "./middleware/AcquireRequestLogger"; +export { + default as DelaySimulator, + type DelaySimulatorLimit, + type DelaySimulatorOptions +} from "./middleware/DelaySimulator.middleware"; +export { + default as RequestLogger, + type RequestLoggerOptions +} from "./middleware/RequestLogger.middleware"; /* -------------------------------------------------------------------------- */ diff --git a/packages/core/src/interfaces/AcquireArgs.interface.ts b/packages/core/src/interfaces/AcquireArgs.interface.ts deleted file mode 100644 index d265976..0000000 --- a/packages/core/src/interfaces/AcquireArgs.interface.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { AxiosRequestConfig } from "axios"; -import { AcquireCallArgs } from "./AcquireCallArgs.interface"; -import { AcquireRequestOptions } from "./AcquireRequestOptions.interface"; -import { ClassOrClassArray } from "./ClassOrClassArray.interface"; - -export interface AcquireArgs< - TCallArgs extends AcquireCallArgs = any, - TResponseDTO extends ClassOrClassArray = never, - TResponseModel extends ClassOrClassArray = never, - TRequestDTO extends ClassOrClassArray = never, - TRequestModel extends ClassOrClassArray = never, - TRequestOptionsValueOnly extends boolean = false -> { - request: AcquireRequestOptions; - callArgs?: TCallArgs; - requestMapping?: { - DTO?: TRequestDTO; - Model?: TRequestModel; - }; - responseMapping?: { - DTO?: TResponseDTO; - Model?: TResponseModel; - }; - axiosConfig?: AxiosRequestConfig; -} diff --git a/packages/core/src/interfaces/AcquireContext.interface.ts b/packages/core/src/interfaces/AcquireContext.interface.ts index 6c7a060..39b4709 100644 --- a/packages/core/src/interfaces/AcquireContext.interface.ts +++ b/packages/core/src/interfaces/AcquireContext.interface.ts @@ -2,34 +2,24 @@ import AcquireMockCache from "@/classes/AcquireMockCache.class"; import { RequestMethodType } from "@/constants/RequestMethod.const"; import AcquireError from "@/errors/AcquireError.error"; import { AxiosResponse } from "axios"; -import { AcquireArgs } from "./AcquireArgs.interface"; -import { AcquireCallArgs } from "./AcquireCallArgs.interface"; -import { ClassOrClassArray } from "./ClassOrClassArray.interface"; -import { InstanceOrInstanceArray } from "./InstanceOrInstanceArray.interface"; +import { AcquireRequestConfig } from "./AcquireRequestConfig.interface"; export interface AcquireContext< - TCallArgs extends AcquireCallArgs = any, - TResponseDTO extends ClassOrClassArray = any, - TResponseModel extends ClassOrClassArray = any, - TRequestDTO extends ClassOrClassArray = any, - TRequestModel extends ClassOrClassArray = any + TCallArgs = never, + TResponseModel = unknown, + TResponseDTO = unknown, + TRequestModel = unknown, + TRequestDTO = unknown > { - readonly acquireArgs: AcquireArgs< - TCallArgs, - TResponseDTO, - TResponseModel, - TRequestDTO, - TRequestModel, - true - >; + readonly requestConfig: AcquireRequestConfig; readonly type: "mocking" | "execution"; readonly method: RequestMethodType; - readonly callArgs?: TCallArgs & { - data?: InstanceOrInstanceArray; - }; + readonly responseModel?: TResponseModel; + readonly responseDTO?: TResponseDTO; + readonly requestModel?: TRequestModel; + readonly requestDTO?: TRequestDTO; + readonly callArgs?: TCallArgs; readonly mockCache?: AcquireMockCache; - readonly mockDataGenerationPrevented: () => boolean; - readonly preventMockDataGeneration: () => void; response: AxiosResponse; error?: AcquireError; diff --git a/packages/core/src/interfaces/AcquireMiddleware.interface.ts b/packages/core/src/interfaces/AcquireMiddleware.interface.ts deleted file mode 100644 index b53e926..0000000 --- a/packages/core/src/interfaces/AcquireMiddleware.interface.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { AcquireContext } from "./AcquireContext.interface"; - -export type AcquireMiddlewareFn = (context: AcquireContext) => void; - -export interface AcquireMiddlewareClass { - order?: number; - handle: AcquireMiddlewareFn; -} - -export type AcquireMiddleware = AcquireMiddlewareFn | AcquireMiddlewareClass; - -export type AcquireMiddlewareWithOrder = [ - middleware: AcquireMiddleware, - order: number -]; diff --git a/packages/core/src/interfaces/AcquireRequestOptions.interface.ts b/packages/core/src/interfaces/AcquireRequestConfig.interface.ts similarity index 89% rename from packages/core/src/interfaces/AcquireRequestOptions.interface.ts rename to packages/core/src/interfaces/AcquireRequestConfig.interface.ts index f560584..3690a8f 100644 --- a/packages/core/src/interfaces/AcquireRequestOptions.interface.ts +++ b/packages/core/src/interfaces/AcquireRequestConfig.interface.ts @@ -11,8 +11,8 @@ type ForwardedAxiosRequestConfigKeys = | "timeout" | "timeoutErrorMessage"; -export type AcquireRequestOptions< - TCallArgs = any, +export type AcquireRequestConfig< + TCallArgs, TValueOnly extends boolean = false > = { [Key in ForwardedAxiosRequestConfigKeys]?: ValueOrCallback< @@ -24,4 +24,5 @@ export type AcquireRequestOptions< path?: ValueOrCallback; params?: ValueOrCallback, TCallArgs, TValueOnly>; method?: ValueOrCallback; + axiosConfig?: AxiosRequestConfig; }; diff --git a/packages/core/src/interfaces/ClassConstructor.interface.ts b/packages/core/src/interfaces/ClassConstructor.interface.ts index e80f655..b65fa9d 100644 --- a/packages/core/src/interfaces/ClassConstructor.interface.ts +++ b/packages/core/src/interfaces/ClassConstructor.interface.ts @@ -1,3 +1 @@ -export interface ClassConstructor { - new (...args: any): T; -} +export type ClassConstructor = new (...args: any) => T; diff --git a/packages/core/src/interfaces/InstanceOrInstanceArray.interface.ts b/packages/core/src/interfaces/InstanceOrInstanceArray.interface.ts index 290a7fd..948d929 100644 --- a/packages/core/src/interfaces/InstanceOrInstanceArray.interface.ts +++ b/packages/core/src/interfaces/InstanceOrInstanceArray.interface.ts @@ -1,8 +1,7 @@ import type { ClassConstructor } from "./ClassConstructor.interface"; -export type InstanceOrInstanceArray = - TTransformTarget extends ClassConstructor - ? InstanceType - : TTransformTarget extends [classConstructor: ClassConstructor] - ? InstanceType[] - : TTransformTarget; +export type InstanceOrInstanceArray = TTarget extends ClassConstructor + ? InstanceType + : TTarget extends [classConstructor: ClassConstructor] + ? InstanceType[] + : TTarget; diff --git a/packages/core/src/interfaces/OmitFirstArg.interface.ts b/packages/core/src/interfaces/OmitFirstArg.interface.ts deleted file mode 100644 index ad940ad..0000000 --- a/packages/core/src/interfaces/OmitFirstArg.interface.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type OmitFirstArg = TFn extends ( - x: any, - ...args: infer TArgs -) => infer TRet - ? (...args: TArgs) => TRet - : never; diff --git a/packages/core/src/interfaces/ValueOrCallback.interface.ts b/packages/core/src/interfaces/ValueOrCallback.interface.ts index e7cdaff..d46545e 100644 --- a/packages/core/src/interfaces/ValueOrCallback.interface.ts +++ b/packages/core/src/interfaces/ValueOrCallback.interface.ts @@ -2,4 +2,4 @@ export type ValueOrCallback< TValue, TArgs, TValueOnly extends boolean = false -> = TValueOnly extends true ? TValue : TValue | ((args?: TArgs) => TValue); +> = TValueOnly extends true ? TValue : TValue | ((args: TArgs) => TValue); diff --git a/packages/core/src/middleware/DelaySimulator.middleware.ts b/packages/core/src/middleware/DelaySimulator.middleware.ts new file mode 100644 index 0000000..9ac21c9 --- /dev/null +++ b/packages/core/src/middleware/DelaySimulator.middleware.ts @@ -0,0 +1,113 @@ +import { + AcquireMiddlewareClass, + AcquireMiddlewareFn +} from "@/classes/AcquireBase.class"; +import isCallable from "@/guards/isCallable.guard"; +import { AcquireCallArgs } from "@/interfaces/AcquireCallArgs.interface"; +import { AcquireContext } from "@/interfaces/AcquireContext.interface"; +import { ClassOrClassArray } from "@/interfaces/ClassOrClassArray.interface"; + +export type DelaySimulatorLimit< + TCallArgs extends AcquireCallArgs = never, + TResponseModel extends ClassOrClassArray | unknown = unknown, + TResponseDTO extends ClassOrClassArray | unknown = unknown, + TRequestModel extends ClassOrClassArray = never, + TRequestDTO extends ClassOrClassArray = never +> = + | number + | (( + context: AcquireContext< + TCallArgs, + TResponseModel, + TResponseDTO, + TRequestModel, + TRequestDTO + > + ) => number); + +export interface DelaySimulatorOptions< + TCallArgs extends AcquireCallArgs = never, + TResponseModel extends ClassOrClassArray | unknown = unknown, + TResponseDTO extends ClassOrClassArray | unknown = unknown, + TRequestModel extends ClassOrClassArray = never, + TRequestDTO extends ClassOrClassArray = never +> { + order?: number; + min?: DelaySimulatorLimit< + TCallArgs, + TResponseModel, + TResponseDTO, + TRequestModel, + TRequestDTO + >; + max?: DelaySimulatorLimit< + TCallArgs, + TResponseModel, + TResponseDTO, + TRequestModel, + TRequestDTO + >; +} + +export default class DelaySimulator< + TCallArgs extends AcquireCallArgs = never, + TResponseModel extends ClassOrClassArray | unknown = unknown, + TResponseDTO extends ClassOrClassArray | unknown = unknown, + TRequestModel extends ClassOrClassArray = never, + TRequestDTO extends ClassOrClassArray = never +> implements + AcquireMiddlewareClass< + TCallArgs, + TResponseModel, + TResponseDTO, + TRequestModel, + TRequestDTO + > +{ + public order: number; + + private min: DelaySimulatorLimit< + TCallArgs, + TResponseModel, + TResponseDTO, + TRequestModel, + TRequestDTO + >; + private max: DelaySimulatorLimit< + TCallArgs, + TResponseModel, + TResponseDTO, + TRequestModel, + TRequestDTO + >; + + constructor( + options?: DelaySimulatorOptions< + TCallArgs, + TResponseModel, + TResponseDTO, + TRequestModel, + TRequestDTO + > + ) { + const { order = 100, min = 100, max = 200 } = options ?? {}; + this.order = order; + this.min = min; + this.max = max; + } + + handle: AcquireMiddlewareFn< + TCallArgs, + TResponseModel, + TResponseDTO, + TRequestModel, + TRequestDTO + > = (context) => { + const min = isCallable(this.min) ? this.min(context) : this.min; + const max = isCallable(this.max) ? this.max(context) : this.max; + + const delay = Math.random() * (max - min) + min; + + return new Promise((resolve) => setTimeout(resolve, delay)); + }; +} diff --git a/packages/core/src/middleware/AcquireRequestLogger.ts b/packages/core/src/middleware/RequestLogger.middleware.ts similarity index 84% rename from packages/core/src/middleware/AcquireRequestLogger.ts rename to packages/core/src/middleware/RequestLogger.middleware.ts index e6ded31..185413c 100644 --- a/packages/core/src/middleware/AcquireRequestLogger.ts +++ b/packages/core/src/middleware/RequestLogger.middleware.ts @@ -1,25 +1,26 @@ +import { + AcquireMiddlewareClass, + AcquireMiddlewareFn +} from "@/classes/AcquireBase.class"; import AcquireLogger, { AcquireLogColor, AcquireLoggerOptions } from "@/classes/AcquireLogger.class"; import RequestMethod from "@/constants/RequestMethod.const"; import { AcquireContext } from "@/interfaces/AcquireContext.interface"; -import { - AcquireMiddlewareClass, - AcquireMiddlewareFn -} from "@/interfaces/AcquireMiddleware.interface"; import axios from "axios"; -export interface AcquireRequestLoggerOptions extends AcquireLoggerOptions { +export interface RequestLoggerOptions extends AcquireLoggerOptions { order?: number; } -export default class AcquireRequestLogger implements AcquireMiddlewareClass { +export default class RequestLogger + implements AcquireMiddlewareClass +{ public order; - private logger: AcquireLogger; - constructor(options?: AcquireRequestLoggerOptions) { + constructor(options?: RequestLoggerOptions) { const { order = 1000, ...rest } = options ?? {}; this.logger = new AcquireLogger(rest); this.order = order; @@ -44,7 +45,10 @@ export default class AcquireRequestLogger implements AcquireMiddlewareClass { } } - logAcquireCall(logger: AcquireLogger, context: AcquireContext): void { + logAcquireCall( + logger: AcquireLogger, + context: AcquireContext + ): void { const { response, type, method, error } = context; const isMocked = type === "mocking"; const isOnDemand = response.data == null && error == null; @@ -86,7 +90,7 @@ export default class AcquireRequestLogger implements AcquireMiddlewareClass { : logger.info(...log); } - handle: AcquireMiddlewareFn = (context) => { + handle: AcquireMiddlewareFn = (context) => { return this.logAcquireCall(this.logger, context); }; } diff --git a/packages/core/tests/classes/Acquire.test.ts b/packages/core/tests/classes/Acquire.test.ts index 6c4a543..b8c04b3 100644 --- a/packages/core/tests/classes/Acquire.test.ts +++ b/packages/core/tests/classes/Acquire.test.ts @@ -1,111 +1,7 @@ import { Acquire } from "@/classes/Acquire.class"; -import { AcquireRequestExecutor } from "@/classes/AcquireRequestExecutor.class"; describe("class: Acquire", () => { const acquire = new Acquire(); - const requestExecutorExecuteSpy = jest - .spyOn(AcquireRequestExecutor.prototype, "execute") - .mockImplementation(() => Promise.resolve() as any); - - const requestExecutorMockSpy = jest - .spyOn(AcquireRequestExecutor.prototype, "mock") - .mockImplementation(() => Promise.resolve() as any); - - beforeEach(() => { - acquire.disableMocking(); - }); - - afterEach(() => { - requestExecutorExecuteSpy.mockClear(); - requestExecutorMockSpy.mockClear(); - }); - - describe("executing callable instance", () => { - it("should call execute on AcquireRequestExecutor with the correct value arguments", async () => { - const getUser = acquire({ - request: { - url: "https://example.com/user/1", - headers: { Authorization: "Bearer token" } - } - }); - - await getUser(); - - expect(requestExecutorExecuteSpy).toHaveBeenCalledWith({ - request: { - url: "https://example.com/user/1", - headers: { Authorization: "Bearer token" } - } - }); - }); - }); - - describe("mocking callable instance", () => { - it("should call mock on AcquireRequestExecutor with the correct value arguments", async () => { - const getUser = acquire({ - request: { - url: "https://example.com/user/1", - headers: { Authorization: "Bearer token" } - } - }); - - await getUser.mock(); - - expect(requestExecutorMockSpy).toHaveBeenCalledWith({ - request: { - url: "https://example.com/user/1", - headers: { Authorization: "Bearer token" } - } - }); - }); - }); - - describe("function: withCallArgs", () => { - it("should call execute on AcquireRequestExecutor with the acquireArgs and callArgs", async () => { - const getUrl = (args?: { userId: number }): string => - `https://example.com/user/${args?.userId}`; - - class UserModel {} - - const getUser = acquire.withCallArgs<{ userId: number }>()({ - request: { - url: getUrl - }, - responseMapping: { - Model: UserModel - } - }); - - await getUser({ userId: 10 }); - - expect(requestExecutorExecuteSpy).toHaveBeenCalledWith( - { - request: { - url: getUrl - }, - responseMapping: { - Model: UserModel - } - }, - { userId: 10 } - ); - }); - }); - - describe("function: isAcquireInstance", () => { - it("should return true when called with an instance of Acquire", () => { - const acquire = new Acquire(); - - expect(Acquire.isAcquireInstance(acquire)).toBe(true); - }); - - it("should return false for any other input", () => { - expect(Acquire.isAcquireInstance({})).toBe(false); - expect(Acquire.isAcquireInstance(undefined)).toBe(false); - expect(Acquire.isAcquireInstance(null)).toBe(false); - expect(Acquire.isAcquireInstance(123)).toBe(false); - expect(Acquire.isAcquireInstance("abc")).toBe(false); - }); - }); + it.todo("Add some tests :)"); }); diff --git a/packages/core/tests/classes/AcquireBase.test.ts b/packages/core/tests/classes/AcquireBase.test.ts new file mode 100644 index 0000000..8ab45c6 --- /dev/null +++ b/packages/core/tests/classes/AcquireBase.test.ts @@ -0,0 +1,189 @@ +import AcquireBase, { + AcquireMiddlewareClass, + AcquireMiddlewareFn +} from "@/classes/AcquireBase.class"; + +describe("class: AcquireBase", () => { + let acquireBase: AcquireBase; + class AcquireBaseTest extends AcquireBase {} + + beforeEach(() => { + acquireBase = new AcquireBaseTest(); + }); + + describe("function: cloneBase", () => { + it("should clone middleware to the target", () => { + const middleware: AcquireMiddlewareFn = () => {}; + acquireBase.use(middleware); + const middlewareWithOrder = [middleware, 0]; + + class AcquireBaseCloneTest extends AcquireBase {} + + let acquireBaseClone = new AcquireBaseCloneTest(); + expect(acquireBaseClone["executionMiddlewares"].length).toEqual(0); + expect(acquireBaseClone["mockingMiddlewares"].length).toEqual(0); + + acquireBaseClone = acquireBase["cloneBase"]().into(acquireBaseClone); + + expect(acquireBaseClone["executionMiddlewares"]).toEqual([ + middlewareWithOrder + ]); + expect(acquireBaseClone["mockingMiddlewares"]).toEqual([ + middlewareWithOrder + ]); + }); + }); + + describe("function: getMiddlewares", () => { + it("should resolve middlewares in the correct order", () => { + const middleware1: AcquireMiddlewareFn = () => {}; + const middleware2: AcquireMiddlewareFn = () => {}; + const middleware3: AcquireMiddlewareFn = () => {}; + + acquireBase.use(middleware1, 2).use(middleware2, 0).use(middleware3, 1); + const executionMiddlewares = acquireBase["getMiddlewares"]("execution"); + const mockingMiddlewares = acquireBase["getMiddlewares"]("mocking"); + + const resolvedMiddlewares = [middleware2, middleware3, middleware1]; + + expect(executionMiddlewares).toEqual(resolvedMiddlewares); + expect(mockingMiddlewares).toEqual(resolvedMiddlewares); + }); + + it("should return the handle method for class based middlewares", () => { + class MiddlewareTest implements AcquireMiddlewareClass { + order = 0; + handle: AcquireMiddlewareFn = + () => {}; + } + + const middlewareTest = new MiddlewareTest(); + acquireBase.use(middlewareTest); + const middlewares = acquireBase["getMiddlewares"]("execution"); + + expect(middlewares).toEqual([middlewareTest.handle]); + }); + + it("should resolve middleware functions and classes in the correct order", () => { + class MiddlewareTest implements AcquireMiddlewareClass { + order = 1; + handle: AcquireMiddlewareFn = + () => {}; + } + + const middlewareClass = new MiddlewareTest(); + const middlewareFn1: AcquireMiddlewareFn = () => {}; + const middlewareFn2: AcquireMiddlewareFn = () => {}; + + acquireBase + .use(middlewareClass) + .use(middlewareFn1, 0) + .use(middlewareFn2, 2); + + const middlewares = acquireBase["getMiddlewares"]("execution"); + + expect(middlewares).toEqual([ + middlewareFn1, + middlewareClass.handle, + middlewareFn2 + ]); + }); + + it("should allow middleware class order to be overridden", () => { + class MiddlewareTest implements AcquireMiddlewareClass { + order = 1; + handle: AcquireMiddlewareFn = + () => {}; + } + + const middlewareClass = new MiddlewareTest(); + const middlewareFn1: AcquireMiddlewareFn = () => {}; + const middlewareFn2: AcquireMiddlewareFn = () => {}; + + acquireBase + .use(middlewareClass, 0) + .use(middlewareFn1, 1) + .use(middlewareFn2, 2); + + const middlewares = acquireBase["getMiddlewares"]("execution"); + + expect(middlewares).toEqual([ + middlewareClass.handle, + middlewareFn1, + middlewareFn2 + ]); + }); + }); + + describe("function: use", () => { + it("should add middleware to both execution and mocking middlewares", () => { + const middleware: AcquireMiddlewareFn = () => {}; + + acquireBase.use(middleware); + const middlewareWithOrder = [middleware, 0]; + + expect(acquireBase["executionMiddlewares"]).toEqual([ + middlewareWithOrder + ]); + expect(acquireBase["mockingMiddlewares"]).toEqual([middlewareWithOrder]); + }); + + it("should add middleware to both execution and mocking middlewares with order", () => { + const middleware: AcquireMiddlewareFn = () => {}; + + acquireBase.use(middleware, 1); + const middlewareWithOrder = [middleware, 1]; + + expect(acquireBase["executionMiddlewares"]).toEqual([ + middlewareWithOrder + ]); + expect(acquireBase["mockingMiddlewares"]).toEqual([middlewareWithOrder]); + }); + }); + + describe("function: useOnExecution", () => { + it("should add middleware to execution middlewares", () => { + const middleware: AcquireMiddlewareFn = () => {}; + + acquireBase.useOnExecution(middleware); + const middlewareWithOrder = [middleware, 0]; + + expect(acquireBase["executionMiddlewares"]).toEqual([ + middlewareWithOrder + ]); + expect(acquireBase["mockingMiddlewares"]).not.toEqual([ + middlewareWithOrder + ]); + }); + }); + + describe("function: useOnMocking", () => { + it("should add middleware to execution middlewares", () => { + const middleware: AcquireMiddlewareFn = () => {}; + + acquireBase.useOnMocking(middleware); + const middlewareWithOrder = [middleware, 0]; + + expect(acquireBase["executionMiddlewares"]).not.toEqual([ + middlewareWithOrder + ]); + expect(acquireBase["mockingMiddlewares"]).toEqual([middlewareWithOrder]); + }); + }); + + describe("function: clearMiddlewares", () => { + it("should clear all middlewares", () => { + const middleware: AcquireMiddlewareFn = () => {}; + + acquireBase.use(middleware).use(middleware).use(middleware); + + expect(acquireBase["executionMiddlewares"].length).toEqual(3); + expect(acquireBase["mockingMiddlewares"].length).toEqual(3); + + acquireBase.clearMiddlewares(); + + expect(acquireBase["executionMiddlewares"].length).toEqual(0); + expect(acquireBase["mockingMiddlewares"].length).toEqual(0); + }); + }); +}); diff --git a/packages/core/tests/classes/AcquireRequestExecutor.test.ts b/packages/core/tests/classes/AcquireRequestExecutor.test.ts deleted file mode 100644 index f77292c..0000000 --- a/packages/core/tests/classes/AcquireRequestExecutor.test.ts +++ /dev/null @@ -1,870 +0,0 @@ -import AcquireMockCache from "@/classes/AcquireMockCache.class"; -import { AcquireRequestExecutor } from "@/classes/AcquireRequestExecutor.class"; -import Mock from "@/decorators/mocks/Mock.decorator"; -import isPlainObject from "@tests/testing-utils/isPlainObject.function"; -import axios, { AxiosError, AxiosResponse } from "axios"; -import MockAdapter from "axios-mock-adapter"; -import { Chance } from "chance"; -import { Expose } from "class-transformer"; - -describe("class: AcquireRequestExecutor", () => { - const axiosInstance = axios.create(); - const mockAxios = new MockAdapter(axiosInstance); - const chance = new Chance(); - - afterEach(() => { - mockAxios.reset(); - }); - - describe("executing callable instance", () => { - class UserDTO { - id: number; - name: string; - age: number; - } - - class UserModel { - @Expose() id: number; - @Expose() name: string; - @Expose() age: number; - } - - class CreateUserDTO { - @Expose() name: string; - @Expose() age: number; - } - - class CreateUserModel { - name: string; - age: number; - } - - it("should call axios with the correct value arguments", async () => { - const getUser = new AcquireRequestExecutor(() => ({ - axiosInstance - })); - - const mockResponse: Partial = { - data: { - id: 1, - name: chance.name(), - age: chance.age() - } - }; - - mockAxios - .onGet("https://example.com/user/1") - .reply(200, mockResponse.data); - - const response = await getUser({ - request: { - url: "https://example.com/user/1", - headers: { Authorization: "Bearer token" } - } - }); - - expect(response.response.data).toEqual(mockResponse.data); - expect(response.response.config.url).toEqual( - "https://example.com/user/1" - ); - expect(response.response.config.headers.Authorization).toEqual( - "Bearer token" - ); - }); - - it("should call axios with the correct function arguments", async () => { - const getUser = new AcquireRequestExecutor<{ userId: number }>(() => ({ - axiosInstance - })); - - const mockResponse: Partial = { - data: { - id: 10, - name: chance.name(), - age: chance.age() - } - }; - - mockAxios - .onGet("https://example.com/user/10") - .reply(200, mockResponse.data); - - const response = await getUser( - { - request: { - url: (args) => `https://example.com/user/${args?.userId}`, - headers: { Authorization: "Bearer token" } - } - }, - { userId: 10 } - ); - - expect(response.response.data).toEqual(mockResponse.data); - expect(response.response.config.url).toEqual( - "https://example.com/user/10" - ); - expect(response.response.config.headers.Authorization).toEqual( - "Bearer token" - ); - }); - - it("should transform the response data into a DTO and Model class instance", async () => { - const getUser = new AcquireRequestExecutor< - any, - typeof UserDTO, - typeof UserModel - >(() => ({ - axiosInstance - })); - - const mockResponse: Partial = { - data: { - id: 1, - name: chance.name(), - age: chance.age() - } - }; - - mockAxios - .onGet("https://example.com/user/1") - .reply(200, mockResponse.data); - - const response = await getUser({ - request: { - url: "https://example.com/user/1" - }, - responseMapping: { - DTO: UserDTO, - Model: UserModel - } - }); - - expect(response.dto).toBeInstanceOf(UserDTO); - expect(response.model).toBeInstanceOf(UserModel); - }); - - it("should transform the response data into a DTO and Model class instance array", async () => { - const getUsers = new AcquireRequestExecutor< - any, - [typeof UserDTO], - [typeof UserModel] - >(() => ({ - axiosInstance - })); - - const mockResponse: Partial = { - data: Array.from({ length: 10 }).map(() => ({ - id: chance.natural(), - name: chance.name(), - age: chance.age() - })) - }; - - mockAxios - .onGet("https://example.com/users") - .reply(200, mockResponse.data); - - const response = await getUsers({ - request: { - url: `https://example.com/users` - }, - responseMapping: { - DTO: [UserDTO], - Model: [UserModel] - } - }); - - expect(response.dto.length).toEqual(10); - response.dto.forEach((dto) => { - expect(dto).toBeInstanceOf(UserDTO); - }); - - expect(response.model.length).toEqual(10); - response.model.forEach((model) => { - expect(model).toBeInstanceOf(UserModel); - }); - }); - - it("should forward data if no request DTO is provided", async () => { - const requestSpy = jest - .spyOn(axiosInstance, "request") - .mockImplementationOnce(() => Promise.resolve({ data: {} })); - - const createUser = new AcquireRequestExecutor(() => ({ - axiosInstance - })); - - const userFormData = new CreateUserModel(); - userFormData.age = chance.age(); - userFormData.name = chance.name(); - - await createUser( - { - request: { - url: "https://example.com/user/1", - method: "POST" - } - }, - { - data: userFormData - } - ); - - expect(requestSpy).toHaveBeenCalledWith( - expect.objectContaining({ - data: { - age: userFormData.age, - name: userFormData.name - } - }) - ); - - requestSpy.mockRestore(); - }); - - it("should transform the request Model instance into a DTO class instance", async () => { - const requestSpy = jest - .spyOn(axiosInstance, "request") - .mockImplementationOnce(() => Promise.resolve({ data: {} })); - - const createUser = new AcquireRequestExecutor< - any, - any, - any, - typeof CreateUserDTO, - typeof CreateUserModel - >(() => ({ - axiosInstance - })); - - const userFormData = new CreateUserModel(); - userFormData.age = chance.age(); - userFormData.name = chance.name(); - - await createUser( - { - request: { - url: "https://example.com/user/1", - method: "POST" - }, - requestMapping: { - DTO: CreateUserDTO, - Model: CreateUserModel - } - }, - { - data: userFormData - } - ); - - expect(requestSpy).toHaveBeenCalledWith( - expect.objectContaining({ - data: { - age: userFormData.age, - name: userFormData.name - } - }) - ); - - requestSpy.mockRestore(); - }); - - it("should execute the right middlewares from the instance", async () => { - axiosInstance.request = jest - .fn() - .mockImplementationOnce(() => Promise.resolve({ data: {} })); - - const getUser = new AcquireRequestExecutor(() => ({ - axiosInstance - })); - - const middlewareExecution = jest.fn(); - const middlewareMocking = jest.fn(); - const middlewareBoth = jest.fn(); - - getUser - .useOnExecution(middlewareExecution) - .useOnMocking(middlewareMocking) - .use(middlewareBoth); - - await getUser({ - request: { - url: "https://example.com/user/10" - } - }); - - expect(middlewareExecution).toHaveBeenCalled(); - expect(middlewareMocking).not.toHaveBeenCalled(); - expect(middlewareBoth).toHaveBeenCalled(); - }); - - it("should execute the right middlewares from the config", async () => { - axiosInstance.request = jest - .fn() - .mockImplementationOnce(() => Promise.resolve({ data: {} })); - - const middlewareExecution = jest.fn(); - const middlewareMocking = jest.fn(); - const middlewareBoth = jest.fn(); - - const getUser = new AcquireRequestExecutor(() => ({ - axiosInstance, - executionMiddlewares: [ - [middlewareExecution, 0], - [middlewareBoth, 0] - ], - mockingMiddlewares: [ - [middlewareMocking, 0], - [middlewareBoth, 0] - ] - })); - - await getUser({ - request: { - url: "https://example.com/user/10" - } - }); - - expect(middlewareExecution).toHaveBeenCalled(); - expect(middlewareMocking).not.toHaveBeenCalled(); - expect(middlewareBoth).toHaveBeenCalled(); - }); - - it("should execute the right middlewares from both the instance and config", async () => { - axiosInstance.request = jest - .fn() - .mockImplementationOnce(() => Promise.resolve({ data: {} })); - - const configMiddlewareExecution = jest.fn(); - const configMiddlewareMocking = jest.fn(); - const configMiddlewareBoth = jest.fn(); - - const getUser = new AcquireRequestExecutor(() => ({ - axiosInstance, - executionMiddlewares: [ - [configMiddlewareExecution, 0], - [configMiddlewareBoth, 0] - ], - mockingMiddlewares: [ - [configMiddlewareMocking, 0], - [configMiddlewareBoth, 0] - ] - })); - - const instanceMiddlewareExecution = jest.fn(); - const instanceMiddlewareMocking = jest.fn(); - const instanceMiddlewareBoth = jest.fn(); - - getUser - .useOnExecution(instanceMiddlewareExecution) - .useOnMocking(instanceMiddlewareMocking) - .use(instanceMiddlewareBoth); - - await getUser({ - request: { - url: "https://example.com/user/10" - } - }); - - expect(configMiddlewareExecution).toHaveBeenCalled(); - expect(configMiddlewareMocking).not.toHaveBeenCalled(); - expect(configMiddlewareBoth).toHaveBeenCalled(); - expect(instanceMiddlewareExecution).toHaveBeenCalled(); - expect(instanceMiddlewareMocking).not.toHaveBeenCalled(); - expect(instanceMiddlewareBoth).toHaveBeenCalled(); - }); - - it("should call middleware with the right context arguments", async () => { - axiosInstance.request = jest.fn().mockImplementationOnce(() => - Promise.resolve({ - data: { - id: 10, - name: "Christian Bale", - age: 49 - } - }) - ); - - const mockInterceptor = jest.fn(); - const mockCache = new AcquireMockCache(); - - const getUser = new AcquireRequestExecutor< - { userId: number }, - typeof UserDTO, - typeof UserModel - >(() => ({ - axiosInstance, - mockCache - })); - - getUser.useOnExecution(mockInterceptor); - - const url = (args: { userId?: number } | undefined): string => - `https://example.com/user/${args?.userId}`; - - await getUser( - { - request: { - url, - headers: { - Accept: "application/json" - } - }, - responseMapping: { - DTO: UserDTO, - Model: UserModel - } - }, - { userId: 10 } - ); - - expect(mockInterceptor).toHaveBeenCalledWith( - expect.objectContaining({ - response: expect.objectContaining({ - data: { - id: 10, - name: "Christian Bale", - age: 49 - } - }), - type: "execution", - method: "GET", - acquireArgs: expect.objectContaining({ - responseMapping: { - DTO: UserDTO, - Model: UserModel - }, - request: expect.objectContaining({ - headers: { - Accept: "application/json" - }, - url: "https://example.com/user/10" - }) - }), - callArgs: { - userId: 10 - }, - mockCache - }) - ); - }); - - it.todo("should execute middleware in the correct order"); - - it("should allow errors to be handled by middleware", async () => { - axiosInstance.request = jest - .fn() - .mockImplementationOnce(() => - Promise.reject(new AxiosError("Not found")) - ); - - const mockInterceptor = jest.fn(); - - const getUser = new AcquireRequestExecutor(() => ({ - axiosInstance - })); - - getUser.useOnExecution(mockInterceptor); - - await expect( - getUser({ - request: { - url: "http://example.com/user/10" - }, - responseMapping: { - DTO: UserDTO, - Model: UserModel - } - }) - ).rejects.toThrow(); - - expect(mockInterceptor).toHaveBeenCalledWith( - expect.objectContaining({ - error: expect.objectContaining({ - message: "Not found" - }) - }) - ); - }); - }); - - describe("mocking callable instance", () => { - class UserDTO { - @Mock(10) id: number; - @Mock("Christian Bale") name: string; - @Mock(49) age: number; - } - - class UserModel { - @Expose() id: number; - @Expose() name: string; - @Expose() age: number; - } - - it("should call mock and not execute when the callable instance is called with mock", async () => { - const getUsers = new AcquireRequestExecutor(() => ({ - axiosInstance - })); - - const executeSpy = jest - .spyOn(getUsers, "execute") - .mockImplementation(() => Promise.resolve({} as any)); - const mockSpy = jest - .spyOn(getUsers, "mock") - .mockImplementation(() => Promise.resolve({} as any)); - - await getUsers.mock({ - request: { - url: "https://example.com/users" - } - }); - - expect(executeSpy).not.toHaveBeenCalled(); - expect(mockSpy).toHaveBeenCalled(); - - executeSpy.mockRestore(); - mockSpy.mockRestore(); - }); - - it("should call mock and not execute when mocking is enabled", async () => { - const getUsers = new AcquireRequestExecutor(() => ({ - axiosInstance, - isMockingEnabled: true - })); - - const executeSpy = jest - .spyOn(getUsers, "execute") - .mockImplementation(() => Promise.resolve({} as any)); - const mockSpy = jest - .spyOn(getUsers, "mock") - .mockImplementation(() => Promise.resolve({} as any)); - - await getUsers({ - request: { - url: "https://example.com/users" - } - }); - - expect(executeSpy).not.toHaveBeenCalled(); - expect(mockSpy).toHaveBeenCalled(); - - executeSpy.mockRestore(); - mockSpy.mockRestore(); - }); - - it("should mock DTO and Model when a DTO class is provided", async () => { - const getUser = new AcquireRequestExecutor< - any, - typeof UserDTO, - typeof UserModel - >(() => ({ - axiosInstance - })); - - const response = await getUser.mock({ - request: { - url: "https://example.com/user/10" - }, - responseMapping: { - DTO: UserDTO, - Model: UserModel - } - }); - - expect(isPlainObject(response.response.data)).toEqual(true); - expect(isPlainObject(response.dto)).toEqual(false); - expect(isPlainObject(response.model)).toEqual(false); - - expect(response.dto).toBeInstanceOf(UserDTO); - expect(response.dto.id).toEqual(10); - expect(response.dto.name).toEqual("Christian Bale"); - expect(response.dto.age).toEqual(49); - - expect(response.model).toBeInstanceOf(UserModel); - expect(response.model.id).toEqual(10); - expect(response.model.name).toEqual("Christian Bale"); - expect(response.model.age).toEqual(49); - }); - - it("should mock DTO and Model arrays when a DTO class array is provided", async () => { - const getUsers = new AcquireRequestExecutor< - any, - [typeof UserDTO], - [typeof UserModel] - >(() => ({ - axiosInstance - })); - - const response = await getUsers.mock({ - request: { - url: "https://example.com/users" - }, - responseMapping: { - DTO: [UserDTO], - Model: [UserModel] - } - }); - - const responseWithMockCount = await getUsers.mock( - { - request: { - url: "https://example.com/users" - }, - responseMapping: { - DTO: [UserDTO], - Model: [UserModel] - } - }, - { $count: 20 } - ); - - expect(response.dto.length).toEqual(10); // The default value of mockCount is 10 - response.dto.forEach((dto) => { - expect(dto).toBeInstanceOf(UserDTO); - }); - - expect(response.model.length).toEqual(10); - response.model.forEach((model) => { - expect(model).toBeInstanceOf(UserModel); - }); - - expect(responseWithMockCount.dto.length).toEqual(20); - responseWithMockCount.dto.forEach((dto) => { - expect(dto).toBeInstanceOf(UserDTO); - }); - - expect(responseWithMockCount.model.length).toEqual(20); - responseWithMockCount.model.forEach((model) => { - expect(model).toBeInstanceOf(UserModel); - }); - }); - - it("should execute the right middlewares from the instance", async () => { - const getUser = new AcquireRequestExecutor(() => ({ - axiosInstance - })); - - const middlewareExecution = jest.fn(); - const middlewareMocking = jest.fn(); - const middlewareBoth = jest.fn(); - - getUser - .useOnExecution(middlewareExecution) - .useOnMocking(middlewareMocking) - .use(middlewareBoth); - - await getUser.mock({ - request: { - url: "https://example.com/user/10" - } - }); - - expect(middlewareExecution).not.toHaveBeenCalled(); - expect(middlewareMocking).toHaveBeenCalled(); - expect(middlewareBoth).toHaveBeenCalled(); - }); - - it("should execute the right middlewares from the config", async () => { - const middlewareExecution = jest.fn(); - const middlewareMocking = jest.fn(); - const middlewareBoth = jest.fn(); - - const getUser = new AcquireRequestExecutor(() => ({ - axiosInstance: axios, - executionMiddlewares: [ - [middlewareExecution, 0], - [middlewareBoth, 0] - ], - mockingMiddlewares: [ - [middlewareMocking, 0], - [middlewareBoth, 0] - ] - })); - - await getUser.mock({ - request: { - url: "https://example.com/user/10" - } - }); - - expect(middlewareExecution).not.toHaveBeenCalled(); - expect(middlewareMocking).toHaveBeenCalled(); - expect(middlewareBoth).toHaveBeenCalled(); - }); - - it("should execute the right middlewares from both the instance and config", async () => { - const configMiddlewareExecution = jest.fn(); - const configMiddlewareMocking = jest.fn(); - const configMiddlewareBoth = jest.fn(); - - const getUser = new AcquireRequestExecutor(() => ({ - axiosInstance, - executionMiddlewares: [ - [configMiddlewareExecution, 0], - [configMiddlewareBoth, 0] - ], - mockingMiddlewares: [ - [configMiddlewareMocking, 0], - [configMiddlewareBoth, 0] - ] - })); - - const instanceMiddlewareExecution = jest.fn(); - const instanceMiddlewareMocking = jest.fn(); - const instanceMiddlewareBoth = jest.fn(); - - getUser - .useOnExecution(instanceMiddlewareExecution) - .useOnMocking(instanceMiddlewareMocking) - .use(instanceMiddlewareBoth); - - await getUser.mock({ - request: { - url: "https://example.com/user/10" - } - }); - - expect(configMiddlewareExecution).not.toHaveBeenCalled(); - expect(configMiddlewareMocking).toHaveBeenCalled(); - expect(configMiddlewareBoth).toHaveBeenCalled(); - expect(instanceMiddlewareExecution).not.toHaveBeenCalled(); - expect(instanceMiddlewareMocking).toHaveBeenCalled(); - expect(instanceMiddlewareBoth).toHaveBeenCalled(); - }); - - it.todo("should execute middleware in the correct order"); - - it("should call middleware with the right context arguments", async () => { - const mockInterceptor = jest.fn(); - const mockCache = new AcquireMockCache(); - - const getUser = new AcquireRequestExecutor< - { userId: number }, - typeof UserDTO, - typeof UserModel - >(() => ({ - axiosInstance, - mockCache - })); - - getUser.useOnMocking(mockInterceptor); - - const url = (args: { userId?: number } | undefined): string => - `https://example.com/user/${args?.userId}`; - - await getUser.mock( - { - request: { - url, - headers: { - Accept: "application/json" - } - }, - responseMapping: { - DTO: UserDTO, - Model: UserModel - } - }, - { userId: 10 } - ); - - expect(mockInterceptor).toHaveBeenCalledWith( - expect.objectContaining({ - response: expect.objectContaining({ - config: expect.objectContaining({ - headers: { - Accept: "application/json" - }, - url: "https://example.com/user/10" - }), - status: 200, - statusText: "OK", - data: { - id: 10, - name: "Christian Bale", - age: 49 - } - }), - type: "mocking", - method: "GET", - acquireArgs: expect.objectContaining({ - responseMapping: { - DTO: UserDTO, - Model: UserModel - }, - request: expect.objectContaining({ - headers: { - Accept: "application/json" - }, - url: "https://example.com/user/10" - }) - }), - callArgs: { - userId: 10 - }, - mockCache - }) - ); - }); - - it("should forward the context parameters to the Mock decorator", async () => { - class MockTestDTO { - @Mock((ctx) => ctx?.callArgs?.id ?? 10) id: number; - } - - const getUser = new AcquireRequestExecutor< - { id: number }, - typeof MockTestDTO - >(() => ({ - axiosInstance - })); - - const response = await getUser.mock( - { - request: { - url: (args) => `https://example.com/test/${args?.id}`, - headers: { - Accept: "application/json" - } - }, - responseMapping: { - DTO: MockTestDTO - } - }, - { id: 5 } - ); - - expect(response.dto.id).toEqual(5); - }); - }); - - describe("function: isAcquireRequestExecutorInstance", () => { - it("should return true when called with an instance of AcquireRequestExecutor", () => { - const requestExecutor = new AcquireRequestExecutor(() => ({ - axiosInstance - })); - - expect( - AcquireRequestExecutor.isAcquireRequestExecutorInstance(requestExecutor) - ).toBe(true); - }); - - it("should return false for any other input", () => { - expect(AcquireRequestExecutor.isAcquireRequestExecutorInstance({})).toBe( - false - ); - expect( - AcquireRequestExecutor.isAcquireRequestExecutorInstance(undefined) - ).toBe(false); - expect( - AcquireRequestExecutor.isAcquireRequestExecutorInstance(null) - ).toBe(false); - expect(AcquireRequestExecutor.isAcquireRequestExecutorInstance(123)).toBe( - false - ); - expect( - AcquireRequestExecutor.isAcquireRequestExecutorInstance("abc") - ).toBe(false); - }); - }); -}); diff --git a/packages/core/tests/classes/AcquireRequestHandler.test.ts b/packages/core/tests/classes/AcquireRequestHandler.test.ts new file mode 100644 index 0000000..eafa980 --- /dev/null +++ b/packages/core/tests/classes/AcquireRequestHandler.test.ts @@ -0,0 +1,555 @@ +import AcquireMockCache from "@/classes/AcquireMockCache.class"; +import { AcquireRequestHandler } from "@/classes/AcquireRequestHandler.class"; +import Mock from "@/decorators/mocks/Mock.decorator"; +import isPlainObject from "@tests/testing-utils/isPlainObject.function"; +import axios, { AxiosError, AxiosResponse } from "axios"; +import MockAdapter from "axios-mock-adapter"; +import { Chance } from "chance"; +import { Expose } from "class-transformer"; + +describe("class: AcquireRequestHandler", () => { + const axiosInstance = axios.create(); + const mockAxios = new MockAdapter(axiosInstance); + const chance = new Chance(); + + afterEach(() => { + mockAxios.reset(); + }); + + describe("execution", () => { + class UserDTO { + id: number; + name: string; + age: number; + } + + class UserModel { + @Expose() id: number; + @Expose() name: string; + @Expose() age: number; + } + + class CreateUserDTO { + @Expose() name: string; + @Expose() age: number; + + isCreateUserDTO = true; + } + + class CreateUserModel { + name: string; + age: number; + } + + it("should call axios with the correct value arguments", async () => { + const getUser = new AcquireRequestHandler( + { + url: "https://api.example.com/user/1", + headers: { Authorization: "Bearer token" } + }, + () => ({ + axiosInstance + }) + ); + + const mockResponse: Partial = { + data: { + id: 1, + name: chance.name(), + age: chance.age() + } + }; + + mockAxios + .onGet("https://api.example.com/user/1") + .reply(200, mockResponse.data); + + const response = await getUser(); + + expect(response.response.data).toEqual(mockResponse.data); + expect(response.response.config.url).toEqual( + "https://api.example.com/user/1" + ); + expect(response.response.config.headers.Authorization).toEqual( + "Bearer token" + ); + }); + + it("should call axios with the correct function arguments", async () => { + const getUser = new AcquireRequestHandler<{ userId: number }>( + { + url: ({ userId }): string => `https://api.example.com/user/${userId}`, + headers: { Authorization: "Bearer token" } + }, + () => ({ + axiosInstance + }) + ); + + const mockResponse: Partial = { + data: { + id: 10, + name: chance.name(), + age: chance.age() + } + }; + + mockAxios + .onGet("https://api.example.com/user/10") + .reply(200, mockResponse.data); + + const response = await getUser({ userId: 10 }); + + expect(response.response.data).toEqual(mockResponse.data); + expect(response.response.config.url).toEqual( + "https://api.example.com/user/10" + ); + expect(response.response.config.headers.Authorization).toEqual( + "Bearer token" + ); + }); + + it("should transform the response data into a DTO and Model class instance", async () => { + const getUser = new AcquireRequestHandler( + { + url: "https://api.example.com/user/1" + }, + () => ({ + axiosInstance + }), + UserModel, + UserDTO + ); + + const mockResponse: Partial = { + data: { + id: 1, + name: chance.name(), + age: chance.age() + } + }; + + mockAxios + .onGet("https://api.example.com/user/1") + .reply(200, mockResponse.data); + + const response = await getUser(); + + expect(response.dto).toBeInstanceOf(UserDTO); + expect(response.model).toBeInstanceOf(UserModel); + }); + + it("should transform the response data into a DTO and Model class instance array", async () => { + const getUsers = new AcquireRequestHandler( + { + url: "https://api.example.com/users" + }, + () => ({ + axiosInstance + }), + [UserModel], + [UserDTO] + ); + + const mockResponse: Partial = { + data: Array.from({ length: 10 }).map(() => ({ + id: chance.natural(), + name: chance.name(), + age: chance.age() + })) + }; + + mockAxios + .onGet("https://api.example.com/users") + .reply(200, mockResponse.data); + + const response = await getUsers(); + + expect(response.dto.length).toEqual(10); + response.dto.forEach((dto) => { + expect(dto).toBeInstanceOf(UserDTO); + }); + + expect(response.model.length).toEqual(10); + response.model.forEach((model) => { + expect(model).toBeInstanceOf(UserModel); + }); + }); + + it("should forward data without transforming it if request Model is provided, but no request DTO", async () => { + const requestSpy = jest + .spyOn(axiosInstance, "request") + .mockImplementationOnce(() => Promise.resolve({ data: {} })); + + const createUser = new AcquireRequestHandler( + { + url: "https://api.example.com/user/1", + method: "POST" + }, + () => ({ + axiosInstance + }), + UserModel, + UserDTO, + CreateUserModel + ); + + const userData = new CreateUserModel(); + userData.age = chance.age(); + userData.name = chance.name(); + + await createUser({ + data: userData + }); + + expect(requestSpy).toHaveBeenCalledWith( + expect.objectContaining({ + data: { + age: userData.age, + name: userData.name + } + }) + ); + + requestSpy.mockRestore(); + }); + + it("should transform the data into a DTO if a request DTO is provided", async () => { + const requestSpy = jest + .spyOn(axiosInstance, "request") + .mockImplementationOnce(() => Promise.resolve({ data: {} })); + + const createUser = new AcquireRequestHandler( + { + url: "https://api.example.com/user/1", + method: "POST" + }, + () => ({ + axiosInstance + }), + UserModel, + UserDTO, + CreateUserModel, + CreateUserDTO + ); + + const userData = new CreateUserModel(); + userData.age = chance.age(); + userData.name = chance.name(); + + await createUser({ + data: userData + }); + + expect(requestSpy).toHaveBeenCalledWith( + expect.objectContaining({ + data: { + age: userData.age, + name: userData.name, + isCreateUserDTO: true + } + }) + ); + + requestSpy.mockRestore(); + }); + + it("should execute the right middlewares", async () => { + axiosInstance.request = jest + .fn() + .mockImplementationOnce(() => Promise.resolve({ data: {} })); + + const getUser = new AcquireRequestHandler( + { + url: "https://api.example.com/user/10" + }, + () => ({ + axiosInstance + }) + ); + + const middlewareExecution = jest.fn(); + const middlewareMocking = jest.fn(); + const middlewareBoth = jest.fn(); + + getUser + .useOnExecution(middlewareExecution) + .useOnMocking(middlewareMocking) + .use(middlewareBoth); + + await getUser(); + + expect(middlewareExecution).toHaveBeenCalled(); + expect(middlewareMocking).not.toHaveBeenCalled(); + expect(middlewareBoth).toHaveBeenCalled(); + }); + + it("should allow errors to be handled by middleware", async () => { + axiosInstance.request = jest + .fn() + .mockImplementationOnce(() => + Promise.reject(new AxiosError("Not found")) + ); + + const mockInterceptor = jest.fn(); + + const getUser = new AcquireRequestHandler( + { + url: "http://api.example.com/user/10" + }, + () => ({ + axiosInstance + }) + ); + + getUser.useOnExecution(mockInterceptor); + + await expect(getUser()).rejects.toThrow(); + + expect(mockInterceptor).toHaveBeenCalledWith( + expect.objectContaining({ + error: expect.objectContaining({ + message: "Not found" + }) + }) + ); + }); + }); + + describe("mocking", () => { + const fakeName = chance.name(); + const fakeAge = chance.age(); + const fakeId = chance.natural(); + class UserDTO { + @Mock(fakeId) id: number; + @Mock(fakeName) name: string; + @Mock(fakeAge) age: number; + } + + class UserModel { + @Expose() id: number; + @Expose() name: string; + @Expose() age: number; + } + + it("should call mock and not execute when the callable instance is called with mock", async () => { + const executeSpy = jest + .spyOn(AcquireRequestHandler.prototype, "execute") + .mockImplementation(() => Promise.resolve({} as any)); + const mockSpy = jest + .spyOn(AcquireRequestHandler.prototype, "mock") + .mockImplementation(() => Promise.resolve({} as any)); + + const getUsers = new AcquireRequestHandler( + { + url: "https://api.example.com/users" + }, + () => ({ + axiosInstance + }) + ); + + await getUsers.mock(); + + expect(executeSpy).not.toHaveBeenCalled(); + expect(mockSpy).toHaveBeenCalled(); + + executeSpy.mockRestore(); + mockSpy.mockRestore(); + }); + + it("should call mock and not execute when mocking is enabled globally", async () => { + const executeSpy = jest + .spyOn(AcquireRequestHandler.prototype, "execute") + .mockImplementation(() => Promise.resolve({} as any)); + const mockSpy = jest + .spyOn(AcquireRequestHandler.prototype, "mock") + .mockImplementation(() => Promise.resolve({} as any)); + + const getUsers = new AcquireRequestHandler( + { + url: "https://api.example.com/users" + }, + () => ({ + axiosInstance, + isMockingEnabled: true + }) + ); + + await getUsers(); + + expect(executeSpy).not.toHaveBeenCalled(); + expect(mockSpy).toHaveBeenCalled(); + + executeSpy.mockRestore(); + mockSpy.mockRestore(); + }); + + it("should mock DTO and Model when a DTO class is provided", async () => { + const getUser = new AcquireRequestHandler( + { + url: "https://api.example.com/user/10" + }, + () => ({ + axiosInstance + }), + UserModel, + UserDTO + ); + + const response = await getUser.mock(); + + expect(isPlainObject(response.response.data)).toEqual(true); + expect(isPlainObject(response.dto)).toEqual(false); + expect(isPlainObject(response.model)).toEqual(false); + + expect(response.dto).toBeInstanceOf(UserDTO); + expect(response.dto.id).toEqual(fakeId); + expect(response.dto.name).toEqual(fakeName); + expect(response.dto.age).toEqual(fakeAge); + + expect(response.model).toBeInstanceOf(UserModel); + expect(response.model.id).toEqual(fakeId); + expect(response.model.name).toEqual(fakeName); + expect(response.model.age).toEqual(fakeAge); + }); + + it("should mock DTO and Model arrays when a DTO class array is provided", async () => { + const getUsers = new AcquireRequestHandler( + { + url: "https://api.example.com/users" + }, + () => ({ + axiosInstance + }), + [UserModel], + [UserDTO] + ); + + const response = await getUsers.mock(); + + const responseWithMockCount = await getUsers.mock(20); + + expect(response.dto.length).toEqual(10); // The default value of mockCount is 10 + response.dto.forEach((dto) => { + expect(dto).toBeInstanceOf(UserDTO); + }); + + expect(response.model.length).toEqual(10); + response.model.forEach((model) => { + expect(model).toBeInstanceOf(UserModel); + }); + + expect(responseWithMockCount.dto.length).toEqual(20); + responseWithMockCount.dto.forEach((dto) => { + expect(dto).toBeInstanceOf(UserDTO); + }); + + expect(responseWithMockCount.model.length).toEqual(20); + responseWithMockCount.model.forEach((model) => { + expect(model).toBeInstanceOf(UserModel); + }); + }); + + it("should execute the right middlewares", async () => { + axiosInstance.request = jest + .fn() + .mockImplementationOnce(() => Promise.resolve({ data: {} })); + + const getUser = new AcquireRequestHandler( + { + url: "https://api.example.com/user/10" + }, + () => ({ + axiosInstance + }) + ); + + const middlewareExecution = jest.fn(); + const middlewareMocking = jest.fn(); + const middlewareBoth = jest.fn(); + + getUser + .useOnExecution(middlewareExecution) + .useOnMocking(middlewareMocking) + .use(middlewareBoth); + + await getUser.mock(); + + expect(middlewareExecution).not.toHaveBeenCalled(); + expect(middlewareMocking).toHaveBeenCalled(); + expect(middlewareBoth).toHaveBeenCalled(); + }); + + it("should call middleware with the right context arguments", async () => { + const mockInterceptor = jest.fn(); + const mockCache = new AcquireMockCache(); + + const getUser = new AcquireRequestHandler<{ userId?: number }>( + { + url: ({ userId }): string => `https://api.example.com/user/${userId}`, + headers: { + Accept: "application/json" + } + }, + () => ({ + axiosInstance, + mockCache + }), + UserModel, + UserDTO + ); + + getUser.useOnMocking(mockInterceptor); + + await getUser.mock({ userId: 10 }); + + expect(mockInterceptor).toHaveBeenCalledWith( + expect.objectContaining({ + requestConfig: expect.objectContaining({ + url: "https://api.example.com/user/10", + headers: { + Accept: "application/json" + } + }), + type: "mocking", + method: "GET", + responseModel: UserModel, + responseDTO: UserDTO, + callArgs: { userId: 10 }, + mockCache + }) + ); + }); + + // it("should forward the context parameters to the Mock decorator", async () => { + // class MockTestDTO { + // @Mock((ctx) => ctx?.callArgs?.id ?? 10) id: number; + // } + + // const getUser = new AcquireRequestHandler< + // { id: number }, + // typeof MockTestDTO + // >(() => ({ + // axiosInstance + // })); + + // const response = await getUser.mock( + // { + // request: { + // url: (args) => `https://example.com/test/${args?.id}`, + // headers: { + // Accept: "application/json" + // } + // }, + // responseMapping: { + // DTO: MockTestDTO + // } + // }, + // { id: 5 } + // ); + + // expect(response.dto.id).toEqual(5); + // }); + }); +}); diff --git a/packages/core/tests/classes/AcquireRequestHandlerFactory.test.ts b/packages/core/tests/classes/AcquireRequestHandlerFactory.test.ts new file mode 100644 index 0000000..bb8e727 --- /dev/null +++ b/packages/core/tests/classes/AcquireRequestHandlerFactory.test.ts @@ -0,0 +1,289 @@ +import { + AcquireRequestHandler, + AcquireRequestHandlerConfig +} from "@/classes/AcquireRequestHandler.class"; +import AcquireRequestHandlerFactory from "@/classes/AcquireRequestHandlerFactory.class"; + +jest.mock("@/classes/AcquireRequestHandler.class"); + +describe("class: AcquireRequestHandlerFactory", () => { + const mockGetConfig = jest.fn( + () => ({}) + ) as unknown as () => AcquireRequestHandlerConfig; + + let factory: AcquireRequestHandlerFactory; + + class UserDTO { + id: number; + name: string; + age: number; + } + + class UserModel { + id: number; + name: string; + age: number; + } + + beforeEach(() => { + factory = new AcquireRequestHandlerFactory(mockGetConfig); + }); + + describe("function: withResponseMapping", () => { + it("should return a new AcquireRequestHandlerFactory with specified response mapping", () => { + const newFactory = factory.withResponseMapping(UserModel, UserDTO); + + expect(newFactory).toBeInstanceOf(AcquireRequestHandlerFactory); + expect(newFactory["responseModel"]).toEqual(UserModel); + expect(newFactory["responseDTO"]).toEqual(UserDTO); + }); + }); + + describe("function: withRequestMapping", () => { + it("should return a new AcquireRequestHandlerFactory with specified request mapping", () => { + const newFactory = factory.withRequestMapping(UserModel, UserDTO); + + expect(newFactory).toBeInstanceOf(AcquireRequestHandlerFactory); + expect(newFactory["requestModel"]).toEqual(UserModel); + expect(newFactory["requestDTO"]).toEqual(UserDTO); + }); + }); + + describe("function: request", () => { + it("should return a generic request handler", () => { + const handler = factory.request({}); + + expect(handler).toBeInstanceOf(AcquireRequestHandler); + }); + + it("should call the request handler with the response DTO and Model classes", () => { + const handler = factory + .withResponseMapping(UserModel, UserDTO) + .request({}); + + expect(handler).toBeInstanceOf(AcquireRequestHandler); + expect(AcquireRequestHandler).toHaveBeenCalledWith( + {}, + mockGetConfig, + UserModel, + UserDTO, + undefined, + undefined + ); + }); + + it("should call the request handler with the request DTO and Model classes", () => { + const handler = factory + .withRequestMapping(UserModel, UserDTO) + .request({}); + + expect(handler).toBeInstanceOf(AcquireRequestHandler); + expect(AcquireRequestHandler).toHaveBeenCalledWith( + {}, + mockGetConfig, + undefined, + undefined, + UserModel, + UserDTO + ); + }); + }); + + describe("function: get", () => { + it("should return a GET request handler", () => { + const handler = factory.get({}); + + expect(handler).toBeInstanceOf(AcquireRequestHandler); + expect(AcquireRequestHandler).toHaveBeenCalledWith( + expect.objectContaining({ + method: "GET" + }), + mockGetConfig, + undefined, + undefined, + undefined, + undefined + ); + }); + + it("should return a GET request handler while allowing additional request config to be set", () => { + const path = ({ id }: { id: number }): string => `/users/${id}`; + const handler = factory.get<{ id: number }>({ path }); + + expect(handler).toBeInstanceOf(AcquireRequestHandler); + expect(AcquireRequestHandler).toHaveBeenCalledWith( + expect.objectContaining({ + method: "GET", + path + }), + mockGetConfig, + undefined, + undefined, + undefined, + undefined + ); + }); + }); + + describe("function: post", () => { + it("should return a POST request handler", () => { + const handler = factory.post({}); + + expect(handler).toBeInstanceOf(AcquireRequestHandler); + expect(AcquireRequestHandler).toHaveBeenCalledWith( + expect.objectContaining({ + method: "POST" + }), + mockGetConfig, + undefined, + undefined, + undefined, + undefined + ); + }); + }); + + describe("function: put", () => { + it("should return a PUT request handler", () => { + const handler = factory.put({}); + + expect(handler).toBeInstanceOf(AcquireRequestHandler); + expect(AcquireRequestHandler).toHaveBeenCalledWith( + expect.objectContaining({ + method: "PUT" + }), + mockGetConfig, + undefined, + undefined, + undefined, + undefined + ); + }); + }); + + describe("function: patch", () => { + it("should return a PATCH request handler", () => { + const handler = factory.patch({}); + + expect(handler).toBeInstanceOf(AcquireRequestHandler); + expect(AcquireRequestHandler).toHaveBeenCalledWith( + expect.objectContaining({ + method: "PATCH" + }), + mockGetConfig, + undefined, + undefined, + undefined, + undefined + ); + }); + }); + + describe("function: delete", () => { + it("should return a DELETE request handler", () => { + const handler = factory.delete({}); + + expect(handler).toBeInstanceOf(AcquireRequestHandler); + expect(AcquireRequestHandler).toHaveBeenCalledWith( + expect.objectContaining({ + method: "DELETE" + }), + mockGetConfig, + undefined, + undefined, + undefined, + undefined + ); + }); + }); + + describe("function: head", () => { + it("should return a HEAD request handler", () => { + const handler = factory.head({}); + + expect(handler).toBeInstanceOf(AcquireRequestHandler); + expect(AcquireRequestHandler).toHaveBeenCalledWith( + expect.objectContaining({ + method: "HEAD" + }), + mockGetConfig, + undefined, + undefined, + undefined, + undefined + ); + }); + }); + + describe("function: options", () => { + it("should return a OPTIONS request handler", () => { + const handler = factory.options({}); + + expect(handler).toBeInstanceOf(AcquireRequestHandler); + expect(AcquireRequestHandler).toHaveBeenCalledWith( + expect.objectContaining({ + method: "OPTIONS" + }), + mockGetConfig, + undefined, + undefined, + undefined, + undefined + ); + }); + }); + + describe("function: purge", () => { + it("should return a PURGE request handler", () => { + const handler = factory.purge({}); + + expect(handler).toBeInstanceOf(AcquireRequestHandler); + expect(AcquireRequestHandler).toHaveBeenCalledWith( + expect.objectContaining({ + method: "PURGE" + }), + mockGetConfig, + undefined, + undefined, + undefined, + undefined + ); + }); + }); + + describe("function: link", () => { + it("should return a LINK request handler", () => { + const handler = factory.link({}); + + expect(handler).toBeInstanceOf(AcquireRequestHandler); + expect(AcquireRequestHandler).toHaveBeenCalledWith( + expect.objectContaining({ + method: "LINK" + }), + mockGetConfig, + undefined, + undefined, + undefined, + undefined + ); + }); + }); + + describe("function: unlink", () => { + it("should return a UNLINK request handler", () => { + const handler = factory.unlink({}); + + expect(handler).toBeInstanceOf(AcquireRequestHandler); + expect(AcquireRequestHandler).toHaveBeenCalledWith( + expect.objectContaining({ + method: "UNLINK" + }), + mockGetConfig, + undefined, + undefined, + undefined, + undefined + ); + }); + }); +}); diff --git a/packages/core/tests/classes/CallableInstance.test.ts b/packages/core/tests/classes/CallableInstance.test.ts deleted file mode 100644 index 4f4595e..0000000 --- a/packages/core/tests/classes/CallableInstance.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { CallableInstance } from "@/classes/CallableInstance.class"; - -interface TestCallableInstance { - (x: number): number; -} - -class TestCallableInstance extends CallableInstance { - private value = 10; - - constructor() { - super((x: number) => this.add(x)); - } - - public add(x: number): number { - return this.value + x; - } -} - -describe("class: CallableInstance", () => { - it("should correctly call the callback", () => { - const callable = new TestCallableInstance(); - expect(callable(5)).toBe(15); - }); -}); diff --git a/packages/core/tests/integration/mock-interceptor.test.ts b/packages/core/tests/integration/mock-interceptor.test.ts index e18a2e5..1cd9981 100644 --- a/packages/core/tests/integration/mock-interceptor.test.ts +++ b/packages/core/tests/integration/mock-interceptor.test.ts @@ -38,27 +38,24 @@ describe("Setup of endpoint with intercepting middleware", () => { /* ---------------------------- Define API method --------------------------- */ - const getUsers = acquire.withCallArgs<{ - sortBy?: keyof UserDTO; - sortByDescending?: boolean; - search?: string; - minAge?: number; - maxAge?: number; - }>()({ - request: { - url: "https://example.com/users", + const getUsers = acquire + .createRequestHandler() + .withResponseMapping([UserModel], [UserDTO]) + .get<{ + sortBy?: keyof UserDTO; + sortByDescending?: boolean; + search?: string; + minAge?: number; + maxAge?: number; + }>({ + url: "https://api.example.com/users", params: (args) => ({ sortBy: args?.sortBy, sortByDescending: args?.sortByDescending, search: args?.search, maxAge: args?.maxAge }) - }, - responseMapping: { - DTO: [UserDTO], - Model: [UserModel] - } - }); + }); /* -------------------------------------------------------------------------- */ /* ---------------------------- Setup mock logic ---------------------------- */ @@ -82,7 +79,7 @@ describe("Setup of endpoint with intercepting middleware", () => { /* ------------------------------ Test function ----------------------------- */ - const allUsers = await getUsers(); + const allUsers = await getUsers({}); expect(allUsers.model.length).toEqual(10); const searchedUsers = await getUsers({ search: "Bob" }); diff --git a/packages/core/tests/integration/mock-mutation.test.ts b/packages/core/tests/integration/mock-mutation.test.ts index eecec92..5e2c8ab 100644 --- a/packages/core/tests/integration/mock-mutation.test.ts +++ b/packages/core/tests/integration/mock-mutation.test.ts @@ -41,20 +41,11 @@ describe("setup of mutation endpoint", () => { /* ---------------------------- Define API method --------------------------- */ - const createPost = acquire({ - request: { - url: "https://www.example.com/users", - method: "POST" - }, - requestMapping: { - DTO: CreatePostDTO, - Model: CreatePostModel - }, - responseMapping: { - DTO: PostDTO, - Model: PostModel - } - }); + const createPost = acquire + .createRequestHandler() + .withRequestMapping(CreatePostModel, CreatePostDTO) + .withResponseMapping(PostModel, PostDTO) + .post({ url: "https://api.example.com/users" }); /* -------------------------------------------------------------------------- */ @@ -75,17 +66,17 @@ describe("setup of mutation endpoint", () => { /* ------------------------------ Test function ----------------------------- */ - const response = await createPost({ + const { response, model } = await createPost({ data: { title: "My title", text: "My text" } }); - expect(isPlainObject(response.response.data)); - expect(response.model).toBeInstanceOf(PostModel); - expect(response.model.title).toEqual("My title"); - expect(response.model.text).toEqual("My text"); - expect(response.model.id).toEqual(16); + expect(isPlainObject(response.data)); + expect(model).toBeInstanceOf(PostModel); + expect(model.title).toEqual("My title"); + expect(model.text).toEqual("My text"); + expect(model.id).toEqual(16); }); }); diff --git a/packages/core/tests/integration/mock-relations.test.ts b/packages/core/tests/integration/mock-relations.test.ts index 09f5845..c7dd67b 100644 --- a/packages/core/tests/integration/mock-relations.test.ts +++ b/packages/core/tests/integration/mock-relations.test.ts @@ -5,6 +5,7 @@ import MockID from "@/decorators/mocks/MockID.decorator"; import MockRelationID from "@/decorators/mocks/MockRelationID.decorator"; import MockRelationProperty from "@/decorators/mocks/MockRelationProperty.decorator"; import { Chance } from "chance"; +import { Expose } from "class-transformer"; describe("setup of endpoint with mocked relations", () => { const chance = new Chance(); @@ -23,18 +24,24 @@ describe("setup of endpoint with mocked relations", () => { @MockRelationProperty(() => UserDTO, "name") userName: string; } + class PostModel { + @Expose() id: number; + @Expose() title: string; + @Expose() text: string; + @Expose() userId: number; + @Expose() userName: string; + } + const mockCache = new AcquireMockCache(); const acquire = new Acquire().useMockCache(mockCache).enableMocking(); - const getPosts = acquire.withCallArgs<{ createdByUserId?: number }>()({ - request: { - url: "https://www.example.com/users", + const getPosts = acquire + .createRequestHandler() + .withResponseMapping([PostModel], [PostDTO]) + .get<{ createdByUserId?: number }>({ + url: "https://api.example.com/posts", params: (args) => args - }, - responseMapping: { - DTO: [PostDTO] - } - }); + }); beforeEach(() => { mockCache.clear(); @@ -42,7 +49,7 @@ describe("setup of endpoint with mocked relations", () => { }); it("should throw an error if no data exists in the cache", () => { - expect(getPosts()).rejects.toThrow( + expect(getPosts({})).rejects.toThrow( "Attempted to instantiate mock data for class 'PostDTO' with relation to class 'UserDTO', but no instances of class 'UserDTO' exists in the mock cache. Consider pre-populating the cache with instances of class 'UserDTO' by calling 'mockCache.fill(UserDTO, 10)'." ); }); @@ -51,14 +58,14 @@ describe("setup of endpoint with mocked relations", () => { await mockCache.fill(UserDTO, 10); await mockCache.fill(PostDTO, 50); - const posts = await getPosts.mock({ $count: 50 }); + const posts = await getPosts.mock({}, 50); const users = mockCache.get(UserDTO); const userIds = users.map((user) => user.id); expect(posts.dto.length).toEqual(50); posts.dto.forEach((post) => { - expect(userIds.includes(post.userId)).toBe(true); + expect(userIds).toContainEqual(post.userId); }); }); diff --git a/packages/core/tests/middleware/AcquireRequestLogger.test.ts b/packages/core/tests/middleware/RequestLogger.test.ts similarity index 77% rename from packages/core/tests/middleware/AcquireRequestLogger.test.ts rename to packages/core/tests/middleware/RequestLogger.test.ts index c0164e6..eeefad9 100644 --- a/packages/core/tests/middleware/AcquireRequestLogger.test.ts +++ b/packages/core/tests/middleware/RequestLogger.test.ts @@ -1,9 +1,10 @@ -import { Acquire, AcquireLogger } from "@/index"; -import AcquireRequestLogger from "@/middleware/AcquireRequestLogger"; +import { Acquire } from "@/classes/Acquire.class"; +import AcquireLogger from "@/classes/AcquireLogger.class"; +import RequestLogger from "@/middleware/RequestLogger.middleware"; import axios, { AxiosError } from "axios"; import MockAdapter from "axios-mock-adapter"; -describe("middleware: AcquireRequestLogger", () => { +describe("middleware: RequestLogger", () => { const { blue, green, red, cyan, yellow, brightBlack, brightBlue, reset } = AcquireLogger.color; const axiosInstance = axios.create(); @@ -12,7 +13,7 @@ describe("middleware: AcquireRequestLogger", () => { const consoleSpy = jest.spyOn(console, "log").mockImplementation(() => {}); const acquire = new Acquire(axiosInstance); acquire.use( - new AcquireRequestLogger({ + new RequestLogger({ timezone: "UTC" }) ); @@ -45,12 +46,10 @@ describe("middleware: AcquireRequestLogger", () => { }); it("should log executed requests", async () => { - mockAxios.onGet("http://example.com/users").reply(200, {}); + mockAxios.onGet("http://api.example.com/users").reply(200, {}); - const getUsers = acquire({ - request: { - url: "http://example.com/users" - } + const getUsers = acquire.createRequestHandler().get({ + url: "http://api.example.com/users" }); await getUsers(); @@ -61,12 +60,10 @@ describe("middleware: AcquireRequestLogger", () => { }); it("should log executed requests with errors", async () => { - mockAxios.onGet("http://example.com/users").reply(404, {}); + mockAxios.onGet("http://api.example.com/users").reply(404, {}); - const getUsers = acquire({ - request: { - url: "http://example.com/users" - } + const getUsers = acquire.createRequestHandler().get({ + url: "http://api.example.com/users" }); await expect(getUsers()).rejects.toThrow(); @@ -77,10 +74,8 @@ describe("middleware: AcquireRequestLogger", () => { }); it("should log mocked requests", async () => { - const getUsers = acquire({ - request: { - url: "http://example.com/users" - } + const getUsers = acquire.createRequestHandler().get({ + url: "http:/api.example.com/users" }); await getUsers.mock(); @@ -91,10 +86,8 @@ describe("middleware: AcquireRequestLogger", () => { }); it("should log mocked requests with errors", async () => { - const getUsers = acquire({ - request: { - url: "http://example.com/users" - } + const getUsers = acquire.createRequestHandler().get({ + url: "http://api.example.com/users" }); getUsers.use((context) => {