@contentstack/cli-cm-import 1.1.0 → 1.2.1

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.
@@ -1,266 +1,191 @@
1
- /* eslint-disable no-console */
2
- /*!
3
- * Contentstack Import
4
- * Copyright (c) 2019 Contentstack LLC
5
- * MIT Licensed
6
- */
7
- let mkdirp = require('mkdirp');
8
- let fs = require('fs');
9
- let fsPromises = require('fs').promises;
10
- let path = require('path');
11
- let _ = require('lodash');
12
- let Promise = require('bluebird');
13
- let chalk = require('chalk');
14
-
15
- let helper = require('../util/fs');
16
- let { addlogs } = require('../util/log');
17
- let config = require('../../config/default');
18
- let supress = require('../util/extensionsUidReplace');
19
- let sdkInstance = require('../util/contentstack-management-sdk');
20
- const { getInstalledExtensions } = require('../util/marketplace-app-helper')
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+ const fileHelper = require('../util/fs');
5
+ const config = require('../../config/default');
6
+ const supress = require('../util/extensionsUidReplace');
7
+ const { getInstalledExtensions } = require('../util/marketplace-app-helper');
8
+ const { executeTask, formatError } = require('../util');
9
+ const { addlogs } = require('../util/log');
10
+ const { cloneDeep, remove, isEmpty, find, findIndex } = require('lodash');
11
+ const schemaTemplate = require('../util/schemaTemplate');
12
+
13
+ class ContentTypesImport {
14
+ constructor(importConfig, stackAPIClient) {
15
+ this.stackAPIClient = stackAPIClient;
16
+ this.importConfig = importConfig;
17
+ this.contentTypeConfig = importConfig.modules.content_types;
18
+ this.globalFieldConfig = importConfig.modules.globalfields;
19
+ this.importConcurrency = this.contentTypeConfig.importConcurrency || this.importConfig.importConcurrency;
20
+ this.writeConcurrency = this.contentTypeConfig.writeConcurrency || this.importConfig.writeConcurrency;
21
+ this.contentTypesFolderPath = path.join(this.importConfig.data, this.contentTypeConfig.dirName);
22
+ this.mapperFolderPath = path.join(this.importConfig.data, 'mapper', 'content_types');
23
+ this.existingContentTypesPath = path.join(this.mapperFolderPath, 'success.json');
24
+ this.globalFieldsFolderPath = path.resolve(this.importConfig.data, this.globalFieldConfig.dirName);
25
+ this.globalFieldMapperFolderPath = path.join(importConfig.data, 'mapper', 'global_fields', 'success.json');
26
+ this.globalFieldPendingPath = path.join(importConfig.data, 'mapper', 'global_fields', 'pending_global_fields.js');
27
+ this.appMapperFolderPath = path.join(importConfig.data, 'mapper', 'marketplace_apps');
28
+ this.ignoredFilesInContentTypesFolder = new Map([
29
+ ['__master.json', 'true'],
30
+ ['__priority.json', 'true'],
31
+ ['schema.json', 'true'],
32
+ ['.DS_Store', 'true'],
33
+ ]);
34
+ this.contentTypes = [];
35
+ this.existingContentTypesUIds;
36
+ this.titleToUIdMap = new Map();
37
+ this.requestOptions = {
38
+ json: {},
39
+ };
40
+ this.fieldRules = [];
41
+ this.installedExtensions = [];
42
+ this.globalFields = [];
43
+ this.existingGlobalFields = [];
44
+ this.pendingGlobalFields = [];
45
+ }
21
46
 
22
- let reqConcurrency = config.concurrency;
23
- let requestLimit = config.rateLimit;
24
- let contentTypeConfig = config.modules.content_types;
25
- let globalFieldConfig = config.modules.globalfields;
26
- let globalFieldsFolderPath;
27
- let contentTypesFolderPath;
28
- let mapperFolderPath;
29
- let globalFieldMapperFolderPath;
30
- let globalFieldUpdateFile;
31
- let globalFieldPendingPath;
32
- let skipFiles = ['__master.json', '__priority.json', 'schema.json', '.DS_Store'];
33
- let fileNames;
34
- let field_rules_ct = [];
35
- let client;
36
- let stack = {};
47
+ async start() {
48
+ try {
49
+ // read content types
50
+ // remove content types already existing
51
+ if (fs.existsSync(this.existingContentTypesPath)) {
52
+ this.existingContentTypesUIds = fileHelper.readFileSync(this.existingContentTypesPath) || [];
53
+ this.existingContentTypesUIds = new Map(this.existingContentTypesUIds.map((id) => [id, 'true']));
54
+ }
37
55
 
38
- function importContentTypes() {
39
- this.contentTypes = [];
40
- this.schemaTemplate = require('../util/schemaTemplate');
41
- this.requestOptions = {
42
- json: {},
43
- };
44
- this.installedExtensions = []
45
- }
56
+ const contentTypeFiles = fileHelper.readdirSync(this.contentTypesFolderPath);
57
+ for (let contentTypeName of contentTypeFiles) {
58
+ if (!this.ignoredFilesInContentTypesFolder.has(contentTypeName)) {
59
+ const contentTypePath = path.join(this.contentTypesFolderPath, contentTypeName);
60
+ const contentType = await fileHelper.readFile(contentTypePath);
61
+ if (!this.existingContentTypesUIds?.has(contentType.uid)) {
62
+ this.contentTypes.push(await fileHelper.readFile(contentTypePath));
63
+ }
64
+ }
65
+ }
66
+ // this.mapUidToTitle(this.csontentTypes);
67
+
68
+ // seed content type
69
+ addlogs(this.importConfig, 'Started to seed content types', 'info');
70
+ await executeTask(this.contentTypes, this.seedContentType.bind(this), { concurrency: this.importConcurrency });
71
+ addlogs(this.importConfig, 'Created content types', 'success');
72
+
73
+ // update content type
74
+ this.installedExtensions =
75
+ fileHelper.readFileSync(path.join(this.appMapperFolderPath, 'marketplace-apps.json')) || {};
76
+ if (isEmpty(this.installedExtensions)) {
77
+ this.installedExtensions = await getInstalledExtensions(this.importConfig);
78
+ }
46
79
 
47
- importContentTypes.prototype = {
48
- start: async function (credentialConfig) {
49
- addlogs(config, 'Migrating contenttypes', 'success');
50
- let self = this;
51
- config = credentialConfig;
52
- client = sdkInstance.Client(config);
53
- stack = client.stack({ api_key: config.target_stack, management_token: config.management_token });
54
- globalFieldsFolderPath = path.resolve(config.data, globalFieldConfig.dirName);
55
- contentTypesFolderPath = path.resolve(config.data, contentTypeConfig.dirName);
56
- mapperFolderPath = path.join(config.data, 'mapper', 'content_types');
57
- const appMapperFolderPath = path.join(config.data, 'mapper', 'marketplace_apps');
58
- globalFieldMapperFolderPath = helper.readFile(path.join(config.data, 'mapper', 'global_fields', 'success.json'));
59
- globalFieldPendingPath = helper.readFile(
60
- path.join(config.data, 'mapper', 'global_fields', 'pending_global_fields.js'),
61
- );
62
- globalFieldUpdateFile = path.join(config.data, 'mapper', 'global_fields', 'success.json');
63
- fileNames = fs.readdirSync(path.join(contentTypesFolderPath));
64
- self.globalfields = helper.readFile(path.resolve(globalFieldsFolderPath, globalFieldConfig.fileName));
80
+ addlogs(this.importConfig, 'Started to update content types with references', 'info');
81
+ await executeTask(this.contentTypes, this.updateContentType.bind(this), { concurrency: this.importConcurrency });
82
+ addlogs(this.importConfig, 'Updated content types with references', 'success');
83
+
84
+ // global field update
85
+ this.pendingGlobalFields = fileHelper.readFileSync(this.globalFieldPendingPath);
86
+ if (Array.isArray(this.pendingGlobalFields) && this.pendingGlobalFields.length > 0) {
87
+ this.globalFields = fileHelper.readFileSync(
88
+ path.resolve(this.globalFieldsFolderPath, this.globalFieldConfig.fileName),
89
+ );
90
+ this.existingGlobalFields = fileHelper.readFileSync(this.globalFieldMapperFolderPath);
91
+ try {
92
+ addlogs(this.importConfig, 'Started to update pending global field with content type references', 'info');
93
+ await executeTask(this.pendingGlobalFields, this.updateGlobalFields.bind(this), {
94
+ concurrency: this.importConcurrency,
95
+ });
96
+ addlogs(this.importConfig, 'Updated pending global fields with content type with references', 'success');
97
+ } catch (error) {
98
+ addlogs(
99
+ this.importConfig,
100
+ `Failed to updates global fields with content type reference ${formatError(error)}`,
101
+ 'error',
102
+ );
103
+ }
104
+ }
65
105
 
66
- for (let index in fileNames) {
67
- if (skipFiles.indexOf(fileNames[index]) === -1) {
68
- self.contentTypes.push(helper.readFile(path.join(contentTypesFolderPath, fileNames[index])));
106
+ // write field rules
107
+ if (this.fieldRules.length > 0) {
108
+ try {
109
+ await fileHelper.writeFile(path.join(this.contentTypesFolderPath, 'field_rules_uid.json'), this.fieldRules);
110
+ } catch (error) {
111
+ addlogs(this.importConfig, `Failed to write field rules ${formatError(error)}`, 'success');
112
+ }
69
113
  }
70
- }
71
114
 
72
- self.contentTypeUids = _.map(self.contentTypes, 'uid').filter(val => val);
73
- self.createdContentTypeUids = [];
74
- if (!fs.existsSync(mapperFolderPath)) {
75
- mkdirp.sync(mapperFolderPath);
76
- }
77
- // avoid re-creating content types that already exists in the stack
78
- if (fs.existsSync(path.join(mapperFolderPath, 'success.json'))) {
79
- self.createdContentTypeUids = helper.readFile(path.join(mapperFolderPath, 'success.json')) || [];
115
+ addlogs(this.importConfig, chalk.green('Content types imported successfully'), 'success');
116
+ } catch (error) {
117
+ addlogs(this.importConfig, formatError(error), 'error');
118
+ throw new Error('Failed to import content types');
80
119
  }
120
+ }
81
121
 
82
- if (fs.existsSync(path.join(appMapperFolderPath, 'marketplace-apps.json'))) {
83
- self.installedExtensions = helper.readFile(path.join(appMapperFolderPath, 'marketplace-apps.json')) || {};
122
+ async seedContentType(contentType) {
123
+ const body = cloneDeep(schemaTemplate);
124
+ body.content_type.uid = contentType.uid;
125
+ body.content_type.title = contentType.title;
126
+ const requestObject = cloneDeep(this.requestOptions);
127
+ requestObject.json = body;
128
+
129
+ try {
130
+ await this.stackAPIClient.contentType().create(requestObject.json);
131
+ } catch (error) {
132
+ if (error.error_code === 115 && (error.errors.uid || error.errors.title)) {
133
+ // content type uid already exists
134
+ // _.remove(self.contentTypes, { uid: uid });
135
+ return true;
136
+ }
137
+ throw error;
84
138
  }
139
+ }
85
140
 
86
- if (_.isEmpty(self.installedExtensions)) {
87
- self.installedExtensions = await getInstalledExtensions(config)
141
+ async updateContentType(contentType) {
142
+ if (typeof contentType !== 'object') return;
143
+ const requestObject = cloneDeep(this.requestOptions);
144
+ if (contentType.field_rules) {
145
+ this.fieldRules.push(contentType.uid);
146
+ delete contentType.field_rules;
88
147
  }
89
148
 
90
- self.contentTypeUids = _.difference(self.contentTypeUids, self.createdContentTypeUids);
91
- self.uidToTitleMap = self.mapUidToTitle(self.contentTypes);
92
- // remove content types, already created
93
- _.remove(this.contentTypes, function (contentType) {
94
- return self.contentTypeUids.indexOf(contentType.uid) === -1;
95
- });
96
-
97
- return new Promise(function (resolve, reject) {
98
- if (self.contentTypes === undefined || _.isEmpty(self.contentTypes)) {
99
- addlogs(config, chalk.yellow('No Content types found'), 'success');
100
- return resolve({ empty: true })
101
- }
102
- return Promise.map(
103
- self.contentTypeUids,
104
- function (contentTypeUid) {
105
- return self
106
- .seedContentTypes(contentTypeUid, self.uidToTitleMap[contentTypeUid])
107
- .catch(reject);
108
- },
109
- {
110
- // seed 3 content types at a time
111
- concurrency: reqConcurrency,
112
- },
113
- )
114
- .then(function () {
115
- let batches = [];
116
- let lenObj = self.contentTypes;
117
- for (let i = 0; i < lenObj.length; i += Math.round(requestLimit / 3)) {
118
- batches.push(lenObj.slice(i, i + Math.round(requestLimit / 3)));
119
- }
120
-
121
- return Promise.map(
122
- batches,
123
- async function (batch) {
124
- return Promise.map(
125
- batch,
126
- async function (contentType) {
127
- await self.updateContentTypes(contentType)
128
- addlogs(config, contentType.uid + ' was updated successfully!', 'success');
129
- },
130
- {
131
- concurrency: reqConcurrency,
132
- },
133
- ).catch((e) => {
134
- console.log('Something went wrong while migrating content type batch', e);
135
- });
136
- },
137
- {
138
- concurrency: reqConcurrency
139
- }
140
- ).then(async function () {
141
- // eslint-disable-next-line quotes
142
- if (field_rules_ct.length > 0) {
143
- await fsPromises.writeFile(
144
- contentTypesFolderPath + '/field_rules_uid.json',
145
- JSON.stringify(field_rules_ct),
146
- );
147
- }
149
+ supress(contentType.schema, this.importConfig.preserveStackVersion, this.installedExtensions);
150
+ requestObject.json.content_type = contentType;
151
+ const contentTypeResponse = this.stackAPIClient.contentType(contentType.uid);
152
+ Object.assign(contentTypeResponse, cloneDeep(contentType));
153
+ await contentTypeResponse.update();
154
+ addlogs(this.importConfig, contentType.uid + ' updated with references', 'success');
155
+ }
148
156
 
149
- if (globalFieldPendingPath && globalFieldPendingPath.length !== 0) {
150
- return self
151
- .updateGlobalfields()
152
- .then(function () {
153
- addlogs(config, chalk.green('Content types have been imported successfully!'), 'success');
154
- return resolve();
155
- })
156
- .catch((_error) => {
157
- addlogs(config, chalk.green('Error in GlobalFields'), 'success');
158
- return reject();
159
- });
160
- }
161
- addlogs(config, chalk.green('Content types have been imported successfully!'), 'success');
162
- return resolve();
163
- }).catch((error) => {
164
- return reject(error);
165
- });
166
- })
167
- .catch((error) => {
168
- return reject(error);
157
+ async updateGlobalFields({ uid }) {
158
+ const globalField = find(this.globalFields, { uid });
159
+ if (globalField) {
160
+ supress(globalField.schema, this.importConfig.preserveStackVersion, this.installedExtensions);
161
+ let globalFieldObj = this.stackAPIClient.globalField(globalField);
162
+ Object.assign(globalFieldObj, cloneDeep(globalField));
163
+ try {
164
+ const globalFieldResponse = await globalFieldObj.update();
165
+ const existingGlobalField = findIndex(this.existingGlobalFields, (existingGlobalFieldUId) => {
166
+ return globalFieldResponse.uid === existingGlobalFieldUId;
169
167
  });
170
- });
171
- },
172
- seedContentTypes: function (uid, title) {
173
- let self = this;
174
- return new Promise(function (resolve, reject) {
175
- let body = _.cloneDeep(self.schemaTemplate);
176
- body.content_type.uid = uid;
177
- body.content_type.title = title;
178
- let requestObject = _.cloneDeep(self.requestOptions);
179
- requestObject.json = body;
180
168
 
181
- return stack
182
- .contentType()
183
- .create(requestObject.json)
184
- .then(resolve)
185
- .catch(function (err) {
186
- let error = JSON.parse(err.message);
187
- if (error.error_code === 115 && (error.errors.uid || error.errors.title)) {
188
- // content type uid already exists
189
- return resolve();
190
- }
191
- return reject(error);
169
+ // Improve write the updated global fields once all updates are completed
170
+ this.existingGlobalFields.splice(existingGlobalField, 1, globalField);
171
+ await fileHelper.writeFile(this.globalFieldMapperFolderPath, this.existingGlobalFields).catch((error) => {
172
+ addlogs(this.importConfig, `failed to write updated the global field ${uid} ${formatError(error)}`);
192
173
  });
193
- });
194
- },
195
- updateContentTypes: function (contentType) {
196
- let self = this;
197
- return new Promise(function (resolve, reject) {
198
- setTimeout(async function () {
199
- let requestObject = _.cloneDeep(self.requestOptions);
200
- if (contentType.field_rules) {
201
- field_rules_ct.push(contentType.uid);
202
- delete contentType.field_rules;
203
- }
204
-
205
- supress(contentType.schema, config.preserveStackVersion, self.installedExtensions);
206
- requestObject.json.content_type = contentType;
207
- let contentTypeResponse = stack.contentType(contentType.uid);
208
- Object.assign(contentTypeResponse, _.cloneDeep(contentType));
209
- contentTypeResponse
210
- .update()
211
- .then((_updatedcontentType) => {
212
- return resolve();
213
- })
214
- .catch((err) => {
215
- addlogs(config, err, 'error');
216
- return reject(err);
217
- });
218
- }, 1000);
219
- });
220
- },
221
-
222
- updateGlobalfields: function () {
223
- let self = this;
224
- return new Promise(function (resolve, reject) {
225
- // eslint-disable-next-line no-undef
226
- return Promise.map(globalFieldPendingPath, async function (globalfield) {
227
- let Obj = _.find(self.globalfields, { uid: globalfield });
228
-
229
- supress(Obj.schema, config.preserveStackVersion, self.installedExtensions);
230
- let globalFieldObj = stack.globalField(globalfield);
231
- Object.assign(globalFieldObj, _.cloneDeep(Obj));
232
- return globalFieldObj
233
- .update()
234
- .then((globalFieldResponse) => {
235
- let updateObjpos = _.findIndex(globalFieldMapperFolderPath, function (successobj) {
236
- let global_field_uid = globalFieldResponse.uid;
237
- return global_field_uid === successobj;
238
- });
239
- globalFieldMapperFolderPath.splice(updateObjpos, 1, Obj);
240
- helper.writeFile(globalFieldUpdateFile, globalFieldMapperFolderPath);
241
-
242
- resolve(globalFieldResponse)
243
- })
244
- .catch(function (err) {
245
- let error = JSON.parse(err.message);
246
- // eslint-disable-next-line no-console
247
- addlogs(config, chalk.red('Global Field failed to update ' + JSON.stringify(error.errors)), 'error');
248
- })
249
- .catch(function (error) {
250
- // failed to update modified schemas back to their original form
251
- return reject(error);
252
- });
253
- });
254
- });
255
- },
174
+ addlogs(this.importConfig, `Updated the global field ${uid} with content type references `);
175
+ return true;
176
+ } catch (error) {
177
+ addlogs(this.importConfig, `failed to update the global field ${uid} ${formatError(error)}`);
178
+ }
179
+ } else {
180
+ addlogs(this.importConfig, `Global field ${uid} does not exist, and hence failed to update.`);
181
+ }
182
+ }
256
183
 
257
- mapUidToTitle: function (contentTypes) {
258
- let result = {};
259
- contentTypes.forEach((ct) => {
260
- result[ct.uid] = ct.title;
184
+ async mapUidToTitle() {
185
+ this.contentTypes.forEach((ct) => {
186
+ this.titleToUIdMap.set(ct.uid, ct.title);
261
187
  });
262
- return result;
263
188
  }
264
- };
189
+ }
265
190
 
266
- module.exports = new importContentTypes();
191
+ module.exports = ContentTypesImport;