Skip to content

Commit

Permalink
Merge pull request #153 from hey-api/feat/middleware
Browse files Browse the repository at this point in the history
feat(api): add support for interceptors
  • Loading branch information
mrlubos authored Mar 27, 2024
2 parents 052bc6c + c455659 commit aa47fd8
Show file tree
Hide file tree
Showing 25 changed files with 346 additions and 104 deletions.
5 changes: 5 additions & 0 deletions .changeset/beige-impalas-collect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hey-api/openapi-ts": minor
---

add support for interceptors
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- [Linting](#linting)
- [Enums](#enums)
- [Config API](#config-api)
- [Interceptors](#interceptors)
- [Migrating](#migrating)
- [Contributing](#contributing)

Expand Down Expand Up @@ -193,6 +194,30 @@ $ openapi-ts --help
-h, --help display help for command
```
## Interceptors
Interceptors (middleware) can be used to modify requests before they're sent or responses before they're returned to the rest of your application. Below is an example request interceptor
```ts
OpenAPI.interceptors.request.use((request) => {
doSomethingWithRequest(request)
return request // <-- must return request
})
```
and an example response interceptor
```ts
OpenAPI.interceptors.response.use(async (response) => {
await doSomethingWithResponse(response) // async
return response // <-- must return response
})
```
If you need to remove an interceptor, pass the same function to `OpenAPI.interceptors.request.eject()` or `OpenAPI.interceptors.response.eject()`.
> ⚠️ Angular client does not currently support request interceptors and async response interceptors.
## Migrating
While we try to avoid breaking changes, sometimes it's unavoidable in order to offer you the latest features.
Expand Down
5 changes: 5 additions & 0 deletions src/templates/client.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { OpenAPI } from './core/OpenAPI';
{{else}}
import type { BaseHttpRequest } from './core/BaseHttpRequest';
import type { OpenAPIConfig } from './core/OpenAPI';
import { Interceptors } from './core/OpenAPI';
import { {{{httpRequest}}} } from './core/{{{httpRequest}}}';
{{/equals}}

Expand Down Expand Up @@ -68,6 +69,10 @@ export class {{{@root.$config.name}}} {
PASSWORD: config?.PASSWORD,
HEADERS: config?.HEADERS,
ENCODE_PATH: config?.ENCODE_PATH,
interceptors: {
request: new Interceptors(),
response: new Interceptors(),
},
});

{{#each services}}
Expand Down
78 changes: 77 additions & 1 deletion src/templates/core/OpenAPI.hbs
Original file line number Diff line number Diff line change
@@ -1,8 +1,53 @@
{{~#equals @root.$config.client 'angular'~}}
import type { HttpResponse } from '@angular/common/http';
{{~/equals~}}
{{~#equals @root.$config.client 'axios'~}}
import type { AxiosRequestConfig, AxiosResponse } from 'axios';
{{~/equals~}}
{{~#equals @root.$config.client 'node'~}}
import type { RequestInit, Response } from 'node-fetch';
{{~/equals~}}

import type { ApiRequestOptions } from './ApiRequestOptions';
{{#if @root.$config.useOptions}}
{{#equals @root.$config.serviceResponse 'generics'}}
import type { TConfig, TResult } from './types';
{{else}}
import type { TResult } from './types';
{{/equals}}
{{else}}
import type { TResult } from './types';
{{/if}}

type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
type Headers = Record<string, string>;
{{#equals @root.$config.client 'angular'}}
type Middleware<T> = (value: T) => T;
{{else}}
type Middleware<T> = (value: T) => T | Promise<T>;
{{/equals}}
type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;

export class Interceptors<T> {
_fns: Middleware<T>[];

constructor() {
this._fns = [];
}

eject(fn: Middleware<T>) {
const index = this._fns.indexOf(fn);
if (index !== -1) {
this._fns = [
...this._fns.slice(0, index),
...this._fns.slice(index + 1),
];
}
}

use(fn: Middleware<T>) {
this._fns = [...this._fns, fn];
}
}

export type OpenAPIConfig = {
BASE: string;
Expand All @@ -15,6 +60,27 @@ export type OpenAPIConfig = {
USERNAME?: string | Resolver<string> | undefined;
VERSION: string;
WITH_CREDENTIALS: boolean;
interceptors: {
{{~#equals @root.$config.client 'angular'~}}
response: Interceptors<HttpResponse<any>>;
{{~/equals~}}
{{~#equals @root.$config.client 'axios'~}}
request: Interceptors<AxiosRequestConfig>;
response: Interceptors<AxiosResponse>;
{{~/equals~}}
{{~#equals @root.$config.client 'fetch'~}}
request: Interceptors<RequestInit>;
response: Interceptors<Response>;
{{~/equals~}}
{{~#equals @root.$config.client 'node'~}}
request: Interceptors<RequestInit>;
response: Interceptors<Response>;
{{~/equals~}}
{{~#equals @root.$config.client 'xhr'~}}
request: Interceptors<XMLHttpRequest>;
response: Interceptors<XMLHttpRequest>;
{{~/equals~}}
};
};

export const OpenAPI: OpenAPIConfig = {
Expand All @@ -28,8 +94,16 @@ export const OpenAPI: OpenAPIConfig = {
USERNAME: undefined,
VERSION: '{{{version}}}',
WITH_CREDENTIALS: false,
interceptors: {
{{~#notEquals @root.$config.client 'angular'~}}
request: new Interceptors(),
{{~/notEquals~}}
response: new Interceptors(),
},
};

{{~#if @root.$config.useOptions~}}
{{~#equals @root.$config.serviceResponse 'generics'~}}
export const mergeOpenApiConfig = <T extends TResult>(config: OpenAPIConfig, overrides: TConfig<T>) => {
const merged = { ...config };
Object.entries(overrides)
Expand All @@ -43,3 +117,5 @@ export const mergeOpenApiConfig = <T extends TResult>(config: OpenAPIConfig, ove
});
return merged;
};
{{~/equals~}}
{{~/if~}}
3 changes: 3 additions & 0 deletions src/templates/core/angular/request.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ export const request = <T>(config: OpenAPIConfig, http: HttpClient, options: Api
return sendRequest<T>(config, options, http, url, body, formData, headers);
}),
map(response => {
for (const fn of config.interceptors.response._fns) {
response = fn(response)
}
const responseBody = getResponseBody(response);
const responseHeader = getResponseHeader(response, options.responseHeader);
return {
Expand Down
7 changes: 6 additions & 1 deletion src/templates/core/axios/request.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,12 @@ export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions, ax
const headers = await getHeaders(config, options, formData);

if (!onCancel.isCancelled) {
const response = await sendRequest<T>(config, options, url, body, formData, headers, onCancel, axiosClient);
let response = await sendRequest<T>(config, options, url, body, formData, headers, onCancel, axiosClient);

for (const fn of config.interceptors.response._fns) {
response = await fn(response)
}

const responseBody = getResponseBody(response);
const responseHeader = getResponseHeader(response, options.responseHeader);

Expand Down
12 changes: 8 additions & 4 deletions src/templates/core/axios/sendRequest.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,21 @@ export const sendRequest = async <T>(
): Promise<AxiosResponse<T>> => {
const controller = new AbortController();

const requestConfig: AxiosRequestConfig = {
url,
headers,
let requestConfig: AxiosRequestConfig = {
data: body ?? formData,
headers,
method: options.method,
withCredentials: config.WITH_CREDENTIALS,
signal: controller.signal,
url,
withCredentials: config.WITH_CREDENTIALS,
};

onCancel(() => controller.abort());

for (const fn of config.interceptors.request._fns) {
requestConfig = await fn(requestConfig)
}

try {
return await axiosClient.request(requestConfig);
} catch (error) {
Expand Down
7 changes: 6 additions & 1 deletion src/templates/core/fetch/request.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,12 @@ export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions): C
const headers = await getHeaders(config, options);

if (!onCancel.isCancelled) {
const response = await sendRequest(config, options, url, body, formData, headers, onCancel);
let response = await sendRequest(config, options, url, body, formData, headers, onCancel);

for (const fn of config.interceptors.response._fns) {
response = await fn(response)
}

const responseBody = await getResponseBody(response);
const responseHeader = getResponseHeader(response, options.responseHeader);

Expand Down
6 changes: 5 additions & 1 deletion src/templates/core/fetch/sendRequest.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const sendRequest = async (
): Promise<Response> => {
const controller = new AbortController();

const request: RequestInit = {
let request: RequestInit = {
headers,
body: body ?? formData,
method: options.method,
Expand All @@ -20,6 +20,10 @@ export const sendRequest = async (
request.credentials = config.CREDENTIALS;
}

for (const fn of config.interceptors.request._fns) {
request = await fn(request)
}

onCancel(() => controller.abort());

return await fetch(url, request);
Expand Down
7 changes: 6 additions & 1 deletion src/templates/core/node/request.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,12 @@ export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions): C
const headers = await getHeaders(config, options);

if (!onCancel.isCancelled) {
const response = await sendRequest(options, url, body, formData, headers, onCancel);
let response = await sendRequest(config, options, url, body, formData, headers, onCancel);

for (const fn of config.interceptors.response._fns) {
response = await fn(response)
}

const responseBody = await getResponseBody(response);
const responseHeader = getResponseHeader(response, options.responseHeader);

Expand Down
7 changes: 6 additions & 1 deletion src/templates/core/node/sendRequest.hbs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export const sendRequest = async (
config: OpenAPIConfig,
options: ApiRequestOptions,
url: string,
body: any,
Expand All @@ -8,13 +9,17 @@ export const sendRequest = async (
): Promise<Response> => {
const controller = new AbortController();

const request: RequestInit = {
let request: RequestInit = {
headers,
method: options.method,
body: body ?? formData,
signal: controller.signal,
};

for (const fn of config.interceptors.request._fns) {
request = await fn(request)
}

onCancel(() => controller.abort());

return await fetch(url, request);
Expand Down
7 changes: 6 additions & 1 deletion src/templates/core/xhr/request.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,12 @@ export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions): C
const headers = await getHeaders(config, options);

if (!onCancel.isCancelled) {
const response = await sendRequest(config, options, url, body, formData, headers, onCancel);
let response = await sendRequest(config, options, url, body, formData, headers, onCancel);

for (const fn of config.interceptors.response._fns) {
response = await fn(response)
}

const responseBody = getResponseBody(response);
const responseHeader = getResponseHeader(response, options.responseHeader);

Expand Down
9 changes: 7 additions & 2 deletions src/templates/core/xhr/sendRequest.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,23 @@ export const sendRequest = async (
headers: Headers,
onCancel: OnCancel
): Promise<XMLHttpRequest> => {
const xhr = new XMLHttpRequest();
let xhr = new XMLHttpRequest();
xhr.open(options.method, url, true);
xhr.withCredentials = config.WITH_CREDENTIALS;

headers.forEach((value, key) => {
xhr.setRequestHeader(key, value);
});

return new Promise<XMLHttpRequest>((resolve, reject) => {
return new Promise<XMLHttpRequest>(async (resolve, reject) => {
xhr.onload = () => resolve(xhr);
xhr.onabort = () => reject(new Error('Request aborted'));
xhr.onerror = () => reject(new Error('Network error'));

for (const fn of config.interceptors.request._fns) {
xhr = await fn(xhr)
}

xhr.send(body ?? formData);

onCancel(() => xhr.abort());
Expand Down
40 changes: 24 additions & 16 deletions test/__snapshots__/v2/test/generated/v2/core/OpenAPI.ts.snap
Original file line number Diff line number Diff line change
@@ -1,8 +1,28 @@
import type { ApiRequestOptions } from './ApiRequestOptions';
import type { TConfig, TResult } from './types';
import type { TResult } from './types';

type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
type Headers = Record<string, string>;
type Middleware<T> = (value: T) => T | Promise<T>;
type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;

export class Interceptors<T> {
_fns: Middleware<T>[];

constructor() {
this._fns = [];
}

eject(fn: Middleware<T>) {
const index = this._fns.indexOf(fn);
if (index !== -1) {
this._fns = [...this._fns.slice(0, index), ...this._fns.slice(index + 1)];
}
}

use(fn: Middleware<T>) {
this._fns = [...this._fns, fn];
}
}

export type OpenAPIConfig = {
BASE: string;
Expand All @@ -15,6 +35,7 @@ export type OpenAPIConfig = {
USERNAME?: string | Resolver<string> | undefined;
VERSION: string;
WITH_CREDENTIALS: boolean;
interceptors: { request: Interceptors<RequestInit>; response: Interceptors<Response> };
};

export const OpenAPI: OpenAPIConfig = {
Expand All @@ -28,18 +49,5 @@ export const OpenAPI: OpenAPIConfig = {
USERNAME: undefined,
VERSION: '1.0',
WITH_CREDENTIALS: false,
};

export const mergeOpenApiConfig = <T extends TResult>(config: OpenAPIConfig, overrides: TConfig<T>) => {
const merged = { ...config };
Object.entries(overrides)
.filter(([key]) => key.startsWith('_'))
.forEach(([key, value]) => {
const k = key.slice(1).toLocaleUpperCase() as keyof typeof merged;
if (merged.hasOwnProperty(k)) {
// @ts-ignore
merged[k] = value;
}
});
return merged;
interceptors: { request: new Interceptors(), response: new Interceptors() },
};
Loading

0 comments on commit aa47fd8

Please sign in to comment.