@aligent/nx-openapi 2.0.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -1
- package/package.json +3 -2
- package/src/generators/client/client-specific-files/client.ts.template +1 -21
- package/src/generators/client/generator.js +7 -2
- package/src/generators/client/helpers/auth-configurations.d.ts +24 -0
- package/src/generators/client/helpers/auth-configurations.js +152 -0
- package/src/generators/client/schema.d.ts +1 -0
- package/src/generators/client/schema.json +6 -0
package/README.md
CHANGED
|
@@ -24,6 +24,11 @@ Run `nx g @aligent/nx-openapi:client` with optional flags:
|
|
|
24
24
|
- `--configPath` path to the redocly config file responsible for authentication when fetching a schema remotely. For more information: https://openapi-ts.dev/cli#auth.
|
|
25
25
|
- `--skipValidate` If passed, this will skip schema pre-validation. Only do this if you have good reason to - not validating the schema beforehand may produce unpredictable results (either things may not generate at all or they may generate something that is incorrect).
|
|
26
26
|
- `--override` Override the schema (and the generated type) of an existing client.
|
|
27
|
+
- `--authMethod` Authentication method for the generated client middleware. Defaults to `api-key`. Available options:
|
|
28
|
+
- `api-key` - API key authentication using SSM Parameter Store
|
|
29
|
+
- `oauth1.0a` - OAuth 1.0a authentication
|
|
30
|
+
- `basic` - Basic authentication (username/password)
|
|
31
|
+
- `oauth2.0` - OAuth 2.0 token-based authentication
|
|
27
32
|
|
|
28
33
|
**:rotating_light: Do not edit the files in the `/generated` folder after generating a client. These files are generated using the OpenAPI Schema and editing them may put you at risk of no longer conforming to the specifications of the API you are using! :rotating_light:**
|
|
29
34
|
|
|
@@ -73,4 +78,9 @@ A `types` file that will contain several typescript interfaces depending on the
|
|
|
73
78
|
- `operations`: The defined operations that can be performed on a given path. [More information.](https://swagger.io/docs/specification/v3_0/paths-and-operations/#operations)
|
|
74
79
|
- `components`: A way of avoid duplication when paths or operations contain the same structure in their responses. Components define a structure that is used multiple time throughout the API. [More information.](https://swagger.io/docs/specification/v3_0/components/)
|
|
75
80
|
|
|
76
|
-
A `client` file that will contain
|
|
81
|
+
A `client` file that will contain boilerplate code to help you get started. The client includes authentication middleware configured based on the `--authMethod` flag:
|
|
82
|
+
|
|
83
|
+
- **api-key**: Configured to fetch credentials from AWS SSM Parameter Store
|
|
84
|
+
- **oauth1.0a**: Configured with HMAC-SHA256 algorithm and placeholder credentials
|
|
85
|
+
- **basic**: Configured with placeholder username/password credentials
|
|
86
|
+
- **oauth2.0**: Configured with placeholder token and Bearer token type
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aligent/nx-openapi",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"type": "commonjs",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"typings": "./src/index.d.ts",
|
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
"directory": "packages/nx-openapi"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@nx/devkit": "22.4.
|
|
13
|
+
"@nx/devkit": "22.4.2",
|
|
14
14
|
"@redocly/openapi-core": "^1.34.2",
|
|
15
15
|
"openapi-typescript": "7.7.3",
|
|
16
|
+
"ts-morph": "^27.0.2",
|
|
16
17
|
"typescript": "^5.9.3"
|
|
17
18
|
},
|
|
18
19
|
"generators": "./generators.json",
|
|
@@ -6,18 +6,15 @@
|
|
|
6
6
|
* - For more information about openapi-fetch middlewares: https://openapi-ts.dev/openapi-fetch/middleware-auth#middleware
|
|
7
7
|
*/
|
|
8
8
|
import {
|
|
9
|
-
apiKeyAuthMiddleware,
|
|
10
|
-
fetchSsmParams,
|
|
11
9
|
retryMiddleware,
|
|
12
10
|
} from '@aligent/microservice-util-lib';
|
|
13
11
|
import createClient, { Client, ClientOptions } from 'openapi-fetch';
|
|
14
12
|
import { paths } from './generated-types';
|
|
15
13
|
|
|
16
14
|
export class <%= className %> {
|
|
17
|
-
private credential: string | null = null;
|
|
18
15
|
public readonly client: Client<paths, `${string}/${string}`>;
|
|
19
16
|
|
|
20
|
-
constructor(options: ClientOptions
|
|
17
|
+
constructor(options: ClientOptions) {
|
|
21
18
|
this.client = createClient<paths>(options);
|
|
22
19
|
|
|
23
20
|
/**
|
|
@@ -25,23 +22,6 @@ export class <%= className %> {
|
|
|
25
22
|
* - For requests, onRequest() will be called in the order registered
|
|
26
23
|
* - For responses, onResponse() will be called in reverse order.
|
|
27
24
|
*/
|
|
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
25
|
|
|
46
26
|
this.client.use(
|
|
47
27
|
retryMiddleware({
|
|
@@ -4,11 +4,12 @@ exports.clientGenerator = clientGenerator;
|
|
|
4
4
|
const devkit_1 = require("@nx/devkit");
|
|
5
5
|
const generate_openapi_types_1 = require("../../helpers/generate-openapi-types");
|
|
6
6
|
const utilities_1 = require("../../helpers/utilities");
|
|
7
|
+
const auth_configurations_1 = require("./helpers/auth-configurations");
|
|
7
8
|
const VALID_EXTENSIONS = ['yaml', 'yml', 'json'];
|
|
8
9
|
// We also use this as the project root for all generated clients
|
|
9
10
|
const PROJECT_NAME = 'clients';
|
|
10
11
|
async function clientGenerator(tree, options) {
|
|
11
|
-
const { name, schemaPath, importPath = `@clients`, skipValidate, override } = options;
|
|
12
|
+
const { name, schemaPath, importPath = `@clients`, skipValidate, override, authMethod = 'api-key', } = options;
|
|
12
13
|
const ext = schemaPath.split('.').pop() || '';
|
|
13
14
|
if (!VALID_EXTENSIONS.includes(ext)) {
|
|
14
15
|
throw new Error(`Invalid schema file extension: ${ext}`);
|
|
@@ -43,9 +44,13 @@ async function clientGenerator(tree, options) {
|
|
|
43
44
|
* Each time we add new API client, we actually add a new class into `clients` project (if it exists).
|
|
44
45
|
* This add a new example client class to `apiClientDest` folder
|
|
45
46
|
*/
|
|
47
|
+
const className = (0, utilities_1.toClassName)(name);
|
|
46
48
|
(0, devkit_1.generateFiles)(tree, (0, devkit_1.joinPathFragments)(__dirname, './client-specific-files'), apiClientDest, {
|
|
47
|
-
className
|
|
49
|
+
className,
|
|
48
50
|
});
|
|
51
|
+
// Apply auth method configuration using ts-morph
|
|
52
|
+
const clientFilePath = (0, devkit_1.joinPathFragments)(apiClientDest, 'client.ts');
|
|
53
|
+
(0, auth_configurations_1.applyAuthMethodConfiguration)(tree, clientFilePath, authMethod, className);
|
|
49
54
|
/**
|
|
50
55
|
* The `clients` project expose all the API clients via `src/index.ts` file.
|
|
51
56
|
* As a result, we need to append new client to the list of exporting;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Tree } from '@nx/devkit';
|
|
2
|
+
interface MiddlewareConfig {
|
|
3
|
+
properties: string;
|
|
4
|
+
}
|
|
5
|
+
interface AuthMethodConfig {
|
|
6
|
+
middlewareName: string;
|
|
7
|
+
extraImports: string[];
|
|
8
|
+
classProperty: string | null;
|
|
9
|
+
constructorParam: string | null;
|
|
10
|
+
middlewareConfig: MiddlewareConfig;
|
|
11
|
+
}
|
|
12
|
+
export declare const AUTH_CONFIGS: Record<string, AuthMethodConfig>;
|
|
13
|
+
/**
|
|
14
|
+
* Applies auth method configuration to a generated client file using ts-morph.
|
|
15
|
+
* This modifies the file in the Nx Tree to add auth-specific imports, properties,
|
|
16
|
+
* constructor parameters, and middleware configuration.
|
|
17
|
+
*
|
|
18
|
+
* @param tree - The Nx virtual file system tree
|
|
19
|
+
* @param filePath - Path to the client.ts file in the tree
|
|
20
|
+
* @param authMethod - The auth method to apply
|
|
21
|
+
* @param className - The name of the client class
|
|
22
|
+
*/
|
|
23
|
+
export declare function applyAuthMethodConfiguration(tree: Tree, filePath: string, authMethod: string, className: string): void;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AUTH_CONFIGS = void 0;
|
|
4
|
+
exports.applyAuthMethodConfiguration = applyAuthMethodConfiguration;
|
|
5
|
+
const ts_morph_1 = require("ts-morph");
|
|
6
|
+
exports.AUTH_CONFIGS = {
|
|
7
|
+
'api-key': {
|
|
8
|
+
middlewareName: 'apiKeyAuthMiddleware',
|
|
9
|
+
extraImports: ['fetchSsmParams'],
|
|
10
|
+
classProperty: 'private credential: string | null = null;',
|
|
11
|
+
constructorParam: 'credentialPath: string',
|
|
12
|
+
middlewareConfig: {
|
|
13
|
+
properties: `header: 'X-Api-Key',
|
|
14
|
+
value: async () => {
|
|
15
|
+
if (!this.credential) {
|
|
16
|
+
const param = await fetchSsmParams(credentialPath);
|
|
17
|
+
if (!param?.Value) {
|
|
18
|
+
throw new Error('Unable to fetch API client credential');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
this.credential = param.Value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return this.credential;
|
|
25
|
+
},`,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
'oauth1.0a': {
|
|
29
|
+
middlewareName: 'oAuth10aAuthMiddleware',
|
|
30
|
+
extraImports: [],
|
|
31
|
+
classProperty: null,
|
|
32
|
+
constructorParam: null,
|
|
33
|
+
middlewareConfig: {
|
|
34
|
+
properties: `algorithm: 'HMAC-SHA256',
|
|
35
|
+
credentials: async () => ({
|
|
36
|
+
// TODO: Provide your OAuth 1.0a credentials
|
|
37
|
+
consumerKey: 'your-consumer-key',
|
|
38
|
+
consumerSecret: 'your-consumer-secret',
|
|
39
|
+
token: 'your-token',
|
|
40
|
+
tokenSecret: 'your-token-secret',
|
|
41
|
+
}),`,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
basic: {
|
|
45
|
+
middlewareName: 'basicAuthMiddleware',
|
|
46
|
+
extraImports: [],
|
|
47
|
+
classProperty: null,
|
|
48
|
+
constructorParam: null,
|
|
49
|
+
middlewareConfig: {
|
|
50
|
+
properties: `credentials: async () => ({
|
|
51
|
+
// TODO: Provide your basic auth credentials
|
|
52
|
+
username: 'your-username',
|
|
53
|
+
password: 'your-password',
|
|
54
|
+
}),`,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
'oauth2.0': {
|
|
58
|
+
middlewareName: 'oAuth20AuthMiddleware',
|
|
59
|
+
extraImports: [],
|
|
60
|
+
classProperty: null,
|
|
61
|
+
constructorParam: null,
|
|
62
|
+
middlewareConfig: {
|
|
63
|
+
properties: `token: async () => {
|
|
64
|
+
// TODO: Send API call to get your OAuth 2.0 access token
|
|
65
|
+
return 'your-access-token';
|
|
66
|
+
}`,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Applies auth method configuration to a generated client file using ts-morph.
|
|
72
|
+
* This modifies the file in the Nx Tree to add auth-specific imports, properties,
|
|
73
|
+
* constructor parameters, and middleware configuration.
|
|
74
|
+
*
|
|
75
|
+
* @param tree - The Nx virtual file system tree
|
|
76
|
+
* @param filePath - Path to the client.ts file in the tree
|
|
77
|
+
* @param authMethod - The auth method to apply
|
|
78
|
+
* @param className - The name of the client class
|
|
79
|
+
*/
|
|
80
|
+
function applyAuthMethodConfiguration(tree, filePath, authMethod, className) {
|
|
81
|
+
const config = exports.AUTH_CONFIGS[authMethod];
|
|
82
|
+
if (!config) {
|
|
83
|
+
throw new Error(`Unknown auth method: ${authMethod}`);
|
|
84
|
+
}
|
|
85
|
+
const fileContent = tree.read(filePath, 'utf-8');
|
|
86
|
+
if (!fileContent) {
|
|
87
|
+
throw new Error(`Unable to read file: ${filePath}`);
|
|
88
|
+
}
|
|
89
|
+
const project = new ts_morph_1.Project({
|
|
90
|
+
useInMemoryFileSystem: true,
|
|
91
|
+
manipulationSettings: {
|
|
92
|
+
indentationText: ts_morph_1.IndentationText.FourSpaces,
|
|
93
|
+
quoteKind: ts_morph_1.QuoteKind.Single,
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
const sourceFile = project.createSourceFile(filePath, fileContent);
|
|
97
|
+
// Add imports to the @aligent/microservice-util-lib import declaration
|
|
98
|
+
const utilLibImport = sourceFile.getImportDeclaration(decl => decl.getModuleSpecifierValue() === '@aligent/microservice-util-lib');
|
|
99
|
+
if (utilLibImport) {
|
|
100
|
+
for (const importName of config.extraImports) {
|
|
101
|
+
utilLibImport.addNamedImport(importName);
|
|
102
|
+
}
|
|
103
|
+
utilLibImport.addNamedImport(config.middlewareName);
|
|
104
|
+
}
|
|
105
|
+
// Get the client class
|
|
106
|
+
const clientClass = sourceFile.getClass(className);
|
|
107
|
+
if (!clientClass) {
|
|
108
|
+
throw new Error(`Unable to find class: ${className}`);
|
|
109
|
+
}
|
|
110
|
+
// Add class property if configured (insert before the 'client' property)
|
|
111
|
+
if (config.classProperty) {
|
|
112
|
+
const clientProperty = clientClass.getProperty('client');
|
|
113
|
+
if (clientProperty) {
|
|
114
|
+
const propertyIndex = clientProperty.getChildIndex();
|
|
115
|
+
clientClass.insertProperty(propertyIndex, {
|
|
116
|
+
name: 'credential',
|
|
117
|
+
type: 'string | null',
|
|
118
|
+
initializer: 'null',
|
|
119
|
+
scope: ts_morph_1.Scope.Private,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Modify constructor to add parameter and middleware
|
|
124
|
+
const constructor = clientClass.getConstructors()[0];
|
|
125
|
+
if (!constructor) {
|
|
126
|
+
throw new Error(`Unable to find constructor in class: ${className}`);
|
|
127
|
+
}
|
|
128
|
+
// Add constructor parameter if configured
|
|
129
|
+
if (config.constructorParam) {
|
|
130
|
+
const parts = config.constructorParam.split(':');
|
|
131
|
+
const paramName = parts[0]?.trim() ?? '';
|
|
132
|
+
const paramType = parts[1]?.trim() ?? '';
|
|
133
|
+
constructor.addParameter({
|
|
134
|
+
name: paramName,
|
|
135
|
+
type: paramType,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
// Build the middleware call
|
|
139
|
+
const middlewareCall = `this.client.use(
|
|
140
|
+
${config.middlewareName}({
|
|
141
|
+
${config.middlewareConfig.properties}
|
|
142
|
+
})
|
|
143
|
+
);`;
|
|
144
|
+
// Find the retryMiddleware use statement and insert before it
|
|
145
|
+
const statements = constructor.getStatements();
|
|
146
|
+
const retryStatementIndex = statements.findIndex(statement => /this\.client\.use\(\s*retryMiddleware/.test(statement.getText()));
|
|
147
|
+
if (retryStatementIndex !== -1) {
|
|
148
|
+
constructor.insertStatements(retryStatementIndex, middlewareCall);
|
|
149
|
+
}
|
|
150
|
+
// Write the modified content back to the tree
|
|
151
|
+
tree.write(filePath, sourceFile.getFullText());
|
|
152
|
+
}
|
|
@@ -40,6 +40,12 @@
|
|
|
40
40
|
"type": "boolean",
|
|
41
41
|
"description": "Override existing files during generation",
|
|
42
42
|
"default": false
|
|
43
|
+
},
|
|
44
|
+
"authMethod": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"description": "Authentication method to use for the generated client middleware",
|
|
47
|
+
"enum": ["api-key", "oauth1.0a", "basic", "oauth2.0"],
|
|
48
|
+
"default": "api-key"
|
|
43
49
|
}
|
|
44
50
|
},
|
|
45
51
|
"required": ["name", "schemaPath"]
|