Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(AWS API Gateway): Allow reuse and customization of schema models #7619

Merged
merged 15 commits into from
Feb 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/deprecations.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ disabledDeprecations:
- '*' # To disable all deprecation messages
```

<a name="AWS_API_GATEWAY_SCHEMAS"><div>&nbsp;</div></a>

## AWS API Gateway schemas

Deprecation code: `AWS_API_GATEWAY_SCHEMAS`

Starting with v3.0.0, `http.request.schema` property will be replaced by `http.request.schemas`. In addition to supporting functionalities such as model name definition and reuse of existing schemas, `http.request.schemas` also supports the same notation as `http.request.schema`, so you can safely migrate your existing configuration to the new property. For more details about the new configuration, please refer to the [API Gateway Event](/framework/docs/providers/aws/events/apigateway.md)

<a name="AWS_EVENT_BRIDGE_CUSTOM_RESOURCE"><div>&nbsp;</div></a>

## AWS EventBridge lambda event triggers
Expand Down
47 changes: 46 additions & 1 deletion docs/providers/aws/events/apigateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -805,10 +805,55 @@ functions:
path: posts/create
method: post
request:
schema:
schemas:
application/json: ${file(create_request.json)}
```

In addition, you can also customize created model with `name` and `description` properties.

```yml
functions:
create:
handler: posts.create
events:
- http:
path: posts/create
method: post
request:
schemas:
application/json:
schema: ${file(create_request.json)}
name: PostCreateModel
description: 'Validation model for Creating Posts'
```

To reuse the same model across different events, you can define global models on provider level.
In order to define global model you need to add its configuration to `provider.apiGateway.request.schemas`.
After defining a global model, you can use it in the event by referencing it by the key. Provider models are created for `application/json` content type.

```yml
provider:
...
apiGateway:
request:
schemas:
post-create-model:
name: PostCreateModel
schema: ${file(api_schema/post_add_schema.json)}
description: "A Model validation for adding posts"

functions:
create:
handler: posts.create
events:
- http:
path: posts/create
method: post
request:
schemas:
application/json: post-create-model
medikoo marked this conversation as resolved.
Show resolved Hide resolved
```

A sample schema contained in `create_request.json` might look something like this:

```json
Expand Down
11 changes: 11 additions & 0 deletions docs/providers/aws/guide/serverless.yml.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,12 @@ provider:
throttle:
burstLimit: 200
rateLimit: 100
request:
schemas: # Optional request schema validation models that can be reused in `http` events. It is always defined for `application/json` content type
global-model:
name: GlobalModel # Optional: Name of the API Gateway model
description: "A global model that can be referenced in functions" # Optional: Description of the API Gateway model
schema: ${file(schema.json)} # Valid JSON Schema
alb:
targetGroupPrefix: xxxxxxxxxx # Optional prefix to prepend when generating names for target groups
authorizers:
Expand Down Expand Up @@ -336,6 +342,11 @@ functions:
template: # Optional custom request mapping templates that overwrite default templates
application/json: '{ "httpMethod" : "$context.httpMethod" }'
passThrough: NEVER # Optional define pass through behavior when content-type does not match any of the specified mapping templates
schemas: # Optional request schema validation, mappped by content type
application/json:
name: ModelName # Optional: Name of the API Gateway model
description: "Some description" # Optional: Description of the API Gateway model
schema: ${file(model_schema.json)} # Schema for selected content type
- httpApi: # HTTP API endpoint
method: GET
path: /some-get-path/{param}
Expand Down
16 changes: 10 additions & 6 deletions lib/plugins/aws/lib/naming.js
Original file line number Diff line number Diff line change
Expand Up @@ -281,13 +281,17 @@ module.exports = {
getMethodLogicalId(resourceId, methodName) {
return `ApiGatewayMethod${resourceId}${this.normalizeMethodName(methodName)}`;
},
getValidatorLogicalId(resourceId, methodName) {
return `${this.getMethodLogicalId(resourceId, methodName)}Validator`;
getValidatorLogicalId(modelLogicalId) {
return `${modelLogicalId}Validator`;
},
getModelLogicalId(resourceId, methodName, contentType) {
return `${this.getMethodLogicalId(resourceId, methodName)}${this.toStartCase(
contentType
).replace(' ', '')}Model`;
getModelLogicalId(schemaId) {
return `ApiGateway${_.upperFirst(_.camelCase(schemaId))}Model`;
},
getEndpointModelLogicalId(resourceId, methodName, contentType) {
return `${this.getMethodLogicalId(resourceId, methodName)}${_.startCase(contentType).replace(
' ',
''
)}Model`;
},
getApiKeyLogicalId(apiKeyNumber, apiKeyName) {
if (apiKeyName) {
Expand Down
21 changes: 20 additions & 1 deletion lib/plugins/aws/package/compile/events/apiGateway/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const _ = require('lodash');

const validate = require('./lib/validate');
const compileRestApi = require('./lib/restApi');
const compileRequestValidators = require('./lib/requestValidator');
const compileApiKeys = require('./lib/apiKeys');
const compileUsagePlan = require('./lib/usagePlan');
const compileUsagePlanKeys = require('./lib/usagePlanKeys');
Expand Down Expand Up @@ -113,7 +114,11 @@ const requestSchema = {
passThrough: { enum: ['NEVER', 'WHEN_NO_MATCH', 'WHEN_NO_TEMPLATES'] },
schema: {
type: 'object',
additionalProperties: { type: 'object' },
additionalProperties: { anyOf: [{ type: 'object' }, { type: 'string' }] },
},
schemas: {
type: 'object',
additionalProperties: { anyOf: [{ type: 'object' }, { type: 'string' }] },
},
template: {
type: 'object',
Expand Down Expand Up @@ -221,6 +226,7 @@ class AwsCompileApigEvents {
compileResources,
compileCors,
compileMethods,
compileRequestValidators,
compileAuthorizers,
compileDeployment,
compilePermissions,
Expand Down Expand Up @@ -262,6 +268,18 @@ class AwsCompileApigEvents {
'to "provider.apiGateway"'
);
}

if (
this.serverless.service.provider.name === 'aws' &&
Object.values(this.serverless.service.functions).some(({ events }) =>
events.some(({ http }) => _.get(http, 'request.schema'))
)
) {
this.serverless._logDeprecation(
'AWS_API_GATEWAY_SCHEMAS',
'Starting with next major version, "http.request.schema" property will be replaced by "http.request.schemas".'
);
}
},
'package:compileEvents': async () => {
this.validated = this.validate();
Expand All @@ -275,6 +293,7 @@ class AwsCompileApigEvents {
.then(this.compileResources)
.then(this.compileCors)
.then(this.compileMethods)
.then(this.compileRequestValidators)
.then(this.compileAuthorizers)
.then(this.compileDeployment)
.then(this.compileApiKeys)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,7 @@ module.exports = {
resourceName,
event.http.method
);
const validatorLogicalId = this.provider.naming.getValidatorLogicalId(
resourceName,
event.http.method
);

const lambdaLogicalId = this.provider.naming.getLambdaLogicalId(event.functionName);
const functionObject = this.serverless.service.functions[event.functionName];
const lambdaAliasName = functionObject.targetAlias && functionObject.targetAlias.name;
Expand Down Expand Up @@ -97,42 +94,6 @@ module.exports = {

this.apiGatewayMethodLogicalIds.push(methodLogicalId);

if (event.http.request && event.http.request.schema) {
for (const requestSchema of Object.entries(event.http.request.schema)) {
const contentType = requestSchema[0];
const schema = requestSchema[1];

const modelLogicalId = this.provider.naming.getModelLogicalId(
resourceName,
event.http.method,
contentType
);

template.Properties.RequestValidatorId = { Ref: validatorLogicalId };
template.Properties.RequestModels = template.Properties.RequestModels || {};
template.Properties.RequestModels[contentType] = { Ref: modelLogicalId };

_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, {
[modelLogicalId]: {
Type: 'AWS::ApiGateway::Model',
Properties: {
RestApiId: this.provider.getApiGatewayRestApiId(),
ContentType: contentType,
Schema: schema,
},
},
[validatorLogicalId]: {
Type: 'AWS::ApiGateway::RequestValidator',
Properties: {
RestApiId: this.provider.getApiGatewayRestApiId(),
ValidateRequestBody: true,
ValidateRequestParameters: true,
},
},
});
}
}

_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, {
[methodLogicalId]: template,
});
Expand Down