@adobe/aio-cli-plugin-api-mesh 1.2.0 → 1.5.0

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 CHANGED
@@ -90,9 +90,13 @@ To submit a new source, please follow the instructions provided in the [Source R
90
90
  ## Commands
91
91
 
92
92
  ```
93
- aio api-mesh:source:get
94
- NAME_OF_THE_SOURCE
95
- aio api-mesh:source:get NAME_OF_THE_SOURCE@VERSION_OF_THE_SOURCE
93
+
94
+ aio api-mesh:source:install SOURCE_NAME
95
+ aio api-mesh:source:install SOURCE_NAME -v VARIABLE_NAME=VARIABLE_VALUE
96
+ aio api-mesh:source:install SOURCE_NAME -f PATH_TO_FILE_WITH_VARIABLES
97
+
98
+ aio api-mesh:source:get SOURCE_NAME
99
+ aio api-mesh:source:get SOURCE_NAME@VERSION_OF_THE_SOURCE
96
100
  aio api-mesh:source:get -m
97
101
  aio api-mesh:source:discover
98
102
  ```
@@ -1 +1 @@
1
- {"version":"1.2.0","commands":{"PLUGINNAME":{"id":"PLUGINNAME","description":"Your description here","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"examples":["$ aio PLUGINNAME:some_command"],"flags":{"someflag":{"name":"someflag","type":"option","char":"f","description":"this is some flag"}},"args":[]},"api-mesh:create":{"id":"api-mesh:create","description":"Create a mesh with the given config.","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false},"autoConfirmAction":{"name":"autoConfirmAction","type":"boolean","char":"c","description":"Auto confirm action prompt. CLI will not check for user approval before executing the action.","allowNo":false}},"args":[{"name":"file"}]},"api-mesh:delete":{"id":"api-mesh:delete","description":"Delete the config of a given mesh","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false},"autoConfirmAction":{"name":"autoConfirmAction","type":"boolean","char":"c","description":"Auto confirm action prompt. CLI will not check for user approval before executing the action.","allowNo":false}},"args":[]},"api-mesh:describe":{"id":"api-mesh:describe","description":"Get details of a mesh","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false}},"args":[]},"api-mesh:get":{"id":"api-mesh:get","description":"Get the config of a given mesh","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false}},"args":[{"name":"file"}]},"api-mesh:update":{"id":"api-mesh:update","description":"Update a mesh with the given config.","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false},"autoConfirmAction":{"name":"autoConfirmAction","type":"boolean","char":"c","description":"Auto confirm action prompt. CLI will not check for user approval before executing the action.","allowNo":false}},"args":[{"name":"file"}]},"api-mesh:source:discover":{"id":"api-mesh:source:discover","description":"Return the list of avaliable sources","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{},"args":[]},"api-mesh:source:get":{"id":"api-mesh:source:get","description":"Command returns the content of a specific source.","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"examples":["$ aio api-mesh:source:get <version>@<source_name>","$ aio api-mesh:source:get <source_name>","$ aio api-mesh:source:get -m"],"flags":{"source":{"name":"source","type":"option","char":"s","description":"Source name"},"multiple":{"name":"multiple","type":"boolean","char":"m","description":"Select multiple sources","allowNo":false}},"args":[]}}}
1
+ {"version":"1.5.0","commands":{"PLUGINNAME":{"id":"PLUGINNAME","description":"Your description here","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"examples":["$ aio PLUGINNAME:some_command"],"flags":{"someflag":{"name":"someflag","type":"option","char":"f","description":"this is some flag"}},"args":[]},"api-mesh:create":{"id":"api-mesh:create","description":"Create a mesh with the given config.","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false},"autoConfirmAction":{"name":"autoConfirmAction","type":"boolean","char":"c","description":"Auto confirm action prompt. CLI will not check for user approval before executing the action.","allowNo":false},"json":{"name":"json","type":"boolean","description":"Output JSON","allowNo":false}},"args":[{"name":"file"}]},"api-mesh:delete":{"id":"api-mesh:delete","description":"Delete the config of a given mesh","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false},"autoConfirmAction":{"name":"autoConfirmAction","type":"boolean","char":"c","description":"Auto confirm action prompt. CLI will not check for user approval before executing the action.","allowNo":false}},"args":[]},"api-mesh:describe":{"id":"api-mesh:describe","description":"Get details of a mesh","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false}},"args":[]},"api-mesh:get":{"id":"api-mesh:get","description":"Get the config of a given mesh","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false}},"args":[{"name":"file"}]},"api-mesh:update":{"id":"api-mesh:update","description":"Update a mesh with the given config.","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false},"autoConfirmAction":{"name":"autoConfirmAction","type":"boolean","char":"c","description":"Auto confirm action prompt. CLI will not check for user approval before executing the action.","allowNo":false}},"args":[{"name":"file"}]},"api-mesh:source:discover":{"id":"api-mesh:source:discover","description":"Return the list of avaliable sources","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"confirm":{"name":"confirm","type":"boolean","char":"c","description":"Auto confirm install action prompt. CLI will not check ask user to install source.","allowNo":false}},"args":[]},"api-mesh:source:get":{"id":"api-mesh:source:get","description":"Command returns the content of a specific source.","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"examples":["$ aio api-mesh:source:get -s=<version>@<source_name>","$ aio api-mesh:source:get -s<source_name>","$ aio api-mesh:source:get -m"],"flags":{"confirm":{"name":"confirm","type":"boolean","char":"c","description":"Auto confirm print action prompt. CLI will not check ask user to print source.","allowNo":false},"source":{"name":"source","type":"option","char":"s","description":"Source name"},"multiple":{"name":"multiple","type":"boolean","char":"m","description":"Select multiple sources","allowNo":false}},"args":[]},"api-mesh:source:install":{"id":"api-mesh:source:install","description":"Command to install the source to your API mesh.","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"examples":["$ aio api-mesh:source:install <version>@<source_name>","$ aio api-mesh:source:install <source_name> -v <variable_name>=<variable_value>","$ aio api-mesh:source:install <source_name> -f <path_to_variables_file>"],"flags":{"source":{"name":"source","type":"option","char":"s","description":"Source name"},"confirm":{"name":"confirm","type":"boolean","char":"c","description":"Auto confirm override action prompt. CLI will not check ask user to override source.","allowNo":false},"variable":{"name":"variable","type":"option","char":"v","description":"Variables required for the source"},"variable-file":{"name":"variable-file","type":"option","char":"f","description":"Variables file path"},"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false}},"args":[{"name":"source"}]}}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/aio-cli-plugin-api-mesh",
3
- "version": "1.2.0",
3
+ "version": "1.5.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -18,6 +18,7 @@
18
18
  "chalk": "^4.1.0",
