api 6.0.0 → 6.1.1
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 +1 -1
- package/dist/cache.d.ts +4 -1
- package/dist/cli/codegen/languages/typescript/util.d.ts +0 -1
- package/dist/cli/codegen/languages/typescript/util.js +1 -10
- package/dist/cli/codegen/languages/typescript.d.ts +22 -5
- package/dist/cli/codegen/languages/typescript.js +53 -17
- package/dist/cli/commands/install.js +33 -31
- package/dist/cli/storage.d.ts +10 -10
- package/dist/cli/storage.js +14 -1
- package/dist/core/errors/fetchError.d.ts +4 -4
- package/dist/core/getJSONSchemaDefaults.d.ts +0 -1
- package/dist/core/getJSONSchemaDefaults.js +0 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/prepareAuth.d.ts +1 -1
- package/dist/core/prepareParams.js +8 -5
- package/dist/core/prepareServer.d.ts +0 -3
- package/dist/core/prepareServer.js +0 -3
- package/dist/fetcher.js +0 -1
- package/dist/index.js +0 -1
- package/dist/packageInfo.d.ts +1 -1
- package/dist/packageInfo.js +1 -1
- package/package.json +9 -22
- package/src/cache.ts +4 -1
- package/src/cli/codegen/index.ts +1 -1
- package/src/cli/codegen/language.ts +1 -1
- package/src/cli/codegen/languages/typescript/util.ts +0 -9
- package/src/cli/codegen/languages/typescript.ts +89 -27
- package/src/cli/commands/install.ts +14 -17
- package/src/cli/lib/prompt.ts +1 -1
- package/src/cli/storage.ts +27 -10
- package/src/core/errors/fetchError.ts +4 -4
- package/src/core/getJSONSchemaDefaults.ts +2 -3
- package/src/core/index.ts +7 -2
- package/src/core/prepareAuth.ts +4 -4
- package/src/core/prepareParams.ts +15 -10
- package/src/core/prepareServer.ts +0 -3
- package/src/fetcher.ts +2 -3
- package/src/index.ts +0 -1
- package/src/packageInfo.ts +1 -1
- package/tsconfig.json +1 -1
package/dist/index.js
CHANGED
|
@@ -70,7 +70,6 @@ var Sdk = /** @class */ (function () {
|
|
|
70
70
|
* Create dynamic accessors for every operation with a defined operation ID. If an operation
|
|
71
71
|
* does not have an operation ID it can be accessed by its `.method('/path')` accessor instead.
|
|
72
72
|
*
|
|
73
|
-
* @param spec
|
|
74
73
|
*/
|
|
75
74
|
function loadOperations(spec) {
|
|
76
75
|
return Object.entries(spec.getPaths())
|
package/dist/packageInfo.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export declare const PACKAGE_NAME = "api";
|
|
2
|
-
export declare const PACKAGE_VERSION = "6.
|
|
2
|
+
export declare const PACKAGE_VERSION = "6.1.1";
|
package/dist/packageInfo.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "api",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.1.1",
|
|
4
4
|
"description": "Magical SDK generation from an OpenAPI definition 🪄",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"build": "tsc",
|
|
12
12
|
"debug:bin": "node -r ts-node/register src/bin.ts",
|
|
13
|
+
"lint:types": "tsc --noEmit",
|
|
13
14
|
"prebuild": "rm -rf dist/; npm run version",
|
|
14
15
|
"prepack": "npm run build",
|
|
15
|
-
"test": "
|
|
16
|
-
"test:smoke": "
|
|
16
|
+
"test": "vitest --coverage",
|
|
17
|
+
"test:smoke": "vitest --config=vitest-smoketest.config.ts ",
|
|
17
18
|
"version": "node -p \"'// This file is automatically updated by the build script.\\nexport const PACKAGE_NAME = \\'' + require('./package.json').name + '\\';\\nexport const PACKAGE_VERSION = \\'' + require('./package.json').version + '\\';'\" > src/packageInfo.ts; git add src/packageInfo.ts"
|
|
18
19
|
},
|
|
19
20
|
"repository": {
|
|
@@ -63,7 +64,6 @@
|
|
|
63
64
|
"node-abort-controller": "^3.1.1",
|
|
64
65
|
"oas": "^20.4.0",
|
|
65
66
|
"ora": "^5.4.1",
|
|
66
|
-
"prettier": "^2.8.3",
|
|
67
67
|
"prompts": "^2.4.2",
|
|
68
68
|
"remove-undefined-objects": "^2.0.2",
|
|
69
69
|
"semver": "^7.3.8",
|
|
@@ -74,7 +74,6 @@
|
|
|
74
74
|
"devDependencies": {
|
|
75
75
|
"@readme/oas-examples": "^5.9.0",
|
|
76
76
|
"@types/caseless": "^0.12.2",
|
|
77
|
-
"@types/chai": "^4.3.4",
|
|
78
77
|
"@types/find-cache-dir": "^3.2.1",
|
|
79
78
|
"@types/js-yaml": "^4.0.5",
|
|
80
79
|
"@types/lodash.camelcase": "^4.3.7",
|
|
@@ -82,31 +81,19 @@
|
|
|
82
81
|
"@types/lodash.merge": "^4.6.7",
|
|
83
82
|
"@types/lodash.setwith": "^4.3.7",
|
|
84
83
|
"@types/lodash.startcase": "^4.4.7",
|
|
85
|
-
"@types/mocha": "^10.0.1",
|
|
86
84
|
"@types/prettier": "^2.7.2",
|
|
87
85
|
"@types/prompts": "^2.4.2",
|
|
88
86
|
"@types/semver": "^7.3.13",
|
|
89
|
-
"@types/sinon-chai": "^3.2.9",
|
|
90
87
|
"@types/ssri": "^7.1.1",
|
|
91
88
|
"@types/validate-npm-package-name": "^4.0.0",
|
|
92
|
-
"
|
|
89
|
+
"@vitest/coverage-v8": "^0.34.1",
|
|
93
90
|
"fetch-mock": "^9.11.0",
|
|
94
|
-
"mocha": "^10.1.0",
|
|
95
|
-
"mock-require": "^3.0.3",
|
|
96
|
-
"nyc": "^15.1.0",
|
|
97
91
|
"oas-normalize": "^8.3.2",
|
|
98
|
-
"
|
|
99
|
-
"sinon-chai": "^3.7.0",
|
|
100
|
-
"type-fest": "^3.5.4",
|
|
92
|
+
"type-fest": "^4.2.0",
|
|
101
93
|
"typescript": "^4.9.5",
|
|
102
|
-
"unique-temp-dir": "^1.0.0"
|
|
94
|
+
"unique-temp-dir": "^1.0.0",
|
|
95
|
+
"vitest": "^0.34.1"
|
|
103
96
|
},
|
|
104
97
|
"prettier": "@readme/eslint-config/prettier",
|
|
105
|
-
"
|
|
106
|
-
"exclude": [
|
|
107
|
-
"dist/",
|
|
108
|
-
"test/"
|
|
109
|
-
]
|
|
110
|
-
},
|
|
111
|
-
"gitHead": "9759db4c067baf3928ad3a9d76b4965658955d78"
|
|
98
|
+
"gitHead": "e440a28841e753895bfb5315b4d31f679824fa39"
|
|
112
99
|
}
|
package/src/cache.ts
CHANGED
|
@@ -16,8 +16,11 @@ type CacheStore = Record<
|
|
|
16
16
|
string,
|
|
17
17
|
{
|
|
18
18
|
hash: string;
|
|
19
|
-
path?: string; // Deprecated in v4.5.0 in favor of `hash`.
|
|
20
19
|
original: string | OASDocument;
|
|
20
|
+
/**
|
|
21
|
+
* @deprecated Deprecated in v4.5.0 in favor of `hash`.
|
|
22
|
+
*/
|
|
23
|
+
path?: string;
|
|
21
24
|
title?: string;
|
|
22
25
|
version?: string;
|
|
23
26
|
}
|
package/src/cli/codegen/index.ts
CHANGED
|
@@ -49,7 +49,7 @@ export default abstract class CodeGeneratorLanguage {
|
|
|
49
49
|
*/
|
|
50
50
|
if (JSON.stringify(spec.api).includes('"$ref":"#/')) {
|
|
51
51
|
throw new Error(
|
|
52
|
-
'Sorry, this library does not yet support generating an SDK for an OpenAPI definition that contains circular references.'
|
|
52
|
+
'Sorry, this library does not yet support generating an SDK for an OpenAPI definition that contains circular references.',
|
|
53
53
|
);
|
|
54
54
|
}
|
|
55
55
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import camelCase from 'lodash.camelcase';
|
|
2
2
|
import deburr from 'lodash.deburr';
|
|
3
3
|
import startCase from 'lodash.startcase';
|
|
4
|
-
import { format as prettier } from 'prettier';
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* This is a mix of reserved JS words and keywords in TypeScript that might be reserved or
|
|
@@ -100,14 +99,6 @@ const RESERVED_WORDS = [
|
|
|
100
99
|
'window',
|
|
101
100
|
];
|
|
102
101
|
|
|
103
|
-
export function formatter(content: string) {
|
|
104
|
-
return prettier(content, {
|
|
105
|
-
parser: 'typescript',
|
|
106
|
-
printWidth: 100,
|
|
107
|
-
singleQuote: true,
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
102
|
/**
|
|
112
103
|
* @see {@link https://www.30secondsofcode.org/js/s/word-wrap}
|
|
113
104
|
*/
|
|
@@ -3,7 +3,13 @@ import type { InstallerOptions } from '../language';
|
|
|
3
3
|
import type Oas from 'oas';
|
|
4
4
|
import type { Operation } from 'oas';
|
|
5
5
|
import type { HttpMethods, SchemaObject } from 'oas/dist/rmoas.types';
|
|
6
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
ClassDeclaration,
|
|
8
|
+
JSDocStructure,
|
|
9
|
+
JSDocTagStructure,
|
|
10
|
+
OptionalKind,
|
|
11
|
+
ParameterDeclarationStructure,
|
|
12
|
+
} from 'ts-morph';
|
|
7
13
|
import type { PackageJson } from 'type-fest';
|
|
8
14
|
|
|
9
15
|
import fs from 'fs';
|
|
@@ -17,19 +23,25 @@ import { IndentationText, Project, QuoteKind, ScriptTarget, VariableDeclarationK
|
|
|
17
23
|
import logger from '../../logger';
|
|
18
24
|
import CodeGeneratorLanguage from '../language';
|
|
19
25
|
|
|
20
|
-
import { docblockEscape,
|
|
26
|
+
import { docblockEscape, generateTypeName, wordWrap } from './typescript/util';
|
|
21
27
|
|
|
22
28
|
export interface TSGeneratorOptions {
|
|
23
|
-
outputJS?: boolean;
|
|
24
29
|
compilerTarget?: 'cjs' | 'esm';
|
|
30
|
+
outputJS?: boolean;
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
interface OperationTypeHousing {
|
|
34
|
+
operation: Operation;
|
|
28
35
|
types: {
|
|
29
36
|
params?: false | Record<'body' | 'formData' | 'metadata', string>;
|
|
30
|
-
responses?: Record<
|
|
37
|
+
responses?: Record<
|
|
38
|
+
string | number,
|
|
39
|
+
{
|
|
40
|
+
description?: string;
|
|
41
|
+
type: string;
|
|
42
|
+
}
|
|
43
|
+
>;
|
|
31
44
|
};
|
|
32
|
-
operation: Operation;
|
|
33
45
|
}
|
|
34
46
|
|
|
35
47
|
export default class TSGenerator extends CodeGeneratorLanguage {
|
|
@@ -60,7 +72,7 @@ export default class TSGenerator extends CodeGeneratorLanguage {
|
|
|
60
72
|
usesHTTPMethodRangeInterface = false;
|
|
61
73
|
|
|
62
74
|
constructor(spec: Oas, specPath: string, identifier: string, opts: TSGeneratorOptions = {}) {
|
|
63
|
-
const options: {
|
|
75
|
+
const options: { compilerTarget: 'cjs' | 'esm'; outputJS: boolean } = {
|
|
64
76
|
outputJS: false,
|
|
65
77
|
compilerTarget: 'cjs',
|
|
66
78
|
...opts,
|
|
@@ -233,7 +245,7 @@ export default class TSGenerator extends CodeGeneratorLanguage {
|
|
|
233
245
|
return {};
|
|
234
246
|
}
|
|
235
247
|
|
|
236
|
-
let code =
|
|
248
|
+
let code = sourceFile.text;
|
|
237
249
|
if (file === 'index.js' && this.compilerTarget === 'cjs') {
|
|
238
250
|
/**
|
|
239
251
|
* There's an annoying quirk with `ts-morph` where if we're exporting a default export
|
|
@@ -259,7 +271,7 @@ export default class TSGenerator extends CodeGeneratorLanguage {
|
|
|
259
271
|
|
|
260
272
|
return [
|
|
261
273
|
...this.project.getSourceFiles().map(sourceFile => ({
|
|
262
|
-
[sourceFile.getBaseName()]:
|
|
274
|
+
[sourceFile.getBaseName()]: sourceFile.getFullText(),
|
|
263
275
|
})),
|
|
264
276
|
|
|
265
277
|
// Because we're returning the raw source files for TS generation we also need to separately
|
|
@@ -269,7 +281,7 @@ export default class TSGenerator extends CodeGeneratorLanguage {
|
|
|
269
281
|
.emitToMemory({ emitOnlyDtsFiles: true })
|
|
270
282
|
.getFiles()
|
|
271
283
|
.map(sourceFile => ({
|
|
272
|
-
[path.basename(sourceFile.filePath)]:
|
|
284
|
+
[path.basename(sourceFile.filePath)]: sourceFile.text,
|
|
273
285
|
})),
|
|
274
286
|
].reduce((prev, next) => Object.assign(prev, next));
|
|
275
287
|
}
|
|
@@ -328,7 +340,7 @@ export default class TSGenerator extends CodeGeneratorLanguage {
|
|
|
328
340
|
{
|
|
329
341
|
tagName: 'param',
|
|
330
342
|
text: wordWrap(
|
|
331
|
-
'config.timeout Override the default `fetch` request timeout of 30 seconds. This number should be represented in milliseconds.'
|
|
343
|
+
'config.timeout Override the default `fetch` request timeout of 30 seconds. This number should be represented in milliseconds.',
|
|
332
344
|
),
|
|
333
345
|
},
|
|
334
346
|
],
|
|
@@ -358,7 +370,7 @@ sdk.auth('username', 'password');
|
|
|
358
370
|
sdk.auth('myBearerToken');
|
|
359
371
|
|
|
360
372
|
@example <caption>API Keys</caption>
|
|
361
|
-
sdk.auth('myApiKey');`)
|
|
373
|
+
sdk.auth('myApiKey');`),
|
|
362
374
|
),
|
|
363
375
|
tags: [
|
|
364
376
|
{ tagName: 'see', text: '{@link https://spec.openapis.org/oas/v3.0.3#fixed-fields-22}' },
|
|
@@ -391,7 +403,7 @@ sdk.server('https://{region}.api.example.com/{basePath}', {
|
|
|
391
403
|
});
|
|
392
404
|
|
|
393
405
|
@example <caption>Fully qualified server URL</caption>
|
|
394
|
-
sdk.server('https://eu.api.example.com/v14');`)
|
|
406
|
+
sdk.server('https://eu.api.example.com/v14');`),
|
|
395
407
|
),
|
|
396
408
|
tags: [
|
|
397
409
|
{ tagName: 'param', text: 'url Server URL' },
|
|
@@ -502,6 +514,20 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
502
514
|
return sourceFile;
|
|
503
515
|
}
|
|
504
516
|
|
|
517
|
+
/**
|
|
518
|
+
* Add a new JSDoc `@tag` to an existing docblock.
|
|
519
|
+
*
|
|
520
|
+
*/
|
|
521
|
+
static addTagToDocblock(docblock: OptionalKind<JSDocStructure>, tag: OptionalKind<JSDocTagStructure>) {
|
|
522
|
+
const tags = docblock.tags ?? [];
|
|
523
|
+
tags.push(tag);
|
|
524
|
+
|
|
525
|
+
return {
|
|
526
|
+
...docblock,
|
|
527
|
+
tags,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
505
531
|
/**
|
|
506
532
|
* Create operation accessors on the SDK.
|
|
507
533
|
*
|
|
@@ -510,9 +536,9 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
510
536
|
operation: Operation,
|
|
511
537
|
operationId: string,
|
|
512
538
|
paramTypes?: OperationTypeHousing['types']['params'],
|
|
513
|
-
responseTypes?: OperationTypeHousing['types']['responses']
|
|
539
|
+
responseTypes?: OperationTypeHousing['types']['responses'],
|
|
514
540
|
) {
|
|
515
|
-
|
|
541
|
+
let docblock: OptionalKind<JSDocStructure> = {};
|
|
516
542
|
const summary = operation.getSummary();
|
|
517
543
|
const description = operation.getDescription();
|
|
518
544
|
if (summary || description) {
|
|
@@ -531,7 +557,10 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
531
557
|
};
|
|
532
558
|
|
|
533
559
|
if (summary && description) {
|
|
534
|
-
docblock
|
|
560
|
+
docblock = TSGenerator.addTagToDocblock(docblock, {
|
|
561
|
+
tagName: 'summary',
|
|
562
|
+
text: docblockEscape(wordWrap(summary)),
|
|
563
|
+
});
|
|
535
564
|
}
|
|
536
565
|
}
|
|
537
566
|
|
|
@@ -568,8 +597,8 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
568
597
|
|
|
569
598
|
let returnType = 'Promise<FetchResponse<number, unknown>>';
|
|
570
599
|
if (responseTypes) {
|
|
571
|
-
|
|
572
|
-
.map(([status, responseType]) => {
|
|
600
|
+
const returnTypes = Object.entries(responseTypes)
|
|
601
|
+
.map(([status, { description: responseDescription, type: responseType }]) => {
|
|
573
602
|
if (status.toLowerCase() === 'default') {
|
|
574
603
|
return `FetchResponse<number, ${responseType}>`;
|
|
575
604
|
} else if (status.length === 3 && status.toUpperCase().endsWith('XX')) {
|
|
@@ -580,19 +609,54 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
580
609
|
return `FetchResponse<number, ${responseType}>`;
|
|
581
610
|
}
|
|
582
611
|
|
|
612
|
+
if (Number(statusPrefix) >= 4) {
|
|
613
|
+
docblock = TSGenerator.addTagToDocblock(docblock, {
|
|
614
|
+
tagName: 'throws',
|
|
615
|
+
text: `FetchError<${status}, ${responseType}>${
|
|
616
|
+
responseDescription ? docblockEscape(wordWrap(` ${responseDescription}`)) : ''
|
|
617
|
+
}`,
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
|
|
583
623
|
this.usesHTTPMethodRangeInterface = true;
|
|
584
624
|
return `FetchResponse<HTTPMethodRange<${statusPrefix}00, ${statusPrefix}99>, ${responseType}>`;
|
|
585
625
|
}
|
|
586
626
|
|
|
627
|
+
// 400 and 500 status code families are thrown as exceptions so adding them as a possible
|
|
628
|
+
// return type isn't valid.
|
|
629
|
+
if (Number(status) >= 400) {
|
|
630
|
+
docblock = TSGenerator.addTagToDocblock(docblock, {
|
|
631
|
+
tagName: 'throws',
|
|
632
|
+
text: `FetchError<${status}, ${responseType}>${
|
|
633
|
+
responseDescription ? docblockEscape(wordWrap(` ${responseDescription}`)) : ''
|
|
634
|
+
}`,
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
return false;
|
|
638
|
+
}
|
|
639
|
+
|
|
587
640
|
return `FetchResponse<${status}, ${responseType}>`;
|
|
588
641
|
})
|
|
589
|
-
.
|
|
642
|
+
.filter(Boolean)
|
|
643
|
+
.join(' | ');
|
|
644
|
+
|
|
645
|
+
// If all of our documented responses are for error status codes then all we can document for
|
|
646
|
+
// anything else that might happen is `unknown`.
|
|
647
|
+
returnType = `Promise<${returnTypes.length ? returnTypes : 'FetchResponse<number, unknown>'}>`;
|
|
590
648
|
}
|
|
591
649
|
|
|
650
|
+
const shouldAddAltTypedOverloads = Object.keys(parameters).length === 2 && hasOptionalBody && !hasOptionalMetadata;
|
|
592
651
|
const operationIdAccessor = this.sdk.addMethod({
|
|
593
652
|
name: operationId,
|
|
594
653
|
returnType,
|
|
595
|
-
|
|
654
|
+
|
|
655
|
+
// If we're going to be creating typed method overloads for optional body an metadata handling
|
|
656
|
+
// we should only add a docblock to the first overload we create because IDE Intellisense will
|
|
657
|
+
// always use that and adding a docblock to all three will bloat the SDK with unused and
|
|
658
|
+
// unsurfaced method documentation.
|
|
659
|
+
docs: shouldAddAltTypedOverloads ? null : Object.keys(docblock).length ? [docblock] : null,
|
|
596
660
|
statements: writer => {
|
|
597
661
|
/**
|
|
598
662
|
* @example return this.core.fetch('/pet/findByStatus', 'get', body, metadata);
|
|
@@ -612,7 +676,7 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
612
676
|
}
|
|
613
677
|
|
|
614
678
|
fetchStmt.write(arg.name);
|
|
615
|
-
if (
|
|
679
|
+
if (i !== totalParams - 1) {
|
|
616
680
|
fetchStmt.write(', ');
|
|
617
681
|
}
|
|
618
682
|
});
|
|
@@ -626,10 +690,6 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
626
690
|
// If we have both body and metadata parameters but only body is optional we need to create
|
|
627
691
|
// a couple function overloads as Typescript doesn't let us have an optional method parameter
|
|
628
692
|
// come before one that's required.
|
|
629
|
-
//
|
|
630
|
-
// None of these accessor overloads will receive a docblock because the original will have
|
|
631
|
-
// that covered.
|
|
632
|
-
const shouldAddAltTypedOverloads = Object.keys(parameters).length === 2 && hasOptionalBody && !hasOptionalMetadata;
|
|
633
693
|
if (shouldAddAltTypedOverloads) {
|
|
634
694
|
// Create an overload that has both `body` and `metadata` parameters as required.
|
|
635
695
|
operationIdAccessor.addOverload({
|
|
@@ -645,7 +705,6 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
645
705
|
operationIdAccessor.addOverload({
|
|
646
706
|
parameters: [{ ...parameters.metadata }],
|
|
647
707
|
returnType,
|
|
648
|
-
docs: Object.keys(docblock).length ? [docblock] : null,
|
|
649
708
|
});
|
|
650
709
|
|
|
651
710
|
// Create an overload that has both `body` and `metadata` parameters as optional. Even though
|
|
@@ -807,7 +866,7 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
807
866
|
.reduce((prev, next) => Object.assign(prev, next));
|
|
808
867
|
|
|
809
868
|
const res = Object.entries(schemas)
|
|
810
|
-
.map(([status, { schema }]) => {
|
|
869
|
+
.map(([status, { description, schema }]) => {
|
|
811
870
|
let typeName;
|
|
812
871
|
|
|
813
872
|
if (typeof schema === 'string' && schema.startsWith('::convert::')) {
|
|
@@ -826,7 +885,10 @@ sdk.server('https://eu.api.example.com/v14');`)
|
|
|
826
885
|
return {
|
|
827
886
|
// Types are prefixed with `types.` because that's how we're importing them from
|
|
828
887
|
// `types.d.ts`.
|
|
829
|
-
[status]:
|
|
888
|
+
[status]: {
|
|
889
|
+
type: `types.${typeName}`,
|
|
890
|
+
description,
|
|
891
|
+
},
|
|
830
892
|
};
|
|
831
893
|
})
|
|
832
894
|
.reduce((prev, next) => Object.assign(prev, next), {});
|
|
@@ -4,7 +4,6 @@ import { Command, Option } from 'commander';
|
|
|
4
4
|
import figures from 'figures';
|
|
5
5
|
import Oas from 'oas';
|
|
6
6
|
import ora from 'ora';
|
|
7
|
-
import validateNPMPackageName from 'validate-npm-package-name';
|
|
8
7
|
|
|
9
8
|
import Fetcher from '../../fetcher';
|
|
10
9
|
import codegen from '../codegen';
|
|
@@ -18,16 +17,17 @@ cmd
|
|
|
18
17
|
.name('install')
|
|
19
18
|
.description('install an API SDK into your codebase')
|
|
20
19
|
.argument('<uri>', 'an API to install')
|
|
20
|
+
.option('-i, --identifier <identifier>', 'API identifier (eg. `@api/petstore`)')
|
|
21
21
|
.addOption(
|
|
22
22
|
new Option('-l, --lang <language>', 'SDK language').choices([
|
|
23
23
|
'js', // User generally wants JS, we'll prompt if they want CJS or ESM files.
|
|
24
24
|
'js-cjs',
|
|
25
25
|
'js-esm',
|
|
26
26
|
'ts',
|
|
27
|
-
])
|
|
27
|
+
]),
|
|
28
28
|
)
|
|
29
29
|
.addOption(new Option('-y, --yes', 'Automatically answer "yes" to any prompts printed'))
|
|
30
|
-
.action(async (uri: string, options: { lang: string; yes?: boolean }) => {
|
|
30
|
+
.action(async (uri: string, options: { identifier?: string; lang: string; yes?: boolean }) => {
|
|
31
31
|
let language: SupportedLanguages;
|
|
32
32
|
if (options.lang) {
|
|
33
33
|
language = options.lang as SupportedLanguages;
|
|
@@ -69,7 +69,12 @@ cmd
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
let identifier;
|
|
72
|
-
if (
|
|
72
|
+
if (options.identifier) {
|
|
73
|
+
// `Storage.isIdentifierValid` will throw an exception if an identifier is invalid.
|
|
74
|
+
if (Storage.isIdentifierValid(options.identifier)) {
|
|
75
|
+
identifier = options.identifier;
|
|
76
|
+
}
|
|
77
|
+
} else if (Fetcher.isAPIRegistryUUID(uri)) {
|
|
73
78
|
identifier = Fetcher.getProjectPrefixFromRegistryUUID(uri);
|
|
74
79
|
} else {
|
|
75
80
|
({ value: identifier } = await promptTerminal({
|
|
@@ -82,19 +87,11 @@ cmd
|
|
|
82
87
|
return false;
|
|
83
88
|
}
|
|
84
89
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const isValidForNPM = validateNPMPackageName(`@api/${value}`);
|
|
91
|
-
if (!isValidForNPM.validForNewPackages) {
|
|
92
|
-
// `prompts` doesn't support surfacing multiple errors in a `validate` call so we can
|
|
93
|
-
// only surface the first to the user.
|
|
94
|
-
return isValidForNPM.errors[0];
|
|
90
|
+
try {
|
|
91
|
+
return Storage.isIdentifierValid(value, true);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
return err.message;
|
|
95
94
|
}
|
|
96
|
-
|
|
97
|
-
return true;
|
|
98
95
|
},
|
|
99
96
|
}));
|
|
100
97
|
}
|
|
@@ -191,7 +188,7 @@ cmd
|
|
|
191
188
|
Examples:
|
|
192
189
|
$ api install @developers/v2.0#nysezql0wwo236
|
|
193
190
|
$ api install https://raw.githubusercontent.com/readmeio/oas-examples/main/3.0/json/petstore-simple.json
|
|
194
|
-
$ api install ./petstore.json
|
|
191
|
+
$ api install ./petstore.json`,
|
|
195
192
|
);
|
|
196
193
|
|
|
197
194
|
export default cmd;
|
package/src/cli/lib/prompt.ts
CHANGED
|
@@ -9,7 +9,7 @@ import prompts from 'prompts';
|
|
|
9
9
|
*/
|
|
10
10
|
export default async function promptTerminal<T extends string = string>(
|
|
11
11
|
question: prompts.PromptObject<T>,
|
|
12
|
-
options?: prompts.Options
|
|
12
|
+
options?: prompts.Options,
|
|
13
13
|
) {
|
|
14
14
|
const enableTerminalCursor = () => {
|
|
15
15
|
process.stdout.write('\x1B[?25h');
|
package/src/cli/storage.ts
CHANGED
|
@@ -5,6 +5,7 @@ import path from 'path';
|
|
|
5
5
|
|
|
6
6
|
import makeDir from 'make-dir';
|
|
7
7
|
import ssri from 'ssri';
|
|
8
|
+
import validateNPMPackageName from 'validate-npm-package-name';
|
|
8
9
|
|
|
9
10
|
import Fetcher from '../fetcher';
|
|
10
11
|
import { PACKAGE_VERSION } from '../packageInfo';
|
|
@@ -105,6 +106,22 @@ export default class Storage {
|
|
|
105
106
|
return Storage.lockfile;
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
static isIdentifierValid(identifier: string, prefixWithAPINamespace?: boolean) {
|
|
110
|
+
// Is this identifier already in storage?
|
|
111
|
+
if (Storage.isInLockFile({ identifier })) {
|
|
112
|
+
throw new Error(`"${identifier}" is already taken in your \`.api/\` directory. Please try another identifier.`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const isValidForNPM = validateNPMPackageName(prefixWithAPINamespace ? `@api/${identifier}` : identifier);
|
|
116
|
+
if (!isValidForNPM.validForNewPackages) {
|
|
117
|
+
// `prompts` doesn't support surfacing multiple errors in a `validate` call so we can only
|
|
118
|
+
// surface the first to the user.
|
|
119
|
+
throw new Error(`Identifier cannot be used for an NPM package: ${isValidForNPM.errors[0]}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
108
125
|
static isInLockFile(search: { identifier?: string; source?: string }) {
|
|
109
126
|
// Because this method may run before we initialize a new storage object we should make sure
|
|
110
127
|
// that we have a storage directory present.
|
|
@@ -207,7 +224,6 @@ export default class Storage {
|
|
|
207
224
|
* ├── openapi.json
|
|
208
225
|
* └── package.json
|
|
209
226
|
*
|
|
210
|
-
* @param spec
|
|
211
227
|
*/
|
|
212
228
|
save(spec: OASDocument) {
|
|
213
229
|
if (!this.identifier) {
|
|
@@ -254,12 +270,13 @@ export default class Storage {
|
|
|
254
270
|
}
|
|
255
271
|
|
|
256
272
|
export interface Lockfile {
|
|
273
|
+
apis: LockfileAPI[];
|
|
274
|
+
|
|
257
275
|
/**
|
|
258
276
|
* The `api.json` schema version. This will only ever change if we introduce breaking changes to
|
|
259
277
|
* this store.
|
|
260
278
|
*/
|
|
261
279
|
version: '1.0';
|
|
262
|
-
apis: LockfileAPI[];
|
|
263
280
|
}
|
|
264
281
|
|
|
265
282
|
export interface LockfileAPI {
|
|
@@ -272,13 +289,11 @@ export interface LockfileAPI {
|
|
|
272
289
|
identifier: string;
|
|
273
290
|
|
|
274
291
|
/**
|
|
275
|
-
* The
|
|
292
|
+
* The version of `api` that was used to install this SDK.
|
|
276
293
|
*
|
|
277
|
-
* @example
|
|
278
|
-
* @example ./petstore.json
|
|
279
|
-
* @example @developers/v2.0#nysezql0wwo236
|
|
294
|
+
* @example 5.0.0
|
|
280
295
|
*/
|
|
281
|
-
|
|
296
|
+
installerVersion: string;
|
|
282
297
|
|
|
283
298
|
/**
|
|
284
299
|
* An integrity hash that will be used to determine on `npx api update` calls if the API has
|
|
@@ -289,9 +304,11 @@ export interface LockfileAPI {
|
|
|
289
304
|
integrity: string;
|
|
290
305
|
|
|
291
306
|
/**
|
|
292
|
-
* The
|
|
307
|
+
* The original source that was used to generate the SDK with.
|
|
293
308
|
*
|
|
294
|
-
* @example
|
|
309
|
+
* @example https://raw.githubusercontent.com/readmeio/oas-examples/main/3.0/json/petstore-simple.json
|
|
310
|
+
* @example ./petstore.json
|
|
311
|
+
* @example @developers/v2.0#nysezql0wwo236
|
|
295
312
|
*/
|
|
296
|
-
|
|
313
|
+
source: string;
|
|
297
314
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
class FetchError extends Error {
|
|
1
|
+
class FetchError<Status = number, Data = unknown> extends Error {
|
|
2
2
|
/** HTTP Status */
|
|
3
|
-
status:
|
|
3
|
+
status: Status;
|
|
4
4
|
|
|
5
5
|
/** The content of the response. */
|
|
6
|
-
data:
|
|
6
|
+
data: Data;
|
|
7
7
|
|
|
8
8
|
/** The Headers of the response. */
|
|
9
9
|
headers: Headers;
|
|
@@ -11,7 +11,7 @@ class FetchError extends Error {
|
|
|
11
11
|
/** The raw `Response` object. */
|
|
12
12
|
res: Response;
|
|
13
13
|
|
|
14
|
-
constructor(status:
|
|
14
|
+
constructor(status: Status, data: Data, headers: Headers, res: Response) {
|
|
15
15
|
super(res.statusText);
|
|
16
16
|
|
|
17
17
|
this.name = 'FetchError';
|
|
@@ -12,7 +12,6 @@ import traverse from 'json-schema-traverse';
|
|
|
12
12
|
*
|
|
13
13
|
* @todo This is a good candidate to be moved into a core `oas` library method.
|
|
14
14
|
* @see {@link https://github.com/mdornseif/json-schema-default}
|
|
15
|
-
* @param jsonSchemas
|
|
16
15
|
*/
|
|
17
16
|
export default function getJSONSchemaDefaults(jsonSchemas: SchemaWrapper[]) {
|
|
18
17
|
return jsonSchemas
|
|
@@ -27,7 +26,7 @@ export default function getJSONSchemaDefaults(jsonSchemas: SchemaWrapper[]) {
|
|
|
27
26
|
parentPointer: string,
|
|
28
27
|
parentKeyword: string,
|
|
29
28
|
parentSchema: SchemaObject,
|
|
30
|
-
indexProperty: string
|
|
29
|
+
indexProperty: string,
|
|
31
30
|
) => {
|
|
32
31
|
if (!pointer.startsWith('/properties/')) {
|
|
33
32
|
return;
|
|
@@ -59,7 +58,7 @@ export default function getJSONSchemaDefaults(jsonSchemas: SchemaWrapper[]) {
|
|
|
59
58
|
}
|
|
60
59
|
}
|
|
61
60
|
}
|
|
62
|
-
}
|
|
61
|
+
},
|
|
63
62
|
);
|
|
64
63
|
|
|
65
64
|
if (!Object.keys(defaults).length) {
|
package/src/core/index.ts
CHANGED
|
@@ -26,9 +26,9 @@ export interface ConfigOptions {
|
|
|
26
26
|
|
|
27
27
|
export interface FetchResponse<status, data> {
|
|
28
28
|
data: data;
|
|
29
|
-
status: status;
|
|
30
29
|
headers: Headers;
|
|
31
30
|
res: Response;
|
|
31
|
+
status: status;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
// https://stackoverflow.com/a/39495173
|
|
@@ -128,7 +128,12 @@ export default class APICore {
|
|
|
128
128
|
const parsed = await parseResponse(res);
|
|
129
129
|
|
|
130
130
|
if (res.status >= 400 && res.status <= 599) {
|
|
131
|
-
throw new FetchError
|
|
131
|
+
throw new FetchError<typeof parsed.status, typeof parsed.data>(
|
|
132
|
+
parsed.status,
|
|
133
|
+
parsed.data,
|
|
134
|
+
parsed.headers,
|
|
135
|
+
parsed.res,
|
|
136
|
+
);
|
|
132
137
|
}
|
|
133
138
|
|
|
134
139
|
return parsed;
|