Skip to content

Commit 7d11a03

Browse files
authored
feat: add new env commands (#290)
1 parent 24d4319 commit 7d11a03

File tree

30 files changed

+1574
-6
lines changed

30 files changed

+1574
-6
lines changed

packages/plugin-serverless/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@
6969
},
7070
"serverless:deploy": {
7171
"description": "deploys your local serverless project"
72+
},
73+
"serverless:env": {
74+
"description": "retrieve and modify the environment variables for your deployment"
7275
}
7376
}
7477
},
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
const {
2+
convertYargsOptionsToOclifFlags,
3+
normalizeFlags,
4+
createExternalCliOptions,
5+
getRegionAndEdge,
6+
} = require('./utils');
7+
8+
const { TwilioClientCommand } = require('@twilio/cli-core').baseCommands;
9+
10+
function createTwilioRunCommand(name, path, inheritedFlags = []) {
11+
const { handler, cliInfo, describe } = require(path);
12+
const { flags, aliasMap } = convertYargsOptionsToOclifFlags(cliInfo.options);
13+
14+
const commandClass = class extends TwilioClientCommand {
15+
async run() {
16+
await super.run();
17+
18+
const flags = normalizeFlags(this.flags, aliasMap, process.argv);
19+
20+
const externalOptions = createExternalCliOptions(
21+
flags,
22+
this.twilioClient
23+
);
24+
25+
const { edge, region } = getRegionAndEdge(flags, this);
26+
flags.region = region;
27+
flags.edge = edge;
28+
29+
const opts = Object.assign({}, flags, this.args);
30+
31+
return handler(opts, externalOptions);
32+
}
33+
};
34+
35+
const inheritedFlagObject = inheritedFlags.reduce((current, flag) => {
36+
return {
37+
...current,
38+
[flag]: TwilioClientCommand.flags[flag],
39+
};
40+
}, {});
41+
42+
Object.defineProperty(commandClass, 'name', { value: name });
43+
commandClass.description = describe;
44+
commandClass.flags = Object.assign(flags, inheritedFlagObject);
45+
46+
return commandClass;
47+
}
48+
49+
module.exports = { createTwilioRunCommand };
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const { createTwilioRunCommand } = require('../../../TwilioRunCommand');
2+
3+
module.exports = createTwilioRunCommand(
4+
'EnvGet',
5+
'twilio-run/dist/commands/env/env-get',
6+
['profile']
7+
);
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const { createTwilioRunCommand } = require('../../../TwilioRunCommand');
2+
3+
module.exports = createTwilioRunCommand(
4+
'EnvImport',
5+
'twilio-run/dist/commands/env/env-import',
6+
['profile']
7+
);
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const { createTwilioRunCommand } = require('../../../TwilioRunCommand');
2+
3+
module.exports = createTwilioRunCommand(
4+
'EnvList',
5+
'twilio-run/dist/commands/env/env-list',
6+
['profile']
7+
);
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const { createTwilioRunCommand } = require('../../../TwilioRunCommand');
2+
3+
module.exports = createTwilioRunCommand(
4+
'EnvSet',
5+
'twilio-run/dist/commands/env/env-set',
6+
['profile']
7+
);
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const { createTwilioRunCommand } = require('../../../TwilioRunCommand');
2+
3+
module.exports = createTwilioRunCommand(
4+
'EnvUnset',
5+
'twilio-run/dist/commands/env/env-unset',
6+
['profile']
7+
);
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
const { createTwilioRunCommand } = require('../src/TwilioRunCommand');
2+
3+
jest.mock(
4+
'twilio-run/test-command',
5+
() => {
6+
return {
7+
handler: jest.fn(),
8+
describe: 'Some test description',
9+
cliInfo: {
10+
options: {
11+
region: {
12+
type: 'string',
13+
hidden: true,
14+
describe: 'Twilio API Region',
15+
},
16+
edge: {
17+
type: 'string',
18+
hidden: true,
19+
describe: 'Twilio API Region',
20+
},
21+
username: {
22+
type: 'string',
23+
alias: 'u',
24+
describe:
25+
'A specific API key or account SID to be used for deployment. Uses fields in .env otherwise',
26+
},
27+
password: {
28+
type: 'string',
29+
describe:
30+
'A specific API secret or auth token for deployment. Uses fields from .env otherwise',
31+
},
32+
'load-system-env': {
33+
default: false,
34+
type: 'boolean',
35+
describe:
36+
'Uses system environment variables as fallback for variables specified in your .env file. Needs to be used with --env explicitly specified.',
37+
},
38+
},
39+
},
40+
};
41+
},
42+
{ virtual: true }
43+
);
44+
45+
const command = require('twilio-run/test-command');
46+
const { convertYargsOptionsToOclifFlags } = require('../src/utils');
47+
const TwilioClientCommand = require('@twilio/cli-core/src/base-commands/twilio-client-command');
48+
49+
describe('createTwilioRunCommand', () => {
50+
test('should create a new class', () => {
51+
const ResultCommand = createTwilioRunCommand(
52+
'TestCommand',
53+
'twilio-run/test-command'
54+
);
55+
expect(ResultCommand.name).toBe('TestCommand');
56+
expect(ResultCommand.description).toBe(command.describe);
57+
expect(ResultCommand.flags.toString()).toEqual(
58+
convertYargsOptionsToOclifFlags(command.cliInfo.options).flags.toString()
59+
);
60+
});
61+
62+
test('should add base properties as defined', () => {
63+
const ResultCommand = createTwilioRunCommand(
64+
'TestCommand',
65+
'twilio-run/test-command',
66+
['profile']
67+
);
68+
expect(ResultCommand.name).toBe('TestCommand');
69+
expect(ResultCommand.description).toBe(command.describe);
70+
expect(ResultCommand.flags.profile.toString()).toEqual(
71+
TwilioClientCommand.flags.profile.toString()
72+
);
73+
});
74+
75+
// takes too long in some runs. We should find a faster way.
76+
// test('should call the handler', async () => {
77+
// const ResultCommand = createTwilioRunCommand(
78+
// 'TestCommand',
79+
// 'twilio-run/test-command'
80+
// );
81+
82+
// await ResultCommand.run(['--region', 'dev']);
83+
// expect(command.handler).toHaveBeenCalled();
84+
// });
85+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Sid } from '../../types';
2+
3+
const SidRegEx = /^[A-Z]{2}[a-f0-9]{32}$/;
4+
5+
export function isSid(value: unknown): value is Sid {
6+
if (typeof value !== 'string') {
7+
return false;
8+
}
9+
10+
if (value.length !== 34) {
11+
return false;
12+
}
13+
14+
return SidRegEx.test(value);
15+
}

packages/serverless-api/src/api/variables.ts

Lines changed: 93 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
/** @module @twilio-labs/serverless-api/dist/api */
22

33
import debug from 'debug';
4+
import { TwilioServerlessApiClient } from '../client';
45
import {
56
EnvironmentVariables,
7+
Sid,
68
Variable,
79
VariableList,
810
VariableResource,
911
} from '../types';
10-
import { TwilioServerlessApiClient } from '../client';
11-
import { getPaginatedResource } from './utils/pagination';
1212
import { ClientApiError } from '../utils/error';
13+
import { getPaginatedResource } from './utils/pagination';
14+
import { isSid } from './utils/type-checks';
1315

1416
const log = debug('twilio-serverless-api:variables');
1517

@@ -137,13 +139,15 @@ function convertToVariableArray(env: EnvironmentVariables): Variable[] {
137139
* @param {string} environmentSid the environment the varibales should be set for
138140
* @param {string} serviceSid the service the environment belongs to
139141
* @param {TwilioServerlessApiClient} client API client
142+
* @param {boolean} [removeRedundantOnes=false] whether to remove variables that are not passed but are currently set
140143
* @returns {Promise<void>}
141144
*/
142145
export async function setEnvironmentVariables(
143146
envVariables: EnvironmentVariables,
144147
environmentSid: string,
145148
serviceSid: string,
146-
client: TwilioServerlessApiClient
149+
client: TwilioServerlessApiClient,
150+
removeRedundantOnes: boolean = false
147151
): Promise<void> {
148152
const existingVariables = await listVariablesForEnvironment(
149153
environmentSid,
@@ -181,4 +185,90 @@ export async function setEnvironmentVariables(
181185
});
182186

183187
await Promise.all(variableResources);
188+
189+
if (removeRedundantOnes) {
190+
const removeVariablePromises = existingVariables.map(async (variable) => {
191+
if (typeof envVariables[variable.key] === 'undefined') {
192+
return deleteEnvironmentVariable(
193+
variable.sid,
194+
environmentSid,
195+
serviceSid,
196+
client
197+
);
198+
}
199+
});
200+
await Promise.all(removeVariablePromises);
201+
}
202+
}
203+
204+
/**
205+
* Deletes a given variable from a given environment
206+
*
207+
* @export
208+
* @param {string} variableSid the SID of the variable to delete
209+
* @param {string} environmentSid the environment the variable belongs to
210+
* @param {string} serviceSid the service the environment belongs to
211+
* @param {TwilioServerlessApiClient} client API client instance
212+
* @returns {Promise<boolean>}
213+
*/
214+
export async function deleteEnvironmentVariable(
215+
variableSid: string,
216+
environmentSid: string,
217+
serviceSid: string,
218+
client: TwilioServerlessApiClient
219+
): Promise<boolean> {
220+
try {
221+
const resp = await client.request(
222+
'delete',
223+
`Services/${serviceSid}/Environments/${environmentSid}/Variables/${variableSid}`
224+
);
225+
return true;
226+
} catch (err) {
227+
log('%O', new ClientApiError(err));
228+
throw err;
229+
}
230+
}
231+
232+
/**
233+
* Deletes all variables matching the passed keys from an environment
234+
*
235+
* @export
236+
* @param {string[]} keys the keys of the variables to delete
237+
* @param {string} environmentSid the environment the variables belong to
238+
* @param {string} serviceSid the service the environment belongs to
239+
* @param {TwilioServerlessApiClient} client API client instance
240+
* @returns {Promise<boolean>}
241+
*/
242+
export async function removeEnvironmentVariables(
243+
keys: string[],
244+
environmentSid: string,
245+
serviceSid: string,
246+
client: TwilioServerlessApiClient
247+
): Promise<boolean> {
248+
const existingVariables = await listVariablesForEnvironment(
249+
environmentSid,
250+
serviceSid,
251+
client
252+
);
253+
254+
const variableSidMap = new Map<string, Sid>();
255+
existingVariables.forEach((variableResource) => {
256+
variableSidMap.set(variableResource.key, variableResource.sid);
257+
});
258+
259+
const requests: Promise<boolean>[] = keys.map((key) => {
260+
const variableSid = variableSidMap.get(key);
261+
if (isSid(variableSid)) {
262+
return deleteEnvironmentVariable(
263+
variableSid,
264+
environmentSid,
265+
serviceSid,
266+
client
267+
);
268+
}
269+
return Promise.resolve(true);
270+
});
271+
272+
await Promise.all(requests);
273+
return true;
184274
}

0 commit comments

Comments
 (0)