Skip to content

Commit b42d7b5

Browse files
committed
Support hot-reloading vtl and schema
1 parent 9c1eda7 commit b42d7b5

File tree

5 files changed

+198
-150
lines changed

5 files changed

+198
-150
lines changed

README.md

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -53,21 +53,22 @@ Serverless: GraphiQl: http://localhost:20002
5353

5454
Put options under `custom.appsync-simulator` in your `serverless.yml` file
5555

56-
| option | default | description |
57-
| ------------------------ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
58-
| apiKey | `0123456789` | When using `API_KEY` as authentication type, the key to authenticate to the endpoint. |
59-
| port | 20002 | AppSync operations port |
60-
| wsPort | 20003 | AppSync subscriptions port |
61-
| location | . (base directory) | Location of the lambda functions handlers. |
62-
| lambda.loadLocalEnv | false | If `true`, all environment variables (`$ env`) will be accessible from the resolver function. Read more in section [Environment variables](#environment-variables). |
63-
| refMap | {} | A mapping of [resource resolutions](#resource-cloudformation-functions-resolution) for the `Ref` function |
64-
| getAttMap | {} | A mapping of [resource resolutions](#resource-cloudformation-functions-resolution) for the `GetAtt` function |
65-
| importValueMap | {} | A mapping of [resource resolutions](#resource-cloudformation-functions-resolution) for the `ImportValue` function |
66-
| functions | {} | A mapping of [external functions](#functions) for providing invoke url for external fucntions |
67-
| 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 |
68-
| dynamoDb.region | localhost | Dynamodb region. Specify it if you're connecting to a remote Dynamodb intance. |
69-
| dynamoDb.accessKeyId | DEFAULT_ACCESS_KEY | AWS Access Key ID to access DynamoDB |
70-
| dynamoDb.secretAccessKey | DEFAULT_SECRET | AWS Secret Key to access DynamoDB |
56+
| option | default | description |
57+
| ------------------------ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
58+
| apiKey | `0123456789` | When using `API_KEY` as authentication type, the key to authenticate to the endpoint. |
59+
| port | 20002 | AppSync operations port |
60+
| wsPort | 20003 | AppSync subscriptions port |
61+
| location | . (base directory) | Location of the lambda functions handlers. |
62+
| lambda.loadLocalEnv | false | If `true`, all environment variables (`$ env`) will be accessible from the resolver function. Read more in section [Environment variables](#environment-variables). |
63+
| refMap | {} | A mapping of [resource resolutions](#resource-cloudformation-functions-resolution) for the `Ref` function |
64+
| getAttMap | {} | A mapping of [resource resolutions](#resource-cloudformation-functions-resolution) for the `GetAtt` function |
65+
| importValueMap | {} | A mapping of [resource resolutions](#resource-cloudformation-functions-resolution) for the `ImportValue` function |
66+
| functions | {} | A mapping of [external functions](#functions) for providing invoke url for external fucntions |
67+
| 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 |
68+
| dynamoDb.region | localhost | Dynamodb region. Specify it if you're connecting to a remote Dynamodb intance. |
69+
| dynamoDb.accessKeyId | DEFAULT_ACCESS_KEY | AWS Access Key ID to access DynamoDB |
70+
| dynamoDb.secretAccessKey | DEFAULT_SECRET | AWS Secret Key to access DynamoDB |
71+
| watch | - \*.graphql<br/> - \*.vtl | Array of glob patterns to watch for hot-reloading. |
7172

7273
Example:
7374

@@ -79,6 +80,18 @@ custom:
7980
endpoint: 'http://my-custom-dynamo:8000'
8081
```
8182

83+
# Hot-reloading
84+
85+
By default, the simulator will hot-relad when changes to `*.graphql` or `*.vtl` files are detected.
86+
Changes to `*.yml` files are not supported (yet? - this is a Serverless Framework limitation). You will need to restart the simulator each time you change yml files.
87+
88+
Hot-reloading relies on [watchman](https://facebook.github.io/watchman). Make sure it is [installed](https://facebook.github.io/watchman/docs/install.html) on your system.
89+
90+
You can change the files being watched with the `watch` option.
91+
Or you can opt-out by leaving an emptry array or set the option to `false`.
92+
93+
Note: Functions should not require hot-reloading, unless you are using a transpiler or a bundler (such as webpack, babel or typescript), un which case you should delegate hot-reloading to that instead.
94+
8295
# Resource CloudFormation functions resolution
8396

8497
This plugin supports _some_ resources resolution from the `Ref`, `Fn::GetAtt` and `Fn::ImportValue` functions

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"axios": "^0.21.0",
2727
"cfn-resolver-lib": "^1.1.7",
2828
"dataloader": "^2.0.0",
29+
"fb-watchman": "^2.0.1",
2930
"lodash": "^4.17.20",
3031
"merge-graphql-schemas": "^1.5.8"
3132
},

src/getAppSyncConfig.js

Lines changed: 66 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ import { mergeTypes } from 'merge-graphql-schemas';
88
import directLambdaRequest from './templates/direct-lambda.request.vtl';
99
import directLambdaResponse from './templates/direct-lambda.response.vtl';
1010

11+
const directLambdaMappingTemplates = {
12+
request: directLambdaRequest,
13+
response: directLambdaResponse,
14+
};
15+
1116
export default function getAppSyncConfig(context, appSyncConfig) {
1217
// Flattening params
1318
const cfg = {
@@ -17,12 +22,24 @@ export default function getAppSyncConfig(context, appSyncConfig) {
1722
dataSources: (appSyncConfig.dataSources || []).flat(),
1823
};
1924

20-
const getFileMap = (basePath, filePath, substitutionPath = null) => ({
21-
path: substitutionPath || filePath,
22-
content: fs.readFileSync(
23-
path.join(basePath, filePath || substitutionPath),
24-
{ encoding: 'utf8' },
25-
),
25+
const mappingTemplatesLocation = path.join(
26+
context.serverless.config.servicePath,
27+
cfg.mappingTemplatesLocation || 'mapping-templates',
28+
);
29+
30+
const { defaultMappingTemplates = {} } = cfg;
31+
32+
const getMappingTemplate = (filePath) => {
33+
return fs.readFileSync(path.join(mappingTemplatesLocation, filePath), {
34+
encoding: 'utf8',
35+
});
36+
};
37+
38+
const getFileMap = (basePath, filePath) => ({
39+
path: filePath,
40+
content: fs.readFileSync(path.join(basePath, filePath), {
41+
encoding: 'utf8',
42+
}),
2643
});
2744

2845
const makeDataSource = (source) => {
@@ -121,34 +138,52 @@ export default function getAppSyncConfig(context, appSyncConfig) {
121138
}
122139
};
123140

124-
const getDefaultTemplatePrefix = (template) => {
125-
const { name, type, field } = template;
126-
return name ? `${name}` : `${type}.${field}`;
141+
const makeMappingTemplate = (resolver, type) => {
142+
const { name, type: parent, field, substitutions = {} } = resolver;
143+
144+
const defaultTemplatePrefix = name || `${parent}.${field}`;
145+
const templatePath = !isNil(resolver?.[type])
146+
? resolver?.[type]
147+
: !isNil(defaultMappingTemplates?.[type])
148+
? defaultMappingTemplates?.[type]
149+
: `${defaultTemplatePrefix}.${type}.vtl`;
150+
151+
let mappingTemplate;
152+
// Direct lambda
153+
// For direct lambdas, we use a default mapping template
154+
// See https://amzn.to/3ncV3Dz
155+
if (templatePath === false) {
156+
mappingTemplate = directLambdaMappingTemplates[type];
157+
} else {
158+
mappingTemplate = getMappingTemplate(templatePath);
159+
// Substitutions
160+
const allSubstitutions = { ...cfg.substitutions, ...substitutions };
161+
forEach(allSubstitutions, (value, variable) => {
162+
const regExp = new RegExp(`\\$\{?${variable}}?`, 'g');
163+
mappingTemplate = mappingTemplate.replace(regExp, value);
164+
});
165+
}
166+
167+
return mappingTemplate;
127168
};
128169

129-
const makeResolver = (resolver) => ({
130-
kind: resolver.kind || 'UNIT',
131-
fieldName: resolver.field,
132-
typeName: resolver.type,
133-
dataSourceName: resolver.dataSource,
134-
functions: resolver.functions,
135-
requestMappingTemplateLocation: `${getDefaultTemplatePrefix(
136-
resolver,
137-
)}.request.vtl`,
138-
responseMappingTemplateLocation: `${getDefaultTemplatePrefix(
139-
resolver,
140-
)}.response.vtl`,
141-
});
170+
const makeResolver = (resolver) => {
171+
return {
172+
kind: resolver.kind || 'UNIT',
173+
fieldName: resolver.field,
174+
typeName: resolver.type,
175+
dataSourceName: resolver.dataSource,
176+
functions: resolver.functions,
177+
requestMappingTemplate: makeMappingTemplate(resolver, 'request'),
178+
responseMappingTemplate: makeMappingTemplate(resolver, 'response'),
179+
};
180+
};
142181

143-
const makeFunctionConfiguration = (functionConfiguration) => ({
144-
dataSourceName: functionConfiguration.dataSource,
145-
name: functionConfiguration.name,
146-
requestMappingTemplateLocation: `${getDefaultTemplatePrefix(
147-
functionConfiguration,
148-
)}.request.vtl`,
149-
responseMappingTemplateLocation: `${getDefaultTemplatePrefix(
150-
functionConfiguration,
151-
)}.response.vtl`,
182+
const makeFunctionConfiguration = (config) => ({
183+
dataSourceName: config.dataSource,
184+
name: config.name,
185+
requestMappingTemplate: makeMappingTemplate(config, 'request'),
186+
responseMappingTemplate: makeMappingTemplate(config, 'response'),
152187
});
153188

154189
const makeAuthType = (authType) => {
@@ -179,90 +214,6 @@ export default function getAppSyncConfig(context, appSyncConfig) {
179214
).map(makeAuthType),
180215
});
181216

182-
const mappingTemplatesLocation = path.join(
183-
context.serverless.config.servicePath,
184-
cfg.mappingTemplatesLocation || 'mapping-templates',
185-
);
186-
187-
const makeMappingTemplate = (
188-
filePath,
189-
substitutionPath = null,
190-
substitutions = {},
191-
) => {
192-
const mapping = getFileMap(
193-
mappingTemplatesLocation,
194-
filePath,
195-
substitutionPath,
196-
);
197-
198-
forEach(substitutions, (value, variable) => {
199-
const regExp = new RegExp(`\\$\{?${variable}}?`, 'g');
200-
mapping.content = mapping.content.replace(regExp, value);
201-
});
202-
203-
return mapping;
204-
};
205-
206-
const makeMappingTemplates = (config) => {
207-
const sources = [].concat(
208-
config.mappingTemplates,
209-
config.functionConfigurations,
210-
);
211-
212-
return sources.reduce((acc, template) => {
213-
const { substitutions = {}, request, response } = template;
214-
const { request: defaultRequest, response: defaultResponse } =
215-
cfg.defaultMappingTemplates || {};
216-
217-
const defaultTemplatePrefix = getDefaultTemplatePrefix(template);
218-
219-
const requestTemplatePath = !isNil(request) ? request : defaultRequest;
220-
const responseTemplatePath = !isNil(response)
221-
? response
222-
: defaultResponse;
223-
224-
// Substitutions
225-
const allSubstitutions = { ...config.substitutions, ...substitutions };
226-
227-
let requestTemplate;
228-
let responseTemplate;
229-
const requestTempalteFileName = `${defaultTemplatePrefix}.request.vtl`;
230-
const responseTempalteFileName = `${defaultTemplatePrefix}.response.vtl`;
231-
232-
// Direct lambda
233-
// For direct lambdas, we use a default mapping template
234-
// See https://amzn.to/3ncV3Dz
235-
if (requestTemplatePath === false) {
236-
requestTemplate = {
237-
path: requestTempalteFileName,
238-
content: directLambdaRequest,
239-
};
240-
} else {
241-
requestTemplate = makeMappingTemplate(
242-
requestTemplatePath,
243-
requestTempalteFileName,
244-
allSubstitutions,
245-
);
246-
}
247-
248-
// Direct lambda
249-
if (responseTemplatePath === false) {
250-
responseTemplate = {
251-
path: responseTempalteFileName,
252-
content: directLambdaResponse,
253-
};
254-
} else {
255-
responseTemplate = makeMappingTemplate(
256-
responseTemplatePath,
257-
responseTempalteFileName,
258-
allSubstitutions,
259-
);
260-
}
261-
262-
return [...acc, requestTemplate, responseTemplate];
263-
}, []);
264-
};
265-
266217
// Load the schema. If multiple provided, merge them
267218
const schemaPaths = Array.isArray(cfg.schema)
268219
? cfg.schema
@@ -281,6 +232,5 @@ export default function getAppSyncConfig(context, appSyncConfig) {
281232
resolvers: cfg.mappingTemplates.map(makeResolver),
282233
dataSources: cfg.dataSources.map(makeDataSource).filter((v) => v !== null),
283234
functions: cfg.functionConfigurations.map(makeFunctionConfiguration),
284-
mappingTemplates: makeMappingTemplates(cfg),
285235
};
286236
}

0 commit comments

Comments
 (0)