19
19
  "dotenv": "^16.0.1",
20
20
  "inquirer": "^8.2.4",
21
+ "json-interpolate": "^1.0.3",
21
22
  "node-clipboardy": "^1.0.3",
22
23
  "pino": "^7.9.2",
23
24
  "pino-pretty": "^7.6.0",
@@ -82,7 +83,7 @@
82
83
  "format": "prettier --check \"./**/*.{ts,tsx,js,css,json,md}\"",
83
84
  "format:fix": "prettier --write \"./**/*.{ts,tsx,js,css,json,md}\""
84
85
  },
85
- "description": "Basic working repo structure for Adobe teams to use as a starting point for their own plugins, and available as a github template to the @adobe github org.",
86
+ "description": "Adobe I/O CLI plugin to develop and manage API mesh sources",
86
87
  "directories": {
87
88
  "test": "test"
88
89
  },
@@ -117,6 +117,13 @@ describe('create command tests', () => {
117
117
  "parse": [Function],
118
118
  "type": "boolean",
119
119
  },
120
+ "json": Object {
121
+ "allowNo": false,
122
+ "default": false,
123
+ "description": "Output JSON",
124
+ "parse": [Function],
125
+ "type": "boolean",
126
+ },
120
127
  }
121
128
  `);
122
129
  expect(CreateCommand.aliases).toMatchInlineSnapshot(`Array []`);
@@ -485,4 +492,18 @@ describe('create command tests', () => {
485
492
  `);
486
493
  expect(errorLogSpy.mock.calls).toMatchInlineSnapshot(`Array []`);
487
494
  });
495
+
496
+ test('must return proper object structure used by adobe/generator-app-api-mesh', async () => {
497
+ parseSpy.mockResolvedValueOnce({
498
+ args: { file: 'src/commands/__fixtures__/sample_mesh.json' },
499
+ flags: {
500
+ json: Promise.resolve(true)
501
+ },
502
+ });
503
+ const output = await CreateCommand.run()
504
+ expect(output).toHaveProperty('mesh')
505
+ expect(output).toHaveProperty('adobeIdIntegrationsForWorkspace')
506
+ expect(output.mesh).toEqual(expect.objectContaining({ meshId: 'dummy_mesh_id' }));
507
+ expect(output.adobeIdIntegrationsForWorkspace).toEqual(expect.objectContaining({ apiKey: 'dummy_api_key' }));
508
+ });
488
509
  });
@@ -9,13 +9,13 @@ OF ANY KIND, either express or implied. See the License for the specific languag
9
9
  governing permissions and limitations under the License.
10
10
  */
11
11
 
12
- const { Command } = require('@oclif/command');
12
+ const { Command } = require('@oclif/core');
13
13
  const { readFile } = require('fs/promises');
14
14
 
15
15
  const { initSdk, initRequestId, promptConfirm } = require('../../helpers');
16
16
  const logger = require('../../classes/logger');
17
17
  const CONSTANTS = require('../../constants');
