@aligent/nx-openapi 0.1.2 → 1.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aligent/nx-openapi",
3
- "version": "0.1.2",
3
+ "version": "1.0.0",
4
4
  "type": "commonjs",
5
5
  "main": "./src/index.js",
6
6
  "typings": "./src/index.d.ts",
@@ -0,0 +1,53 @@
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';
15
+
16
+ export class <%= className %> {
17
+ private credential: string | null = null;
18
+ public readonly client: Client<paths, `${string}/${string}`>;
19
+
20
+ constructor(options: ClientOptions, credentialPath: string) {
21
+ this.client = createClient<paths>(options);
22
+
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
+ }
@@ -1 +1,29 @@
1
- The <%= name %> REST API Client was generated using Nx Generators and openapi-typescript codegen
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
+ ```
@@ -0,0 +1,3 @@
1
+ import baseConfig from '../eslint.config.mjs';
2
+
3
+ export default [...baseConfig];
@@ -1,22 +1 @@
1
- // The following is an example file generated to demonstrate how to use your newly generated types
2
- import { paths } from '../generated';
3
- import createClient from 'openapi-fetch';
4
-
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'];
9
-
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
- const client = createClient<paths>({
13
- baseUrl: '',
14
- signal: AbortSignal.timeout(10000),
15
- });
16
-
17
- // Client getters are then fully typed. Try deleting '/customers' and seeing what routes you can use!
18
- const response = client.GET('/customers', {
19
- params: {
20
- query: {},
21
- },
22
- });
1
+ // When a client is generated, its added as an export to this index file. This makes sure that your entire application has access to all of your clients.
@@ -1,5 +1,5 @@
1
1
  {
2
- "extends": "../../tsconfig.base.json",
2
+ "extends": "../tsconfig.base.json",
3
3
  "compilerOptions": {
4
4
  "forceConsistentCasingInFileNames": true,
5
5
  "strict": true,
@@ -6,7 +6,7 @@ const generate_openapi_types_1 = require("../../helpers/generate-openapi-types")
6
6
  const utilities_1 = require("../../helpers/utilities");
7
7
  const VALID_EXTENSIONS = ['yaml', 'yml', 'json'];
8
8
  async function clientGenerator(tree, options) {
9
- const { name, schemaPath, importPath = `@clients/${name}`, skipValidate, override } = options;
9
+ const { name, schemaPath, importPath = `@clients`, skipValidate, override } = options;
10
10
  const ext = schemaPath.split('.').pop() || '';
11
11
  if (!VALID_EXTENSIONS.includes(ext)) {
12
12
  throw new Error(`Invalid schema file extension: ${ext}`);
@@ -17,23 +17,29 @@ async function clientGenerator(tree, options) {
17
17
  throw new Error('Schema validation failed!');
18
18
  }
19
19
  }
20
- const projectRoot = `clients/${name}`;
21
- const schemaDest = `${projectRoot}/schema.${ext}`;
22
- const typesDest = `${projectRoot}/generated/index.ts`;
23
- const isNewProject = (0, utilities_1.attemptToAddProjectConfiguration)(tree, name, projectRoot);
24
- if (!isNewProject && !override) {
25
- devkit_1.logger.info(`Project ${name} already exists. Use --override to override the existing schema.`);
26
- return;
20
+ const projectRoot = `clients`;
21
+ const apiClientDest = `${projectRoot}/src/${name}`;
22
+ const schemaDest = `${apiClientDest}/schema.${ext}`;
23
+ const typesDest = `${apiClientDest}/generated-types.ts`;
24
+ if (!override && tree.exists(apiClientDest)) {
25
+ throw new Error(`Directory "${name}" already exists. If you want to override the current api client in this directory use "--override"`);
27
26
  }
27
+ const isNewProject = (0, utilities_1.attemptToAddProjectConfiguration)(tree, projectRoot);
28
28
  await (0, generate_openapi_types_1.copySchema)(tree, schemaDest, schemaPath);
29
29
  await (0, generate_openapi_types_1.generateOpenApiTypes)(tree, schemaDest, typesDest);
30
30
  if (isNewProject) {
31
31
  devkit_1.logger.info(`Creating new project at ${projectRoot}`);
32
- // Generate other files
33
32
  (0, devkit_1.generateFiles)(tree, (0, devkit_1.joinPathFragments)(__dirname, './files'), projectRoot, options);
34
- // Add the project to the tsconfig paths so it can be imported by namespace
35
33
  (0, utilities_1.addTsConfigPath)(tree, importPath, [(0, devkit_1.joinPathFragments)(projectRoot, './src', 'index.ts')]);
36
34
  }
35
+ // Generate the files for the specific new client
36
+ (0, devkit_1.generateFiles)(tree, (0, devkit_1.joinPathFragments)(__dirname, './client-specific-files'), apiClientDest, {
37
+ className: (0, utilities_1.toClassName)(name),
38
+ });
39
+ // Append to index file for imports
40
+ (0, utilities_1.appendToIndexFile)(tree, projectRoot, name);
37
41
  await (0, devkit_1.formatFiles)(tree);
42
+ devkit_1.logger.info(`Successfully generated ${name} API client`);
43
+ devkit_1.logger.info(`Next step: Run "nx affected -t lint" to fix any linting issues that may arise from the generated code.`);
38
44
  }
39
45
  exports.default = clientGenerator;
@@ -6,6 +6,7 @@
6
6
  "properties": {
7
7
  "name": {
8
8
  "type": "string",
9
+ "pattern": "^[a-z0-9-]+$",
9
10
  "description": "Name of the api client.",
10
11
  "$default": {
11
12
  "$source": "argv",
@@ -37,7 +38,7 @@
37
38
  },
38
39
  "override": {
39
40
  "type": "boolean",
40
- "description": "Override existing project schema",
41
+ "description": "Override existing files during generation",
41
42
  "default": false
42
43
  }
43
44
  },
@@ -103,7 +103,6 @@ async function copySchema(tree, destination, schemaPath) {
103
103
  async function validateSchema(path) {
104
104
  let hasError = false;
105
105
  try {
106
- // TODO: MI-203 - Support private schema endpoint
107
106
  const config = await (0, openapi_core_1.loadConfig)();
108
107
  const results = await (0, openapi_core_1.lint)({ ref: path, config });
109
108
  results.forEach(result => {
@@ -8,10 +8,10 @@ import { Tree } from '@nx/devkit';
8
8
  * @param {Tree} tree - The file system tree representing the current project.
9
9
  * @param {string} name - The name of the project to add.
10
10
  * @param {string} projectRoot - The root directory of the project.
11
- * @returns {boolean} `true` if the project configuration was added successfully, `false` if the project already exists.
11
+ * @returns - Whether project configuration was added successfully, or it already exists.
12
12
  * @throws {Error} If an error occurs that is not related to the project already existing.
13
13
  */
14
- export declare function attemptToAddProjectConfiguration(tree: Tree, name: string, projectRoot: string): boolean;
14
+ export declare function attemptToAddProjectConfiguration(tree: Tree, projectRoot: string): boolean;
15
15
  /**
16
16
  * Adds a new path mapping to the `paths` property in the root TypeScript configuration file.
17
17
  *
@@ -24,3 +24,21 @@ export declare function attemptToAddProjectConfiguration(tree: Tree, name: strin
24
24
  * @throws {Error} If the import path already exists in the `paths` property.
25
25
  */
26
26
  export declare function addTsConfigPath(tree: Tree, importPath: string, lookupPaths: string[]): void;
27
+ /**
28
+ * Appends a new export statement to the index file.
29
+ *
30
+ * This function reads the content of the index file and appends a new export statement
31
+ * for the specified client.
32
+ *
33
+ * @param {Tree} tree - The file system tree representing the current project.
34
+ * @param {string} clientName - The name of the client to export.
35
+ */
36
+ export declare function appendToIndexFile(tree: Tree, projectRoot: string, clientName: string): void;
37
+ /**
38
+ * Convert a lower-case alphanumeric string (may include hyphens) into a PascalCase string.
39
+ *
40
+ * @param input - The input string to convert.
41
+ * @example:
42
+ * - "my-client" -> "MyClient"
43
+ */
44
+ export declare function toClassName(input: string): string;
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.attemptToAddProjectConfiguration = attemptToAddProjectConfiguration;
4
4
  exports.addTsConfigPath = addTsConfigPath;
5
+ exports.appendToIndexFile = appendToIndexFile;
6
+ exports.toClassName = toClassName;
5
7
  const devkit_1 = require("@nx/devkit");
6
8
  /**
7
9
  * Attempts to add a new project configuration to the workspace.
@@ -12,17 +14,17 @@ const devkit_1 = require("@nx/devkit");
12
14
  * @param {Tree} tree - The file system tree representing the current project.
13
15
  * @param {string} name - The name of the project to add.
14
16
  * @param {string} projectRoot - The root directory of the project.
15
- * @returns {boolean} `true` if the project configuration was added successfully, `false` if the project already exists.
17
+ * @returns - Whether project configuration was added successfully, or it already exists.
16
18
  * @throws {Error} If an error occurs that is not related to the project already existing.
17
19
  */
18
- function attemptToAddProjectConfiguration(tree, name, projectRoot) {
20
+ function attemptToAddProjectConfiguration(tree, projectRoot) {
19
21
  try {
20
- (0, devkit_1.addProjectConfiguration)(tree, name, {
22
+ (0, devkit_1.addProjectConfiguration)(tree, 'clients', {
21
23
  root: projectRoot,
22
24
  projectType: 'library',
23
25
  sourceRoot: `${projectRoot}/src`,
24
26
  targets: {},
25
- tags: ['client', name],
27
+ tags: ['clients'],
26
28
  });
27
29
  return true;
28
30
  }
@@ -69,3 +71,33 @@ function addTsConfigPath(tree, importPath, lookupPaths) {
69
71
  return json;
70
72
  });
71
73
  }
74
+ /**
75
+ * Appends a new export statement to the index file.
76
+ *
77
+ * This function reads the content of the index file and appends a new export statement
78
+ * for the specified client.
79
+ *
80
+ * @param {Tree} tree - The file system tree representing the current project.
81
+ * @param {string} clientName - The name of the client to export.
82
+ */
83
+ function appendToIndexFile(tree, projectRoot, clientName) {
84
+ const indexPath = `${projectRoot}/src/index.ts`;
85
+ const newLine = `export * from "./${clientName}/client";\n`;
86
+ const indexContent = tree.read(indexPath, 'utf-8');
87
+ tree.write(indexPath, indexContent + newLine);
88
+ }
89
+ /**
90
+ * Convert a lower-case alphanumeric string (may include hyphens) into a PascalCase string.
91
+ *
92
+ * @param input - The input string to convert.
93
+ * @example:
94
+ * - "my-client" -> "MyClient"
95
+ */
96
+ function toClassName(input) {
97
+ return input
98
+ .trim()
99
+ .toLowerCase()
100
+ .split('-')
101
+ .map(c => c.charAt(0).toUpperCase() + c.slice(1))
102
+ .join('');
103
+ }