api 5.0.0-beta.2 → 5.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/README.md +7 -8
- package/dist/bin.js +1 -1
- package/dist/cache.d.ts +38 -3
- package/dist/cache.js +7 -26
- package/dist/cli/codegen/index.d.ts +1 -1
- package/dist/cli/codegen/language.d.ts +1 -1
- package/dist/cli/codegen/language.js +13 -0
- package/dist/cli/codegen/languages/typescript/util.d.ts +21 -0
- package/dist/cli/codegen/languages/typescript/util.js +185 -0
- package/dist/cli/codegen/languages/typescript.d.ts +36 -41
- package/dist/cli/codegen/languages/typescript.js +394 -414
- package/dist/cli/commands/install.js +6 -6
- package/dist/cli/storage.d.ts +1 -1
- package/dist/cli/storage.js +2 -2
- package/dist/core/errors/fetchError.d.ts +12 -0
- package/dist/core/errors/fetchError.js +36 -0
- package/dist/core/getJSONSchemaDefaults.d.ts +1 -1
- package/dist/core/index.d.ts +12 -4
- package/dist/core/index.js +36 -11
- package/dist/core/parseResponse.d.ts +6 -1
- package/dist/core/parseResponse.js +9 -3
- package/dist/core/prepareAuth.js +47 -18
- package/dist/core/prepareParams.d.ts +0 -3
- package/dist/core/prepareParams.js +102 -41
- package/dist/fetcher.d.ts +1 -1
- package/dist/fetcher.js +3 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +24 -40
- package/dist/packageInfo.d.ts +1 -1
- package/dist/packageInfo.js +1 -1
- package/package.json +31 -17
- package/src/bin.ts +2 -1
- package/src/cache.ts +9 -31
- package/src/cli/codegen/index.ts +1 -1
- package/src/cli/codegen/language.ts +18 -1
- package/src/cli/codegen/languages/typescript/util.ts +183 -0
- package/src/cli/codegen/languages/typescript.ts +348 -340
- package/src/cli/commands/install.ts +6 -8
- package/src/cli/storage.ts +4 -4
- package/src/core/errors/fetchError.ts +31 -0
- package/src/core/getJSONSchemaDefaults.ts +3 -2
- package/src/core/index.ts +53 -18
- package/src/core/parseResponse.ts +8 -2
- package/src/core/prepareAuth.ts +55 -31
- package/src/core/prepareParams.ts +112 -41
- package/src/fetcher.ts +5 -4
- package/src/index.ts +24 -32
- package/src/packageInfo.ts +1 -1
- package/src/typings.d.ts +0 -1
- package/tsconfig.json +1 -1
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
import type { SupportedLanguages } from '../codegen';
|
|
2
2
|
|
|
3
3
|
import { Command, Option } from 'commander';
|
|
4
|
-
import
|
|
4
|
+
import figures from 'figures';
|
|
5
5
|
import Oas from 'oas';
|
|
6
|
+
import ora from 'ora';
|
|
7
|
+
import validateNPMPackageName from 'validate-npm-package-name';
|
|
6
8
|
|
|
7
|
-
import codegen from '../codegen';
|
|
8
9
|
import Fetcher from '../../fetcher';
|
|
9
|
-
import
|
|
10
|
-
import logger from '../logger';
|
|
11
|
-
|
|
10
|
+
import codegen from '../codegen';
|
|
12
11
|
import promptTerminal from '../lib/prompt';
|
|
13
|
-
|
|
14
|
-
import
|
|
15
|
-
import validateNPMPackageName from 'validate-npm-package-name';
|
|
12
|
+
import logger from '../logger';
|
|
13
|
+
import Storage from '../storage';
|
|
16
14
|
|
|
17
15
|
// @todo log logs to `.api/.logs` and have `.logs` ignored
|
|
18
16
|
const cmd = new Command();
|
package/src/cli/storage.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import type { OASDocument } from 'oas
|
|
1
|
+
import type { OASDocument } from 'oas/dist/rmoas.types';
|
|
2
2
|
|
|
3
|
-
import ssri from 'ssri';
|
|
4
3
|
import fs from 'fs';
|
|
5
4
|
import path from 'path';
|
|
6
|
-
import makeDir from 'make-dir';
|
|
7
5
|
|
|
8
|
-
import
|
|
6
|
+
import makeDir from 'make-dir';
|
|
7
|
+
import ssri from 'ssri';
|
|
9
8
|
|
|
10
9
|
import Fetcher from '../fetcher';
|
|
10
|
+
import { PACKAGE_VERSION } from '../packageInfo';
|
|
11
11
|
|
|
12
12
|
export default class Storage {
|
|
13
13
|
static dir: string;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
class FetchError extends Error {
|
|
2
|
+
/** HTTP Status */
|
|
3
|
+
status: number;
|
|
4
|
+
|
|
5
|
+
/** The content of the response. */
|
|
6
|
+
data: unknown;
|
|
7
|
+
|
|
8
|
+
/** The Headers of the response. */
|
|
9
|
+
headers: Headers;
|
|
10
|
+
|
|
11
|
+
/** The raw `Response` object. */
|
|
12
|
+
res: Response;
|
|
13
|
+
|
|
14
|
+
constructor(status: number, data: unknown, headers: Headers, res: Response) {
|
|
15
|
+
super(res.statusText);
|
|
16
|
+
|
|
17
|
+
this.name = 'FetchError';
|
|
18
|
+
this.status = status;
|
|
19
|
+
this.data = data;
|
|
20
|
+
this.headers = headers;
|
|
21
|
+
this.res = res;
|
|
22
|
+
|
|
23
|
+
// We could fix this by updating our target to ES2015 but because we support exporting to CJS
|
|
24
|
+
// we can't.
|
|
25
|
+
//
|
|
26
|
+
// https://www.dannyguo.com/blog/how-to-fix-instanceof-not-working-for-custom-errors-in-typescript/
|
|
27
|
+
Object.setPrototypeOf(this, FetchError.prototype);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default FetchError;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
1
|
+
import type { SchemaWrapper } from 'oas/dist/operation/get-parameters-as-json-schema';
|
|
2
|
+
import type { SchemaObject } from 'oas/dist/rmoas.types';
|
|
3
|
+
|
|
3
4
|
import traverse from 'json-schema-traverse';
|
|
4
5
|
|
|
5
6
|
/**
|
package/src/core/index.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
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
|
|
|
5
|
-
import 'isomorphic-fetch';
|
|
6
|
-
import fetchHar from 'fetch-har';
|
|
7
5
|
import oasToHar from '@readme/oas-to-har';
|
|
6
|
+
import fetchHar from 'fetch-har';
|
|
8
7
|
import { FormDataEncoder } from 'form-data-encoder';
|
|
8
|
+
import 'isomorphic-fetch';
|
|
9
|
+
// `AbortController` was shipped in Node 15 so when Node 14 is EOL'd we can drop this dependency.
|
|
10
|
+
import { AbortController } from 'node-abort-controller';
|
|
9
11
|
|
|
12
|
+
import FetchError from './errors/fetchError';
|
|
10
13
|
import getJSONSchemaDefaults from './getJSONSchemaDefaults';
|
|
11
14
|
import parseResponse from './parseResponse';
|
|
12
15
|
import prepareAuth from './prepareAuth';
|
|
@@ -15,12 +18,26 @@ import prepareServer from './prepareServer';
|
|
|
15
18
|
|
|
16
19
|
export interface ConfigOptions {
|
|
17
20
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
21
|
+
* Override the default `fetch` request timeout of 30 seconds. This number should be represented
|
|
22
|
+
* in milliseconds.
|
|
20
23
|
*/
|
|
21
|
-
|
|
24
|
+
timeout?: number;
|
|
22
25
|
}
|
|
23
26
|
|
|
27
|
+
export type FetchResponse<status, data> = {
|
|
28
|
+
data: data;
|
|
29
|
+
status: status;
|
|
30
|
+
headers: Headers;
|
|
31
|
+
res: Response;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// https://stackoverflow.com/a/39495173
|
|
35
|
+
type Enumerate<N extends number, Acc extends number[] = []> = Acc['length'] extends N
|
|
36
|
+
? Acc[number]
|
|
37
|
+
: Enumerate<N, [...Acc, Acc['length']]>;
|
|
38
|
+
|
|
39
|
+
export type HTTPMethodRange<F extends number, T extends number> = Exclude<Enumerate<T>, Enumerate<F>>;
|
|
40
|
+
|
|
24
41
|
export { getJSONSchemaDefaults, parseResponse, prepareAuth, prepareParams, prepareServer };
|
|
25
42
|
|
|
26
43
|
export default class APICore {
|
|
@@ -35,7 +52,7 @@ export default class APICore {
|
|
|
35
52
|
variables?: Record<string, string | number>;
|
|
36
53
|
} = false;
|
|
37
54
|
|
|
38
|
-
private config: ConfigOptions = {
|
|
55
|
+
private config: ConfigOptions = {};
|
|
39
56
|
|
|
40
57
|
private userAgent: string;
|
|
41
58
|
|
|
@@ -88,21 +105,39 @@ export default class APICore {
|
|
|
88
105
|
}
|
|
89
106
|
}
|
|
90
107
|
|
|
108
|
+
// @ts-expect-error `this.auth` typing is off. FIXME
|
|
91
109
|
const har = oasToHar(this.spec, operation, data, prepareAuth(this.auth, operation));
|
|
92
110
|
|
|
93
|
-
|
|
94
|
-
|
|
111
|
+
let timeoutSignal: any;
|
|
112
|
+
const init: RequestInit = {};
|
|
113
|
+
if (this.config.timeout) {
|
|
114
|
+
const controller = new AbortController();
|
|
115
|
+
timeoutSignal = setTimeout(() => controller.abort(), this.config.timeout);
|
|
116
|
+
// @todo Typing on `AbortController` coming out of `node-abort-controler` isn't right so when
|
|
117
|
+
// we eventually drop that dependency we can remove the `as any` here.
|
|
118
|
+
init.signal = controller.signal as any;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return fetchHar(har as any, {
|
|
95
122
|
files: data.files || {},
|
|
123
|
+
init,
|
|
96
124
|
multipartEncoder: FormDataEncoder,
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
125
|
+
userAgent: this.userAgent,
|
|
126
|
+
})
|
|
127
|
+
.then(async (res: Response) => {
|
|
128
|
+
const parsed = await parseResponse(res);
|
|
129
|
+
|
|
130
|
+
if (res.status >= 400 && res.status <= 599) {
|
|
131
|
+
throw new FetchError(parsed.status, parsed.data, parsed.headers, parsed.res);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return parsed;
|
|
135
|
+
})
|
|
136
|
+
.finally(() => {
|
|
137
|
+
if (this.config.timeout) {
|
|
138
|
+
clearTimeout(timeoutSignal);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
106
141
|
});
|
|
107
142
|
}
|
|
108
143
|
}
|
|
@@ -8,13 +8,19 @@ export default async function getResponseBody(response: Response) {
|
|
|
8
8
|
|
|
9
9
|
const responseBody = await response.text();
|
|
10
10
|
|
|
11
|
+
let data = responseBody;
|
|
11
12
|
if (isJSON) {
|
|
12
13
|
try {
|
|
13
|
-
|
|
14
|
+
data = JSON.parse(responseBody);
|
|
14
15
|
} catch (e) {
|
|
15
16
|
// If our JSON parsing failed then we can just return plaintext instead.
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
return
|
|
20
|
+
return {
|
|
21
|
+
data,
|
|
22
|
+
status: response.status,
|
|
23
|
+
headers: response.headers,
|
|
24
|
+
res: response,
|
|
25
|
+
};
|
|
20
26
|
}
|
package/src/core/prepareAuth.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
/* eslint-disable no-underscore-dangle */
|
|
2
2
|
import type { Operation } from 'oas';
|
|
3
3
|
|
|
4
|
-
type SecurityType = 'Basic' | 'Bearer' | 'Query' | 'Header' | 'Cookie' | 'OAuth2' | 'http' | 'apiKey';
|
|
5
|
-
|
|
6
4
|
export default function prepareAuth(authKey: (number | string)[], operation: Operation) {
|
|
7
5
|
if (authKey.length === 0) {
|
|
8
6
|
return {};
|
|
@@ -18,25 +16,67 @@ export default function prepareAuth(authKey: (number | string)[], operation: Ope
|
|
|
18
16
|
}
|
|
19
17
|
> = {};
|
|
20
18
|
|
|
21
|
-
const security = operation.
|
|
22
|
-
|
|
23
|
-
const securitySchemes = Object.keys(security);
|
|
24
|
-
if (securitySchemes.length === 0) {
|
|
19
|
+
const security = operation.getSecurity();
|
|
20
|
+
if (security.length === 0) {
|
|
25
21
|
// If there's no auth configured on this operation, don't prepare anything (even if it was
|
|
26
22
|
// supplied by the user).
|
|
27
23
|
return {};
|
|
28
24
|
}
|
|
29
25
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
// Does this operation require multiple forms of auth?
|
|
27
|
+
if (security.every(s => Object.keys(s).length > 1)) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
"Sorry, this operation currently requires multiple forms of authentication which this library doesn't yet support."
|
|
30
|
+
);
|
|
31
|
+
}
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
// Since we can only handle single auth security configurations, let's pull those out. This code
|
|
34
|
+
// is a bit opaque but `security` here may look like `[{ basic: [] }, { oauth2: [], basic: []}]`
|
|
35
|
+
// and are filtering it down to only single-auth requirements of `[{ basic: [] }]`.
|
|
36
|
+
const usableSecurity = security
|
|
37
|
+
.map(s => {
|
|
38
|
+
return Object.keys(s).length === 1 ? s : false;
|
|
39
|
+
})
|
|
40
|
+
.filter(Boolean);
|
|
41
|
+
|
|
42
|
+
const usableSecuritySchemes = usableSecurity.map(s => Object.keys(s)).reduce((prev, next) => prev.concat(next), []);
|
|
43
|
+
const preparedSecurity = operation.prepareSecurity();
|
|
44
|
+
|
|
45
|
+
// If we have two auth tokens present let's look for Basic Auth in their configuration.
|
|
46
|
+
if (authKey.length >= 2) {
|
|
47
|
+
// If this operation doesn't support HTTP Basic auth but we have two tokens, that's a paddlin.
|
|
48
|
+
if (!('Basic' in preparedSecurity)) {
|
|
49
|
+
throw new Error('Multiple auth tokens were supplied for this endpoint but only a single token is needed.');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// If we have two auth keys for Basic Auth but Basic isn't a usable security scheme (maybe it's
|
|
53
|
+
// part of an AND or auth configuration -- which we don't support) then we need to error out.
|
|
54
|
+
const schemes = preparedSecurity.Basic.filter(s => usableSecuritySchemes.includes(s._key));
|
|
55
|
+
if (!schemes.length) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
'Credentials for Basic Authentication were supplied but this operation requires another form of auth in that case, which this library does not yet support. This operation does, however, allow supplying a single auth token.'
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const scheme = schemes.shift();
|
|
62
|
+
preparedAuth[scheme._key] = {
|
|
63
|
+
user: authKey[0],
|
|
64
|
+
pass: authKey.length === 2 ? authKey[1] : '',
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return preparedAuth;
|
|
36
68
|
}
|
|
37
69
|
|
|
38
|
-
|
|
70
|
+
// If we know we don't need to use HTTP Basic auth because we have a username+password then we
|
|
71
|
+
// can pick the first usable security scheme available and try to use that. This might not always
|
|
72
|
+
// be the auth scheme that the user wants, but we don't have any other way for the user to tell
|
|
73
|
+
// us what they want with the current `sdk.auth()` API.
|
|
74
|
+
const usableScheme = usableSecuritySchemes[0];
|
|
75
|
+
const schemes = Object.entries(preparedSecurity)
|
|
76
|
+
.map(([, ps]) => ps.filter(s => usableScheme === s._key))
|
|
77
|
+
.reduce((prev, next) => prev.concat(next), []);
|
|
39
78
|
|
|
79
|
+
const scheme = schemes.shift();
|
|
40
80
|
switch (scheme.type) {
|
|
41
81
|
case 'http':
|
|
42
82
|
if (scheme.scheme === 'basic') {
|
|
@@ -45,40 +85,24 @@ export default function prepareAuth(authKey: (number | string)[], operation: Ope
|
|
|
45
85
|
pass: authKey.length === 2 ? authKey[1] : '',
|
|
46
86
|
};
|
|
47
87
|
} else if (scheme.scheme === 'bearer') {
|
|
48
|
-
if (authKey.length > 1) {
|
|
49
|
-
throw new Error(
|
|
50
|
-
'Multiple auth tokens were supplied for the auth on this endpoint, but only a single token is needed.'
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
88
|
preparedAuth[scheme._key] = authKey[0];
|
|
55
89
|
}
|
|
56
90
|
break;
|
|
57
91
|
|
|
58
92
|
case 'oauth2':
|
|
59
|
-
if (authKey.length > 1) {
|
|
60
|
-
throw new Error(
|
|
61
|
-
'Multiple auth tokens were supplied for the auth on this endpoint, but only a single token is needed.'
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
93
|
preparedAuth[scheme._key] = authKey[0];
|
|
66
94
|
break;
|
|
67
95
|
|
|
68
96
|
case 'apiKey':
|
|
69
|
-
if (authKey.length > 1) {
|
|
70
|
-
throw new Error(
|
|
71
|
-
'Multiple auth keys were supplied for the auth on this endpoint, but only a single key is needed.'
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
97
|
if (scheme.in === 'query' || scheme.in === 'header' || scheme.in === 'cookie') {
|
|
76
98
|
preparedAuth[scheme._key] = authKey[0];
|
|
77
99
|
}
|
|
78
100
|
break;
|
|
79
101
|
|
|
80
102
|
default:
|
|
81
|
-
throw new Error(
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Sorry, this API currently uses a security scheme, ${scheme.type}, which this library doesn't yet support.`
|
|
105
|
+
);
|
|
82
106
|
}
|
|
83
107
|
|
|
84
108
|
return preparedAuth;
|
|
@@ -1,23 +1,30 @@
|
|
|
1
|
-
import type { Operation } from 'oas';
|
|
2
|
-
import type { ParameterObject, SchemaObject } from 'oas/@types/rmoas.types';
|
|
3
1
|
import type { ReadStream } from 'fs';
|
|
2
|
+
import type { Operation } from 'oas';
|
|
3
|
+
import type { ParameterObject, SchemaObject } from 'oas/dist/rmoas.types';
|
|
4
4
|
|
|
5
|
-
import lodashMerge from 'lodash.merge';
|
|
6
5
|
import fs from 'fs/promises';
|
|
7
6
|
import path from 'path';
|
|
8
7
|
import stream from 'stream';
|
|
9
|
-
|
|
10
|
-
import
|
|
8
|
+
|
|
9
|
+
import caseless from 'caseless';
|
|
11
10
|
import DatauriParser from 'datauri/parser';
|
|
11
|
+
import datauri from 'datauri/sync';
|
|
12
|
+
import getStream from 'get-stream';
|
|
13
|
+
import lodashMerge from 'lodash.merge';
|
|
14
|
+
import removeUndefinedObjects from 'remove-undefined-objects';
|
|
15
|
+
|
|
12
16
|
import getJSONSchemaDefaults from './getJSONSchemaDefaults';
|
|
13
17
|
|
|
18
|
+
// These headers are normally only defined by the OpenAPI definition but we allow the user to
|
|
19
|
+
// manually supply them in their `metadata` parameter if they wish.
|
|
20
|
+
const specialHeaders = ['accept', 'authorization'];
|
|
21
|
+
|
|
14
22
|
/**
|
|
15
23
|
* Extract all available parameters from an operations Parameter Object into a digestable array
|
|
16
24
|
* that we can use to apply to the request.
|
|
17
25
|
*
|
|
18
26
|
* @see {@link https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#parameterObject}
|
|
19
27
|
* @see {@link https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#parameterObject}
|
|
20
|
-
* @param parameters
|
|
21
28
|
*/
|
|
22
29
|
function digestParameters(parameters: ParameterObject[]): Record<string, ParameterObject> {
|
|
23
30
|
return parameters.reduce((prev, param) => {
|
|
@@ -65,8 +72,6 @@ function merge(src: any, target: any) {
|
|
|
65
72
|
* Ingest a file path or readable stream into a common object that we can later use to process it
|
|
66
73
|
* into a parameters object for making an API request.
|
|
67
74
|
*
|
|
68
|
-
* @param paramName
|
|
69
|
-
* @param file
|
|
70
75
|
*/
|
|
71
76
|
function processFile(
|
|
72
77
|
paramName: string,
|
|
@@ -133,20 +138,49 @@ function processFile(
|
|
|
133
138
|
* operation to see what's what and prepare any available parameters to be used in an API request
|
|
134
139
|
* with `@readme/oas-to-har`.
|
|
135
140
|
*
|
|
136
|
-
* @param operation
|
|
137
|
-
* @param body
|
|
138
|
-
* @param metadata
|
|
139
141
|
*/
|
|
140
142
|
export default async function prepareParams(operation: Operation, body?: unknown, metadata?: Record<string, unknown>) {
|
|
141
143
|
let metadataIntersected = false;
|
|
142
144
|
const digestedParameters = digestParameters(operation.getParameters());
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
+
const jsonSchema = operation.getParametersAsJSONSchema();
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* It might be common for somebody to run `sdk.findPetsByStatus({ status: 'available' }, {})`, in
|
|
149
|
+
* which case we want to filter out the second (metadata) parameter and treat the first parameter
|
|
150
|
+
* as the metadata instead. If we don't do this, their supplied `status` metadata will be treated
|
|
151
|
+
* as a body parameter, and because there's no `status` body parameter, and no supplied metadata
|
|
152
|
+
* (because it's an empty object), the request won't send a payload.
|
|
153
|
+
*
|
|
154
|
+
* @see {@link https://github.com/readmeio/api/issues/449}
|
|
155
|
+
*/
|
|
156
|
+
// eslint-disable-next-line no-param-reassign
|
|
157
|
+
metadata = removeUndefinedObjects(metadata);
|
|
145
158
|
|
|
146
159
|
if (!jsonSchema && (body !== undefined || metadata !== undefined)) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
160
|
+
let throwNoParamsError = true;
|
|
161
|
+
|
|
162
|
+
// If this operation doesn't have any parameters for us to transform to JSON Schema but they've
|
|
163
|
+
// sent us either an `Accept` or `Authorization` header (or both) we should let them do that.
|
|
164
|
+
// We should, however, only do this check for the `body` parameter as if they've sent this
|
|
165
|
+
// request both `body` and `metadata` we can reject it outright as the operation won't have any
|
|
166
|
+
// body data.
|
|
167
|
+
if (body !== undefined) {
|
|
168
|
+
if (typeof body === 'object' && body !== null && !Array.isArray(body)) {
|
|
169
|
+
if (Object.keys(body).length <= 2) {
|
|
170
|
+
const bodyParams = caseless(body);
|
|
171
|
+
|
|
172
|
+
if (specialHeaders.some(header => bodyParams.has(header))) {
|
|
173
|
+
throwNoParamsError = false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (throwNoParamsError) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
"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."
|
|
182
|
+
);
|
|
183
|
+
}
|
|
150
184
|
}
|
|
151
185
|
|
|
152
186
|
const jsonSchemaDefaults = jsonSchema ? getJSONSchemaDefaults(jsonSchema) : {};
|
|
@@ -176,27 +210,44 @@ export default async function prepareParams(operation: Operation, body?: unknown
|
|
|
176
210
|
} else if (typeof metadata === 'undefined') {
|
|
177
211
|
// No metadata was explicitly provided so we need to analyze the body to determine if it's a
|
|
178
212
|
// body or should be actually be treated as metadata.
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
213
|
+
const headerParams = caseless({});
|
|
214
|
+
Object.entries(digestedParameters).forEach(([paramName, param]) => {
|
|
215
|
+
// Headers are sent case-insensitive so we need to make sure that we're properly
|
|
216
|
+
// matching them when detecting what our incoming payload looks like.
|
|
217
|
+
if (param.in === 'header') {
|
|
218
|
+
headerParams.set(paramName, '');
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// `Accept` and `Authorization` headers can't be defined as normal parameters but we should
|
|
223
|
+
// always allow the user to supply them if they wish.
|
|
224
|
+
specialHeaders.forEach(header => {
|
|
225
|
+
if (!headerParams.has(header)) {
|
|
226
|
+
headerParams.set(header, '');
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const intersection = Object.keys(body).filter(value => {
|
|
231
|
+
if (Object.keys(digestedParameters).includes(value)) {
|
|
232
|
+
return true;
|
|
233
|
+
} else if (headerParams.has(value)) {
|
|
234
|
+
return true;
|
|
199
235
|
}
|
|
236
|
+
|
|
237
|
+
return false;
|
|
238
|
+
}).length;
|
|
239
|
+
|
|
240
|
+
if (intersection && intersection / Object.keys(body).length > 0.25) {
|
|
241
|
+
/* eslint-disable no-param-reassign */
|
|
242
|
+
// If more than 25% of the body intersects with the parameters that we've got on hand,
|
|
243
|
+
// then we should treat it as a metadata object and organize into parameters.
|
|
244
|
+
metadataIntersected = true;
|
|
245
|
+
metadata = merge(params.body, body) as Record<string, unknown>;
|
|
246
|
+
body = undefined;
|
|
247
|
+
/* eslint-enable no-param-reassign */
|
|
248
|
+
} else {
|
|
249
|
+
// For all other cases, we should just treat the supplied body as a body.
|
|
250
|
+
params.body = merge(params.body, body);
|
|
200
251
|
}
|
|
201
252
|
} else {
|
|
202
253
|
// Body and metadata were both supplied.
|
|
@@ -268,7 +319,7 @@ export default async function prepareParams(operation: Operation, body?: unknown
|
|
|
268
319
|
// Only spend time trying to organize metadata into parameters if we were able to digest
|
|
269
320
|
// parameters out of the operation schema. If we couldn't digest anything, but metadata was
|
|
270
321
|
// supplied then we wouldn't know how to send it in the request!
|
|
271
|
-
if (
|
|
322
|
+
if (typeof metadata !== 'undefined') {
|
|
272
323
|
if (!('cookie' in params)) params.cookie = {};
|
|
273
324
|
if (!('header' in params)) params.header = {};
|
|
274
325
|
if (!('path' in params)) params.path = {};
|
|
@@ -276,8 +327,16 @@ export default async function prepareParams(operation: Operation, body?: unknown
|
|
|
276
327
|
|
|
277
328
|
Object.entries(digestedParameters).forEach(([paramName, param]) => {
|
|
278
329
|
let value: any;
|
|
279
|
-
|
|
280
|
-
|
|
330
|
+
let metadataHeaderParam;
|
|
331
|
+
if (typeof metadata === 'object' && !isEmpty(metadata)) {
|
|
332
|
+
if (paramName in metadata) {
|
|
333
|
+
value = metadata[paramName];
|
|
334
|
+
} else if (param.in === 'header') {
|
|
335
|
+
// Headers are sent case-insensitive so we need to make sure that we're properly
|
|
336
|
+
// matching them when detecting what our incoming payload looks like.
|
|
337
|
+
metadataHeaderParam = Object.keys(metadata).find(k => k.toLowerCase() === paramName.toLowerCase());
|
|
338
|
+
value = metadata[metadataHeaderParam];
|
|
339
|
+
}
|
|
281
340
|
}
|
|
282
341
|
|
|
283
342
|
if (value === undefined) {
|
|
@@ -295,8 +354,8 @@ export default async function prepareParams(operation: Operation, body?: unknown
|
|
|
295
354
|
delete metadata[paramName];
|
|
296
355
|
break;
|
|
297
356
|
case 'header':
|
|
298
|
-
params.header[paramName] = value;
|
|
299
|
-
delete metadata[
|
|
357
|
+
params.header[paramName.toLowerCase()] = value;
|
|
358
|
+
delete metadata[metadataHeaderParam];
|
|
300
359
|
break;
|
|
301
360
|
case 'cookie':
|
|
302
361
|
params.cookie[paramName] = value;
|
|
@@ -321,6 +380,18 @@ export default async function prepareParams(operation: Operation, body?: unknown
|
|
|
321
380
|
if (!isEmpty(metadata)) {
|
|
322
381
|
if (operation.isFormUrlEncoded()) {
|
|
323
382
|
params.formData = merge(params.formData, metadata);
|
|
383
|
+
} else if (typeof metadata === 'object') {
|
|
384
|
+
// If the user supplied an `accept` or `authorization` header themselves we should allow it
|
|
385
|
+
// through. Normally these headers are automatically handled by `@readme/oas-to-har` but in
|
|
386
|
+
// the event that maybe the user wants to return XML for an API that normally returns JSON
|
|
387
|
+
// or specify a custom auth header (maybe we can't handle their auth case right) this is the
|
|
388
|
+
// only way with this library that they can do that.
|
|
389
|
+
specialHeaders.forEach(headerName => {
|
|
390
|
+
const headerParam = Object.keys(metadata).find(m => m.toLowerCase() === headerName);
|
|
391
|
+
if (headerParam) {
|
|
392
|
+
params.header[headerName] = metadata[headerParam] as string;
|
|
393
|
+
}
|
|
394
|
+
});
|
|
324
395
|
} else {
|
|
325
396
|
// Any other remaining unused metadata will be unused because we don't know where to place
|
|
326
397
|
// it in the request.
|
package/src/fetcher.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import type { OASDocument } from 'oas
|
|
1
|
+
import type { OASDocument } from 'oas/dist/rmoas.types';
|
|
2
2
|
|
|
3
|
-
import 'isomorphic-fetch';
|
|
4
|
-
import OpenAPIParser from '@readme/openapi-parser';
|
|
5
|
-
import yaml from 'js-yaml';
|
|
6
3
|
import fs from 'fs';
|
|
7
4
|
import path from 'path';
|
|
8
5
|
|
|
6
|
+
import OpenAPIParser from '@readme/openapi-parser';
|
|
7
|
+
import 'isomorphic-fetch';
|
|
8
|
+
import yaml from 'js-yaml';
|
|
9
|
+
|
|
9
10
|
export default class Fetcher {
|
|
10
11
|
uri: string | OASDocument;
|
|
11
12
|
|