Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(openapi): add update option to automatically update an only available spec file without any prompts #579

Merged
merged 11 commits into from
Aug 23, 2022
Merged
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,15 +109,15 @@ rdme openapi [path-to-file.json] --version={project-version} --create

#### Editing (Re-Syncing) an Existing API Definition

This will edit (re-sync) an existing API definition (identified by `--id`) within your ReadMe project.
This will edit (re-sync) an existing API definition (identified by `--id`) within your ReadMe project. **This is the recommended approach for usage in CI environments.**

```sh
rdme openapi [path-to-file.json] --id={existing-id}
```

#### Uploading or Editing an API Definition in a Project Version

You can additional include a version flag, specifying the target version for your file's destination. This approach will provide you with CLI prompts, so we do not recommend this technique in CI environments.
You can additionally include a version flag, specifying the target version for your file's destination. This approach will provide you with CLI prompts, so we do not recommend this technique in CI environments.
shaiarmis marked this conversation as resolved.
Show resolved Hide resolved

```sh
rdme openapi [path-to-file.json] --version={project-version}
Expand All @@ -139,6 +139,12 @@ You can pass in the `--useSpecVersion` option, which would be equivalent to pass
rdme openapi [path-to-file.json] --useSpecVersion
```

You can add `--update` to the command so if there's only one API definition for the given project version to update, it will select it without any prompts:

```sh
rdme openapi [path-to-file.json] --version={project-version} --update
```

#### Omitting the File Path