18
- const { ignoreCacheFlag, autoConfirmActionFlag } = require('../../utils');
18
+ const { ignoreCacheFlag, autoConfirmActionFlag, jsonFlag } = require('../../utils');
19
19
  const {
20
20
  createMesh,
21
21
  createAPIMeshCredentials,
@@ -31,8 +31,11 @@ class CreateCommand extends Command {
31
31
  static flags = {
32
32
  ignoreCache: ignoreCacheFlag,
33
33
  autoConfirmAction: autoConfirmActionFlag,
34
+ json: jsonFlag
34
35
  };
35
36
 
37
+ static enableJsonFlag = true
38
+
36
39
  async run() {
37
40
  await initRequestId();
38
41
 
@@ -116,11 +119,12 @@ class CreateCommand extends Command {
116
119
  } else {
117
120
  this.log('Unable to create API Key');
118
121
  }
119
-
122
+ // Do not remove or rename return values.
123
+ // Template adobe/generator-app-api-mesh relies on "mesh" & "adobeIdIntegrationsForWorkspace" obj structure
120
124
  return {
121
125
  adobeIdIntegrationsForWorkspace,
122
126
  sdkList,
123
- mesh,
127
+ mesh
124
128
  };
125
129
  } else {
126
130
  this.error(`Unable to create a mesh. Please try again. RequestId: ${global.requestId}`, {
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "Test 03",
3
+ "version": "0.0.1",
4
+ "description": "Mock for variable injection",
5
+ "author": "VladimirZaets",
6
+ "variables": {
7
+ "ENDPOINT_URL": {
8
+ "name": "Test API",
9
+ "description": "This URL will be used to query the third-party API",
10
+ "type": "string"
11
+ }
12
+ },
13
+ "provider": {
14
+ "name": "Commerce",
15
+ "handler": {
16
+ "graphql": {
17
+ "endpoint": "${ENDPOINT_URL}"
18
+ }
19
+ },
20
+ "transforms": [
21
+ {
22
+ "rename": {
23
+ "mode": "bare | wrap",
24
+ "renames": [
25
+ {
26
+ "from": {
27
+ "type": "Query",
28
+ "field": "compareList"
29
+ },
30
+ "to": {
31
+ "type": "Query",
32
+ "field": "productCompareList"
33
+ }
34
+ }
35
+ ]
36
+ }
37
+ }
38
+ ]
39
+ }
40
+ }
@@ -12,5 +12,12 @@
12
12
  "author": "VZ",
13
13
  "latest": "0.0.1",
14
14
  "versions": ["0.0.1"]
15
+ },
16
+ "test-03": {
17
+ "name": "test-03",
18
+ "description": "Mock for variable injection",
19
+ "author": "VladimirZaets",
20
+ "latest": "0.0.1",
21
+ "versions": ["0.0.1"]
15
22
  }
16
23
  }
@@ -12,10 +12,10 @@ governing permissions and limitations under the License.
12
12
 
13
13
  const mockMetadataFixture = require('../__fixtures__/connectors-metadata.json');
14
14
  const mockAdapter = require('source-registry-storage-adapter');
15
- const { promptConfirm } = require('../../../../helpers');
16
- const GetCommand = require('../get');
15
+ const { promptConfirm, promptMultiselect } = require('../../../../helpers');
17
16
  const { CliUx } = require('@oclif/core');
18
17
  const DiscoverCommand = require('../discover');
18
+ const InstallCommand = require('../install');
19
19
  jest.mock('source-registry-storage-adapter');
20
20
  jest.mock('../../../../helpers');
21
21
  jest.mock('../get');
@@ -43,16 +43,17 @@ describe('source:discover command tests', () => {
43
43
  await DiscoverCommand.run([]);
44
44
  expect(CliUx.Table.table).toHaveBeenCalledTimes(1);
45
45
  });
46
- test('Check that "source:get -m" command is called', async () => {
47
- GetCommand.run = jest.fn().mockImplementation(() => 'source:get -m');
46
+ test('Check that "source:install" command is called', async () => {
47
+ InstallCommand.run = jest.fn().mockImplementation(() => 'source:install');
48
48
  promptConfirm.mockResolvedValue(true);
49
+ promptMultiselect.mockResolvedValue([mockMetadataFixture['test-01']])
49
50
  await DiscoverCommand.run([]);
50
- expect(GetCommand.run).toHaveBeenCalledTimes(1);
51
+ expect(InstallCommand.run).toHaveBeenCalledTimes(1);
51
52
  });
52
- test('Check that "source:get -m" command is not called', async () => {
53
- GetCommand.run = jest.fn().mockImplementation(() => 'source:get -m');
53
+ test('Check that "source:install" command is not called', async () => {
54
+ InstallCommand.run = jest.fn().mockImplementation(() => 'source:install');
54
55
  promptConfirm.mockResolvedValue(false);
55
56
  await DiscoverCommand.run([]);
56
- expect(GetCommand.run).toHaveBeenCalledTimes(0);
57
+ expect(InstallCommand.run).toHaveBeenCalledTimes(0);
57
58
  });
58
59
  });
@@ -30,6 +30,9 @@ const expectedResultForSuccessScenarios = JSON.stringify(
30
30
  null,
31
31
  4,
32
32
  );
33
+ const normalizeAssertString = str => {
34
+ return str.replace(/[\r\t]/gm, '').replace(/[\r\n]/gm, '').replace(/ +/g, ' ')
35
+ }
33
36
  mockAdapter.mockImplementation(() => ({
34
37
  get: jest
35
38
  .fn()
@@ -54,6 +57,14 @@ describe('source:get command tests', () => {
54
57
  );
55
58
  expect(GetCommand.flags).toMatchInlineSnapshot(`
56
59
  Object {
60
+ "confirm": Object {
61
+ "allowNo": false,
62
+ "char": "c",
63
+ "default": false,
64
+ "description": "Auto confirm print action prompt. CLI will not check ask user to print source.",
65
+ "parse": [Function],
66
+ "type": "boolean",
67
+ },
57
68
  "multiple": Object {
58
69
  "allowNo": false,
59
70
  "char": "m",
@@ -78,15 +89,13 @@ describe('source:get command tests', () => {
78
89
  });
79
90
  test('Check executing without parameters', async () => {
80
91
  await GetCommand.run([]).catch(err => {
81
- expect(err.message).toEqual(`The "aio api-mesh:source:get" command requires additional parameters` +
82
- `\nUse "aio api-mesh:source:get --help" to see parameters information.`)
92
+ expect(normalizeAssertString(err.message)).toEqual(
93
+ normalizeAssertString(`Something went wrong with "get" command. Please try again later.` +
94
+ `Error: \nThe "aio api-mesh:source:get" command requires additional parameters` +
95
+ `Use "aio api-mesh:source:get --help" to see parameters information.`)
96
+ );
83
97
  });
84
- });
85
- test('Check executing success with multiple, copied to clipboard and logged to console', async () => {
86
- await GetCommand.run(['-m']);
87
- expect(ncp.readSync()).toEqual(expectedResultForSuccessScenarios);
88
- expect(logSpy.mock.calls.pop()[0]).toEqual(expectedResultForSuccessScenarios);
89
- });
98
+ });
90
99
  test('Check executing success with provided name and version, copied to clipboard and logged to console', async () => {
91
100
  await GetCommand.run(['-s=test-01@0.0.1', '-s=test-02@0.0.1']);
92
101
  expect(ncp.readSync()).toEqual(expectedResultForSuccessScenarios);
@@ -100,11 +109,8 @@ describe('source:get command tests', () => {
100
109
  test('Check executing failed due to requested source does not exist', async () => {
101
110
  const name = 'test-99';
102
111
  await GetCommand.run([`-s=${name}`]).catch(err => {
103
- expect(err.message).toEqual(
104
- chalk.red(
105
- `The source with the name "${name}" doesn't exist.` +
106
- `\nUse "aio api-mesh:source:discover" command to see avaliable sources.`,
107
- ),
112
+ expect(err.message).toContain(
113
+ chalk.red(`The source with the name "test-99" doesn't exist.`),
108
114
  );
109
115
  });
110
116
  });
@@ -112,11 +118,8 @@ describe('source:get command tests', () => {
112
118
  const name = 'test-01';
113
119
  const version = '1.1.1';
114
120
  await GetCommand.run([`-s=${name}@${version}`]).catch(err => {
115
- expect(err.message).toEqual(
116
- chalk.red(
117
- `The version "${version}" for source name "${name}" doesn't exist.` +
118
- `\nUse "aio api-mesh:source:discover" command to see avaliable source versions.`,
119
- ),
121
+ expect(err.message).toContain(
122
+ chalk.red(`The version \"1.1.1\" for source name \"test-01\" doesn't exist.`),
120
123
  );
121
124
  });
122
125
  });
@@ -0,0 +1,153 @@
1
+ /*
2
+ Copyright 2021 Adobe. All rights reserved.
3
+ This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License. You may obtain a copy
5
+ of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+
7
+ Unless required by applicable law or agreed to in writing, software distributed under
8
+ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ OF ANY KIND, either express or implied. See the License for the specific language
10
+ governing permissions and limitations under the License.
11
+ */
12
+
13
+ const mockMetadataFixture = require('../__fixtures__/connectors-metadata.json');
14
+ const mockSourceTest01v1Fixture = require('../__fixtures__/0.0.1-test-01.json');
15
+ const mockSourceTest02v1Fixture = require('../__fixtures__/0.0.1-test-02.json');
16
+ const mockSourceTest03v1Fixture = require('../__fixtures__/0.0.1-test-03.json');
17
+ const mockAdapter = require('source-registry-storage-adapter');
18
+ const chalk = require('chalk');
19
+ const { initSdk, initRequestId, promptInput } = require('../../../../helpers');
20
+ const mockSources = { '0.0.1-test-03': mockSourceTest03v1Fixture };
21
+ jest.mock('source-registry-storage-adapter');
22
+ jest.mock('../../../../helpers');
23
+ const InstallCommand = require('../install');
24
+ const { getMeshId, getMesh, updateMesh } = require('../../../../lib/devConsole');
25
+ const mockGetMeshConfig = require('../../../__fixtures__/sample_mesh.json');
26
+ const selectedOrg = { id: '1234', code: 'CODE1234@AdobeOrg', name: 'ORG01', type: 'entp' };
27
+ const selectedProject = { id: '5678', title: 'Project01' };
28
+ const selectedWorkspace = { id: '123456789', title: 'Workspace01' };
29
+ global.requestId = 'dummy_request_id';
30
+ jest.mock('../../../../lib/devConsole');
31
+ mockAdapter.mockImplementation(() => ({
32
+ get: jest.fn().mockResolvedValue(mockSources[`0.0.1-test-03`]),
33
+ getList: jest.fn().mockImplementation(() => mockMetadataFixture),
34
+ }));
35
+ getMeshId.mockResolvedValue('dummy_meshId');
36
+ getMesh.mockResolvedValue(mockGetMeshConfig);
37
+ updateMesh.mockResolvedValue({ status: 'success' });
38
+
39
+ initSdk.mockResolvedValue({
40
+ imsOrgId: selectedOrg.id,
41
+ projectId: selectedProject.id,
42
+ workspaceId: selectedWorkspace.id,
43
+ });
44
+ initRequestId.mockResolvedValue({});
45
+ promptInput.mockResolvedValueOnce('test-03');
46
+
47
+ describe('source:install command tests', () => {
48
+ test('Snapshot install command description', () => {
49
+ expect(InstallCommand.description).toMatchInlineSnapshot(
50
+ `"Command to install the source to your API mesh."`,
51
+ );
52
+ expect(InstallCommand.flags).toMatchInlineSnapshot(`
53
+ Object {
54
+ "confirm": Object {
55
+ "allowNo": false,
56
+ "char": "c",
57
+ "default": false,
58
+ "description": "Auto confirm override action prompt. CLI will not check ask user to override source.",
59
+ "parse": [Function],
60
+ "type": "boolean",
61
+ },
62
+ "ignoreCache": Object {
63
+ "allowNo": false,
64
+ "char": "i",
65
+ "default": false,
66
+ "description": "Ignore cache and force manual org -> project -> workspace selection",
67
+ "parse": [Function],
68
+ "type": "boolean",
69
+ },
70
+ "source": Object {
71
+ "char": "s",
72
+ "description": "Source name",
73
+ "input": Array [],
74
+ "multiple": true,
75
+ "parse": [Function],
76
+ "type": "option",
77
+ },
78
+ "variable": Object {
79
+ "char": "v",
80
+ "description": "Variables required for the source",
81
+ "input": Array [],
82
+ "multiple": true,
83
+ "parse": [Function],
84
+ "type": "option",
85
+ },
86
+ "variable-file": Object {
87
+ "char": "f",
88
+ "description": "Variables file path",
89
+ "input": Array [],
90
+ "multiple": false,
91
+ "parse": [Function],
92
+ "type": "option",
93
+ },
94
+ }
95
+ `);
96
+ expect(InstallCommand.aliases).toMatchInlineSnapshot(`Array []`);
97
+ });
98
+ test('Check executing without parameters', async () => {
99
+ await InstallCommand.run([]).catch(err => {
100
+ expect(err.message).toEqual(
101
+ `The "aio api-mesh:source:install" command requires additional parameters` +
102
+ `\nUse "aio api-mesh:source:install --help" to see parameters details.`,
103
+ );
104
+ });
105
+ });
106
+ test('Check executing with invalid file parameter', async () => {
107
+ await InstallCommand.run(['test-03', '-f=notexist.json']).catch(err => {
108
+ expect(err.message).toEqual(
109
+ `Something went wrong trying to read the variables file.` +
110
+ `\nENOENT: no such file or directory, open 'notexist.json'`,
111
+ );
112
+ });
113
+ });
114
+ test('Check executing with invalid variable type', async () => {
115
+ const path = `-f=${__dirname}/../__fixtures__/variables-file-invalid.json`;
116
+ await InstallCommand.run(['test-03', path]).catch(err => {
117
+ expect(err.message).toEqual(
118
+ chalk.red(
119
+ `The next variables has invalid type.\nVariable: ENDPOINT_URL\nRequested type: string`,
120
+ ),
121
+ );
122
+ });
123
+ });
124
+ test('Check executing without passing variables', async () => {
125
+ await InstallCommand.run(['test-03']);
126
+ expect(promptInput).toHaveBeenCalledTimes(1);
127
+ });
128
+ test('Check executing without passing variables and input data', async () => {
129
+ await InstallCommand.run(['test-03']).catch(err => {
130
+ expect(err.message).toEqual(
131
+ chalk.red(
132
+ `The next variables has invalid type.\nVariable: ENDPOINT_URL\nRequested type: string`,
133
+ ),
134
+ );
135
+ });
136
+ expect(promptInput).toHaveBeenCalledTimes(1);
137
+ });
138
+ test('Check executing with passing variables in CLI', async () => {
139
+ const parsed =
140
+ '{"name":"Test 03","version":"0.0.1","description":"Mock for variable injection","author":"VladimirZaets","variables":{"ENDPOINT_URL":{"name":"Test API","description":"This URL will be used to query the third-party API","type":"string"}},"provider":{"name":"Commerce","handler":{"graphql":{"endpoint":"https:www.myendpoint.com/api"}},"transforms":[{"rename":{"mode":"bare | wrap","renames":[{"from":{"type":"Query","field":"compareList"},"to":{"type":"Query","field":"productCompareList"}}]}}]}}';
141
+ const res = { ...mockGetMeshConfig };
142
+ res.meshConfig.sources.push(JSON.parse(parsed));
143
+ await InstallCommand.run(['test-03', '-v ENDPOINT_URL=https:www.myendpoint.com/api']);
144
+ expect(promptInput).toHaveBeenCalledTimes(0);
145
+ expect(updateMesh).toHaveBeenCalledWith(
146
+ selectedOrg.id,
147
+ selectedProject.id,
148
+ selectedWorkspace.id,
149
+ 'dummy_meshId',
150
+ res,
151
+ );
152
+ });
153
+ });
@@ -9,12 +9,12 @@ OF ANY KIND, either express or implied. See the License for the specific languag
9
9
  governing permissions and limitations under the License.
10
10
  */
11
11
 
12
- const { Command, CliUx } = require('@oclif/core');
13
- const { promptConfirm, initRequestId } = require('../../../helpers');
14
- const GetCommand = require('./get');
12
+ const { Command, CliUx, Flags } = require('@oclif/core');
13
+ const { promptConfirm, initRequestId, promptMultiselect, promptSelect } = require('../../../helpers');
15
14
  const SourceRegistryStorage = require('source-registry-storage-adapter');
16
15
  const config = require('@adobe/aio-lib-core-config');
17
16
  const logger = require('../../../classes/logger');
17
+ const InstallCommand = require('./install');
18
18
 
19
19
 
20
20
  class DiscoverCommand extends Command {
@@ -23,6 +23,7 @@ class DiscoverCommand extends Command {
23
23
  await initRequestId();
24
24
 
25
25
  logger.info(`RequestId: ${global.requestId}`);
26
+ const { flags } = await this.parse(DiscoverCommand);
26
27
  const srs = new SourceRegistryStorage(config.get('api-mesh.sourceRegistry.path'));
27
28
  let list;
28
29
  try {
@@ -33,10 +34,24 @@ class DiscoverCommand extends Command {
33
34
  }
34
35
 
35
36
  this.generateSourcesTable(list);
36
- const needInstall = await promptConfirm(`Are you want to install sources?`);
37
+ let needInstall = false;
38
+
39
+ if (flags.confirm) {
40
+ needInstall = true;
41
+ } else {
42
+ needInstall = await promptConfirm(`Do you want to install sources?`);
43
+ }
44
+
37
45
  if (needInstall) {
38
- GetCommand.run(['-m']);
46
+ const toInstall = await this.handleMultiple(list)
47
+ const params = [];
48
+ toInstall.forEach(source => {
49
+ params.push('-s');
50
+ params.push(source)
51
+ })
52
+ InstallCommand.run(params);
39
53
  }
54
+
40
55
  } catch (error) {
41
56
  logger.error(error);
42
57
  this.error(`
@@ -46,6 +61,36 @@ class DiscoverCommand extends Command {
46
61
  }
47
62
  }
48
63
 
64
+ async handleMultiple(data) {
65
+ const result = [];
66
+ let selectedList = await promptMultiselect(
67
+ 'Select sources to install',
68
+ Object.values(data).map(elem => ({ name: elem.name, value: elem })),
69
+ ) || [];
70
+
71
+ if (!selectedList.length) {
72
+ while (!selectedList.length) {
73
+ selectedList = await promptMultiselect(
74
+ 'Please choose at least one source',
75
+ Object.values(data).map(elem => ({ name: elem.name, value: elem })),
76
+ ) || []
77
+ }
78
+ }
79
+
80
+ for (const selected of selectedList) {
81
+ if (selected.versions.length > 1) {
82
+ const version = await promptSelect(
83
+ `Please choose the version of "${selected.name}" source`,
84
+ selected.versions.map(v => ({ name: `v${v}`, value: `${selected.name}@${v}` })),
85
+ );
86
+ result.push(version);
87
+ } else {
88
+ result.push(`${selected.name}@${selected.latest}`);
89
+ }
90
+ }
91
+ return result;
92
+ }
93
+
49
94
  async generateSourcesTable(data) {
50
95
  const columns = {
51
96
  name: {
@@ -65,6 +110,14 @@ class DiscoverCommand extends Command {
65
110
  }
66
111
  }
67
112
 
113
+ DiscoverCommand.flags = {
114
+ confirm: Flags.boolean({
115
+ char: 'c',
116
+ description:'Auto confirm install action prompt. CLI will not check ask user to install source.',
117
+ default: false,
118
+ }),
119
+ };
120
+
68
121
  DiscoverCommand.description = 'Return the list of avaliable sources';
69
122
 
70
123
  module.exports = DiscoverCommand;
@@ -41,7 +41,7 @@ class GetCommand extends Command {
41
41
  const { flags } = await this.parse(GetCommand);
42
42
  if (!flags.source && !flags.multiple) {
43
43
  this.error(
44
- `The "aio api-mesh:source:get" command requires additional parameters` +
44
+ `\nThe "aio api-mesh:source:get" command requires additional parameters` +
45
45
  `\nUse "aio api-mesh:source:get --help" to see parameters information.`,
46
46
  );
47
47
  }
@@ -77,16 +77,20 @@ class GetCommand extends Command {
77
77
  'The sources are copied to the clipboard, please paste them to your API Mesh configuration',
78
78
  ),
79
79
  );
80
- const print = await promptConfirm(`Do you want to print Source configurations in console?`);
81
- if (print) {
80
+ if (!flags.confirm) {
81
+ const print = await promptConfirm(`Do you want to print Source configurations in console?`);
82
+ if (print) {
83
+ this.log(sourceConfigsString);
84
+ }
85
+ } else {
82
86
  this.log(sourceConfigsString);
83
- }
87
+ }
84
88
  } catch (error) {
85
89
  logger.error(error);
86
90
  this.error(`
87
- Something went wrong with "get" command. Please try again later.
88
- ${error}
89
- `);
91
+ \nSomething went wrong with "get" command. Please try again later.
92
+ \n${error}`
93
+ );
90
94
  }
91
95
  }
92
96
 
@@ -116,6 +120,11 @@ class GetCommand extends Command {
116
120
  }
117
121
 
118
122
  GetCommand.flags = {
123
+ confirm: Flags.boolean({
124
+ char: 'c',
125
+ description:'Auto confirm print action prompt. CLI will not check ask user to print source.',
126
+ default: false,
127
+ }),
119
128
  source: Flags.string({
120
129
  char: 's',
121
130
  description: 'Source name',
@@ -130,8 +139,8 @@ GetCommand.flags = {
130
139
 
131
140
  GetCommand.description = 'Command returns the content of a specific source.';
132
141
  GetCommand.examples = [
133
- '$ aio api-mesh:source:get <version>@<source_name>',
134
- '$ aio api-mesh:source:get <source_name>',
142
+ '$ aio api-mesh:source:get -s=<version>@<source_name>',
143
+ '$ aio api-mesh:source:get -s<source_name>',
135
144
  '$ aio api-mesh:source:get -m',
136
145
  ];
137
146
 
@@ -0,0 +1,291 @@
1
+ /*
2
+ Copyright 2020 Adobe. All rights reserved.
3
+ This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License. You may obtain a copy
5
+ of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+
7
+ Unless required by applicable law or agreed to in writing, software distributed under
8
+ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ OF ANY KIND, either express or implied. See the License for the specific language
10
+ governing permissions and limitations under the License.
11
+ */
12
+
13
+ const { Command, Flags } = require('@oclif/core');
14
+ const SourceRegistryStorage = require('source-registry-storage-adapter');
15
+ const { promptConfirm, promptInput, initRequestId, initSdk } = require('../../../helpers');
16
+ const { ignoreCacheFlag } = require('../../../utils');
17
+ const config = require('@adobe/aio-lib-core-config');
18
+ const logger = require('../../../classes/logger');
19
+ const { readFile } = require('fs/promises');
20
+ const chalk = require('chalk');
21
+ const { getMeshId, getMesh, updateMesh } = require('../../../lib/devConsole');
22
+ const JsonInterpolate = require('json-interpolate');
23
+
24
+ class InstallCommand extends Command {
25
+ static args = [{ name: 'source' }];
26
+
27
+ constructor() {
28
+ super(...arguments);
29
+ this.sourceRegistryStorage = new SourceRegistryStorage(
30
+ config.get('api-mesh.sourceRegistry.path'),
31
+ );
32
+ }
33
+
34
+ async run() {
35
+ const { flags, args } = await this.parse(InstallCommand);
36
+ await initRequestId();
37
+ logger.info(`RequestId: ${global.requestId}`);
38
+ const ignoreCache = await flags.ignoreCache;
39
+ const { imsOrgId, projectId, workspaceId } = await initSdk({ ignoreCache });
40
+ const filepath = flags['variable-file'];
41
+ let variables = flags.variable
42
+ ? flags.variable.reduce((obj, val) => {
43
+ const splited = val.split('=');
44
+ obj[splited[0].trim()] = splited[1].trim();
45
+ return obj;
46
+ }, {})
47
+ : {};
48
+
49
+ if (filepath) {
50
+ try {
51
+ variables = { ...variables, ...JSON.parse(await readFile(filepath, 'utf8')) };
52
+ } catch (e) {
53
+ this.error(`Something went wrong trying to read the variables file.` + `\n${e.message}`);
54
+ }
55
+ }
56
+
57
+ let meshId = null;
58
+ if (!args.source && !flags.source) {
59
+ this.error(
60
+ `The "aio api-mesh:source:install" command requires additional parameters` +
61
+ `\nUse "aio api-mesh:source:install --help" to see parameters details.`,
62
+ );
63
+ }
64
+ let sourceProviders;
65
+ try {
66
+ sourceProviders = await this.sourceRegistryStorage.getList();
67
+ } catch (err) {
68
+ this.error(`Cannot get the list of sources: ${err}. RequestId: ${global.requestId}`);
69
+ }
70
+ const sources = flags.source ? flags.source : [args.source];
71
+ const sourceConfigs = {sources: [], files: {}};
72
+ for (const source of sources) {
73
+ let [name, version] = source.split('@');
74
+ const normalizedName = this.normalizeName(name);
75
+ if (!sourceProviders[normalizedName]) {
76
+ this.error(
77
+ chalk.red(
78
+ `The source named "${name}" doesn't exist.` +
79
+ `\nUse "aio api-mesh:source:discover" command to see avaliable sources.`,
80
+ ),
81
+ );
82
+ }
83
+ version = version || sourceProviders[normalizedName].latest;
84
+ if (!sourceProviders[normalizedName].versions.includes(version)) {
85
+ this.error(
86
+ chalk.red(
87
+ `The version "${version}" for source name "${name}" doesn't exist.` +
88
+ `\nUse "aio api-mesh:source:discover" command to see avaliable source versions.`,
89
+ ),
90
+ );
91
+ }
92
+ const sourceConfig = await this.sourceRegistryStorage.get(name, version);
93
+ const jsonInterpolate = new JsonInterpolate({ variablesSchema: sourceConfig.variables });
94
+ const sourceProviderString = JSON.stringify(sourceConfig.provider);
95
+ const sourceVariables = jsonInterpolate.getJsonVariables(sourceProviderString);
96
+ const passedSourceVariables = this.getPassedSourceVariables(sourceVariables || [], variables);
97
+ const missedVariables = jsonInterpolate.getMissedVariables(passedSourceVariables, sourceVariables);
98
+ for (const missedVariable of missedVariables.map(item => item.name).filter((value, index, self) => self.indexOf(value) === index)) {
99
+ passedSourceVariables[missedVariable] = await promptInput(
100
+ `Enter the value for variable ${missedVariable}:`,
101
+ );
102
+ }
103
+
104
+ const { error, data } = jsonInterpolate.interpolate(
105
+ JSON.stringify(sourceConfig.provider),
106
+ passedSourceVariables,
107
+ );
108
+ if (error) {
109
+ this.error(chalk.red(`${error.message}\n${error.list.map(err => err.message).join('\n')}`));
110
+ }
111
+
112
+ sourceConfigs.sources.push(JSON.parse(data));
113
+ sourceConfigs.files[sourceConfig.provider.name] = sourceConfig.files;
114
+ }
115
+
116
+ try {
117
+ meshId = await getMeshId(imsOrgId, projectId, workspaceId);
118
+ } catch (err) {
119
+ this.error(
120
+ `Unable to get mesh ID. Please check the details and try again. RequestId: ${global.requestId}`,
121
+ );
122
+ }
123
+
124
+ if (!meshId) {
125
+ this.error(
126
+ `Unable to get mesh config. No mesh found for Org(${imsOrgId}) -> Project(${projectId}) -> Workspace(${workspaceId}). Please check the details and try again.`,
127
+ );
128
+ }
129
+
130
+ try {
131
+ const mesh = await getMesh(imsOrgId, projectId, workspaceId, meshId);
132
+
133
+ if (!mesh) {
134
+ this.error(
135
+ `Unable to get mesh with the ID ${meshId}. Please check the mesh ID and try again. RequestId: ${global.requestId}`,
136
+ { exit: false },
137
+ );
138
+ }
139
+ const verifiedSources = this.verifySourceAlreadyExists(
140
+ mesh.meshConfig.sources,
141
+ sourceConfigs.sources,
142
+ );
143
+
144
+ let override = false;
145
+ if (verifiedSources.installed.length) {
146
+ override = flags.confirm ? true : await promptConfirm(
147
+ `The following sources are already installed: ${verifiedSources.installed
148
+ .map(source => source.name)
149
+ .join(', ')}.
150
+ Do you want to override?`,
151
+ );
152
+ }
153
+
154
+ const uniqueFiles = this.getSourceFiles(verifiedSources.unique.map(source => source.name), sourceConfigs.files);
155
+ const installedFiles = this.getSourceFiles(verifiedSources.installed.map(source => source.name), sourceConfigs.files);
156
+ let meshConfigFiles = mesh.meshConfig.files || [];
157
+
158
+ if (override) {
159
+ const installedMap = verifiedSources.installed.reduce((obj, source) => {
160
+ obj[source.name] = true;
161
+ return obj
162
+ }, {});
163
+
164
+ mesh.meshConfig.sources = [
165
+ ...mesh.meshConfig.sources.filter(source => !installedMap[source.name]),
166
+ ...verifiedSources.installed
167
+ ];
168
+
169
+ const installedFilesMap = installedFiles.reduce((obj, file) => {
170
+ obj[file.path] = true;
171
+ return obj
172
+ }, {});
173
+
174
+ meshConfigFiles = [
175
+ ...meshConfigFiles.filter(file => !installedFilesMap[file.path]),
176
+ ...installedFiles
177
+ ];
178
+ }
179
+
180
+ mesh.meshConfig.sources = [
181
+ ...mesh.meshConfig.sources,
182
+ ...verifiedSources.unique
183
+ ]
184
+
185
+ meshConfigFiles = [
186
+ ...meshConfigFiles,
187
+ ...uniqueFiles
188
+ ]
189
+
190
+ if (meshConfigFiles.length) {
191
+ mesh.meshConfig.files = meshConfigFiles
192
+ }
193
+
194
+ try {
195
+ const response = await updateMesh(imsOrgId, projectId, workspaceId, meshId, {
196
+ meshConfig: mesh.meshConfig,
197
+ });
198
+
199
+ this.log('Successfully updated the mesh with the id: %s', meshId);
200
+
201
+ return response;
202
+ } catch (error) {
203
+ this.log(error.message);
204
+
205
+ this.error(
206
+ `Unable to update the mesh. Please check the mesh configuration file and try again. If the error persists please contact support. RequestId: ${global.requestId}`,
207
+ );
208
+ }
209
+
210
+ return mesh;
211
+ } catch (error) {
212
+ this.log(error.message);
213
+ this.error(
214
+ `Unable to get mesh. Please check the details and try again. If the error persists please contact support. RequestId: ${global.requestId}`,
215
+ );
216
+ }
217
+ }
218
+
219
+ normalizeName(name) {
220
+ return name.toLowerCase().split(' ').join('-');
221
+ }
222
+
223
+ getSourceFiles(sourcesList, filesList) {
224
+ let result = [];
225
+ for (const source of sourcesList) {
226
+ if (Array.isArray(filesList[source])) {
227
+ result = [...result, ...filesList[source]]
228
+ }
229
+ }
230
+ return result;
231
+ }
232
+
233
+ getPassedSourceVariables(variablesInSource, passedVariables) {
234
+ const res = {}
235
+ variablesInSource.forEach(variable => {
236
+ if (passedVariables[variable.name]) {
237
+ res[variable.name] = passedVariables[variable.name]
238
+ }
239
+ });
240
+ return res;
241
+ }
242
+
243
+ verifySourceAlreadyExists(meshSources, installSources) {
244
+ const alreadyInstalledSources = [];
245
+ const uniqueSourcesToInstall = [];
246
+ installSources.forEach(installSource => {
247
+ const source = meshSources.find(meshSource => meshSource.name === installSource.name);
248
+ if (source) {
249
+ alreadyInstalledSources.push(source);
250
+ } else {
251
+ uniqueSourcesToInstall.push(installSource);
252
+ }
253
+ });
254
+ return {
255
+ installed: alreadyInstalledSources,
256
+ unique: uniqueSourcesToInstall,
257
+ };
258
+ }
259
+ }
260
+
261
+ InstallCommand.flags = {
262
+ source: Flags.string({
263
+ char: 's',
264
+ description: 'Source name',
265
+ multiple: true,
266
+ }),
267
+ confirm: Flags.boolean({
268
+ char: 'c',
269
+ description:'Auto confirm override action prompt. CLI will not check ask user to override source.',
270
+ default: false,
271
+ }),
272
+ variable: Flags.string({
273
+ char: 'v',
274
+ description: 'Variables required for the source',
275
+ multiple: true,
276
+ }),
277
+ 'variable-file': Flags.string({
278
+ char: 'f',
279
+ description: 'Variables file path',
280
+ }),
281
+ ignoreCache: ignoreCacheFlag,
282
+ };
283
+
284
+ InstallCommand.description = 'Command to install the source to your API mesh.';
285
+ InstallCommand.examples = [
286
+ '$ aio api-mesh:source:install <version>@<source_name>',
287
+ '$ aio api-mesh:source:install <source_name> -v <variable_name>=<variable_value>',
288
+ '$ aio api-mesh:source:install <source_name> -f <path_to_variables_file>',
289
+ ];
290
+
291
+ module.exports = InstallCommand;
package/src/helpers.js CHANGED
@@ -434,7 +434,27 @@ async function promptSelect(message, choices) {
434
434
  return selected.item;
435
435
  }
436
436
 
437
+ /**
438
+ * Function to run the CLI selectable list
439
+ *
440
+ * @param {string} message - prompt message
441
+ * @param {object[]} choices - list of options
442
+ * @returns {object[]} - selected options
443
+ */
444
+ async function promptInput(message) {
445
+ const selected = await inquirer.prompt([
446
+ {
447
+ name: 'item',
448
+ message: message,
449
+ type: 'input',
450
+ },
451
+ ]);
452
+
453
+ return selected.item;
454
+ }
455
+
437
456
  module.exports = {
457
+ promptInput,
438
458
  promptConfirm,
439
459
  getLibConsoleCLI,
440
460
  getDevConsoleConfig,
package/src/utils.js CHANGED
@@ -49,8 +49,15 @@ const autoConfirmActionFlag = Flags.boolean({
49
49
  default: false,
50
50
  });
51
51
 
52
+ const jsonFlag = Flags.boolean({
53
+ description:
54
+ 'Output JSON',
55
+ default: false,
56
+ });
57
+
52
58
  module.exports = {
53
59
  objToString,
54
60
  ignoreCacheFlag,
55
61
  autoConfirmActionFlag,
62
+ jsonFlag,
56
63
  };