@aligent/nx-openapi 0.1.3 → 2.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.3",
3
+ "version": "2.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];
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "clients",
3
+ "type": "module",
4
+ "main": "./src/index.ts",
5
+ "types": "./src/index.ts",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./src/index.ts",
9
+ "import": "./src/index.ts",
10
+ "default": "./src/index.ts"
11
+ },
12
+ "./package.json": "./package.json"
13
+ },
14
+ "nx": {
15
+ "tags": [
16
+ "scope:libs"
17
+ ]
18
+ }
19
+ }
@@ -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,19 +1,6 @@
1
1
  {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "forceConsistentCasingInFileNames": true,
5
- "strict": true,
6
- "importHelpers": true,
7
- "noImplicitOverride": true,
8
- "noImplicitReturns": true,
9
- "noFallthroughCasesInSwitch": true,
10
- "noPropertyAccessFromIndexSignature": true
11
- },
2
+ "extends": "@aligent/ts-code-standards/tsconfigs-extend",
12
3
  "files": [],
13
4
  "include": [],
14
- "references": [
15
- {
16
- "path": "./tsconfig.lib.json"
17
- }
18
- ]
5
+ "references": []
19
6
  }
@@ -5,35 +5,54 @@ 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
7
  const VALID_EXTENSIONS = ['yaml', 'yml', 'json'];
