fork of hey-api/openapi-ts because I need some additional things
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #1787 from hey-api/chore/config-platform

chore: add config options for platform api

authored by

Lubos and committed by
GitHub
318175cc 3a3ff618

+368 -53
+5
.changeset/itchy-plums-explain.md
··· 1 + --- 2 + '@hey-api/openapi-ts': patch 3 + --- 4 + 5 + fix: support Hey API platform input arguments
+5
.changeset/ten-spiders-act.md
··· 1 + --- 2 + '@hey-api/openapi-ts': patch 3 + --- 4 + 5 + fix: handle raw OpenAPI specification input
+83
packages/openapi-ts/src/__tests__/createClient.test.ts
··· 1 + import { describe, expect, it } from 'vitest'; 2 + 3 + import { compileInputPath } from '../createClient'; 4 + 5 + describe('compileInputPath', () => { 6 + it('with raw OpenAPI specification', () => { 7 + const path = compileInputPath({ 8 + path: { 9 + info: { 10 + version: '1.0.0', 11 + }, 12 + openapi: '3.1.0', 13 + }, 14 + }); 15 + expect(path).toEqual({ 16 + path: { 17 + info: { 18 + version: '1.0.0', 19 + }, 20 + openapi: '3.1.0', 21 + }, 22 + }); 23 + }); 24 + 25 + it('with arbitrary string', () => { 26 + const path = compileInputPath({ 27 + path: 'path/to/openapi.json', 28 + }); 29 + expect(path).toEqual({ 30 + path: 'path/to/openapi.json', 31 + }); 32 + }); 33 + 34 + it('with platform string', () => { 35 + const path = compileInputPath({ 36 + path: 'https://get.heyapi.dev/foo/bar?branch=main&commit_sha=sha&tags=a,b,c&version=1.0.0', 37 + }); 38 + expect(path).toEqual({ 39 + branch: 'main', 40 + commit_sha: 'sha', 41 + organization: 'foo', 42 + path: 'https://get.heyapi.dev/foo/bar?branch=main&commit_sha=sha&tags=a,b,c&version=1.0.0', 43 + project: 'bar', 44 + tags: ['a', 'b', 'c'], 45 + version: '1.0.0', 46 + }); 47 + }); 48 + 49 + it('with platform arguments', () => { 50 + const path = compileInputPath({ 51 + branch: 'main', 52 + commit_sha: 'sha', 53 + organization: 'foo', 54 + path: '', 55 + project: 'bar', 56 + tags: ['a', 'b', 'c'], 57 + version: '1.0.0', 58 + }); 59 + expect(path).toEqual({ 60 + branch: 'main', 61 + commit_sha: 'sha', 62 + organization: 'foo', 63 + path: 'https://get.heyapi.dev/foo/bar?branch=main&commit_sha=sha&tags=a,b,c&version=1.0.0', 64 + project: 'bar', 65 + tags: ['a', 'b', 'c'], 66 + version: '1.0.0', 67 + }); 68 + }); 69 + 70 + it('loads API key from HEY_API_TOKEN', () => { 71 + process.env.HEY_API_TOKEN = 'foo'; 72 + const path = compileInputPath({ 73 + path: 'https://get.heyapi.dev/foo/bar', 74 + }); 75 + delete process.env.HEY_API_TOKEN; 76 + expect(path).toEqual({ 77 + api_key: 'foo', 78 + organization: 'foo', 79 + path: 'https://get.heyapi.dev/foo/bar?api_key=foo', 80 + project: 'bar', 81 + }); 82 + }); 83 + });
+153 -12
packages/openapi-ts/src/createClient.ts
··· 13 13 import { Performance } from './utils/performance'; 14 14 import { postProcessClient } from './utils/postprocess'; 15 15 16 + const isPlatformPath = (path: string) => 17 + path.startsWith('https://get.heyapi.dev'); 18 + 19 + export const compileInputPath = (input: Config['input']) => { 20 + const result: Pick< 21 + Partial<Config['input']>, 22 + | 'api_key' 23 + | 'branch' 24 + | 'commit_sha' 25 + | 'organization' 26 + | 'project' 27 + | 'tags' 28 + | 'version' 29 + > & 30 + Pick<Required<Config['input']>, 'path'> = { 31 + path: '', 32 + }; 33 + 34 + if ( 35 + input.path && 36 + (typeof input.path !== 'string' || !isPlatformPath(input.path)) 37 + ) { 38 + result.path = input.path; 39 + return result; 40 + } 41 + 42 + const [basePath, baseQuery] = input.path.split('?'); 43 + const queryParts = (baseQuery || '').split('&'); 44 + const queryPath = queryParts.map((part) => part.split('=')); 45 + 46 + let path = basePath || ''; 47 + if (path.endsWith('/')) { 48 + path = path.slice(0, path.length - 1); 49 + } 50 + 51 + const [, pathUrl] = path.split('://'); 52 + const [baseUrl, organization, project] = (pathUrl || '').split('/'); 53 + result.organization = organization || input.organization; 54 + result.project = project || input.project; 55 + 56 + const queryParams: Array<string> = []; 57 + 58 + const kApiKey = 'api_key'; 59 + result.api_key = 60 + queryPath.find(([key]) => key === kApiKey)?.[1] || 61 + input.api_key || 62 + process.env.HEY_API_TOKEN; 63 + if (result.api_key) { 64 + queryParams.push(`${kApiKey}=${result.api_key}`); 65 + } 66 + 67 + const kBranch = 'branch'; 68 + result.branch = 69 + queryPath.find(([key]) => key === kBranch)?.[1] || input.branch; 70 + if (result.branch) { 71 + queryParams.push(`${kBranch}=${result.branch}`); 72 + } 73 + 74 + const kCommitSha = 'commit_sha'; 75 + result.commit_sha = 76 + queryPath.find(([key]) => key === kCommitSha)?.[1] || input.commit_sha; 77 + if (result.commit_sha) { 78 + queryParams.push(`${kCommitSha}=${result.commit_sha}`); 79 + } 80 + 81 + const kTags = 'tags'; 82 + result.tags = 83 + queryPath.find(([key]) => key === kTags)?.[1]?.split(',') || input.tags; 84 + if (result.tags?.length) { 85 + queryParams.push(`${kTags}=${result.tags.join(',')}`); 86 + } 87 + 88 + const kVersion = 'version'; 89 + result.version = 90 + queryPath.find(([key]) => key === kVersion)?.[1] || input.version; 91 + if (result.version) { 92 + queryParams.push(`${kVersion}=${result.version}`); 93 + } 94 + 95 + if (!result.organization) { 96 + throw new Error( 97 + '🚫 missing organization - from which Hey API platform organization do you want to generate your output?', 98 + ); 99 + } 100 + 101 + if (!result.project) { 102 + throw new Error( 103 + '🚫 missing project - from which Hey API platform project do you want to generate your output?', 104 + ); 105 + } 106 + 107 + const query = queryParams.join('&'); 108 + const platformUrl = baseUrl || 'get.heyapi.dev'; 109 + const compiledPath = `https://${[platformUrl, result.organization, result.project].join('/')}`; 110 + result.path = query ? `${compiledPath}?${query}` : compiledPath; 111 + 112 + return result; 113 + }; 114 + 115 + const logInputPath = ({ 116 + config, 117 + inputPath, 118 + watch, 119 + }: { 120 + config: Config; 121 + inputPath: ReturnType<typeof compileInputPath>; 122 + watch?: boolean; 123 + }) => { 124 + if (config.logs.level === 'silent') { 125 + return; 126 + } 127 + 128 + if (watch) { 129 + console.clear(); 130 + } 131 + 132 + const baseString = watch 133 + ? 'Input changed, generating from' 134 + : 'Generating from'; 135 + 136 + if (typeof inputPath.path === 'string') { 137 + const baseInput = isPlatformPath(inputPath.path) 138 + ? `${inputPath.organization}/${inputPath.project}` 139 + : inputPath.path; 140 + console.log(`⏳ ${baseString} ${baseInput}`); 141 + if (isPlatformPath(inputPath.path)) { 142 + if (inputPath.branch) { 143 + console.log(`branch: ${inputPath.branch}`); 144 + } 145 + if (inputPath.commit_sha) { 146 + console.log(`commit: ${inputPath.commit_sha}`); 147 + } 148 + if (inputPath.tags?.length) { 149 + console.log(`tags: ${inputPath.tags.join(', ')}`); 150 + } 151 + if (inputPath.version) { 152 + console.log(`version: ${inputPath.version}`); 153 + } 154 + } 155 + } else { 156 + console.log(`⏳ ${baseString} raw OpenAPI specification`); 157 + } 158 + }; 159 + 16 160 export const createClient = async ({ 17 161 config, 18 162 templates, ··· 22 166 templates: Templates; 23 167 watch?: WatchValues; 24 168 }) => { 25 - const inputPath = config.input.path; 169 + const inputPath = compileInputPath(config.input); 26 170 const timeout = config.watch.timeout; 27 171 28 172 const watch: WatchValues = _watch || { headers: new Headers() }; 29 173 174 + logInputPath({ 175 + config, 176 + inputPath, 177 + watch: Boolean(_watch), 178 + }); 179 + 30 180 Performance.start('spec'); 31 181 const { data, error, response } = await getSpec({ 32 - inputPath, 182 + inputPath: inputPath.path, 33 183 timeout, 34 184 watch, 35 185 }); ··· 48 198 let context: IR.Context | undefined; 49 199 50 200 if (data) { 51 - if (config.logs.level !== 'silent') { 52 - if (_watch) { 53 - console.clear(); 54 - console.log(`⏳ Input changed, generating from ${inputPath}`); 55 - } else { 56 - console.log(`⏳ Generating from ${inputPath}`); 57 - } 58 - } 59 - 60 201 Performance.start('parser'); 61 202 if ( 62 203 config.experimentalParser && ··· 95 236 Performance.end('postprocess'); 96 237 } 97 238 98 - if (config.watch.enabled && typeof inputPath === 'string') { 239 + if (config.watch.enabled && typeof inputPath.path === 'string') { 99 240 setTimeout(() => { 100 241 createClient({ config, templates, watch }); 101 242 }, config.watch.interval);
+9 -1
packages/openapi-ts/src/getSpec.ts
··· 30 30 watch: WatchValues; 31 31 }): Promise<SpecResponse | SpecError> => { 32 32 const refParser = new $RefParser(); 33 - const resolvedInput = getResolvedInput({ pathOrUrlOrSchema: inputPath }); 33 + // TODO: patch @hey-api/json-schema-ref-parser to correctly handle raw spec 34 + const resolvedInput = 35 + typeof inputPath === 'string' 36 + ? getResolvedInput({ pathOrUrlOrSchema: inputPath }) 37 + : ({ 38 + path: '', 39 + schema: inputPath, 40 + type: 'json', 41 + } as const); 34 42 35 43 let arrayBuffer: ArrayBuffer | undefined; 36 44 // boolean signals whether the file has **definitely** changed
+1 -1
packages/openapi-ts/src/index.test.ts packages/openapi-ts/src/__tests__/index.test.ts
··· 1 1 import { describe, it } from 'vitest'; 2 2 3 - import { createClient } from './index'; 3 + import { createClient } from '../index'; 4 4 5 5 describe('index', () => { 6 6 it('parses v2 without issues', async () => {
+6 -2
packages/openapi-ts/src/initConfigs.ts
··· 28 28 }; 29 29 if (typeof userConfig.input === 'string') { 30 30 input.path = userConfig.input; 31 - } else if (userConfig.input && userConfig.input.path) { 31 + } else if ( 32 + userConfig.input && 33 + (userConfig.input.path || userConfig.input.organization) 34 + ) { 32 35 input = { 33 36 ...input, 37 + path: 'https://get.heyapi.dev', 34 38 ...userConfig.input, 35 39 }; 36 40 } else { 37 41 input = { 38 42 ...input, 39 - path: userConfig.input, 43 + path: userConfig.input as Record<string, unknown>, 40 44 }; 41 45 } 42 46 return input;
+91 -36
packages/openapi-ts/src/types/config.d.ts
··· 1 1 import type { ClientPlugins, UserPlugins } from '../plugins'; 2 - import type { 3 - ArrayOfObjectsToObjectMap, 4 - ExtractArrayOfObjects, 5 - ExtractWithDiscriminator, 6 - } from './utils'; 2 + import type { ArrayOfObjectsToObjectMap, ExtractArrayOfObjects } from './utils'; 7 3 8 4 export type Formatters = 'biome' | 'prettier'; 9 5 ··· 16 12 | 'snake_case' 17 13 | 'SCREAMING_SNAKE_CASE'; 18 14 15 + interface Input { 16 + /** 17 + * **Requires `path` to start with `https://get.heyapi.dev` or be undefined** 18 + * 19 + * Projects are private by default, you will need to be authenticated 20 + * to download OpenAPI specifications. We recommend using project API 21 + * keys in CI workflows and personal API keys for local development. 22 + * 23 + * API key isn't required for public projects. You can also omit this 24 + * parameter and provide an environment variable `HEY_API_TOKEN`. 25 + */ 26 + api_key?: string; 27 + /** 28 + * **Requires `path` to start with `https://get.heyapi.dev` or be undefined** 29 + * 30 + * You can fetch the last build from branch by providing the branch 31 + * name. 32 + */ 33 + branch?: string; 34 + /** 35 + * **Requires `path` to start with `https://get.heyapi.dev` or be undefined** 36 + * 37 + * You can fetch an exact specification by providing a commit sha. 38 + * This will always return the same file. 39 + */ 40 + commit_sha?: string; 41 + /** 42 + * Prevent parts matching the regular expression from being processed. 43 + * You can select both operations and components by reference within 44 + * the bundled input. In case of conflicts, `exclude` takes precedence 45 + * over `include`. 46 + * 47 + * @example 48 + * operation: '^#/paths/api/v1/foo/get$' 49 + * schema: '^#/components/schemas/Foo$' 50 + */ 51 + exclude?: string; 52 + /** 53 + * Process only parts matching the regular expression. You can select both 54 + * operations and components by reference within the bundled input. In 55 + * case of conflicts, `exclude` takes precedence over `include`. 56 + * 57 + * @example 58 + * operation: '^#/paths/api/v1/foo/get$' 59 + * schema: '^#/components/schemas/Foo$' 60 + */ 61 + include?: string; 62 + /** 63 + * **Requires `path` to start with `https://get.heyapi.dev` or be undefined** 64 + * 65 + * Organization created in Hey API platform. 66 + */ 67 + organization?: string; 68 + /** 69 + * Path to the OpenAPI specification. This can be either local or remote path. 70 + * Both JSON and YAML file formats are supported. You can also pass the parsed 71 + * object directly if you're fetching the file yourself. 72 + */ 73 + path?: 74 + | 'https://get.heyapi.dev/<organization>/<project>' 75 + | (string & {}) 76 + | Record<string, unknown>; 77 + /** 78 + * **Requires `path` to start with `https://get.heyapi.dev` or be undefined** 79 + * 80 + * Project created in Hey API platform. 81 + */ 82 + project?: string; 83 + /** 84 + * **Requires `path` to start with `https://get.heyapi.dev` or be undefined** 85 + * 86 + * If you're tagging your specifications with custom tags, you can use 87 + * them to filter the results. When you provide multiple tags, only 88 + * the first match will be returned. 89 + */ 90 + tags?: ReadonlyArray<string>; 91 + /** 92 + * **Requires `path` to start with `https://get.heyapi.dev` or be undefined** 93 + * 94 + * Every OpenAPI document contains a required version field. You can 95 + * use this value to fetch the last uploaded specification matching 96 + * the value. 97 + */ 98 + version?: string; 99 + } 100 + 19 101 export interface UserConfig { 20 102 /** 21 103 * Path to the config file. Set this value if you don't use the default ··· 36 118 * Alternatively, you can define a configuration object with more options. 37 119 */ 38 120 input: 39 - | string 121 + | 'https://get.heyapi.dev/<organization>/<project>' 122 + | (string & {}) 40 123 | Record<string, unknown> 41 - | { 42 - /** 43 - * Prevent parts matching the regular expression from being processed. 44 - * You can select both operations and components by reference within 45 - * the bundled input. In case of conflicts, `exclude` takes precedence 46 - * over `include`. 47 - * 48 - * @example 49 - * operation: '^#/paths/api/v1/foo/get$' 50 - * schema: '^#/components/schemas/Foo$' 51 - */ 52 - exclude?: string; 53 - /** 54 - * Process only parts matching the regular expression. You can select both 55 - * operations and components by reference within the bundled input. In 56 - * case of conflicts, `exclude` takes precedence over `include`. 57 - * 58 - * @example 59 - * operation: '^#/paths/api/v1/foo/get$' 60 - * schema: '^#/components/schemas/Foo$' 61 - */ 62 - include?: string; 63 - /** 64 - * Path to the OpenAPI specification. This can be either local or remote path. 65 - * Both JSON and YAML file formats are supported. You can also pass the parsed 66 - * object directly if you're fetching the file yourself. 67 - */ 68 - path: string | Record<string, unknown>; 69 - }; 124 + | Input; 70 125 /** 71 126 * The relative location of the logs folder 72 127 * ··· 257 312 | 'watch' 258 313 > & 259 314 Pick<UserConfig, 'base' | 'name' | 'request'> & { 260 - input: ExtractWithDiscriminator<UserConfig['input'], { path: unknown }>; 315 + input: Omit<Input, 'path'> & Pick<Required<Input>, 'path'>; 261 316 logs: Extract<Required<UserConfig['logs']>, object>; 262 317 output: Extract<UserConfig['output'], object>; 263 318 pluginOrder: ReadonlyArray<ClientPlugins['name']>;
+15 -1
packages/openapi-ts/test/openapi-ts.config.ts
··· 5 5 export default defineConfig({ 6 6 // experimentalParser: false, 7 7 input: { 8 + // branch: 'main', 8 9 // exclude: '^#/components/schemas/ModelWithCircularReference$', 9 10 // include: 10 11 // '^(#/components/schemas/import|#/paths/api/v{api-version}/simple/options)$', 11 - path: path.resolve(__dirname, 'spec', '3.1.x', 'full.json'), 12 + // organization: 'hey-api', 13 + path: { 14 + components: {}, 15 + info: { 16 + version: '1.0.0', 17 + }, 18 + openapi: '3.1.0', 19 + paths: {}, 20 + }, 21 + // path: path.resolve(__dirname, 'spec', '3.1.x', 'full.json'), 22 + // path: 'https://get.heyapi.dev/', 23 + // path: 'https://get.heyapi.dev/hey-api/backend?branch=main&version=1.0.0', 12 24 // path: 'http://localhost:8000/openapi.json', 13 25 // path: './test/spec/v3-transforms.json', 14 26 // path: 'https://mongodb-mms-prod-build-server.s3.amazonaws.com/openapi/2caffd88277a4e27c95dcefc7e3b6a63a3b03297-v2-2023-11-15.json', 15 27 // path: 'https://raw.githubusercontent.com/swagger-api/swagger-petstore/master/src/main/resources/openapi.yaml', 28 + // project: 'backend', 29 + // version: '1.0.0', 16 30 }, 17 31 logs: { 18 32 // level: 'debug',