@commercetools-frontend/mc-scripts 21.0.1 → 21.3.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/build/bin/cli.js CHANGED
@@ -19,6 +19,8 @@ const dotenvExpand = require('dotenv-expand');
19
19
 
20
20
  const spawn = require('react-dev-utils/crossSpawn');
21
21
 
22
+ const pkg = require('../../package.json');
23
+
22
24
  const flags = mri(process.argv.slice(2), {
23
25
  alias: {
24
26
  help: ['h']
@@ -50,11 +52,18 @@ Commands:
50
52
 
51
53
  serve Serves previously built and compiled application from the "public" folder.
52
54
 
55
+ login Log in to your Merchant Center account through the CLI, using the cloud environment information from the Custom Application config file. An API token is generated and stored in a configuration file for the related cloud environment, and valid for 36 hours.
56
+
57
+ config:sync Synchronizes the local Custom Application config with the Merchant Center. A new Custom Application will be created if none existed, otherwise it will be updated.
58
+ --dry-run (optional) Executes the command but does not send any mutation request.
53
59
  `);
54
60
  process.exit(0);
55
61
  }
56
62
 
57
- const command = commands[0]; // Get the current directory where the CLI is executed from. Usually this is the application folder.
63
+ const command = commands[0];
64
+ console.log('');
65
+ console.log(`mc-scripts: v${pkg.version}`);
66
+ console.log(''); // Get the current directory where the CLI is executed from. Usually this is the application folder.
58
67
 
59
68
  const applicationDirectory = fs.realpathSync(process.cwd());
60
69
 
@@ -91,7 +100,7 @@ const applicationDirectory = fs.realpathSync(process.cwd());
91
100
  // Do this as the first thing so that any code reading it knows the right env.
92
101
  process.env.NODE_ENV = 'production'; // Get specific flag for this command.
93
102
 
94
- const commandArgs = getArgsForCommand(['transformer']);
103
+ const commandArgs = getArgsForCommand(['transformer', 'print-security-headers']);
95
104
  proxyCommand(command, {
96
105
  commandArgs
97
106
  });
@@ -107,6 +116,26 @@ const applicationDirectory = fs.realpathSync(process.cwd());
107
116
  break;
108
117
  }
109
118
 
119
+ case 'login':
120
+ {
121
+ // Do this as the first thing so that any code reading it knows the right env.
122
+ process.env.NODE_ENV = 'production';
123
+ proxyCommand(command);
124
+ break;
125
+ }
126
+
127
+ case 'config:sync':
128
+ {
129
+ // Do this as the first thing so that any code reading it knows the right env.
130
+ process.env.NODE_ENV = 'production'; // Get specific flag for this command.
131
+
132
+ const commandArgs = getArgsForCommand(['dry-run']);
133
+ proxyCommand(command, {
134
+ commandArgs
135
+ });
136
+ break;
137
+ }
138
+
110
139
  default:
111
140
  console.log(`Unknown script "${command}".`);
112
141
  console.log('Perhaps you need to update mc-scripts?');
@@ -199,7 +228,7 @@ function loadDotEnvFiles(flags) {
199
228
  const dotenvFilePath = path.resolve(path.join(applicationDirectory, dotenvFile));
200
229
 
201
230
  if (fs.existsSync(dotenvFilePath)) {
202
- dotenvExpand(dotenv.config({
231
+ dotenvExpand.expand(dotenv.config({
203
232
  path: dotenvFilePath
204
233
  }));
205
234
  }
@@ -0,0 +1,171 @@
1
+ "use strict";
2
+
3
+ const omit = require('lodash/omit');
4
+
5
+ const prompts = require('prompts');
6
+
7
+ const mri = require('mri');
8
+
9
+ const chalk = require('chalk');
10
+
11
+ const {
12
+ processConfig
13
+ } = require('@commercetools-frontend/application-config');
14
+
15
+ const CredentialsStorage = require('../utils/credentials-storage');
16
+
17
+ const {
18
+ fetchCustomApplication,
19
+ updateCustomApplication,
20
+ createCustomApplication,
21
+ fetchUserOrganizations
22
+ } = require('../utils/graphql-requests');
23
+
24
+ const updateApplicationIdInCustomApplicationConfig = require('../utils/update-application-id-in-custom-application-config');
25
+
26
+ const flags = mri(process.argv.slice(2), {
27
+ boolean: ['dry-run']
28
+ });
29
+ const credentialsStorage = new CredentialsStorage();
30
+
31
+ const getMcUrlLink = (mcApiUrl, organizationId, applicationId) => {
32
+ const mcUrl = mcApiUrl.replace('mc-api', 'mc');
33
+ const customAppLink = `${mcUrl}/account/organizations/${organizationId}/custom-applications/owned/${applicationId}`;
34
+ return customAppLink;
35
+ };
36
+
37
+ const configSync = async () => {
38
+ const applicationConfig = processConfig();
39
+ const {
40
+ data: localCustomAppData
41
+ } = applicationConfig;
42
+ const {
43
+ mcApiUrl
44
+ } = applicationConfig.env;
45
+
46
+ if (!credentialsStorage.isSessionValid(mcApiUrl)) {
47
+ throw new Error(`You don't have a valid session for the ${mcApiUrl} environment. Please, run the "mc-scripts login" command to authenticate yourself.`);
48
+ }
49
+
50
+ const token = credentialsStorage.getToken(mcApiUrl);
51
+ const fetchedCustomApplication = await fetchCustomApplication({
52
+ mcApiUrl,
53
+ token,
54
+ entryPointUriPath: localCustomAppData.entryPointUriPath
55
+ });
56
+
57
+ if (!fetchedCustomApplication) {
58
+ const userOrganizations = await fetchUserOrganizations({
59
+ mcApiUrl,
60
+ token
61
+ });
62
+ let organizationId, organizationName;
63
+
64
+ if (userOrganizations.total === 0) {
65
+ throw new Error(`It seems you are not an admin of any Organization. Please make sure to be part of the Administrators team of the Organization you want the Custom Application to be configured to.`);
66
+ }
67
+
68
+ if (userOrganizations.total === 1) {
69
+ const [organization] = userOrganizations.results;
70
+ organizationId = organization.id;
71
+ organizationName = organization.name;
72
+ } else {
73
+ const organizationChoices = userOrganizations.results.map(organization => ({
74
+ title: organization.name,
75
+ value: organization.id
76
+ }));
77
+ const {
78
+ organizationId: selectedOrganizationId
79
+ } = await prompts({
80
+ type: 'select',
81
+ name: 'organizationId',
82
+ message: 'Select Organization',
83
+ choices: organizationChoices,
84
+ initial: 0
85
+ });
86
+
87
+ if (!selectedOrganizationId) {
88
+ throw new Error(`No Organization selected, aborting.`);
89
+ }
90
+
91
+ organizationId = selectedOrganizationId;
92
+ organizationName = organizationChoices.find(({
93
+ value
94
+ }) => value === organizationId).title;
95
+ }
96
+
97
+ const {
98
+ confirmation
99
+ } = await prompts({
100
+ type: 'text',
101
+ name: 'confirmation',
102
+ message: `You are about to create a new Custom Application in the "${organizationName}" organization for the ${mcApiUrl} environment. Are you sure you want to proceed?`,
103
+ initial: 'yes'
104
+ });
105
+
106
+ if (!confirmation || confirmation.toLowerCase().charAt(0) !== 'y') {
107
+ console.log(chalk.red('Aborted.'));
108
+ return;
109
+ }
110
+
111
+ const data = omit(localCustomAppData, ['id']);
112
+
113
+ if (flags['dry-run']) {
114
+ console.log(chalk.gray('DRY RUN mode'));
115
+ console.log(`A new Custom Application would be created for the Organization ${organizationName} with the following payload:`);
116
+ console.log(JSON.stringify(data));
117
+ return;
118
+ }
119
+
120
+ const createdCustomApplication = await createCustomApplication({
121
+ mcApiUrl,
122
+ token,
123
+ organizationId,
124
+ data
125
+ }); // update applicationID in the custom-application-config file
126
+
127
+ updateApplicationIdInCustomApplicationConfig(createdCustomApplication.id);
128
+ const customAppLink = getMcUrlLink(mcApiUrl, organizationId, createdCustomApplication.id);
129
+ console.log(chalk.green(`Custom Application created.\nThe "applicationId" in your local Custom Application config file has been updated with the application ID.\nYou can see the Custom Application data in the Merchant Center at ${customAppLink}.`));
130
+ return;
131
+ } // TODO: show diff (followup task)
132
+
133
+
134
+ const {
135
+ confirmation
136
+ } = await prompts({
137
+ type: 'text',
138
+ name: 'confirmation',
139
+ message: `You are about to update the Custom Application "${localCustomAppData.entryPointUriPath}" in the ${mcApiUrl} environment. Are you sure you want to proceed?`,
140
+ initial: 'yes'
141
+ });
142
+
143
+ if (!confirmation || confirmation.toLowerCase().charAt(0) !== 'y') {
144
+ console.log(chalk.red('Aborted.'));
145
+ return;
146
+ }
147
+
148
+ const data = omit(localCustomAppData, ['id']);
149
+
150
+ if (flags['dry-run']) {
151
+ console.log(chalk.gray('DRY RUN mode'));
152
+ console.log(`The Custom Application ${data.name} would be updated with the following payload:`);
153
+ console.log(JSON.stringify(data));
154
+ return;
155
+ }
156
+
157
+ await updateCustomApplication({
158
+ mcApiUrl,
159
+ token,
160
+ organizationId: fetchedCustomApplication.organizationId,
161
+ data: omit(localCustomAppData, ['id']),
162
+ applicationId: fetchedCustomApplication.application.id
163
+ });
164
+ const customAppLink = getMcUrlLink(mcApiUrl, fetchedCustomApplication.organizationId, fetchedCustomApplication.application.id);
165
+ console.log(chalk.green(`Custom Application updated.\nYou can see the Custom Application data in the Merchant Center at ${customAppLink}.`));
166
+ };
167
+
168
+ configSync().catch(error => {
169
+ console.log(chalk.red(error));
170
+ process.exit(1);
171
+ });
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+
3
+ const prompts = require('prompts');
4
+
5
+ const chalk = require('chalk');
6
+
7
+ const {
8
+ processConfig
9
+ } = require('@commercetools-frontend/application-config');
10
+
11
+ const CredentialsStorage = require('../utils/credentials-storage');
12
+
13
+ const {
14
+ getAuthToken
15
+ } = require('../utils/auth');
16
+
17
+ const credentialsStorage = new CredentialsStorage();
18
+
19
+ const login = async () => {
20
+ const applicationConfig = processConfig();
21
+ const {
22
+ mcApiUrl
23
+ } = applicationConfig.env;
24
+
25
+ if (credentialsStorage.isSessionValid(mcApiUrl)) {
26
+ console.log(`You already have a valid session for the ${mcApiUrl} environment.\n`);
27
+ return;
28
+ }
29
+
30
+ const {
31
+ email
32
+ } = await prompts({
33
+ type: 'text',
34
+ name: 'email',
35
+ message: 'Email'
36
+ });
37
+ const {
38
+ password
39
+ } = await prompts({
40
+ type: 'invisible',
41
+ name: 'password',
42
+ message: 'Password (hidden)'
43
+ });
44
+
45
+ if (!email || !password) {
46
+ throw new Error(`Missing email or password values. Aborting.`);
47
+ }
48
+
49
+ const credentials = await getAuthToken(mcApiUrl, {
50
+ email,
51
+ password
52
+ });
53
+ credentialsStorage.setToken(mcApiUrl, credentials);
54
+ console.log(chalk.green(`Login successful for the ${mcApiUrl} environment.\n`));
55
+ };
56
+
57
+ login().catch(error => {
58
+ console.log(chalk.red(error));
59
+ process.exit(1);
60
+ });
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+
3
+ const fetch = require('node-fetch');
4
+
5
+ const userAgent = require('./user-agent');
6
+
7
+ const getAuthToken = async (mcApiUrl, payload) => {
8
+ const response = await fetch(`${mcApiUrl}/tokens/cli`, {
9
+ method: 'POST',
10
+ headers: {
11
+ Accept: 'application/json',
12
+ 'Content-Type': 'application/json',
13
+ 'x-user-agent': userAgent
14
+ },
15
+ body: JSON.stringify(payload)
16
+ });
17
+
18
+ if (!response.ok) {
19
+ const text = await response.text();
20
+ let parsed;
21
+
22
+ try {
23
+ parsed = JSON.parse(text);
24
+ } catch (error) {}
25
+
26
+ const errorMessage = parsed ? parsed.message : text;
27
+ throw new Error(errorMessage);
28
+ }
29
+
30
+ const authToken = await response.json();
31
+ return authToken;
32
+ };
33
+
34
+ exports.getAuthToken = getAuthToken;
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+
3
+ const {
4
+ rest
5
+ } = require('msw');
6
+
7
+ const {
8
+ setupServer
9
+ } = require('msw/node');
10
+
11
+ const {
12
+ getAuthToken
13
+ } = require('./auth');
14
+
15
+ const mockServer = setupServer();
16
+ afterEach(() => {
17
+ mockServer.resetHandlers();
18
+ });
19
+ beforeAll(() => mockServer.listen({
20
+ onUnhandledRequest: 'error'
21
+ }));
22
+ afterAll(() => mockServer.close());
23
+ const mcApiUrl = 'https://mc-api.europe-west1.gcp.commercetools.com';
24
+ describe('when login details are correct', () => {
25
+ beforeEach(() => {
26
+ mockServer.use(rest.post(`${mcApiUrl}/tokens/cli`, (req, res, ctx) => {
27
+ return res(ctx.status(200), ctx.json({
28
+ token: 'hello-world',
29
+ expiresAt: Math.floor(Date.now() / 1000) + 60 * 60 * 36 // 1,5 days
30
+
31
+ }));
32
+ }));
33
+ });
34
+ it('should match returned credentials', async () => {
35
+ const sessionData = await getAuthToken(mcApiUrl, {
36
+ email: 'user@email.com',
37
+ password: 'secret'
38
+ });
39
+ expect(sessionData).toEqual({
40
+ token: 'hello-world',
41
+ expiresAt: expect.any(Number)
42
+ });
43
+ expect(sessionData.expiresAt).toBeGreaterThan(Math.floor(Date.now() / 1000));
44
+ expect(sessionData.expiresAt).toBeLessThanOrEqual(Math.floor(Date.now() / 1000) + 60 * 60 * 36);
45
+ });
46
+ });
47
+ describe('when login details are incorrect', () => {
48
+ beforeEach(() => {
49
+ mockServer.use(rest.post(`${mcApiUrl}/tokens/cli`, (req, res, ctx) => {
50
+ return res(ctx.status(400), ctx.json({
51
+ message: 'Invalid email or password'
52
+ }));
53
+ }));
54
+ });
55
+ it('should throw error', async () => {
56
+ await expect(async () => await getAuthToken(mcApiUrl, {
57
+ email: 'user@email.com',
58
+ password: 'secret'
59
+ })).rejects.toThrow('Invalid email or password');
60
+ });
61
+ });
@@ -0,0 +1,8 @@
1
+ mutation CreateCustomApplicationFromCli(
2
+ $organizationId: String!
3
+ $data: CustomApplicationDraftDataInput!
4
+ ) {
5
+ createCustomApplication(organizationId: $organizationId, data: $data) {
6
+ id
7
+ }
8
+ }
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+
3
+ const fs = require('fs');
4
+
5
+ const path = require('path');
6
+
7
+ const homedir = require('os').homedir();
8
+
9
+ const credentialsFolderPath = path.join(homedir, `.commercetools`);
10
+ const credentialsFilePath = path.join(credentialsFolderPath, 'mc-credentials.json');
11
+
12
+ class CredentialsStorage {
13
+ static location = credentialsFilePath;
14
+
15
+ constructor() {
16
+ // Ensure the credentials file is present
17
+ if (!fs.existsSync(credentialsFilePath)) {
18
+ fs.mkdirSync(credentialsFolderPath, {
19
+ recursive: true
20
+ }); // Initialize with an empty object
21
+
22
+ this._writeCredentials({});
23
+ }
24
+ }
25
+
26
+ _writeCredentials(credentials) {
27
+ fs.writeFileSync(credentialsFilePath, JSON.stringify(credentials, null, 2), {
28
+ encoding: 'utf8'
29
+ });
30
+ }
31
+
32
+ _loadCredentials() {
33
+ const data = fs.readFileSync(credentialsFilePath, {
34
+ encoding: 'utf8'
35
+ });
36
+ return JSON.parse(data);
37
+ }
38
+
39
+ getToken(environmentKey) {
40
+ const allCredentials = this._loadCredentials();
41
+
42
+ if (!this.isSessionValid(environmentKey)) {
43
+ return null;
44
+ }
45
+
46
+ return allCredentials[environmentKey].token;
47
+ }
48
+
49
+ setToken(environmentKey, credentials) {
50
+ const allCredentials = this._loadCredentials();
51
+
52
+ allCredentials[environmentKey] = credentials;
53
+
54
+ this._writeCredentials(allCredentials);
55
+ }
56
+
57
+ isSessionValid(environmentKey) {
58
+ const allCredentials = this._loadCredentials();
59
+
60
+ const credentials = allCredentials[environmentKey];
61
+
62
+ if (!credentials) {
63
+ return false;
64
+ }
65
+
66
+ const now = Math.floor(Date.now() / 1000);
67
+ return now < credentials.expiresAt;
68
+ }
69
+
70
+ }
71
+
72
+ module.exports = CredentialsStorage;
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+
3
+ const mock = require('mock-fs');
4
+
5
+ const CredentialsStorage = require('./credentials-storage');
6
+
7
+ afterEach(() => {
8
+ mock.restore();
9
+ });
10
+ const mcApiUrl = 'https://mc-api.europe-west1.gcp.commercetools.com';
11
+ describe('when session is valid', () => {
12
+ let credentialsStorage;
13
+ beforeEach(() => {
14
+ mock({
15
+ [CredentialsStorage.location]: JSON.stringify({
16
+ [mcApiUrl]: {
17
+ token: 'hello-world',
18
+ expiresAt: Math.floor(Date.now() / 1000) + 60 * 60 * 36
19
+ }
20
+ })
21
+ });
22
+ credentialsStorage = new CredentialsStorage();
23
+ });
24
+ it('should load credentials and update token', () => {
25
+ expect(credentialsStorage.getToken(mcApiUrl)).toBe('hello-world');
26
+ expect(credentialsStorage.isSessionValid(mcApiUrl)).toBe(true);
27
+ const newSessionData = {
28
+ token: 'fizz-buzz',
29
+ expiresAt: Math.floor(Date.now() / 1000) + 60 * 60 * 36
30
+ };
31
+ credentialsStorage.setToken(mcApiUrl, newSessionData);
32
+ expect(credentialsStorage.getToken(mcApiUrl)).toBe('fizz-buzz');
33
+ });
34
+ });
35
+ describe('when session is expired', () => {
36
+ let credentialsStorage;
37
+ beforeEach(() => {
38
+ mock({
39
+ [CredentialsStorage.location]: JSON.stringify({
40
+ [mcApiUrl]: {
41
+ token: 'hello-world',
42
+ expiresAt: Math.floor(Date.now() / 1000) - 1
43
+ }
44
+ })
45
+ });
46
+ credentialsStorage = new CredentialsStorage();
47
+ });
48
+ it('should not load credentials', () => {
49
+ expect(credentialsStorage.isSessionValid(mcApiUrl)).toBe(false);
50
+ expect(credentialsStorage.getToken(mcApiUrl)).toBe(null);
51
+ });
52
+ });
53
+ describe('when credentials file is missing', () => {
54
+ let credentialsStorage;
55
+ beforeEach(() => {
56
+ mock({});
57
+ credentialsStorage = new CredentialsStorage();
58
+ });
59
+ it('should not load credentials and update token', () => {
60
+ expect(credentialsStorage.getToken(mcApiUrl)).toBe(null);
61
+ expect(credentialsStorage.isSessionValid(mcApiUrl)).toBe(false);
62
+ const newSessionData = {
63
+ token: 'fizz-buzz',
64
+ expiresAt: Math.floor(Date.now() / 1000) + 60 * 60 * 36
65
+ };
66
+ credentialsStorage.setToken(mcApiUrl, newSessionData);
67
+ expect(credentialsStorage.getToken(mcApiUrl)).toBe('fizz-buzz');
68
+ });
69
+ });
@@ -0,0 +1,35 @@
1
+ query FetchCustomApplicationFromCli($entryPointUriPath: String!) {
2
+ organizationExtensionForCustomApplication(
3
+ entryPointUriPath: $entryPointUriPath
4
+ ) {
5
+ organizationId
6
+ application {
7
+ id
8
+ entryPointUriPath
9
+ name
10
+ description
11
+ url
12
+ icon
13
+ permissions {
14
+ name
15
+ oAuthScopes
16
+ }
17
+ mainMenuLink {
18
+ defaultLabel
19
+ labelAllLocales {
20
+ locale
21
+ value
22
+ }
23
+ }
24
+ submenuLinks {
25
+ uriPath
26
+ defaultLabel
27
+ permissions
28
+ labelAllLocales {
29
+ locale
30
+ value
31
+ }
32
+ }
33
+ }
34
+ }
35
+ }
@@ -0,0 +1,9 @@
1
+ query FetchMyOrganizationsFromCli {
2
+ myOrganizations {
3
+ total
4
+ results {
5
+ id
6
+ name
7
+ }
8
+ }
9
+ }
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+
3
+ const {
4
+ GraphQLClient
5
+ } = require('graphql-request');
6
+
7
+ const {
8
+ GRAPHQL_TARGETS
9
+ } = require('@commercetools-frontend/constants');
10
+
11
+ const userAgent = require('./user-agent');
12
+
13
+ const requireGraphqlHelper = require('./require-graphql');
14
+
15
+ const requireGraphql = requireGraphqlHelper(__dirname);
16
+ const FetchCustomApplicationFromCli = requireGraphql('./fetch-custom-application.settings.graphql');
17
+ const UpdateCustomApplicationFromCli = requireGraphql('./update-custom-application.settings.graphql');
18
+ const CreateCustomApplicationFromCli = requireGraphql('./create-custom-application.settings.graphql');
19
+ const FetchMyOrganizationsFromCli = requireGraphql('./fetch-user-organizations.core.graphql');
20
+
21
+ const graphQLClient = (uri, token, target = GRAPHQL_TARGETS.SETTINGS_SERVICE) => new GraphQLClient(`${uri}/graphql`, {
22
+ headers: {
23
+ Accept: 'application/json',
24
+ 'Content-Type': 'application/json',
25
+ 'x-graphql-target': target,
26
+ 'x-mc-cli-access-token': token,
27
+ 'x-user-agent': userAgent
28
+ }
29
+ });
30
+
31
+ const fetchCustomApplication = async ({
32
+ mcApiUrl,
33
+ token,
34
+ entryPointUriPath
35
+ }) => {
36
+ const variables = {
37
+ entryPointUriPath
38
+ };
39
+
40
+ try {
41
+ const customAppData = await graphQLClient(mcApiUrl, token).request(FetchCustomApplicationFromCli, variables);
42
+ return customAppData.organizationExtensionForCustomApplication;
43
+ } catch (error) {
44
+ throw new Error(error.response.message);
45
+ }
46
+ };
47
+
48
+ const updateCustomApplication = async ({
49
+ mcApiUrl,
50
+ token,
51
+ applicationId,
52
+ organizationId,
53
+ data
54
+ }) => {
55
+ const variables = {
56
+ organizationId,
57
+ applicationId,
58
+ data
59
+ };
60
+
61
+ try {
62
+ const updatedCustomAppsData = await graphQLClient(mcApiUrl, token).request(UpdateCustomApplicationFromCli, variables);
63
+ return updatedCustomAppsData.updateCustomApplication;
64
+ } catch (error) {
65
+ throw new Error(error.response.message);
66
+ }
67
+ };
68
+
69
+ const createCustomApplication = async ({
70
+ mcApiUrl,
71
+ token,
72
+ organizationId,
73
+ data
74
+ }) => {
75
+ const variables = {
76
+ organizationId,
77
+ data
78
+ };
79
+
80
+ try {
81
+ const createdCustomAppData = await graphQLClient(mcApiUrl, token).request(CreateCustomApplicationFromCli, variables);
82
+ return createdCustomAppData.createCustomApplication;
83
+ } catch (error) {
84
+ throw new Error(error.response.message);
85
+ }
86
+ };
87
+
88
+ const fetchUserOrganizations = async ({
89
+ mcApiUrl,
90
+ token
91
+ }) => {
92
+ try {
93
+ const userOrganizations = await graphQLClient(mcApiUrl, token, GRAPHQL_TARGETS.ADMINISTRATION_SERVICE).request(FetchMyOrganizationsFromCli);
94
+ return userOrganizations.myOrganizations;
95
+ } catch (error) {
96
+ throw new Error(error.response.message);
97
+ }
98
+ };
99
+
100
+ module.exports = {
101
+ fetchCustomApplication,
102
+ updateCustomApplication,
103
+ createCustomApplication,
104
+ fetchUserOrganizations
105
+ };
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+
3
+ const {
4
+ graphql
5
+ } = require('msw');
6
+
7
+ const {
8
+ setupServer
9
+ } = require('msw/node');
10
+
11
+ const {
12
+ createCustomApplication,
13
+ updateCustomApplication,
14
+ fetchCustomApplication,
15
+ fetchUserOrganizations
16
+ } = require('./graphql-requests');
17
+
18
+ const mockServer = setupServer();
19
+ afterEach(() => {
20
+ mockServer.resetHandlers();
21
+ });
22
+ beforeAll(() => mockServer.listen({
23
+ onUnhandledRequest: 'bypass'
24
+ }));
25
+ afterAll(() => mockServer.close());
26
+ const mcApiUrl = 'https://mc-api.europe-west1.gcp.commercetools.com';
27
+ describe('fetch custom application data', () => {
28
+ beforeEach(() => {
29
+ mockServer.use(graphql.query('FetchCustomApplicationFromCli', (req, res, ctx) => {
30
+ return res(ctx.data({
31
+ organizationExtensionForCustomApplication: {
32
+ id: 'test-id',
33
+ organizationId: 'org-id',
34
+ application: {
35
+ url: 'https://test.com',
36
+ name: 'Test name',
37
+ description: 'Test description',
38
+ entryPointUriPath: 'test-custom-app',
39
+ icon: '<svg><path fill="#000000"></path></svg>',
40
+ submenuLinks: [],
41
+ mainMenuLink: [],
42
+ permissions: [{
43
+ oAuthScopes: ['view_products', 'view_customers'],
44
+ name: 'viewTestCustomApp'
45
+ }, {
46
+ oAuthScopes: [],
47
+ name: 'manageTestCustomApp'
48
+ }]
49
+ }
50
+ }
51
+ }));
52
+ }));
53
+ });
54
+ it('should match returned data', async () => {
55
+ const organizationExtensionForCustomApplication = await fetchCustomApplication({
56
+ entryPointUriPath: 'test-custom-app',
57
+ mcApiUrl,
58
+ token: 'test-token'
59
+ });
60
+ expect(organizationExtensionForCustomApplication.application.entryPointUriPath).toEqual('test-custom-app');
61
+ expect(organizationExtensionForCustomApplication.id).toEqual('test-id');
62
+ expect(organizationExtensionForCustomApplication.organizationId).toEqual('org-id');
63
+ });
64
+ });
65
+ describe('register custom application', () => {
66
+ beforeEach(() => {
67
+ mockServer.use(graphql.mutation('CreateCustomApplicationFromCli', (req, res, ctx) => {
68
+ return res(ctx.data({
69
+ createCustomApplication: {
70
+ id: 'new-test-id',
71
+ application: {
72
+ url: 'https://test.com',
73
+ name: 'New Test name',
74
+ description: 'Test description',
75
+ entryPointUriPath: 'new-test-custom-app',
76
+ icon: '<svg><path fill="#000000"></path></svg>',
77
+ submenuLinks: [],
78
+ mainMenuLink: [],
79
+ permissions: [{
80
+ oAuthScopes: ['view_products', 'view_customers'],
81
+ name: 'viewNewTestCustomApp'
82
+ }, {
83
+ oAuthScopes: [],
84
+ name: 'manageNewTestCustomApp'
85
+ }]
86
+ }
87
+ }
88
+ }));
89
+ }));
90
+ });
91
+ it('should match returned data', async () => {
92
+ const createdCustomAppsData = await createCustomApplication({
93
+ entryPointUriPath: 'new-test-custom-app',
94
+ mcApiUrl,
95
+ token: 'new-test-token'
96
+ });
97
+ expect(createdCustomAppsData.application.entryPointUriPath).toEqual('new-test-custom-app');
98
+ expect(createdCustomAppsData.id).toEqual('new-test-id');
99
+ });
100
+ });
101
+ describe('update custom application', () => {
102
+ beforeEach(() => {
103
+ mockServer.use(graphql.mutation('UpdateCustomApplicationFromCli', (req, res, ctx) => {
104
+ return res(ctx.data({
105
+ updateCustomApplication: {
106
+ id: 'test-id',
107
+ application: {
108
+ url: 'https://test.com',
109
+ name: 'Updated Test name',
110
+ description: 'Updated Test description',
111
+ entryPointUriPath: 'updated-test-custom-app',
112
+ icon: '<svg><path fill="#000000"></path></svg>',
113
+ submenuLinks: [],
114
+ mainMenuLink: [],
115
+ permissions: [{
116
+ oAuthScopes: ['view_products', 'view_customers'],
117
+ name: 'viewNewTestCustomApp'
118
+ }, {
119
+ oAuthScopes: [],
120
+ name: 'manageNewTestCustomApp'
121
+ }]
122
+ }
123
+ }
124
+ }));
125
+ }));
126
+ });
127
+ it('should match returned data', async () => {
128
+ const updatedCustomAppsData = await updateCustomApplication({
129
+ entryPointUriPath: 'updated-test-custom-app',
130
+ mcApiUrl,
131
+ token: 'test-token'
132
+ });
133
+ expect(updatedCustomAppsData.application.name).toEqual('Updated Test name');
134
+ expect(updatedCustomAppsData.application.description).toEqual('Updated Test description');
135
+ });
136
+ });
137
+ describe('fetch user organizations', () => {
138
+ beforeEach(() => {
139
+ mockServer.use(graphql.query('FetchMyOrganizationsFromCli', (req, res, ctx) => {
140
+ return res(ctx.data({
141
+ myOrganizations: {
142
+ total: 1,
143
+ results: [{
144
+ id: 'test-organization-id',
145
+ name: 'test-organization-name'
146
+ }]
147
+ }
148
+ }));
149
+ }));
150
+ });
151
+ it('should match returned data', async () => {
152
+ const data = await fetchUserOrganizations({
153
+ mcApiUrl,
154
+ token: 'test-token'
155
+ });
156
+ expect(data.results[0].id).toEqual('test-organization-id');
157
+ expect(data.results[0].name).toEqual('test-organization-name');
158
+ });
159
+ });
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+
3
+ const path = require('path');
4
+
5
+ const {
6
+ readFileSync
7
+ } = require('fs'); // At the moment there is no proper way of loading `.graphql` files
8
+ // in nodejs. This workaround basically uses `readFileSync` to read the file
9
+ // content as a string.
10
+ // https://github.com/apollographql/graphql-tools/issues/273
11
+
12
+
13
+ const requireGraphql = folderPath => filePath => readFileSync(path.join(folderPath, filePath), 'utf-8');
14
+
15
+ module.exports = requireGraphql;
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+
3
+ const fs = require('fs');
4
+
5
+ const rcfile = require('rcfile');
6
+
7
+ const prettier = require('prettier');
8
+
9
+ const babel = require('@babel/core');
10
+
11
+ const {
12
+ getConfigPath
13
+ } = require('@commercetools-frontend/application-config');
14
+
15
+ function updateApplicationIdInCustomApplicationConfig(applicationId) {
16
+ const filePath = getConfigPath();
17
+
18
+ if (filePath.endsWith('.json')) {
19
+ const customApplicationConfig = require(filePath);
20
+
21
+ customApplicationConfig.env.production.applicationId = applicationId;
22
+ const prettierConfig = rcfile('prettier');
23
+ const formattedData = prettier.format(JSON.stringify(customApplicationConfig), { ...prettierConfig,
24
+ parser: 'json'
25
+ });
26
+ fs.writeFileSync(filePath, formattedData, {
27
+ encoding: 'utf8'
28
+ });
29
+ return;
30
+ }
31
+
32
+ const result = babel.transformFileSync(filePath, {
33
+ plugins: [function replaceCustomApplicationConfig() {
34
+ return {
35
+ visitor: {
36
+ Identifier(nodePath) {
37
+ if (nodePath.isIdentifier({
38
+ name: 'applicationId'
39
+ })) {
40
+ if (nodePath.findParent(parentPath => parentPath.get('key').isIdentifier({
41
+ name: 'env'
42
+ }))) {
43
+ nodePath.parent.value = babel.types.stringLiteral(applicationId);
44
+ }
45
+ }
46
+ }
47
+
48
+ }
49
+ };
50
+ }],
51
+ retainLines: true
52
+ });
53
+ const prettierConfig = rcfile('prettier');
54
+ const formattedData = prettier.format(result.code, prettierConfig);
55
+ fs.writeFileSync(filePath, formattedData, {
56
+ encoding: 'utf8'
57
+ });
58
+ }
59
+
60
+ module.exports = updateApplicationIdInCustomApplicationConfig;
@@ -0,0 +1,13 @@
1
+ mutation UpdateCustomApplicationFromCli(
2
+ $organizationId: String!
3
+ $data: CustomApplicationDraftDataInput!
4
+ $applicationId: ID!
5
+ ) {
6
+ updateCustomApplication(
7
+ organizationId: $organizationId
8
+ data: $data
9
+ applicationId: $applicationId
10
+ ) {
11
+ id
12
+ }
13
+ }
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+
3
+ const createHttpUserAgent = require('@commercetools/http-user-agent');
4
+
5
+ const pkgJson = require('../../package.json');
6
+
7
+ const userAgent = createHttpUserAgent({
8
+ name: 'cli-login',
9
+ libraryName: 'mc-scripts',
10
+ libraryVersion: pkgJson.version,
11
+ contactUrl: 'https://git.io/fjuyC',
12
+ // points to the appkit repo issues
13
+ contactEmail: 'support@commercetools.com'
14
+ });
15
+ module.exports = userAgent;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commercetools-frontend/mc-scripts",
3
- "version": "21.0.1",
3
+ "version": "21.3.0",
4
4
  "description": "Configuration and scripts for developing a MC application",
5
5
  "bugs": "https://github.com/commercetools/merchant-center-application-kit/issues",
6
6
  "repository": {
@@ -22,58 +22,67 @@
22
22
  "development": ["last 2 firefox versions", "last 2 chrome versions"]
23
23
  },
24
24
  "scripts": {
25
- "build": "rimraf build && babel src --out-dir build",
25
+ "build": "rimraf build && babel src --out-dir build && ../../scripts/copy-graphql-files.mjs src build",
26
26
  "build:bundles:watch": "yarn build -w"
27
27
  },
28
28
  "dependencies": {
29
- "@babel/runtime": "^7.16.7",
30
- "@babel/runtime-corejs3": "^7.16.8",
31
- "@commercetools-frontend/application-config": "21.0.1",
29
+ "@babel/core": "^7.17.8",
30
+ "@babel/runtime": "^7.17.8",
31
+ "@babel/runtime-corejs3": "^7.17.8",
32
+ "@commercetools-frontend/application-config": "21.3.0",
32
33
  "@commercetools-frontend/assets": "21.0.0",
33
- "@commercetools-frontend/babel-preset-mc-app": "21.0.0",
34
+ "@commercetools-frontend/babel-preset-mc-app": "21.3.0",
35
+ "@commercetools-frontend/constants": "21.3.0",
34
36
  "@commercetools-frontend/mc-dev-authentication": "21.0.0",
35
- "@commercetools-frontend/mc-html-template": "21.0.1",
37
+ "@commercetools-frontend/mc-html-template": "21.3.0",
36
38
  "@pmmmwh/react-refresh-webpack-plugin": "0.5.4",
37
- "@svgr/webpack": "6.2.0",
38
- "autoprefixer": "^10.4.2",
39
- "babel-loader": "8.2.3",
40
- "browserslist": "^4.19.1",
41
- "core-js": "^3.20.3",
42
- "css-loader": "6.5.1",
39
+ "@svgr/webpack": "6.2.1",
40
+ "autoprefixer": "^10.4.4",
41
+ "babel-loader": "8.2.4",
42
+ "browserslist": "^4.20.2",
43
+ "chalk": "4.1.2",
44
+ "core-js": "^3.21.1",
45
+ "css-loader": "6.7.1",
43
46
  "css-minimizer-webpack-plugin": "3.4.1",
44
- "dotenv": "10.0.0",
45
- "dotenv-expand": "5.1.0",
46
- "fs-extra": "10.0.0",
47
+ "dotenv": "16.0.0",
48
+ "dotenv-expand": "8.0.3",
49
+ "fs-extra": "10.0.1",
50
+ "graphql-request": "^4.1.0",
47
51
  "graphql-tag": "^2.12.6",
48
52
  "html-webpack-plugin": "5.5.0",
49
53
  "json-loader": "0.5.7",
50
- "mini-css-extract-plugin": "2.5.3",
54
+ "mini-css-extract-plugin": "2.6.0",
51
55
  "moment-locales-webpack-plugin": "1.2.0",
52
56
  "mri": "1.2.0",
53
- "postcss": "8.4.6",
57
+ "postcss": "8.4.12",
54
58
  "postcss-custom-media": "8.0.0",
55
59
  "postcss-custom-properties": "12.1.4",
56
- "postcss-import": "14.0.2",
60
+ "postcss-import": "14.1.0",
57
61
  "postcss-loader": "6.2.1",
58
62
  "postcss-reporter": "7.0.5",
63
+ "prettier": "2.6.1",
64
+ "prompts": "^2.4.2",
59
65
  "querystring-es3": "^0.2.1",
66
+ "rcfile": "1.0.3",
60
67
  "react-dev-utils": "12.0.0",
61
- "react-refresh": "0.11.0",
68
+ "react-refresh": "0.12.0",
62
69
  "serve-handler": "6.1.3",
63
70
  "shelljs": "0.8.5",
64
71
  "style-loader": "3.3.1",
65
72
  "svg-url-loader": "7.1.1",
66
- "terser-webpack-plugin": "5.3.0",
73
+ "terser-webpack-plugin": "5.3.1",
67
74
  "thread-loader": "3.0.4",
68
75
  "url": "^0.11.0",
69
- "webpack": "5.66.0",
76
+ "webpack": "5.70.0",
70
77
  "webpack-bundle-analyzer": "4.5.0",
71
- "webpack-dev-server": "4.7.3",
78
+ "webpack-dev-server": "4.7.4",
72
79
  "webpackbar": "5.0.2"
73
80
  },
74
81
  "devDependencies": {
75
- "@babel/plugin-transform-runtime": "^7.16.10",
82
+ "@babel/plugin-transform-runtime": "^7.17.0",
76
83
  "@babel/preset-env": "^7.16.11",
84
+ "mock-fs": "^5.1.2",
85
+ "msw": "0.39.2",
77
86
  "rimraf": "3.0.2"
78
87
  },
79
88
  "engines": {