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 CHANGED
@@ -31,7 +31,6 @@ $ npx api install https://raw.githubusercontent.com/OAI/OpenAPI-Specification/ma
31
31
  ```js
32
32
  const SDK = require('@api/petstore');
33
33
 
34
- const petstore = new SDK();
35
34
  petstore.listPets().then(res => {
36
35
  console.log(`My pets name is ${res[0].name}!`);
37
36
  });
package/dist/cache.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { OASDocument } from 'oas/@types/rmoas.types';
1
+ import type { OASDocument } from 'oas/dist/rmoas.types';
2
2
  import 'isomorphic-fetch';
3
3
  import Fetcher from './fetcher';
4
4
  declare type CacheStore = Record<string, {
@@ -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/@types/rmoas.types';
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, outDir: 'dist' }, (options.compilerTarget === 'cjs' ? { esModuleInterop: true } : {}))
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
- // There's an annoying quirk with `ts-morph` where if we set the SDK class to be the default
199
- // export with `isDefaultExport` then when we compile it to an ES5 target for CJS environments
200
- // it'll be exported as `export.default = SDK`, which when you try to load it you'll need to
201
- // run `require('@api/sdk').default`.
202
- //
203
- // Instead here by plainly creating the SDK class in the source file and then setting this
204
- // export assignment it'll export the SDK class as `module.exports = SDK` so people can cleanly
205
- // load the SDK with `require('@api/sdk)`.
206
- //
207
- // A whole lot of debugging went into here to let people not have to worry about `.default`
208
- // messes. I hope it's worth it!
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: 'SDK'
226
+ expression: 'createSDK'
212
227
  });
213
228
  }
214
229
  else {
215
- this.sdk.setIsDefaultExport(true);
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
- sdkSource.addStatements(exp);
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
@@ -1,4 +1,4 @@
1
- import type { OASDocument } from 'oas/@types/rmoas.types';
1
+ import type { OASDocument } from 'oas/dist/rmoas.types';
2
2
  import Fetcher from '../fetcher';
3
3
  export default class Storage {
4
4
  static dir: string;
@@ -1,4 +1,4 @@
1
- import type { SchemaWrapper } from 'oas/@types/operation/get-parameters-as-json-schema';
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.
@@ -1,6 +1,6 @@
1
1
  import type Oas from 'oas';
2
2
  import type { Operation } from 'oas';
3
- import type { HttpMethods } from 'oas/@types/rmoas.types';
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
- intersection = Object.keys(body).filter(function (value) { return Object.keys(digestedParameters).includes(value); }).length;
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) && paramName in metadata) {
294
- value = metadata[paramName];
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/@types/rmoas.types';
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;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { OASDocument } from 'oas/@types/rmoas.types';
1
+ import type { OASDocument } from 'oas/dist/rmoas.types';
2
2
  interface SDKOptions {
3
3
  cacheDir?: string;
4
4
  }
@@ -1,2 +1,2 @@
1
1
  export declare const PACKAGE_NAME = "api";
2
- export declare const PACKAGE_VERSION = "5.0.0-beta.2";
2
+ export declare const PACKAGE_VERSION = "5.0.0-beta.3";
@@ -3,4 +3,4 @@ exports.__esModule = true;
3
3
  exports.PACKAGE_VERSION = exports.PACKAGE_NAME = void 0;
4
4
  // This file is automatically updated by the build script.
5
5
  exports.PACKAGE_NAME = 'api';
6
- exports.PACKAGE_VERSION = '5.0.0-beta.2';
6
+ exports.PACKAGE_VERSION = '5.0.0-beta.3';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "api",
3
- "version": "5.0.0-beta.2",
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.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": "aa738b1bf46b447afed38507d3fc49acbbad6309"
97
+ "gitHead": "24d5b83545735176786d212a69121a029cf6dea1"
95
98
  }
package/src/cache.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { OASDocument } from 'oas/@types/rmoas.types';
1
+ import type { OASDocument } from 'oas/dist/rmoas.types';
2
2
 
3
3
  import 'isomorphic-fetch';
4
4
  import OpenAPIParser from '@readme/openapi-parser';
@@ -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/@types/rmoas.types';
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
- // There's an annoying quirk with `ts-morph` where if we set the SDK class to be the default
190
- // export with `isDefaultExport` then when we compile it to an ES5 target for CJS environments
191
- // it'll be exported as `export.default = SDK`, which when you try to load it you'll need to
192
- // run `require('@api/sdk').default`.
193
- //
194
- // Instead here by plainly creating the SDK class in the source file and then setting this
195
- // export assignment it'll export the SDK class as `module.exports = SDK` so people can cleanly
196
- // load the SDK with `require('@api/sdk)`.
197
- //
198
- // A whole lot of debugging went into here to let people not have to worry about `.default`
199
- // messes. I hope it's worth it!
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: 'SDK',
218
+ expression: 'createSDK',
203
219
  });
204
220
  } else {
205
- this.sdk.setIsDefaultExport(true);
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
- sdkSource.addStatements(exp);
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) {
@@ -1,4 +1,4 @@
1
- import type { OASDocument } from 'oas/@types/rmoas.types';
1
+ import type { OASDocument } from 'oas/dist/rmoas.types';
2
2
 
3
3
  import ssri from 'ssri';
4
4
  import fs from 'fs';
@@ -1,5 +1,5 @@
1
- import type { SchemaObject } from 'oas/@types/rmoas.types';
2
- import type { SchemaWrapper } from 'oas/@types/operation/get-parameters-as-json-schema';
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,6 +1,6 @@
1
1
  import type Oas from 'oas';
2
2
  import type { Operation } from 'oas';
3
- import type { HttpMethods } from 'oas/@types/rmoas.types';
3
+ import type { HttpMethods } from 'oas/dist/rmoas.types';
4
4
 
5
5
  import 'isomorphic-fetch';
6
6
  import fetchHar from 'fetch-har';
@@ -1,5 +1,5 @@
1
1
  import type { Operation } from 'oas';
2
- import type { ParameterObject, SchemaObject } from 'oas/@types/rmoas.types';
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 intersection = Object.keys(body).filter(value => Object.keys(digestedParameters).includes(value)).length;
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) && paramName in metadata) {
280
- value = metadata[paramName];
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/@types/rmoas.types';
1
+ import type { OASDocument } from 'oas/dist/rmoas.types';
2
2
 
3
3
  import 'isomorphic-fetch';
4
4
  import OpenAPIParser from '@readme/openapi-parser';
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Operation } from 'oas';
2
- import type { HttpMethods, OASDocument } from 'oas/@types/rmoas.types';
2
+ import type { HttpMethods, OASDocument } from 'oas/dist/rmoas.types';
3
3
  import type { ConfigOptions } from './core';
4
4
 
5
5
  import Oas from 'oas';
@@ -1,3 +1,3 @@
1
1
  // This file is automatically updated by the build script.
2
2
  export const PACKAGE_NAME = 'api';
3
- export const PACKAGE_VERSION = '5.0.0-beta.2';
3
+ export const PACKAGE_VERSION = '5.0.0-beta.3';