If you run `rdme` within a directory that contains your OpenAPI or Swagger definition, you can omit the file path. `rdme` will then look for JSON or YAML files (including in sub-directories) that contain a top-level [`openapi`](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#fixed-fields) or [`swagger`](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#fixed-fields) property.
Expand Down
6 changes: 6 additions & 0 deletions __tests__/__snapshots__/index.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ Options
--useSpecVersion Uses the version listed in the \`info.version\` field in the API
definition for the project version parameter.
--workingDirectory string Working directory (for usage with relative external references)
--update Automatically update an existing API definition in ReadMe if it's the
only one associated with the current version.
-h, --help Display this usage guide

Related commands
Expand Down Expand Up @@ -47,6 +49,8 @@ Options
--useSpecVersion Uses the version listed in the \`info.version\` field in the API
definition for the project version parameter.
--workingDirectory string Working directory (for usage with relative external references)
--update Automatically update an existing API definition in ReadMe if it's the
only one associated with the current version.
-h, --help Display this usage guide

Related commands
Expand Down Expand Up @@ -75,6 +79,8 @@ Options
--useSpecVersion Uses the version listed in the \`info.version\` field in the API
definition for the project version parameter.
--workingDirectory string Working directory (for usage with relative external references)
--update Automatically update an existing API definition in ReadMe if it's the
only one associated with the current version.
-h, --help Display this usage guide

Related commands
Expand Down
100 changes: 100 additions & 0 deletions __tests__/cmds/openapi/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,100 @@ describe('rdme openapi', () => {
return mock.done();
});

describe('--update', () => {
it("should update a spec file without prompts if providing `update` and it's the only spec available", async () => {
const registryUUID = getRandomRegistryId();

const mock = getAPIMock()
.get(`/api/v1/version/${version}`)
.basicAuth({ user: key })
.reply(200, [{ version }])
.post('/api/v1/api-registry', body => body.match('form-data; name="spec"'))
.reply(201, { registryUUID, spec: { openapi: '3.0.0' } })
.get('/api/v1/api-specification')
.basicAuth({ user: key })
.reply(200, [{ _id: 'spec1', title: 'spec1_title' }])
.put('/api/v1/api-specification/spec1', { registryUUID })
.delayConnection(1000)
.basicAuth({ user: key })
.reply(201, { _id: 1 }, { location: exampleRefLocation });

const spec = './__tests__/__fixtures__/ref-oas/petstore.json';

await expect(
openapi.run({
key,
version,
spec,
update: true,
})
).resolves.toBe(successfulUpdate(spec));
return mock.done();
});

it('should error if providing `update` and there are multiple specs available', async () => {
const registryUUID = getRandomRegistryId();

const mock = getAPIMock()
.get(`/api/v1/version/${version}`)
.basicAuth({ user: key })
.reply(200, [{ version }])
.post('/api/v1/api-registry', body => body.match('form-data; name="spec"'))
.reply(201, { registryUUID, spec: { openapi: '3.0.0' } })
.get('/api/v1/api-specification')
.basicAuth({ user: key })
.reply(200, [
{ _id: 'spec1', title: 'spec1_title' },
{ _id: 'spec2', title: 'spec2_title' },
]);

const spec = './__tests__/__fixtures__/ref-oas/petstore.json';

await expect(
openapi.run({
key,
version,
spec,
update: true,
})
).rejects.toStrictEqual(
new Error(
"The `--update` option cannot be used when there's more than one API definition available (found 2)."
)
);
return mock.done();
});

it('should warn if providing both `update` and `id`', async () => {
const registryUUID = getRandomRegistryId();

const mock = getAPIMock()
.post('/api/v1/api-registry', body => body.match('form-data; name="spec"'))
.reply(201, { registryUUID, spec: { openapi: '3.0.0' } })
.put('/api/v1/api-specification/spec1', { registryUUID })
.delayConnection(1000)
.basicAuth({ user: key })
.reply(201, { _id: 1 }, { location: exampleRefLocation });
const spec = './__tests__/__fixtures__/ref-oas/petstore.json';

await expect(
openapi.run({
key,
spec,
update: true,
id: 'spec1',
})
).resolves.toBe(successfulUpdate(spec));

expect(console.warn).toHaveBeenCalledTimes(1);
expect(console.info).toHaveBeenCalledTimes(0);

const output = getCommandOutput();
expect(output).toMatch(/the `--update` parameter will be ignored./);
return mock.done();
});
});

it.todo('should paginate to next and previous pages of specs');
});

Expand Down Expand Up @@ -584,6 +678,12 @@ describe('rdme openapi', () => {
).rejects.toStrictEqual(new Error('No project API key provided. Please use `--key`.'));
});

it('should error if `--create` and `--update` flags are passed simultaneously', () => {
return expect(openapi.run({ key, create: true, update: true })).rejects.toStrictEqual(
new Error('The `--create` and `--update` options cannot be used simultaneously. Please use one or the other!')
);
});

it('should error if invalid API key is sent and version list does not load', async () => {
const errorObject = {
error: 'APIKEY_NOTFOUND',
Expand Down
31 changes: 30 additions & 1 deletion src/cmds/openapi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export type Options = {
create?: boolean;
useSpecVersion?: boolean;
workingDirectory?: string;
update?: boolean;
};

export default class OpenAPICommand extends Command {
Expand Down Expand Up @@ -70,18 +71,30 @@ export default class OpenAPICommand extends Command {
type: String,
description: 'Working directory (for usage with relative external references)',
},
{
name: 'update',
type: Boolean,
description:
"Automatically update an existing API definition in ReadMe if it's the only one associated with the current version.",
},
];
}

async run(opts: CommandOptions<Options>) {
super.run(opts);

const { key, id, spec, create, useSpecVersion, version, workingDirectory } = opts;
const { key, id, spec, create, useSpecVersion, version, workingDirectory, update } = opts;

let selectedVersion = version;
let isUpdate: boolean;
const spinner = ora({ ...oraOptions() });

if (create && update) {
throw new Error(
'The `--create` and `--update` options cannot be used simultaneously. Please use one or the other!'
);
}

if (workingDirectory) {
process.chdir(workingDirectory);
}
Expand All @@ -96,6 +109,12 @@ export default class OpenAPICommand extends Command {
Command.warn("We'll be using the `--create` option, so the `--id` parameter will be ignored.");
}

if (update && id) {
Command.warn(
"We'll be updating the API definition associated with the `--id` parameter, so the `--update` 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');
Expand Down Expand Up @@ -239,6 +258,16 @@ export default class OpenAPICommand extends Command {
Command.debug(`api settings list response payload: ${JSON.stringify(apiSettingsBody)}`);
if (!apiSettingsBody.length) return createSpec();

if (update) {
if (apiSettingsBody.length > 1) {
throw new Error(
`The \`--update\` option cannot be used when there's more than one API definition available (found ${apiSettingsBody.length}).`
);
}
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(
Expand Down