Skip to content

Commit

Permalink
feat: Runtime config validation (#1199)
Browse files Browse the repository at this point in the history
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]
  • Loading branch information
seb-cr authored Mar 28, 2024
1 parent 87f765f commit 0d55ff1
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 1 deletion.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,17 @@ export default class MyService extends DependencyAwareClass {
}
```

If you need to override the constructor, it must take a `DependencyInjection` instance and pass it to `super`.

```ts
export default class MyService extends DependencyAwareClass {
constructor(di: DependencyInjection) {
super(di);
// now do your other constructor stuff
}
}
```

Then add it to your Lambda Wrapper configuration in the `dependencies` key.

```ts
Expand Down
2 changes: 2 additions & 0 deletions docs/migration/v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ export default lambdaWrapper.wrap(async (di) => {

`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.

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.

The `definitions` property has been removed.

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.
Expand Down
25 changes: 24 additions & 1 deletion src/core/LambdaWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Context } from '../index';
import ResponseModel from '../models/ResponseModel';
import LoggerService from '../services/LoggerService';
import RequestService from '../services/RequestService';
import DependencyAwareClass from './DependencyAwareClass';
import DependencyInjection from './DependencyInjection';
import { LambdaWrapperConfig, mergeConfig } from './config';

Expand All @@ -20,7 +21,29 @@ export interface WrapOptions {
}

export default class LambdaWrapper<TConfig extends LambdaWrapperConfig = LambdaWrapperConfig> {
constructor(readonly config: TConfig) {}
constructor(readonly config: TConfig) {
LambdaWrapper.validateConfig(config);
}

/**
* Validate the given config object.
*
* This is mainly to benefit projects that are not using TypeScript, where
* missing properties or incorrect types would not otherwise be flagged up.
*
* @param config
*/
static validateConfig(config: LambdaWrapperConfig): void {
if (!config.dependencies) {
throw new TypeError("config is missing the 'dependencies' key");
}

Object.values(config.dependencies).forEach((dep) => {
if (!(dep.prototype instanceof DependencyAwareClass)) {
throw new TypeError(`dependency '${dep.name}' does not extend DependencyAwareClass`);
}
});
}

/**
* Returns a new Lambda Wrapper with the given configuration applied.
Expand Down
50 changes: 50 additions & 0 deletions tests/unit/core/LambdaWrapper.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { RESPONSE_HEADERS } from '@/src/models/ResponseModel';

import {
DependencyAwareClass,
DependencyInjection,
LambdaTermination,
LambdaWrapper,
Expand Down Expand Up @@ -33,6 +34,55 @@ describe('unit.core.LambdaWrapper', () => {

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

describe('validateConfig', () => {
describe('valid config', () => {
([
{
name: 'empty',
input: {
dependencies: {},
},
},
{
name: 'good dependency',
input: {
dependencies: {
Good: class Good extends DependencyAwareClass {},
},
},
},
{
name: 'extra keys',
input: {
dependencies: {},
sqs: {},
extra: {},
},
},
] as const).forEach(({ name, input }) => {
it(`should pass on valid config: ${name}`, () => {
expect(() => LambdaWrapper.validateConfig(input)).not.toThrow();
});
});
});

describe('invalid config', () => {
// these scenarios are prevented by TypeScript, but may happen in plain JS

it('should throw if config is missing dependencies', () => {
expect(() => new LambdaWrapper({} as any)).toThrow(TypeError);
});

it('should throw if a dependency does not extend DependencyAwareClass', () => {
expect(() => new LambdaWrapper({
dependencies: {
Bad: class Bad {},
},
} as any)).toThrow(TypeError);
});
});
});

describe('config', () => {
it('should expose the config object', () => {
const lw = new LambdaWrapper(config);
Expand Down

0 comments on commit 0d55ff1

Please sign in to comment.