Skip to content

Commit 9113f01

Browse files
stefanjudisdkundel
authored andcommitted
refactor(api): extract file system logic from API layer (#8)
**Breaking changes**: Meaning of `path` in serverless resources changed from filePath to URL path changed. Additionally, `name` configuration was added. fix #7
1 parent af2d876 commit 9113f01

File tree

6 files changed

+123
-93
lines changed

6 files changed

+123
-93
lines changed

packages/serverless-api/README.md

+25
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,31 @@ Unless a deployConfig. serviceSid is specified, it will try to create one. If a
9191

9292
Updates to the deployment will be emitted as events to status-update.
9393

94+
```js
95+
const result = await client.deployProject({
96+
env: {},
97+
pkgJson: {},
98+
serviceName: 'serverless-example',
99+
functionsEnv: 'dev',
100+
functions: [
101+
{
102+
name: 'hello-world',
103+
path: '/hello-world-path',
104+
content: await readFile(path.join(__dirname, 'some-dir', 'handler.js')),
105+
access: 'public',
106+
},
107+
],
108+
assets: [
109+
{
110+
name: 'image',
111+
path: '/foo/image.jpg',
112+
access: 'public',
113+
content: await readFile(path.join(__dirname, 'another-dir', 'image.jpg')),
114+
},
115+
]
116+
});
117+
```
118+
94119
[More in the Docs](https://serverless-api.twilio-labs.com/classes/_twilio_labs_serverless_api.twilioserverlessapiclient.html#deployproject)
95120

96121
### `client.getClient(): GotClient`

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

+12-24
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,12 @@ import {
88
AssetApiResource,
99
AssetList,
1010
AssetResource,
11-
FileInfo,
1211
GotClient,
13-
RawAssetWithPath,
12+
ServerlessResourceConfig,
1413
Sid,
1514
VersionResource,
1615
} from '../types';
1716
import { uploadToAws } from '../utils/aws-upload';
18-
import { getPathAndAccessFromFileInfo, readFile } from '../utils/fs';
1917

2018
const log = debug('twilio-serverless-api:assets');
2119

@@ -53,7 +51,10 @@ async function createAssetResource(
5351
* @param {GotClient} client API client
5452
* @returns {Promise<AssetApiResource[]>}
5553
*/
56-
export async function listAssetResources(serviceSid: string, client: GotClient) {
54+
export async function listAssetResources(
55+
serviceSid: string,
56+
client: GotClient
57+
) {
5758
try {
5859
const resp = await client.get(`/Services/${serviceSid}/Assets`);
5960
const content = (resp.body as unknown) as AssetList;
@@ -73,26 +74,23 @@ export async function listAssetResources(serviceSid: string, client: GotClient)
7374
* @returns {Promise<AssetResource[]>}
7475
*/
7576
export async function getOrCreateAssetResources(
76-
assets: FileInfo[],
77+
assets: ServerlessResourceConfig[],
7778
serviceSid: string,
7879
client: GotClient
7980
): Promise<AssetResource[]> {
8081
const output: AssetResource[] = [];
8182
const existingAssets = await listAssetResources(serviceSid, client);
82-
const assetsToCreate: RawAssetWithPath[] = [];
83+
const assetsToCreate: ServerlessResourceConfig[] = [];
8384

8485
assets.forEach(asset => {
85-
const { path: assetPath, access } = getPathAndAccessFromFileInfo(asset);
8686
const existingAsset = existingAssets.find(
87-
x => assetPath === x.friendly_name
87+
x => asset.name === x.friendly_name
8888
);
8989
if (!existingAsset) {
90-
assetsToCreate.push({ ...asset, assetPath, access });
90+
assetsToCreate.push(asset);
9191
} else {
9292
output.push({
9393
...asset,
94-
assetPath,
95-
access,
9694
sid: existingAsset.sid,
9795
});
9896
}
@@ -101,7 +99,7 @@ export async function getOrCreateAssetResources(
10199
const createdAssets = await Promise.all(
102100
assetsToCreate.map(async asset => {
103101
const newAsset = await createAssetResource(
104-
asset.assetPath,
102+
asset.name,
105103
serviceSid,
106104
client
107105
);
@@ -134,7 +132,7 @@ async function createAssetVersion(
134132
{
135133
form: true,
136134
body: {
137-
Path: asset.assetPath,
135+
Path: asset.path,
138136
Visibility: asset.access,
139137
},
140138
}
@@ -161,22 +159,12 @@ export async function uploadAsset(
161159
serviceSid: string,
162160
client: GotClient
163161
): Promise<Sid> {
164-
let content: Buffer | string | undefined;
165-
if (typeof asset.content !== 'undefined') {
166-
content = asset.content;
167-
} else if (typeof asset.path !== 'undefined') {
168-
const encoding = extname(asset.path) === '.js' ? 'utf8' : undefined;
169-
content = await readFile(asset.path, encoding);
170-
} else {
171-
throw new Error('Missing either content or path for file');
172-
}
173-
174162
const version = await createAssetVersion(asset, serviceSid, client);
175163
const { pre_signed_upload_url: awsData } = version;
176164
const awsResult = await uploadToAws(
177165
awsData.url,
178166
awsData.kmsARN,
179-
content,
167+
asset.content,
180168
asset.name
181169
);
182170
return version.sid;

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

+11-35
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
11
/** @module @twilio-labs/serverless-api/dist/api */
22

33
import debug from 'debug';
4-
import { extname } from 'path';
54
import {
6-
FileInfo,
75
FunctionApiResource,
6+
ServerlessResourceConfig,
87
FunctionList,
98
FunctionResource,
109
GotClient,
11-
RawFunctionWithPath,
1210
Sid,
1311
VersionResource,
1412
} from '../types';
1513
import { uploadToAws } from '../utils/aws-upload';
16-
import { getPathAndAccessFromFileInfo, readFile } from '../utils/fs';
1714

1815
const log = debug('twilio-serverless-api:functions');
1916

@@ -76,29 +73,21 @@ export async function listFunctionResources(
7673
* @returns {Promise<FunctionResource[]>}
7774
*/
7875
export async function getOrCreateFunctionResources(
79-
functions: FileInfo[],
76+
functions: ServerlessResourceConfig[],
8077
serviceSid: string,
8178
client: GotClient
8279
): Promise<FunctionResource[]> {
8380
const output: FunctionResource[] = [];
8481
const existingFunctions = await listFunctionResources(serviceSid, client);
85-
const functionsToCreate: RawFunctionWithPath[] = [];
82+
const functionsToCreate: ServerlessResourceConfig[] = [];
8683

8784
functions.forEach(fn => {
88-
const { path: functionPath, access } = getPathAndAccessFromFileInfo(
89-
fn,
90-
'.js'
91-
);
92-
const existingFn = existingFunctions.find(
93-
f => functionPath === f.friendly_name
94-
);
85+
const existingFn = existingFunctions.find(f => fn.name === f.friendly_name);
9586
if (!existingFn) {
96-
functionsToCreate.push({ ...fn, functionPath, access });
87+
functionsToCreate.push({ ...fn });
9788
} else {
9889
output.push({
9990
...fn,
100-
functionPath,
101-
access,
10291
sid: existingFn.sid,
10392
});
10493
}
@@ -107,7 +96,7 @@ export async function getOrCreateFunctionResources(
10796
const createdFunctions = await Promise.all(
10897
functionsToCreate.map(async fn => {
10998
const newFunction = await createFunctionResource(
110-
fn.functionPath,
99+
fn.name,
111100
serviceSid,
112101
client
113102
);
@@ -135,19 +124,16 @@ async function createFunctionVersion(
135124
client: GotClient
136125
): Promise<VersionResource> {
137126
if (fn.access === 'private') {
138-
throw new Error(`Function ${fn.functionPath} cannnot be "private".
139-
Please rename the file "${fn.name}" to "${fn.name.replace(
140-
'.private.',
141-
'.protected.'
142-
)}" or deploy it as an asset.`);
127+
throw new Error(`Function ${fn.name} cannnot be "private".
128+
Please change it to have 'protected' access or deploy it as an asset.`);
143129
}
144130
try {
145131
const resp = await client.post(
146132
`/Services/${serviceSid}/Functions/${fn.sid}/Versions`,
147133
{
148134
form: true,
149135
body: {
150-
Path: fn.functionPath,
136+
Path: fn.path,
151137
Visibility: fn.access,
152138
},
153139
}
@@ -156,7 +142,7 @@ Please rename the file "${fn.name}" to "${fn.name.replace(
156142
return (resp.body as unknown) as VersionResource;
157143
} catch (err) {
158144
log('%O', err);
159-
throw new Error(`Failed to upload Function ${fn.functionPath}`);
145+
throw new Error(`Failed to upload Function ${fn.name}`);
160146
}
161147
}
162148

@@ -174,22 +160,12 @@ export async function uploadFunction(
174160
serviceSid: string,
175161
client: GotClient
176162
): Promise<Sid> {
177-
let content: Buffer | string | undefined;
178-
if (typeof fn.content !== 'undefined') {
179-
content = fn.content;
180-
} else if (typeof fn.path !== 'undefined') {
181-
const encoding = extname(fn.path) === '.js' ? 'utf8' : undefined;
182-
content = await readFile(fn.path, encoding);
183-
} else {
184-
throw new Error('Missing either content or path for file');
185-
}
186-
187163
const version = await createFunctionVersion(fn, serviceSid, client);
188164
const { pre_signed_upload_url: awsData } = version;
189165
const awsResult = await uploadToAws(
190166
awsData.url,
191167
awsData.kmsARN,
192-
content,
168+
fn.content,
193169
fn.name
194170
);
195171
return version.sid;

packages/serverless-api/src/types/deploy.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { DeployStatus } from './consts';
66
import {
77
AssetResource,
88
EnvironmentVariables,
9-
FileInfo,
9+
ServerlessResourceConfig,
1010
FunctionResource,
1111
} from './generic';
1212
import { Sid } from './serverless-api';
@@ -53,11 +53,11 @@ export type DeployProjectConfig = ClientConfig &
5353
/**
5454
* List of functions that should be deployed
5555
*/
56-
functions: FileInfo[];
56+
functions: ServerlessResourceConfig[];
5757
/**
5858
* List of assets that should be deployed
5959
*/
60-
assets: FileInfo[];
60+
assets: ServerlessResourceConfig[];
6161
};
6262

6363
/**

packages/serverless-api/src/types/generic.ts

+28-24
Original file line numberDiff line numberDiff line change
@@ -8,43 +8,47 @@ export type EnvironmentVariables = {
88
[key: string]: string | undefined;
99
};
1010

11+
export type FileInfo = {
12+
name: string;
13+
path: string;
14+
};
15+
1116
/**
12-
* Necessary info to deploy a function or asset
17+
* Necessary info to deploy a function
1318
*/
14-
export type FileInfo = {
19+
export type ServerlessResourceConfig = {
1520
/**
16-
* Relative file name/path. Will be used to create the Serverless paths. Example:
17-
* `example.js` => `/example`
18-
* `sms/reply.protected.js` => `/sms/reply`
19-
* `index.html` => `/index.html`
21+
* Access for the function
2022
*/
21-
name: string;
23+
access: AccessOptions;
2224
/**
23-
* Path on file system to read the contents from
25+
* Content of the uploaded function
2426
*/
25-
path?: string;
27+
content: string | Buffer;
2628
/**
27-
* Alternantive to `path` if you want to specify the content directly.
28-
* Can be a string or a `Buffer` instance representing the content.
29+
* Function name
2930
*/
30-
content?: string | Buffer;
31-
};
32-
33-
export type RawFunctionWithPath = FileInfo & {
34-
functionPath: string;
35-
access: AccessOptions;
31+
name: string;
32+
/**
33+
* Path for the serverless resource
34+
* Functions: '/some-function'
35+
* Assets: '/some-assets.jpg'
36+
*/
37+
path: string;
3638
};
3739

38-
export type RawAssetWithPath = FileInfo & {
39-
assetPath: string;
40-
access: AccessOptions;
40+
export type ServerlessResourceConfigWithFilePath = ServerlessResourceConfig & {
41+
/**
42+
* Path to the actual file on the file system.
43+
*/
44+
filePath: string;
4145
};
4246

43-
export type FunctionResource = RawFunctionWithPath & {
47+
export type FunctionResource = ServerlessResourceConfig & {
4448
sid: string;
4549
};
4650

47-
export type AssetResource = RawAssetWithPath & {
51+
export type AssetResource = ServerlessResourceConfig & {
4852
sid: string;
4953
};
5054

@@ -66,6 +70,6 @@ export type ResourcePathAndAccess = {
6670
};
6771

6872
export type DirectoryContent = {
69-
assets: FileInfo[];
70-
functions: FileInfo[];
73+
assets: ServerlessResourceConfigWithFilePath[];
74+
functions: ServerlessResourceConfigWithFilePath[];
7175
};

0 commit comments

Comments
 (0)