api 5.0.0-beta.2 → 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 +0 -1
- package/dist/cache.d.ts +1 -1
- package/dist/cli/codegen/languages/typescript.d.ts +8 -6
- package/dist/cli/codegen/languages/typescript.js +83 -15
- 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 +42 -5
- package/dist/fetcher.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/packageInfo.d.ts +1 -1
- package/dist/packageInfo.js +1 -1
- package/package.json +6 -3
- package/src/cache.ts +1 -1
- package/src/cli/codegen/languages/typescript.ts +96 -26
- 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 +43 -5
- package/src/fetcher.ts +1 -1
- package/src/index.ts +1 -1
- package/src/packageInfo.ts +1 -1
package/README.md
CHANGED
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
|
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
|
|
@@ -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
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "api",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.3",
|
|
4
4
|
"description": "Magical SDK generation from an OpenAPI definition 🪄",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@readme/oas-to-har": "^17.0.8",
|
|
40
40
|
"@readme/openapi-parser": "^2.2.0",
|
|
41
|
+
"caseless": "^0.12.0",
|
|
41
42
|
"chalk": "^4.1.2",
|
|
42
43
|
"commander": "^9.2.0",
|
|
43
44
|
"datauri": "^4.1.0",
|
|
@@ -54,16 +55,18 @@
|
|
|
54
55
|
"json-schema-traverse": "^1.0.0",
|
|
55
56
|
"lodash.merge": "^4.6.2",
|
|
56
57
|
"make-dir": "^3.1.0",
|
|
57
|
-
"oas": "^18.3.
|
|
58
|
+
"oas": "^18.3.4",
|
|
58
59
|
"object-hash": "^3.0.0",
|
|
59
60
|
"ora": "^5.4.1",
|
|
60
61
|
"prompts": "^2.4.2",
|
|
62
|
+
"remove-undefined-objects": "^2.0.1",
|
|
61
63
|
"ssri": "^9.0.0",
|
|
62
64
|
"ts-morph": "^15.1.0",
|
|
63
65
|
"validate-npm-package-name": "^4.0.0"
|
|
64
66
|
},
|
|
65
67
|
"devDependencies": {
|
|
66
68
|
"@readme/oas-examples": "^5.4.1",
|
|
69
|
+
"@types/caseless": "^0.12.2",
|
|
67
70
|
"@types/chai": "^4.3.1",
|
|
68
71
|
"@types/find-cache-dir": "^3.2.1",
|
|
69
72
|
"@types/js-yaml": "^4.0.5",
|
|
@@ -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) {
|
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
|
/**
|
|
@@ -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
package/src/index.ts
CHANGED
package/src/packageInfo.ts
CHANGED