Skip to content

Commit a0e03b1

Browse files
Merge pull request #73 from aligent/fix/MI-275-nx-openapi-improvement
MI-275: Added couple of improvements to nx-openapi generator
2 parents 421f0e7 + 94d0e5a commit a0e03b1

8 files changed

Lines changed: 110 additions & 34 deletions

File tree

packages/nx-openapi/src/generators/client/client-specific-files/README.md.template

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 50 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,53 @@
1-
// The following is an example file generated to demonstrate how to use your newly generated types
2-
import { paths } from './types';
3-
import createClient from 'openapi-fetch';
1+
/**
2+
* Example client demonstrating how to use the generated API types.
3+
* Adjust middlewares usage according to your need.
4+
*
5+
* - Check out Aligent's middlewares: https://github.com/aligent/microservice-development-utilities
6+
* - For more information about openapi-fetch middlewares: https://openapi-ts.dev/openapi-fetch/middleware-auth#middleware
7+
*/
8+
import {
9+
apiKeyAuthMiddleware,
10+
fetchSsmParams,
11+
retryMiddleware,
12+
} from '@aligent/microservice-util-lib';
13+
import createClient, { Client, ClientOptions } from 'openapi-fetch';
14+
import { paths } from './generated-types';
415

5-
// This type is an example of what is initialised using the types generated in the paths interface which is created when generation occurs.
6-
// Its worth looking into that paths interface, to see the the types that were generated for your client.
7-
type ExampleResponse =
8-
paths['/customers']['get']['responses']['200']['content']['application/json'];
16+
export class <%= className %> {
17+
private credential: string | null = null;
18+
public readonly client: Client<paths, `${string}/${string}`>;
919

10-
// Using openapi-fetch we can create a fully typed REST client by passing in paths as a generic.
11-
// If you wish however, you can use any api client you want (axios, basic fetch etc.) and use the paths separately to maintain type safety in your client.
12-
// Keep this variable exported. It will need to be to make sure all other services in your application can access and use the client.
13-
export const client = createClient<paths>({
14-
baseUrl: '',
15-
signal: AbortSignal.timeout(10000),
16-
});
20+
constructor(options: ClientOptions, credentialPath: string) {
21+
this.client = createClient<paths>(options);
1722

18-
// Client getters are then fully typed. Try deleting '/customers' and seeing what routes you can use!
19-
const response = client.GET('/customers', {
20-
params: {
21-
query: {},
22-
},
23-
});
23+
/**
24+
* The order in which middleware are registered matters.
25+
* - For requests, onRequest() will be called in the order registered
26+
* - For responses, onResponse() will be called in reverse order.
27+
*/
28+
this.client.use(
29+
apiKeyAuthMiddleware({
30+
header: 'Authorization',
31+
value: async () => {
32+
if (!this.credential) {
33+
const param = await fetchSsmParams(credentialPath);
34+
if (!param?.Value) {
35+
throw new Error('Unable to fetch API client credential');
36+
}
37+
38+
this.credential = param.Value;
39+
}
40+
41+
return `Bearer ${this.credential}`;
42+
},
43+
})
44+
);
45+
46+
this.client.use(
47+
retryMiddleware({
48+
onRetry: ({ attempt, error }) =>
49+
console.log(`Retrying...${attempt} due to ${String(error)}`),
50+
})
51+
);
52+
}
53+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Generated OpenAPI REST API Clients
2+
3+
This folder contains TypeScript API clients generated from OpenAPI specifications using Nx generators and `openapi-typescript`. Each client exposes strongly typed request and response types under `generated-types` and a ready-to-use `openapi-fetch` client class.
4+
5+
## Usage
6+
- Import the generated client into your code.
7+
- Instantiate the client with appropriate ClientOptions (base URL, fetch implementation, etc.) and call the typed endpoints.
8+
9+
## Example
10+
```typescript
11+
import { MyApiClient } from '@clients'; // Adjust the import name as necessary
12+
13+
const client = new MyApiClient(
14+
{ baseUrl: 'https://my-api-client.base-url', signal: AbortSignal.timeout(30000) },
15+
'/my/api/client/access-token/path'
16+
).client;
17+
18+
// Example of calling a typed endpoint
19+
async function fetchData() {
20+
try {
21+
const response = await client.GET('/path/to/endpoint'); // Replace with actual endpoint method
22+
console.log(response);
23+
} catch (error) {
24+
console.error('Error fetching data:', error);
25+
}
26+
}
27+
28+
fetchData();
29+
```
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import baseConfig from '../eslint.config.mjs';
2+
3+
export default [...baseConfig];

packages/nx-openapi/src/generators/client/generator.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
addTsConfigPath,
99
appendToIndexFile,
1010
attemptToAddProjectConfiguration,
11+
toClassName,
1112
} from '../../helpers/utilities';
1213
import { ClientGeneratorSchema } from './schema';
1314