8
+ // We also use this as the project root for all generated clients
9
+ const PROJECT_NAME = 'clients';
8
10
  async function clientGenerator(tree, options) {
9
- const { name, schemaPath, importPath = `@clients/${name}`, skipValidate, override } = options;
11
+ const { name, schemaPath, importPath = `@clients`, skipValidate, override } = options;
10
12
  const ext = schemaPath.split('.').pop() || '';
11
13
  if (!VALID_EXTENSIONS.includes(ext)) {
12
14
  throw new Error(`Invalid schema file extension: ${ext}`);
13
15
  }
14
- if (!skipValidate) {
15
- const hasError = await (0, generate_openapi_types_1.validateSchema)(schemaPath);
16
- if (hasError) {
17
- throw new Error('Schema validation failed!');
18
- }
16
+ const hasError = await (0, generate_openapi_types_1.validateSchema)(schemaPath);
17
+ if (!skipValidate && hasError) {
18
+ throw new Error('Schema validation failed!');
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 = PROJECT_NAME;
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
  }
28
- await (0, generate_openapi_types_1.copySchema)(tree, schemaDest, schemaPath);
29
- await (0, generate_openapi_types_1.generateOpenApiTypes)(tree, schemaDest, typesDest);
30
- if (isNewProject) {
31
- devkit_1.logger.info(`Creating new project at ${projectRoot}`);
32
- // Generate other files
27
+ const existingProject = (0, utilities_1.getExistingProject)(tree, PROJECT_NAME);
28
+ if (!existingProject) {
29
+ devkit_1.logger.warn(`Creating new project ${PROJECT_NAME} at ${projectRoot}`);
33
30
  (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
- (0, utilities_1.addTsConfigPath)(tree, importPath, [(0, devkit_1.joinPathFragments)(projectRoot, './src', 'index.ts')]);
31
+ const tsConfigFile = (0, utilities_1.getRootTsConfigPathInTree)(tree);
32
+ if (tsConfigFile === 'tsconfig.json') {
33
+ (0, utilities_1.addTsConfigReference)(tree, tsConfigFile, `./${projectRoot}`);
34
+ }
35
+ else {
36
+ const lookupPath = (0, devkit_1.joinPathFragments)(projectRoot, './src', 'index.ts');
37
+ (0, utilities_1.addTsConfigPath)(tree, tsConfigFile, importPath, [lookupPath]);
38
+ }
36
39
  }
40
+ await (0, generate_openapi_types_1.copySchema)(tree, schemaDest, schemaPath);
41
+ await (0, generate_openapi_types_1.generateOpenApiTypes)(tree, schemaDest, typesDest);
42
+ /**
43
+ * Each time we add new API client, we actually add a new class into `clients` project (if it exists).
44
+ * This add a new example client class to `apiClientDest` folder
45
+ */
46
+ (0, devkit_1.generateFiles)(tree, (0, devkit_1.joinPathFragments)(__dirname, './client-specific-files'), apiClientDest, {
47
+ className: (0, utilities_1.toClassName)(name),
48
+ });
49
+ /**
50
+ * The `clients` project expose all the API clients via `src/index.ts` file.
51
+ * As a result, we need to append new client to the list of exporting;
52
+ */
53
+ (0, utilities_1.appendToIndexFile)(tree, projectRoot, name);
37
54
  await (0, devkit_1.formatFiles)(tree);
55
+ devkit_1.logger.info(`Successfully generated ${name} API client`);
56
+ devkit_1.logger.info(`Next step: Run "nx affected -t lint" to fix any linting issues that may arise from the generated code.`);
38
57
  }
39
58
  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 => {
@@ -1,26 +1,55 @@
1
1
  import { Tree } from '@nx/devkit';
2
2
  /**
3
- * Attempts to add a new project configuration to the workspace.
3
+ * Get existing project by name.
4
+ * If the project exists, it returns project configuration or undefined otherwise.
4
5
  *
5
- * This function adds a new project configuration to the workspace. If a project with the same name
6
- * already exists, it returns `false`. If an error occurs for any other reason, the error is re-thrown.
6
+ * @param tree - The file system tree representing the current project.
7
+ * @param projectName - The name of the project to add.
8
+ * @returns - The project configuration.
9
+ */
10
+ export declare function getExistingProject(tree: Tree, projectName: string): import("@nx/devkit").ProjectConfiguration | undefined;
11
+ /**
12
+ * The utility functions below are only exported by '@nx/js', not '@nx/devkit'
13
+ * They're simple so we recreate them here instead of adding '@nx/js' as a dependency
14
+ * Source: {@link https://github.com/nrwl/nx/blob/master/packages/js/src/utils/typescript/ts-config.ts}
15
+ */
16
+ export declare function getRootTsConfigPathInTree(tree: Tree): string;
17
+ /**
18
+ * Adds a new path mapping to the `compilerOptions.paths` property in the root TypeScript configuration file.
19
+ * If the import path already exists, an error is thrown.
7
20
  *
8
- * @param {Tree} tree - The file system tree representing the current project.
9
- * @param {string} name - The name of the project to add.
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.
12
- * @throws {Error} If an error occurs that is not related to the project already existing.
21
+ * @param tree - The file system tree representing the current project.
22
+ * @param tsConfigFile - The root TypeScript configuration file path.
23
+ * @param importPath - The import path to add to the `paths` property.
24
+ * @param lookupPaths - The array of paths to associate with the import path.
25
+ * @throws - If the import path already exists in the `paths` property.
13
26
  */
14
- export declare function attemptToAddProjectConfiguration(tree: Tree, name: string, projectRoot: string): boolean;
27
+ export declare function addTsConfigPath(tree: Tree, tsConfigFile: string, importPath: string, lookupPaths: string[]): void;
15
28
  /**
16
- * Adds a new path mapping to the `paths` property in the root TypeScript configuration file.
29
+ * Adds a new referencing path to the `references` property in the root TypeScript configuration file.
30
+ * If the referencing path already exists, an error is thrown.
17
31
  *
18
- * This function updates the `tsconfig.base.json` or `tsconfig.json` file to include a new path mapping
19
- * for the specified import path. If the import path already exists, an error is thrown.
32
+ * @param tree - The file system tree representing the current project.
33
+ * @param tsConfigFile - The root TypeScript configuration file path.
34
+ * @param referencePath - The referencing path.
35
+ * @throws - If the import path already exists in the `paths` property.
36
+ */
37
+ export declare function addTsConfigReference(tree: Tree, tsConfigFile: string, referencePath: string): void;
38
+ /**
39
+ * Appends a new export statement to the index file.
40
+ *
41
+ * This function reads the content of the index file and appends a new export statement
42
+ * for the specified client.
20
43
  *
21
44
  * @param {Tree} tree - The file system tree representing the current project.
22
- * @param {string} importPath - The import path to add to the `paths` property.
23
- * @param {string[]} lookupPaths - The array of paths to associate with the import path.
24
- * @throws {Error} If the import path already exists in the `paths` property.
45
+ * @param {string} clientName - The name of the client to export.
46
+ */
47
+ export declare function appendToIndexFile(tree: Tree, projectRoot: string, clientName: string): void;
48
+ /**
49
+ * Convert a lower-case alphanumeric string (may include hyphens) into a PascalCase string.
50
+ *
51
+ * @param input - The input string to convert.
52
+ * @example:
53
+ * - "my-client" -> "MyClient"
25
54
  */
26
- export declare function addTsConfigPath(tree: Tree, importPath: string, lookupPaths: string[]): void;
55
+ export declare function toClassName(input: string): string;
@@ -1,36 +1,27 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.attemptToAddProjectConfiguration = attemptToAddProjectConfiguration;
3
+ exports.getExistingProject = getExistingProject;
4
+ exports.getRootTsConfigPathInTree = getRootTsConfigPathInTree;
4
5
  exports.addTsConfigPath = addTsConfigPath;
6
+ exports.addTsConfigReference = addTsConfigReference;
7
+ exports.appendToIndexFile = appendToIndexFile;
8
+ exports.toClassName = toClassName;
5
9
  const devkit_1 = require("@nx/devkit");
6
10
  /**
7
- * Attempts to add a new project configuration to the workspace.
11
+ * Get existing project by name.
12
+ * If the project exists, it returns project configuration or undefined otherwise.
8
13
  *
9
- * This function adds a new project configuration to the workspace. If a project with the same name
10
- * already exists, it returns `false`. If an error occurs for any other reason, the error is re-thrown.
11
- *
12
- * @param {Tree} tree - The file system tree representing the current project.
13
- * @param {string} name - The name of the project to add.
14
- * @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.
16
- * @throws {Error} If an error occurs that is not related to the project already existing.
14
+ * @param tree - The file system tree representing the current project.
15
+ * @param projectName - The name of the project to add.
16
+ * @returns - The project configuration.
17
17
  */
18
- function attemptToAddProjectConfiguration(tree, name, projectRoot) {
18
+ function getExistingProject(tree, projectName) {
19
19
  try {
20
- (0, devkit_1.addProjectConfiguration)(tree, name, {
21
- root: projectRoot,
22
- projectType: 'library',
23
- sourceRoot: `${projectRoot}/src`,
24
- targets: {},
25
- tags: ['client', name],
26
- });
27
- return true;
20
+ return (0, devkit_1.readProjectConfiguration)(tree, projectName);
28
21
  }
29
- catch (err) {
30
- if (err instanceof Error && err.message.includes('already exists')) {
31
- return false;
32
- }
33
- throw err;
22
+ catch (error) {
23
+ devkit_1.logger.debug(`Project ${projectName} doesn't exist`, String(error));
24
+ return undefined;
34
25
  }
35
26
  }
36
27
  /**
@@ -47,18 +38,17 @@ function getRootTsConfigPathInTree(tree) {
47
38
  return 'tsconfig.base.json';
48
39
  }
49
40
  /**
50
- * Adds a new path mapping to the `paths` property in the root TypeScript configuration file.
51
- *
52
- * This function updates the `tsconfig.base.json` or `tsconfig.json` file to include a new path mapping
53
- * for the specified import path. If the import path already exists, an error is thrown.
41
+ * Adds a new path mapping to the `compilerOptions.paths` property in the root TypeScript configuration file.
42
+ * If the import path already exists, an error is thrown.
54
43
  *
55
- * @param {Tree} tree - The file system tree representing the current project.
56
- * @param {string} importPath - The import path to add to the `paths` property.
57
- * @param {string[]} lookupPaths - The array of paths to associate with the import path.
58
- * @throws {Error} If the import path already exists in the `paths` property.
44
+ * @param tree - The file system tree representing the current project.
45
+ * @param tsConfigFile - The root TypeScript configuration file path.
46
+ * @param importPath - The import path to add to the `paths` property.
47
+ * @param lookupPaths - The array of paths to associate with the import path.
48
+ * @throws - If the import path already exists in the `paths` property.
59
49
  */
60
- function addTsConfigPath(tree, importPath, lookupPaths) {
61
- (0, devkit_1.updateJson)(tree, getRootTsConfigPathInTree(tree), json => {
50
+ function addTsConfigPath(tree, tsConfigFile, importPath, lookupPaths) {
51
+ (0, devkit_1.updateJson)(tree, tsConfigFile, json => {
62
52
  json.compilerOptions ??= {};
63
53
  const c = json.compilerOptions;
64
54
  c.paths ??= {};
@@ -69,3 +59,54 @@ function addTsConfigPath(tree, importPath, lookupPaths) {
69
59
  return json;
70
60
  });
71
61
  }
62
+ /**
63
+ * Adds a new referencing path to the `references` property in the root TypeScript configuration file.
64
+ * If the referencing path already exists, an error is thrown.
65
+ *
66
+ * @param tree - The file system tree representing the current project.
67
+ * @param tsConfigFile - The root TypeScript configuration file path.
68
+ * @param referencePath - The referencing path.
69
+ * @throws - If the import path already exists in the `paths` property.
70
+ */
71
+ function addTsConfigReference(tree, tsConfigFile, referencePath) {
72
+ (0, devkit_1.updateJson)(tree, tsConfigFile, json => {
73
+ json.references ??= [];
74
+ if (json.references.some((r) => r.path === referencePath)) {
75
+ throw new Error(`You already have a library using the import path "${referencePath}". Make sure to specify a unique one.`);
76
+ }
77
+ json.references.push({
78
+ path: referencePath,
79
+ });
80
+ return json;
81
+ });
82
+ }
83
+ /**
84
+ * Appends a new export statement to the index file.
85
+ *
86
+ * This function reads the content of the index file and appends a new export statement
87
+ * for the specified client.
88
+ *
89
+ * @param {Tree} tree - The file system tree representing the current project.
90
+ * @param {string} clientName - The name of the client to export.
91
+ */
92
+ function appendToIndexFile(tree, projectRoot, clientName) {
93
+ const indexPath = `${projectRoot}/src/index.ts`;
94
+ const newLine = `export * from "./${clientName}/client";\n`;
95
+ const indexContent = tree.read(indexPath, 'utf-8');
96
+ tree.write(indexPath, indexContent + newLine);
97
+ }
98
+ /**
99
+ * Convert a lower-case alphanumeric string (may include hyphens) into a PascalCase string.
100
+ *
101
+ * @param input - The input string to convert.
102
+ * @example:
103
+ * - "my-client" -> "MyClient"
104
+ */
105
+ function toClassName(input) {
106
+ return input
107
+ .trim()
108
+ .toLowerCase()
109
+ .split('-')
110
+ .map(c => c.charAt(0).toUpperCase() + c.slice(1))
111
+ .join('');
112
+ }
@@ -1,9 +0,0 @@
1
- {
2
- "extends": "./tsconfig.json",
3
- "compilerOptions": {
4
- "outDir": "../../dist/out-tsc",
5
- "declaration": true,
6
- "types": ["node"]
7
- },
8
- "include": ["src/**/*.ts"]
9
- }