api 5.0.0-beta.0 â 5.0.0-beta.3
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 +31 -161
- package/dist/cache.d.ts +1 -1
- package/dist/cli/codegen/languages/typescript.d.ts +8 -6
- package/dist/cli/codegen/languages/typescript.js +98 -23
- package/dist/cli/storage.d.ts +1 -1
- package/dist/core/getJSONSchemaDefaults.d.ts +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/prepareParams.js +43 -6
- package/dist/fetcher.d.ts +2 -1
- package/dist/fetcher.js +20 -4
- package/dist/index.d.ts +1 -1
- package/dist/packageInfo.d.ts +1 -1
- package/dist/packageInfo.js +1 -1
- package/package.json +17 -14
- package/src/cache.ts +1 -1
- package/src/cli/codegen/languages/typescript.ts +113 -34
- package/src/cli/storage.ts +1 -1
- package/src/core/getJSONSchemaDefaults.ts +2 -2
- package/src/core/index.ts +1 -1
- package/src/core/prepareParams.ts +45 -7
- package/src/fetcher.ts +20 -5
- package/src/index.ts +1 -1
- package/src/packageInfo.ts +1 -1
package/README.md
CHANGED
|
@@ -1,179 +1,49 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img width="400" src="https://raw.githubusercontent.com/readmeio/api/main/docs/images/logo.svg" />
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<p align="center">
|
|
6
|
+
Magical SDK generation from an OpenAPI definition đĒ
|
|
7
|
+
</p>
|
|
4
8
|
|
|
5
|
-
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="https://npm.im/api"><img src="https://img.shields.io/npm/v/api.svg?style=for-the-badge" alt="NPM Version"></a>
|
|
11
|
+
<a href="https://npm.im/api"><img src="https://img.shields.io/node/v/api.svg?style=for-the-badge" alt="Node Version"></a>
|
|
12
|
+
<a href="https://npm.im/api"><img src="https://img.shields.io/npm/l/api.svg?style=for-the-badge" alt="MIT License"></a>
|
|
13
|
+
<a href="https://github.com/readmeio/api"><img src="https://img.shields.io/github/workflow/status/readmeio/api/CI.svg?style=for-the-badge" alt="Build status"></a>
|
|
14
|
+
</p>
|
|
6
15
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
* [FAQ](#faq)
|
|
16
|
+
- [Installation](https://api.readme.dev/docs/installation)
|
|
17
|
+
- [Usage](https://api.readme.dev/docs/usage)
|
|
18
|
+
- [Authentication](https://api.readme.dev/docs/authentication)
|
|
19
|
+
- [Parameters and Payloads](https://api.readme.dev/docs/parameters-and-payloads)
|
|
20
|
+
- [HTTP requests](https://api.readme.dev/docs/http-requests)
|
|
21
|
+
- [Server configurations](https://api.readme.dev/docs/server-configurations)
|
|
22
|
+
- [How does it work?](https://api.readme.dev/docs/how-it-works)
|
|
23
|
+
- [FAQ](https://api.readme.dev/docs/faq)
|
|
16
24
|
|
|
17
|
-
|
|
18
|
-
```
|
|
19
|
-
npm install api --save
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
## Usage
|
|
23
|
-
All you need to use `api` is to supply it an OpenAPI definition and then use the SDK as you would any other!
|
|
24
|
-
|
|
25
|
-
```js
|
|
26
|
-
const sdk = require('api')('https://raw.githubusercontent.com/readmeio/oas-examples/main/3.0/json/petstore.json');
|
|
27
|
-
|
|
28
|
-
sdk.listPets().then(res => {
|
|
29
|
-
console.log(`My pets name is ${res[0].name}!`);
|
|
30
|
-
});
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
The OpenAPI definition is automatically downloaded, cached, and transformed into a chainable [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) Promise that you can use to make API requests.
|
|
34
|
-
|
|
35
|
-
### Authentication
|
|
36
|
-
`api` supports API authentication through an `.auth()` method:
|
|
37
|
-
|
|
38
|
-
```js
|
|
39
|
-
sdk.auth('myApiToken');
|
|
40
|
-
sdk.listPets().then(...);
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
With the exception of OpenID, it supports all forms of authentication supported by the OpenAPI specification! Supply `.auth()` with your auth credentials and it'll magically figure out how to use it according to the API you're using. đ§ââī¸
|
|
44
|
-
|
|
45
|
-
For example:
|
|
46
|
-
|
|
47
|
-
* HTTP Basic auth: `sdk.auth('username', 'password')`
|
|
48
|
-
* Bearer tokens (HTTP or OAuth 2): `sdk.auth('myBearerToken')`
|
|
49
|
-
* API Keys: `sdk.auth('myApiKey')`
|
|
50
|
-
|
|
51
|
-
> âšī¸ Note that `sdk.auth()` is not chainable.
|
|
25
|
+
`api` is a library that facilitates creating an SDK from an OpenAPI definition. You can use its codegen offering to create an opinionated SDK for TypeScript or JS (+ TypeScript types).
|
|
52
26
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
* `body`: This will contain all data required for a request body payload for a POST, PUT, etc. request. It can either be an array or an object â whichever you need to use the API operation you're using.
|
|
57
|
-
* `metadata`: This is an object where all parameters (path, query, header, cookie) go. Again, don't worry about telling the SDK that a path parameter is for the path, that's all handled for you.
|
|
58
|
-
|
|
59
|
-
For example, if you wanted to make a GET request:
|
|
60
|
-
|
|
61
|
-
```js
|
|
62
|
-
sdk.showPetById({ petId: 1234 }).then(...)
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
Since `petId` matches up with the `petId` path parameter, the SDK here will issue a GET request against `/pets/1234`.
|
|
66
|
-
|
|
67
|
-
What about a POST request?
|
|
68
|
-
|
|
69
|
-
```js
|
|
70
|
-
sdk.createPets({ name: 'Buster' }).then(...)
|
|
27
|
+
```sh
|
|
28
|
+
$ npx api install https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/examples/v3.0/petstore.json
|
|
71
29
|
```
|
|
72
30
|
|
|
73
|
-
Since `name` here would correspond on `createPets` to request body payload, this will issue a POST request against `/pets` to make a new pet named "Buster".
|
|
74
|
-
|
|
75
|
-
What about operations that require both? Well you can mix them too!
|
|
76
|
-
|
|
77
31
|
```js
|
|
78
|
-
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
Since we've supplied two objects here, the SDK automatically knows that you're supplying both a `body` and `metadata`, and can make a PUT request against `/pets/1234` for you.
|
|
82
|
-
|
|
83
|
-
What about a `multipart/form-data` request? That works too, and you don't even have to worry about the fun of multipart boundaries!
|
|
84
|
-
|
|
85
|
-
```js
|
|
86
|
-
sdk.uploadFile({ file: '/path/to/a/file.txt' }).then(...)
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
You can also give it a stream and it'll handle all of the hard work for you.
|
|
32
|
+
const SDK = require('@api/petstore');
|
|
90
33
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
### HTTP requests
|
|
96
|
-
If the API you're using doesn't have any documented operation IDs, you can make requests with HTTP verbs instead:
|
|
97
|
-
|
|
98
|
-
```js
|
|
99
|
-
sdk.get('/pets/{petId}', { petId: 1234 }).then(...)
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
The SDK supports GET, PUT, POST, DELETE, OPTIONS, HEAD, and TRACE requests.
|
|
103
|
-
|
|
104
|
-
### Server configurations
|
|
105
|
-
If the API you're using offers alternate server URLs and server variables in its [`servers`](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#serverObject) definition you can supply this to the SDK with `.server()`:
|
|
106
|
-
|
|
107
|
-
```js
|
|
108
|
-
sdk.server('https://{region}.api.example.com/{basePath}', {
|
|
109
|
-
name: 'eu',
|
|
110
|
-
basePath: 'v14',
|
|
34
|
+
petstore.listPets().then(res => {
|
|
35
|
+
console.log(`My pets name is ${res[0].name}!`);
|
|
111
36
|
});
|
|
112
|
-
|
|
113
|
-
sdk.get('/pets').then(...)
|
|
114
37
|
```
|
|
115
38
|
|
|
116
|
-
|
|
39
|
+
Or you can use it dynamically (though you won't have fancy TypeScript types):
|
|
117
40
|
|
|
118
41
|
```js
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
## How does it work?
|
|
123
|
-
Behind the scenes, `api` will:
|
|
124
|
-
|
|
125
|
-
1. Download the supplied OpenAPI definition, either from a publically accessible URLs or an absolute/relative path.
|
|
126
|
-
2. Dereference the definition so it's easier for us to handle.
|
|
127
|
-
3. Hash the definition and cache it into a directory in `node_modules/.cache/api/`.
|
|
128
|
-
4. Process the definition and instantiate chainable methods for HTTP verbs and operation IDs the API contains via a JS [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy).
|
|
129
|
-
|
|
130
|
-
On subsequent requests, `api` will look in its cache, and if the supplied definition exists there, it'll retrieve it from the cache instead of re-retrieving it again.
|
|
131
|
-
|
|
132
|
-
## Interested in contributing?
|
|
133
|
-
Welcome! Have a look at [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
134
|
-
|
|
135
|
-
## FAQ
|
|
136
|
-
#### Does this support YAML definitions?
|
|
137
|
-
Yes! YAML definitions will be automatically converted to JSON before they're cached and loaded as an SDK.
|
|
42
|
+
const petstore = require('api')(
|
|
43
|
+
'https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/examples/v3.0/petstore.json'
|
|
44
|
+
);
|
|
138
45
|
|
|
139
|
-
|
|
140
|
-
At the moment it does not. If you wish to use an API that has a Swagger 2.0 file, you'll need to first convert it to an OpenAPI 3 definition.
|
|
141
|
-
|
|
142
|
-
#### Does this support traditional OAuth 2 flows of creating tokens?
|
|
143
|
-
Not yet, unfortunately. For APIs that use OAuth 2, you'll need a fully-qualified token already for `api` to make requests.
|
|
144
|
-
|
|
145
|
-
#### Does this support APIs that use multiple forms of authentication on a single request?
|
|
146
|
-
Not yet! This is something we're thinking about how to handle, but it's difficult with the simplified nature of the `.auth()` method as it currently does not require the user to inform the SDK of what kind of authentication scheme the token they're supplying it should match up against.
|
|
147
|
-
|
|
148
|
-
#### Will this work in browsers?
|
|
149
|
-
Not at the moment as the library requires some filesystem handling in order to manage its cache state, but it's something we're actively thinking about. If you'd like to help us out in making this compatible with browsers we'd love to help you out on a pull request.
|
|
150
|
-
|
|
151
|
-
#### Will this validate my data before it reaches the API?
|
|
152
|
-
Not yet! This is something we've got planned down the road.
|
|
153
|
-
|
|
154
|
-
#### Does this support OpenAPI definitions that require authentication to download?
|
|
155
|
-
Not yet! The URL that you give the module must be publicy accessible. If it isn't, you can download it to your computer/server and then use the absolute path to that file instead.
|
|
156
|
-
|
|
157
|
-
```js
|
|
158
|
-
const sdk = require('api')('/path/to/downloaded.json');
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
#### How do I access the Response object (for status and headers)?
|
|
162
|
-
By default we parse the response based on the `content-type` header for you. You can disable this by doing the following:
|
|
163
|
-
|
|
164
|
-
```js
|
|
165
|
-
sdk.config({ parseResponse: false });
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
#### Where is the cache stored?
|
|
169
|
-
|
|
170
|
-
By default the cache is configured with the [find-cache-dir](https://npm.im/find-cache-dir) library so the cache will be in `node_modules/.cache/api`. If placing this cache within the `node_modules/` directory is a problem for your environment (maybe you use `npm prune`) you can configure this by supplying an additional argument to the SDK instantiator:
|
|
171
|
-
|
|
172
|
-
```js
|
|
173
|
-
const sdk = require('api')('https://raw.githubusercontent.com/readmeio/oas-examples/main/3.0/json/petstore.json', {
|
|
174
|
-
cacheDir: './path/to/my/custom/cache/dir',
|
|
175
|
-
});
|
|
176
|
-
sdk.listPets().then(res => {
|
|
46
|
+
petstore.listPets().then(res => {
|
|
177
47
|
console.log(`My pets name is ${res[0].name}!`);
|
|
178
48
|
});
|
|
179
49
|
```
|
package/dist/cache.d.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import type Oas from 'oas';
|
|
2
2
|
import type { Operation } from 'oas';
|
|
3
|
-
import type { JSONSchema, SchemaObject } from 'oas
|
|
4
|
-
import type { ClassDeclaration, MethodDeclaration } from 'ts-morph';
|
|
3
|
+
import type { JSONSchema, SchemaObject } from 'oas/dist/rmoas.types';
|
|
4
|
+
import type { ClassDeclaration, MethodDeclaration, VariableStatement } from 'ts-morph';
|
|
5
5
|
import type Storage from '../../storage';
|
|
6
6
|
import type { InstallerOptions } from '../language';
|
|
7
7
|
import CodeGeneratorLanguage from '../language';
|
|
8
8
|
import { Project } from 'ts-morph';
|
|
9
|
+
export declare type TSGeneratorOptions = {
|
|
10
|
+
outputJS?: boolean;
|
|
11
|
+
compilerTarget?: 'cjs' | 'esm';
|
|
12
|
+
};
|
|
9
13
|
declare type OperationTypeHousing = {
|
|
10
14
|
types: {
|
|
11
15
|
params?: false | Record<'body' | 'formData' | 'metadata', string>;
|
|
@@ -21,15 +25,13 @@ export default class TSGenerator extends CodeGeneratorLanguage {
|
|
|
21
25
|
files: Record<string, string>;
|
|
22
26
|
methodGenerics: Map<string, MethodDeclaration>;
|
|
23
27
|
sdk: ClassDeclaration;
|
|
28
|
+
sdkExport: VariableStatement;
|
|
24
29
|
schemas: Map<string, {
|
|
25
30
|
schema: SchemaObject;
|
|
26
31
|
name: string;
|
|
27
32
|
tsType?: string;
|
|
28
33
|
}>;
|
|
29
|
-
constructor(spec: Oas, specPath: string, identifier: string, opts?:
|
|
30
|
-
outputJS?: boolean;
|
|
31
|
-
compilerTarget?: 'cjs' | 'esm';
|
|
32
|
-
});
|
|
34
|
+
constructor(spec: Oas, specPath: string, identifier: string, opts?: TSGeneratorOptions);
|
|
33
35
|
static formatter(content: string): string;
|
|
34
36
|
installer(storage: Storage, opts?: InstallerOptions): Promise<void>;
|
|
35
37
|
/**
|
|
@@ -114,7 +114,7 @@ var TSGenerator = /** @class */ (function (_super) {
|
|
|
114
114
|
indentationText: ts_morph_1.IndentationText.TwoSpaces,
|
|
115
115
|
quoteKind: ts_morph_1.QuoteKind.Single
|
|
116
116
|
},
|
|
117
|
-
compilerOptions: __assign({ declaration: true, resolveJsonModule: true, target: options.compilerTarget === 'cjs' ? ts_morph_1.ScriptTarget.ES5 : ts_morph_1.ScriptTarget.ES2020
|
|
117
|
+
compilerOptions: __assign({ declaration: true, outDir: 'dist', resolveJsonModule: true, target: options.compilerTarget === 'cjs' ? ts_morph_1.ScriptTarget.ES5 : ts_morph_1.ScriptTarget.ES2020 }, (options.compilerTarget === 'cjs' ? { esModuleInterop: true } : {}))
|
|
118
118
|
});
|
|
119
119
|
_this.compilerTarget = options.compilerTarget;
|
|
120
120
|
_this.outputJS = options.outputJS;
|
|
@@ -195,24 +195,57 @@ var TSGenerator = /** @class */ (function (_super) {
|
|
|
195
195
|
this.sdk = sdkSource.addClass({
|
|
196
196
|
name: 'SDK'
|
|
197
197
|
});
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
198
|
+
this.sdkExport = sdkSource.addVariableStatement({
|
|
199
|
+
declarationKind: ts_morph_1.VariableDeclarationKind.Const,
|
|
200
|
+
declarations: [
|
|
201
|
+
{
|
|
202
|
+
name: 'createSDK',
|
|
203
|
+
initializer: function (writer) {
|
|
204
|
+
// `ts-morph` doesn't have any way to cleanly create an IFEE.
|
|
205
|
+
writer.writeLine('(() => { return new SDK(); })()');
|
|
206
|
+
return writer;
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
]
|
|
210
|
+
});
|
|
211
|
+
/**
|
|
212
|
+
* There's an annoying quirk with `ts-morph` where if we set the `createSDK` function to be the
|
|
213
|
+
* default export with `isDefaultExport` then when we compile it to an ES5 target for CJS
|
|
214
|
+
* environments it'll be exported as `export.default = createSDK`, which when you try to load it
|
|
215
|
+
* you'll need to run `require('@api/sdk').default`.
|
|
216
|
+
*
|
|
217
|
+
* Instead here by plainly creating `createSDK` in the source file and then setting this export
|
|
218
|
+
* assignment it'll export the SDK IFEE initializer as `module.exports = createSDK` so people
|
|
219
|
+
* can cleanly load their SDK with `require('@api/sdk)`.
|
|
220
|
+
*
|
|
221
|
+
* A whole lot of debugging went into here to let people not have to worry about `.default`
|
|
222
|
+
* messes. I hope it's worth it!
|
|
223
|
+
*/
|
|
209
224
|
if (this.compilerTarget === 'cjs') {
|
|
210
225
|
sdkSource.addExportAssignment({
|
|
211
|
-
expression: '
|
|
226
|
+
expression: 'createSDK'
|
|
212
227
|
});
|
|
213
228
|
}
|
|
214
229
|
else {
|
|
215
|
-
|
|
230
|
+
/**
|
|
231
|
+
* Because `createSDK` above is an IFEE constant we can't use `setIsDefaultExport` on it due
|
|
232
|
+
* to `ts-morph` not having great handling for IFEE's.
|
|
233
|
+
*
|
|
234
|
+
* If we were to call `setIsDefaultExport` on our IFEE to attempt to compile it as
|
|
235
|
+
* `export default createSDK` then `ts-morph` hard crashes with a "Error replacing tree: The
|
|
236
|
+
* children of the old and new trees were expected to have the same count" exception due to
|
|
237
|
+
* it not being able properly handle IFEE's. It's for that reason that we need to manually
|
|
238
|
+
* write a statement expression to set `createSDK` as the default export.
|
|
239
|
+
*
|
|
240
|
+
* Another quirk that this work avoids is there being an empty `export {};` at the very end
|
|
241
|
+
* of our compiled `d.ts` declaration file. I'm not sure why it was being added, and it
|
|
242
|
+
* didn't appear to be harming anything, but us manually creating this export statement
|
|
243
|
+
* causes it to go away.
|
|
244
|
+
*
|
|
245
|
+
* Thankfully, fortunately, and curiously, these are all only problems in non-CJS compiled
|
|
246
|
+
* targets. ¯\_(ã)_/¯
|
|
247
|
+
*/
|
|
248
|
+
sdkSource.addStatements('export default createSDK');
|
|
216
249
|
}
|
|
217
250
|
this.sdk.addProperties([
|
|
218
251
|
{ name: 'spec', type: 'Oas' },
|
|
@@ -313,7 +346,42 @@ var TSGenerator = /** @class */ (function (_super) {
|
|
|
313
346
|
// @todo should all of these isolated into their own file outside of the main sdk class file?
|
|
314
347
|
// Add all known types that we're using into the SDK.
|
|
315
348
|
Array.from(this.types.values()).forEach(function (exp) {
|
|
316
|
-
|
|
349
|
+
/**
|
|
350
|
+
* When `ts-morph` compiles declaration files when we're targeting CJS environments it creates
|
|
351
|
+
* the default export as `export = _default` instead of `export default const _default`. This
|
|
352
|
+
* causes TS to throw a TS2309 error for "An export assignment cannot be used in a module
|
|
353
|
+
* with other exported elements" because our types and interfaces are also being exported and
|
|
354
|
+
* the `export =` overrides those.
|
|
355
|
+
*
|
|
356
|
+
* Fixing this is, to be frank, a fucking HARD problem for a couple reasons:
|
|
357
|
+
*
|
|
358
|
+
* 1. Our JSON Schema types and interfaces are coming from `json-schema-to-typescript` and
|
|
359
|
+
* that library exports its data a raw string containing multiple types and interfaces.
|
|
360
|
+
* The only way we're able to capture and use them in our codegenerated SDK is because
|
|
361
|
+
* we're ingesting that string into `ts-morph` and then using its APIs to extract exported
|
|
362
|
+
* declarations (which are still strings) and then they're re-inserted into our main
|
|
363
|
+
* source file here.
|
|
364
|
+
* 2. Though `ts-morph` has APIs for adding type aliases and interfaces to a source file what
|
|
365
|
+
* it doesn't have is the ability to pass in a string, or a `Writer` class that exposes,
|
|
366
|
+
* to write raw strings to a type or an interface. If it did we'd be able to replace this
|
|
367
|
+
* `addStatements` call with an `addTypeAlias` and `addInterface` call for each of our
|
|
368
|
+
* JSON Schema schemas that we've got along with an `isExported` flag for `ts-morph` to
|
|
369
|
+
* export it.
|
|
370
|
+
*
|
|
371
|
+
* Because neither of these are solvable problems right now we're instead opting to **not**
|
|
372
|
+
* export types and interfaces from these SDKs. This isn't a great solution because it
|
|
373
|
+
* /slightly/ reduces the usability of the TS codegen functionality but in order for the TS
|
|
374
|
+
* declaration files that we generate to be valid this is the only option that we've got.
|
|
375
|
+
*
|
|
376
|
+
* However, that said, if somebody needs an interface or type exported they can export it
|
|
377
|
+
* themselves in the SDK code that we compile for them.
|
|
378
|
+
*
|
|
379
|
+
* @fixme
|
|
380
|
+
*/
|
|
381
|
+
sdkSource.addStatements(
|
|
382
|
+
// All expressions coming out of `json-schema-to-typescript` are exported so by popping this
|
|
383
|
+
// off we'll just be inserting plain interfaces and types into the SDK source.
|
|
384
|
+
exp.substring('export '.length));
|
|
317
385
|
});
|
|
318
386
|
if (this.outputJS) {
|
|
319
387
|
return [2 /*return*/, this.project
|
|
@@ -354,7 +422,7 @@ var TSGenerator = /** @class */ (function (_super) {
|
|
|
354
422
|
var parameters = [{ name: 'path', type: 'string' }];
|
|
355
423
|
var docblock = {
|
|
356
424
|
description: function (writer) {
|
|
357
|
-
writer.writeLine("Access any ".concat(method, " endpoint on your API."));
|
|
425
|
+
writer.writeLine("Access any ".concat(method.toUpperCase(), " endpoint on your API."));
|
|
358
426
|
return writer;
|
|
359
427
|
},
|
|
360
428
|
tags: [{ tagName: 'param', text: 'path API path to make a request against.' }]
|
|
@@ -540,7 +608,7 @@ var TSGenerator = /** @class */ (function (_super) {
|
|
|
540
608
|
this.methodGenerics.get(operation.method).addOverload({
|
|
541
609
|
typeParameters: typeParameters,
|
|
542
610
|
parameters: [
|
|
543
|
-
{ name: 'path', type: '
|
|
611
|
+
{ name: 'path', type: "'".concat(operation.path, "'") },
|
|
544
612
|
__assign(__assign({}, parameters.body), { hasQuestionToken: false }),
|
|
545
613
|
__assign(__assign({}, parameters.metadata), { hasQuestionToken: false }),
|
|
546
614
|
],
|
|
@@ -550,7 +618,7 @@ var TSGenerator = /** @class */ (function (_super) {
|
|
|
550
618
|
// Create an overload that just has a single `metadata` parameter.
|
|
551
619
|
this.methodGenerics.get(operation.method).addOverload({
|
|
552
620
|
typeParameters: typeParameters,
|
|
553
|
-
parameters: [{ name: 'path', type: '
|
|
621
|
+
parameters: [{ name: 'path', type: "'".concat(operation.path, "'") }, parameters.metadata],
|
|
554
622
|
returnType: returnType,
|
|
555
623
|
docs: docblock ? [docblock] : null
|
|
556
624
|
});
|
|
@@ -558,7 +626,7 @@ var TSGenerator = /** @class */ (function (_super) {
|
|
|
558
626
|
else {
|
|
559
627
|
this.methodGenerics.get(operation.method).addOverload({
|
|
560
628
|
typeParameters: responseTypes ? null : ['T = unknown'],
|
|
561
|
-
parameters: __spreadArray([{ name: 'path', type: '
|
|
629
|
+
parameters: __spreadArray([{ name: 'path', type: "'".concat(operation.path, "'") }], Object.values(parameters), true),
|
|
562
630
|
returnType: returnType,
|
|
563
631
|
docs: docblock ? [docblock] : null
|
|
564
632
|
});
|
|
@@ -629,11 +697,15 @@ var TSGenerator = /** @class */ (function (_super) {
|
|
|
629
697
|
Object.entries(ops).forEach(function (_a) {
|
|
630
698
|
var method = _a[0], operation = _a[1];
|
|
631
699
|
methods.add(method);
|
|
632
|
-
var operationId = operation.getOperationId(
|
|
700
|
+
var operationId = operation.getOperationId({
|
|
701
|
+
// This `camelCase` option will clean up any weird characters that might be present in
|
|
702
|
+
// the `operationId` so as we don't break TS compilation with an invalid method accessor.
|
|
703
|
+
camelCase: true
|
|
704
|
+
});
|
|
633
705
|
var params = _this.prepareParameterTypesForOperation(operation, operationId);
|
|
634
706
|
var responses = _this.prepareResponseTypesForOperation(operation, operationId);
|
|
635
707
|
if (operation.hasOperationId()) {
|
|
636
|
-
operations[
|
|
708
|
+
operations[operationId] = {
|
|
637
709
|
types: {
|
|
638
710
|
params: params,
|
|
639
711
|
responses: responses
|
|
@@ -722,8 +794,11 @@ var TSGenerator = /** @class */ (function (_super) {
|
|
|
722
794
|
*/
|
|
723
795
|
TSGenerator.prototype.prepareResponseTypesForOperation = function (operation, operationId) {
|
|
724
796
|
var _this = this;
|
|
725
|
-
var
|
|
726
|
-
|
|
797
|
+
var responseStatusCodes = operation.getResponseStatusCodes();
|
|
798
|
+
if (!responseStatusCodes.length) {
|
|
799
|
+
return undefined;
|
|
800
|
+
}
|
|
801
|
+
var schemas = responseStatusCodes
|
|
727
802
|
.map(function (status) {
|
|
728
803
|
var _a;
|
|
729
804
|
var schema = operation.getResponseAsJsonSchema(status);
|
package/dist/cli/storage.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SchemaWrapper } from 'oas
|
|
1
|
+
import type { SchemaWrapper } from 'oas/dist/operation/get-parameters-as-json-schema';
|
|
2
2
|
/**
|
|
3
3
|
* Run through a JSON Schema object and compose up an object containing default data for any schema
|
|
4
4
|
* property that is required and also has a defined default.
|
package/dist/core/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type Oas from 'oas';
|
|
2
2
|
import type { Operation } from 'oas';
|
|
3
|
-
import type { HttpMethods } from 'oas
|
|
3
|
+
import type { HttpMethods } from 'oas/dist/rmoas.types';
|
|
4
4
|
import 'isomorphic-fetch';
|
|
5
5
|
import getJSONSchemaDefaults from './getJSONSchemaDefaults';
|
|
6
6
|
import parseResponse from './parseResponse';
|
|
@@ -46,6 +46,8 @@ var stream_1 = __importDefault(require("stream"));
|
|
|
46
46
|
var get_stream_1 = __importDefault(require("get-stream"));
|
|
47
47
|
var sync_1 = __importDefault(require("datauri/sync"));
|
|
48
48
|
var parser_1 = __importDefault(require("datauri/parser"));
|
|
49
|
+
var remove_undefined_objects_1 = __importDefault(require("remove-undefined-objects"));
|
|
50
|
+
var caseless_1 = __importDefault(require("caseless"));
|
|
49
51
|
var getJSONSchemaDefaults_1 = __importDefault(require("./getJSONSchemaDefaults"));
|
|
50
52
|
/**
|
|
51
53
|
* Extract all available parameters from an operations Parameter Object into a digestable array
|
|
@@ -142,7 +144,7 @@ function processFile(paramName, file) {
|
|
|
142
144
|
}
|
|
143
145
|
return Promise.reject(new TypeError(paramName
|
|
144
146
|
? "The data supplied for the `".concat(paramName, "` request body parameter is not a file handler that we support.")
|
|
145
|
-
:
|
|
147
|
+
: 'The data supplied for the request body payload is not a file handler that we support.'));
|
|
146
148
|
}
|
|
147
149
|
/**
|
|
148
150
|
* With potentially supplied body and/or metadata we need to run through them against a given API
|
|
@@ -156,7 +158,7 @@ function processFile(paramName, file) {
|
|
|
156
158
|
function prepareParams(operation, body, metadata) {
|
|
157
159
|
var _a, _b, _c, _d;
|
|
158
160
|
return __awaiter(this, void 0, void 0, function () {
|
|
159
|
-
var metadataIntersected, digestedParameters, hasDigestedParams, jsonSchema, jsonSchemaDefaults, params, intersection, payloadJsonSchema, conversions_1;
|
|
161
|
+
var metadataIntersected, digestedParameters, hasDigestedParams, jsonSchema, jsonSchemaDefaults, params, headerParams_1, intersection, payloadJsonSchema, conversions_1;
|
|
160
162
|
return __generator(this, function (_e) {
|
|
161
163
|
switch (_e.label) {
|
|
162
164
|
case 0:
|
|
@@ -164,6 +166,17 @@ function prepareParams(operation, body, metadata) {
|
|
|
164
166
|
digestedParameters = digestParameters(operation.getParameters());
|
|
165
167
|
hasDigestedParams = !!Object.keys(digestedParameters).length;
|
|
166
168
|
jsonSchema = operation.getParametersAsJsonSchema();
|
|
169
|
+
/**
|
|
170
|
+
* It might be common for somebody to run `sdk.findPetsByStatus({ status: 'available' }, {})`, in
|
|
171
|
+
* which case we want to filter out the second (metadata) parameter and treat the first parameter
|
|
172
|
+
* as the metadata instead. If we don't do this, their supplied `status` metadata will be treated
|
|
173
|
+
* as a body parameter, and because there's no `status` body parameter, and no supplied metadata
|
|
174
|
+
* (because it's an empty object), the request won't send a payload.
|
|
175
|
+
*
|
|
176
|
+
* @see {@link https://github.com/readmeio/api/issues/449}
|
|
177
|
+
*/
|
|
178
|
+
// eslint-disable-next-line no-param-reassign
|
|
179
|
+
metadata = (0, remove_undefined_objects_1["default"])(metadata);
|
|
167
180
|
if (!jsonSchema && (body !== undefined || metadata !== undefined)) {
|
|
168
181
|
throw new Error("You supplied metadata and/or body data for this operation but it doesn't have any documented parameters or request payloads. If you think this is an error please contact support for the API you're using.");
|
|
169
182
|
}
|
|
@@ -190,7 +203,24 @@ function prepareParams(operation, body, metadata) {
|
|
|
190
203
|
params.body = merge(params.body, body);
|
|
191
204
|
}
|
|
192
205
|
else {
|
|
193
|
-
|
|
206
|
+
headerParams_1 = (0, caseless_1["default"])({});
|
|
207
|
+
Object.entries(digestedParameters).forEach(function (_a) {
|
|
208
|
+
var paramName = _a[0], param = _a[1];
|
|
209
|
+
// Headers are sent case-insensitive so we need to make sure that we're properly
|
|
210
|
+
// matching them when detecting what our incoming payload looks like.
|
|
211
|
+
if (param["in"] === 'header') {
|
|
212
|
+
headerParams_1.set(paramName, '');
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
intersection = Object.keys(body).filter(function (value) {
|
|
216
|
+
if (Object.keys(digestedParameters).includes(value)) {
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
else if (headerParams_1.has(value)) {
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
return false;
|
|
223
|
+
}).length;
|
|
194
224
|
if (intersection && intersection / Object.keys(body).length > 0.25) {
|
|
195
225
|
/* eslint-disable no-param-reassign */
|
|
196
226
|
// If more than 25% of the body intersects with the parameters that we've got on hand,
|
|
@@ -290,8 +320,15 @@ function prepareParams(operation, body, metadata) {
|
|
|
290
320
|
Object.entries(digestedParameters).forEach(function (_a) {
|
|
291
321
|
var paramName = _a[0], param = _a[1];
|
|
292
322
|
var value;
|
|
293
|
-
if (typeof metadata === 'object' && !isEmpty(metadata)
|
|
294
|
-
|
|
323
|
+
if (typeof metadata === 'object' && !isEmpty(metadata)) {
|
|
324
|
+
if (paramName in metadata) {
|
|
325
|
+
value = metadata[paramName];
|
|
326
|
+
}
|
|
327
|
+
else if (param["in"] === 'header') {
|
|
328
|
+
// Headers are sent case-insensitive so we need to make sure that we're properly
|
|
329
|
+
// matching them when detecting what our incoming payload looks like.
|
|
330
|
+
value = metadata[Object.keys(metadata).find(function (k) { return k.toLowerCase() === paramName.toLowerCase(); })];
|
|
331
|
+
}
|
|
295
332
|
}
|
|
296
333
|
if (value === undefined) {
|
|
297
334
|
return;
|
|
@@ -307,7 +344,7 @@ function prepareParams(operation, body, metadata) {
|
|
|
307
344
|
delete metadata[paramName];
|
|
308
345
|
break;
|
|
309
346
|
case 'header':
|
|
310
|
-
params.header[paramName] = value;
|
|
347
|
+
params.header[paramName.toLowerCase()] = value;
|
|
311
348
|
delete metadata[paramName];
|
|
312
349
|
break;
|
|
313
350
|
case 'cookie':
|
package/dist/fetcher.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OASDocument } from 'oas
|
|
1
|
+
import type { OASDocument } from 'oas/dist/rmoas.types';
|
|
2
2
|
import 'isomorphic-fetch';
|
|
3
3
|
export default class Fetcher {
|
|
4
4
|
uri: string | OASDocument;
|
|
@@ -9,6 +9,7 @@ export default class Fetcher {
|
|
|
9
9
|
static registryUUIDRegex: RegExp;
|
|
10
10
|
constructor(uri: string | OASDocument);
|
|
11
11
|
static isAPIRegistryUUID(uri: string): boolean;
|
|
12
|
+
static isGitHubBlobURL(uri: string): boolean;
|
|
12
13
|
static getProjectPrefixFromRegistryUUID(uri: string): string;
|
|
13
14
|
load(): Promise<(Omit<Omit<import("openapi-types").OpenAPIV3.Document<{}>, "paths" | "components">, "paths" | "components" | "info" | "servers" | "webhooks" | "jsonSchemaDialect"> & {
|
|
14
15
|
info: import("openapi-types").OpenAPIV3_1.InfoObject;
|
package/dist/fetcher.js
CHANGED
|
@@ -47,10 +47,23 @@ var path_1 = __importDefault(require("path"));
|
|
|
47
47
|
var Fetcher = /** @class */ (function () {
|
|
48
48
|
function Fetcher(uri) {
|
|
49
49
|
if (typeof uri === 'string') {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
if (Fetcher.isAPIRegistryUUID(uri)) {
|
|
51
|
+
// Resolve OpenAPI definition shorthand accessors from within the ReadMe API Registry.
|
|
52
|
+
this.uri = uri.replace(Fetcher.registryUUIDRegex, 'https://dash.readme.com/api/v1/api-registry/$4');
|
|
53
|
+
}
|
|
54
|
+
else if (Fetcher.isGitHubBlobURL(uri)) {
|
|
55
|
+
/**
|
|
56
|
+
* People may try to use a public repository URL to the source viewer on GitHub not knowing
|
|
57
|
+
* that this page actually serves HTML. In this case we want to rewrite these to the "raw"
|
|
58
|
+
* version of this page that'll allow us to access the API definition.
|
|
59
|
+
*
|
|
60
|
+
* @example https://github.com/readmeio/oas-examples/blob/main/3.1/json/petstore.json
|
|
61
|
+
*/
|
|
62
|
+
this.uri = uri.replace(/\/\/github.com/, '//raw.githubusercontent.com').replace(/\/blob\//, '/');
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
this.uri = uri;
|
|
66
|
+
}
|
|
54
67
|
}
|
|
55
68
|
else {
|
|
56
69
|
this.uri = uri;
|
|
@@ -59,6 +72,9 @@ var Fetcher = /** @class */ (function () {
|
|
|
59
72
|
Fetcher.isAPIRegistryUUID = function (uri) {
|
|
60
73
|
return Fetcher.registryUUIDRegex.test(uri);
|
|
61
74
|
};
|
|
75
|
+
Fetcher.isGitHubBlobURL = function (uri) {
|
|
76
|
+
return /\/\/github.com\/[-_a-zA-Z0-9]+\/[-_a-zA-Z0-9]+\/blob\/(.*).(yaml|json|yml)/.test(uri);
|
|
77
|
+
};
|
|
62
78
|
Fetcher.getProjectPrefixFromRegistryUUID = function (uri) {
|
|
63
79
|
var matches = uri.match(Fetcher.registryUUIDRegex);
|
|
64
80
|
if (!matches) {
|
package/dist/index.d.ts
CHANGED
package/dist/packageInfo.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export declare const PACKAGE_NAME = "api";
|
|
2
|
-
export declare const PACKAGE_VERSION = "5.0.0-beta.
|
|
2
|
+
export declare const PACKAGE_VERSION = "5.0.0-beta.3";
|
package/dist/packageInfo.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "api",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "5.0.0-beta.3",
|
|
4
|
+
"description": "Magical SDK generation from an OpenAPI definition đĒ",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
7
7
|
"bin": {
|
|
@@ -10,12 +10,9 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"build": "tsc",
|
|
12
12
|
"debug:bin": "node -r ts-node/register src/bin.ts",
|
|
13
|
-
"lint": "eslint . --ext .js,.ts",
|
|
14
13
|
"prebuild": "rm -rf dist/; npm run prebuild.packageConfig",
|
|
15
|
-
"prebuild.packageConfig": "node -p \"'// This file is automatically updated by the build script.\\nexport const PACKAGE_NAME = \\'' + require('./package.json').name + '\\';\\nexport const PACKAGE_VERSION = \\'' + require('./package.json').version + '\\';'\" > src/packageInfo.ts",
|
|
14
|
+
"prebuild.packageConfig": "node -p \"'// This file is automatically updated by the build script.\\nexport const PACKAGE_NAME = \\'' + require('./package.json').name + '\\';\\nexport const PACKAGE_VERSION = \\'' + require('./package.json').version + '\\';'\" > src/packageInfo.ts; git add src/packageInfo.ts",
|
|
16
15
|
"prepack": "npm run build",
|
|
17
|
-
"pretest": "npm run lint",
|
|
18
|
-
"prettier": "prettier --list-different --write \"./**/**.{js,ts}\"",
|
|
19
16
|
"test": "nyc mocha \"test/**/*.test.ts\""
|
|
20
17
|
},
|
|
21
18
|
"repository": {
|
|
@@ -23,7 +20,7 @@
|
|
|
23
20
|
"url": "https://github.com/readmeio/api.git",
|
|
24
21
|
"directory": "packages/api"
|
|
25
22
|
},
|
|
26
|
-
"homepage": "https://
|
|
23
|
+
"homepage": "https://api.readme.dev",
|
|
27
24
|
"bugs": {
|
|
28
25
|
"url": "https://github.com/readmeio/api/issues"
|
|
29
26
|
},
|
|
@@ -32,9 +29,16 @@
|
|
|
32
29
|
"engines": {
|
|
33
30
|
"node": ">=14"
|
|
34
31
|
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"api",
|
|
34
|
+
"openapi",
|
|
35
|
+
"sdk",
|
|
36
|
+
"swagger"
|
|
37
|
+
],
|
|
35
38
|
"dependencies": {
|
|
36
39
|
"@readme/oas-to-har": "^17.0.8",
|
|
37
40
|
"@readme/openapi-parser": "^2.2.0",
|
|
41
|
+
"caseless": "^0.12.0",
|
|
38
42
|
"chalk": "^4.1.2",
|
|
39
43
|
"commander": "^9.2.0",
|
|
40
44
|
"datauri": "^4.1.0",
|
|
@@ -47,21 +51,22 @@
|
|
|
47
51
|
"get-stream": "^6.0.1",
|
|
48
52
|
"isomorphic-fetch": "^3.0.0",
|
|
49
53
|
"js-yaml": "^4.1.0",
|
|
50
|
-
"json-schema-to-typescript": "^
|
|
54
|
+
"json-schema-to-typescript": "^10.1.5",
|
|
51
55
|
"json-schema-traverse": "^1.0.0",
|
|
52
56
|
"lodash.merge": "^4.6.2",
|
|
53
57
|
"make-dir": "^3.1.0",
|
|
54
|
-
"oas": "^18.3.
|
|
58
|
+
"oas": "^18.3.4",
|
|
55
59
|
"object-hash": "^3.0.0",
|
|
56
60
|
"ora": "^5.4.1",
|
|
57
61
|
"prompts": "^2.4.2",
|
|
62
|
+
"remove-undefined-objects": "^2.0.1",
|
|
58
63
|
"ssri": "^9.0.0",
|
|
59
64
|
"ts-morph": "^15.1.0",
|
|
60
65
|
"validate-npm-package-name": "^4.0.0"
|
|
61
66
|
},
|
|
62
67
|
"devDependencies": {
|
|
63
|
-
"@readme/eslint-config": "^8.7.3",
|
|
64
68
|
"@readme/oas-examples": "^5.4.1",
|
|
69
|
+
"@types/caseless": "^0.12.2",
|
|
65
70
|
"@types/chai": "^4.3.1",
|
|
66
71
|
"@types/find-cache-dir": "^3.2.1",
|
|
67
72
|
"@types/js-yaml": "^4.0.5",
|
|
@@ -73,15 +78,13 @@
|
|
|
73
78
|
"@types/ssri": "^7.1.1",
|
|
74
79
|
"@types/validate-npm-package-name": "^4.0.0",
|
|
75
80
|
"chai": "^4.3.6",
|
|
76
|
-
"eslint": "^8.14.0",
|
|
77
81
|
"fetch-mock": "^9.11.0",
|
|
78
82
|
"mocha": "^10.0.0",
|
|
79
83
|
"mock-require": "^3.0.3",
|
|
80
84
|
"nyc": "^15.1.0",
|
|
81
|
-
"prettier": "^2.6.2",
|
|
82
85
|
"sinon": "^14.0.0",
|
|
83
86
|
"sinon-chai": "^3.7.0",
|
|
84
|
-
"typescript": "^4.
|
|
87
|
+
"typescript": "^4.7.4",
|
|
85
88
|
"unique-temp-dir": "^1.0.0"
|
|
86
89
|
},
|
|
87
90
|
"prettier": "@readme/eslint-config/prettier",
|
|
@@ -91,5 +94,5 @@
|
|
|
91
94
|
"test/"
|
|
92
95
|
]
|
|
93
96
|
},
|
|
94
|
-
"gitHead": "
|
|
97
|
+
"gitHead": "24d5b83545735176786d212a69121a029cf6dea1"
|
|
95
98
|
}
|
package/src/cache.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type Oas from 'oas';
|
|
2
2
|
import type { Operation } from 'oas';
|
|
3
|
-
import type { HttpMethods, JSONSchema, SchemaObject } from 'oas
|
|
3
|
+
import type { HttpMethods, JSONSchema, SchemaObject } from 'oas/dist/rmoas.types';
|
|
4
4
|
import type {
|
|
5
5
|
ClassDeclaration,
|
|
6
6
|
JSDocStructure,
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
OptionalKind,
|
|
9
9
|
ParameterDeclarationStructure,
|
|
10
10
|
TypeParameterDeclarationStructure,
|
|
11
|
+
VariableStatement,
|
|
11
12
|
} from 'ts-morph';
|
|
12
13
|
import type { Options as JSONSchemaToTypescriptOptions } from 'json-schema-to-typescript';
|
|
13
14
|
import type Storage from '../../storage';
|
|
@@ -18,11 +19,16 @@ import path from 'path';
|
|
|
18
19
|
import CodeGeneratorLanguage from '../language';
|
|
19
20
|
import logger from '../../logger';
|
|
20
21
|
import objectHash from 'object-hash';
|
|
21
|
-
import { IndentationText, Project, QuoteKind, ScriptTarget } from 'ts-morph';
|
|
22
|
+
import { IndentationText, Project, QuoteKind, ScriptTarget, VariableDeclarationKind } from 'ts-morph';
|
|
22
23
|
import { compile } from 'json-schema-to-typescript';
|
|
23
24
|
import { format as prettier } from 'json-schema-to-typescript/dist/src/formatter';
|
|
24
25
|
import execa from 'execa';
|
|
25
26
|
|
|
27
|
+
export type TSGeneratorOptions = {
|
|
28
|
+
outputJS?: boolean;
|
|
29
|
+
compilerTarget?: 'cjs' | 'esm';
|
|
30
|
+
};
|
|
31
|
+
|
|
26
32
|
type OperationTypeHousing = {
|
|
27
33
|
types: {
|
|
28
34
|
params?: false | Record<'body' | 'formData' | 'metadata', string>;
|
|
@@ -51,6 +57,8 @@ export default class TSGenerator extends CodeGeneratorLanguage {
|
|
|
51
57
|
|
|
52
58
|
sdk: ClassDeclaration;
|
|
53
59
|
|
|
60
|
+
sdkExport: VariableStatement;
|
|
61
|
+
|
|
54
62
|
schemas: Map<
|
|
55
63
|
string,
|
|
56
64
|
{
|
|
@@ -60,15 +68,7 @@ export default class TSGenerator extends CodeGeneratorLanguage {
|
|
|
60
68
|
}
|
|
61
69
|
>;
|
|
62
70
|
|
|
63
|
-
constructor(
|
|
64
|
-
spec: Oas,
|
|
65
|
-
specPath: string,
|
|
66
|
-
identifier: string,
|
|
67
|
-
opts: {
|
|
68
|
-
outputJS?: boolean;
|
|
69
|
-
compilerTarget?: 'cjs' | 'esm';
|
|
70
|
-
} = {}
|
|
71
|
-
) {
|
|
71
|
+
constructor(spec: Oas, specPath: string, identifier: string, opts: TSGeneratorOptions = {}) {
|
|
72
72
|
const options: { outputJS: boolean; compilerTarget: 'cjs' | 'esm' } = {
|
|
73
73
|
outputJS: false,
|
|
74
74
|
compilerTarget: 'cjs',
|
|
@@ -100,9 +100,9 @@ export default class TSGenerator extends CodeGeneratorLanguage {
|
|
|
100
100
|
},
|
|
101
101
|
compilerOptions: {
|
|
102
102
|
declaration: true,
|
|
103
|
+
outDir: 'dist',
|
|
103
104
|
resolveJsonModule: true,
|
|
104
105
|
target: options.compilerTarget === 'cjs' ? ScriptTarget.ES5 : ScriptTarget.ES2020,
|
|
105
|
-
outDir: 'dist',
|
|
106
106
|
|
|
107
107
|
// If we're compiling to a CJS target then we need to include this compiler option
|
|
108
108
|
// otherwise TS will attempt to load our `openapi.json` import with a `.default` property
|
|
@@ -186,23 +186,57 @@ export default class TSGenerator extends CodeGeneratorLanguage {
|
|
|
186
186
|
name: 'SDK',
|
|
187
187
|
});
|
|
188
188
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
189
|
+
this.sdkExport = sdkSource.addVariableStatement({
|
|
190
|
+
declarationKind: VariableDeclarationKind.Const,
|
|
191
|
+
declarations: [
|
|
192
|
+
{
|
|
193
|
+
name: 'createSDK',
|
|
194
|
+
initializer: writer => {
|
|
195
|
+
// `ts-morph` doesn't have any way to cleanly create an IFEE.
|
|
196
|
+
writer.writeLine('(() => { return new SDK(); })()');
|
|
197
|
+
return writer;
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* There's an annoying quirk with `ts-morph` where if we set the `createSDK` function to be the
|
|
205
|
+
* default export with `isDefaultExport` then when we compile it to an ES5 target for CJS
|
|
206
|
+
* environments it'll be exported as `export.default = createSDK`, which when you try to load it
|
|
207
|
+
* you'll need to run `require('@api/sdk').default`.
|
|
208
|
+
*
|
|
209
|
+
* Instead here by plainly creating `createSDK` in the source file and then setting this export
|
|
210
|
+
* assignment it'll export the SDK IFEE initializer as `module.exports = createSDK` so people
|
|
211
|
+
* can cleanly load their SDK with `require('@api/sdk)`.
|
|
212
|
+
*
|
|
213
|
+
* A whole lot of debugging went into here to let people not have to worry about `.default`
|
|
214
|
+
* messes. I hope it's worth it!
|
|
215
|
+
*/
|
|
200
216
|
if (this.compilerTarget === 'cjs') {
|
|
201
217
|
sdkSource.addExportAssignment({
|
|
202
|
-
expression: '
|
|
218
|
+
expression: 'createSDK',
|
|
203
219
|
});
|
|
204
220
|
} else {
|
|
205
|
-
|
|
221
|
+
/**
|
|
222
|
+
* Because `createSDK` above is an IFEE constant we can't use `setIsDefaultExport` on it due
|
|
223
|
+
* to `ts-morph` not having great handling for IFEE's.
|
|
224
|
+
*
|
|
225
|
+
* If we were to call `setIsDefaultExport` on our IFEE to attempt to compile it as
|
|
226
|
+
* `export default createSDK` then `ts-morph` hard crashes with a "Error replacing tree: The
|
|
227
|
+
* children of the old and new trees were expected to have the same count" exception due to
|
|
228
|
+
* it not being able properly handle IFEE's. It's for that reason that we need to manually
|
|
229
|
+
* write a statement expression to set `createSDK` as the default export.
|
|
230
|
+
*
|
|
231
|
+
* Another quirk that this work avoids is there being an empty `export {};` at the very end
|
|
232
|
+
* of our compiled `d.ts` declaration file. I'm not sure why it was being added, and it
|
|
233
|
+
* didn't appear to be harming anything, but us manually creating this export statement
|
|
234
|
+
* causes it to go away.
|
|
235
|
+
*
|
|
236
|
+
* Thankfully, fortunately, and curiously, these are all only problems in non-CJS compiled
|
|
237
|
+
* targets. ¯\_(ã)_/¯
|
|
238
|
+
*/
|
|
239
|
+
sdkSource.addStatements('export default createSDK');
|
|
206
240
|
}
|
|
207
241
|
|
|
208
242
|
this.sdk.addProperties([
|
|
@@ -334,7 +368,43 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
334
368
|
// @todo should all of these isolated into their own file outside of the main sdk class file?
|
|
335
369
|
// Add all known types that we're using into the SDK.
|
|
336
370
|
Array.from(this.types.values()).forEach(exp => {
|
|
337
|
-
|
|
371
|
+
/**
|
|
372
|
+
* When `ts-morph` compiles declaration files when we're targeting CJS environments it creates
|
|
373
|
+
* the default export as `export = _default` instead of `export default const _default`. This
|
|
374
|
+
* causes TS to throw a TS2309 error for "An export assignment cannot be used in a module
|
|
375
|
+
* with other exported elements" because our types and interfaces are also being exported and
|
|
376
|
+
* the `export =` overrides those.
|
|
377
|
+
*
|
|
378
|
+
* Fixing this is, to be frank, a fucking HARD problem for a couple reasons:
|
|
379
|
+
*
|
|
380
|
+
* 1. Our JSON Schema types and interfaces are coming from `json-schema-to-typescript` and
|
|
381
|
+
* that library exports its data a raw string containing multiple types and interfaces.
|
|
382
|
+
* The only way we're able to capture and use them in our codegenerated SDK is because
|
|
383
|
+
* we're ingesting that string into `ts-morph` and then using its APIs to extract exported
|
|
384
|
+
* declarations (which are still strings) and then they're re-inserted into our main
|
|
385
|
+
* source file here.
|
|
386
|
+
* 2. Though `ts-morph` has APIs for adding type aliases and interfaces to a source file what
|
|
387
|
+
* it doesn't have is the ability to pass in a string, or a `Writer` class that exposes,
|
|
388
|
+
* to write raw strings to a type or an interface. If it did we'd be able to replace this
|
|
389
|
+
* `addStatements` call with an `addTypeAlias` and `addInterface` call for each of our
|
|
390
|
+
* JSON Schema schemas that we've got along with an `isExported` flag for `ts-morph` to
|
|
391
|
+
* export it.
|
|
392
|
+
*
|
|
393
|
+
* Because neither of these are solvable problems right now we're instead opting to **not**
|
|
394
|
+
* export types and interfaces from these SDKs. This isn't a great solution because it
|
|
395
|
+
* /slightly/ reduces the usability of the TS codegen functionality but in order for the TS
|
|
396
|
+
* declaration files that we generate to be valid this is the only option that we've got.
|
|
397
|
+
*
|
|
398
|
+
* However, that said, if somebody needs an interface or type exported they can export it
|
|
399
|
+
* themselves in the SDK code that we compile for them.
|
|
400
|
+
*
|
|
401
|
+
* @fixme
|
|
402
|
+
*/
|
|
403
|
+
sdkSource.addStatements(
|
|
404
|
+
// All expressions coming out of `json-schema-to-typescript` are exported so by popping this
|
|
405
|
+
// off we'll just be inserting plain interfaces and types into the SDK source.
|
|
406
|
+
exp.substring('export '.length)
|
|
407
|
+
);
|
|
338
408
|
});
|
|
339
409
|
|
|
340
410
|
if (this.outputJS) {
|
|
@@ -373,7 +443,7 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
373
443
|
const parameters: OptionalKind<ParameterDeclarationStructure>[] = [{ name: 'path', type: 'string' }];
|
|
374
444
|
const docblock: OptionalKind<JSDocStructure> = {
|
|
375
445
|
description: writer => {
|
|
376
|
-
writer.writeLine(`Access any ${method} endpoint on your API.`);
|
|
446
|
+
writer.writeLine(`Access any ${method.toUpperCase()} endpoint on your API.`);
|
|
377
447
|
return writer;
|
|
378
448
|
},
|
|
379
449
|
tags: [{ tagName: 'param', text: 'path API path to make a request against.' }],
|
|
@@ -589,7 +659,7 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
589
659
|
this.methodGenerics.get(operation.method).addOverload({
|
|
590
660
|
typeParameters,
|
|
591
661
|
parameters: [
|
|
592
|
-
{ name: 'path', type: '
|
|
662
|
+
{ name: 'path', type: `'${operation.path}'` },
|
|
593
663
|
{ ...parameters.body, hasQuestionToken: false },
|
|
594
664
|
{ ...parameters.metadata, hasQuestionToken: false },
|
|
595
665
|
],
|
|
@@ -600,14 +670,14 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
600
670
|
// Create an overload that just has a single `metadata` parameter.
|
|
601
671
|
this.methodGenerics.get(operation.method).addOverload({
|
|
602
672
|
typeParameters,
|
|
603
|
-
parameters: [{ name: 'path', type: '
|
|
673
|
+
parameters: [{ name: 'path', type: `'${operation.path}'` }, parameters.metadata],
|
|
604
674
|
returnType,
|
|
605
675
|
docs: docblock ? [docblock] : null,
|
|
606
676
|
});
|
|
607
677
|
} else {
|
|
608
678
|
this.methodGenerics.get(operation.method).addOverload({
|
|
609
679
|
typeParameters: responseTypes ? null : ['T = unknown'],
|
|
610
|
-
parameters: [{ name: 'path', type: '
|
|
680
|
+
parameters: [{ name: 'path', type: `'${operation.path}'` }, ...Object.values(parameters)],
|
|
611
681
|
returnType,
|
|
612
682
|
docs: docblock ? [docblock] : null,
|
|
613
683
|
});
|
|
@@ -673,12 +743,17 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
673
743
|
Object.entries(ops).forEach(([method, operation]: [HttpMethods, Operation]) => {
|
|
674
744
|
methods.add(method);
|
|
675
745
|
|
|
676
|
-
const operationId = operation.getOperationId(
|
|
746
|
+
const operationId = operation.getOperationId({
|
|
747
|
+
// This `camelCase` option will clean up any weird characters that might be present in
|
|
748
|
+
// the `operationId` so as we don't break TS compilation with an invalid method accessor.
|
|
749
|
+
camelCase: true,
|
|
750
|
+
});
|
|
751
|
+
|
|
677
752
|
const params = this.prepareParameterTypesForOperation(operation, operationId);
|
|
678
753
|
const responses = this.prepareResponseTypesForOperation(operation, operationId);
|
|
679
754
|
|
|
680
755
|
if (operation.hasOperationId()) {
|
|
681
|
-
operations[
|
|
756
|
+
operations[operationId] = {
|
|
682
757
|
types: {
|
|
683
758
|
params,
|
|
684
759
|
responses,
|
|
@@ -758,8 +833,12 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
758
833
|
* @param operationId
|
|
759
834
|
*/
|
|
760
835
|
prepareResponseTypesForOperation(operation: Operation, operationId: string) {
|
|
761
|
-
const
|
|
762
|
-
|
|
836
|
+
const responseStatusCodes = operation.getResponseStatusCodes();
|
|
837
|
+
if (!responseStatusCodes.length) {
|
|
838
|
+
return undefined;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const schemas = responseStatusCodes
|
|
763
842
|
.map(status => {
|
|
764
843
|
const schema = operation.getResponseAsJsonSchema(status);
|
|
765
844
|
if (!schema) {
|
package/src/cli/storage.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { SchemaObject } from 'oas
|
|
2
|
-
import type { SchemaWrapper } from 'oas
|
|
1
|
+
import type { SchemaObject } from 'oas/dist/rmoas.types';
|
|
2
|
+
import type { SchemaWrapper } from 'oas/dist/operation/get-parameters-as-json-schema';
|
|
3
3
|
import traverse from 'json-schema-traverse';
|
|
4
4
|
|
|
5
5
|
/**
|
package/src/core/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Operation } from 'oas';
|
|
2
|
-
import type { ParameterObject, SchemaObject } from 'oas
|
|
2
|
+
import type { ParameterObject, SchemaObject } from 'oas/dist/rmoas.types';
|
|
3
3
|
import type { ReadStream } from 'fs';
|
|
4
4
|
|
|
5
5
|
import lodashMerge from 'lodash.merge';
|
|
@@ -9,6 +9,8 @@ import stream from 'stream';
|
|
|
9
9
|
import getStream from 'get-stream';
|
|
10
10
|
import datauri from 'datauri/sync';
|
|
11
11
|
import DatauriParser from 'datauri/parser';
|
|
12
|
+
import removeUndefinedObjects from 'remove-undefined-objects';
|
|
13
|
+
import caseless from 'caseless';
|
|
12
14
|
import getJSONSchemaDefaults from './getJSONSchemaDefaults';
|
|
13
15
|
|
|
14
16
|
/**
|
|
@@ -22,7 +24,7 @@ import getJSONSchemaDefaults from './getJSONSchemaDefaults';
|
|
|
22
24
|
function digestParameters(parameters: ParameterObject[]): Record<string, ParameterObject> {
|
|
23
25
|
return parameters.reduce((prev, param) => {
|
|
24
26
|
if ('$ref' in param || 'allOf' in param || 'anyOf' in param || 'oneOf' in param) {
|
|
25
|
-
throw new Error(
|
|
27
|
+
throw new Error("The OpenAPI document for this operation wasn't dereferenced before processing.");
|
|
26
28
|
} else if (param.name in prev) {
|
|
27
29
|
throw new Error(
|
|
28
30
|
`The operation you are using has the same parameter, ${param.name}, spread across multiple entry points. We unfortunately can't handle this right now.`
|
|
@@ -123,7 +125,7 @@ function processFile(
|
|
|
123
125
|
new TypeError(
|
|
124
126
|
paramName
|
|
125
127
|
? `The data supplied for the \`${paramName}\` request body parameter is not a file handler that we support.`
|
|
126
|
-
:
|
|
128
|
+
: 'The data supplied for the request body payload is not a file handler that we support.'
|
|
127
129
|
)
|
|
128
130
|
);
|
|
129
131
|
}
|
|
@@ -143,6 +145,18 @@ export default async function prepareParams(operation: Operation, body?: unknown
|
|
|
143
145
|
const hasDigestedParams = !!Object.keys(digestedParameters).length;
|
|
144
146
|
const jsonSchema = operation.getParametersAsJsonSchema();
|
|
145
147
|
|
|
148
|
+
/**
|
|
149
|
+
* It might be common for somebody to run `sdk.findPetsByStatus({ status: 'available' }, {})`, in
|
|
150
|
+
* which case we want to filter out the second (metadata) parameter and treat the first parameter
|
|
151
|
+
* as the metadata instead. If we don't do this, their supplied `status` metadata will be treated
|
|
152
|
+
* as a body parameter, and because there's no `status` body parameter, and no supplied metadata
|
|
153
|
+
* (because it's an empty object), the request won't send a payload.
|
|
154
|
+
*
|
|
155
|
+
* @see {@link https://github.com/readmeio/api/issues/449}
|
|
156
|
+
*/
|
|
157
|
+
// eslint-disable-next-line no-param-reassign
|
|
158
|
+
metadata = removeUndefinedObjects(metadata);
|
|
159
|
+
|
|
146
160
|
if (!jsonSchema && (body !== undefined || metadata !== undefined)) {
|
|
147
161
|
throw new Error(
|
|
148
162
|
"You supplied metadata and/or body data for this operation but it doesn't have any documented parameters or request payloads. If you think this is an error please contact support for the API you're using."
|
|
@@ -184,7 +198,25 @@ export default async function prepareParams(operation: Operation, body?: unknown
|
|
|
184
198
|
// isn't anything we can do about it.
|
|
185
199
|
params.body = merge(params.body, body);
|
|
186
200
|
} else {
|
|
187
|
-
const
|
|
201
|
+
const headerParams = caseless({});
|
|
202
|
+
Object.entries(digestedParameters).forEach(([paramName, param]) => {
|
|
203
|
+
// Headers are sent case-insensitive so we need to make sure that we're properly
|
|
204
|
+
// matching them when detecting what our incoming payload looks like.
|
|
205
|
+
if (param.in === 'header') {
|
|
206
|
+
headerParams.set(paramName, '');
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const intersection = Object.keys(body).filter(value => {
|
|
211
|
+
if (Object.keys(digestedParameters).includes(value)) {
|
|
212
|
+
return true;
|
|
213
|
+
} else if (headerParams.has(value)) {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return false;
|
|
218
|
+
}).length;
|
|
219
|
+
|
|
188
220
|
if (intersection && intersection / Object.keys(body).length > 0.25) {
|
|
189
221
|
/* eslint-disable no-param-reassign */
|
|
190
222
|
// If more than 25% of the body intersects with the parameters that we've got on hand,
|
|
@@ -276,8 +308,14 @@ export default async function prepareParams(operation: Operation, body?: unknown
|
|
|
276
308
|
|
|
277
309
|
Object.entries(digestedParameters).forEach(([paramName, param]) => {
|
|
278
310
|
let value: any;
|
|
279
|
-
if (typeof metadata === 'object' && !isEmpty(metadata)
|
|
280
|
-
|
|
311
|
+
if (typeof metadata === 'object' && !isEmpty(metadata)) {
|
|
312
|
+
if (paramName in metadata) {
|
|
313
|
+
value = metadata[paramName];
|
|
314
|
+
} else if (param.in === 'header') {
|
|
315
|
+
// Headers are sent case-insensitive so we need to make sure that we're properly
|
|
316
|
+
// matching them when detecting what our incoming payload looks like.
|
|
317
|
+
value = metadata[Object.keys(metadata).find(k => k.toLowerCase() === paramName.toLowerCase())];
|
|
318
|
+
}
|
|
281
319
|
}
|
|
282
320
|
|
|
283
321
|
if (value === undefined) {
|
|
@@ -295,7 +333,7 @@ export default async function prepareParams(operation: Operation, body?: unknown
|
|
|
295
333
|
delete metadata[paramName];
|
|
296
334
|
break;
|
|
297
335
|
case 'header':
|
|
298
|
-
params.header[paramName] = value;
|
|
336
|
+
params.header[paramName.toLowerCase()] = value;
|
|
299
337
|
delete metadata[paramName];
|
|
300
338
|
break;
|
|
301
339
|
case 'cookie':
|
package/src/fetcher.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OASDocument } from 'oas
|
|
1
|
+
import type { OASDocument } from 'oas/dist/rmoas.types';
|
|
2
2
|
|
|
3
3
|
import 'isomorphic-fetch';
|
|
4
4
|
import OpenAPIParser from '@readme/openapi-parser';
|
|
@@ -18,10 +18,21 @@ export default class Fetcher {
|
|
|
18
18
|
|
|
19
19
|
constructor(uri: string | OASDocument) {
|
|
20
20
|
if (typeof uri === 'string') {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
if (Fetcher.isAPIRegistryUUID(uri)) {
|
|
22
|
+
// Resolve OpenAPI definition shorthand accessors from within the ReadMe API Registry.
|
|
23
|
+
this.uri = uri.replace(Fetcher.registryUUIDRegex, 'https://dash.readme.com/api/v1/api-registry/$4');
|
|
24
|
+
} else if (Fetcher.isGitHubBlobURL(uri)) {
|
|
25
|
+
/**
|
|
26
|
+
* People may try to use a public repository URL to the source viewer on GitHub not knowing
|
|
27
|
+
* that this page actually serves HTML. In this case we want to rewrite these to the "raw"
|
|
28
|
+
* version of this page that'll allow us to access the API definition.
|
|
29
|
+
*
|
|
30
|
+
* @example https://github.com/readmeio/oas-examples/blob/main/3.1/json/petstore.json
|
|
31
|
+
*/
|
|
32
|
+
this.uri = uri.replace(/\/\/github.com/, '//raw.githubusercontent.com').replace(/\/blob\//, '/');
|
|
33
|
+
} else {
|
|
34
|
+
this.uri = uri;
|
|
35
|
+
}
|
|
25
36
|
} else {
|
|
26
37
|
this.uri = uri;
|
|
27
38
|
}
|
|
@@ -31,6 +42,10 @@ export default class Fetcher {
|
|
|
31
42
|
return Fetcher.registryUUIDRegex.test(uri);
|
|
32
43
|
}
|
|
33
44
|
|
|
45
|
+
static isGitHubBlobURL(uri: string) {
|
|
46
|
+
return /\/\/github.com\/[-_a-zA-Z0-9]+\/[-_a-zA-Z0-9]+\/blob\/(.*).(yaml|json|yml)/.test(uri);
|
|
47
|
+
}
|
|
48
|
+
|
|
34
49
|
static getProjectPrefixFromRegistryUUID(uri: string) {
|
|
35
50
|
const matches = uri.match(Fetcher.registryUUIDRegex);
|
|
36
51
|
if (!matches) {
|
package/src/index.ts
CHANGED
package/src/packageInfo.ts
CHANGED