@contentstack/cli-cm-import 1.1.0 → 1.2.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.
@@ -1,266 +1,187 @@
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
- }
148
-
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);
169
- });
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
-
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);
192
- });
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 });
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
+ }
228
156
 
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);
157
+ async updateGlobalFields({ uid }) {
158
+ const globalField = find(this.globalFields, { uid });
159
+ supress(globalField.schema, this.importConfig.preserveStackVersion, this.installedExtensions);
160
+ let globalFieldObj = this.stackAPIClient.globalField(globalField);
161
+ Object.assign(globalFieldObj, cloneDeep(globalField));
162
+ try {
163
+ const globalFieldResponse = await globalFieldObj.update();
164
+ const existingGlobalField = findIndex(this.existingGlobalFields, (existingGlobalFieldUId) => {
165
+ return globalFieldResponse.uid === existingGlobalFieldUId;
166
+ });
241
167
 
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
- });
168
+ // Improve write the updated global fields once all updates are completed
169
+ this.existingGlobalFields.splice(existingGlobalField, 1, globalField);
170
+ await fileHelper.writeFile(this.globalFieldMapperFolderPath, this.existingGlobalFields).catch((error) => {
171
+ addlogs(this.importConfig, `failed to write updated the global field ${uid} ${formatError(error)}`);
253
172
  });
254
- });
255
- },
173
+ addlogs(this.importConfig, `Updated the global field ${uid} with content type references `);
174
+ return true;
175
+ } catch (error) {
176
+ addlogs(this.importConfig, `failed to update the global field ${uid} ${formatError(error)}`);
177
+ }
178
+ }
256
179
 
257
- mapUidToTitle: function (contentTypes) {
258
- let result = {};
259
- contentTypes.forEach((ct) => {
260
- result[ct.uid] = ct.title;
180
+ async mapUidToTitle() {
181
+ this.contentTypes.forEach((ct) => {
182
+ this.titleToUIdMap.set(ct.uid, ct.title);
261
183
  });
262
- return result;
263
184
  }
264
- };
185
+ }
265
186
 
266
- module.exports = new importContentTypes();
187
+ module.exports = ContentTypesImport;