Skip to content

Commit

Permalink
Merge branch 'JCN-347-invocar-lambdas-servicios'
Browse files Browse the repository at this point in the history
# Conflicts:
#	CHANGELOG.md
#	lib/helpers/lambda-wrapper.js
#	package.json
  • Loading branch information
juanhapes committed Feb 2, 2022
2 parents 5e9d86c + e3c4311 commit ac3d452
Show file tree
Hide file tree
Showing 19 changed files with 2,001 additions and 56 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [3.6.0] - 2022-02-02
### Added
- `serviceCall` method to call Lambda functions from external services
- `serviceSafeCall` method, same as serviceCall without throwing an error if the lambda response code is 400 or higher
- `serviceClientCall` method to call Lambda functions from external services with session
- `serviceSafeClientCall` method, same as serviceClientCall without throwing an error if the lambda response code is 400 or higher
- IAM Statement for calling external functions into the `invoke-permissions`

## [3.5.0] - 2022-01-31
### Added
- Now each `Lambda` function will emit the event `janiscommerce.ended` using `@janiscommerce/events` after processing
Expand Down
291 changes: 277 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,24 @@ class MyLambda extends LambdaWithClientAndPayload {
module.exports.handler = () => Handler.handle(MyLambda, ...arguments);
```

#### :three: Lambda with Payload

This extends from the base Lambda class but overrides one default: `mustHavePayload` is set to `true`.

To use it, simply import and extend it:

```js
const { Handler, LambdaWithPayload } = require('@janiscommerce/lambda');

class MyLambda extends LambdaWithPayload {
process() {
return 'Hi';
}
}

module.exports.handler = () => Handler.handle(MyLambda, ...arguments);
```

#### Example

```js
Expand Down Expand Up @@ -416,6 +434,243 @@ class AwakeKitties {
module.exports.handler = () => Handler.handle(AwakeKitties, ...arguments);
```

### :children_crossing: Service Invoker
The Invoker make *sync* invokes to a Lambda Function across different Services
> :warning: **Local usage**
> In order to use this functionality in local environments, the setting `localServicePorts` should be set in `.janiscommercerc` config file for local envionment.
> The setting format is the `serviceCode` as key and `port` as value, example:
> ```json
> // .janiscommercerc
> {
> "localServicePorts": {
> "my-service-code": 2532
> }
> }
> ```
#### :new: SERVICE-CALL
> :information_source: Service function names
> In order to call external services functions we need to know their function names first, they will be documented in [Janis Docs](https://docs.janis.in) for each service.
* `serviceCall(serviceCode, functionName, payload)` (*async*) : Invoke a function from external service with a payload body and returns its response.
* `serviceCode` (*string*) **required**, JANIS Service code
* `functionName` (*string*) **required**, function name in TitleCase or dash-case
* `payload` (*object*), the data to send
* returns (*object*), with `statusCode` and `payload` fields
* throws (*LambdaError*), when the lambda response code is 400 or higher
```js
//
'use strict'
const { Invoker } = require('@janiscommerce/lambda');
const responseOnlyFunctionName = await Invoke.serviceCall('kitty', 'AwakeKitties');
/*
Invoke JanisKittyService-readme-AwakeKitties function without payload
responseOnlyFunctionName = { statusCode: 202, payload: 'Kittens have been awakened' };
*/
const responseWithPayload = await Invoke.serviceCall('kitty', 'GetKitty', { name: 'Kohi' });
/*
Invoke JanisKittyService-readme-GetKitty function with { name: 'Kohi' }
responseWithPayload = {
statusCode: 202,
payload: {
id: 61df4f545b95ddb21cc35628,
name: 'Kohi',
furColor: 'black',
likes: ['coffee', 'tuna'],
personality: lovely
}
}
*/
const failedInvocation = await Invoker.serviceCall('kitty', 'GetKitty', { name: 'Redtail' });
/*
Invoke JanisKittyService-readme-GetKitty function with { name: 'Redtail' }
Expected Lambda response:
{
statusCode: 404,
payload: 'Unable to find kitty with name "Redtail"';
}
Caught error:
{
message: Failed to invoke function 'GetKitty' from service 'kitty': 'Unable to find kitty with name "Redtail"',
code: 18
}
*/
```
#### SERVICE-SAFE-CALL

* `serviceSafeCall(serviceCode, functionName, payload)` (*async*) : Invoke a function from external service with a payload body and returns its response.
* `serviceCode` (*string*) **required**, JANIS Service code
* `functionName` (*string*) **required**, function name in TitleCase or dash-case
* `payload` (*object*), the data to send
* returns (*object*), with `statusCode` and `payload` fields

```js
//
'use strict'

const { Invoker } = require('@janiscommerce/lambda');

const responseOnlyFunctionName = await Invoke.serviceSafeCall('kitty', 'AwakeKitties');

/*
Invoke JanisKittyService-readme-AwakeKitties function without payload
responseOnlyFunctionName = { statusCode: 202, payload: 'Kittens have been awakened' };
*/

const responseWithPayload = await Invoke.serviceSafeCall('kitty', 'GetKitty', { name: 'Kohi' });

/*
Invoke JanisKittyService-readme-GetKitty function with { name: 'Kohi' }
responseWithPayload = {
statusCode: 202,
payload: {
id: 61df4f545b95ddb21cc35628,
name: 'Kohi',
furColor: 'black',
likes: ['coffee', 'tuna'],
personality: lovely
}
}
*/

const failedInvocation = await Invoker.serviceSafeCall('kitty', 'GetKitty', { name: 'Redtail' });

/*
Invoke JanisKittyService-readme-GetKitty function with { name: 'Redtail' }
Expected Lambda response:
{
statusCode: 404,
payload: 'Unable to find kitty with name "Redtail"';
}
*/
```

#### :new: SERVICE-CLIENT-CALL

* `serviceClientCall(serviceCode, functionName, clientCode, payload)` (*async*) : Invoke a function from external service with a payload body and returns its response.
* `serviceCode` (*string*) **required**, JANIS Service code
* `functionName` (*string*) **required**, function name in TitleCase or dash-case
* `clientCode` (*string* or *array of strings*) **required**, client code
* `payload` (*object*), the data to send
* returns (*object*), with `statusCode` and `payload` fields
* throws (*LambdaError*), when the lambda response code is 400 or higher

```js
//
'use strict'

const { Invoker } = require('@janiscommerce/lambda');

const responseOnlyFunctionName = await Invoke.serviceClientCall('kitty', 'AwakeKitties', 'kittenMaster');

/*
Invoke JanisKittyService-readme-AwakeKitties function without payload
responseOnlyFunctionName = { statusCode: 202, payload: 'Kittens have been awakened, my master.' };
*/

const responseWithPayload = await Invoke.serviceClientCall('kitty', 'GetKitty', { name: 'Kohi' }, 'kittenMaster');

/*
Invoke JanisKittyService-readme-GetKitty function with { name: 'Kohi' }
responseWithPayload = {
statusCode: 202,
payload: {
id: 61df4f545b95ddb21cc35628,
name: 'Kohi',
furColor: 'black',
likes: ['coffee', 'tuna'],
personality: lovely
}
}
*/

const failedInvocation = await Invoker.serviceClientCall('kitty', 'GetKitty', { name: 'Redtail' }, 'kittenMaster');

/*
Invoke JanisKittyService-readme-GetKitty function with { name: 'Redtail' }
Expected Lambda response:
{
statusCode: 404,
payload: 'Unable to find kitty with name "Redtail"';
}
Caught error:
{
message: Failed to invoke function 'GetKitty' from service 'kitty': 'Unable to find kitty with name "Redtail"',
code: 18
}
*/
```

#### SERVICE-SAFE-CLIENT-CALL

* `serviceSafeCall(serviceCode, functionName, clientCode, payload)` (*async*) : Invoke a function from external service with a payload body and returns its response.
* `serviceCode` (*string*) **required**, JANIS Service code
* `functionName` (*string*) **required**, function name in TitleCase or dash-case
* `clientCode` (*string* or *array of strings*) **required**, client code
* `payload` (*object*), the data to send
* returns (*object*), with `statusCode` and `payload` fields

```js
//
'use strict'

const { Invoker } = require('@janiscommerce/lambda');

const responseOnlyFunctionName = await Invoke.serviceSafeClientCall('kitty', 'AwakeKitties','kittenMaster');

/*
Invoke JanisKittyService-readme-AwakeKitties function without payload
responseOnlyFunctionName = { statusCode: 202, payload: 'Kittens have been awakened, my master.' };
*/

const responseWithPayload = await Invoke.serviceSafeClientCall('kitty', 'GetKitty', { name: 'Kohi' }, 'kittenMaster');

/*
Invoke JanisKittyService-readme-GetKitty function with { name: 'Kohi' }
responseWithPayload = {
statusCode: 202,
payload: {
id: 61df4f545b95ddb21cc35628,
name: 'Kohi',
furColor: 'black',
likes: ['coffee', 'tuna'],
personality: lovely
}
}
*/

const failedInvocation = await Invoker.serviceSafeClientCall('kitty', 'GetKitty', { name: 'Redtail' }, 'kittenMaster');

/*
Invoke JanisKittyService-readme-GetKitty function with { name: 'Redtail' }
Expected Lambda response:
{
statusCode: 404,
payload: 'Unable to find kitty with name "Redtail"';
}
*/
```

#### Invoker-Errors

The Invokes are **async** so the rejections (throw Errors) while using `Invoker` could happen when the function doesn't have enough capacity to handle all incoming request in the queue (in AWS SNS services). Or in Local environment when the lambda-invoked failed (because serverless-offline management).
Expand All @@ -430,20 +685,28 @@ The errors of `Handler` and `Invoker` are informed with a `LambdaError`.
This object has a code that can be useful for debugging or error handling.
The codes are the following:

| Code | Description |
|------|-------------------------- |
| 1 | No Lambda |
| 2 | Invalid Lambda |
| 3 | No Client Found |
| 4 | Invalid Client |
| 5 | No Payload body is found |
| 6 | No Service Name is found |
| 7 | No Function Name is found |
| 8 | Invalid Function Name |
| 9 | Invalid Session |
| 10 | Invalid User |
| 11 | No user ID is found |
| 12 | No session is found |
| Code | Description |
|------|--------------------------------------------------------------|
| 1 | No Lambda |
| 2 | Invalid Lambda |
| 3 | No Client Found |
| 4 | Invalid Client |
| 5 | No Payload body is found |
| 6 | No Service Name is found |
| 7 | No Function Name is found |
| 8 | Invalid Function Name |
| 9 | Invalid Session |
| 10 | Invalid User |
| 11 | No user ID is found |
| 12 | No session is found |
| 13 | Invalid Task token |
| 14 | No Service Code is found |
| 15 | Invalid Service Code |
| 16 | Can't get Janis services Account IDs from AWS Secret Manager |
| 17 | Can't find Janis service's Account ID |
| 18 | Lambda invocation failed (responseCode 400 or higher) |
| 19 | Failed to assume Janis service IAM Role |
| 20 | Local Janis Service Ports not set in service settings |

Struct Error, AWS Errors are informed with their own Error Class.

Expand Down
47 changes: 47 additions & 0 deletions lib/helpers/aws-wrappers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use strict';

const STS = require('aws-sdk/clients/sts');
const Lambda = require('aws-sdk/clients/lambda');

class LambdaWrapper {

constructor(config = {}) {

/** @private */
this._lambda = new Lambda(config);
}

/* istanbul ignore next */
// AWS generates the Lambda class on the fly, the invoke method do not exists before creating the instance
/**
* @param {Lambda.InvocationRequest} params
* @returns {Promise<import('aws-sdk/clients/lambda').InvocationResponse}
*/
invoke(params) {
return this._lambda.invoke(params).promise();
}
}

class StsWrapper {

constructor(config) {

/** @private */
this._sts = new STS(config);
}

/* istanbul ignore next */
// AWS generates the STS class on the fly, the assumeRole method do not exists before creating the insance
/**
* @param {STS.AssumeRoleRequest} params
* @returns {Promise<import('aws-sdk/clients/sts').AssumeRoleResponse>}
*/
assumeRole(params) {
return this._sts.assumeRole(params).promise();
}
}

module.exports = {
Lambda: LambdaWrapper,
STS: StsWrapper
};
9 changes: 7 additions & 2 deletions lib/helpers/get-lambda-function-name.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const dashCaseToTitleCase = string => {
* @param {string} functionName In TitleCase or dash-case
* @returns {string}
*/
module.exports = functionName => {
module.exports = (functionName, serviceAccountId) => {

if(!process.env.JANIS_SERVICE_NAME)
throw new LambdaError('No Service Name is found', LambdaError.codes.NO_SERVICE);
Expand All @@ -40,5 +40,10 @@ module.exports = functionName => {
const env = process.env.JANIS_ENV;
const lambdaFunctionName = dashCaseToTitleCase(functionName);

return `${serviceName}-${env}-${lambdaFunctionName}`;
const formattedFunctionName = `${serviceName}-${env}-${lambdaFunctionName}`;

if(!serviceAccountId)
return formattedFunctionName;

return `${serviceAccountId}:function:${formattedFunctionName}`;
};
3 changes: 3 additions & 0 deletions lib/helpers/is-local-env.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
'use strict';

module.exports = process.env.JANIS_ENV === 'local';
Loading

0 comments on commit ac3d452

Please sign in to comment.