mirror of https://github.com/grafana/grafana
API clients: Add generator (#104093)
* Add API client generator * Extract config entry template * Fix index file * Fix message and file pattern * Fix generate-rtk template * Match generated-api * Format * Split helpers * Cleanup * Remove unused helpers * Simplify group name handling * Run generate-apis * Prettier * Format + lint * improve lint/format * Optional filterEndpoints * Format * Update readme * More updates * Move the helpers out * Switch to TS * Cleanup types * Add support for Enterprise * Add comments * Refactor endpoint handling and update README * Simplify checks * Do not register reducers and middleware for enterprise * More docs updates * Remove redundant sections * Format gitignored files * Add limitations * Simplify types * Simplify path logic * Do not format OSS paths for enterprise * dedupe * format * Simplify instructions * Update lockfile * Add comments * Remove custom typespull/104428/head^2
parent
7ed17cacbf
commit
618ffd0275
@ -0,0 +1,52 @@ |
||||
# RTK Query API Client Generator |
||||
|
||||
This generator automates the process of creating RTK Query API clients for Grafana's API groups. It replaces the manual steps outlined in the [main API documentation](../README.md). |
||||
|
||||
## Usage |
||||
|
||||
```bash |
||||
yarn generate:api-client |
||||
``` |
||||
|
||||
The CLI will prompt for: |
||||
|
||||
1. **Enterprise or OSS API** - Whether this is an Enterprise or OSS API. This affects paths and build commands. |
||||
2. **API group name** - The basic name for the API (e.g., `dashboard`) |
||||
3. **API group** - The full API group name (defaults to `<group-name>.grafana.app`) |
||||
4. **API version** - The API version (e.g., `v0alpha1`) |
||||
5. **Reducer path** - The Redux reducer path (defaults to `<group-name>API`). This will also be used as the API's named export. |
||||
6. **Endpoints** - Optional comma-separated list of endpoints to include (e.g., `createDashboard,updateDashboard`). If not provided, all endpoints will be included. |
||||
|
||||
## What It Does |
||||
|
||||
The generator automates the following: |
||||
|
||||
1. Creates the `baseAPI.ts` file for the API group |
||||
2. Updates the appropriate generate script to include the API client |
||||
- `scripts/generate-rtk-apis.ts` for OSS APIs |
||||
- `local/generate-enterprise-apis.ts` for Enterprise APIs |
||||
3. Creates the `index.ts` file with proper exports |
||||
4. For OSS APIs only: Registers Redux reducers and middleware in the store. For Enterprise this needs to be done manually |
||||
5. Formats all generated files using Prettier and ESLint |
||||
6. Automatically runs the appropriate command to generate endpoints from the OpenAPI schema |
||||
|
||||
## Limitations |
||||
|
||||
- The generator is optimized for Kubernetes-style APIs, as it requires Kubernetes resource details. For legacy APIs, manual adjustments may be needed. |
||||
- It expects processed OpenAPI specifications to exist in the `openapi_snapshots` directory |
||||
|
||||
## Troubleshooting |
||||
|
||||
### Missing OpenAPI Schema |
||||
|
||||
If an error about a missing OpenAPI schema appears, check that: |
||||
|
||||
1. The API group and version exist in the backend |
||||
2. The `TestIntegrationOpenAPIs` test has been run to generate the schema (step 1 in the [main API documentation](../README.md)). |
||||
3. The schema file exists at `data/openapi/<group>-<version>.json` |
||||
|
||||
### Validation Errors |
||||
|
||||
- API group must include `.grafana.app` |
||||
- Version must be in format `v0alpha1`, `v1beta2`, etc. |
||||
- Reducer path must end with `API` |
@ -0,0 +1,165 @@ |
||||
import path from 'path'; |
||||
import type { NodePlopAPI, PlopGeneratorConfig } from 'plop'; |
||||
|
||||
import { |
||||
formatEndpoints, |
||||
validateGroup, |
||||
validateVersion, |
||||
getFilesToFormat, |
||||
runGenerateApis, |
||||
formatFiles, |
||||
// The file extension is necessary to make the imports
|
||||
// work with the '--experimental-strip-types' flag
|
||||
// @ts-ignore
|
||||
} from './helpers.ts'; |
||||
// @ts-ignore
|
||||
import { type ActionConfig, type PlopData, isPlopData } from './types.ts'; |
||||
|
||||
export default function plopGenerator(plop: NodePlopAPI) { |
||||
// Grafana root path
|
||||
const basePath = path.resolve(import.meta.dirname, '../../../..'); |
||||
|
||||
// Register custom action types
|
||||
plop.setActionType('runGenerateApis', runGenerateApis(basePath)); |
||||
plop.setActionType('formatFiles', formatFiles(basePath)); |
||||
|
||||
// Used in templates to format endpoints
|
||||
plop.setHelper('formatEndpoints', formatEndpoints()); |
||||
|
||||
const generateRtkApiActions = (data: PlopData) => { |
||||
const { reducerPath, groupName, isEnterprise } = data; |
||||
|
||||
const apiClientBasePath = isEnterprise ? 'public/app/extensions/api/clients' : 'public/app/api/clients'; |
||||
const generateScriptPath = isEnterprise ? 'local/generate-enterprise-apis.ts' : 'scripts/generate-rtk-apis.ts'; |
||||
|
||||
// Using app path, so the imports work on any file level
|
||||
const clientImportPath = isEnterprise ? '../extensions/api/clients' : 'app/api/clients'; |
||||
|
||||
const apiPathPrefix = isEnterprise ? '../public/app/extensions/api/clients' : '../public/app/api/clients'; |
||||
|
||||
const templateData = { |
||||
...data, |
||||
apiPathPrefix, |
||||
}; |
||||
|
||||
// Base actions that are always added
|
||||
const actions: ActionConfig[] = [ |
||||
{ |
||||
type: 'add', |
||||
path: path.join(basePath, `${apiClientBasePath}/${groupName}/baseAPI.ts`), |
||||
templateFile: './templates/baseAPI.ts.hbs', |
||||
}, |
||||
{ |
||||
type: 'modify', |
||||
path: path.join(basePath, generateScriptPath), |
||||
pattern: '// PLOP_INJECT_API_CLIENT - Used by the API client generator', |
||||
templateFile: './templates/config-entry.hbs', |
||||
data: templateData, |
||||
}, |
||||
{ |
||||
type: 'add', |
||||
path: path.join(basePath, `${apiClientBasePath}/${groupName}/index.ts`), |
||||
templateFile: './templates/index.ts.hbs', |
||||
}, |
||||
]; |
||||
|
||||
// Only add redux reducer and middleware for OSS clients
|
||||
if (!isEnterprise) { |
||||
actions.push( |
||||
{ |
||||
type: 'modify', |
||||
path: path.join(basePath, 'public/app/core/reducers/root.ts'), |
||||
pattern: '// PLOP_INJECT_IMPORT', |
||||
template: `import { ${reducerPath} } from '${clientImportPath}/${groupName}';\n// PLOP_INJECT_IMPORT`, |
||||
}, |
||||
{ |
||||
type: 'modify', |
||||
path: path.join(basePath, 'public/app/core/reducers/root.ts'), |
||||
pattern: '// PLOP_INJECT_REDUCER', |
||||
template: `[${reducerPath}.reducerPath]: ${reducerPath}.reducer,\n // PLOP_INJECT_REDUCER`, |
||||
}, |
||||
{ |
||||
type: 'modify', |
||||
path: path.join(basePath, 'public/app/store/configureStore.ts'), |
||||
pattern: '// PLOP_INJECT_IMPORT', |
||||
template: `import { ${reducerPath} } from '${clientImportPath}/${groupName}';\n// PLOP_INJECT_IMPORT`, |
||||
}, |
||||
{ |
||||
type: 'modify', |
||||
path: path.join(basePath, 'public/app/store/configureStore.ts'), |
||||
pattern: '// PLOP_INJECT_MIDDLEWARE', |
||||
template: `${reducerPath}.middleware,\n // PLOP_INJECT_MIDDLEWARE`, |
||||
} |
||||
); |
||||
} |
||||
|
||||
// Add formatting and generation actions
|
||||
actions.push( |
||||
{ |
||||
type: 'formatFiles', |
||||
files: getFilesToFormat(groupName, isEnterprise), |
||||
}, |
||||
{ |
||||
type: 'runGenerateApis', |
||||
isEnterprise, |
||||
} |
||||
); |
||||
|
||||
return actions; |
||||
}; |
||||
|
||||
const generator: PlopGeneratorConfig = { |
||||
description: 'Generate RTK Query API client for a Grafana API group', |
||||
prompts: [ |
||||
{ |
||||
type: 'confirm', |
||||
name: 'isEnterprise', |
||||
message: 'Is this a Grafana Enterprise API?', |
||||
default: false, |
||||
}, |
||||
{ |
||||
type: 'input', |
||||
name: 'groupName', |
||||
message: 'API group name (e.g. dashboard):', |
||||
validate: (input: string) => (input?.trim() ? true : 'Group name is required'), |
||||
}, |
||||
{ |
||||
type: 'input', |
||||
name: 'group', |
||||
message: 'API group (e.g. dashboard.grafana.app):', |
||||
default: (answers: { groupName?: string }) => `${answers.groupName}.grafana.app`, |
||||
validate: validateGroup, |
||||
}, |
||||
{ |
||||
type: 'input', |
||||
name: 'version', |
||||
message: 'API version (e.g. v0alpha1):', |
||||
default: 'v0alpha1', |
||||
validate: validateVersion, |
||||
}, |
||||
{ |
||||
type: 'input', |
||||
name: 'reducerPath', |
||||
message: 'Reducer path (e.g. dashboardAPI):', |
||||
default: (answers: { groupName?: string }) => `${answers.groupName}API`, |
||||
validate: (input: string) => |
||||
input?.endsWith('API') ? true : 'Reducer path should end with "API" (e.g. dashboardAPI)', |
||||
}, |
||||
{ |
||||
type: 'input', |
||||
name: 'endpoints', |
||||
message: 'Endpoints to include (comma-separated, optional):', |
||||
validate: () => true, |
||||
}, |
||||
], |
||||
actions: function (data) { |
||||
if (!isPlopData(data)) { |
||||
throw new Error('Invalid data format received from prompts'); |
||||
} |
||||
|
||||
return generateRtkApiActions(data); |
||||
}, |
||||
}; |
||||
|
||||
plop.setGenerator('rtk-api-client', generator); |
||||
} |
@ -0,0 +1,14 @@ |
||||
import { createApi } from '@reduxjs/toolkit/query/react'; |
||||
|
||||
import { createBaseQuery } from 'app/api/createBaseQuery'; |
||||
import { getAPIBaseURL } from 'app/api/utils'; |
||||
|
||||
export const BASE_URL = getAPIBaseURL('{{group}}', '{{version}}'); |
||||
|
||||
export const api = createApi({ |
||||
reducerPath: '{{reducerPath}}', |
||||
baseQuery: createBaseQuery({ |
||||
baseURL: BASE_URL, |
||||
}), |
||||
endpoints: () => ({}), |
||||
}); |
@ -0,0 +1,9 @@ |
||||
'{{apiPathPrefix}}/{{groupName}}/endpoints.gen.ts': { |
||||
apiFile: '{{apiPathPrefix}}/{{groupName}}/baseAPI.ts', |
||||
schemaFile: '../data/openapi/{{group}}-{{version}}.json', |
||||
{{#if endpoints}} |
||||
filterEndpoints: [{{{formatEndpoints endpoints}}}], |
||||
{{/if}} |
||||
tag: true, |
||||
}, |
||||
// PLOP_INJECT_API_CLIENT - Used by the API client generator |
@ -0,0 +1,3 @@ |
||||
import { generatedAPI } from './endpoints.gen'; |
||||
|
||||
export const {{reducerPath}} = generatedAPI.enhanceEndpoints({}); |
@ -0,0 +1,27 @@ |
||||
import type { AddActionConfig, ModifyActionConfig } from 'plop'; |
||||
|
||||
export interface FormatFilesActionConfig { |
||||
type: 'formatFiles'; |
||||
files: string[]; |
||||
} |
||||
|
||||
export interface RunGenerateApisActionConfig { |
||||
type: 'runGenerateApis'; |
||||
isEnterprise: boolean; |
||||
} |
||||
|
||||
// Union type of all possible action configs
|
||||
export type ActionConfig = AddActionConfig | ModifyActionConfig | FormatFilesActionConfig | RunGenerateApisActionConfig; |
||||
|
||||
export interface PlopData { |
||||
groupName: string; |
||||
group: string; |
||||
version: string; |
||||
reducerPath: string; |
||||
endpoints: string; |
||||
isEnterprise: boolean; |
||||
} |
||||
|
||||
export function isPlopData(data: unknown): data is PlopData { |
||||
return typeof data === 'object' && data !== null; |
||||
} |
Loading…
Reference in new issue