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
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jmcguffee great thanks for giving this a spin!
We definitely should have that supported. Still I think it'll be great first to agree on final configuration proposal.
I've proposed something slightly different, but I think more powerful here: #6171 (comment) It takes into account also some other needs. What do you think about it?
Let's maybe first clarify final shape at #6171 and then come back here to finalize implementation
@medikoo Sounds good. |
@medikoo updated to include proposed in issue. Have a look and let me know what looks good and what doesn't. |
I don't know why the prettier commands aren't working properly on my machine |
Codecov Report
@@ Coverage Diff @@
## master #7619 +/- ##
==========================================
+ Coverage 87.99% 88.05% +0.06%
==========================================
Files 245 246 +1
Lines 9188 9254 +66
==========================================
+ Hits 8085 8149 +64
- Misses 1103 1105 +2
Continue to review full report at Codecov.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you @jmcguffee ! It looks really promising, I've proposed some improvements that should help us in future maintenance of this feature
README.md
Outdated
@@ -23,10 +23,11 @@ Serverless is an MIT open-source project, actively maintained by a full-time, ve | |||
|
|||
<a href="https://www.youtube.com/watch?v=-Nf0ui3qP2E" target="_blank">Watch the video overview here.</a> | |||
|
|||
In 2020, the Serverless Framework began introducing advanced functionality for specific serverless use-cases, known as Serverless Framework Components. Check out the Components featured below, [and more here](https://github.com/serverless-components). | |||
In 2020, the Serverless Framework began introducing advanced functionality for specific serverless use-cases, known as Serverless Framework Components. Check out the Components featured below, [and more here](https://github.com/serverless-components). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's not include unrelated changes (otherwise we're happy to take it in other PR)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess one of the plugins updated this file. I will revert it.
let description; | ||
let definition; | ||
if (schema.schema) { | ||
name = schema.name ? schema.name : `${resourceName}Model`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there's no name, I would let CF generate on (as automatic generation as above is immune to errors)
let definition; | ||
if (schema.schema) { | ||
name = schema.name ? schema.name : `${resourceName}Model`; | ||
description = schema.description ? schema.description : `Validation model for ${name}`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, if there's no description, then let's not add description
@@ -139,6 +138,77 @@ module.exports = { | |||
); | |||
} | |||
|
|||
// Models are defined in the provider and can be used in http events later | |||
if (!_.isUndefined(apiGateway.schemas)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's write it as if (apiGateway.schemas) {
if (!resourceDefinition.name) { | ||
name = _.upperFirst(_.camelCase(resourceName)); | ||
} else { | ||
name = _.upperFirst(_.camelCase(resourceDefinition.name)); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's not implement auto generation for name
, it increases maintenance cost and we're not applying necessary guards to ensure we produce a valid name (that fits max length etc)
name = _.upperFirst(_.camelCase(resourceDefinition.name)); | ||
} | ||
|
||
const modelLogicalId = this.getModelLogicalId(name); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should use this.provider.naming.getModelLogicalId
, and ensure it supports case of reusable models
@@ -139,6 +138,77 @@ module.exports = { | |||
); | |||
} | |||
|
|||
// Models are defined in the provider and can be used in http events later | |||
if (!_.isUndefined(apiGateway.schemas)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would also seclude it into dedicated lib/plugins/aws/package/compile/events/apiGateway/lib/requestValidators.js
(and handle generation of all AWS::ApiGateway::Model
and AWS::ApiGateway::RequestValidator
resources, so it's in one place)
validatorLogicalId, | ||
}; | ||
|
||
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's use Object.assign
@@ -157,6 +164,163 @@ describe('#compileMethods()', () => { | |||
}); | |||
}); | |||
|
|||
it('should support inline model configuration', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I suggested in other comment it'll be great to seclude logic into lib/plugins/aws/package/compile/events/apiGateway/lib/requestValidators.js
, then lib/plugins/aws/package/compile/events/apiGateway/lib/requestValidators.test.js
test file should have few tests configured through runServerless
util.
Currently we're coming away from tests as you can see in most parts of the framework. Usually they focus on implementation internals and do not test against fully build serverless
instance, which make them very weak.
See how new tests are written in those examples (sorry for not having a proper document on that yet, I will try to prepare one shortly):
serverless/lib/plugins/aws/package/compile/functions/index.test.js
Lines 2624 to 2719 in 7d3636f
describe('AwsCompileFunctions #2', () => { after(fixtures.cleanup); describe('Destinations', () => { it('Should reference function from same service as destination', () => runServerless({ cwd: fixtures.map.functionDestinations, cliArgs: ['package'], }).then(serverless => { const cfResources = serverless.service.provider.compiledCloudFormationTemplate.Resources; const naming = serverless.getProvider('aws').naming; const destinationConfig = cfResources[naming.getLambdaEventConfigLogicalId('trigger')].Properties.DestinationConfig; expect(destinationConfig).to.deep.equal({ OnSuccess: { Destination: { 'Fn::GetAtt': [naming.getLambdaLogicalId('target'), 'Arn'] }, }, }); expect(destinationConfig).to.not.have.property('OnFailure'); })); it('Should support OnFailure destinations', () => fixtures .extend('functionDestinations', { functions: { trigger: { destinations: { onSuccess: null, onFailure: 'target' } } }, }) .then(fixturePath => runServerless({ cwd: fixturePath, cliArgs: ['package'], }).then(serverless => { const cfResources = serverless.service.provider.compiledCloudFormationTemplate.Resources; const naming = serverless.getProvider('aws').naming; const destinationConfig = cfResources[naming.getLambdaEventConfigLogicalId('trigger')].Properties .DestinationConfig; expect(destinationConfig).to.not.have.property('OnSuccess'); expect(destinationConfig).to.deep.equal({ OnFailure: { Destination: { 'Fn::GetAtt': [naming.getLambdaLogicalId('target'), 'Arn'] }, }, }); }) )); it('Should support ARN to external function as destination', () => { const arn = 'arn:aws:lambda:us-east-1:12313231:function:external'; return fixtures .extend('functionDestinations', { functions: { trigger: { destinations: { onSuccess: arn } } }, }) .then(fixturePath => runServerless({ cwd: fixturePath, cliArgs: ['package'], }).then(serverless => { const cfResources = serverless.service.provider.compiledCloudFormationTemplate.Resources; const naming = serverless.getProvider('aws').naming; const destinationConfig = cfResources[naming.getLambdaEventConfigLogicalId('trigger')].Properties .DestinationConfig; expect(destinationConfig).to.deep.equal({ OnSuccess: { Destination: arn } }); }) ); }); it('Should respect `role` setting', () => fixtures .extend('functionDestinations', { provider: { role: ' arn:aws:iam::XXXXXX:role/role' } }) .then(fixturePath => runServerless({ cwd: fixturePath, cliArgs: ['package'], }).then(serverless => { const cfResources = serverless.service.provider.compiledCloudFormationTemplate.Resources; const naming = serverless.getProvider('aws').naming; const destinationConfig = cfResources[naming.getLambdaEventConfigLogicalId('trigger')].Properties .DestinationConfig; expect(destinationConfig).to.deep.equal({ OnSuccess: { Destination: { 'Fn::GetAtt': [naming.getLambdaLogicalId('target'), 'Arn'] }, }, }); expect(destinationConfig).to.not.have.property('OnFailure'); }) )); }); }); - https://github.com/serverless/serverless/blob/7d3636f9682c7c9929a9061f105ed232d139aa56/lib/plugins/aws/package/compile/events/httpApi/index.test.js
You can find documentation for runServerless
util here: https://github.com/serverless/test/blob/master/docs/run-serverless.md
@medikoo Thanks for the feedback. I will work on getting them updated today. |
@medikoo take a look at the updates and let me know if that is more of the direction that you were thinking. If so I will update all the tests and push for review again. As per your direction I will use the testing procedure specified in the PR. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jmcguffee thank you! Yes direction is perfect, just ensure to respect lint and prettier formatting, and reduce lodash usage to minimum.
@@ -269,6 +274,13 @@ functions: | |||
identitySource: method.request.header.Authorization | |||
identityValidationExpression: someRegex | |||
type: token # token or request. Determines input to the authorizer function, called with the auth token or the entire request event. Defaults to token | |||
schema: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can remove schema
from here now, as I would consider this as deprecated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
@medikoo this is ready for review again with updated test |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jmcguffee that looks great. Thank you!
Please see my suggestions, and let me know what you think. Once we have them settled I believe we're fine to get that in.
const resourceName = _.upperFirst(_.camelCase(schemaId)); | ||
const modelLogicalId = this.provider.naming.getModelLogicalId( | ||
resourceName, | ||
'any', | ||
'application/json' | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we need something more solid, in current form we're using method which was strictly designed to resolve logical id for specific HTTP endpoint and content type, without any endpoint and content type context.
I know I suggested to use same method, but that just doesn't look good.
I propose to rename current method to getEndpointModelLogicalId
, and introduce another getModelLogicalId
that takes just schemaId
and resolves id as:
return `ApiGateway${_.upperFirst(_.camelCase(schemaId))}Model`;
'application/json' | ||
); | ||
|
||
const validatorLogicalId = this.provider.naming.getValidatorLogicalId(resourceName, 'any'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I light of above comment I would refactor getValidatorLogicalId
to just take modelLogicalId
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one is going to be a little more involved. The resource only defines one validator as a whole and multiple models can be defined. With that route multiple validators will be created while only one is being referenced in the Method Resource.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As i checked this method is only used in two places.. It can simply be updated to accept just modelLogicalId
and places were it's invoked should then be updated to just pass it (and over there it's resolved upfront)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was updated and refactored out to another method
validatorLogicalId, | ||
}; | ||
|
||
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's use Object.assign
}, | ||
}); | ||
|
||
if (modelName != null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can write it as if (modelName)
, at least I would treat eventual empty string as lack of value
|
||
if (schemaConfig.schema) { | ||
modelName = schemaConfig.name ? schemaConfig.name : null; | ||
validatorName = modelName !== null ? `${modelName}Validator` : null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's simplify to: modelName ? ...
let validatorLogicalId; | ||
|
||
// New resources need to be created | ||
if (!models[schemaConfig]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Logic seems to be a bit leaky, as we're also running this check if schemaConfig
is object.
I propose to restructure above logic into:
if (_.isObject(schemaConfig)) {
// Schema defined inline, specific to given endpont
} else {
// String
if (models[schemaConfig]) {
// Schema reused
} else {
// Simple schema defined inline, specific to given endpoint
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will add an or
to it as if (_.isObject(schemaConfig) || !models[schemaConfig])
. I was trying to DRY out as much as possible without writing a bunch of helper methods.
event.http.method | ||
); | ||
|
||
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's use Object.assign
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't seem addressed
describe('#compileRequestValidators()', () => { | ||
after(fixtures.cleanup); | ||
|
||
describe(' - provider configuration -', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's stick to convention of a project, so no -
prefixes and postfixes.
Also let's maybe name it reusable schemas
, note provider configuration
, suggests it's a configuration of provider.
name += 'Model'; | ||
} | ||
return name; | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I belive there should be no changes in that file
@medikoo I believe all issues have been addressed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jmcguffee that looks really good, thank you!
Please see my remarks, I believe they final, and we'd be able shortly to take that in
lib/plugins/aws/lib/naming.js
Outdated
getValidatorLogicalId(modelLogicalId) { | ||
return `${modelLogicalId}Validator`; | ||
}, | ||
getEndpointValidatorLogicalId(resourceId, methodName) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function can be removed, as it's used now in places where we have modelLogicalId
, so we can use getValidatorLogicalId(modelLogicalId)
instead
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added the additional method because there is only 1 validator allowed per API Gateway Method. This was causing issues when using the contentType in the ModelLogicalID. This only becomes an issue when multiple content types are defined in the resource. I have updated to utilize getModelLogicalId
within this method now to prevent that.
} | ||
|
||
// New Functionality | ||
if (event.http.requestSchema) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's do:
if (event.http.requestSchema) {
...
} else if (event.http.request && event.http.request.schema) {
...
}
At least I believe we should not support both formats put together, and if it's the case, new way should be favored
event.http.method | ||
); | ||
|
||
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't seem addressed
|
||
describe(' reusable schemas ', () => { | ||
it('Should process schema from apiGateway provider, full config', () => | ||
fixtures.extend('requestSchema', {}).then(fixturePath => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do not extend fixture with anything, so let's not use fixture.extend
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Im really running that to help populate the serverless block using the fixturePath. I didn't see another example just using runServerless
, but I will work through it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's an example of using fixture directly:
serverless/lib/plugins/aws/package/compile/functions/index.test.js
Lines 2629 to 2632 in 7310782
runServerless({ | |
cwd: fixtures.map.functionDestinations, | |
cliArgs: ['package'], | |
}).then(serverless => { |
)); | ||
|
||
it('Should process schema from apiGateway provider, missing name and description', () => | ||
fixtures.extend('requestSchema', {}).then(fixturePath => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here
@@ -6,7 +6,6 @@ const BbPromise = require('bluebird'); | |||
module.exports = { | |||
compileRestApi() { | |||
const apiGateway = this.serverless.service.provider.apiGateway || {}; | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's remove this change
@@ -0,0 +1,14 @@ | |||
{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's use camelCase convention for file names
)); | ||
|
||
it('Should process schema from apiGateway provider, missing name and description', () => | ||
fixtures.extend('requestSchema', {}).then(fixturePath => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's reuse one serverless
instance when we do not change fixture. We can put serverless
instance initialization into before
block, and then in it
's just confirm on generated template.
It'll run also way faster then
@medikoo sorry for the delay. Got tied up with work and really busy. Will update and submit new commit sometime this week. |
@jmcguffee no problem, looking forward for update |
Codecov Report
@@ Coverage Diff @@
## master #7619 +/- ##
==========================================
+ Coverage 87.02% 87.04% +0.02%
==========================================
Files 285 287 +2
Lines 10922 10987 +65
==========================================
+ Hits 9505 9564 +59
- Misses 1417 1423 +6
Continue to review full report at Codecov.
|
@medikoo should be good now. Let me know if you spot any more issues. I noticed I couldn't use the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jmcguffee that's outsanding work, thank you!
I've pointed just few last details, having that I believe we will be ready to get this in
|
||
const validatorLogicalId = this.provider.naming.getValidatorLogicalId(modelLogicalId); | ||
|
||
if (schemaConfig.name) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be on a safe side, I believe this condition should be run only if definition
was provided on schemaConfig.schema
property.
Otherwise we're inspecting the actual definition for those properties, and e.g. description
is a perfectly valid JSON Schema property, which with logic as proposed may be used as model description, which is not right
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These properties are separate from the actual JSON for the schema definition. In that sense there should be no issues adding name even if those properties don't exist. I can do it just for the sake of it, but I am not seeing where the clash would happen.
Object.assign(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { | ||
[modelLogicalId]: { | ||
Type: 'AWS::ApiGateway::Model', | ||
Properties: { | ||
RestApiId: { Ref: this.apiGatewayRestApiLogicalId }, | ||
Schema: definition, | ||
ContentType: 'application/json', | ||
}, | ||
}, | ||
[validatorLogicalId]: { | ||
Type: 'AWS::ApiGateway::RequestValidator', | ||
Properties: { | ||
RestApiId: { Ref: this.apiGatewayRestApiLogicalId }, | ||
ValidateRequestBody: true, | ||
ValidateRequestParameters: true, | ||
}, | ||
}, | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'll be nice to add those resources to template only if those models are actually used in events
let globalServerless; | ||
|
||
before(() => { | ||
globalServerless = runServerless({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's rename it to serverlessInstance
, also we should store resolved instance and not promise that eventually resolves the instance
"$schema": "http://json-schema.org/draft-04/schema#", | ||
"type": "object", | ||
"title": "Test Validation Schema", | ||
"required": ["id"], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We may add description
here, to expose eventual vulnerability I've pointed in logic
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems not addressed
expect(modelResource).to.deep.equal({ | ||
Type: 'AWS::ApiGateway::Model', | ||
Properties: { | ||
ContentType: 'application/json', | ||
Description: 'Test Description', | ||
Name: 'TestModel', | ||
RestApiId: { | ||
Ref: 'ApiGatewayRestApi', | ||
}, | ||
Schema: { | ||
$schema: 'http://json-schema.org/draft-04/schema#', | ||
definitions: {}, | ||
properties: { | ||
id: { | ||
pattern: '[0-9]+', | ||
title: 'ID for object', | ||
type: 'number', | ||
}, | ||
}, | ||
required: ['id'], | ||
title: 'Test Validation Schema', | ||
type: 'object', | ||
}, | ||
}, | ||
}); | ||
})); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really great tests! Thank you!
I'm always trying to minimize dependency on implementation details, so usually I do not confirm on every single property but on one or few crucial. So e.g. some cleanup doesn't necessarily break the tests.
(just a note for future, I do not expect you to update above)
@medikoo my greatest apologies for the delay. I picked up a couple of contracts and they have tied my time up. I submitted some of the changes requested from the feedback. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jmcguffee great thanks for update, and now worries for delay it's totally understood.
Over last months few things got improved in a framework and also critieria's for PR's change. Recently, we've added a configuration validation (that works against pre-configured JSON schema), and we're doing our best so all incoming improvements also come with fully configured schema.
Additionally we have a dedicated deprecation logging mechanism which would be good to use here for deprecating old schema configuration format.
I've also noticed that our decision to name new schemas with requestSchemas
is probably not great.
Please see my comments
- http: | ||
path: posts/create | ||
method: post | ||
requestSchema: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've just looked back and our naming decisions, and I think we overlooked one thing. Currently http
event has request
configuration that supports both parameters
and schema
.
When we were discussing global configuration of schemas on provider.apiGateway
I've pointed that schemas
name is ambigous in that context and that it'll be better to name it requestSchemas
.
Having that we followed also with introduction of requestSchemas
on HTTP event, and that doesn't feel right over there, as we have now http.request.parameters
and http.requestSchema
.
I think it was not great decision (I missed that it's not just schema
that we configure there), and I believe we should not go into that.
I propose that we go with provider.apiGateway.request.schemas
and http.request.schemas
, and deprecate http.request.schema
(note that now we have a dedicated deprecation function that should be used for that)
Also it'll be nice to back new request schema configuration with JSON schema validation, of which support was merged recently into project (for reference issue in which we tackle complete schema for http
event: #8018 )
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While I was wrapping up the last changes (deprecation), I've started wondering about this approach to having provider.apiGateway.request.schemas
and http.request.schemas
and deprecating http.request.schema
- while provider.apiGateway.request.schemas
sounds good, I'm wondering if we should/need to move from http.request.schema
to http.request.schemas
- what is the main reason here? Is it to be consistent and use plural schemas
? Otherwise, we could totally accomodate the old http.request.schema
to support both new and old approach (because it's only extending it). Please let me know what do you think. 🙇
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it to be consistent and use plural schemas?
Yes, in first it's to be consistent (it's a bit confusing to have schemas
as global, and schema
as local, with very same definition underneath). Also I think plural form is right, as we're configuring dictionary of schemas. Singular name may suggest we're defining the single schema, and those are its properties, which is again confusing.
While I understand the pain of creating deprecation just because we add s
I feel there's good reasoning behind it, and that it'll leave our API more solid.
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the explanation - that's what I thought here, but wanted to confirm as I wasn't 100% sure. I think it makes sense to be consistent here - the migration path from schema
to schemas
is not complicated so it shouldn't be too painful for users. Thanks for the clarification 💯
|
||
module.exports = { | ||
compileRequestValidators() { | ||
const apiGateway = this.serverless.service.provider.apiGateway || {}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'll be nicer to name it apiGatewayConfig
(apiGateway
suggests we deal with API Gateway instance)
); | ||
|
||
// Old functionality | ||
if (event.http.request && event.http.request.schema) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'll be nice to put precedence on event.http.request.schemas
and not on deprecated event.http.request.schema
if (schemaConfig.schema) { | ||
modelName = schemaConfig.name ? schemaConfig.name : null; | ||
validatorName = modelName ? `${modelName}Validator` : null; | ||
description = schemaConfig.description ? schemaConfig.description : null; | ||
definition = schemaConfig.schema; | ||
} else { | ||
definition = schemaConfig; | ||
} | ||
|
||
let modelLogicalId; | ||
let validatorLogicalId; | ||
|
||
// New resources need to be created | ||
if ( | ||
_.isObject(schemaConfig) || | ||
!(apiGateway.requestSchemas && apiGateway.requestSchemas[schemaConfig]) | ||
) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we improve readability of this construct with:
if (_.isObject(schemaConfig) {
modelName = schemaConfig.name ? schemaConfig.name : null;
...
} else {
definition = schemaConfig;
}
...
if (_.isObject(schemaConfig) || !_.get(apiGatewayConfig.requestSchemas, definition)) {
...
Above will also make it more bulletproof, e.g. with what's configured currently lack of schemaConfig.schema
enforces assumption that schemaConfig
is string, when it doesn't have to be the case.
modelLogicalId | ||
].Properties.Description = description; | ||
} | ||
} else if (apiGateway.requestSchemas && apiGateway.requestSchemas[schemaConfig]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this condition is unconditionally true at this point ( it should be just } else {
)
let serverlessInstance; | ||
|
||
before(() => { | ||
return Promise.resolve( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's not use Promise.resolve
(it's an anti pattern, runServerless().then()
can be returned directly and effect is exactly same)
}); | ||
|
||
after(() => { | ||
fixtures.cleanup(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's an async operation that needs to be returned
cwd: fixtures.map.requestSchema, | ||
cliArgs: ['package'], | ||
}) | ||
).then(serverless => (serverlessInstance = serverless)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
runServerless
got improved over course of time, and now it needs to be setup as: ({ serverless }) => (serverlessInstance = serverless)
const cfResources = | ||
serverlessInstance.serverless.service.provider.compiledCloudFormationTemplate.Resources; | ||
const naming = serverlessInstance.serverless.getProvider('aws').naming; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'll be nice to get both of this directly from runServerless
result. See: https://github.com/serverless/test/blob/master/docs/run-serverless.md#result-values (it's also used in other tests across the project)
path: test | ||
method: get | ||
requestSchema: | ||
application/json: test-model | ||
- http: | ||
path: testSimple | ||
method: get | ||
requestSchema: | ||
application/json: test-model-simple | ||
- http: | ||
path: test2 | ||
method: get | ||
requestSchema: | ||
application/json: '${file(dummySchema.json)}' | ||
- http: | ||
path: test3 | ||
method: get | ||
requestSchema: | ||
application/json: | ||
name: TestMethodModel | ||
description: 'Test Method Model Desc' | ||
schema: '${file(dummySchema.json)}' | ||
- http: | ||
path: test4 | ||
method: get | ||
request: | ||
schema: | ||
application/json: '${file(dummySchema.json)}' | ||
- http: | ||
path: test5 | ||
method: get | ||
requestSchema: | ||
application/json: '${file(dummySchema.json)}' | ||
text/plain: 'foo' | ||
- http: | ||
path: test6 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'll be nice to name paths after what they test, so:
test
-> test-model-full
testSimple
-> test-model-simple
test2
-> test-direct-simple
test3
-> test-direct-full
test4
-> test-deprecated-simple
test5
-> test-multiple
test6
-> test-deprecated-multiple
…s the text reference in the method schema. If it is plain text and doesn't match a model in the provider it will be parsed. If there are issues within CloudFormation it will error. i.e application/json having invalid json formation as part of teh draft-04 syntax. This was created this way for instances of `text/html` and `text/plain` not really having a structured layout.
…test that will validate against a YML configuration.
…block and utilizing it throughout the tests.
…r. This will prevent the creation of unnecessary resources.
cc737b6
to
0ba1046
Compare
Hello 👋 I've rebased + adjusted the existing changes, I've also introduced the switch to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great @pgrzesik ! I have just one minor style suggestion
) { | ||
this.serverless._logDeprecation( | ||
'AWS_API_GATEWAY_SCHEMAS', | ||
'Starting with next major version, `http.request.schema` property will be replaced by `http.request.schemas`.' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In CLI logs, we usually do not use back quotes, but double quotes. They look better for wrapping configuration properties
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch - fixed 👍
if (!schemaConfig.schema) { | ||
definition = schemaConfig; | ||
} else { | ||
definition = schemaConfig.schema; | ||
} | ||
|
||
const modelLogicalId = this.provider.naming.getModelLogicalId(schemaId); | ||
|
||
const validatorLogicalId = this.provider.naming.getValidatorLogicalId(modelLogicalId); | ||
|
||
if (schemaConfig.name) { | ||
modelName = schemaConfig.name; | ||
validatorName = `${modelName}Validator`; | ||
} | ||
|
||
if (schemaConfig.description) { | ||
description = schemaConfig.description; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @pgrzesik I think I also got a bit lost, as here as you pointed we will deal just with object notation.
Great job on clarifying that in upper part of the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excellent 👍
What did you implement
Added in model definition capabilities. This can be done globally to create one resource for a specific model vs new for every function or resources at the function level that are customizable.
Closes #6171
How can we verify it
Todos
Useful Scripts
npm run test:ci
--> Run all validation checks on proposed changesnpm run lint:updated
--> Lint all the updated filesnpm run lint:fix
--> Automatically fix lint problems (if possible)npm run prettier-check:updated
--> Check if updated files adhere to Prettier confignpm run prettify:updated
--> Prettify all the updated filesIs this ready for review?: YES
Is it a breaking change?: NO (Existing operation behaves as before, but can be extended)