UNPKG

ask-cli

Version:

Alexa Skills Kit (ASK) Command Line Interfaces

334 lines (313 loc) 14.6 kB
const R = require('ramda'); const fs = require('fs-extra'); const path = require('path'); const retryUtils = require('@src/utils/retry-utility'); const ResourcesConfig = require('@src/model/resources-config'); const SmapiClient = require('@src/clients/smapi-client/index.js'); const httpClient = require('@src/clients/http-client'); const Manifest = require('@src/model/manifest'); const Messenger = require('@src/view/messenger'); const jsonView = require('@src/view/json-view'); const stringUtils = require('@src/utils/string-utils'); const zipUtils = require('@src/utils/zip-utils'); const hashUtils = require('@src/utils/hash-utils'); const CONSTANTS = require('@src/utils/constants'); const CLiError = require('@src/exceptions/cli-error'); module.exports = class SkillMetadataController { /** * Constructor for SkillMetadataController * @param {Object} configuration { profile, doDebug } */ constructor(configuration) { const { profile, doDebug } = configuration; this.smapiClient = new SmapiClient({ profile, doDebug }); this.profile = profile; this.doDebug = doDebug; } /** * Entry method for all the skill package deployment logic * @param {String} vendorId * @param {Function} callback (error) */ deploySkillPackage(vendorId, ignoreHash, callback) { // 1.get valid skillMetada src path const skillPackageSrc = ResourcesConfig.getInstance().getSkillMetaSrc(this.profile); if (!stringUtils.isNonBlankString(skillPackageSrc)) { return callback('Skill package src is not found in ask-resources.json.'); } if (!fs.existsSync(skillPackageSrc)) { return callback(`File ${skillPackageSrc} does not exist.`); } // 2.compare hashcode between current and previous status to decide if necessary to upload hashUtils.getHash(skillPackageSrc, (hashErr, currentHash) => { if (hashErr) { return callback(hashErr); } const lastDeployHash = ResourcesConfig.getInstance().getSkillMetaLastDeployHash(this.profile); if (!ignoreHash && stringUtils.isNonBlankString(lastDeployHash) && lastDeployHash === currentHash) { return callback('The hash of current skill package folder does not change compared to the last deploy hash result, ' + 'CLI will skip the deploy of skill package.'); } // 3.call smapiClient to create/upload skillPackage const skillId = ResourcesConfig.getInstance().getSkillId(this.profile); this.putSkillPackage(skillId, skillId ? null : vendorId, (putErr, currentSkillId) => { if (putErr) { return callback(putErr); } ResourcesConfig.getInstance().setSkillId(this.profile, currentSkillId); ResourcesConfig.getInstance().setSkillMetaLastDeployHash(this.profile, currentHash); callback(); }); }); } /** * Validates domain info */ validateDomain() { const domainInfo = Manifest.getInstance().getApis(); if (!domainInfo || R.isEmpty(domainInfo)) { throw new CLiError('[Error]: Skill information is not valid. Please make sure "apis" field in the skill.json is not empty.'); } const domainList = R.keys(domainInfo); if (domainList.length !== 1) { throw new CLiError('[Warn]: Skill with multiple api domains cannot be enabled. Skip the enable process.'); } if (CONSTANTS.SKILL.DOMAIN.CAN_ENABLE_DOMAIN_LIST.indexOf(domainList[0]) === -1) { throw new CLiError(`[Warn]: Skill api domain "${domainList[0]}" cannot be enabled. Skip the enable process.`); } } /** * Function used to enable skill. It calls smapi getSkillEnablement function first to check if skill is already enabled, * if not, it will enable the skill by calling smapi enableSkill function. * @param {Function} callback (err, null) */ enableSkill(callback) { const skillId = ResourcesConfig.getInstance().getSkillId(this.profile); if (!stringUtils.isNonBlankString(skillId)) { return callback(`[Fatal]: Failed to find the skillId for profile [${this.profile}], please make sure the skill metadata deployment has succeeded with result of a valid skillId.`); } this.smapiClient.skill.getSkillEnablement(skillId, CONSTANTS.SKILL.STAGE.DEVELOPMENT, (err, response) => { if (err) { return callback(err); } if (response.statusCode === CONSTANTS.HTTP_REQUEST.STATUS_CODE.NOT_FOUND) { this.smapiClient.skill.enableSkill(skillId, CONSTANTS.SKILL.STAGE.DEVELOPMENT, (enableErr, enableResponse) => { if (enableErr) { return callback(enableErr); } if (enableResponse.statusCode >= 300) { return callback(jsonView.toString(enableResponse.body)); } Messenger.getInstance().info('Skill is enabled successfully.'); callback(); }); } else if (response.statusCode >= 300) { callback(jsonView.toString(response.body)); } else { Messenger.getInstance().info('Skill is already enabled, skip the enable process.'); callback(); } }); } /** * Put skill package based on the input of skillId and vendorId: * when vendorId is set but skillId is not, create skill package; * when skillId is set but vendorId is not, update skill package. * * @param {String} skillId * @param {String} vendorId * @param {Function} callback (error, skillId) */ putSkillPackage(skillId, vendorId, callback) { // 1.zip and upload skill package const skillPackageSrc = ResourcesConfig.getInstance().getSkillMetaSrc(this.profile); this.uploadSkillPackage(skillPackageSrc, (uploadErr, uploadResult) => { if (uploadErr) { return callback(uploadErr); } // 2.import skill package with upload URL this._importPackage(skillId, vendorId, uploadResult.uploadUrl, (importErr, importResponse) => { if (importErr) { return callback(importErr); } const importId = path.basename(importResponse.headers.location); // 3.poll for the skill package import status this._pollImportStatus(importId, (pollErr, pollResponse) => { if (pollErr) { return callback(pollErr); } if (pollResponse.body.status !== CONSTANTS.SKILL.PACKAGE_STATUS.SUCCEEDED) { callback(jsonView.toString(pollResponse.body)); } else { callback(null, pollResponse.body.skill.skillId); } }); }); }); } /** * Download the skill package by exporting the skill package and then download it into the skill project * @param {String} rootFolder Folder path for the skill project root * @param {String} skillId * @param {String stage * @param {Function} callback */ getSkillPackage(rootFolder, skillId, stage, callback) { // 1.request to export skill package this._exportPackage(skillId, stage, (exportErr, exportResponse) => { if (exportErr) { return callback(exportErr); } const exportId = path.basename(R.view(R.lensPath(['headers', 'location']), exportResponse)); // 2.poll for the skill package export status this._pollExportStatus(exportId, (pollErr, pollResponse) => { if (pollErr) { return callback(pollErr); } // TODO: check the error when statusCode is not 200 or check the body strucutre when status is not SUCCEEDED or check non skill case // 3.download skill package into local file system const skillPackageLocation = R.view(R.lensPath(['body', 'skill', 'location']), pollResponse); const targetPath = path.join(rootFolder, 'skill-package'); zipUtils.unzipRemoteZipFile(skillPackageLocation, targetPath, false, (unzipErr) => { callback(unzipErr); }); }); }); } /** * Upload skill package by zipping, creating upload URL, and then upload * @param {String} skillPackageSrc * @param {Function} callback (err, { uploadUrl, expiresAt }) */ uploadSkillPackage(skillPackageSrc, callback) { // 1.create upload URL for CLI to upload this._createUploadUrl((createUploadErr, createUploadResult) => { if (createUploadErr) { return callback(createUploadErr); } // 2.zip skill package zipUtils.createTempZip(skillPackageSrc, (zipErr, zipFilePath) => { if (zipErr) { return callback(zipErr); } // 3.upload zip file const uploadPayload = fs.readFileSync(zipFilePath); const operation = 'upload-skill-package'; httpClient.putByUrl(createUploadResult.uploadUrl, uploadPayload, operation, this.doDebug, (uploadErr, uploadResponse) => { fs.removeSync(zipFilePath); if (uploadErr) { return callback(uploadErr); } if (uploadResponse.statusCode >= 300) { return callback('[Error]: Upload of skill package failed. Please try again with --debug to see more details.'); } callback(null, createUploadResult); }); }); }); } /** * Wrapper for smapi createUpload function * @param {Function} callback (err, { uploadUrl, expiresAt }) */ _createUploadUrl(callback) { this.smapiClient.skillPackage.createUpload((createErr, createResponse) => { if (createErr) { return callback(createErr); } if (createResponse.statusCode >= 300) { return callback(jsonView.toString(createResponse.body)); } callback(null, { uploadUrl: createResponse.body.uploadUrl, expiresAt: createResponse.body.expiresAt }); }); } /** * Wrapper for smapi importPackage function. The response contains importId in its headers' location url. * @param {String} skillId * @param {String} vendorId * @param {String} location * @param {Function} callback (err, importResponse) */ _importPackage(skillId, vendorId, location, callback) { this.smapiClient.skillPackage.importPackage(skillId, vendorId, location, (importErr, importResponse) => { if (importErr) { return callback(importErr); } if (importResponse.statusCode >= 300) { return callback(jsonView.toString(importResponse.body)); } callback(null, importResponse); }); } /** * Wrapper for smapi exportPackage function * @param {String} skillId * @param {String} stage * @param {Function} callback */ _exportPackage(skillId, stage, callback) { this.smapiClient.skillPackage.exportPackage(skillId, stage, (exportErr, exportResponse) => { if (exportErr) { return callback(exportErr); } if (exportResponse.statusCode >= 300) { return callback(jsonView.toString(exportResponse.body)); } callback(null, exportResponse); }); } /** * Wrapper for polling smapi skill package import status. * @param {String} importId * @param {Function} callback (err, lastImportStatus) */ _pollImportStatus(importId, callback) { const retryConfig = { base: CONSTANTS.CONFIGURATION.RETRY.GET_PACKAGE_IMPORT_STATUS.MIN_TIME_OUT, factor: CONSTANTS.CONFIGURATION.RETRY.GET_PACKAGE_IMPORT_STATUS.FACTOR, maxRetry: CONSTANTS.CONFIGURATION.RETRY.GET_PACKAGE_IMPORT_STATUS.MAX_RETRY }; const retryCall = (loopCallback) => { this.smapiClient.skillPackage.getImportStatus(importId, (pollErr, pollResponse) => { if (pollErr) { return loopCallback(pollErr); } if (pollResponse.statusCode >= 300) { return loopCallback(jsonView.toString(pollResponse.body)); } loopCallback(null, pollResponse); }); }; const shouldRetryCondition = retryResponse => retryResponse.body.status === CONSTANTS.SKILL.PACKAGE_STATUS.IN_PROGRESS; retryUtils.retry(retryConfig, retryCall, shouldRetryCondition, (err, res) => callback(err, err ? null : res)); } /** * Wrapper for polling smapi skill package export status. * @param {String} exportId * @param {Function} callback (err, lastExportStatus) */ _pollExportStatus(exportId, callback) { const retryConfig = { base: CONSTANTS.CONFIGURATION.RETRY.GET_PACKAGE_EXPORT_STATUS.MIN_TIME_OUT, factor: CONSTANTS.CONFIGURATION.RETRY.GET_PACKAGE_EXPORT_STATUS.FACTOR, maxRetry: CONSTANTS.CONFIGURATION.RETRY.GET_PACKAGE_EXPORT_STATUS.MAX_RETRY }; const retryCall = (loopCallback) => { this.smapiClient.skillPackage.getExportStatus(exportId, (pollErr, pollResponse) => { if (pollErr) { return loopCallback(pollErr); } if (pollResponse.statusCode >= 300) { return loopCallback(jsonView.toString(pollResponse.body)); } loopCallback(null, pollResponse); }); }; const shouldRetryCondition = retryResponse => retryResponse.body.status === CONSTANTS.SKILL.PACKAGE_STATUS.IN_PROGRESS; retryUtils.retry(retryConfig, retryCall, shouldRetryCondition, (err, res) => callback(err, err ? null : res)); } };