Skip to content

Commit

Permalink
feat(clients): add node-experimental client
Browse files Browse the repository at this point in the history
  • Loading branch information
jordanshatford committed Mar 22, 2024
1 parent 60f4d4b commit a2a4b5c
Show file tree
Hide file tree
Showing 11 changed files with 498 additions and 2 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- [Quick Start](#quick-start)
- [Installation](#installation)
- [Configuration](#configuration)
- [Clients](#clients)
- [Formatting](#formatting)
- [Linting](#linting)
- [Enums](#enums)
Expand Down Expand Up @@ -96,6 +97,18 @@ export default {

Alternatively, you can use `openapi-ts.config.js` and configure the export statement depending on your project setup.

### Clients

We provide a variety of possible clients to use when generating your `openapi-ts` client. The following are available:

- `angular`: An [Angular](https://angular.io/) client using [RxJS](https://rxjs.dev/).
- `axios`: An [Axios](https://axios-http.com/docs/intro) client.
- `fetch`: A [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) client.
- `node`: A [Node.js](https://nodejs.org/) client using [node-fetch](https://www.npmjs.com/package/node-fetch) client.
- `node-experimental`: A [Node.js](https://nodejs.org/) client using [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API).
> NOTE: [Experimental until Node.js v21](https://nodejs.org/docs/latest-v21.x/api/globals.html#fetch)
- `xhr`: A [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) client.

### Formatting

By default, `openapi-ts` will automatically format your client according to your project configuration. To disable automatic formatting, set `format` to false
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ const logClientMessage = (client: Config['client']) => {
return console.log('✨ Creating Fetch client');
case 'node':
return console.log('✨ Creating Node.js client');
case 'node-experimental':
return console.log('✨ Creating Node.js (experimental) client');
case 'xhr':
return console.log('✨ Creating XHR client');
}
Expand Down
1 change: 1 addition & 0 deletions src/templates/core/request.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
{{~#equals @root.$config.client 'axios'}}{{>axios/request}}{{/equals~}}
{{~#equals @root.$config.client 'fetch'}}{{>fetch/request}}{{/equals~}}
{{~#equals @root.$config.client 'node'}}{{>node/request}}{{/equals~}}
{{~#equals @root.$config.client 'node-experimental'}}{{>fetch/request}}{{/equals~}}
{{~#equals @root.$config.client 'xhr'}}{{>xhr/request}}{{/equals~}}
1 change: 1 addition & 0 deletions src/templates/partials/base.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
{{~#equals @root.$config.client 'axios'}}Blob{{/equals~}}
{{~#equals @root.$config.client 'angular'}}Blob{{/equals~}}
{{~#equals @root.$config.client 'node'}}Blob{{/equals~}}
{{~#equals @root.$config.client 'node-experimental'}}Blob{{/equals~}}
{{~else~}}
{{~#useDateType @root.$config format~}}
Date
Expand Down
2 changes: 1 addition & 1 deletion src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export interface UserConfig {
* The selected HTTP client (fetch, xhr, node or axios)
* @default 'fetch'
*/
client?: 'angular' | 'axios' | 'fetch' | 'node' | 'xhr';
client?: 'angular' | 'axios' | 'fetch' | 'node' | 'node-experimental' | 'xhr';
/**
* Generate JavaScript objects from enum definitions?
* @default false
Expand Down
1 change: 1 addition & 0 deletions src/utils/getHttpRequestName.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const getHttpRequestName = (client: Config['client']): string => {
case 'fetch':
return 'FetchHttpRequest';
case 'node':
case 'node-experimental':
return 'NodeHttpRequest';
case 'xhr':
return 'XHRHttpRequest';
Expand Down
16 changes: 16 additions & 0 deletions test/bin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ describe('bin', () => {
expect(result.stdout.toString()).toContain('');
expect(result.stderr.toString()).toBe('');
});

it('generates node client', async () => {
const result = sync('node', [
'./bin/index.js',
Expand All @@ -75,6 +76,21 @@ describe('bin', () => {
expect(result.stderr.toString()).toBe('');
});

it('generates node experimental client', async () => {
const result = sync('node', [
'./bin/index.js',
'--input',
'./test/spec/v3.json',
'--output',
'./test/generated/bin',
'--client',
'node-experimental',
'--no-write',
]);
expect(result.stdout.toString()).toContain('');
expect(result.stderr.toString()).toBe('');
});

it('generates xhr client', async () => {
const result = sync('node', [
'./bin/index.js',
Expand Down
155 changes: 155 additions & 0 deletions test/e2e/client.node-experimental.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';

import { cleanup } from './scripts/cleanup';
import { compileWithTypescript } from './scripts/compileWithTypescript';
import { generateClient } from './scripts/generateClient';
import server from './scripts/server';

describe('client.node-experimental', () => {
beforeAll(async () => {
cleanup('client/node-experimental');
await generateClient('client/node-experimental', 'v3', 'node-experimental', false, 'ApiClient');
compileWithTypescript('client/node-experimental');
await server.start('client/node-experimental');
}, 30000);

afterAll(async () => {
await server.stop();
});

it('requests token', async () => {
const { ApiClient } = await import('./generated/client/node-experimental/index.js');
const tokenRequest = vi.fn().mockResolvedValue('MY_TOKEN');
const client = new ApiClient({
TOKEN: tokenRequest,
USERNAME: undefined,
PASSWORD: undefined,
});
const result = await client.simple.getCallWithoutParametersAndResponse();
expect(tokenRequest.mock.calls.length).toBe(1);
// @ts-ignore
expect(result.headers.authorization).toBe('Bearer MY_TOKEN');
});

it('uses credentials', async () => {
const { ApiClient } = await import('./generated/client/node-experimental/index.js');
const client = new ApiClient({
TOKEN: undefined,
USERNAME: 'username',
PASSWORD: 'password',
});
const result = await client.simple.getCallWithoutParametersAndResponse();
// @ts-ignore
expect(result.headers.authorization).toBe('Basic dXNlcm5hbWU6cGFzc3dvcmQ=');
});

it('supports complex params', async () => {
const { ApiClient } = await import('./generated/client/node-experimental/index.js');
const client = new ApiClient();
// @ts-ignore
const result = await client.complex.complexTypes({
first: {
second: {
third: 'Hello World!',
},
},
});
expect(result).toBeDefined();
});

it('support form data', async () => {
const { ApiClient } = await import('./generated/client/node-experimental/index.js');
const client = new ApiClient();
// @ts-ignore
const result = await client.parameters.callWithParameters(
'valueHeader',
'valueQuery',
'valueForm',
'valueCookie',
'valuePath',
{
prop: 'valueBody',
}
);
expect(result).toBeDefined();
});

it('can abort the request', async () => {
let error;
try {
const { ApiClient } = await import('./generated/client/node-experimental/index.js');
const client = new ApiClient();
const promise = client.simple.getCallWithoutParametersAndResponse();
setTimeout(() => {
promise.cancel();
}, 10);
await promise;
} catch (e) {
error = (e as Error).message;
}
expect(error).toContain('Request aborted');
});

it('should throw known error (500)', async () => {
let error;
try {
const { ApiClient } = await import('./generated/client/node-experimental/index.js');
const client = new ApiClient();
await client.error.testErrorCode(500);
} catch (err) {
error = JSON.stringify({
name: err.name,
message: err.message,
url: err.url,
status: err.status,
statusText: err.statusText,
body: err.body,
});
}
expect(error).toBe(
JSON.stringify({
name: 'ApiError',
message: 'Custom message: Internal Server Error',
url: 'http://localhost:3000/base/api/v1.0/error?status=500',
status: 500,
statusText: 'Internal Server Error',
body: {
status: 500,
message: 'hello world',
},
})
);
});

it('should throw unknown error (409)', async () => {
let error;
try {
const { ApiClient } = await import('./generated/client/node-experimental/index.js');
const client = new ApiClient();
await client.error.testErrorCode(409);
} catch (err) {
error = JSON.stringify({
name: err.name,
message: err.message,
url: err.url,
status: err.status,
statusText: err.statusText,
body: err.body,
});
}
expect(error).toBe(
JSON.stringify({
name: 'ApiError',
message:
'Generic Error: status: 409; status text: Conflict; body: {\n "status": 409,\n "message": "hello world"\n}',
url: 'http://localhost:3000/base/api/v1.0/error?status=409',
status: 409,
statusText: 'Conflict',
body: {
status: 409,
message: 'hello world',
},
})
);
});
});
3 changes: 2 additions & 1 deletion test/e2e/scripts/generateClient.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { createClient } from '../../../';
import type { Config } from '../../../src/types/config';

export const generateClient = async (
dir: string,
version: string,
client: 'fetch' | 'xhr' | 'node' | 'axios' | 'angular',
client: Config['client'],
useOptions: boolean = false,
name?: string
) => {
Expand Down
98 changes: 98 additions & 0 deletions test/e2e/v2.node-experimental.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';

import { cleanup } from './scripts/cleanup';
import { compileWithTypescript } from './scripts/compileWithTypescript';
import { generateClient } from './scripts/generateClient';
import server from './scripts/server';

describe('v2.node-experimental', () => {
beforeAll(async () => {
cleanup('v2/node-experimental');
await generateClient('v2/node-experimental', 'v2', 'node-experimental');
compileWithTypescript('v2/node-experimental');
await server.start('v2/node-experimental');
}, 30000);

afterAll(async () => {
await server.stop();
});

it('requests token', async () => {
const { OpenAPI, SimpleService } = await import('./generated/v2/node-experimental/index.js');
const tokenRequest = vi.fn().mockResolvedValue('MY_TOKEN');
OpenAPI.TOKEN = tokenRequest;
const result = await SimpleService.getCallWithoutParametersAndResponse();
expect(tokenRequest.mock.calls.length).toBe(1);
// @ts-ignore
expect(result.headers.authorization).toBe('Bearer MY_TOKEN');
});

it('supports complex params', async () => {
const { ComplexService } = await import('./generated/v2/node-experimental/index.js');
const result = await ComplexService.complexTypes({
// @ts-ignore
first: {
second: {
third: 'Hello World!',
},
},
});
expect(result).toBeDefined();
});

it('can abort the request', async () => {
let error;
try {
const { SimpleService } = await import('./generated/v2/node-experimental/index.js');
const promise = SimpleService.getCallWithoutParametersAndResponse();
setTimeout(() => {
promise.cancel();
}, 10);
await promise;
} catch (e) {
error = (e as Error).message;
}
expect(error).toContain('Request aborted');
});
});

describe('v2.node-experimental useOptions', () => {
beforeAll(async () => {
cleanup('v2/node-experimental');
await generateClient('v2/node-experimental', 'v2', 'node-experimental', true);
compileWithTypescript('v2/node-experimental');
await server.start('v2/node-experimental');
}, 30000);

afterAll(async () => {
await server.stop();
});

it('returns result body by default', async () => {
const { SimpleService } = await import('./generated/v2/node-experimental/index.js');
const result = await SimpleService.getCallWithoutParametersAndResponse();
// @ts-ignore
expect(result.body).toBeUndefined();
});

it('returns result body', async () => {
const { SimpleService } = await import('./generated/v2/node-experimental/index.js');
// @ts-ignore
const result = await SimpleService.getCallWithoutParametersAndResponse({
_result: 'body',
});
// @ts-ignore
expect(result.body).toBeUndefined();
});

it('returns raw result', async ({ skip }) => {
skip();
const { SimpleService } = await import('./generated/v2/node-experimental/index.js');
// @ts-ignore
const result = await SimpleService.getCallWithoutParametersAndResponse({
_result: 'raw',
});
// @ts-ignore
expect(result.body).toBeDefined();
});
});
Loading

0 comments on commit a2a4b5c

Please sign in to comment.