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

Added support for Promises in the variable system #3554

Merged
merged 4 commits into from
May 4, 2017
Merged

Conversation

eahefnawy
Copy link
Member

@eahefnawy eahefnawy commented May 3, 2017

What did you implement:

Closes #3073

Added support for async sources in the variable system, beginning with the file source. Your JS files can now perform async operations and return a promise that would be resolved by the variable system.

How did you implement it:

Previously, the variable system was completely sync, so I had to control the flow of the entire system and it's methods and make it promise-ready. All methods now return a promise to assure everything is running in the correct flow.

I've also ditched the traverse module since it doesn't work smoothly with promises and instead I'm using a lodash based implementation for object traversal and updates.

How can we verify it:

I've tested 17 different cases for the variable systems that covers pretty much everything the variables system can do, including the new promises in files support. Here's the config I used, but feel free to get crazy creative:

# serverless.yml

service: serverless-async-vars

provider:
  name: aws
  runtime: nodejs6.10

custom:
  data: # some data to reference
    zero: 0
    nooo: false
  varA: ${env:TESTING} # env vars
  varB: ${opt:stage} # opts vars
  varC: ${self:provider.name} # self vars
  varD: ${env:TESTING}-${opt:stage} # two vars in a string
  varE: ${opt:${env:TESTING}hoo} # nested vars
  varF: ${opt:region, opt:stage} # overwrite functionality
  varG: ${file(./vars.js):hello} # JS file running sync
  varH: ${file(./vars.js):promised} # JS file running async/promised NEW!!!
  varI: ${file(./vars.json):hoo.hoo2} # deep JSON file
  varJ: ${file(./vars.yml):hee.hee2} # deep YAML file
  varK: my stage is ${opt:stage} # vars sub string
  varL: your account number is ${opt:number} # number vars as sub string
  varM: ${opt:number} # preserving data type
  varN: ${self:plugins, self:package, self:service} # multiple overwrites when empty object and undefined
  varO: ${self:custom.data.zero, self:service} # shouldn't overwrite 0 values
  varP: ${self:custom.data.nooo, self:service} # shouldn't overwrite false values
  varQ: ${self:} # referencing the entire config file

functions:
  hello:
    handler: handler.hello

You'll need the TESTING env var to be set, and also the following vars files in your service root dir:
vars.js

module.exports.hello = () => {
  // Sync code
  return {nested: 'world'};
}

module.exports.promised = () => {
  // Async code
  return Promise.resolve('world');
}

vars.yml

hee:
  hee2: haa

vars.json

{
  "hoo": {
    "hoo2": "hum"
  }
}

You'll need to run the following command against this service for full test coverage:

sls package --stage dev --yahoo hi --region us-east-1 --number 1234

Todos:

  • Write tests
  • Write documentation
  • Fix linting errors
  • Make sure code coverage hasn't dropped
  • Provide verification config / commands / resources
  • Enable "Allow edits from maintainers" for this PR
  • Update the messages below

Is this ready for review?: YES
Is it a breaking change?: NO

@jthomerson
Copy link
Contributor

Sweet @eahefnawy! Thanks! This will be a huge help for anyone who wants to load dynamic config from, for instance, an S3 bucket, a DynamoDB table, an API call, etc, etc!

Copy link
Contributor

@pmuens pmuens left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a really exciting PR! 💯

