Skip to content

Commit 8f62faa

Browse files
authored
Implement HTTP and ES DataLoaders (#5)
1 parent f5550e9 commit 8f62faa

File tree

8 files changed

+306
-42
lines changed

8 files changed

+306
-42
lines changed

README.md

Lines changed: 110 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,13 @@ Put options under `custom.appsync-simulator` in your `serverless.yml` file
5252
| port | 20002 | AppSync operations port |
5353
| wsPort | 20003 | AppSync subscriptions port |
5454
| location | . (base directory) | Location of the lambda functions handlers. |
55+
| refMap | {} | A mapping of [resource resolutions](#resource-cloudformation-functions-resolution) for the `Ref` function |
56+
| getAttMap | {} | A mapping of [resource resolutions](#resource-cloudformation-functions-resolution) for the `GetAtt` function |
5557
| dynamoDb.endpoint | http://localhost:8000 | Dynamodb endpoint. Specify it if you're not using serverless-dynamodb-local. Otherwise, port is taken from dynamodb-local conf |
5658
| dynamoDb.region | localhost | Dynamodb region. Specify it if you're connecting to a remote Dynamodb intance. |
5759
| dynamoDb.accessKeyId | DEFAULT_ACCESS_KEY | AWS Access Key ID to access DynamoDB |
58-
| dynamoDb.secretAccessKey | DEFAULT_SECRET | AWS Secret Key to access DynamoDB |
60+
| dynamoDb.secretAccessKey | DEFAULT_SECRET | AWS Secret Key to access DynamoDB |
61+
5962
Example:
6063

6164
````yml
@@ -67,13 +70,115 @@ custom:
6770

6871
````
6972

70-
# Caveats
73+
# Resource CloudFormation functions resolution
74+
75+
This plugin supports *some* resources resolution from the `Ref` and `Fn::GetAtt` functions
76+
in your yaml file. It also supports *some* other Cfn functions such as `Fn::Join`, `Fb::Sub`, etc.
77+
78+
**Note:** Under the hood, this features relies on the [cfn-resolver-lib](https://github.com/robessog/cfn-resolver-lib) package. For more info on supported cfn functions, refer to [the documentation](https://github.com/robessog/cfn-resolver-lib/blob/master/README.md)
79+
80+
## Basic usage
81+
82+
You can reference resources in your functions' environment variables (that will be accessible from your lambda functions) or datasource definitions.
83+
The plugin will automatically resolve them for you.
84+
85+
````yaml
86+
provider:
87+
environment:
88+
BUCKET_NAME:
89+
Ref: MyBucket # resolves to `my-bucket-name`
90+
91+
resources:
92+
Resources:
93+
MyDbTable:
94+
Type: AWS::DynamoDB::Table
95+
Properties:
96+
TableName: myTable
97+
...
98+
MyBucket:
99+
Type: AWS::S3::Bucket
100+
Properties:
101+
BucketName: my-bucket-name
102+
...
103+
104+
# in your appsync config
105+
dataSources:
106+
- type: AMAZON_DYNAMODB
107+
name: dynamosource
108+
config:
109+
tableName:
110+
Ref: MyDbTable # resolves to `myTable`
111+
````
112+
113+
## Override (or mock) values
114+
115+
Sometimes, some references **cannot** be resolved, as they come from an *Output* from Cloudformation; or you might want to use mocked values in your local environment.
116+
117+
In those cases, you can define (or override) those values using the `refMap` and `getAttMap` options.
118+
119+
- `refMap` takes a mapping of *resource name* to *value* pairs
120+
- `getAttMap` takes a mapping of *resource name* to *attribute/values* pairs
121+
122+
Example:
123+
124+
````yaml
125+
custom:
126+
serverless-appsync-simulator:
127+
refMap:
128+
# Override `MyDbTable` resolution from the previous example.
129+
MyDbTable: 'mock-myTable'
130+
getAttMap:
131+
# define ElasticSearchInstance DomainName
132+
ElasticSearchInstance:
133+
DomainEndpoint: "localhost:9200"
134+
135+
# in your appsync config
136+
dataSources:
137+
- type: AMAZON_ELASTICSEARCH
138+
name: elasticsource
139+
config:
140+
# endpoint resolves as 'http://localhost:9200'
141+
endpoint:
142+
Fn::Join:
143+
- ""
144+
- - https://
145+
- Fn::GetAtt:
146+
- ElasticSearchInstance
147+
- DomainEndpoint
148+
````
149+
150+
## Limitations
151+
152+
This plugin only tries to resolve the following parts of the yml tree:
153+
- `provider.environment`
154+
- `functions[*].environment`
155+
- `custom.appSync`
156+
157+
If you have the need of resolving others, feel free to open an issue and explain your use case.
158+
159+
For now, the supported resources to be automatically resovled by `Ref:` are:
160+
- DynamoDb tables
161+
- S3 Buckets
71162

72-
This plugin currently only supports resolvers implemented by `amplify-appsync-simulator`.
73-
At the time of writing, this is:
163+
Feel free to open a PR or an issue to extend them as well.
74164

165+
# Supported Resolver types
166+
167+
This plugin supports resolvers implemented by `amplify-appsync-simulator`, as well as custom resolvers.
168+
169+
**From Aws Amplify:**
75170
- NONE
76171
- AWS_LAMBDA (*)
77172
- AMAZON_DYNAMODB
173+
- PIPELINE
174+
175+
**Implemented by this plugin**
176+
- AWS_LAMBDA (*)
177+
- AMAZON_ELASTIC_SEARCH
178+
- HTTP
179+
180+
(*) The `AWS_LAMBDA` dataloader has been partially copied from Aws Amplify but has been extended
181+
to support the *BatchInvoke* operations
78182

79-
(*) Note: This plugin also supports `AWS_LAMBDA`'s `BatchInvoke` (which Amplify Simulator doesn't)
183+
**Not Supported / TODO**
184+
- RELATIONAL_DATABASE

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"author": "bboure",
66
"license": "MIT",
77
"private": false,
8+
"description": "Offline support for serverless-appsync-plugin",
89
"repository": {
910
"type": "git",
1011
"url": "git+https://github.com/bboure/serverless-appsync-simulator.git"
@@ -21,6 +22,8 @@
2122
"amplify-appsync-simulator": "^1.5.0",
2223
"amplify-util-mock": "^3.5.0",
2324
"aws-sdk": "^2.585.0",
25+
"axios": "^0.19.0",
26+
"cfn-resolver-lib": "^1.1.2",
2427
"dataloader": "^2.0.0",
2528
"lodash": "^4.17.15"
2629
},

src/data-loaders/ElasticDataLoader.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import axios from 'axios';
2+
3+
export default class ElasticDataLoader {
4+
constructor(config) {
5+
this.config = config;
6+
}
7+
8+
async load(req) {
9+
try {
10+
const { data } = await axios.request({
11+
baseURL: this.config.endpoint,
12+
url: req.path,
13+
headers: req.params.headers,
14+
params: req.params.queryString,
15+
method: req.operation.toLowerCase(),
16+
data: req.params.body,
17+
});
18+
19+
return data;
20+
} catch (err) {
21+
console.log(err);
22+
}
23+
24+
return null;
25+
}
26+
}

src/data-loaders/HttpDataLoader.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import axios from 'axios';
2+
3+
export default class HttpDataLoader {
4+
constructor(config) {
5+
this.config = config;
6+
}
7+
8+
async load(req) {
9+
try {
10+
const { data } = await axios.request({
11+
baseURL: this.config.endpoint,
12+
url: req.resourcePath,
13+
headers: req.params.headers,
14+
params: req.params.query,
15+
method: req.method.toLowerCase(),
16+
data: req.params.body,
17+
});
18+
19+
return data;
20+
} catch (err) {
21+
console.log(err);
22+
}
23+
24+
return null;
25+
}
26+
}
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
/* eslint-disable class-methods-use-this */
22
export default class NotImplementedDataLoader {
3+
constructor(config) {
4+
this.config = config;
5+
}
6+
37
async load() {
4-
throw new Error('Data Loader not implemented');
8+
console.log(`Data Loader not implemented for ${this.config.type} (${this.config.name})`);
9+
10+
return null;
511
}
612
}

src/getAppSyncConfig.js

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
} from 'amplify-appsync-simulator';
44
import { invoke } from 'amplify-util-mock/lib/utils/lambda/invoke';
55
import fs from 'fs';
6-
import { find, get } from 'lodash';
6+
import { find } from 'lodash';
77
import path from 'path';
88

99
export default function getAppSyncConfig(context, appSyncConfig) {
@@ -30,22 +30,6 @@ export default function getAppSyncConfig(context, appSyncConfig) {
3030
type: source.type,
3131
};
3232

33-
/**
34-
* Returns the tableName resolving reference. Throws exception if reference is not found
35-
*/
36-
const getTableName = (table) => {
37-
if (table && table.Ref) {
38-
const tableName = get(context.serverless.service, `resources.Resources.${table.Ref}.Properties.TableName`);
39-
40-
if (!tableName) {
41-
throw new Error(`Unable to find table Reference for ${table} inside Serverless resources`);
42-
}
43-
44-
return tableName;
45-
}
46-
return table;
47-
};
48-
4933
switch (source.type) {
5034
case 'AMAZON_DYNAMODB': {
5135
const {
@@ -62,28 +46,49 @@ export default function getAppSyncConfig(context, appSyncConfig) {
6246
region,
6347
accessKeyId,
6448
secretAccessKey,
65-
tableName: getTableName(source.config.tableName),
49+
tableName: source.config.tableName,
6650
},
6751
};
6852
}
6953
case 'AWS_LAMBDA': {
7054
const { functionName } = source.config;
71-
if (context.serverless.service.functions[functionName] === undefined) {
55+
if (functionName === undefined) {
56+
context.plugin.log(
57+
`${source.name} does not have a functionName`,
58+
{ color: 'orange' },
59+
);
7260
return null;
7361
}
74-
75-
const [fileName, handler] = context.serverless.service.functions[functionName].handler.split('.');
62+
const func = context.serverless.service.functions[functionName];
63+
if (func === undefined) {
64+
context.plugin.log(
65+
`The ${functionName} function is not defined`,
66+
{ color: 'orange' },
67+
);
68+
return null;
69+
}
70+
const [fileName, handler] = func.handler.split('.');
7671
return {
7772
...dataSource,
7873
invoke: (payload) => invoke({
7974
packageFolder: context.serverless.config.servicePath,
8075
handler,
8176
fileName: path.join(context.options.location, fileName),
8277
event: payload,
83-
environment: context.serverless.service.provider.environment || {},
78+
environment: {
79+
...context.serverless.service.provider.environment,
80+
...func.environment,
81+
},
8482
}),
8583
};
8684
}
85+
case 'AMAZON_ELASTICSEARCH':
86+
case 'HTTP': {
87+
return {
88+
...dataSource,
89+
endpoint: source.config.endpoint,
90+
};
91+
}
8792
default:
8893
return dataSource;
8994
}

0 commit comments

Comments
 (0)