@twilio-labs/serverless-api
Version:
API-wrapper for the Twilio Serverless API
556 lines (555 loc) • 25.5 kB
JavaScript
"use strict";
/** @module @twilio-labs/serverless-api */
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TwilioServerlessApiClient = exports.createGotClient = void 0;
const debug_1 = __importDefault(require("debug"));
const events_1 = __importDefault(require("events"));
const hpagent_1 = require("hpagent");
const p_limit_1 = __importDefault(require("p-limit"));
const util_1 = require("util");
const assets_1 = require("./api/assets");
const builds_1 = require("./api/builds");
const dependencies_1 = require("./api/dependencies");
const environments_1 = require("./api/environments");
const functions_1 = require("./api/functions");
const logs_1 = require("./api/logs");
const services_1 = require("./api/services");
const api_client_1 = require("./api/utils/api-client");
const http_config_1 = require("./api/utils/http_config");
const variables_1 = require("./api/variables");
const got_1 = __importDefault(require("./got"));
const logs_2 = require("./streams/logs");
const consts_1 = require("./types/consts");
const error_1 = require("./utils/error");
const fs_1 = require("./utils/fs");
const user_agent_1 = __importDefault(require("./utils/user-agent"));
const log = (0, debug_1.default)('twilio-serverless-api:client');
function createGotClient(config) {
let username, password;
if ('accountSid' in config) {
username = config.accountSid;
password = config.authToken;
(0, util_1.deprecate)(() => { }, '`accountSid` and `authToken` client config is deprecated, please use `username` and `password` instead.')();
}
else {
username = config.username;
password = config.password;
}
let client = got_1.default.extend({
prefixUrl: (0, api_client_1.getApiUrl)(config),
responseType: 'json',
username: username,
password: password,
headers: {
'User-Agent': (0, user_agent_1.default)(config.userAgentExtensions),
},
});
if (process.env.HTTP_PROXY) {
/*
* If environment variable HTTP_PROXY is set,
* add a proxy agent to the got client.
*/
client = client.extend({
agent: {
https: new hpagent_1.HttpsProxyAgent({
proxy: process.env.HTTP_PROXY,
}),
},
});
}
return client;
}
exports.createGotClient = createGotClient;
class TwilioServerlessApiClient extends events_1.default.EventEmitter {
constructor(config) {
debug_1.default.enable(process.env.DEBUG || '');
super();
this.config = config;
this.client = createGotClient(config);
this.limit = (0, p_limit_1.default)(config.concurrency || http_config_1.CONCURRENCY);
}
/**
* Returns the internally used GotClient instance used to make API requests
* @returns {GotClient} A client instance with set-up credentials
*/
getClient() {
debug_1.default.enable(process.env.DEBUG || '');
return this.client;
}
/**
* Sets a set of environment variables for a given Twilio Serverless environment
* If append is false it will remove all existing environment variables.
*
* @param {SetEnvironmentVariablesConfig} config
* @returns {Promise<SetEnvironmentVariablesResult>}
* @memberof TwilioServerlessApiClient
*/
async setEnvironmentVariables(config) {
let serviceSid = config.serviceSid;
if (typeof serviceSid === 'undefined' &&
typeof config.serviceName !== 'undefined') {
serviceSid = await (0, services_1.findServiceSid)(config.serviceName, this);
}
if (typeof serviceSid === 'undefined') {
throw new Error('Missing service SID argument');
}
let environmentSid;
if (!(0, environments_1.isEnvironmentSid)(config.environment)) {
const environmentResource = await (0, environments_1.getEnvironmentFromSuffix)(config.environment, serviceSid, this);
environmentSid = environmentResource.sid;
}
else {
environmentSid = config.environment;
}
const removeRedundantVariables = !config.append;
await (0, variables_1.setEnvironmentVariables)(config.env, environmentSid, serviceSid, this, removeRedundantVariables);
return { serviceSid, environmentSid };
}
/**
* Retrieves a list of environment variables for a given Twilio Serverless environment.
* If config.getValues is false (default) the values will be all set to undefined.
*
* @param {GetEnvironmentVariablesConfig} config
* @returns {Promise<GetEnvironmentVariablesResult>}
* @memberof TwilioServerlessApiClient
*/
async getEnvironmentVariables(config) {
let serviceSid = config.serviceSid;
if (typeof serviceSid === 'undefined' &&
typeof config.serviceName !== 'undefined') {
serviceSid = await (0, services_1.findServiceSid)(config.serviceName, this);
}
if (typeof serviceSid === 'undefined') {
throw new Error('Missing service SID argument');
}
let environmentSid;
if (!(0, environments_1.isEnvironmentSid)(config.environment)) {
const environmentResource = await (0, environments_1.getEnvironmentFromSuffix)(config.environment, serviceSid, this);
environmentSid = environmentResource.sid;
}
else {
environmentSid = config.environment;
}
const result = await (0, variables_1.listVariablesForEnvironment)(environmentSid, serviceSid, this);
let variables = result.map((resource) => {
return {
key: resource.key,
value: config.getValues ? resource.value : undefined,
};
});
if (config.keys.length > 0) {
variables = variables.filter((entry) => {
return config.keys.includes(entry.key);
});
}
return { serviceSid, environmentSid, variables };
}
/**
* Deletes a list of environment variables (by key) for a given Twilio Serverless environment.
*
* @param {RemoveEnvironmentVariablesConfig} config
* @returns {Promise<RemoveEnvironmentVariablesResult>}
* @memberof TwilioServerlessApiClient
*/
async removeEnvironmentVariables(config) {
let serviceSid = config.serviceSid;
if (typeof serviceSid === 'undefined' &&
typeof config.serviceName !== 'undefined') {
serviceSid = await (0, services_1.findServiceSid)(config.serviceName, this);
}
if (typeof serviceSid === 'undefined') {
throw new Error('Missing service SID argument');
}
let environmentSid;
if (!(0, environments_1.isEnvironmentSid)(config.environment)) {
const environmentResource = await (0, environments_1.getEnvironmentFromSuffix)(config.environment, serviceSid, this);
environmentSid = environmentResource.sid;
}
else {
environmentSid = config.environment;
}
await (0, variables_1.removeEnvironmentVariables)(config.keys, environmentSid, serviceSid, this);
return { serviceSid, environmentSid };
}
/**
* Returns an object containing lists of services, environments, variables
* functions or assets, depending on which have beeen requested in `listConfig`
* @param {ListConfig} listConfig Specifies info around which things should be listed
* @returns Promise<ListResult> Object containing the different lists.
*/
async list(listConfig) {
try {
let { types, serviceSid, serviceName: serviceName, environment: environmentSid, } = listConfig;
if (types === 'services' ||
(types.length === 1 && types[0] === 'services')) {
const services = await (0, services_1.listServices)(this);
return { services };
}
if (typeof serviceSid === 'undefined' &&
typeof serviceName !== 'undefined') {
serviceSid = await (0, services_1.findServiceSid)(serviceName, this);
}
if (typeof serviceSid === 'undefined') {
throw new Error('Missing service SID argument');
}
const result = {};
let currentBuildSidForEnv;
let currentBuild;
for (const type of types) {
try {
if (type === 'environments') {
result.environments = await (0, environments_1.listEnvironments)(serviceSid, this);
}
if (type === 'builds') {
result.builds = await (0, builds_1.listBuilds)(serviceSid, this);
}
if (typeof environmentSid === 'string') {
if (!(0, environments_1.isEnvironmentSid)(environmentSid)) {
const environment = await (0, environments_1.getEnvironmentFromSuffix)(environmentSid, serviceSid, this);
environmentSid = environment.sid;
currentBuildSidForEnv = environment.build_sid;
}
else if (!currentBuildSidForEnv) {
const environment = await (0, environments_1.getEnvironment)(environmentSid, serviceSid, this);
currentBuildSidForEnv = environment.build_sid;
}
if (type === 'functions' || type === 'assets') {
if (!currentBuild) {
currentBuild = await (0, builds_1.getBuild)(currentBuildSidForEnv, serviceSid, this);
}
if (type === 'functions') {
result.functions = {
environmentSid,
entries: currentBuild.function_versions,
};
}
else if (type === 'assets') {
result.assets = {
environmentSid,
entries: currentBuild.asset_versions,
};
}
}
if (type === 'variables') {
result.variables = {
entries: await (0, variables_1.listVariablesForEnvironment)(environmentSid, serviceSid, this),
environmentSid,
};
}
}
}
catch (err) {
log(new error_1.ClientApiError(err));
}
}
return result;
}
catch (err) {
(0, error_1.convertApiErrorsAndThrow)(err);
}
}
async getLogsStream(logsConfig) {
try {
let { serviceSid, environment, filterByFunction } = logsConfig;
if (!(0, environments_1.isEnvironmentSid)(environment)) {
const environmentResource = await (0, environments_1.getEnvironmentFromSuffix)(environment, serviceSid, this);
environment = environmentResource.sid;
}
if (filterByFunction && !(0, functions_1.isFunctionSid)(filterByFunction)) {
const availableFunctions = await (0, functions_1.listFunctionResources)(serviceSid, this);
const foundFunction = availableFunctions.find((fn) => fn.friendly_name === filterByFunction);
if (!foundFunction) {
throw new Error('Invalid Function Name or SID');
}
filterByFunction = foundFunction.sid;
}
const logsStream = new logs_2.LogsStream(environment, serviceSid, this, logsConfig);
return logsStream;
}
catch (err) {
(0, error_1.convertApiErrorsAndThrow)(err);
}
}
async getLogs(logsConfig) {
try {
let { serviceSid, environment, filterByFunction } = logsConfig;
if (!(0, environments_1.isEnvironmentSid)(environment)) {
const environmentResource = await (0, environments_1.getEnvironmentFromSuffix)(environment, serviceSid, this);
environment = environmentResource.sid;
}
if (filterByFunction && !(0, functions_1.isFunctionSid)(filterByFunction)) {
const availableFunctions = await (0, functions_1.listFunctionResources)(serviceSid, this);
const foundFunction = availableFunctions.find((fn) => fn.friendly_name === filterByFunction);
if (!foundFunction) {
throw new Error('Invalid Function Name or SID');
}
filterByFunction = foundFunction.sid;
}
return (0, logs_1.listOnePageLogResources)(environment, serviceSid, this, {
pageSize: 50,
functionSid: filterByFunction,
});
}
catch (err) {
(0, error_1.convertApiErrorsAndThrow)(err);
}
}
/**
* "Activates" a build by taking a specified build SID or a "source environment"
* and activating the same build in the specified `environment`.
*
* Can optionally create the new environment when called with `activateConfig.createEnvironment`
* @param {ActivateConfig} activateConfig Config to specify which build to activate in which environment
* @returns Promise<ActivateResult> Object containing meta information around deployment
*/
async activateBuild(activateConfig) {
try {
let { buildSid, targetEnvironment, serviceSid, sourceEnvironment, env } = activateConfig;
if (!buildSid && !sourceEnvironment) {
const error = new Error('You need to specify either a build SID or source environment to activate');
error.name = 'activate-missing-source';
throw error;
}
if (!(0, environments_1.isEnvironmentSid)(targetEnvironment)) {
try {
const environment = await (0, environments_1.getEnvironmentFromSuffix)(targetEnvironment, serviceSid, this);
targetEnvironment = environment.sid;
}
catch (err) {
if (activateConfig.force || activateConfig.createEnvironment) {
const environment = await (0, environments_1.createEnvironmentFromSuffix)(targetEnvironment, serviceSid, this);
targetEnvironment = environment.sid;
}
else {
throw err;
}
}
}
if (!buildSid && sourceEnvironment) {
let currentEnv;
if (!(0, environments_1.isEnvironmentSid)(sourceEnvironment)) {
currentEnv = await (0, environments_1.getEnvironmentFromSuffix)(sourceEnvironment, serviceSid, this);
}
else {
currentEnv = await (0, environments_1.getEnvironment)(sourceEnvironment, serviceSid, this);
}
buildSid = currentEnv.build_sid;
}
if (!buildSid) {
throw new Error('Could not determine build SID');
}
this.emit('status-update', {
status: consts_1.DeployStatus.SETTING_VARIABLES,
message: 'Setting environment variables',
});
await (0, variables_1.setEnvironmentVariables)(env, targetEnvironment, serviceSid, this);
const { domain_name } = await (0, environments_1.getEnvironment)(targetEnvironment, serviceSid, this);
await (0, builds_1.activateBuild)(buildSid, targetEnvironment, serviceSid, this);
return {
serviceSid,
buildSid,
environmentSid: targetEnvironment,
domain: domain_name,
};
}
catch (err) {
(0, error_1.convertApiErrorsAndThrow)(err);
}
}
/**
* Deploys a set of functions, assets, variables and dependencies specified
* in `deployConfig`. Functions & assets can either be paths to the local
* filesystem or `Buffer` instances allowing you to dynamically upload
* even without a file system.
*
* Unless a `deployConfig. serviceSid` is specified, it will try to create one. If a service
* with the name `deployConfig.serviceName` already exists, it will throw
* an error. You can make it use the existing service by setting `overrideExistingService` to
* true.
*
* Updates to the deployment will be emitted as events to `status-update`. Example:
*
* ```js
* client.on('status-update', ({ status, message }) => {
* console.log('[%s]: %s', status, message);
* })
* ```
* @param {DeployProjectConfig} deployConfig Config containing all details for deployment
* @returns Promise<DeployResult> Object containing meta information around deployment
*/
async deployProject(deployConfig) {
try {
const config = {
...this.config,
...deployConfig,
};
const { functions, assets, runtime } = config;
let serviceName = config.serviceName;
let serviceSid = config.serviceSid;
if (!serviceSid) {
this.emit('status-update', {
status: consts_1.DeployStatus.CREATING_SERVICE,
message: 'Creating Service',
});
try {
serviceSid = await (0, services_1.createService)(config.serviceName, this, config.uiEditable);
}
catch (err) {
const alternativeServiceSid = await (0, services_1.findServiceSid)(config.serviceName, this);
if (!alternativeServiceSid) {
throw err;
}
if (config.overrideExistingService || config.force) {
serviceSid = alternativeServiceSid;
}
else {
const error = new Error(`Service with name "${config.serviceName}" already exists with SID "${alternativeServiceSid}".`);
error.name = 'conflicting-servicename';
Object.defineProperty(error, 'serviceSid', {
value: alternativeServiceSid,
});
Object.defineProperty(error, 'serviceName', {
value: config.serviceName,
});
throw error;
}
}
}
else {
const serviceResource = await (0, services_1.getService)(serviceSid, this);
serviceName = serviceResource.unique_name;
}
this.emit('status-update', {
status: consts_1.DeployStatus.CONFIGURING_ENVIRONMENT,
message: `Configuring ${config.functionsEnv.length === 0 ? 'bare' : `"${config.functionsEnv}"`} environment`,
});
const environment = await (0, environments_1.createEnvironmentIfNotExists)(config.functionsEnv, serviceSid, this);
const { sid: environmentSid, domain_name: domain } = environment;
//
// Functions
//
this.emit('status-update', {
status: consts_1.DeployStatus.CREATING_FUNCTIONS,
message: `Creating ${functions.length} Functions`,
});
const functionResources = await (0, functions_1.getOrCreateFunctionResources)(functions, serviceSid, this);
this.emit('status-update', {
status: consts_1.DeployStatus.UPLOADING_FUNCTIONS,
message: `Uploading ${functions.length} Functions`,
});
const functionVersions = await Promise.all(functionResources.map((fn) => {
return (0, functions_1.uploadFunction)(fn, serviceSid, this, this.config);
}));
//
// Assets
//
this.emit('status-update', {
status: consts_1.DeployStatus.CREATING_ASSETS,
message: `Creating ${assets.length} Assets`,
});
const assetResources = await (0, assets_1.getOrCreateAssetResources)(assets, serviceSid, this);
this.emit('status-update', {
status: consts_1.DeployStatus.UPLOADING_ASSETS,
message: `Uploading ${assets.length} Assets`,
});
const assetVersions = await Promise.all(assetResources.map((asset) => {
return (0, assets_1.uploadAsset)(asset, serviceSid, this, this.config);
}));
this.emit('status-update', {
status: consts_1.DeployStatus.BUILDING,
message: 'Waiting for deployment.',
});
const dependencies = (0, dependencies_1.getDependencies)(config.pkgJson);
const build = await (0, builds_1.triggerBuild)({ functionVersions, dependencies, assetVersions, runtime }, serviceSid, this);
await (0, builds_1.waitForSuccessfulBuild)(build.sid, serviceSid, this, this);
this.emit('status-update', {
status: consts_1.DeployStatus.SETTING_VARIABLES,
message: 'Setting environment variables',
});
await (0, variables_1.setEnvironmentVariables)(config.env, environmentSid, serviceSid, this);
this.emit('status-update', {
status: consts_1.DeployStatus.ACTIVATING_DEPLOYMENT,
message: 'Activating deployment',
});
await (0, builds_1.activateBuild)(build.sid, environmentSid, serviceSid, this);
this.emit('status', {
status: consts_1.DeployStatus.DONE,
message: 'Project successfully deployed',
});
return {
serviceSid,
environmentSid,
buildSid: build.sid,
domain,
functionResources,
assetResources,
runtime: build.runtime,
serviceName,
};
}
catch (err) {
(0, error_1.convertApiErrorsAndThrow)(err);
}
}
/**
* Deploys a local project by reading existing functions and assets
* from `deployConfig.cwd` and calling `this.deployProject` with it.
*
* Functions have to be placed in a `functions` or `src` directory to be found.
* Assets have to be placed into an `assets` or `static` directory.
*
* Nested folder structures will result in nested routes.
* @param {DeployLocalProjectConfig} deployConfig
* @returns Promise<DeployResult> Object containing meta information around deployment
*/
async deployLocalProject(deployConfig) {
try {
this.emit('status-update', {
status: consts_1.DeployStatus.READING_FILESYSTEM,
message: 'Gathering Functions and Assets to deploy',
});
log('Deploy config %P', deployConfig);
const searchConfig = {};
if (deployConfig.functionsFolderName) {
searchConfig.functionsFolderNames = [deployConfig.functionsFolderName];
}
if (deployConfig.assetsFolderName) {
searchConfig.assetsFolderNames = [deployConfig.assetsFolderName];
}
let { functions, assets } = await (0, fs_1.getListOfFunctionsAndAssets)(deployConfig.cwd, searchConfig);
if (deployConfig.noFunctions) {
log('Disabling functions upload by emptying functions array');
functions = [];
}
if (deployConfig.noAssets) {
log('Disabling assets upload by emptying assets array');
assets = [];
}
const config = {
...this.config,
...deployConfig,
functions,
assets,
};
return this.deployProject(config);
}
catch (err) {
(0, error_1.convertApiErrorsAndThrow)(err);
}
}
// general implementation
async request(method, path, options = {}) {
options.retry = {
limit: this.config.retryLimit || http_config_1.RETRY_LIMIT,
methods: ['GET', 'POST', 'DELETE'],
statusCodes: [429],
errorCodes: [],
};
return this.limit(() => this.client[method](path, options));
}
}
exports.TwilioServerlessApiClient = TwilioServerlessApiClient;
exports.default = TwilioServerlessApiClient;