diff --git a/README.md b/README.md index 11a8999a2..3435632aa 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ 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} @@ -117,7 +117,7 @@ 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. ```sh rdme openapi [path-to-file.json] --version={project-version} @@ -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. diff --git a/__tests__/__snapshots__/index.test.ts.snap b/__tests__/__snapshots__/index.test.ts.snap index c15bd494d..4ee64a210 100644 --- a/__tests__/__snapshots__/index.test.ts.snap +++ b/__tests__/__snapshots__/index.test.ts.snap @@ -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 @@ -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 @@ -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 diff --git a/__tests__/cmds/openapi/index.test.ts b/__tests__/cmds/openapi/index.test.ts index 1150246be..d31a79ff1 100644 --- a/__tests__/cmds/openapi/index.test.ts +++ b/__tests__/cmds/openapi/index.test.ts @@ -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'); }); @@ -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', diff --git a/src/cmds/openapi/index.ts b/src/cmds/openapi/index.ts index 0eaa47ed6..ae12b2735 100644 --- a/src/cmds/openapi/index.ts +++ b/src/cmds/openapi/index.ts @@ -23,6 +23,7 @@ export type Options = { create?: boolean; useSpecVersion?: boolean; workingDirectory?: string; + update?: boolean; }; export default class OpenAPICommand extends Command { @@ -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) { 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); } @@ -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'); @@ -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(