diff --git a/package.json b/package.json index f8f0a6ee..d768f017 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/aio-cli-plugin-api-mesh", - "version": "5.4.2", + "version": "5.5.0", "description": "Adobe I/O CLI plugin to develop and manage API mesh sources", "keywords": [ "oclif-plugin" diff --git a/src/commands/api-mesh/__tests__/get.test.js b/src/commands/api-mesh/__tests__/get.test.js index 7c9a1370..c1d9fca1 100644 --- a/src/commands/api-mesh/__tests__/get.test.js +++ b/src/commands/api-mesh/__tests__/get.test.js @@ -34,7 +34,7 @@ const { writeFile } = require('fs/promises'); const { initSdk } = require('../../../helpers'); const GetCommand = require('../get'); const mockGetMeshConfig = require('../../__fixtures__/sample_mesh.json'); -const { getMeshId, getMesh } = require('../../../lib/smsClient'); +const { getMesh } = require('../../../lib/smsClient'); let logSpy = null; let errorLogSpy = null; @@ -47,6 +47,7 @@ describe('get command tests', () => { beforeEach(() => { initSdk.mockResolvedValue({ imsOrgId: selectedOrg.id, + imsOrgCode: selectedOrg.code, projectId: selectedProject.id, workspaceId: selectedWorkspace.id, workspaceName: selectedWorkspace.title, @@ -59,7 +60,6 @@ describe('get command tests', () => { writeFile.mockResolvedValue(true); - getMeshId.mockResolvedValue('dummy_meshId'); getMesh.mockResolvedValue({ meshId: 'dummy_meshId', mesh: mockGetMeshConfig, @@ -79,7 +79,9 @@ describe('get command tests', () => { }); test('snapshot get command', () => { - expect(GetCommand.description).toMatchInlineSnapshot(`"Get the config of a given mesh"`); + expect(GetCommand.description).toMatchInlineSnapshot( + `"Get the config of a specified mesh. Use the --active flag to retrieve the last successfully deployed mesh config"`, + ); expect(GetCommand.args).toMatchInlineSnapshot(` [ { @@ -89,6 +91,14 @@ describe('get command tests', () => { `); expect(GetCommand.flags).toMatchInlineSnapshot(` { + "active": { + "allowNo": false, + "char": "a", + "default": false, + "description": "Retrieve the last successfully deployed mesh config", + "parse": [Function], + "type": "boolean", + }, "ignoreCache": { "allowNo": false, "char": "i", @@ -109,52 +119,14 @@ describe('get command tests', () => { expect(GetCommand.aliases).toMatchInlineSnapshot(`[]`); }); - test('should fail if mesh id is missing', async () => { - getMeshId.mockResolvedValueOnce(null); - const runResult = GetCommand.run(); - - return runResult.catch(err => { - expect(err.message).toMatchInlineSnapshot( - `"Unable to get mesh config. No mesh found for Org(1234) -> Project(5678) -> Workspace(123456789). Check the details and try again."`, - ); - expect(logSpy.mock.calls).toMatchInlineSnapshot(`[]`); - expect(errorLogSpy.mock.calls).toMatchInlineSnapshot(` - [ - [ - "Unable to get mesh config. No mesh found for Org(1234) -> Project(5678) -> Workspace(123456789). Check the details and try again.", - ], - ] - `); - }); - }); - - test('should fail if getMeshId failed', async () => { - getMeshId.mockRejectedValueOnce(new Error('getMeshId failed')); - const runResult = GetCommand.run(); - - return runResult.catch(err => { - expect(err.message).toMatchInlineSnapshot( - `"Unable to get mesh ID. Check the details and try again. RequestId: dummy_request_id"`, - ); - expect(logSpy.mock.calls).toMatchInlineSnapshot(`[]`); - expect(errorLogSpy.mock.calls).toMatchInlineSnapshot(` - [ - [ - "Unable to get mesh ID. Check the details and try again. RequestId: dummy_request_id", - ], - ] - `); - }); - }); - - test('should fail if mesh id is not found', async () => { - getMesh.mockResolvedValueOnce(null); + test('should fail if mesh is missing', async () => { + getMesh.mockRejectedValueOnce(new Error('MeshNotFound')); + await GetCommand.run(); - await GetCommand.run().catch(err => { - expect(err.message).toContain( - 'Unable to get mesh with the ID dummy_meshId. Please check the mesh ID and try again.', - ); - }); + expect(logSpy.mock.calls).toMatchInlineSnapshot(`[]`); + expect(errorLogSpy.mock.calls[0][0]).toBe( + 'Unable to get mesh config. No mesh found for Org(CODE1234@AdobeOrg) -> Project(5678) -> Workspace(123456789). Please check the details and try again.', + ); }); test('should fail if get mesh method failed', async () => { @@ -183,9 +155,7 @@ describe('get command tests', () => { `); }); - test('should pass if mesh id is valid', async () => { - const meshId = 'dummy_meshId'; - getMeshId.mockResolvedValueOnce(meshId); + test('should pass if mesh is found', async () => { const runResult = await GetCommand.run(); expect(initSdk).toHaveBeenCalledWith({ @@ -402,4 +372,119 @@ describe('get command tests', () => { `); expect(errorLogSpy.mock.calls).toMatchInlineSnapshot(`[]`); }); + + // Active flag test cases + test('should get last successfully deployed mesh config with --active flag', async () => { + getMesh.mockResolvedValueOnce({ + meshId: 'dummy_meshId', + mesh: mockGetMeshConfig, + }); + + parseSpy.mockResolvedValueOnce({ + args: {}, + flags: { + ignoreCache: mockIgnoreCacheFlag, + active: true, + }, + }); + + const runResult = await GetCommand.run(); + + expect(getMesh).toHaveBeenCalledWith( + selectedOrg.code, + selectedProject.id, + selectedWorkspace.id, + selectedWorkspace.title, + true, + ); + + expect(runResult).toBeDefined(); + expect(runResult.meshId).toBe('dummy_meshId'); + }); + + test('should get last successfully deployed mesh config with shorthand -a flag', async () => { + getMesh.mockResolvedValueOnce({ + meshId: 'dummy_meshId', + mesh: mockGetMeshConfig, + }); + + parseSpy.mockResolvedValueOnce({ + args: {}, + flags: { + ignoreCache: mockIgnoreCacheFlag, + active: true, // -a flag also sets active to true + }, + }); + + const runResult = await GetCommand.run(); + + expect(getMesh).toHaveBeenCalledWith( + selectedOrg.code, + selectedProject.id, + selectedWorkspace.id, + selectedWorkspace.title, + true, + ); + + expect(runResult).toBeDefined(); + expect(runResult.meshId).toBe('dummy_meshId'); + }); + + test('should handle NoActiveDeploymentFound error when using --active flag', async () => { + getMesh.mockRejectedValueOnce(new Error('NoActiveDeploymentFound')); + + parseSpy.mockResolvedValueOnce({ + args: {}, + flags: { + ignoreCache: mockIgnoreCacheFlag, + active: true, + }, + }); + + const runResult = GetCommand.run(); + + await expect(runResult).rejects.toEqual( + new Error( + 'No active deployment found for mesh. Check the details and try again or try without the --active flag. RequestId: dummy_request_id', + ), + ); + + expect(logSpy.mock.calls).toMatchInlineSnapshot(`[]`); + expect(errorLogSpy.mock.calls[0][0]).toBe( + 'No active deployment found for mesh. Check the details and try again or try without the --active flag. RequestId: dummy_request_id', + ); + expect(getMesh).toHaveBeenCalledWith( + selectedOrg.code, + selectedProject.id, + selectedWorkspace.id, + selectedWorkspace.title, + true, + ); + }); + + test('should handle mesh not found when using --active flag', async () => { + getMesh.mockRejectedValueOnce(new Error('MeshNotFound')); + + parseSpy.mockResolvedValueOnce({ + args: {}, + flags: { + ignoreCache: mockIgnoreCacheFlag, + active: true, + }, + }); + + await GetCommand.run(); + + expect(logSpy.mock.calls).toMatchInlineSnapshot(`[]`); + expect(errorLogSpy.mock.calls[0][0]).toBe( + 'Unable to get mesh config. No mesh found for Org(CODE1234@AdobeOrg) -> Project(5678) -> Workspace(123456789). Please check the details and try again.', + ); + expect(getMesh).toHaveBeenCalledWith( + selectedOrg.code, + selectedProject.id, + selectedWorkspace.id, + selectedWorkspace.title, + true, + ); + }); }); diff --git a/src/commands/api-mesh/get.js b/src/commands/api-mesh/get.js index 03955505..61775cd8 100644 --- a/src/commands/api-mesh/get.js +++ b/src/commands/api-mesh/get.js @@ -14,8 +14,8 @@ const { writeFile } = require('fs/promises'); const logger = require('../../classes/logger'); const { initSdk } = require('../../helpers'); -const { ignoreCacheFlag, jsonFlag } = require('../../utils'); -const { getMeshId, getMesh } = require('../../lib/smsClient'); +const { ignoreCacheFlag, jsonFlag, activeFlag } = require('../../utils'); +const { getMesh } = require('../../lib/smsClient'); const { buildMeshUrl } = require('../../urlBuilder'); require('dotenv').config(); @@ -24,6 +24,7 @@ class GetCommand extends Command { static flags = { ignoreCache: ignoreCacheFlag, json: jsonFlag, + active: activeFlag, }; static enableJsonFlag = true; @@ -35,67 +36,62 @@ class GetCommand extends Command { const ignoreCache = await flags.ignoreCache; const json = await flags.json; + const active = await flags.active; const { imsOrgId, imsOrgCode, projectId, workspaceId, workspaceName } = await initSdk({ ignoreCache, verbose: !json, }); - let meshId = null; - try { - meshId = await getMeshId(imsOrgCode, projectId, workspaceId, workspaceName); - } catch (err) { - this.error( - `Unable to get mesh ID. Check the details and try again. RequestId: ${global.requestId}`, - ); - } + const mesh = await getMesh(imsOrgCode, projectId, workspaceId, workspaceName, active); - if (meshId) { - try { - const mesh = await getMesh(imsOrgCode, projectId, workspaceId, workspaceName, meshId); + if (mesh) { + this.log('Successfully retrieved mesh %s', JSON.stringify(mesh, null, 2)); - if (mesh) { - this.log('Successfully retrieved mesh %s', JSON.stringify(mesh, null, 2)); + const meshUrl = buildMeshUrl(mesh.meshId, workspaceName); - const meshUrl = buildMeshUrl(meshId, workspaceName); + if (args.file) { + try { + const { meshConfig } = mesh; + await writeFile(args.file, JSON.stringify({ meshConfig }, null, 2)); - if (args.file) { - try { - const { meshConfig } = mesh; - await writeFile(args.file, JSON.stringify({ meshConfig }, null, 2)); + this.log('Successfully wrote mesh to file %s', args.file); + } catch (error) { + this.log('Unable to write mesh to file %s', args.file); - this.log('Successfully wrote mesh to file %s', args.file); - } catch (error) { - this.log('Unable to write mesh to file %s', args.file); - - logger.error(error); - } + logger.error(error); } - - return { ...mesh, meshUrl, imsOrgId, projectId, workspaceId, workspaceName }; - } else { - logger.error( - `Unable to get mesh with the ID ${meshId}. Check the mesh ID and try again. RequestId: ${global.requestId}`, - { exit: false }, - ); } - } catch (error) { - this.log(error.message); + return { ...mesh, meshUrl, imsOrgId, projectId, workspaceId, workspaceName }; + } else { + this.error( + `Unable to get mesh config. No mesh found for Org(${imsOrgCode}) -> Project(${projectId}) -> Workspace(${workspaceId}). Please check the details and try again.`, + { exit: false }, + ); + } + } catch (error) { + if (error.message === 'MeshNotFound') { + this.error( + `Unable to get mesh config. No mesh found for Org(${imsOrgCode}) -> Project(${projectId}) -> Workspace(${workspaceId}). Please check the details and try again.`, + { exit: false }, + ); + } else if (error.message === 'NoActiveDeploymentFound') { + this.error( + `No active deployment found for mesh. Check the details and try again or try without the --active flag. RequestId: ${global.requestId}`, + ); + } else { + this.log(error.message); this.error( `Unable to get mesh. Check the details and try again. If the error persists please contact support. RequestId: ${global.requestId}`, ); } - } else { - this.error( - `Unable to get mesh config. No mesh found for Org(${imsOrgCode}) -> Project(${projectId}) -> Workspace(${workspaceId}). Please check the details and try again.`, - { exit: false }, - ); } } } -GetCommand.description = 'Get the config of a given mesh'; +GetCommand.description = + 'Get the config of a specified mesh. Use the --active flag to retrieve the last successfully deployed mesh config'; module.exports = GetCommand; diff --git a/src/commands/api-mesh/source/install.js b/src/commands/api-mesh/source/install.js index b563faed..9b7694c0 100644 --- a/src/commands/api-mesh/source/install.js +++ b/src/commands/api-mesh/source/install.js @@ -140,7 +140,7 @@ class InstallCommand extends Command { } try { - const mesh = await getMesh(imsOrgCode, projectId, workspaceId, meshId, workspaceName); + const mesh = await getMesh(imsOrgCode, projectId, workspaceId, workspaceName); if (!mesh) { this.error( diff --git a/src/commands/api-mesh/status.js b/src/commands/api-mesh/status.js index dbc84d56..58b32ba9 100644 --- a/src/commands/api-mesh/status.js +++ b/src/commands/api-mesh/status.js @@ -41,7 +41,7 @@ class StatusCommand extends Command { } try { - const mesh = await getMesh(imsOrgCode, projectId, workspaceId, workspaceName, meshId); + const mesh = await getMesh(imsOrgCode, projectId, workspaceId, workspaceName); this.log(''.padEnd(102, '*')); await this.displayMeshStatus(mesh, imsOrgCode, projectId, workspaceId); this.log(''.padEnd(102, '*')); diff --git a/src/lib/smsClient.js b/src/lib/smsClient.js index 5d931ca9..038bd4d1 100644 --- a/src/lib/smsClient.js +++ b/src/lib/smsClient.js @@ -142,11 +142,27 @@ const listLogs = async (organizationCode, projectId, workspaceId, meshId, fileNa } }; -const getMesh = async (organizationId, projectId, workspaceId, workspaceName, meshId) => { +/** + * Retrieves mesh configuration from the Schema Management Service. + * + * @param {string} organizationId - The organization ID + * @param {string} projectId - The project ID + * @param {string} workspaceId - The workspace ID + * @param {string} workspaceName - The workspace name + * @param {boolean} active - Whether to retrieve the last successful deployed mesh configuration. + * @returns {Promise} The mesh configuration object, or null if mesh not found + * @throws {Error} Throws 'NoActiveDeploymentFound' when active=true but no successful deployment exists + * @throws {Error} Throws generic error for other API failures + */ +const getMesh = async (organizationId, projectId, workspaceId, workspaceName, active) => { const { accessToken } = await getDevConsoleConfig(); + + const url = `${SMS_BASE_URL}/organizations/${organizationId}/projects/${projectId}/workspaces/${workspaceId}/mesh`; + const params = active ? { active } : undefined; + const config = { method: 'get', - url: `${SMS_BASE_URL}/organizations/${organizationId}/projects/${projectId}/workspaces/${workspaceId}/meshes/${meshId}`, + url: url, headers: { ...global?.metadataHeaders, 'Authorization': `Bearer ${accessToken}`, @@ -154,12 +170,10 @@ const getMesh = async (organizationId, projectId, workspaceId, workspaceName, me 'workspaceName': workspaceName, 'x-api-key': SMS_API_KEY, }, + params: params, }; - logger.info( - 'Initiating GET %s', - `${SMS_BASE_URL}/organizations/${organizationId}/projects/${projectId}/workspaces/${workspaceId}/meshes/${meshId}`, - ); + logger.info('Initiating GET %s', url); try { const response = await axios(config); @@ -186,10 +200,15 @@ const getMesh = async (organizationId, projectId, workspaceId, workspaceName, me logger.info('Response from GET %s', error.response.status); if (error.response.status === 404) { - // The request was made and the server responded with a 404 status code - logger.error('Mesh not found'); - - return null; + // Check if no active deployment found + if (active && error.response.data?.message?.includes('No active deployment found')) { + logger.error('No active deployment found for mesh'); + throw new Error('NoActiveDeploymentFound'); + } else { + // General mesh not found case + logger.error('Mesh not found'); + throw new Error('MeshNotFound'); + } } else if (error.response && error.response.data) { // The request was made and the server responded with an unsupported status code logger.error( diff --git a/src/utils.js b/src/utils.js index ea117067..547f7c84 100644 --- a/src/utils.js +++ b/src/utils.js @@ -70,6 +70,12 @@ const inspectPortNoFlag = Flags.integer({ default: 9229, }); +const activeFlag = Flags.boolean({ + char: 'a', + description: 'Retrieve the last successfully deployed mesh config', + default: false, +}); + const debugFlag = Flags.boolean({ description: 'Enable debugging mode', default: false, @@ -815,6 +821,7 @@ module.exports = { getAppRootDir, portNoFlag, inspectPortNoFlag, + activeFlag, debugFlag, selectFlag, secretsFlag,