@comicrelief/lambda-wrapper 1.10.1 → 2.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/README.md +172 -47
  2. package/dist/core/DependencyAwareClass.d.ts +15 -0
  3. package/dist/core/DependencyAwareClass.d.ts.map +1 -0
  4. package/dist/core/DependencyAwareClass.js +20 -0
  5. package/dist/core/DependencyAwareClass.js.map +1 -0
  6. package/dist/core/DependencyInjection.d.ts +62 -0
  7. package/dist/core/DependencyInjection.d.ts.map +1 -0
  8. package/dist/core/DependencyInjection.js +83 -0
  9. package/dist/core/DependencyInjection.js.map +1 -0
  10. package/dist/core/LambdaWrapper.d.ts +61 -0
  11. package/dist/core/LambdaWrapper.d.ts.map +1 -0
  12. package/dist/core/LambdaWrapper.js +126 -0
  13. package/dist/core/LambdaWrapper.js.map +1 -0
  14. package/dist/core/config.d.ts +20 -0
  15. package/dist/core/config.d.ts.map +1 -0
  16. package/dist/core/config.js +21 -0
  17. package/dist/core/config.js.map +1 -0
  18. package/dist/index.d.ts +28 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +59 -162
  21. package/dist/index.js.map +1 -0
  22. package/dist/models/ResponseModel.d.ts +91 -0
  23. package/dist/models/ResponseModel.d.ts.map +1 -0
  24. package/dist/models/ResponseModel.js +100 -0
  25. package/dist/models/ResponseModel.js.map +1 -0
  26. package/dist/models/SQSMessageModel.d.ts +46 -0
  27. package/dist/models/SQSMessageModel.d.ts.map +1 -0
  28. package/dist/models/SQSMessageModel.js +65 -0
  29. package/dist/models/SQSMessageModel.js.map +1 -0
  30. package/dist/models/StatusModel.d.ts +40 -0
  31. package/dist/models/StatusModel.d.ts.map +1 -0
  32. package/dist/models/StatusModel.js +50 -0
  33. package/dist/models/StatusModel.js.map +1 -0
  34. package/dist/services/BaseConfigService.d.ts +101 -0
  35. package/dist/services/BaseConfigService.d.ts.map +1 -0
  36. package/dist/services/BaseConfigService.js +180 -0
  37. package/dist/services/BaseConfigService.js.map +1 -0
  38. package/dist/services/HTTPService.d.ts +29 -0
  39. package/dist/services/HTTPService.d.ts.map +1 -0
  40. package/dist/services/HTTPService.js +55 -0
  41. package/dist/services/HTTPService.js.map +1 -0
  42. package/dist/services/LoggerService.d.ts +102 -0
  43. package/dist/services/LoggerService.d.ts.map +1 -0
  44. package/dist/services/LoggerService.js +258 -0
  45. package/dist/services/LoggerService.js.map +1 -0
  46. package/dist/services/RequestService.d.ts +119 -0
  47. package/dist/services/RequestService.d.ts.map +1 -0
  48. package/dist/services/RequestService.js +293 -0
  49. package/dist/services/RequestService.js.map +1 -0
  50. package/dist/services/SQSService.d.ts +181 -0
  51. package/dist/services/SQSService.d.ts.map +1 -0
  52. package/dist/services/SQSService.js +366 -0
  53. package/dist/services/SQSService.js.map +1 -0
  54. package/dist/services/TimerService.d.ts +22 -0
  55. package/dist/services/TimerService.d.ts.map +1 -0
  56. package/dist/services/TimerService.js +40 -0
  57. package/dist/services/TimerService.js.map +1 -0
  58. package/dist/utils/LambdaTermination.d.ts +14 -0
  59. package/dist/utils/LambdaTermination.d.ts.map +1 -0
  60. package/dist/utils/LambdaTermination.js +23 -0
  61. package/dist/utils/LambdaTermination.js.map +1 -0
  62. package/dist/utils/PromisifiedDelay.d.ts +17 -0
  63. package/dist/utils/PromisifiedDelay.d.ts.map +1 -0
  64. package/dist/utils/PromisifiedDelay.js +48 -0
  65. package/dist/utils/PromisifiedDelay.js.map +1 -0
  66. package/package.json +29 -19
  67. package/.eslintrc.yml +0 -16
  68. package/.github/dependabot.yml +0 -11
  69. package/.github/workflows/main.yml +0 -74
  70. package/babel.config.js +0 -13
  71. package/dist/Config/Dependencies.js +0 -40
  72. package/dist/DependencyInjection/DependencyAware.class.js +0 -47
  73. package/dist/DependencyInjection/DependencyInjection.class.js +0 -131
  74. package/dist/Model/CloudEvent.model.js +0 -156
  75. package/dist/Model/Model.model.js +0 -45
  76. package/dist/Model/Response.model.js +0 -150
  77. package/dist/Model/SQS/MarketingPreference.constraints.json +0 -28
  78. package/dist/Model/SQS/MarketingPreference.model.js +0 -641
  79. package/dist/Model/SQS/Message.model.js +0 -104
  80. package/dist/Model/Status.model.js +0 -82
  81. package/dist/Service/BaseConfig.service.js +0 -225
  82. package/dist/Service/HTTP.service.js +0 -70
  83. package/dist/Service/Logger.service.js +0 -293
  84. package/dist/Service/Request.service.js +0 -341
  85. package/dist/Service/SQS.service.js +0 -420
  86. package/dist/Service/Timer.service.js +0 -55
  87. package/dist/Wrapper/LambdaTermination.js +0 -39
  88. package/dist/Wrapper/LambdaWrapper.js +0 -164
  89. package/dist/Wrapper/PromisifiedDelay.js +0 -59
