Skip to content

Commit 22ff6d3

Browse files
committed
Initial commit
0 parents  commit 22ff6d3

File tree

11 files changed

+10565
-0
lines changed

11 files changed

+10565
-0
lines changed

.babelrc

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"plugins": [
3+
"@babel/plugin-transform-modules-commonjs"
4+
],
5+
"presets": [
6+
["@babel/preset-env",
7+
{
8+
"shippedProposals": true,
9+
"targets": { "node": "8.12" }
10+
}
11+
]
12+
]
13+
}

.editorconfig

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
root = true
2+
3+
[*]
4+
indent_style = space
5+
indent_size = 2
6+
end_of_line = lf
7+
charset = utf-8
8+
trim_trailing_whitespace = true
9+
insert_final_newline = true
10+
11+
[*.js]
12+
indent_style = space
13+
indent_size = 2

.eslintrc.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
env:
2+
browser: true
3+
es6: true
4+
extends: airbnb-base
5+
globals:
6+
Atomics: readonly
7+
SharedArrayBuffer: readonly
8+
parserOptions:
9+
ecmaVersion: 2018
10+
sourceType: module
11+
parser: babel-eslint
12+
rules:
13+
no-console: off

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules
2+
lib

README.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
This serverless plugin is a wrapper for [amplify-appsync-simulator](amplify-appsync-simulator) made for testing AppSync APIs built with [serverless-appsync-plugin](https://github.com/sid88in/serverless-appsync-plugin).
2+
3+
4+
# Requires
5+
- [serverless framework](https://github.com/serverless/serverless)
6+
- [serverless-appsync-plugin](https://github.com/sid88in/serverless-appsync-plugin)
7+
- [serverless-offline](https://github.com/dherault/serverless-offline)
8+
- [serverless-dynamodb-local](https://github.com/99xt/serverless-dynamodb-local) (when using dynamodb resolvers only)
9+
10+
# Usage
11+
12+
This plugin relies on your serverless yml file and on the `serverless-offline` plugin*.
13+
14+
To start it, run the followin command:
15+
````bash
16+
sls offline start
17+
````
18+
19+
You should see in the logs something like:
20+
21+
````bash
22+
...
23+
Serverless: AppSync endpoint: http://localhost:20002/graphql
24+
Serverless: GraphiQl: http://localhost:20002
25+
...
26+
````
27+
28+
(*): If you are using DynamoDb resolvers, you'll also need [serverless-dynamodb-local](https://github.com/99xt/serverless-dynamodb-local)
29+
30+
# Configuration
31+
32+
Put options under `custom.appsync-simulator` in your `serverless.yml` file
33+
34+
| option | default | description |
35+
|--------|---------|-------------|
36+
| port | 20002 | AppSync operations port |
37+
| wsPort | 20003 | AppSync subscriptions port |
38+
| location | . (base directory) | Location of the lambda functions handlers. |
39+
40+
Example:
41+
42+
````yml
43+
custom:
44+
appsync-simulator:
45+
location: '.webpack/service' # use webpack build directory
46+
````
47+
48+
# Caveats
49+
50+
This plugin currently only supports resolvers implemented by `amplify-appsync-simulator`.
51+
At the time of writing, this is:
52+
53+
- NONE
54+
- AWS_LAMBDA (*)
55+
- AMAZON_DYNAMODB
56+
57+
(*) Note: This plugin also supports `AWS_LAMBDA`'s `BatchInvoke` (which Amplify Simulator doesn't)

package.json

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"name": "serverless-appsync-simulator",
3+
"version": "0.1.0",
4+
"main": "lib/index.js",
5+
"author": "bboure",
6+
"license": "MIT",
7+
"private": false,
8+
"scripts": {
9+
"build": "babel src/ -d lib/ --delete-dir-on-start",
10+
"prepare": "yarn run build"
11+
},
12+
"files": [
13+
"/lib"
14+
],
15+
"dependencies": {
16+
"amplify-appsync-simulator": "^1.5.0",
17+
"amplify-util-mock": "^3.5.0",
18+
"aws-sdk": "^2.585.0",
19+
"dataloader": "^2.0.0",
20+
"lodash": "^4.17.15"
21+
},
22+
"devDependencies": {
23+
"@babel/cli": "^7.7.4",
24+
"@babel/core": "^7.7.4",
25+
"@babel/plugin-transform-modules-commonjs": "^7.7.4",
26+
"@babel/preset-env": "^7.7.4",
27+
"babel-eslint": "^10.0.3",
28+
"eslint": "^6.7.1",
29+
"eslint-config-airbnb-base": "^14.0.0",
30+
"eslint-plugin-import": "^2.18.2",
31+
"eslint-plugin-node": "^10.0.0",
32+
"eslint-plugin-promise": "^4.2.1",
33+
"eslint-plugin-standard": "^4.0.1"
34+
}
35+
}

src/data-loaders/LambdaDataLoader.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
const DataLoader = require('dataloader');
2+
3+
const batchLoaders = {};
4+
5+
const getBatchDataResolver = (loaderName, resolver) => {
6+
if (batchLoaders[loaderName] === undefined) {
7+
batchLoaders[loaderName] = new DataLoader(resolver, { cache: false });
8+
}
9+
return batchLoaders[loaderName];
10+
};
11+
12+
export default class LambdaDataLoader {
13+
constructor(config) {
14+
this.config = config;
15+
}
16+
17+
async load(req) {
18+
try {
19+
let result;
20+
if (req.operation === 'BatchInvoke') {
21+
const dataLoader = getBatchDataResolver(this.config.name, this.config.invoke);
22+
result = await dataLoader.load(req.payload);
23+
} else {
24+
result = await this.config.invoke(req.payload);
25+
}
26+
return result;
27+
} catch (e) {
28+
console.log('Lambda Data source failed with the following error');
29+
console.log(e);
30+
throw e;
31+
}
32+
}
33+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/* eslint-disable class-methods-use-this */
2+
export default class NotImplementedDataLoader {
3+
async load() {
4+
throw new Error('Data Loader not implemented');
5+
}
6+
}

src/getAppSyncConfig.js

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import {
2+
AmplifyAppSyncSimulatorAuthenticationType as AuthTypes,
3+
} from 'amplify-appsync-simulator';
4+
import { invoke } from 'amplify-util-mock/lib/utils/lambda/invoke';
5+
import fs from 'fs';
6+
import { find } from 'lodash';
7+
import path from 'path';
8+
9+
export default function getAppSyncConfig(context, appSyncConfig) {
10+
const getFileMap = (basePath, filePath) => ({
11+
path: filePath,
12+
content: fs.readFileSync(path.join(basePath, filePath), { encoding: 'utf8' }),
13+
});
14+
15+
const makeDataSource = (source) => {
16+
if (source.name === undefined || source.type === undefined) {
17+
return null;
18+
}
19+
20+
const dataSource = {
21+
name: source.name,
22+
type: source.type,
23+
};
24+
25+
switch (source.type) {
26+
case 'AMAZON_DYNAMODB': {
27+
const { port } = context.options.dynamoDb;
28+
return {
29+
...dataSource,
30+
config: {
31+
endpoint: `http://localhost:${port}`,
32+
region: 'localhost',
33+
tableName: source.config.tableName, // FIXME: Handle Ref:
34+
},
35+
};
36+
}
37+
case 'AWS_LAMBDA': {
38+
const { functionName } = source.config;
39+
if (context.serverless.service.functions[functionName] === undefined) {
40+
return null;
41+
}
42+
43+
const [fileName, handler] = context.serverless.service.functions[functionName].handler.split('.');
44+
return {
45+
...dataSource,
46+
invoke: (payload) => invoke({
47+
packageFolder: context.serverless.config.servicePath,
48+
handler,
49+
fileName: path.join(context.options.location, fileName),
50+
event: payload,
51+
environment: context.serverless.service.provider.environment || {},
52+
}),
53+
};
54+
}
55+
default:
56+
return dataSource;
57+
}
58+
};
59+
60+
const makeResolver = (resolver) => ({
61+
kind: 'UNIT',
62+
fieldName: resolver.field,
63+
typeName: resolver.type,
64+
dataSourceName: resolver.dataSource,
65+
requestMappingTemplateLocation: resolver.request,
66+
responseMappingTemplateLocation: resolver.response,
67+
});
68+
69+
const makeAuthType = (authType) => {
70+
const auth = {
71+
authenticationType: authType.authenticationType,
72+
};
73+
74+
if (auth.authenticationType === AuthTypes.AMAZON_COGNITO_USER_POOLS) {
75+
auth.cognitoUserPoolConfig = {
76+
AppIdClientRegex: authType.userPoolConfig.appIdClientRegex,
77+
};
78+
} else if (auth.authenticationType === AuthTypes.OPENID_CONNECT) {
79+
auth.openIDConnectConfig = {
80+
Issuer: authType.openIdConnectConfig.issuer,
81+
ClientId: authType.openIdConnectConfig.clientId,
82+
};
83+
}
84+
85+
return auth;
86+
};
87+
88+
const makeAppSync = () => ({
89+
name: appSyncConfig.name,
90+
apiKey: context.options.apiKey || '123456',
91+
defaultAuthenticationType: makeAuthType(appSyncConfig),
92+
additionalAuthenticationProviders: (appSyncConfig.additionalAuthenticationProviders || [])
93+
.map(makeAuthType),
94+
});
95+
96+
const mappingTemplatesLocation = path.join(
97+
context.serverless.config.servicePath,
98+
appSyncConfig.mappingTemplatesLocation || 'mapping-templates',
99+
);
100+
101+
return {
102+
appSync: makeAppSync(),
103+
schema: getFileMap(context.serverless.config.servicePath, appSyncConfig.schema || 'schema.graphql'),
104+
resolvers: appSyncConfig.mappingTemplates.map(makeResolver),
105+
dataSources: appSyncConfig.dataSources.map(makeDataSource).filter((v) => v !== null),
106+
mappingTemplates: appSyncConfig.mappingTemplates.reduce((acc, template) => {
107+
const requestTemplate = template.request || `${template.type}.${template.field}.request.vtl`;
108+
if (!find(acc, (e) => e.path === requestTemplate)) {
109+
acc.push(getFileMap(mappingTemplatesLocation, requestTemplate));
110+
}
111+
const responseTemplate = template.response || `${template.type}.${template.field}.response.vtl`;
112+
if (!find(acc, (e) => e.path === responseTemplate)) {
113+
acc.push(getFileMap(mappingTemplatesLocation, responseTemplate));
114+
}
115+
116+
return acc;
117+
}, []),
118+
};
119+
}

src/index.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import {
2+
AmplifyAppSyncSimulator,
3+
addDataLoader,
4+
removeDataLoader,
5+
} from 'amplify-appsync-simulator';
6+
import { get, merge } from 'lodash';
7+
import getAppSyncConfig from './getAppSyncConfig';
8+
import LambdaDataLoader from './data-loaders/LambdaDataLoader';
9+
import NotImplementedDataLoader from './data-loaders/NotImplementedDataLoader';
10+
11+
class ServerlessAppSyncSimulator {
12+
constructor(serverless) {
13+
this.serverless = serverless;
14+
this.serverlessLog = serverless.cli.log.bind(serverless.cli);
15+
this.options = merge(
16+
{
17+
port: 20002,
18+
wsPort: 20003,
19+
location: '.',
20+
dynamoDb: {
21+
port: get(this.serverless.service, 'custom.dynamodb.start.port', 8000),
22+
},
23+
},
24+
get(this.serverless.service, 'custom.appsync-simulator', {}),
25+
);
26+
this.simulator = null;
27+
28+
// Hack: appsync-cli-simulator does not support BatchInvoke.
29+
removeDataLoader('AWS_LAMBDA');
30+
addDataLoader('AWS_LAMBDA', LambdaDataLoader);
31+
addDataLoader('HTTP', NotImplementedDataLoader);
32+
addDataLoader('RELATIONAL_DATABASE', NotImplementedDataLoader);
33+
addDataLoader('AMAZON_ELASTICSEARCH', NotImplementedDataLoader);
34+
35+
this.hooks = {
36+
'before:offline:start:init': this.startServer.bind(this),
37+
'before:offline:start:end': this.endServer.bind(this),
38+
};
39+
}
40+
41+
async startServer() {
42+
try {
43+
this.simulator = new AmplifyAppSyncSimulator({
44+
port: this.options.port,
45+
wsPort: this.options.wsPort,
46+
});
47+
48+
await this.simulator.start();
49+
50+
// TODO: suport several API's
51+
const appSync = Array.isArray(this.serverless.service.custom.appSync)
52+
? this.serverless.service.custom.appSync[0]
53+
: this.serverless.service.custom.appSync;
54+
55+
const config = getAppSyncConfig({
56+
serverless: this.serverless,
57+
options: this.options,
58+
}, appSync);
59+
60+
61+
if (process.env.SLS_DEBUG) {
62+
this.serverlessLog(`AppSync Config ${appSync.name}`);
63+
this.serverlessLog(JSON.stringify(config, null, 4));
64+
}
65+
66+
this.simulator.init(config);
67+
this.serverlessLog(`AppSync endpoint: ${this.simulator.url}/graphql`);
68+
this.serverlessLog(`GraphiQl: ${this.simulator.url}`);
69+
} catch (error) {
70+
this.serverlessLog(error);
71+
}
72+
}
73+
74+
endServer() {
75+
this.serverlessLog('Halting AppSync Simulator');
76+
this.simulator.stop();
77+
}
78+
}
79+
80+
module.exports = ServerlessAppSyncSimulator;

0 commit comments

Comments
 (0)