UNPKG

19.4 kBJavaScriptView Raw
1/**
2 * Copyright (c) 2018, Kinvey, Inc. All rights reserved.
3 *
4 * This software is licensed to you under the Kinvey terms of service located at
5 * http://www.kinvey.com/terms-of-use. By downloading, accessing and/or using this
6 * software, you hereby accept such terms of service (and any agreement referenced
7 * therein) and agree that you have read, understand and agree to be bound by such
8 * terms of service and are of legal age to agree to such terms with Kinvey.
9 *
10 * This software contains valuable confidential and proprietary information of
11 * KINVEY, INC and is subject to applicable licensing agreements.
12 * Unauthorized reproduction, transmission or distribution of this file and its
13 * contents is a violation of applicable laws.
14 */
15
16const fs = require('fs');
17const os = require('os');
18const path = require('path');
19
20const AESjs = require('aes-js');
21const async = require('async');
22const chalk = require('chalk');
23const validUrl = require('valid-url');
24const isemail = require('isemail');
25const uuidV4 = require('uuid-v4');
26const isempty = require('lodash.isempty');
27const lodashGet = require('lodash.get');
28const lodashCloneDeep = require('lodash.clonedeep');
29const lodashPick = require('lodash.pick');
30const lodashOmitBy = require('lodash.omitby');
31const lodashSortBy = require('lodash.sortby');
32const lodashPickBy = require('lodash.pickby');
33const logger = require('./logger.js');
34const moment = require('moment');
35const { ActiveItemTypes, DomainTypes, Errors, PromptMessages } = require('./Constants');
36const KinveyError = require('./KinveyError');
37
38const Utils = {};
39Utils.formatHost = function formatHost(host) {
40 let validHost = validUrl.isHttpUri(host);
41 if (validHost) {
42 if (validHost.slice(-1) !== '/') {
43 validHost = `${validHost}/`;
44 }
45 return validHost;
46 }
47
48 validHost = validUrl.isHttpsUri(host);
49 if (validHost) {
50 if (validHost.slice(-1) !== '/') {
51 validHost = `${validHost}/`;
52 }
53 return validHost;
54 }
55
56 return `https://${host}-manage.kinvey.com/`;
57};
58
59Utils.formatList = function formatList(list, name = 'name') {
60 const result = list.map(el => ({
61 name: el[name],
62 value: el
63 }));
64 result.sort((x, y) => {
65 if (x[name].toLowerCase() < y[name].toLowerCase()) return -1;
66 return 1;
67 });
68 return result;
69};
70
71Utils.formatHostList = function formatHostList(list) {
72 const result = list.map(el => ({
73 name: el,
74 value: el
75 }));
76 result.unshift({
77 name: 'all hosts',
78 value: null
79 });
80 return result;
81};
82
83Utils.readFile = function readFile(file, cb) {
84 logger.debug('Reading contents from file %s', chalk.cyan(file));
85 return fs.readFile(file, cb);
86};
87
88Utils.readJSON = function readJSON(file, cb) {
89 logger.debug('Reading JSON from file %s', chalk.cyan(file));
90 return async.waterfall([
91 next => Utils.readFile(file, next),
92 (data, next) => {
93 let json = null;
94 try {
95 logger.debug('Parsing JSON from file: %s', chalk.cyan(file));
96 json = JSON.parse(data);
97 } catch (error) {
98 logger.warn('Invalid JSON in file %s', chalk.cyan(file));
99 return next(error);
100 }
101 return next(null, json);
102 }
103 ], cb);
104};
105
106/**
107 * Reads a file synchronously and returns the data in JSON. Exceptions are not caught.
108 * @param {string} filePath
109 */
110Utils.readJSONSync = function readJSONSync(filePath) {
111 const rawData = fs.readFileSync(filePath);
112 const jsonData = JSON.parse(rawData);
113 return jsonData;
114};
115
116Utils.writeFile = function writeFile(file, contents, cb) {
117 logger.debug('Writing contents to file %s', chalk.cyan(file));
118 return fs.writeFile(file, contents, cb);
119};
120
121Utils.writeJSON = function writeJSON({ file, data, createParentDir, space }, done) {
122 async.series([
123 (next) => {
124 if (!createParentDir) {
125 return setImmediate(next);
126 }
127
128 Utils.mkdirp(file, next);
129 },
130 (next) => {
131 const contents = JSON.stringify(data, null, space);
132 Utils.writeFile(file, contents, next);
133 }
134 ], done);
135};
136
137Utils.writeConfigFile = function writeConfigFile(filePath, data, done) {
138 Utils.writeJSON({ file: filePath, data, createParentDir: true, space: 4 }, (err) => {
139 done(err, data);
140 });
141};
142
143Utils.isValidMFAToken = function isValidMFAToken(value) {
144 return (/^\d{6}$/.test(value));
145};
146
147Utils.isValidEmail = function isValidEmail(value) {
148 return !Utils.isNullOrUndefined(value) && isemail(value);
149};
150
151Utils.isNullOrUndefined = function isNullOrUndefined(value) {
152 return value == null;
153};
154
155Utils.isEmpty = function isEmpty(value) {
156 return isempty(value);
157};
158
159Utils.isValidTimestamp = function isValidTimestamp(ts) {
160 return moment(ts, moment.ISO_8601, true).isValid();
161};
162
163Utils.isValidNonZeroInteger = function isValidNonZeroInteger(number) {
164 if (number === 0 || number === '0') return false;
165 return /^\d+$/.test(number);
166};
167
168Utils.validateEmail = function validateEmail(value) {
169 if (Utils.isValidEmail(value)) {
170 return true;
171 }
172
173 return PromptMessages.INVALID_EMAIL_ADDRESS;
174};
175
176Utils.isStringWhitespace = function isStringWhitespace(value) {
177 const trimmedValue = value.trim();
178 return trimmedValue.length < 1;
179};
180
181Utils.askForValue = function askForValue(value) {
182 if (Utils.isNullOrUndefined(value)) {
183 return true;
184 }
185
186 return false;
187};
188
189Utils.validateString = function validateString(value) {
190 if (!Utils.isStringWhitespace(value)) {
191 return true;
192 }
193
194 return PromptMessages.INVALID_STRING;
195};
196
197Utils.validateMFAToken = function validateMFAToken(value) {
198 if (Utils.isValidMFAToken(value)) {
199 return true;
200 }
201
202 return PromptMessages.INVALID_MFA_TOKEN;
203};
204
205Utils.Endpoints = {
206 getIdPartFromId: id => (Utils.isNullOrUndefined(id) ? '' : `/${id}`),
207 version: schemaVersion => `v${schemaVersion}`,
208 session: () => 'session',
209 apps: (schemaVersion, appId) => `${Utils.Endpoints.version(schemaVersion)}/apps${Utils.Endpoints.getIdPartFromId(appId)}`,
210 envs: (schemaVersion, envId) => `${Utils.Endpoints.version(schemaVersion)}/environments${Utils.Endpoints.getIdPartFromId(envId)}`,
211 push: (schemaVersion, envId) => `${Utils.Endpoints.envs(schemaVersion, envId)}/push`,
212 envsByAppId: (schemaVersion, appId) => `${Utils.Endpoints.apps(schemaVersion, appId)}/environments`,
213 collections: (schemaVersion, envId, collName) => `${Utils.Endpoints.envs(schemaVersion, envId)}/collections${Utils.Endpoints.getIdPartFromId(collName)}`,
214 commonCode: (schemaVersion, envId, name) => `${Utils.Endpoints.envs(schemaVersion, envId)}/business-logic/common${Utils.Endpoints.getIdPartFromId(name)}`,
215 endpoints: (schemaVersion, envId, name) => `${Utils.Endpoints.envs(schemaVersion, envId)}/business-logic/endpoints${Utils.Endpoints.getIdPartFromId(name)}`,
216 hooks: (schemaVersion, envId, collName, hookName) => `${Utils.Endpoints.envs(schemaVersion, envId)}/business-logic/collections/${collName}${Utils.Endpoints.getIdPartFromId(hookName)}`,
217 collectionHooks: (schemaVersion, envId, collectionName) => {
218 const namePart = collectionName ? `/${collectionName}` : '';
219 return `${Utils.Endpoints.envs(schemaVersion, envId)}/business-logic/collections${namePart}`;
220 },
221 orgs: (schemaVersion, orgId) => `${Utils.Endpoints.version(schemaVersion)}/organizations${Utils.Endpoints.getIdPartFromId(orgId)}`,
222 appsByOrg: (schemaVersion, orgId) => `${Utils.Endpoints.orgs(schemaVersion, orgId)}/apps`,
223 jobs: (schemaVersion, id) => `${Utils.Endpoints.version(schemaVersion)}/jobs${Utils.Endpoints.getIdPartFromId(id)}`,
224 services: (schemaVersion, serviceId) => `${Utils.Endpoints.version(schemaVersion)}/services${Utils.Endpoints.getIdPartFromId(serviceId)}`,
225 serviceEnvs: (schemaVersion, serviceId, svcEnvId) => `${Utils.Endpoints.services(schemaVersion, serviceId)}/environments${Utils.Endpoints.getIdPartFromId(svcEnvId)}`,
226 serviceStatus: (schemaVersion, serviceId, svcEnvId) => `${Utils.Endpoints.serviceEnvs(schemaVersion, serviceId, svcEnvId)}/status`,
227 serviceLogs: (schemaVersion, serviceId, svcEnvId) => `${Utils.Endpoints.serviceEnvs(schemaVersion, serviceId, svcEnvId)}/logs`,
228 sites: (schemaVersion, siteId) => `${Utils.Endpoints.version(schemaVersion)}/sites${Utils.Endpoints.getIdPartFromId(siteId)}`,
229 siteEnvs: (schemaVersion, siteId, siteEnvId) => `${Utils.Endpoints.sites(schemaVersion, siteId)}/environments${Utils.Endpoints.getIdPartFromId(siteEnvId)}`,
230 siteDeploy: (schemaVersion, siteId, siteEnvId) => `${Utils.Endpoints.siteEnvs(schemaVersion, siteId, siteEnvId)}/files`,
231 sitePublish: (schemaVersion, siteId) => `${Utils.Endpoints.sites(schemaVersion, siteId)}/publish`,
232 siteUnpublish: (schemaVersion, siteId) => `${Utils.Endpoints.sites(schemaVersion, siteId)}/unpublish`,
233 siteStatus: (schemaVersion, siteId) => `${Utils.Endpoints.sites(schemaVersion, siteId)}/status`
234};
235
236Utils.getCommandNameFromOptions = function getCommandNameFromOptions(options) {
237 let name;
238 if (options._ && options._.length) {
239 name = options._.join(' ');
240 }
241
242 return name;
243};
244
245Utils.getErrorFromRequestError = function getErrorFromRequestError(err) {
246 let errResult;
247 const errCode = err.code;
248 if (errCode === 'ENOTFOUND') {
249 errResult = new KinveyError(Errors.InvalidConfigUrl);
250 } else if (errCode === 'ETIMEDOUT') {
251 errResult = new KinveyError(Errors.RequestTimedOut);
252 } else if (errCode === 'ECONNRESET') {
253 errResult = new KinveyError(Errors.ConnectionReset);
254 } else if (errCode === 'ECONNREFUSED') {
255 errResult = new KinveyError(errCode, `Connection refused at ${err.address}`);
256 } else {
257 errResult = err;
258 }
259
260 return errResult;
261};
262
263Utils.isMFATokenError = function isMFATokenError(err) {
264 return err && err.name === 'InvalidTwoFactorAuth';
265};
266
267Utils.findAndSortInternalServices = function findAndSortInternalServices(services) {
268 const result = services.filter(el => el.type === 'internal');
269 result.sort((x, y) => {
270 if (x.name.toLowerCase() < y.name.toLowerCase()) {
271 return -1;
272 }
273
274 return 1;
275 });
276
277 return result;
278};
279
280Utils.isArtifact = function isArtifact(artifacts, base, filepath) {
281 const relative = path.normalize(path.relative(base, filepath));
282 for (let i = 0; i < artifacts.length; i += 1) {
283 const pattern = artifacts[i];
284 if (relative.indexOf(pattern) === 0 || (`${relative}/`) === pattern) return true;
285 }
286 return false;
287};
288
289Utils.getDeviceInformation = function getDeviceInformation(cliVersion) {
290 const info = `kinvey-cli/${cliVersion} ${os.platform()} ${os.release()}`;
291 return info;
292};
293
294Utils.mkdirp = function mkdirp(pathToFile, done) {
295 try {
296 const filepath = path.resolve(pathToFile);
297 filepath.split(path.sep).slice().reduce((prev, curr, i) => {
298 if (prev && !fs.existsSync(prev)) {
299 fs.mkdirSync(prev);
300 }
301 return prev + path.sep + curr;
302 });
303 done();
304 } catch (err) {
305 done(err);
306 }
307};
308
309/**
310 * Asserts whether a value is of UUID v4 format. Values without dashes are accepted, too.
311 * Returns false if value is null or undefined.
312 * @param {String} value
313 * @returns {*}
314 */
315Utils.isUUID = function isUUID(value) {
316 if (Utils.isNullOrUndefined(value)) {
317 return false;
318 }
319
320 const dash = '-';
321 const lengthWithDashes = 36;
322 const lengthValue = value.length;
323 const couldBeRegularUUUID = value.includes(dash) && lengthValue === lengthWithDashes;
324 if (couldBeRegularUUUID) {
325 return uuidV4.isUUID(value);
326 }
327
328 const lengthWithoutDashes = 32;
329 const couldBeUUIDWithoutDashes = lengthValue === lengthWithoutDashes;
330 if (!couldBeUUIDWithoutDashes) {
331 return false;
332 }
333
334 const valueWithDashes = `${value.slice(0, 8)}${dash}${value.slice(8, 12)}${dash}${value.slice(12, 16)}${dash}${value.slice(16, 20)}${dash}${value.slice(20)}`;
335 return uuidV4.isUUID(valueWithDashes);
336};
337
338Utils.isEnvID = function isEnvID(value) {
339 if (Utils.isNullOrUndefined(value)) {
340 return false;
341 }
342
343 // kid_SklZwh7dN
344 const lengthEnvId = 13;
345 const isId = value.length === lengthEnvId && value.startsWith('kid_');
346 return isId;
347};
348
349Utils.validateActiveItemType = function validateActiveItemType(itemType) {
350 if (!ActiveItemTypes.includes(itemType)) {
351 throw new KinveyError(`Invalid item type: ${itemType}.`);
352 }
353};
354
355/**
356 * Returns error for ItemNotSpecified.
357 * @param {Constants.EntityType} entityType
358 * @returns {KinveyError}
359 */
360Utils.getItemError = function getItemError(entityType) {
361 const msg = `No ${entityType} identifier is specified and active ${entityType} is not set.`;
362 return new KinveyError(Errors.ItemNotSpecified.NAME, msg);
363};
364
365Utils.getConfigTypeError = function getConfigTypeError(type) {
366 const msg = `Unrecognized config type: ${type}`;
367 return new KinveyError('ValidationError', msg);
368};
369
370Utils.isEntityError = function isEntityError(err) {
371 if (Utils.isNullOrUndefined(err)) {
372 return false;
373 }
374
375 return err.name === Errors.NoEntityFound.NAME || err.name === Errors.TooManyEntitiesFound.NAME;
376};
377
378/**
379 * Returns error for generic entity errors with custom message.
380 * @param {Constants.EntityType} entityType
381 * @param {String} identifier
382 * @param {String} errName Error name. Must be either 'NotFound' or 'TooManyFound'.
383 * @returns {KinveyError}
384 */
385Utils.getCustomEntityError = function getCustomEntityError(entityType, identifier, errName) {
386 if (errName === Errors.NoEntityFound.NAME) {
387 return Utils.getCustomNotFoundError(entityType, identifier);
388 }
389
390 if (errName === Errors.TooManyEntitiesFound.NAME) {
391 const lastPart = identifier ? ` with identifier '${identifier}'.` : '.';
392 const msg = `Found too many ${entityType}s${lastPart}`;
393 return new KinveyError(Errors.TooManyEntitiesFound.NAME, msg);
394 }
395
396 return new KinveyError('UnknownError', `Failed to construct proper error for error name: ${errName}`);
397};
398
399Utils.getCustomNotFoundError = function getCustomNotFoundError(entityType, identifier) {
400 const lastPart = identifier ? ` with identifier '${identifier}'.` : '.';
401 const msg = `Could not find ${entityType}${lastPart}`;
402 return new KinveyError(Errors.NoEntityFound.NAME, msg);
403};
404
405/**
406 * Returns original error or an error with custom message (if initial error is a generic entity error).
407 * @param {Error} err
408 * @param {Constants.EntityType} entityType
409 * @param {String} identifier
410 * @returns {Error}
411 */
412Utils.getTransformedError = function getTransformedError(err, entityType, identifier) {
413 if (!Utils.isEntityError(err)) {
414 return err;
415 }
416
417 return Utils.getCustomEntityError(entityType, identifier, err.name);
418};
419
420Utils.getValueFromObject = function getValueFromObject(obj, path, defaultValue = 'Not set') {
421 return lodashGet(obj, path, defaultValue);
422};
423
424Utils.mapFromSource = function mapFromSource(mapping, originalSource) {
425 const mappingKeys = Object.keys(mapping);
426 const sourceIsArr = Array.isArray(originalSource);
427 let source;
428 if (!sourceIsArr) {
429 source = [originalSource];
430 } else {
431 source = originalSource;
432 }
433
434 const result = source.map((item) => {
435 const targetItem = {};
436
437 mappingKeys.forEach((k) => {
438 let targetKeyValue;
439 const valueOfMappingKey = mapping[k];
440 if (typeof valueOfMappingKey === 'function') {
441 targetKeyValue = valueOfMappingKey(item);
442 } else {
443 targetKeyValue = Utils.getValueFromObject(item, mapping[k]);
444 }
445
446 targetItem[k] = targetKeyValue;
447 });
448
449 return targetItem;
450 });
451
452 return sourceIsArr ? result : result[0];
453};
454
455Utils.getObjectByOmitting = function getObjectByOmitting(source, propsToOmit) {
456 const result = {};
457 const keys = Object.keys(source);
458 keys.forEach((k) => {
459 if (!propsToOmit.includes(k)) {
460 result[k] = lodashCloneDeep(source[k]);
461 }
462 });
463
464 return result;
465};
466
467Utils.getObjectByPicking = function getObjectByPicking(source, propsToPick) {
468 return lodashPick(source, propsToPick);
469};
470
471/**
472 * Returns an object from a string of key-value pairs where key1[secondaryDelimiter]value[primaryDelimiter] (e.g key1=value1,key2=value2).
473 * Throws an error if pattern is not valid.
474 * @param {String} dsv Delimiter separated key-values.
475 * @param {String} primaryDelimiter Delimiter that separates key-value pairs.
476 * @param {String} secondaryDelimiter Delimiter that separates keys from values.
477 * @returns {Object}
478 */
479Utils.getObjectFromDelimiterSeparatedKeyValuePairs = function getObjectFromDelimiterSeparatedKeyValuePairs(dsv, primaryDelimiter = ',', secondaryDelimiter = '=') {
480 if (typeof dsv !== 'string') {
481 throw new Error('Expected string.');
482 }
483
484 const listOfKeyValuePairs = dsv.split(primaryDelimiter);
485 if (Utils.isEmpty(listOfKeyValuePairs)) {
486 throw new Error('Empty list.');
487 }
488
489 const result = {};
490 listOfKeyValuePairs.reduce((accumulator, pair) => {
491 const delimiterIndex = pair.indexOf(secondaryDelimiter);
492 if (delimiterIndex < 0) {
493 throw new Error(`No delimiter ('${secondaryDelimiter}') in '${pair}'.`);
494 }
495
496 const key = pair.substring(0, delimiterIndex);
497 const value = pair.substring(delimiterIndex + 1, pair.length);
498 if (key.length < 1 || value.length < 1) {
499 throw new Error(`Invalid key-value pair: ${pair}`);
500 }
501
502 accumulator[key] = value;
503 return accumulator;
504 }, result);
505
506 return result;
507};
508
509/**
510 * Sorts a list of objects.
511 * @param {Array} list
512 * @param {String} [property] Property to sort by. Defaults to 'name'.
513 * @returns {Array.<Object>}
514 */
515Utils.sortList = function sortList(list, property = 'name') {
516 return lodashSortBy(list, x => x[property].toLowerCase());
517};
518
519Utils.pickBy = function pickBy(source, predicate) {
520 return lodashPickBy(source, predicate);
521};
522
523/**
524 * Returns a message that contains info about the entity in use.
525 * @param {Constants.EntityType} entityType
526 * @param identifier
527 * @returns {string}
528 */
529Utils.getUsingEntityMsg = function getUsingEntityMsg(entityType, identifier) {
530 return `Using ${entityType}: ${identifier}`;
531};
532
533/**
534 * Returns secret key. 256-bit key derived from 58 digit key.
535 * @returns {String}
536 */
537Utils.generateSecretKey = function generateSecretKey() {
538 let ret = '';
539 const length = 58;
540 while (ret.length < length) {
541 ret += Math.random().toString(16).substring(2);
542 }
543
544 const text = ret.substring(0, length);
545
546 const key256 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
547 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28,
548 29, 30, 31];
549 const textBytes = AESjs.utils.utf8.toBytes(text);
550
551 // The counter is optional, and if omitted will begin at 1
552 const aesCtr = new AESjs.ModeOfOperation.ctr(key256, new AESjs.Counter(5)); // eslint-disable-line
553 const encryptedBytes = aesCtr.encrypt(textBytes);
554 const result = AESjs.utils.hex.fromBytes(encryptedBytes);
555 return result;
556};
557
558/**
559 * Removes keys from object that have value of null or undefined. Returns new object.
560 * @param {Object} obj
561 * @returns {Object}
562 */
563Utils.stripNullOrUndefined = function stripNullOrUndefined(obj) {
564 return lodashOmitBy(obj, Utils.isNullOrUndefined);
565};
566
567module.exports = Utils;