@@ -45,28 +46,26 @@ export async function clientGenerator(tree: Tree, options: ClientGeneratorSchema
4546
await generateOpenApiTypes(tree, schemaDest, typesDest);
4647

4748
if (isNewProject) {
48-
logger.info('No clients currently exist. Generating a clients folder...');
4949
logger.info(`Creating new project at ${projectRoot}`);
5050

51-
// Generate other files
5251
generateFiles(tree, joinPathFragments(__dirname, './files'), projectRoot, options);
53-
54-
// Add the project to the tsconfig paths so it can be imported by namespace
5552
addTsConfigPath(tree, importPath, [joinPathFragments(projectRoot, './src', 'index.ts')]);
5653
}
5754

5855
// Generate the files for the specific new client
59-
generateFiles(
60-
tree,
61-
joinPathFragments(__dirname, './client-specific-files'),
62-
apiClientDest,
63-
options
64-
);
56+
generateFiles(tree, joinPathFragments(__dirname, './client-specific-files'), apiClientDest, {
57+
className: toClassName(name),
58+
});
6559

6660
// Append to index file for imports
6761
appendToIndexFile(tree, projectRoot, name);
6862

6963
await formatFiles(tree);
64+
65+
logger.info(`Successfully generated ${name} API client`);
66+
logger.info(
67+
`Next step: Run "nx affected -t lint" to fix any linting issues that may arise from the generated code.`
68+
);
7069
}
7170

7271
export default clientGenerator;

packages/nx-openapi/src/generators/client/schema.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"properties": {
77
"name": {
88
"type": "string",
9+
"pattern": "^[a-z0-9-]+$",
910
"description": "Name of the api client.",
1011
"$default": {
1112
"$source": "argv",

packages/nx-openapi/src/helpers/generate-openapi-types.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ export async function copySchema(
8282
export async function validateSchema(path: string): Promise<boolean> {
8383
let hasError = false;
8484
try {
85-
// TODO: MI-203 - Support private schema endpoint
8685
const config = await loadConfig();
8786
const results = await lint({ ref: path, config });
8887

packages/nx-openapi/src/helpers/utilities.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { addProjectConfiguration, Tree, updateJson } from '@nx/devkit';
99
* @param {Tree} tree - The file system tree representing the current project.
1010
* @param {string} name - The name of the project to add.
1111
* @param {string} projectRoot - The root directory of the project.
12-
* @returns {boolean} `true` if the project configuration was added successfully, `false` if the project already exists.
12+
* @returns - Whether project configuration was added successfully, or it already exists.
1313
* @throws {Error} If an error occurs that is not related to the project already existing.
1414
*/
1515
export function attemptToAddProjectConfiguration(tree: Tree, projectRoot: string) {
@@ -86,8 +86,24 @@ export function addTsConfigPath(tree: Tree, importPath: string, lookupPaths: str
8686
*/
8787
export function appendToIndexFile(tree: Tree, projectRoot: string, clientName: string) {
8888
const indexPath = `${projectRoot}/src/index.ts`;
89-
const newLine = `\nexport * as ${clientName}Client from "./${clientName}/client";`;
89+
const newLine = `export * from "./${clientName}/client";\n`;
9090

9191
const indexContent = tree.read(indexPath, 'utf-8');
9292
tree.write(indexPath, indexContent + newLine);
9393
}
94+
95+
/**
96+
* Convert a lower-case alphanumeric string (may include hyphens) into a PascalCase string.
97+
*
98+
* @param input - The input string to convert.
99+
* @example:
100+
* - "my-client" -> "MyClient"
101+
*/
102+
export function toClassName(input: string): string {
103+
return input
104+
.trim()
105+
.toLowerCase()
106+
.split('-')
107+
.map(c => c.charAt(0).toUpperCase() + c.slice(1))
108+
.join('');
109+
}

0 commit comments

Comments
 (0)