package/README.md CHANGED
@@ -4,95 +4,220 @@
4
4
  [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
5
5
  [![semantic-release](https://badge.fury.io/js/%40comicrelief%2Flambda-wrapper.svg)](https://www.npmjs.com/package/@comicrelief/lambda-wrapper)
6
6
 
7
- When writing Serverless endpoints, we have found ourselves replicating a lot of boiler plate code to do basic actions, such as reading request variables or writing to SQS. The aim of this package is to provide a wrapper for our Lambda functions, to provide some level of dependency and configuration injection and to reduce time spent on project setup.
7
+ When writing Serverless applications, we have found ourselves replicating a lot of boilerplate code to do basic actions, such as reading request data or sending messages to SQS. The aim of this package is to provide a wrapper for our Lambda functions, to provide some level of dependency and configuration injection and to reduce time spent on project setup.
8
8
 
9
- ## Installation & usage
9
+ If you're coming from v1 and updating to v2, check out the [v2 migration guide](docs/migration/v2.md).
10
10
 
11
- Install via npm:
11
+ ## Getting started
12
12
 
13
- ```bash
14
- npm install --save @comicrelief/lambda-wrapper
15
- ```
16
-
17
- Or via yarn:
13
+ Install via npm or Yarn:
18
14
 
19
15
  ```bash
16
+ npm i @comicrelief/lambda-wrapper
17
+ # or
20
18
  yarn add @comicrelief/lambda-wrapper
21
19
  ```
22
20
 
23
- You can then wrap your lambdas as follows.
21
+ You can then wrap your Lambda handler functions like this:
24
22
 
25
- ```js
26
- import {
27
- LambdaWrapper,
23
+ ```ts
24
+ // src/action/Hello.ts
25
+ import lambdaWrapper, {
28
26
  ResponseModel,
29
27
  RequestService,
30
28
  } from '@comicrelief/lambda-wrapper';
31
29
 
32
- export default LambdaWrapper({}, (di, request, done) => {
33
- const response = new ResponseModel({}, 200, `hello ${request.get('name', 'nobody')}`);
34
- done(null, response.generate());
30
+ export default lambdaWrapper.wrap(async (di) => {
31
+ const request = di.get(RequestService);
32
+ return ResponseModel.generate(
33
+ {},
34
+ 200,
35
+ `hello ${request.get('name', 'nobody')}`,
36
+ );
35
37
  });
36
38
  ```
37
39
 
38
- ## Serverless Offline & SQS Emulation
40
+ Here we've used the default export `lambdaWrapper` which is a preconfigured instance that can be used out of the box. You'll likely want to add your own dependencies and service config using the `configure` method:
39
41
 
40
- Serverless Offline only emulates API Gateway and Lambda, so publishing an SQS message would use the real SQS queue and trigger the consumer function (if any) in AWS. When working with offline code, you often want the local functions to be invoked instead.
42
+ ```ts
43
+ // src/config/LambdaWrapper.ts
44
+ import lambdaWrapper from '@comicrelief/lambda-wrapper';
41
45
 
42
- Offline SQS behaviour can be configured by setting the `LAMBDA_WRAPPER_OFFLINE_SQS_MODE` environment variable. Available modes are:
46
+ export default lambdaWrapper.configure({
47
+ // your config goes here
48
+ });
49
+ ```
43
50
 
44
- - `direct` (the default): invokes the consumer function directly via an offline Lambda endpoint
45
- - `local`: send messages to an offline SQS endpoint, such as Localstack
46
- - `aws`: no special handling of SQS offline; messages will be sent to AWS
51
+ `configure` returns a new Lambda Wrapper instance with the given configuration. You'll want to export it and then use this when wrapping your handler functions.
47
52
 
48
- Details of each mode are documented in the sections below. When you send a message using `SQSService.prototype.publish`, it will check which mode to use and dispatch the message appropriately. These modes take effect only when running offline (as defined by `DependencyInjection.prototype.isOffline`). In a deployed environment, SQS messages will always be sent to AWS SQS.
53
+ Read the next section to see what goes inside the config object!
49
54
 
50
- ### Direct Lambda mode
55
+ If you want to start from scratch without the built-in dependencies, you can use the `LambdaWrapper` constructor directly.
51
56
 
52
- This is the default mode if `LAMBDA_WRAPPER_OFFLINE_SQS_MODE` is not set. A Lambda client will be created and the message will be delivered to the offline Lambda endpoint, effectively running the consumer function _immediately_ as part of the original Lambda invocation. This works very well in the offline environment because invoking a Lambda function will trigger its whole (local) execution tree.
57
+ ```ts
58
+ // src/config/LambdaWrapper.ts
59
+ import { LambdaWrapper } from '@comicrelief/lambda-wrapper';
53
60
 
54
- To take advantage of SQS emulation, you will need to define the following in the implementing service:
61
+ export default new LambdaWrapper({
62
+ // your config goes here
63
+ });
64
+ ```
55
65
 
56
- **QUEUE_CONSUMERS**
66
+ ## Dependencies
57
67
 
58
- In your `src/Config/Configuration` define a `QUEUE_CONSUMERS` object. `QUEUE_CONSUMERS` will map the queue name to the fully qualified `FunctionName` that we want to trigger when messages are published to that queue.
68
+ Lambda Wrapper comes with some commonly used dependencies built in:
59
69
 
60
- You will need to export `QUEUE_CONSUMERS` as part of your default export, alongside `DEFINITIONS`, `DEPENDENCIES`, `QUEUES`, `QUEUE_DEFINITIONS`, etc.
70
+ - [HTTPService](docs/services/HTTPService.md)
71
+ - [LoggerService](docs/services/LoggerService.md)
72
+ - [RequestService](docs/services/RequestService.md)
73
+ - [SQSService](docs/services/SQSService.md)
74
+ - [TimerService](docs/services/TimerService.md)
61
75
 
62
- A `Configuration` example can be found in the `serverless-prize-platform` repository [here](https://github.com/comicrelief/serverless-prize-platform/blob/master/src/Config/Configuration.js).
76
+ Access these via dependency injection. You've already seen an example of this where we got `RequestService`. Pass the dependency class to `di.get()` to get its instance:
63
77
 
64
- **process.env.SERVICE_LAMBDA_URL**
78
+ ```ts
79
+ export default lambdaWrapper.wrap(async (di) => {
80
+ const request = di.get(RequestService);
81
+ const sqs = di.get(SQSService);
82
+ // ...
83
+ });
84
+ ```
65
85
 
66
- While creating the Lambda client, we need to point it to our offline environment. LambdaWrapper will take care of the specifics, but it will need to know the Lambda endpoint URL. This _can_ and _must_ be specified via the `SERVICE_LAMBDA_URL` environment variable.
86
+ To add your own dependencies, first extend `DependencyAwareClass`.
67
87
 
68
- The URL is likely to be your localhost URL and the next available port from the offline API Gateway. So, if you are running Serverless Offline on `http://localhost:3001`, the Lambda URL is likely to be `http://localhost:3002`. You can check the port in the output during Serverless Offline startup by looking for the following line:
88
+ ```ts
89
+ // src/services/MyService.ts
90
+ import { DependencyAwareClass } from '@comicrelief/lambda-wrapper';
69
91
 
70
- offline: Offline [http for lambda] listening on http://localhost:3002
92
+ export default class MyService extends DependencyAwareClass {
93
+ doSomething() {
94
+ // ...
95
+ }
96
+ }
97
+ ```
71
98
 
72
- #### Caveats
99
+ Then add it to your Lambda Wrapper configuration in the `dependencies` key.
73
100
 
74
- 1. You will be running the SQS-triggered lambdas in the same Serverless Offline context as your triggering lambda. Expect logs from both lambdas in the Serverless Offline output.
101
+ ```ts
102
+ // src/config/LambdaWrapper.ts
103
+ import lambdaWrapper from '@comicrelief/lambda-wrapper';
75
104
 
76
- 2. If you await `sqs.publish` you will effectively wait until all SQS-triggered lambdas (and possibly their own SQS-triggered lambdas) have all completed. This is necessary to avoid any pending execution (i.e. the lambda terminating before its async processes are completed).
105
+ import MyService from '@/src/services/MyService';
77
106
 
78
- 3. If the triggered lambda incurs an exception, this will be propagated upstream, effectively killing the execution of the calling lambda.
107
+ export default lambdaWrapper.configure({
108
+ dependencies: {
109
+ MyService,
110
+ },
111
+ });
112
+ ```
113
+
114
+ Now you can use it inside your handler functions and other dependencies!
115
+
116
+ ```ts
117
+ // src/action/DoSomething.ts
118
+ import lambdaWrapper from '@/src/config/LambdaWrapper';
119
+ import MyService from '@/src/services/MyService';
120
+
121
+ export default lambdaWrapper.wrap(async (di) => {
122
+ di.get(MyService).doSomething();
123
+ });
124
+ ```
125
+
126
+ ## Service config
127
+
128
+ Some dependencies need their own config. This goes in per-service keys within your Lambda Wrapper config. For an example, see [SQSService](docs/services/SQSService.md) which uses the `sqs` key.
129
+
130
+ ```ts
131
+ export default lambdaWrapper.configure({
132
+ dependencies: {
133
+ // your dependencies
134
+ },
135
+ sqs: {
136
+ // your SQSService config
137
+ },
138
+ // ... other configs ...
139
+ });
140
+ ```
141
+
142
+ To use config with your own dependencies, you need to do three things:
143
+
144
+ 1. Define the key and type of your config object.
145
+
146
+ Using `SQSService` as an example, we have the `sqs` key which has the `SQSServiceConfig` type:
147
+
148
+ ```ts
149
+ export interface SQSServiceConfig {
150
+ queues?: Record<string, string>;
151
+ queueConsumers?: Record<string, string>;
152
+ }
153
+ ```
154
+
155
+ 2. Define a type that can be applied to a Lambda Wrapper config.
156
+
157
+ This simply combines the key and type defined in step 1. Conventionally we name these `With...` types.
158
+
159
+ ```ts
160
+ export interface WithSQSServiceConfig {
161
+ sqs?: SQSServiceConfig;
162
+ }
163
+ ```
164
+
165
+ In the case of `SQSService`, the `sqs` key is optional because this dependency is included by default and not all applications need it. If your dependency requires config in order to work, you can make this a required key.
166
+
167
+ 3. In your dependency constructor, cast the config to this type.
168
+
169
+ ```ts
170
+ export default class SQSService extends DependencyAwareClass {
171
+ constructor(di: DependencyInjection) {
172
+ super(di);
173
+
174
+ const config = (this.di.config as WithSQSServiceConfig).sqs;
175
+ // Bear in mind that because the `sqs` key is optional, the type of
176
+ // `config` will be `SQSServiceConfig | undefined`. Take care when
177
+ // accessing its properties! You can use optional chaining:
178
+ const queues = config?.queues || {};
179
+ // ...
180
+ }
181
+ }
182
+ ```
183
+
184
+ When you go to configure your Lambda Wrapper, you can now include your dependency's config type in the generic for `configure` to get IntelliSense completions and type checking for your config keys.
185
+
186
+ ```ts
187
+ lambdaWrapper.configure<WithSQSServiceConfig>({
188
+ sqs: {
189
+ queues: 42 // Oops! This will be flaggeed as a type error by TypeScript
190
+ },
191
+ });
192
+ ```
193
+
194
+ You can combine types for multiple dependencies if needed using `&`:
195
+
196
+ ```ts
197
+ lambdaWrapper.configure<WithSQSServiceConfig & WithOtherServiceConfig>({
198
+ sqs: {
199
+ // SQSService config
200
+ },
201
+ other: {
202
+ // OtherService config
203
+ },
204
+ });
205
+ ```
79
206
 
80
- ### Local SQS mode
207
+ ## Development
81
208
 
82
- Use this mode by setting `LAMBDA_WRAPPER_OFFLINE_SQS_MODE=local`. Messages will still be sent to an SQS queue, but using a locally simulated version instead of AWS. This allows you to test your service using a tool like Localstack.
209
+ ### Testing
83
210
 
84
- By default, messages will be sent to a SQS service running on `localhost:4576`. If you need to change the hostname, you can set `process.env.LAMBDA_WRAPPER_OFFLINE_SQS_HOST`.
85
- Also, if you need to change the port, you can set `process.env.LAMBDA_WRAPPER_OFFLINE_SQS_PORT`.
211
+ Run `yarn test` to run the unit tests.
86
212
 
87
- ### AWS SQS mode
213
+ When writing a bugfix, start by writing a test that reproduces the problem. It should fail with the current version of Lambda Wrapper, and pass once you've implemented the fix.
88
214
 
89
- Use this mode by setting `LAMBDA_WRAPPER_OFFLINE_SQS_MODE=aws`. Messages will be sent to the real queue in AWS. This mode is useful when a queue is consumed by an external service, rather than another function in the service under test.
215
+ When adding a feature, ensure it's covered by tests that adequately define its behaviour.
90
216
 
91
- In order for queue URLs to be correctly constructed, you must either:
217
+ ### Linting
92
218
 
93
- - set `AWS_ACCOUNT_ID` to the account ID that hosts your queue; or
94
- - invoke offline functions via the Lambda API, passing a context that contains a realistic `invokedFunctionArn` including the account ID.
219
+ Run `yarn lint` to check code style complies to our standard. Many problems can be auto-fixed using `yarn lint --fix`.
95
220
 
96
- ## Semantic release
221
+ ### Releases
97
222
 
98
223
  Release management is automated using [semantic-release](https://www.npmjs.com/package/semantic-release).
@@ -0,0 +1,15 @@
1
+ import DependencyInjection from './DependencyInjection';
2
+ /**
3
+ * Base class for dependencies.
4
+ */
5
+ export default class DependencyAwareClass {
6
+ readonly di: DependencyInjection;
7
+ constructor(di: DependencyInjection);
8
+ /**
9
+ * Get dependency injection container.
10
+ *
11
+ * @deprecated Use `this.di` instead.
12
+ */
13
+ getContainer(): DependencyInjection;
14
+ }
15
+ //# sourceMappingURL=DependencyAwareClass.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DependencyAwareClass.d.ts","sourceRoot":"","sources":["../../src/core/DependencyAwareClass.ts"],"names":[],"mappings":"AAAA,OAAO,mBAAmB,MAAM,uBAAuB,CAAC;AAExD;;GAEG;AACH,MAAM,CAAC,OAAO,OAAO,oBAAoB;IAC3B,QAAQ,CAAC,EAAE,EAAE,mBAAmB;gBAAvB,EAAE,EAAE,mBAAmB;IAE5C;;;;OAIG;IACH,YAAY,IAAI,mBAAmB;CAGpC"}
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * Base class for dependencies.
5
+ */
6
+ class DependencyAwareClass {
7
+ constructor(di) {
8
+ this.di = di;
9
+ }
10
+ /**
11
+ * Get dependency injection container.
12
+ *
13
+ * @deprecated Use `this.di` instead.
14
+ */
15
+ getContainer() {
16
+ return this.di;
17
+ }
18
+ }
19
+ exports.default = DependencyAwareClass;
20
+ //# sourceMappingURL=DependencyAwareClass.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DependencyAwareClass.js","sourceRoot":"","sources":["../../src/core/DependencyAwareClass.ts"],"names":[],"mappings":";;AAEA;;GAEG;AACH,MAAqB,oBAAoB;IACvC,YAAqB,EAAuB;QAAvB,OAAE,GAAF,EAAE,CAAqB;IAAG,CAAC;IAEhD;;;;OAIG;IACH,YAAY;QACV,OAAO,IAAI,CAAC,EAAE,CAAC;IACjB,CAAC;CACF;AAXD,uCAWC"}
@@ -0,0 +1,62 @@
1
+ import { Context } from 'aws-lambda';
2
+ import DependencyAwareClass from './DependencyAwareClass';
3
+ import { LambdaWrapperConfig } from './config';
4
+ declare type Class<T> = new (di: DependencyInjection) => T;
5
+ /**
6
+ * Dependency injection container.
7
+ *
8
+ * Dependencies (singleton instances of dependency-aware classes) are provided
9
+ * to the main Lambda handler and other dependencies via this class.
10
+ */
11
+ export default class DependencyInjection {
12
+ readonly config: LambdaWrapperConfig;
13
+ readonly event: any;
14
+ readonly context: Context;
15
+ /**
16
+ * Instantiated dependencies.
17
+ */
18
+ readonly dependencies: Record<string, DependencyAwareClass>;
19
+ /**
20
+ * True until all dependencies have been constructed.
21
+ */
22
+ private isConstructing;
23
+ constructor(config: LambdaWrapperConfig, event: any, context: Context);
24
+ /**
25
+ * Get the singleton instance of the given dependency.
26
+ *
27
+ * @param dependency
28
+ */
29
+ get<T extends DependencyAwareClass>(dependency: Class<T>): T;
30
+ /**
31
+ * Get the event passed to AWS Lambda.
32
+ *
33
+ * @deprecated Use `di.event` instead.
34
+ */
35
+ getEvent(): any;
36
+ /**
37
+ * Get the AWS Lambda context object.
38
+ *
39
+ * @deprecated Use `di.context` instead.
40
+ */
41
+ getContext(): Context;
42
+ /**
43
+ * Get Lambda Wrapper configuration.
44
+ *
45
+ * @deprecated Use `di.config` instead.
46
+ */
47
+ getConfiguration(): LambdaWrapperConfig;
48
+ /**
49
+ * True if the function is being executed in `serverless-offline`.
50
+ *
51
+ * We use the following checks for this:
52
+ *
53
+ * - if there is no function ARN, or the ARN includes 'offline'
54
+ * - if `process.env.USE_SERVERLESS_OFFLINE` is set
55
+ *
56
+ * TODO: This is nothing to do with dependency injection and should be moved
57
+ * somewhere else! Any ideas?
58
+ */
59
+ get isOffline(): boolean;
60
+ }
61
+ export {};
62
+ //# sourceMappingURL=DependencyInjection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DependencyInjection.d.ts","sourceRoot":"","sources":["../../src/core/DependencyInjection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAErC,OAAO,oBAAoB,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAG/C,aAAK,KAAK,CAAC,CAAC,IAAI,KAAK,EAAE,EAAE,mBAAmB,KAAK,CAAC,CAAC;AAEnD;;;;;GAKG;AACH,MAAM,CAAC,OAAO,OAAO,mBAAmB;IAYpC,QAAQ,CAAC,MAAM,EAAE,mBAAmB;IACpC,QAAQ,CAAC,KAAK,EAAE,GAAG;IACnB,QAAQ,CAAC,OAAO,EAAE,OAAO;IAb3B;;OAEG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC;IAE5D;;OAEG;IACH,OAAO,CAAC,cAAc,CAAQ;gBAGnB,MAAM,EAAE,mBAAmB,EAC3B,KAAK,EAAE,GAAG,EACV,OAAO,EAAE,OAAO;IAU3B;;;;OAIG;IACH,GAAG,CAAC,CAAC,SAAS,oBAAoB,EAAE,UAAU,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC;IAsB5D;;;;OAIG;IACH,QAAQ;IAIR;;;;OAIG;IACH,UAAU;IAIV;;;;OAIG;IACH,gBAAgB;IAIhB;;;;;;;;;;OAUG;IACH,IAAI,SAAS,IAAI,OAAO,CAIvB;CACF"}
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * Dependency injection container.
5
+ *
6
+ * Dependencies (singleton instances of dependency-aware classes) are provided
7
+ * to the main Lambda handler and other dependencies via this class.
8
+ */
9
+ class DependencyInjection {
10
+ constructor(config, event, context) {
11
+ this.config = config;
12
+ this.event = event;
13
+ this.context = context;
14
+ /**
15
+ * True until all dependencies have been constructed.
16
+ */
17
+ this.isConstructing = true;
18
+ const classes = Object.values(config.dependencies);
19
+ this.dependencies = Object.fromEntries(classes.map((Constructor) => [Constructor.name, new Constructor(this)]));
20
+ this.isConstructing = false;
21
+ }
22
+ /**
23
+ * Get the singleton instance of the given dependency.
24
+ *
25
+ * @param dependency
26
+ */
27
+ get(dependency) {
28
+ if (this.isConstructing) {
29
+ throw new Error('Dependencies are not available in dependency class constructors.\n\n'
30
+ + 'To fix this, call `di.get` in the function where the dependency is'
31
+ + 'used instead of inside your constructor.');
32
+ }
33
+ const name = dependency.name;
34
+ if (!this.dependencies[name]) {
35
+ throw new Error(`${name} does not exist in dependency container\n\n`
36
+ + `Make sure you've included ${name} in the 'dependencies' key of your `
37
+ + 'Lambda Wrapper config.');
38
+ }
39
+ return this.dependencies[name];
40
+ }
41
+ /**
42
+ * Get the event passed to AWS Lambda.
43
+ *
44
+ * @deprecated Use `di.event` instead.
45
+ */
46
+ getEvent() {
47
+ return this.event;
48
+ }
49
+ /**
50
+ * Get the AWS Lambda context object.
51
+ *
52
+ * @deprecated Use `di.context` instead.
53
+ */
54
+ getContext() {
55
+ return this.context;
56
+ }
57
+ /**
58
+ * Get Lambda Wrapper configuration.
59
+ *
60
+ * @deprecated Use `di.config` instead.
61
+ */
62
+ getConfiguration() {
63
+ return this.config;
64
+ }
65
+ /**
66
+ * True if the function is being executed in `serverless-offline`.
67
+ *
68
+ * We use the following checks for this:
69
+ *
70
+ * - if there is no function ARN, or the ARN includes 'offline'
71
+ * - if `process.env.USE_SERVERLESS_OFFLINE` is set
72
+ *
73
+ * TODO: This is nothing to do with dependency injection and should be moved
74
+ * somewhere else! Any ideas?
75
+ */
76
+ get isOffline() {
77
+ return !this.context.invokedFunctionArn
78
+ || this.context.invokedFunctionArn.includes('offline')
79
+ || !!process.env.USE_SERVERLESS_OFFLINE;
80
+ }
81
+ }
82
+ exports.default = DependencyInjection;
83
+ //# sourceMappingURL=DependencyInjection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DependencyInjection.js","sourceRoot":"","sources":["../../src/core/DependencyInjection.ts"],"names":[],"mappings":";;AAQA;;;;;GAKG;AACH,MAAqB,mBAAmB;IAWtC,YACW,MAA2B,EAC3B,KAAU,EACV,OAAgB;QAFhB,WAAM,GAAN,MAAM,CAAqB;QAC3B,UAAK,GAAL,KAAK,CAAK;QACV,YAAO,GAAP,OAAO,CAAS;QAR3B;;WAEG;QACK,mBAAc,GAAG,IAAI,CAAC;QAO5B,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;QACnD,IAAI,CAAC,YAAY,GAAG,MAAM,CAAC,WAAW,CACpC,OAAO,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,WAAW,CAAC,IAAI,EAAE,IAAI,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CACxE,CAAC;QAEF,IAAI,CAAC,cAAc,GAAG,KAAK,CAAC;IAC9B,CAAC;IAED;;;;OAIG;IACH,GAAG,CAAiC,UAAoB;QACtD,IAAI,IAAI,CAAC,cAAc,EAAE;YACvB,MAAM,IAAI,KAAK,CACb,sEAAsE;kBACpE,oEAAoE;kBACpE,0CAA0C,CAC7C,CAAC;SACH;QAED,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC;QAE7B,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE;YAC5B,MAAM,IAAI,KAAK,CACb,GAAG,IAAI,6CAA6C;kBAClD,6BAA6B,IAAI,qCAAqC;kBACtE,wBAAwB,CAC3B,CAAC;SACH;QAED,OAAO,IAAI,CAAC,YAAY,CAAC,IAAI,CAAM,CAAC;IACtC,CAAC;IAED;;;;OAIG;IACH,QAAQ;QACN,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IAED;;;;OAIG;IACH,UAAU;QACR,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IAED;;;;OAIG;IACH,gBAAgB;QACd,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAED;;;;;;;;;;OAUG;IACH,IAAI,SAAS;QACX,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,kBAAkB;eAClC,IAAI,CAAC,OAAO,CAAC,kBAAkB,CAAC,QAAQ,CAAC,SAAS,CAAC;eACnD,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC;IAC5C,CAAC;CACF;AA9FD,sCA8FC"}
@@ -0,0 +1,61 @@
1
+ import { Context } from '../index';
2
+ import DependencyInjection from './DependencyInjection';
3
+ import { LambdaWrapperConfig } from './config';
4
+ export interface WrapOptions {
5
+ /**
6
+ * Whether uncaught errors should be handled to return an HTTP 500 response
7
+ * instead of causing a function error. (default: `true`)
8
+ *
9
+ * This is what you usually want when working on an HTTP endpoint, but in
10
+ * other contexts (e.g. queue consumers) you may want AWS Lambda to report a
11
+ * failure so that the event is retried.
12
+ */
13
+ handleUncaughtErrors?: boolean;
14
+ }
15
+ export default class LambdaWrapper<TConfig extends LambdaWrapperConfig = LambdaWrapperConfig> {
16
+ readonly config: TConfig;
17
+ constructor(config: TConfig);
18
+ /**
19
+ * Returns a new Lambda Wrapper with the given configuration applied.
20
+ *
21
+ * @param config
22
+ */
23
+ configure<TMoreConfig>(config: Partial<TConfig> & TMoreConfig): LambdaWrapper<TConfig & TMoreConfig>;
24
+ /**
25
+ * Wrap the given function.
26
+ */
27
+ wrap<T>(handler: (di: DependencyInjection) => Promise<T>, options?: WrapOptions): (event: any, context: Context) => Promise<any>;
28
+ /**
29
+ * Process the result once we have one.
30
+ *
31
+ * @param di
32
+ * @param result
33
+ */
34
+ static handleSuccess(di: DependencyInjection, result: any): any;
35
+ /**
36
+ * Gracefully handles an error, logging in Epsagon and generating a response
37
+ * reflecting the `code` of the error, if defined.
38
+ *
39
+ * Note about Epsagon:
40
+ * Epsagon generates alerts for logs on level ERROR. This means that
41
+ * `logger.error` will produce an alert. To avoid meaningless notifications,
42
+ * most likely coming from tests, we log INFO unless either:
43
+ *
44
+ * 1. `error.raiseOnEpsagon` is defined & truthy
45
+ * 2. `error.code` is defined and `error.code >= 500`.
46
+ *
47
+ * @param di
48
+ * @param error
49
+ * @param [throwError=false]
50
+ */
51
+ static handleError(di: DependencyInjection, error: Error, throwError?: boolean): {
52
+ statusCode: any;
53
+ headers: {
54
+ 'Content-Type': string;
55
+ 'Access-Control-Allow-Origin': string;
56
+ 'Access-Control-Allow-Credentials': boolean;
57
+ };
58
+ body: string;
59
+ } | Error;
60
+ }
61
+ //# sourceMappingURL=LambdaWrapper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LambdaWrapper.d.ts","sourceRoot":"","sources":["../../src/core/LambdaWrapper.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAInC,OAAO,mBAAmB,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAe,MAAM,UAAU,CAAC;AAE5D,MAAM,WAAW,WAAW;IAC1B;;;;;;;OAOG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED,MAAM,CAAC,OAAO,OAAO,aAAa,CAAC,OAAO,SAAS,mBAAmB,GAAG,mBAAmB;IAC9E,QAAQ,CAAC,MAAM,EAAE,OAAO;gBAAf,MAAM,EAAE,OAAO;IAEpC;;;;OAIG;IACH,SAAS,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,WAAW,GAAG,aAAa,CAAC,OAAO,GAAG,WAAW,CAAC;IAIpG;;OAEG;IACH,IAAI,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,EAAE,mBAAmB,KAAK,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,WAAW,WAKjD,GAAG,WAAW,OAAO;IAyDnD;;;;;OAKG;IACH,MAAM,CAAC,aAAa,CAAC,EAAE,EAAE,mBAAmB,EAAE,MAAM,EAAE,GAAG;IASzD;;;;;;;;;;;;;;;OAeG;IACH,MAAM,CAAC,WAAW,CAAC,EAAE,EAAE,mBAAmB,EAAE,KAAK,EAAE,KAAK,EAAE,UAAU,UAAQ;;;;;;;;;CA8B7E"}
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const epsagon_1 = __importDefault(require("epsagon"));
7
+ const ResponseModel_1 = __importDefault(require("../models/ResponseModel"));
8
+ const LoggerService_1 = __importDefault(require("../services/LoggerService"));
9
+ const RequestService_1 = __importDefault(require("../services/RequestService"));
10
+ const DependencyInjection_1 = __importDefault(require("./DependencyInjection"));
11
+ const config_1 = require("./config");
12
+ class LambdaWrapper {
13
+ constructor(config) {
14
+ this.config = config;
15
+ }
16
+ /**
17
+ * Returns a new Lambda Wrapper with the given configuration applied.
18
+ *
19
+ * @param config
20
+ */
21
+ configure(config) {
22
+ return new LambdaWrapper((0, config_1.mergeConfig)(this.config, config));
23
+ }
24
+ /**
25
+ * Wrap the given function.
26
+ */
27
+ wrap(handler, options) {
28
+ const { handleUncaughtErrors = true, } = options || {};
29
+ let wrapper = async (event, context) => {
30
+ const di = new DependencyInjection_1.default(this.config, event, context);
31
+ const request = di.get(RequestService_1.default);
32
+ const logger = di.get(LoggerService_1.default);
33
+ context.callbackWaitsForEmptyEventLoop = false;
34
+ // if the event is a warmup, don't bother running the function
35
+ if (event.source === 'serverless-plugin-warmup') {
36
+ return 'Lambda is warm!';
37
+ }
38
+ // log the user's IP address silently for use in error tracing
39
+ const ipAddress = request.getIp();
40
+ if (ipAddress) {
41
+ logger.metric('ipAddress', ipAddress, true);
42
+ }
43
+ // add metrics with user browser information for rapid debugging
44
+ const userBrowserAndDevice = request.getUserBrowserAndDevice();
45
+ if (userBrowserAndDevice) {
46
+ Object.entries(userBrowserAndDevice).forEach(([key, value]) => {
47
+ logger.metric(key, value, true);
48
+ });
49
+ }
50
+ try {
51
+ const result = await handler.call(wrapper, di);
52
+ return LambdaWrapper.handleSuccess(di, result);
53
+ }
54
+ catch (error) {
55
+ const handled = LambdaWrapper.handleError(di, error, !handleUncaughtErrors);
56
+ if (!handleUncaughtErrors) {
57
+ // AWS Lambda with async handler is looking for a rejection
58
+ // and not an error object directly
59
+ // and will treat resolved errors as successful
60
+ // as it will cast the error to JSON, i.e. `{}`
61
+ throw handled;
62
+ }
63
+ return handled;
64
+ }
65
+ };
66
+ // If Epsagon is enabled, wrap the instance in the Epsagon wrapper
67
+ if (process.env.EPSAGON_TOKEN && process.env.EPSAGON_SERVICE_NAME) {
68
+ epsagon_1.default.init({
69
+ token: process.env.EPSAGON_TOKEN,
70
+ appName: process.env.EPSAGON_SERVICE_NAME,
71
+ });
72
+ wrapper = epsagon_1.default.lambdaWrapper(wrapper);
73
+ }
74
+ return wrapper;
75
+ }
76
+ /**
77
+ * Process the result once we have one.
78
+ *
79
+ * @param di
80
+ * @param result
81
+ */
82
+ static handleSuccess(di, result) {
83
+ const logger = di.get(LoggerService_1.default);
84
+ // result may be undefined as not all lambdas have a return value
85
+ logger.metric('lambda.statusCode', result?.statusCode || 200);
86
+ return result;
87
+ }
88
+ /**
89
+ * Gracefully handles an error, logging in Epsagon and generating a response
90
+ * reflecting the `code` of the error, if defined.
91
+ *
92
+ * Note about Epsagon:
93
+ * Epsagon generates alerts for logs on level ERROR. This means that
94
+ * `logger.error` will produce an alert. To avoid meaningless notifications,
95
+ * most likely coming from tests, we log INFO unless either:
96
+ *
97
+ * 1. `error.raiseOnEpsagon` is defined & truthy
98
+ * 2. `error.code` is defined and `error.code >= 500`.
99
+ *
100
+ * @param di
101
+ * @param error
102
+ * @param [throwError=false]
103
+ */
104
+ static handleError(di, error, throwError = false) {
105
+ const logger = di.get(LoggerService_1.default);
106
+ const { code, raiseOnEpsagon, body = {}, details = 'unknown error', } = error;
107
+ logger.metric('lambda.statusCode', code || 500);
108
+ if (raiseOnEpsagon || !code || code >= 500) {
109
+ logger.error(error);
110
+ }
111
+ else {
112
+ logger.info(error);
113
+ }
114
+ if (throwError) {
115
+ if (error instanceof Error) {
116
+ return error;
117
+ }
118
+ // We want to be absolutely sure that we are returning an error, as
119
+ // Lambda sync handlers will only fail if the object is instanceof Error
120
+ return new Error(error);
121
+ }
122
+ return ResponseModel_1.default.generate(body, code || 500, details);
123
+ }
124
+ }
125
+ exports.default = LambdaWrapper;
126
+ //# sourceMappingURL=LambdaWrapper.js.map