-
Notifications
You must be signed in to change notification settings - Fork 43
/
Copy pathindex.ts
269 lines (233 loc) · 8.66 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
import type { CommandOptions } from '../../lib/baseCommand';
import type { RequestInit, Response } from 'node-fetch';
import chalk from 'chalk';
import config from 'config';
import { Headers } from 'node-fetch';
import ora from 'ora';
import parse from 'parse-link-header';
import Command, { CommandCategories } from '../../lib/baseCommand';
import fetch, { cleanHeaders, handleRes } from '../../lib/fetch';
import { oraOptions } from '../../lib/logger';
import prepareOas from '../../lib/prepareOas';
import * as promptHandler from '../../lib/prompts';
import promptTerminal from '../../lib/promptWrapper';
import streamSpecToRegistry from '../../lib/streamSpecToRegistry';
import { getProjectVersion } from '../../lib/versionSelect';
export type Options = {
id?: string;
spec?: string;
version?: string;
create?: boolean;
useSpecVersion?: boolean;
workingDirectory?: string;
updateSingleSpec?: boolean;
};
export default class OpenAPICommand extends Command {
constructor() {
super();
this.command = 'openapi';
this.usage = 'openapi [file] [options]';
this.description = 'Upload, or resync, your OpenAPI/Swagger definition to ReadMe.';
this.cmdCategory = CommandCategories.APIS;
this.position = 1;
this.hiddenArgs = ['spec'];
this.args = [
{
name: 'key',
type: String,
description: 'Project API key',
},
{
name: 'id',
type: String,
description:
"Unique identifier for your API definition. Use this if you're re-uploading an existing API definition.",
},
this.getVersionArg(),
{
name: 'spec',
type: String,
defaultOption: true,
},
{
name: 'create',
type: Boolean,
description: 'Bypasses the create/update prompt and creates a new API definition.',
},
{
name: 'useSpecVersion',
type: Boolean,
description:
'Uses the version listed in the `info.version` field in the API definition for the project version parameter.',
},
{
name: 'workingDirectory',
type: String,
description: 'Working directory (for usage with relative external references)',
},
{
name: 'updateSingleSpec',
type: Boolean,
description: "Automatically update an existing spec file if it's the only one found",
},
];
}
async run(opts: CommandOptions<Options>) {
super.run(opts);
const { key, id, spec, create, useSpecVersion, version, workingDirectory, updateSingleSpec } = opts;
let selectedVersion = version;
let isUpdate: boolean;
const spinner = ora({ ...oraOptions() });
if (workingDirectory) {
process.chdir(workingDirectory);
}
if (version && id) {
Command.warn(
"We'll be using the version associated with the `--id` option, so the `--version` option will be ignored."
);
}
if (create && id) {
Command.warn("We'll be using the `--create` option , so the `--id` parameter will be ignored.");
}
// Reason we're hardcoding in command here is because `swagger` command
// relies on this and we don't want to use `swagger` in this function
const { bundledSpec, specPath, specType, specVersion } = await prepareOas(spec, 'openapi');
if (useSpecVersion) {
Command.info(
`Using the version specified in your API definition for your ReadMe project version (${specVersion})`
);
selectedVersion = specVersion;
}
if (!id) {
selectedVersion = await getProjectVersion(selectedVersion, key, true);
}
Command.debug(`selectedVersion: ${selectedVersion}`);
async function success(data: Response) {
const message = !isUpdate
? `You've successfully uploaded a new ${specType} file to your ReadMe project!`
: `You've successfully updated an existing ${specType} file on your ReadMe project!`;
Command.debug(`successful ${data.status} response`);
const body = await data.json();
Command.debug(`successful response payload: ${JSON.stringify(body)}`);
return Promise.resolve(
[
message,
'',
`\t${chalk.green(`${data.headers.get('location')}`)}`,
'',
`To update your ${specType} definition, run the following:`,
'',
// eslint-disable-next-line no-underscore-dangle
`\t${chalk.green(`rdme openapi ${specPath} --key=<key> --id=${body._id}`)}`,
].join('\n')
);
}
async function error(res: Response) {
return handleRes(res).catch(err => {
// If we receive an APIError, no changes needed! Throw it as is.
if (err.name === 'APIError') {
throw err;
}
// If we receive certain text responses, it's likely a 5xx error from our server.
if (
typeof err === 'string' &&
(err.includes('<title>Application Error</title>') || // Heroku error
err.includes('520: Web server is returning an unknown error</title>')) // Cloudflare error
) {
throw new Error(
"We're sorry, your upload request timed out. Please try again or split your file up into smaller chunks."
);
}
// As a fallback, we throw a more generic error.
throw new Error(
`Yikes, something went wrong! Please try uploading your spec again and if the problem persists, get in touch with our support team at ${chalk.underline(
'support@readme.io'
)}.`
);
});
}
const registryUUID = await streamSpecToRegistry(bundledSpec);
const options: RequestInit = {
headers: cleanHeaders(
key,
new Headers({
Accept: 'application/json',
'Content-Type': 'application/json',
'x-readme-version': selectedVersion,
})
),
body: JSON.stringify({ registryUUID }),
};
function createSpec() {
options.method = 'post';
spinner.start('Creating your API docs in ReadMe...');
return fetch(`${config.get('host')}/api/v1/api-specification`, options).then(res => {
if (res.ok) {
spinner.succeed(`${spinner.text} done! 🦉`);
return success(res);
}
spinner.fail();
return error(res);
});
}
function updateSpec(specId: string) {
isUpdate = true;
options.method = 'put';
spinner.start('Updating your API docs in ReadMe...');
return fetch(`${config.get('host')}/api/v1/api-specification/${specId}`, options).then(res => {
if (res.ok) {
spinner.succeed(`${spinner.text} done! 🦉`);
return success(res);
}
spinner.fail();
return error(res);
});
}
/*
Create a new OAS file in Readme:
- Enter flow if user does not pass an id as cli arg
- Check to see if any existing files exist with a specific version
- If none exist, default to creating a new instance of a spec
- If found, prompt user to either create a new spec or update an existing one
*/
function getSpecs(url: string) {
return fetch(`${config.get('host')}${url}`, {
method: 'get',
headers: cleanHeaders(
key,
new Headers({
'x-readme-version': selectedVersion,
})
),
});
}
if (create) return createSpec();
if (!id) {
Command.debug('no id parameter, retrieving list of API specs');
const apiSettings = await getSpecs('/api/v1/api-specification');
const totalPages = Math.ceil(parseInt(apiSettings.headers.get('x-total-count'), 10) / 10);
const parsedDocs = parse(apiSettings.headers.get('link'));
Command.debug(`total pages: ${totalPages}`);
Command.debug(`pagination result: ${JSON.stringify(parsedDocs)}`);
const apiSettingsBody = await apiSettings.json();
Command.debug(`api settings list response payload: ${JSON.stringify(apiSettingsBody)}`);
if (!apiSettingsBody.length) return createSpec();
if (apiSettingsBody.length === 1 && updateSingleSpec) {
const { _id: specId } = apiSettingsBody[0];
return updateSpec(specId);
}
// @todo: figure out how to add a stricter type here, see:
// https://github.com/readmeio/rdme/pull/570#discussion_r949715913
const { option } = await promptTerminal(
promptHandler.createOasPrompt(apiSettingsBody, parsedDocs, totalPages, getSpecs)
);
Command.debug(`selection result: ${option}`);
return option === 'create' ? createSpec() : updateSpec(option);
}
/*
Update an existing OAS file in Readme:
- Enter flow if user passes an id as cli arg
*/
return updateSpec(id);
}
}