module twilio-labs import debug from 'debug' ; import events from 'events' ; import { HTTPAlias , Options , OptionsOfJSONResponseBody , OptionsOfTextResponseBody , Response , } from 'got/dist/source' ; import { HttpsProxyAgent } from 'hpagent' ; import pLimit , { Limit } from 'p-limit' ; import { deprecate } from 'util' ; import { getOrCreateAssetResources , uploadAsset } from './api/assets' ; import { activateBuild , getBuild , listBuilds , triggerBuild , waitForSuccessfulBuild , } from './api/builds' ; import { getDependencies } from './api/dependencies' ; import { createEnvironmentFromSuffix , createEnvironmentIfNotExists , getEnvironment , getEnvironmentFromSuffix , isEnvironmentSid , listEnvironments , } from './api/environments' ; import { getOrCreateFunctionResources , isFunctionSid , listFunctionResources , uploadFunction , } from './api/functions' ; import { listOnePageLogResources } from './api/logs' ; import { createService , findServiceSid , getService , listServices , } from './api/services' ; import { getApiUrl } from './api/utils/api-client' ; import { CONCURRENCY , RETRY_LIMIT } from './api/utils/http_config' ; import { listVariablesForEnvironment , removeEnvironmentVariables , setEnvironmentVariables , } from './api/variables' ; import got from './got' ; import { LogsStream } from './streams/logs' ; import { ActivateConfig , ActivateResult , BuildResource , ClientConfig , DeployLocalProjectConfig , DeployProjectConfig , DeployResult , GotClient , ListConfig , ListResult , LogApiResource , LogsConfig , Sid , } from './types' ; import { DeployStatus } from './types/consts' ; import { GetEnvironmentVariablesConfig , GetEnvironmentVariablesResult , KeyValue , RemoveEnvironmentVariablesConfig , RemoveEnvironmentVariablesResult , SetEnvironmentVariablesConfig , SetEnvironmentVariablesResult , } from './types/env' ; import { ClientApiError , convertApiErrorsAndThrow } from './utils/error' ; import { getListOfFunctionsAndAssets , SearchConfig } from './utils/fs' ; import getUserAgent from './utils/user-agent' ; const log = debug ( 'twilio-serverless-api:client' ) ; export function createGotClient ( config ) { let username , password ; if ( 'accountSid' in config ) { username = config . accountSid ; password = config . authToken ; deprecate ( ( ) => { } , '`accountSid` and `authToken` client config is deprecated, please use `username` and `password` instead.' ) ( ) ; } else { username = config . username ; password = config . password ; } let client = got . extend ( { prefixUrl : getApiUrl ( config ) , responseType : 'json' , username : username , password : password , headers : { 'User-Agent' : getUserAgent ( config . userAgentExtensions ) , } , } ) as GotClient ; if ( process . env . HTTP_PROXY ) { client = client . extend ( { agent : { https : new HttpsProxyAgent ( { proxy : process . env . HTTP_PROXY , } ) , } , } ) ; } return client ; } export class TwilioServerlessApiClient extends events . EventEmitter { private type { ClientConfig } memberof private config : ClientConfig ; private type { GotClient } memberof private client : GotClient ; private type { Limit } memberof private limit : Limit ; constructor ( config ) { debug . enable ( process . env . DEBUG || '' ) ; super ( ) ; this . config = config ; this . client = createGotClient ( config ) ; this . limit = pLimit ( config . concurrency || CONCURRENCY ) ; } returns { GotClient } getClient ( ) { debug . enable ( process . env . DEBUG || '' ) ; return this . client ; } param { SetEnvironmentVariablesConfig } config returns { Promise < SetEnvironmentVariablesResult > } memberof async setEnvironmentVariables ( config ) { let serviceSid = config . serviceSid ; if ( typeof serviceSid === 'undefined' && typeof config . serviceName !== 'undefined' ) { serviceSid = await findServiceSid ( config . serviceName , this ) ; } if ( typeof serviceSid === 'undefined' ) { throw new Error ( 'Missing service SID argument' ) ; } let environmentSid ; if ( ! isEnvironmentSid ( config . environment ) ) { const environmentResource = await getEnvironmentFromSuffix ( config . environment , serviceSid , this ) ; environmentSid = environmentResource . sid ; } else { environmentSid = config . environment ; } const removeRedundantVariables = ! config . append ; await setEnvironmentVariables ( config . env , environmentSid , serviceSid , this , removeRedundantVariables ) ; return { serviceSid , environmentSid } ; } param { GetEnvironmentVariablesConfig } config returns { Promise < GetEnvironmentVariablesResult > } memberof async getEnvironmentVariables ( config ) { let serviceSid = config . serviceSid ; if ( typeof serviceSid === 'undefined' && typeof config . serviceName !== 'undefined' ) { serviceSid = await findServiceSid ( config . serviceName , this ) ; } if ( typeof serviceSid === 'undefined' ) { throw new Error ( 'Missing service SID argument' ) ; } let environmentSid ; if ( ! isEnvironmentSid ( config . environment ) ) { const environmentResource = await getEnvironmentFromSuffix ( config . environment , serviceSid , this ) ; environmentSid = environmentResource . sid ; } else { environmentSid = config . environment ; } const result = await 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 } ; } param { RemoveEnvironmentVariablesConfig } config returns { Promise < RemoveEnvironmentVariablesResult > } memberof async removeEnvironmentVariables ( config ) { let serviceSid = config . serviceSid ; if ( typeof serviceSid === 'undefined' && typeof config . serviceName !== 'undefined' ) { serviceSid = await findServiceSid ( config . serviceName , this ) ; } if ( typeof serviceSid === 'undefined' ) { throw new Error ( 'Missing service SID argument' ) ; } let environmentSid ; if ( ! isEnvironmentSid ( config . environment ) ) { const environmentResource = await getEnvironmentFromSuffix ( config . environment , serviceSid , this ) ; environmentSid = environmentResource . sid ; } else { environmentSid = config . environment ; } await removeEnvironmentVariables ( config . keys , environmentSid , serviceSid , this ) ; return { serviceSid , environmentSid } ; } param { ListConfig } listConfig returns async list ( listConfig ) { try { let { types , serviceSid , serviceName : serviceName , environment : environmentSid , } = listConfig ; if ( types === 'services' || ( types . length === 1 && types [ 0 ] === 'services' ) ) { const services = await listServices ( this ) ; return { services } ; } if ( typeof serviceSid === 'undefined' && typeof serviceName !== 'undefined' ) { serviceSid = await 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 listEnvironments ( serviceSid , this ) ; } if ( type === 'builds' ) { result . builds = await listBuilds ( serviceSid , this ) ; } if ( typeof environmentSid === 'string' ) { if ( ! isEnvironmentSid ( environmentSid ) ) { const environment = await getEnvironmentFromSuffix ( environmentSid , serviceSid , this ) ; environmentSid = environment . sid ; currentBuildSidForEnv = environment . build_sid ; } else if ( ! currentBuildSidForEnv ) { const environment = await getEnvironment ( environmentSid , serviceSid , this ) ; currentBuildSidForEnv = environment . build_sid ; } if ( type === 'functions' || type === 'assets' ) { if ( ! currentBuild ) { currentBuild = await 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 listVariablesForEnvironment ( environmentSid , serviceSid , this ) , environmentSid , } ; } } } catch ( err ) { log ( new ClientApiError ( err ) ) ; } } return result ; } catch ( err ) { convertApiErrorsAndThrow ( err ) ; } } async getLogsStream ( logsConfig ) { try { let { serviceSid , environment , filterByFunction } = logsConfig ; if ( ! isEnvironmentSid ( environment ) ) { const environmentResource = await getEnvironmentFromSuffix ( environment , serviceSid , this ) ; environment = environmentResource . sid ; } if ( filterByFunction && ! isFunctionSid ( filterByFunction ) ) { const availableFunctions = await 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 LogsStream ( environment , serviceSid , this , logsConfig ) ; return logsStream ; } catch ( err ) { convertApiErrorsAndThrow ( err ) ; } } async getLogs ( logsConfig ) { try { let { serviceSid , environment , filterByFunction } = logsConfig ; if ( ! isEnvironmentSid ( environment ) ) { const environmentResource = await getEnvironmentFromSuffix ( environment , serviceSid , this ) ; environment = environmentResource . sid ; } if ( filterByFunction && ! isFunctionSid ( filterByFunction ) ) { const availableFunctions = await 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 listOnePageLogResources ( environment , serviceSid , this , { pageSize : 50 , functionSid : filterByFunction , } ) ; } catch ( err ) { convertApiErrorsAndThrow ( err ) ; } } param { ActivateConfig } activateConfig returns 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 ( ! isEnvironmentSid ( targetEnvironment ) ) { try { const environment = await getEnvironmentFromSuffix ( targetEnvironment , serviceSid , this ) ; targetEnvironment = environment . sid ; } catch ( err ) { if ( activateConfig . force || activateConfig . createEnvironment ) { const environment = await createEnvironmentFromSuffix ( targetEnvironment , serviceSid , this ) ; targetEnvironment = environment . sid ; } else { throw err ; } } } if ( ! buildSid && sourceEnvironment ) { let currentEnv ; if ( ! isEnvironmentSid ( sourceEnvironment ) ) { currentEnv = await getEnvironmentFromSuffix ( sourceEnvironment , serviceSid , this ) ; } else { currentEnv = await getEnvironment ( sourceEnvironment , serviceSid , this ) ; } buildSid = currentEnv . build_sid ; } if ( ! buildSid ) { throw new Error ( 'Could not determine build SID' ) ; } this . emit ( 'status-update' , { status : DeployStatus . SETTING_VARIABLES , message : 'Setting environment variables' , } ) ; await setEnvironmentVariables ( env , targetEnvironment , serviceSid , this ) ; const { domain_name } = await getEnvironment ( targetEnvironment , serviceSid , this ) ; await activateBuild ( buildSid , targetEnvironment , serviceSid , this ) ; return { serviceSid , buildSid , environmentSid : targetEnvironment , domain : domain_name , } ; } catch ( err ) { convertApiErrorsAndThrow ( err ) ; } } param { DeployProjectConfig } deployConfig returns 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 : DeployStatus . CREATING_SERVICE , message : 'Creating Service' , } ) ; try { serviceSid = await createService ( config . serviceName , this ) ; } catch ( err ) { const alternativeServiceSid = await 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 getService ( serviceSid , this ) ; serviceName = serviceResource . unique_name ; } this . emit ( 'status-update' , { status : DeployStatus . CONFIGURING_ENVIRONMENT , message : `Configuring ${ config . functionsEnv . length === 0 ? 'bare' : `"${ config . functionsEnv }"` } environment` , } ) ; const environment = await createEnvironmentIfNotExists ( config . functionsEnv , serviceSid , this ) ; const { sid : environmentSid , domain_name : domain } = environment ; this . emit ( 'status-update' , { status : DeployStatus . CREATING_FUNCTIONS , message : `Creating ${ functions . length } Functions` , } ) ; const functionResources = await getOrCreateFunctionResources ( functions , serviceSid , this ) ; this . emit ( 'status-update' , { status : DeployStatus . UPLOADING_FUNCTIONS , message : `Uploading ${ functions . length } Functions` , } ) ; const functionVersions = await Promise . all ( functionResources . map ( ( fn ) => { return uploadFunction ( fn , serviceSid as string , this , this . config ) ; } ) ) ; this . emit ( 'status-update' , { status : DeployStatus . CREATING_ASSETS , message : `Creating ${ assets . length } Assets` , } ) ; const assetResources = await getOrCreateAssetResources ( assets , serviceSid , this ) ; this . emit ( 'status-update' , { status : DeployStatus . UPLOADING_ASSETS , message : `Uploading ${ assets . length } Assets` , } ) ; const assetVersions = await Promise . all ( assetResources . map ( ( asset ) => { return uploadAsset ( asset , serviceSid as string , this , this . config ) ; } ) ) ; this . emit ( 'status-update' , { status : DeployStatus . BUILDING , message : 'Waiting for deployment.' , } ) ; const dependencies = getDependencies ( config . pkgJson ) ; const build = await triggerBuild ( { functionVersions , dependencies , assetVersions , runtime } , serviceSid , this ) ; await waitForSuccessfulBuild ( build . sid , serviceSid , this , this ) ; this . emit ( 'status-update' , { status : DeployStatus . SETTING_VARIABLES , message : 'Setting environment variables' , } ) ; await setEnvironmentVariables ( config . env , environmentSid , serviceSid , this ) ; this . emit ( 'status-update' , { status : DeployStatus . ACTIVATING_DEPLOYMENT , message : 'Activating deployment' , } ) ; await activateBuild ( build . sid , environmentSid , serviceSid , this ) ; this . emit ( 'status' , { status : DeployStatus . DONE , message : 'Project successfully deployed' , } ) ; return { serviceSid , environmentSid , buildSid : build . sid , domain , functionResources , assetResources , runtime : build . runtime , serviceName , } ; } catch ( err ) { convertApiErrorsAndThrow ( err ) ; } } param { DeployLocalProjectConfig } deployConfig returns async deployLocalProject ( deployConfig ) { try { this . emit ( 'status-update' , { status : 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 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 ) { convertApiErrorsAndThrow ( err ) ; } } async request ( method , path ) ; async request ( method , path , options ) ; async request ( method , path , options ) ; async request ( method , path , options = { } ) { options . retry = { limit : this . config . retryLimit || RETRY_LIMIT , methods : [ 'GET' , 'POST' , 'DELETE' ] , statusCodes : [ 429 ] , errorCodes : [ ] , } ; return this . limit ( ( ) => this . client [ method ] ( path , options ) ) ; } } export default TwilioServerlessApiClient ;