Skip to content

Commit 0d55ff1

Browse files
authored
feat: Runtime config validation (#1199)
Adds some basic config validation at runtime, which will help avoid problems in projects that are not using TypeScript. In particular, we will now throw an error if a dependency does not extend `DependencyAwareClass`. Not marking this as a separate breaking change because bad configs will already cause compile-time errors in TypeScript. Jira: [ENG-3207]
1 parent 87f765f commit 0d55ff1

4 files changed

Lines changed: 87 additions & 1 deletion

File tree

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,17 @@ export default class MyService extends DependencyAwareClass {
9696
}
9797
```
9898

99+
If you need to override the constructor, it must take a `DependencyInjection` instance and pass it to `super`.
100+
101+
```ts
102+
export default class MyService extends DependencyAwareClass {
103+
constructor(di: DependencyInjection) {
104+
super(di);
105+
// now do your other constructor stuff
106+
}
107+
}
108+
```
109+
99110
Then add it to your Lambda Wrapper configuration in the `dependencies` key.
100111

101112
```ts

docs/migration/v2.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ export default lambdaWrapper.wrap(async (di) => {
150150

151151
`get` will also always throw an error when used in a constructor to avoid surprises where other dependencies may be `undefined`. Instead of storing references to dependencies in class members, `get` them just before use.
152152

153+
A further breaking change in v2 is that all dependencies _must_ extend `DependencyAwareClass`. This is enforced at the type level and also at runtime, for those using plain JavaScript. Remember to add a call to `super` if you are overriding the constructor.
154+
153155
The `definitions` property has been removed.
154156

155157
The `getEvent`, `getContext` and `getConfiguration` methods have been deprecated and will be removed in a future major release. Use the `event`, `context` and `config` properties directly.

src/core/LambdaWrapper.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Context } from '../index';
44
import ResponseModel from '../models/ResponseModel';
55
import LoggerService from '../services/LoggerService';
66
import RequestService from '../services/RequestService';
7+
import DependencyAwareClass from './DependencyAwareClass';
78
import DependencyInjection from './DependencyInjection';
89
import { LambdaWrapperConfig, mergeConfig } from './config';
910

@@ -20,7 +21,29 @@ export interface WrapOptions {
2021
}
2122

2223
export default class LambdaWrapper<TConfig extends LambdaWrapperConfig = LambdaWrapperConfig> {
23-
constructor(readonly config: TConfig) {}
24+
constructor(readonly config: TConfig) {
25+
LambdaWrapper.validateConfig(config);
26+
}
27+
28+
/**
29+
* Validate the given config object.
30+
*
31+
* This is mainly to benefit projects that are not using TypeScript, where
32+
* missing properties or incorrect types would not otherwise be flagged up.
33+
*
34+
* @param config
35+
*/
36+
static validateConfig(config: LambdaWrapperConfig): void {
37+
if (!config.dependencies) {
38+
throw new TypeError("config is missing the 'dependencies' key");
39+
}
40+
41+
Object.values(config.dependencies).forEach((dep) => {
42+
if (!(dep.prototype instanceof DependencyAwareClass)) {
43+
throw new TypeError(`dependency '${dep.name}' does not extend DependencyAwareClass`);
44+
}
45+
});
46+
}
2447

2548
/**
2649
* Returns a new Lambda Wrapper with the given configuration applied.

tests/unit/core/LambdaWrapper.spec.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { RESPONSE_HEADERS } from '@/src/models/ResponseModel';
22

33
import {
4+
DependencyAwareClass,
45
DependencyInjection,
56
LambdaTermination,
67
LambdaWrapper,
@@ -33,6 +34,55 @@ describe('unit.core.LambdaWrapper', () => {
3334

3435
afterEach(() => jest.resetAllMocks());
3536

37+
describe('validateConfig', () => {
38+
describe('valid config', () => {
39+
([
40+
{
41+
name: 'empty',
42+
input: {
43+
dependencies: {},
44+
},
45+
},
46+
{
47+
name: 'good dependency',
48+
input: {
49+
dependencies: {
50+
Good: class Good extends DependencyAwareClass {},
51+
},
52+
},
53+
},
54+
{
55+
name: 'extra keys',
56+
input: {
57+
dependencies: {},
58+
sqs: {},
59+
extra: {},
60+
},
61+
},
62+
] as const).forEach(({ name, input }) => {
63+
it(`should pass on valid config: ${name}`, () => {
64+
expect(() => LambdaWrapper.validateConfig(input)).not.toThrow();
65+
});
66+
});
67+
});
68+
69+
describe('invalid config', () => {
70+
// these scenarios are prevented by TypeScript, but may happen in plain JS
71+
72+
it('should throw if config is missing dependencies', () => {
73+
expect(() => new LambdaWrapper({} as any)).toThrow(TypeError);
74+
});
75+
76+
it('should throw if a dependency does not extend DependencyAwareClass', () => {
77+
expect(() => new LambdaWrapper({
78+
dependencies: {
79+
Bad: class Bad {},
80+
},
81+
} as any)).toThrow(TypeError);
82+
});
83+
});
84+
});
85+
3686
describe('config', () => {
3787
it('should expose the config object', () => {
3888
const lw = new LambdaWrapper(config);

0 commit comments

Comments
 (0)