Here are a few comments / questions:

  • Why are native Promises used instead of Bluebird promises (since we use then throughout the whole codebase?)
  • Could you update the tests so that sinon-bluebird is used (introduced in add sinon-bluebird to make promises in tests a bit cleaner #3000). This way the tests are in sync
  • Shoud we maybe remove the traverse package in this PR? AFAIK there are no other places where traverse is used.

Also tested it locally. So far everything works fine. Will test it more thoroughly the upcoming days!


Below is the serverless.yml file I've used for testing.

I exported the TESTING=ya env variable and ran serverless package --stage dev --yahoo hi --region us-east-1 --number 1234.

To validate it you could look into the Outputs section in the updated CloudFormation file

service: async-vars

provider:
  name: aws
  runtime: nodejs6.10

custom:
  data: # some data to reference
    zero: 0
    nooo: false
  varA: ${env:TESTING} # env vars
  varB: ${opt:stage} # opts vars
  varC: ${self:provider.name} # self vars
  varD: ${env:TESTING}-${opt:stage} # two vars in a string
  varE: ${opt:${env:TESTING}hoo} # nested vars
  varF: ${opt:region, opt:stage} # overwrite functionality
  varG: ${file(./vars.js):hello} # JS file running sync
  varH: ${file(./vars.js):promised} # JS file running async/promised NEW!!!
  varI: ${file(./vars.json):hoo.hoo2} # deep JSON file
  varJ: ${file(./vars.yml):hee.hee2} # deep YAML file
  varK: my stage is ${opt:stage} # vars sub string
  varL: your account number is ${opt:number} # number vars as sub string
  varM: ${opt:number} # preserving data type
  varN: ${self:plugins, self:package, self:service} # multiple overwrites when empty object and undefined
  varO: ${self:custom.data.zero, self:service} # shouldn't overwrite 0 values
  varP: ${self:custom.data.nooo, self:service} # shouldn't overwrite false values
  # varQ: ${self:} # referencing the entire config file

functions:
  hello:
    handler: handler.hello

resources:
  Outputs:
    varA:
      Value: ${self:custom.varA}
      Export:
        Name: varA
    varB:
      Value: ${self:custom.varB}
      Export:
        Name: varB
    varC:
      Value: ${self:custom.varC}
      Export:
        Name: varC
    varD:
      Value: ${self:custom.varD}
      Export:
        Name: varD
    varE:
      Value: ${self:custom.varE}
      Export:
        Name: varE
    varF:
      Value: ${self:custom.varF}
      Export:
        Name: varF
    varG:
      Value: ${self:custom.varG}
      Export:
        Name: varG
    varH:
      Value: ${self:custom.varH}
      Export:
        Name: varH
    varI:
      Value: ${self:custom.varI}
      Export:
        Name: varI
    varJ:
      Value: ${self:custom.varJ}
      Export:
        Name: varJ
    varK:
      Value: ${self:custom.varK}
      Export:
        Name: varK
    varL:
      Value: ${self:custom.varL}
      Export:
        Name: varL
    varM:
      Value: ${self:custom.varM}
      Export:
        Name: varM
    varN:
      Value: ${self:custom.varN}
      Export:
        Name: varN
    varO:
      Value: ${self:custom.varO}
      Export:
        Name: varO
    varP:
      Value: ${self:custom.varP}
      Export:
        Name: varP
    # varQ:
    #   Value: ${self:custom.varQ}
    #   Export:
    #     Name: varQ

}
});

this.service.provider.variableSyntax = variableSyntaxProperty;
return this.service;
return Promise.all(populateAll).then(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason why native Promises are used vs. Bluebird promises? (AFAIK Bluebird promises are faster. Not sure if this hold true for Node.js).

@@ -42,12 +42,12 @@ describe('Variables', () => {
const serverless = new Serverless();

const populatePropertyStub = sinon
.stub(serverless.variables, 'populateProperty');
.stub(serverless.variables, 'populateProperty').returns(Promise.resolve());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could do .resolves() since we're now using the sinon-bluebird package (introduced in #3000)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@@ -96,6 +96,7 @@
"js-yaml": "^3.6.1",
"json-refs": "^2.1.5",
"lodash": "^4.13.1",
"lodash-deep": "^2.0.0",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having this we could npm uninstall the traverse package, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@@ -164,6 +164,25 @@ module.exports.schedule = () => {
};
}
```
If your use case requires handling with dynamic/async data sources (ie. DynamoDB, API calls...etc), you can also return a promise that would be resolved as the value of the variable:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uppercase 'Promise'

@eahefnawy eahefnawy merged commit 358ea69 into master May 4, 2017
@eahefnawy eahefnawy deleted the async-vars branch May 4, 2017 18:00
Copy link
Contributor

@pmuens pmuens left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like some old Promise references are still there.

Will do a follow-up PR and clean things up 👍

if (typeof property === 'string') {
property = that.populateProperty(property, true);
t.update(property);
const populateSingleProperty = new Promise((resolve) => this
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be new BbPromise()?

valueToPopulate = this.overwrite(variableString);
} else {
valueToPopulate = this.getValueFromSource(variableString);
const singleValueToPopulate = new Promise((resolve) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be new BbPromise()?

serverless.variables.populateVariable.restore();
serverless.variables.overwrite.restore();
serverless.variables.populateVariable.restore();
return Promise.resolve();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return BbPromise.resolve()

serverless.variables.populateVariable.restore();
serverless.variables.getValueFromSource.restore();
serverless.variables.populateVariable.restore();
return Promise.resolve();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return BbPromise.resolve()

@@ -104,7 +105,6 @@
"semver": "^5.0.3",
"semver-regex": "^1.0.0",
"shelljs": "^0.6.0",
"traverse": "^0.6.6",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs some shrinkwrapping,

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support Promises in variables referencing javascript ( ${file(file.js):value} )
4 participants