diff --git a/.eslintrc.yml b/.eslintrc.yml index 1a98544e..794163b6 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -1,10 +1,8 @@ root: true extends: - - '@comicrelief/eslint-config' - - '@comicrelief/eslint-config/mixins/jsdoc' - -parser: '@babel/eslint-parser' + - '@comicrelief/eslint-config/mixins/base' + - '@comicrelief/eslint-config/mixins/ts' ignorePatterns: - node_modules @@ -14,3 +12,4 @@ ignorePatterns: rules: unicorn/prefer-node-protocol: off + '@typescript-eslint/no-explicit-any': off diff --git a/.nycrc.yml b/.nycrc.yml new file mode 100644 index 00000000..8fb1de64 --- /dev/null +++ b/.nycrc.yml @@ -0,0 +1,6 @@ +extends: '@istanbuljs/nyc-config-typescript' + +all: true +check-coverage: true +include: + - src/** diff --git a/README.md b/README.md index fab1d61f..2bd7288a 100644 --- a/README.md +++ b/README.md @@ -4,95 +4,220 @@ [![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) [![semantic-release](https://badge.fury.io/js/%40comicrelief%2Flambda-wrapper.svg)](https://www.npmjs.com/package/@comicrelief/lambda-wrapper) -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. +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. -## Installation & usage +If you're coming from v1 and updating to v2, check out the [v2 migration guide](docs/migration/v2.md). -Install via npm: +## Getting started -```bash -npm install --save @comicrelief/lambda-wrapper -``` - -Or via yarn: +Install via npm or Yarn: ```bash +npm i @comicrelief/lambda-wrapper +# or yarn add @comicrelief/lambda-wrapper ``` -You can then wrap your lambdas as follows. +You can then wrap your Lambda handler functions like this: -```js -import { - LambdaWrapper, +```ts +// src/action/Hello.ts +import lambdaWrapper, { ResponseModel, RequestService, } from '@comicrelief/lambda-wrapper'; -export default LambdaWrapper({}, (di, request, done) => { - const response = new ResponseModel({}, 200, `hello ${request.get('name', 'nobody')}`); - done(null, response.generate()); +export default lambdaWrapper.wrap(async (di) => { + const request = di.get(RequestService); + return ResponseModel.generate( + {}, + 200, + `hello ${request.get('name', 'nobody')}`, + ); }); ``` -## Serverless Offline & SQS Emulation +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: -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. +```ts +// src/config/LambdaWrapper.ts +import lambdaWrapper from '@comicrelief/lambda-wrapper'; -Offline SQS behaviour can be configured by setting the `LAMBDA_WRAPPER_OFFLINE_SQS_MODE` environment variable. Available modes are: +export default lambdaWrapper.configure({ + // your config goes here +}); +``` -- `direct` (the default): invokes the consumer function directly via an offline Lambda endpoint -- `local`: send messages to an offline SQS endpoint, such as Localstack -- `aws`: no special handling of SQS offline; messages will be sent to AWS +`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. -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. +Read the next section to see what goes inside the config object! -### Direct Lambda mode +If you want to start from scratch without the built-in dependencies, you can use the `LambdaWrapper` constructor directly. -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. +```ts +// src/config/LambdaWrapper.ts +import { LambdaWrapper } from '@comicrelief/lambda-wrapper'; -To take advantage of SQS emulation, you will need to define the following in the implementing service: +export default new LambdaWrapper({ + // your config goes here +}); +``` -**QUEUE_CONSUMERS** +## Dependencies -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. +Lambda Wrapper comes with some commonly used dependencies built in: -You will need to export `QUEUE_CONSUMERS` as part of your default export, alongside `DEFINITIONS`, `DEPENDENCIES`, `QUEUES`, `QUEUE_DEFINITIONS`, etc. +- [HTTPService](docs/services/HTTPService.md) +- [LoggerService](docs/services/LoggerService.md) +- [RequestService](docs/services/RequestService.md) +- [SQSService](docs/services/SQSService.md) +- [TimerService](docs/services/TimerService.md) -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). +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: -**process.env.SERVICE_LAMBDA_URL** +```ts +export default lambdaWrapper.wrap(async (di) => { + const request = di.get(RequestService); + const sqs = di.get(SQSService); + // ... +}); +``` -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. +To add your own dependencies, first extend `DependencyAwareClass`. -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: +```ts +// src/services/MyService.ts +import { DependencyAwareClass } from '@comicrelief/lambda-wrapper'; - offline: Offline [http for lambda] listening on http://localhost:3002 +export default class MyService extends DependencyAwareClass { + doSomething() { + // ... + } +} +``` -#### Caveats +Then add it to your Lambda Wrapper configuration in the `dependencies` key. -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. +```ts +// src/config/LambdaWrapper.ts +import lambdaWrapper from '@comicrelief/lambda-wrapper'; -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). +import MyService from '@/src/services/MyService'; -3. If the triggered lambda incurs an exception, this will be propagated upstream, effectively killing the execution of the calling lambda. +export default lambdaWrapper.configure({ + dependencies: { + MyService, + }, +}); +``` + +Now you can use it inside your handler functions and other dependencies! + +```ts +// src/action/DoSomething.ts +import lambdaWrapper from '@/src/config/LambdaWrapper'; +import MyService from '@/src/services/MyService'; + +export default lambdaWrapper.wrap(async (di) => { + di.get(MyService).doSomething(); +}); +``` + +## Service config + +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. + +```ts +export default lambdaWrapper.configure({ + dependencies: { + // your dependencies + }, + sqs: { + // your SQSService config + }, + // ... other configs ... +}); +``` + +To use config with your own dependencies, you need to do three things: + +1. Define the key and type of your config object. + + Using `SQSService` as an example, we have the `sqs` key which has the `SQSServiceConfig` type: + + ```ts + export interface SQSServiceConfig { + queues?: Record; + queueConsumers?: Record; + } + ``` + +2. Define a type that can be applied to a Lambda Wrapper config. + + This simply combines the key and type defined in step 1. Conventionally we name these `With...` types. + + ```ts + export interface WithSQSServiceConfig { + sqs?: SQSServiceConfig; + } + ``` + + 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. + +3. In your dependency constructor, cast the config to this type. + + ```ts + export default class SQSService extends DependencyAwareClass { + constructor(di: DependencyInjection) { + super(di); + + const config = (this.di.config as WithSQSServiceConfig).sqs; + // Bear in mind that because the `sqs` key is optional, the type of + // `config` will be `SQSServiceConfig | undefined`. Take care when + // accessing its properties! You can use optional chaining: + const queues = config?.queues || {}; + // ... + } + } + ``` + +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. + +```ts +lambdaWrapper.configure({ + sqs: { + queues: 42 // Oops! This will be flaggeed as a type error by TypeScript + }, +}); +``` + +You can combine types for multiple dependencies if needed using `&`: + +```ts +lambdaWrapper.configure({ + sqs: { + // SQSService config + }, + other: { + // OtherService config + }, +}); +``` -### Local SQS mode +## Development -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. +### Testing -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`. -Also, if you need to change the port, you can set `process.env.LAMBDA_WRAPPER_OFFLINE_SQS_PORT`. +Run `yarn test` to run the unit tests. -### AWS SQS mode +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. -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. +When adding a feature, ensure it's covered by tests that adequately define its behaviour. -In order for queue URLs to be correctly constructed, you must either: +### Linting -- set `AWS_ACCOUNT_ID` to the account ID that hosts your queue; or -- invoke offline functions via the Lambda API, passing a context that contains a realistic `invokedFunctionArn` including the account ID. +Run `yarn lint` to check code style complies to our standard. Many problems can be auto-fixed using `yarn lint --fix`. -## Semantic release +### Releases Release management is automated using [semantic-release](https://www.npmjs.com/package/semantic-release). diff --git a/babel.config.js b/babel.config.js deleted file mode 100644 index b57ac9e8..00000000 --- a/babel.config.js +++ /dev/null @@ -1,13 +0,0 @@ -module.exports = { - presets: [ - [ - '@babel/preset-env', - { - targets: { - node: 'current', - }, - }, - ], - ], - plugins: ['@babel/plugin-syntax-flow', '@babel/plugin-transform-flow-strip-types'], -}; diff --git a/docs/migration/v2.md b/docs/migration/v2.md new file mode 100644 index 00000000..2690540c --- /dev/null +++ b/docs/migration/v2.md @@ -0,0 +1,155 @@ +# Migrating from v1 to v2 + +This doc summarises the breaking changes introduced in v2 and what you need to do to update your projects to work with it. + +- [Configuration](#configuration) +- [Wrapping a function](#wrapping-a-function) +- [Dependency injection](#dependency-injection) +- [Models](#models) + +## Configuration + +v1 required several consts with shouty names. In v2 these are replaced with a single config object with camel-case keys. You pass this to the new `configure` method to get a configured instance of `LambdaWrapper`. + +Instead of this: + +```js +// src/config/Configuration.js +import { DEFINITIONS as CORE_DEFINITIONS } from '@comicrelief/lambda-wrapper'; + +import { MyService } from '@/src/services/MyService'; + +export const DEFINITIONS = { + ...CORE_DEFINITIONS, + MY_SERVICE: 'MY_SERVICE', +}; + +export const DEPENDENCIES = { + [DEFINITIONS.MY_SERVICE]: MyService, +}; + +export const QUEUE_DEFINITIONS = { + MY_QUEUE: 'MY_QUEUE', +}; + +export const QUEUES = { + [QUEUE_DEFINITIONS.MY_QUEUE]: process.env.SQS_MY_QUEUE, +}; + +export default { + DEFINITIONS, + DEPENDENCIES, + QUEUE_DEFINITIONS, + QUEUES, +}; +``` + +do this: + +```ts +// src/config/LambdaWrapper.ts +import lw from '@comicrelief/lambda-wrapper'; + +import { MyService } from '@/src/services/MyService'; + +export const lambdaWrapper = lw.configure({ + dependencies: { + MyService, + }, + sqs: { + queues: { + myQueue: process.env.SQS_MY_QUEUE, + }, + }, +}); +``` + +## Wrapping a function + +Rather than passing in a config object everywhere you use Lambda Wrapper, you now simply use the configured instance. + +v2 also drops support for callback-style async. Use promises instead. + +Finally, there is no longer a `request` parameter provided to your wrapped function. You can get this from `di` if you need it. + +Instead of this: + +```js +import { LambdaWrapper } from '@comicrelief/lambda-wrapper'; + +import { CONFIGURATION, DEFINITIONS } from '@/src/config/Configuration'; + +export default LambdaWrapper(CONFIGURATION, (di, request, done) => { + // ... + done(null, response); +}); +``` + +do this: + +```ts +import lambdaWrapper from '@/src/config/LambdaWrapper'; + +export default lambdaWrapper.wrap(async (di) => { + const request = di.get(RequestService); + // ... +}); +``` + +If your project doesn't add any additional services to dependency injection, you can also now use `lambdaWrapper` straight out of the box: + +```ts +import lambdaWrapper from '@comicrelief/lambda-wrapper'; + +export default lambdaWrapper.wrap(async (di) => { + // ... +}); +``` + +## Dependency injection + +As you'll have seen in the above examples, dependencies are no longer identified by a `DEFINITIONS` string. `get` now takes the dependency class directly. + +Instead of this: + +```js +import { LambdaWrapper } from '@comicrelief/lambda-wrapper'; + +import { CONFIGURATION, DEFINITIONS } from '@/src/config/Configuration'; + +export default LambdaWrapper(CONFIGURATION, (di, request, done) => { + const logger = di.get(DEFINITIONS.LOGGER); + const myService = di.get(DEFINITIONS.MY_SERVICE); + // ... +}); +``` + +do this: + +```ts +import { LoggerService, RequestService } from '@comicrelief/lambda-wrapper'; + +import lambdaWrapper from '@/src/config/LambdaWrapper'; +import { MyService } from '@/src/services/MyService'; + +export default lambdaWrapper.wrap(async (di) => { + const logger = di.get(LoggerService); + const request = di.get(RequestService); + const myService = di.get(MyService); + // ... +}); +``` + +`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. + +`definitions` has been removed. + +`getEvent`, `getContext` and `getConfiguration` have been deprecated and will be removed in a future major release. Use the `event`, `context` and `config` properties directly. + +## Models + +The `Model` base class has been removed. It's hard to make it type-safe (it tries to dynamically call setter methods) and we do modelling and validation differently now, using our [data-models](https://github.com/comicrelief/data-models) repo which is based around [Yup](https://github.com/jquense/yup). + +The `MarketingPreference` model is removed, as this is application-specific and again is replaced by our [data-models](https://github.com/comicrelief/data-models) repo. + +Other models (`ResponseModel`, `SQSMessageModel`, `StatusModel`) are unaffected except that they no longer inherit from a common `Model` class. diff --git a/docs/services/BaseConfigService.md b/docs/services/BaseConfigService.md new file mode 100644 index 00000000..0a6cb3f1 --- /dev/null +++ b/docs/services/BaseConfigService.md @@ -0,0 +1,46 @@ +# BaseConfigService + +Instead of reimplementing the service status get and set logic across several services, Lambda Wrapper provides a Status service that handles these two operations for us. + +## Usage + +This class is to be extended by the implementing services so that `defaultConfig` and possibly `s3Config` can be overriden / extended. As such, it is not included as a dependency by default and must be explicitly added. + +Example implementation with validation: + +```ts +// src/services/ConfigService.ts +import { BaseConfigService } from '@comicrelief/lambda-wrapper'; + +import { ConfigModel, ConfigProps } from '@/src/models/Config'; + +export default class ConfigService extends BaseConfigService { + async put(config): Promise { + const validated = await ConfigModel.validate(config); + return super.put(validated); + } + + async get(): Promise { + const config = await super.get(); + return ConfigModel.validate(config); + } +} +``` + +Config is typed as `unknown` in the base class since you shouldn't trust what's in the bucket. Override the `get` and `put` methods to pass the results through some validation to ensure the config is valid and can safely be typed. + +Then add to your Lambda Wrapper dependencies: + +```ts +// src/config/LambdaWrapper.ts +import lambdaWrapper from '@comicrelief/lambda-wrapper'; + +import ConfigService from '@/src/services/ConfigService'; + +export default lambdaWrapper.configure({ + dependencies: { + ConfigService, + // ... + }, +}); +``` diff --git a/docs/services/HTTPService.md b/docs/services/HTTPService.md new file mode 100644 index 00000000..49e64deb --- /dev/null +++ b/docs/services/HTTPService.md @@ -0,0 +1,6 @@ +# HTTPService + +Wrapper for `axios.request` that: + +- sets a default timeout of 10 seconds +- forwards a `x-comicrelief-test-metadata` header if one was provided in the request from upstream diff --git a/docs/services/LoggerService.md b/docs/services/LoggerService.md new file mode 100644 index 00000000..5c877590 --- /dev/null +++ b/docs/services/LoggerService.md @@ -0,0 +1,56 @@ +# LoggerService + +Provides logging and integrations with our monitoring tools. + +For logging we use [Winston](https://github.com/winstonjs/winston). Errors will also be sent to [Sentry](https://sentry.io/) and [Epsagon](https://epsagon.com/) if those are configured. + +## Usage + +The logger exposes various methods that you can pass messages or objects to for logging: + +```ts +import lambdaWrapper, { LoggerService } from '@comicrelief/lambda-wrapper'; + +export default lambdaWrapper.wrap(async (di) => { + const logger = di.get(LoggerService); + + // general log message + logger.info('Doing something'); + + // tag the trace so we can find certain tracess more easily in Epsagon + logger.label('flag'); + logger.metric('transactionId', value); + + try { + // do something that might throw an error... + } catch (error) { + // log the error and flag the trace on Epsagon and Sentry + logger.error(error); + + // alternatively, use `warning` if this error is not relevant in staging + // (see Soft Warnings below) + logger.warning(error); + } +}); +``` + +## Configuration + +### Soft warnings + +The `warning` method is equivalent to `error` by default, but can be switched to use `info` by setting `LOGGER_SOFT_WARNING=1` in the environment. + +This is handy for muting certain errors in staging, where we expect our integration tests to cause a lot of errors deliberately that would otherwise spam us with Epsagon alerts. + +### Epsagon + +To configure Epsagon, set the following environment variables: + +- `EPSAGON_TOKEN` – your access token +- `EPSAGON_SERVICE_NAME` – the application name (including stage) to record traces under + +### Sentry + +To configure Sentry, set the following environment variables: + +- `RAVEN_DSN` – your Sentry DSN URL diff --git a/docs/services/RequestService.md b/docs/services/RequestService.md new file mode 100644 index 00000000..eefef05d --- /dev/null +++ b/docs/services/RequestService.md @@ -0,0 +1,49 @@ +# RequestService + +Provides access to components of the HTTP request being handled. + +## Usage + +Since Lambda Wrapper v2, the `RequestService` instance is no longer passed as an argument to your wrapped handler, and must be obtained via `di`. + +```ts +import lambdaWrapper, { RequestService } from '@comicrelief/lambda-wrapper'; + +export default lambdaWrapper.wrap(async (di) => { + const request = di.get(RequestService); + // get the 'name' request parameter, defaulting to 'world' if not set + const name = request.get('name', 'world'); + return ResponseModel.generate({}, 200, `Hello, ${name}`); +}); +``` + +### Headers + +- `getAllHeaders` returns an object containing all headers +- `getHeader` returns the value of an HTTP header +- `getAuthorizationToken` extracts a Bearer token from the `Authorization` header + +### Body + +For requests that submit data in their body (POST, PATCH, PUT), + +- `getAll` parses the body according to the `Content-Type` header +- `get` fetches a single value from the body + +### URL parameters + +For other request methods without a body (GET, HEAD, DELETE), + +- `getAll` returns an object containing all query string parameters +- `get` fetches a single query string parameter + +For all requests, + +- `getPathParameter` fetches a path parameter value + +### Client info + +Some limited information about the client making the request is available. + +- `getIp` returns the request's source IP address +- `getUserBrowserAndDevice` returns user agent details diff --git a/docs/services/SQSService.md b/docs/services/SQSService.md new file mode 100644 index 00000000..de10c1fa --- /dev/null +++ b/docs/services/SQSService.md @@ -0,0 +1,113 @@ +# SQSService + +## Usage + +SQS queues are configured inside an `sqs` key in your Lambda Wrapper config. + +The `queues` key maps short friendly names to the full SQS queue name. Usually we define queue names in our `serverless.yml` and provide them to the application via environment variables. + +```ts +import lambdaWrapper, { WithSQSServiceConfig } from '@comicrelief/lambda-wrapper'; + +export default lambdaWrapper.configure({ + sqs: { + queues: { + // add an entry for each queue mapping to its AWS name + submissions: process.env.SQS_QUEUE_SUBMISSIONS, + }, + }, +}); +``` + +This config is optional – not every application uses SQS! + +You can then send messages to a queue within your Lambda handler using the `publish` method. + +```ts +import { SQSService } from '@comicrelief/lambda-wrapper'; + +import lambdaWrapper from '@/src/config/LambdaWrapper'; + +export default lambdaWrapper.wrap(async (di) => { + const sqs = di.get(SQSService); + const message = { data: 'Hello SQS!' }; + await sqs.publish('submissions', message); +}); +``` + +## Serverless Offline & SQS Emulation + +Serverless Offline only emulates API Gateway and Lambda, so sending 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. + +Offline SQS behaviour can be configured by setting the `LAMBDA_WRAPPER_OFFLINE_SQS_MODE` environment variable. Available modes are: + +- `direct` (the default): invokes the consumer function directly via an offline Lambda endpoint +- `local`: send messages to an offline SQS endpoint, such as Localstack +- `aws`: no special handling of SQS offline; messages will be sent to AWS + +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. + +### Direct Lambda mode + +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. + +To take advantage of SQS emulation, you will need to do the following in your project: + +- Include the `queueConsumers` key in your `SQSService` config. + + This maps the queue name to the fully qualified `FunctionName` that we want to trigger when messages are sent to that queue. + + Extending the example from above, your config might look like this: + + ```ts + const lambdaWrapper = lw.configure({ + sqs: { + queues: { + // Add an entry for each queue with its AWS name. + // Usually we define queue names in our serverless.yml and provide them + // to the application via environment variables. + submissions: process.env.SQS_QUEUE_SUBMISSIONS, + }, + queueConsumers: { + // See section below about offline SQS emulation. + submissions: 'SubmissionConsumer', + }, + } + }); + ``` + + Now when a message is sent using `sqs.publish('submissions', message)`, the `SubmissionConsumer` function will be directly invoked to consume the message. + +- Set `process.env.SERVICE_LAMBDA_URL`. + + While creating the Lambda client, we need to point it to our offline environment. Lambda Wrapper 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. + + 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: + + ```plaintext + offline: Offline [http for lambda] listening on http://localhost:3002 + ``` + +#### Caveats + +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. + +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). + +3. If the triggered lambda incurs an exception, this will be propagated upstream, effectively killing the execution of the calling lambda. + +### Local SQS mode + +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. + +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`. +Also, if you need to change the port, you can set `process.env.LAMBDA_WRAPPER_OFFLINE_SQS_PORT`. + +### AWS SQS mode + +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. + +In order for queue URLs to be correctly constructed, you must either: + +- set `AWS_ACCOUNT_ID` to the account ID that hosts your queue; or +- invoke offline functions via the Lambda API, passing a context that contains a realistic `invokedFunctionArn` including the account ID. diff --git a/docs/services/TimerService.md b/docs/services/TimerService.md new file mode 100644 index 00000000..9c256712 --- /dev/null +++ b/docs/services/TimerService.md @@ -0,0 +1,21 @@ +# TimerService + +Timer helper that can be used to measure how long operations take. + +## Usage + +Start and stop the timer using the `start` and `stop` methods. + +```ts +import lambdaWrapper, { TimerService } from '@comicrelief/lambda-wrapper'; + +export default lambdaWrapper.wrap(async (di) => { + const timer = di.get(TimerService); + + const timerId = 'someLongSlowOperation'; + timer.start(timerId); + await someLongSlowOperation(); + timer.stop(timerId); + // logs 'someLongSlowOperation took 12345 ms to complete' +}); +``` diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000..4ce18349 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,9 @@ +/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['/tests/unit/**/*.spec.ts'], + moduleNameMapper: { + '^@/(.*)$': '/$1', + }, +}; diff --git a/package.json b/package.json index 08eae247..2a84cf35 100644 --- a/package.json +++ b/package.json @@ -3,36 +3,46 @@ "version": "0.0.0-see.readme.for.semantic.release.process", "description": "Lambda wrapper for all Comic Relief Serverless Projects", "main": "dist/index.js", + "author": "Adam Clark", + "license": "ISC", + "repository": { + "type": "git", + "url": "git+https://github.com/comicrelief/lambda-wrapper.git" + }, + "files": [ + "dist" + ], "scripts": { + "prepare": "yarn clean && yarn build", + "build": "tsc -p tsconfig-build.json", + "clean": "rm -rf dist", "lint": "eslint src tests", - "test": "jest --coverage", - "build": "babel src --out-dir dist --copy-files", - "prepublish": "yarn build" + "test": "jest", + "coverage": "yarn test --coverage" }, - "author": "Adam Clark", - "license": "ISC", "devDependencies": { - "@babel/cli": "^7.18.10", - "@babel/core": "^7.18.10", - "@babel/eslint-parser": "^7.18.9", - "@babel/node": "^7.18.10", - "@babel/plugin-syntax-flow": "^7.18.6", - "@babel/plugin-transform-flow-strip-types": "^7.18.9", - "@babel/plugin-transform-react-jsx": "^7.18.10", - "@babel/preset-env": "^7.18.10", "@comicrelief/eslint-config": "^2.0.3", + "@istanbuljs/nyc-config-typescript": "^1.0.2", + "@types/async": "^3.2.15", + "@types/aws-lambda": "^8.10.102", "@types/jest": "^28.1.6", + "@types/node": "14", + "@types/useragent": "^2.3.1", + "@types/uuid": "^8.3.4", + "@types/xml2js": "^0.4.11", + "@typescript-eslint/eslint-plugin": "^5.33.0", + "@typescript-eslint/parser": "^5.33.0", "aws-sdk": "^2.1194.0", - "babel-jest": "^28.1.3", "eslint": "^8.22.0", - "eslint-plugin-flowtype": "^8.0.3", "eslint-plugin-import": "^2.25.2", "eslint-plugin-jsdoc": "^39.3.2", - "eslint-plugin-sonarjs": "^0.13.0", - "eslint-plugin-unicorn": "^42.0.0", "jest": "^28.1.3", "nyc": "^15.1.0", - "semantic-release": "^19.0.3" + "semantic-release": "^19.0.3", + "ts-jest": "^28.0.8", + "ts-node": "^10.9.1", + "tsconfig-paths": "^4.1.0", + "typescript": "^4.7.4" }, "peerDependencies": { "aws-sdk": "^2.831.0" diff --git a/src/Config/Dependencies.js b/src/Config/Dependencies.js deleted file mode 100644 index ab2680ee..00000000 --- a/src/Config/Dependencies.js +++ /dev/null @@ -1,26 +0,0 @@ -import HTTPService from '../Service/HTTP.service'; -import LoggerService from '../Service/Logger.service'; -import RequestService from '../Service/Request.service'; -import SQSService from '../Service/SQS.service'; -import TimerService from '../Service/Timer.service'; - -export const DEFINITIONS = { - HTTP: 'HTTP', - LOGGER: 'LOGGER', - REQUEST: 'REQUEST', - SQS: 'SQS', - TIMER: 'TIMER', -}; - -export const DEPENDENCIES = { - [DEFINITIONS.HTTP]: HTTPService, - [DEFINITIONS.LOGGER]: LoggerService, - [DEFINITIONS.REQUEST]: RequestService, - [DEFINITIONS.SQS]: SQSService, - [DEFINITIONS.TIMER]: TimerService, -}; - -export default { - DEFINITIONS, - DEPENDENCIES, -}; diff --git a/src/DependencyInjection/DependencyAware.class.js b/src/DependencyInjection/DependencyAware.class.js deleted file mode 100644 index 61d7b800..00000000 --- a/src/DependencyInjection/DependencyAware.class.js +++ /dev/null @@ -1,33 +0,0 @@ -import DependencyInjection from './DependencyInjection.class'; - -/** - * DependencyAwareClass Class - */ -export default class DependencyAwareClass { - /** - * DependencyAwareClass constructor - * - * @param {DependencyInjection} di - */ - constructor(di: DependencyInjection) { - this.di = di; - } - - /** - * Get Dependency Injection Container - * - * @returns {DependencyInjection} - */ - getContainer() { - return this.di; - } - - /** - * Shortcut for `this.getContainer().definitions` - * - * @returns {object} - */ - get definitions() { - return this.getContainer().definitions; - } -} diff --git a/src/DependencyInjection/DependencyInjection.class.js b/src/DependencyInjection/DependencyInjection.class.js deleted file mode 100644 index 16ac73bd..00000000 --- a/src/DependencyInjection/DependencyInjection.class.js +++ /dev/null @@ -1,116 +0,0 @@ -import { DEFINITIONS, DEPENDENCIES } from '../Config/Dependencies'; - -/** - * DependencyInjection class - */ -export default class DependencyInjection { - /** - * DependencyInjection constructor - * - * @param configuration - * @param event - * @param context - */ - constructor(configuration, event, context) { - this.event = event; - this.context = context; - - this.dependencies = {}; - this.configuration = configuration; - - for (let x = 0; x <= 1; x += 1) { - // Iterate over lapper dependencies and add to container - Object.keys(DEFINITIONS).forEach((dependencyKey) => { - this.dependencies[dependencyKey] = new DEPENDENCIES[dependencyKey](this); - }); - - // Iterate over child dependencies and add to container - if (typeof configuration.DEPENDENCIES !== 'undefined') { - Object.keys(configuration.DEPENDENCIES).forEach((dependencyKey) => { - this.dependencies[dependencyKey] = new configuration.DEPENDENCIES[dependencyKey](this); - }); - } - } - } - - /** - * Get Dependency - * - * @param definition - * @returns {*} - */ - get(definition) { - if (typeof this.dependencies[definition] === 'undefined') { - throw new TypeError(`${definition} does not exist in di container`); - } - - return this.dependencies[definition]; - } - - /** - * Get Event - * - * @returns {*} - */ - getEvent() { - return this.event; - } - - /** - * Get Context - * - * @returns {*} - */ - getContext() { - return this.context; - } - - /** - * Get Configuration - * - * @param definition string - * @returns {*} - */ - getConfiguration(definition = null) { - if (definition !== null && typeof this.configuration[definition] === 'undefined') { - return null; - } - if (typeof this.configuration[definition] !== 'undefined') { - return this.configuration[definition]; - } - - return this.configuration; - } - - /** - * Check whether the function - * is being executed in a serverless-offline context - * - * @returns {boolean} - */ - get isOffline() { - const context = this.getContext() || {}; - - if (!Object.prototype.hasOwnProperty.call(context, 'invokedFunctionArn')) { - return true; - } - - if (context.invokedFunctionArn.includes('offline')) { - return true; - } - - return !!process.env.USE_SERVERLESS_OFFLINE; - } - - /** - * Returns the definitions - * associated to this DependencyInjection - * so that services can refer to them - * without causing circular imports. - * - * @returns {object} - */ - get definitions() { - return this.configuration.DEFINITIONS; - } -} diff --git a/src/Model/CloudEvent.model.js b/src/Model/CloudEvent.model.js deleted file mode 100644 index 98776c98..00000000 --- a/src/Model/CloudEvent.model.js +++ /dev/null @@ -1,133 +0,0 @@ -import { v4 as UUID } from 'uuid'; - -import Model from './Model.model'; - -/** - * CloudEventModel class - * Class to implement cloud events - https://github.com/cloudevents/spec/blob/master/spec.md - */ -export default class CloudEventModel extends Model { - /** - * CloudEventModel constructor - */ - constructor() { - super(); - - this.cloudEventsVersion = '0.1'; - this.eventType = ''; - this.source = ''; - this.eventID = UUID(); - this.eventTime = new Date().toISOString(); - this.extensions = {}; - this.contentType = 'application/json'; - this.data = {}; - } - - /** - * Get Cloud Events Version - * - * @returns {number} - */ - getCloudEventsVersion(): string { - return this.cloudEventsVersion; - } - - /** - * Get event type - * - * @returns {string|*} - */ - getEventType() { - return this.eventType; - } - - /** - * Set event type - * - * @param value string - */ - setEventType(value: string) { - this.eventType = value; - } - - /** - * Get source - * - * @returns {string|*} - */ - getSource() { - return this.source; - } - - /** - * Set source - * - * @param value string - */ - setSource(value: string) { - this.source = value; - } - - /** - * Get event id - * - * @returns {*|string} - */ - getEventID() { - return this.eventID; - } - - /** - * Get event time - * - * @returns {*|string} - */ - getEventTime() { - return this.eventTime; - } - - /** - * Get extensions - * - * @returns {{}|*} - */ - getExtensions() { - return this.extensions; - } - - /** - * Set extensions - * - * @param value object - */ - setExtensions(value: object) { - this.extensions = value; - } - - /** - * Get content type - * - * @returns {string} - */ - getContentType() { - return this.contentType; - } - - /** - * Get data - * - * @returns {{}|*} - */ - getData() { - return this.data; - } - - /** - * Set data - * - * @param value object - */ - setData(value: object) { - this.data = value; - } -} diff --git a/src/Model/Model.model.js b/src/Model/Model.model.js deleted file mode 100644 index a2e17452..00000000 --- a/src/Model/Model.model.js +++ /dev/null @@ -1,31 +0,0 @@ -/* eslint-disable class-methods-use-this */ -import validate from 'validate.js/validate'; - -/** - * Model base class - */ -export default class Model { - /** - * Instantiate a function with a value if defined - * - * @param classFunctionName string - * @param value mixed - */ - instantiateFunctionWithDefinedValue(classFunctionName, value) { - if (typeof value !== 'undefined') { - this[classFunctionName](value); - } - } - - /** - * Validate values against constraints - * - * @param values object - * @param constraints object - * @returns {boolean} - */ - validateAgainstConstraints(values: object, constraints: object): boolean { - const validation = validate(values, constraints); - return typeof validation === 'undefined'; - } -} diff --git a/src/Model/Response.model.js b/src/Model/Response.model.js deleted file mode 100644 index 78fd9a26..00000000 --- a/src/Model/Response.model.js +++ /dev/null @@ -1,127 +0,0 @@ -import Model from './Model.model'; - -/** - * - * @type {object} - */ -export const RESPONSE_HEADERS = { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*', // Required for CORS support to work - 'Access-Control-Allow-Credentials': true, // Required for cookies, authorization headers with HTTPS -}; - -/** - * Default message provided as part of response - * - * @type {string} - */ -export const DEFAULT_MESSAGE = 'success'; - -/** - * class ResponseModel - */ -export default class ResponseModel extends Model { - /** - * ResponseModel Constructor - * - * @param data - * @param code - * @param message - */ - constructor(data = null, code = null, message = null) { - super(); - - this.body = { - data: data !== null ? data : {}, - message: message !== null ? message : DEFAULT_MESSAGE, - }; - this.code = code !== null ? code : {}; - } - - /** - * Add or update a body variable - * - * @param variable - * @param value - */ - setBodyVariable(variable: string, value) { - this.body[variable] = value; - } - - /** - * Set Data - * - * @param data - */ - setData(data: object) { - this.body.data = data; - } - - /** - * Set Status Code - * - * @param code - */ - setCode(code: number) { - this.code = code; - } - - /** - * Get Status Code - * - * @returns {*} - */ - getCode() { - return this.code; - } - - /** - * Set message - * - * @param message - */ - setMessage(message: string) { - this.body.message = message; - } - - /** - * Get Message - * - * @returns {string|*} - */ - getMessage() { - return this.body.message; - } - - /** - * Geneate a response - * - * @returns {object} - */ - generate() { - return { - statusCode: this.code, - headers: RESPONSE_HEADERS, - body: JSON.stringify(this.body), - }; - } - - /** - * Shorthand static method - * that generates the response immediately - * if no additional processing is required. - * - * Saves only 1 line of code - * but keeps code terse in a lot of places. - * - * @param {*} data - * @param {*} code - * @param {*} message - * @returns {object} - */ - static generate(data = null, code = null, message = null) { - const response = new this(data, code, message); - - return response.generate(); - } -} diff --git a/src/Model/SQS/MarketingPreference.constraints.json b/src/Model/SQS/MarketingPreference.constraints.json deleted file mode 100644 index d1858f81..00000000 --- a/src/Model/SQS/MarketingPreference.constraints.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "campaign": { - "presence": { - "allowEmpty": false - } - }, - "transSource": { - "presence": { - "allowEmpty": false - } - }, - "transSourceUrl": { - "presence": { - "allowEmpty": false - }, - "url": true - }, - "transType": { - "presence": { - "allowEmpty": false - } - }, - "timestamp": { - "presence": { - "allowEmpty": false - } - } -} diff --git a/src/Model/SQS/MarketingPreference.model.js b/src/Model/SQS/MarketingPreference.model.js deleted file mode 100644 index ee4cb243..00000000 --- a/src/Model/SQS/MarketingPreference.model.js +++ /dev/null @@ -1,559 +0,0 @@ -/* eslint-disable sonarjs/cognitive-complexity */ -import validate from 'validate.js'; - -import Model from '../Model.model'; -import ResponseModel from '../Response.model'; -import requestConstraints from './MarketingPreference.constraints.json'; - -// Define action specific error types -export const ERROR_TYPES = { - VALIDATION_ERROR: new ResponseModel({}, 400, 'required fields are missing'), -}; - -/** - * Marketing Preference - */ -export default class MarketingPreference extends Model { - /** - * Message constructor - * - * @param data object - */ - constructor(data = {}) { - super(); - - this.firstname = null; - this.lastname = null; - this.phone = null; - this.mobile = null; - this.address1 = null; - this.address2 = null; - this.address3 = null; - this.town = null; - this.postcode = null; - this.country = null; - this.campaign = ''; - this.transactionId = null; - this.transSource = ''; - this.transSourceUrl = ''; - this.transType = 'prefs'; - this.email = null; - this.permissionPost = null; - this.permissionEmail = null; - this.permissionPhone = null; - this.permissionSMS = null; - this.timestamp = null; - - this.instantiateFunctionWithDefinedValue('setFirstName', data.firstName); - this.instantiateFunctionWithDefinedValue('setFirstName', data.firstname); - this.instantiateFunctionWithDefinedValue('setLastName', data.lastName); - this.instantiateFunctionWithDefinedValue('setLastName', data.lastname); - this.instantiateFunctionWithDefinedValue('setPhone', data.phone); - this.instantiateFunctionWithDefinedValue('setMobile', data.mobile); - this.instantiateFunctionWithDefinedValue('setAddress1', data.address1); - this.instantiateFunctionWithDefinedValue('setAddress2', data.address2); - this.instantiateFunctionWithDefinedValue('setAddress3', data.address3); - this.instantiateFunctionWithDefinedValue('setTown', data.town); - this.instantiateFunctionWithDefinedValue('setPostcode', data.postcode); - this.instantiateFunctionWithDefinedValue('setCountry', data.country); - this.instantiateFunctionWithDefinedValue('setCampaign', data.campaign); - this.instantiateFunctionWithDefinedValue('setTransactionId', data.transactionId); - this.instantiateFunctionWithDefinedValue('setTransSource', data.transSource); - this.instantiateFunctionWithDefinedValue('setTransSourceUrl', data.transSourceUrl); - this.instantiateFunctionWithDefinedValue('setEmail', data.email); - this.instantiateFunctionWithDefinedValue('setPermissionPost', data.permissionPost); - this.instantiateFunctionWithDefinedValue('setPermissionEmail', data.permissionEmail); - this.instantiateFunctionWithDefinedValue('setPermissionPhone', data.permissionPhone); - this.instantiateFunctionWithDefinedValue('setPermissionSMS', data.permissionSMS); - if (typeof data.timestamp !== 'undefined' && data.timestamp !== '' && data.timestamp !== null) { - this.instantiateFunctionWithDefinedValue('setTimestamp', data.timestamp); - } else { - this.generateTimestamp(); - } - } - - /** - * Get First Name - * - * @returns {string|*} - */ - getFirstName() { - return this.firstname; - } - - /** - * Set First Name - * - * @param value string - */ - setFirstName(value: string) { - this.firstname = value; - } - - /** - * Get Last Name - * - * @returns {string|*} - */ - getLastName() { - return this.lastname; - } - - /** - * Set Last Name - * - * @param value string - */ - setLastName(value: string) { - this.lastname = value; - } - - /** - * Get phone - * - * @returns {string|*} - */ - getPhone() { - return this.phone; - } - - /** - * Set phone - * - * @param value string - */ - setPhone(value: string) { - this.phone = value; - } - - /** - * Get Mobile - * - * @returns {string|*} - */ - getMobile() { - return this.mobile; - } - - /** - * Set Mobile - * - * @param value string - */ - setMobile(value: string) { - this.mobile = value; - } - - /** - * Get Address Line 1 - * - * @returns {string|*} - */ - getAddress1() { - return this.address1; - } - - /** - * Set Address Line 1 - * - * @param value string - */ - setAddress1(value: string) { - this.address1 = value; - } - - /** - * Get Address Line 2 - * - * @returns {string|*} - */ - getAddress2() { - return this.address2; - } - - /** - * Set Address Line 2 - * - * @param value string - */ - setAddress2(value: string) { - this.address2 = typeof value === 'undefined' || value === '' ? null : value; - } - - /** - * Get Address Line 3 - * - * @returns {string|*} - */ - getAddress3() { - return this.address3; - } - - /** - * Set Address Line 3 - * - * @param value string - */ - setAddress3(value: string) { - this.address3 = typeof value === 'undefined' || value === '' ? null : value; - } - - /** - * Get Town - * - * @returns {string|*} - */ - getTown() { - return this.town; - } - - /** - * Set Town - * - * @param value string - */ - setTown(value: string) { - this.town = value; - } - - /** - * Get Postcode - * - * @returns {string|*} - */ - getPostcode() { - return this.postcode; - } - - /** - * Set Postcode - * - * @param value string - */ - setPostcode(value: string) { - this.postcode = value; - } - - /** - * Get Country - * - * @returns {string|*} - */ - getCountry() { - return this.country; - } - - /** - * Set Country - * - * @param value string - */ - setCountry(value: string) { - this.country = value; - } - - /** - * Get Campaign - * - * @returns {string|*} - */ - getCampaign() { - return this.campaign; - } - - /** - * Set Campaign - * - * @param value string - */ - setCampaign(value: string) { - this.campaign = value; - } - - /** - * Get Transaction Id - * - * @returns {string|*} - */ - getTransactionId() { - return this.transactionId; - } - - /** - * Set Transaction Id - * - * @param value string - */ - setTransactionId(value: string) { - this.transactionId = value; - } - - /** - * Get Transaction Source - * - * @returns {string|*} - */ - getTransSource() { - return this.transSource; - } - - /** - * Set Transaction Source - * - * @param value string - */ - setTransSource(value: string) { - this.transSource = value; - } - - /** - * Get Transaction Source URL - * - * @returns {string|*} - */ - getTransSourceUrl() { - return this.transSourceUrl; - } - - /** - * Set Transaction Source URL - * - * @param value string - */ - setTransSourceUrl(value: string) { - this.transSourceUrl = value; - } - - /** - * Get Transaction Type - * - * @returns {string|*} - */ - getTransType() { - return this.transType; - } - - /** - * Set Transaction Type - * - * @param value string - */ - setTransType(value: string) { - this.transType = value; - } - - /** - * Get Email - * - * @returns {string|*} - */ - getEmail() { - return this.email; - } - - /** - * Set Email - * - * @param value string - */ - setEmail(value: string) { - this.email = value; - } - - /** - * Get Email Permission - * - * @returns {string|*} - */ - getPermissionEmail() { - return this.permissionEmail; - } - - /** - * Set Email Permission - * - * @param value string - */ - setPermissionEmail(value: string) { - this.permissionEmail = typeof value === 'undefined' || value === '' ? null : value; - } - - /** - * Get Post Permission - * - * @returns {string|*} - */ - getPermissionPost() { - return this.permissionPost; - } - - /** - * Set Post Permission - * - * @param value string - */ - setPermissionPost(value: string) { - this.permissionPost = typeof value === 'undefined' || value === '' ? null : value; - } - - /** - * Get Phone Permission - * - * @returns {string|*} - */ - getPermissionPhone() { - return this.permissionPhone; - } - - /** - * Set Phone Permission - * - * @param value string - */ - setPermissionPhone(value: string) { - this.permissionPhone = typeof value === 'undefined' || value === '' ? null : value; - } - - /** - * Get SMS Permission - * - * @returns {string|*} - */ - getPermissionSMS() { - return this.permissionSMS; - } - - /** - * Set SMS Permission - * - * @param value string - */ - setPermissionSMS(value: string) { - this.permissionSMS = typeof value === 'undefined' || value === '' ? null : value; - } - - /** - * Get Timestamp - * - * @returns {string|*} - */ - getTimestamp() { - return this.timestamp; - } - - /** - * Set Timestamp - * - * @param value string - */ - setTimestamp(value: string) { - this.timestamp = value; - } - - /** - * Generate Timestamp - */ - generateTimestamp() { - this.timestamp = Math.floor(Date.now() / 1000); - } - - /** - * Get Base entity mappings - * - * @returns {object} - */ - getEntityMappings() { - return { - firstname: this.getFirstName(), - lastname: this.getLastName(), - phone: this.getPhone(), - mobile: this.getMobile(), - address1: this.getAddress1(), - address2: this.getAddress2(), - address3: this.getAddress3(), - town: this.getTown(), - postcode: this.getPostcode(), - country: this.getCountry(), - campaign: this.getCampaign(), - transactionId: this.getTransactionId(), - transSource: this.getTransSource(), - transSourceUrl: this.getTransSourceUrl(), - transType: this.getTransType(), - email: this.getEmail(), - permissionEmail: this.getPermissionEmail(), - permissionPost: this.getPermissionPost(), - permissionPhone: this.getPermissionPhone(), - permissionSMS: this.getPermissionSMS(), - timestamp: this.getTimestamp(), - }; - } - - /** - * Check if any permission is set - * - * @returns {boolean} - */ - isPermissionSet() { - return ( - (this.getPermissionEmail() !== null && this.getPermissionEmail() !== '') - || (this.getPermissionPost() !== null && this.getPermissionPost() !== '') - || (this.getPermissionPhone() !== null && this.getPermissionPhone() !== '') - || (this.getPermissionSMS() !== null && this.getPermissionSMS() !== '') - ); - } - - /** - * Validate the model - * - * @returns {Promise} - */ - validate() { - return new Promise((resolve, reject) => { - const requestConstraintsClone = { ...requestConstraints }; - if ( - (this.getPermissionEmail() !== null - && this.getPermissionEmail() !== '' - && this.getPermissionEmail() !== '0' - && this.getPermissionEmail() !== 0) - || this.getEmail() - ) { - if (this.getEmail()) { - requestConstraintsClone.email = { email: true }; - } else { - requestConstraintsClone.email = { presence: { allowEmpty: false }, email: true }; - } - } - // Update constraints if fields are not empty - requestConstraintsClone.firstname = this.getFirstName() !== null && this.getFirstName() !== '' - ? { format: { pattern: "[a-zA-Z.'-_ ]+", flags: 'i', message: 'can only contain alphabetical characters' } } - : ''; - requestConstraintsClone.lastname = this.getLastName() !== null && this.getLastName() !== '' - ? { format: { pattern: "[a-zA-Z.'-_ ]+", flags: 'i', message: 'can only contain alphabetical characters' } } - : ''; - requestConstraintsClone.phone = this.getPhone() !== null && this.getPhone() !== '' - ? { format: { pattern: '[0-9 ]+', flags: 'i', message: 'can only contain numerical characters' } } - : ''; - requestConstraintsClone.mobile = this.getMobile() !== null && this.getMobile() !== '' - ? { format: { pattern: '[0-9 ]+', flags: 'i', message: 'can only contain numerical characters' } } - : ''; - requestConstraintsClone.address1 = this.getAddress1() !== null && this.getAddress1() !== '' - ? { format: { pattern: "[a-zA-Z.'-_& ]+", flags: 'i', message: "can only contain alphanumeric characters and . ' - _ &" } } - : ''; - requestConstraintsClone.country = this.getCountry() !== null && this.getCountry() !== '' - ? { format: { pattern: "[a-zA-Z.'-_& ]+", flags: 'i', message: "can only contain alphabetical characters and . ' - _ &" } } - : ''; - - const validation = validate(this.getEntityMappings(), requestConstraintsClone); - - if (typeof validation === 'undefined') { - resolve(); - return; - } - - const validationErrorResponse = ERROR_TYPES.VALIDATION_ERROR; - validationErrorResponse.setBodyVariable('validation_errors', validation); - - reject(validationErrorResponse); - }); - } -} diff --git a/src/Model/SQS/Message.model.js b/src/Model/SQS/Message.model.js deleted file mode 100644 index 2bd6c087..00000000 --- a/src/Model/SQS/Message.model.js +++ /dev/null @@ -1,88 +0,0 @@ -import Model from '../Model.model'; - -/** - * Message Model - */ -export default class Message extends Model { - /** - * Message constructor - * - * @param message - */ - constructor(message) { - super(); - - this.messageId = message.MessageId; - this.receiptHandle = message.ReceiptHandle; - - this.body = JSON.parse(message.Body); - this.forDeletion = false; - this.metadata = {}; - } - - /** - * Get Message ID - * - * @returns {*} - */ - getMessageId() { - return this.messageId; - } - - /** - * Get Receipt Handle - * - * @returns {*} - */ - getReceiptHandle() { - return this.receiptHandle; - } - - /** - * Get Body - * - * @returns {any | *} - */ - getBody() { - return this.body; - } - - /** - * Set for deletion status - * - * @param forDeletion - */ - setForDeletion(forDeletion: boolean) { - this.forDeletion = forDeletion; - } - - /** - * Whether message is for deletion - * - * @returns {boolean|*} - */ - isForDeletion() { - return this.forDeletion; - } - - /** - * Get all of the message metadata - * - * @returns {{}} - */ - getMetaData() { - return this.metadata; - } - - /** - * Set message metadata value - * - * @param key - * @param value - */ - setMetaData(key, value) { - this.metadata[key] = value; - - return this; - } -} diff --git a/src/Service/Timer.service.js b/src/Service/Timer.service.js deleted file mode 100644 index 5d544444..00000000 --- a/src/Service/Timer.service.js +++ /dev/null @@ -1,40 +0,0 @@ -import { DEFINITIONS } from '../Config/Dependencies'; -import DependencyAwareClass from '../DependencyInjection/DependencyAware.class'; -import DependencyInjection from '../DependencyInjection/DependencyInjection.class'; - -/** - * TimerService class - */ -export default class TimerService extends DependencyAwareClass { - /** - * TimerService constructor - * - * @param di - */ - constructor(di: DependencyInjection) { - super(di); - this.timers = {}; - } - - /** - * Start timer - * - * @param identifier - */ - start(identifier: string) { - this.timers[identifier] = Date.now(); - } - - /** - * Stop timer - * - * @param identifier - */ - stop(identifier: string) { - if (typeof this.timers[identifier] !== 'undefined') { - const duration = Date.now() - this.timers[identifier]; - - this.getContainer().get(DEFINITIONS.LOGGER).info(`Timing - ${identifier} took ${duration}ms to complete`); - } - } -} diff --git a/src/Wrapper/LambdaTermination.js b/src/Wrapper/LambdaTermination.js deleted file mode 100644 index 09d05cfc..00000000 --- a/src/Wrapper/LambdaTermination.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * - */ -export default class LambdaTermination extends Error { - /** - * Triggers a Lambda Termination. - * Offers developer details (that are logged) - * an code for the Lambda and a front facing - * consumer message. - * - * @param {object|string} internal - * @param {number?} code - * @param {object|string?} body - * @param details - */ - constructor(internal, code = 500, body = null, details = 'unknown error') { - let stringified = internal; - - if (typeof internal !== 'string') { - stringified = JSON.stringify(internal); - } - super(stringified); - - this.internal = internal; - this.code = code; - this.body = body || 'unknown error'; - this.details = details; - } -} diff --git a/src/Wrapper/LambdaWrapper.js b/src/Wrapper/LambdaWrapper.js deleted file mode 100644 index 9e31abbb..00000000 --- a/src/Wrapper/LambdaWrapper.js +++ /dev/null @@ -1,156 +0,0 @@ -/* eslint-disable sonarjs/cognitive-complexity */ -import Epsagon from 'epsagon'; - -import { DEFINITIONS } from '../Config/Dependencies'; -import DependencyInjection from '../DependencyInjection/DependencyInjection.class'; -import ResponseModel from '../Model/Response.model'; - -/** - * Processes the outcome once we have one - * - * @param di - * @param outcome - */ -export const handleSuccess = (di, outcome) => { - const logger = di.get(DEFINITIONS.LOGGER); - - // Outcome may be undefined as not all lambdas have a return value. - logger.metric('lambda.statusCode', (outcome && outcome.statusCode) || 200); - - return outcome; -}; - -/** - * Gracefully handles an error - * logging in Epsagon and generating - * a response reflecting the `code` - * of the error, if defined. - * - * Note about Epsagon: - * Epsagon generates alerts for logs on level ERROR. - * This means that logger.error will produce an alert. - * To avoid not meaningful notifications, most likely - * coming from tests, we log INFO unless either: - * - * 1. `error.raiseOnEpsagon` is defined & truthy - * 2. `error.code` is defined and `error.code >= 500`. - * - * @param {DependencyInjection} di - * @param {Error} error - * @param {boolean} [throwError=false] - */ -export const handleError = (di, error, throwError = false) => { - const logger = di.get(DEFINITIONS.LOGGER); - - logger.metric('lambda.statusCode', error.code || 500); - - if (error.raiseOnEpsagon || !error.code || error.code >= 500) { - logger.error(error); - } else { - logger.info(error); - } - - if (throwError) { - if (error instanceof Error) { - return error; - } - - // We want to be absolutely sure - // that we are returning an error - // as Lambda sync handlers will only fail - // if the object is instanceof Error - return new Error(error); - } - - const responseDetails = { - body: error.body || {}, - code: error.code || 500, - details: error.details || 'unknown error', - }; - - return ResponseModel.generate(responseDetails.body, responseDetails.code, responseDetails.details); -}; - -/** - * Lambda Wrapper. - * - * Wraps a lambda handler, generating a new function - * that has access to the dependency injection - * for the service and handles logging and exceptions. - * - * @param configuration - * @param handler - * @param throwError - */ -export default (configuration, handler, throwError = false) => { - let instance = (event, context, callback) => { - const di = new DependencyInjection(configuration, event, context); - const request = di.get(DEFINITIONS.REQUEST); - const logger = di.get(DEFINITIONS.LOGGER); - - context.callbackWaitsForEmptyEventLoop = false; - - // If the event is to trigger a warm up, then don't bother returning the function. - if (di.getEvent().source === 'serverless-plugin-warmup') { - return callback(null, 'Lambda is warm!'); - } - - // Log the users ip address silently for use in error tracing - if (request.getIp() !== null) { - logger.metric('ipAddress', request.getIp(), true); - } - - // Add metrics with user browser information for rapid debugging - const userBrowserAndDevice = request.getUserBrowserAndDevice(); - if (userBrowserAndDevice !== null && typeof userBrowserAndDevice === 'object') { - Object.keys(userBrowserAndDevice).forEach((metricKey) => { - logger.metric(metricKey, userBrowserAndDevice[metricKey], true); - }); - } - - let outcome; - - try { - outcome = handler.call(instance, di, request, callback); - - if (outcome instanceof Promise) { - outcome = outcome - .then((value) => handleSuccess(di, value)) - .catch((error) => { - const handled = handleError(di, error, throwError); - - if (throwError) { - // AWS Lambda with async handler is looking for a rejection - // and not an error object directly - // and will treat resolved errors as successful - // as it will cast the error to JSON, i.e. `{}` - throw handled; - } - - return handled; - }); - } - } catch (error) { - outcome = handleError(di, error, throwError); - } - - return outcome; - }; - - // If the Epsagon token is enabled, then wrap the instance in the Epsagon wrapper - if ( - typeof process.env.EPSAGON_TOKEN === 'string' - && process.env.EPSAGON_TOKEN !== 'undefined' - && typeof process.env.EPSAGON_SERVICE_NAME === 'string' - && process.env.EPSAGON_SERVICE_NAME !== 'undefined' - ) { - Epsagon.init({ - token: process.env.EPSAGON_TOKEN, - appName: process.env.EPSAGON_SERVICE_NAME, - }); - - instance = Epsagon.lambdaWrapper(instance); - } - - return instance; -}; diff --git a/src/core/DependencyAwareClass.ts b/src/core/DependencyAwareClass.ts new file mode 100644 index 00000000..8957cf93 --- /dev/null +++ b/src/core/DependencyAwareClass.ts @@ -0,0 +1,17 @@ +import DependencyInjection from './DependencyInjection'; + +/** + * Base class for dependencies. + */ +export default class DependencyAwareClass { + constructor(readonly di: DependencyInjection) {} + + /** + * Get dependency injection container. + * + * @deprecated Use `this.di` instead. + */ + getContainer(): DependencyInjection { + return this.di; + } +} diff --git a/src/core/DependencyInjection.ts b/src/core/DependencyInjection.ts new file mode 100644 index 00000000..81ff20dc --- /dev/null +++ b/src/core/DependencyInjection.ts @@ -0,0 +1,109 @@ +import { Context } from 'aws-lambda'; + +import DependencyAwareClass from './DependencyAwareClass'; +import { LambdaWrapperConfig } from './config'; + +// eslint-disable-next-line no-use-before-define +type Class = new (di: DependencyInjection) => T; + +/** + * Dependency injection container. + * + * Dependencies (singleton instances of dependency-aware classes) are provided + * to the main Lambda handler and other dependencies via this class. + */ +export default class DependencyInjection { + /** + * Instantiated dependencies. + */ + readonly dependencies: Record; + + /** + * True until all dependencies have been constructed. + */ + private isConstructing = true; + + constructor( + readonly config: LambdaWrapperConfig, + readonly event: any, + readonly context: Context, + ) { + const classes = Object.values(config.dependencies); + this.dependencies = Object.fromEntries( + classes.map((Constructor) => [Constructor.name, new Constructor(this)]), + ); + + this.isConstructing = false; + } + + /** + * Get the singleton instance of the given dependency. + * + * @param dependency + */ + get(dependency: Class): T { + if (this.isConstructing) { + throw new Error( + 'Dependencies are not available in dependency class constructors.\n\n' + + 'To fix this, call `di.get` in the function where the dependency is' + + 'used instead of inside your constructor.', + ); + } + + const name = dependency.name; + + if (!this.dependencies[name]) { + throw new Error( + `${name} does not exist in dependency container\n\n` + + `Make sure you've included ${name} in the 'dependencies' key of your ` + + 'Lambda Wrapper config.', + ); + } + + return this.dependencies[name] as T; + } + + /** + * Get the event passed to AWS Lambda. + * + * @deprecated Use `di.event` instead. + */ + getEvent() { + return this.event; + } + + /** + * Get the AWS Lambda context object. + * + * @deprecated Use `di.context` instead. + */ + getContext() { + return this.context; + } + + /** + * Get Lambda Wrapper configuration. + * + * @deprecated Use `di.config` instead. + */ + getConfiguration() { + return this.config; + } + + /** + * True if the function is being executed in `serverless-offline`. + * + * We use the following checks for this: + * + * - if there is no function ARN, or the ARN includes 'offline' + * - if `process.env.USE_SERVERLESS_OFFLINE` is set + * + * TODO: This is nothing to do with dependency injection and should be moved + * somewhere else! Any ideas? + */ + get isOffline(): boolean { + return !this.context.invokedFunctionArn + || this.context.invokedFunctionArn.includes('offline') + || !!process.env.USE_SERVERLESS_OFFLINE; + } +} diff --git a/src/core/LambdaWrapper.ts b/src/core/LambdaWrapper.ts new file mode 100644 index 00000000..59085d62 --- /dev/null +++ b/src/core/LambdaWrapper.ts @@ -0,0 +1,160 @@ +import Epsagon from 'epsagon'; + +import { Context } from '../index'; +import ResponseModel from '../models/ResponseModel'; +import LoggerService from '../services/LoggerService'; +import RequestService from '../services/RequestService'; +import DependencyInjection from './DependencyInjection'; +import { LambdaWrapperConfig, mergeConfig } from './config'; + +export interface WrapOptions { + /** + * Whether uncaught errors should be handled to return an HTTP 500 response + * instead of causing a function error. (default: `true`) + * + * This is what you usually want when working on an HTTP endpoint, but in + * other contexts (e.g. queue consumers) you may want AWS Lambda to report a + * failure so that the event is retried. + */ + handleUncaughtErrors?: boolean; +} + +export default class LambdaWrapper { + constructor(readonly config: TConfig) {} + + /** + * Returns a new Lambda Wrapper with the given configuration applied. + * + * @param config + */ + configure(config: Partial & TMoreConfig): LambdaWrapper { + return new LambdaWrapper(mergeConfig(this.config, config)); + } + + /** + * Wrap the given function. + */ + wrap(handler: (di: DependencyInjection) => Promise, options?: WrapOptions) { + const { + handleUncaughtErrors = true, + } = options || {}; + + let wrapper = async (event: any, context: Context) => { + const di = new DependencyInjection(this.config, event, context); + const request = di.get(RequestService); + const logger = di.get(LoggerService); + + context.callbackWaitsForEmptyEventLoop = false; + + // if the event is a warmup, don't bother running the function + if (event.source === 'serverless-plugin-warmup') { + return 'Lambda is warm!'; + } + + // log the user's IP address silently for use in error tracing + const ipAddress = request.getIp(); + if (ipAddress) { + logger.metric('ipAddress', ipAddress, true); + } + + // add metrics with user browser information for rapid debugging + const userBrowserAndDevice = request.getUserBrowserAndDevice(); + if (userBrowserAndDevice) { + Object.entries(userBrowserAndDevice).forEach(([key, value]) => { + logger.metric(key, value, true); + }); + } + + try { + const result = await handler.call(wrapper, di); + return LambdaWrapper.handleSuccess(di, result); + } catch (error: any) { + const handled = LambdaWrapper.handleError(di, error, !handleUncaughtErrors); + + if (!handleUncaughtErrors) { + // AWS Lambda with async handler is looking for a rejection + // and not an error object directly + // and will treat resolved errors as successful + // as it will cast the error to JSON, i.e. `{}` + throw handled; + } + + return handled; + } + }; + + // If Epsagon is enabled, wrap the instance in the Epsagon wrapper + if (process.env.EPSAGON_TOKEN && process.env.EPSAGON_SERVICE_NAME) { + Epsagon.init({ + token: process.env.EPSAGON_TOKEN, + appName: process.env.EPSAGON_SERVICE_NAME, + }); + + wrapper = Epsagon.lambdaWrapper(wrapper); + } + + return wrapper; + } + + /** + * Process the result once we have one. + * + * @param di + * @param result + */ + static handleSuccess(di: DependencyInjection, result: any) { + const logger = di.get(LoggerService); + + // result may be undefined as not all lambdas have a return value + logger.metric('lambda.statusCode', result?.statusCode || 200); + + return result; + } + + /** + * Gracefully handles an error, logging in Epsagon and generating a response + * reflecting the `code` of the error, if defined. + * + * Note about Epsagon: + * Epsagon generates alerts for logs on level ERROR. This means that + * `logger.error` will produce an alert. To avoid meaningless notifications, + * most likely coming from tests, we log INFO unless either: + * + * 1. `error.raiseOnEpsagon` is defined & truthy + * 2. `error.code` is defined and `error.code >= 500`. + * + * @param di + * @param error + * @param [throwError=false] + */ + static handleError(di: DependencyInjection, error: Error, throwError = false) { + const logger = di.get(LoggerService); + + const { + code, + raiseOnEpsagon, + body = {}, + details = 'unknown error', + } = error as any; + + logger.metric('lambda.statusCode', code || 500); + + if (raiseOnEpsagon || !code || code >= 500) { + logger.error(error); + } else { + logger.info(error); + } + + if (throwError) { + if (error instanceof Error) { + return error; + } + + // We want to be absolutely sure that we are returning an error, as + // Lambda sync handlers will only fail if the object is instanceof Error + return new Error(error); + } + + return ResponseModel.generate(body, code || 500, details); + } +} diff --git a/src/core/config.ts b/src/core/config.ts new file mode 100644 index 00000000..43c5e64c --- /dev/null +++ b/src/core/config.ts @@ -0,0 +1,36 @@ +import DependencyAwareClass from './DependencyAwareClass'; + +/** + * Config for Lambda Wrapper defining dependencies and their configuration. + */ +export interface LambdaWrapperConfig { + /** + * Dependencies to be provided by dependency injection. + * + * TODO: should this just be a list instead? keys are currently unused + */ + dependencies: Record; +} + +/** + * Combine two Lambda Wrapper configs. + * + * @param old Current config. + * @param new_ New config that will override the old. + */ +export function mergeConfig< + A extends LambdaWrapperConfig, + B extends Partial, +>( + old: A, + new_: B, +): A & B { + return { + ...old, + ...new_, + dependencies: { + ...old.dependencies, + ...new_.dependencies, + }, + }; +} diff --git a/src/index.js b/src/index.js deleted file mode 100644 index 81fc1775..00000000 --- a/src/index.js +++ /dev/null @@ -1,24 +0,0 @@ -export { DEFINITIONS } from './Config/Dependencies'; - -// DependencyInjection -export { default as DependencyAwareClass } from './DependencyInjection/DependencyAware.class'; -export { default as DependencyInjection } from './DependencyInjection/DependencyInjection.class'; - -// Model -export { default as Model } from './Model/Model.model'; -export { default as ResponseModel } from './Model/Response.model'; -export { default as StatusModel, STATUS_TYPES } from './Model/Status.model'; -export { default as SQSMessageModel } from './Model/SQS/Message.model'; -export { default as MarketingPreferenceModel } from './Model/SQS/MarketingPreference.model'; - -// Service -export { default as BaseConfigService } from './Service/BaseConfig.service'; -export { default as HTTPService, COMICRELIEF_TEST_METADATA_HEADER } from './Service/HTTP.service'; -export { default as LoggerService } from './Service/Logger.service'; -export { default as RequestService } from './Service/Request.service'; -export { default as SQSService, SQS_OFFLINE_MODES, SQS_PUBLISH_FAILURE_MODES } from './Service/SQS.service'; - -// Wrapper -export { default as LambdaTermination } from './Wrapper/LambdaTermination'; -export { default as LambdaWrapper } from './Wrapper/LambdaWrapper'; -export { default as PromisifiedDelay } from './Wrapper/PromisifiedDelay'; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 00000000..612c06e2 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,70 @@ +import LambdaWrapper from './core/LambdaWrapper'; +import { LambdaWrapperConfig } from './core/config'; +import LoggerService from './services/LoggerService'; +import RequestService from './services/RequestService'; +import SQSService, { WithSQSServiceConfig } from './services/SQSService'; +import TimerService from './services/TimerService'; + +/** + * Lambda Wrapper preconfigured with our core services that can be used + * straight out of the box. + * + * Use `lambdaWrapper.configure()` to add your own dependencies. + */ +const lambdaWrapper = new LambdaWrapper({ + dependencies: { + LoggerService, + RequestService, + SQSService, + TimerService, + }, +}); + +export default lambdaWrapper; + +export { Context, Handler } from 'aws-lambda'; + +export { LambdaWrapperConfig } from './core/config'; +export { default as DependencyAwareClass } from './core/DependencyAwareClass'; +export { default as DependencyInjection } from './core/DependencyInjection'; +export { default as LambdaWrapper, WrapOptions } from './core/LambdaWrapper'; + +export { + default as ResponseModel, +} from './models/ResponseModel'; +export { + default as SQSMessageModel, +} from './models/SQSMessageModel'; +export { + default as StatusModel, + STATUS_TYPES, +} from './models/StatusModel'; + +export { + default as BaseConfigService, +} from './services/BaseConfigService'; +export { + default as HTTPService, + COMICRELIEF_TEST_METADATA_HEADER, +} from './services/HTTPService'; +export { + default as LoggerService, +} from './services/LoggerService'; +export { + default as RequestService, + REQUEST_TYPES, + RequestFile, +} from './services/RequestService'; +export { + default as SQSService, + SQS_OFFLINE_MODES, + SQS_PUBLISH_FAILURE_MODES, + SQSServiceConfig, + WithSQSServiceConfig, +} from './services/SQSService'; +export { + default as TimerService, +} from './services/TimerService'; + +export { default as LambdaTermination } from './utils/LambdaTermination'; +export { default as PromisifiedDelay } from './utils/PromisifiedDelay'; diff --git a/src/models/ResponseModel.ts b/src/models/ResponseModel.ts new file mode 100644 index 00000000..c6697c8e --- /dev/null +++ b/src/models/ResponseModel.ts @@ -0,0 +1,109 @@ +/** + * HTTP headers to be included in all responses. + */ +export const RESPONSE_HEADERS = { + 'Content-Type': 'application/json', + /** Required for CORS support to work */ + 'Access-Control-Allow-Origin': '*', + /** Required for cookies, authorization headers with HTTPS */ + 'Access-Control-Allow-Credentials': true, +}; + +/** + * Default message provided as part of response. + */ +export const DEFAULT_MESSAGE = 'success'; + +/** + * Our standard response model for HTTP endpoints. + */ +export default class ResponseModel { + body: any; + + code: any; + + constructor(data?: any, code?: number, message?: string) { + this.body = { + data: data ?? {}, + message: message ?? DEFAULT_MESSAGE, + }; + this.code = code ?? {}; + } + + /** + * Add or update a body variable. + * + * @param key + * @param value + */ + setBodyVariable(key: string, value: any) { + this.body[key] = value; + } + + /** + * Set data. + * + * @param data + */ + setData(data: object) { + this.body.data = data; + } + + /** + * Set status code. + * + * @param code + */ + setCode(code: number) { + this.code = code; + } + + /** + * Get status code. + */ + getCode() { + return this.code; + } + + /** + * Set message. + * + * @param message + */ + setMessage(message: string) { + this.body.message = message; + } + + /** + * Get message. + */ + getMessage() { + return this.body.message; + } + + /** + * Geneate a response. + */ + generate() { + return { + statusCode: this.code, + headers: RESPONSE_HEADERS, + body: JSON.stringify(this.body), + }; + } + + /** + * Shorthand static method that generates the response immediately if no + * additional processing is required. + * + * Saves only 1 line of code but keeps code terse in a lot of places. + * + * @param data + * @param code + * @param message + */ + static generate(data?: any, code?: number, message?: string) { + const response = new this(data, code, message); + return response.generate(); + } +} diff --git a/src/models/SQSMessageModel.ts b/src/models/SQSMessageModel.ts new file mode 100644 index 00000000..58d00afe --- /dev/null +++ b/src/models/SQSMessageModel.ts @@ -0,0 +1,79 @@ +import { SQS } from 'aws-sdk'; + +/** + * Message model for SQS. + */ +export default class Message { + messageId: string; + + receiptHandle: string; + + body: string; + + forDeletion = false; + + metadata: Record = {}; + + constructor(message: SQS.Message) { + // todo: validate rather than assert the type + this.messageId = message.MessageId!; + this.receiptHandle = message.ReceiptHandle!; + this.body = JSON.parse(message.Body!); + } + + /** + * Get message ID. + */ + getMessageId() { + return this.messageId; + } + + /** + * Get message receipt handle. + */ + getReceiptHandle() { + return this.receiptHandle; + } + + /** + * Get message body. + */ + getBody() { + return this.body; + } + + /** + * Set for deletion status. + * + * @param forDeletion + */ + setForDeletion(forDeletion: boolean) { + this.forDeletion = forDeletion; + } + + /** + * Whether message is for deletion. + */ + isForDeletion() { + return this.forDeletion; + } + + /** + * Get all of the message metadata. + */ + getMetaData() { + return this.metadata; + } + + /** + * Set message metadata value + * + * @param key + * @param value + */ + setMetaData(key: string, value: any) { + this.metadata[key] = value; + + return this; + } +} diff --git a/src/Model/Status.model.js b/src/models/StatusModel.ts similarity index 59% rename from src/Model/Status.model.js rename to src/models/StatusModel.ts index cc6b0190..942bc19d 100644 --- a/src/Model/Status.model.js +++ b/src/models/StatusModel.ts @@ -1,5 +1,3 @@ -import Model from './Model.model'; - export const STATUS_TYPES = { OK: 'OK', ACCEPTABLE_FAILURE: 'ACCEPTABLE_FAILURE', @@ -7,33 +5,33 @@ export const STATUS_TYPES = { }; /** - * StatusModel Class + * Model for our status check endpoints. */ -export default class StatusModel extends Model { +export default class StatusModel { /** - * StatusModel constructor - * - * @param service - * @param status + * Service name. */ - constructor(service: string, status: string) { - super(); + service: string; - this.setService(service); - this.setStatus(status); + /** + * One of the `STATUS_TYPES` values. + */ + status: string; + + constructor(service: string, status: string) { + this.service = service; + this.status = status; } /** - * Get Service - * - * @returns {*} + * Get the service name. */ getService(): string { return this.service; } /** - * Set Service + * Set the service name. * * @param service */ @@ -42,12 +40,12 @@ export default class StatusModel extends Model { } /** - * Set the status + * Set the status. * * @param status */ setStatus(status: string) { - if (typeof STATUS_TYPES[status] === 'undefined') { + if (!(status in STATUS_TYPES)) { throw new TypeError(`${StatusModel.name} - ${status} is not a valid status type`); } @@ -55,9 +53,7 @@ export default class StatusModel extends Model { } /** - * Get status - * - * @returns {string|*} + * Get the status. */ getStatus(): string { return this.status; diff --git a/src/Service/BaseConfig.service.js b/src/services/BaseConfigService.ts similarity index 52% rename from src/Service/BaseConfig.service.js rename to src/services/BaseConfigService.ts index 65dc2ba2..0a6805d9 100644 --- a/src/Service/BaseConfig.service.js +++ b/src/services/BaseConfigService.ts @@ -1,14 +1,15 @@ import { S3 } from 'aws-sdk'; -import DependencyAwareClass from '../DependencyInjection/DependencyAware.class'; -import LambdaTermination from '../Wrapper/LambdaTermination'; +import DependencyAwareClass from '../core/DependencyAwareClass'; +import LambdaTermination from '../utils/LambdaTermination'; + /** - * Error.code for S3 404 errors + * `error.code` for S3 404 errors. */ export const S3_NO_SUCH_KEY_ERROR_CODE = 'NoSuchKey'; /** - * Represents the service states + * Represents the service states. */ export const ServiceStates = { OK: 'OK', @@ -17,7 +18,7 @@ export const ServiceStates = { }; /** - * Maps service states to HTTP codes + * Maps service states to HTTP codes. */ export const ServiceStatesHttpCodes = { [ServiceStates.OK]: 200, @@ -26,18 +27,19 @@ export const ServiceStatesHttpCodes = { }; /** - * BaseConfigService class + * This class is to be extended by the implementing services so that + * `defaultConfig` and possibly `s3Config` can be overriden / extended. * - * This class is to be extended by the implementing services - * so that `defaultConfig` and possibly `s3Config` can be - * overriden / extended. + * Config is typed as `unknown` since you shouldn't trust what's in the bucket. + * Override the `get` and `put` methods to pass the results through some + * validation to ensure the config is valid and can safely be typed. */ export default class BaseConfigService extends DependencyAwareClass { /** * Returns the basic config. - * This getter is used to set the default config - * should the service not find any - * on the configured S3 Bucket. + * + * This getter is used to set the default config should the service not find + * any on the configured S3 Bucket. * * See `getOrCreate` and `patch` methods. */ @@ -48,21 +50,18 @@ export default class BaseConfigService extends DependencyAwareClass { } /** - * Returns the S3 configuration - * used to retrieve / update the - * service configuration. + * Returns the S3 configuration used to retrieve or update the service + * configuration. */ - static get s3config() { + static get s3config(): { Bucket: string; Key: string; } { return { - Bucket: process.env.SERVICE_CONFIG_S3_BUCKET, - Key: process.env.SERVICE_CONFIG_S3_KEY, + Bucket: process.env.SERVICE_CONFIG_S3_BUCKET || '', + Key: process.env.SERVICE_CONFIG_S3_KEY || '', }; } /** - * Returns an S3 client - * - * @returns {S3} + * Returns an S3 client. */ static get client() { return new S3({ @@ -72,32 +71,30 @@ export default class BaseConfigService extends DependencyAwareClass { /** * Returns an S3 client - * - * @returns {S3} */ get client() { - return this.constructor.client; + return (this.constructor as typeof BaseConfigService).client; } /** - * Deletes the configuration stored on S3. - * Helpful in feature tests. + * Deletes the configuration stored on S3. Helpful in feature tests. */ async delete() { - return this.client.deleteObject(this.constructor.s3config).promise(); + return this.client.deleteObject( + (this.constructor as typeof BaseConfigService).s3config, + ).promise(); } /** - * Puts the given configuration on S3 + * Puts the given configuration on S3. * * @param config */ - async put(config) { + async put(config: T): Promise { await this.client.putObject({ - ...this.constructor.s3config, + ...(this.constructor as typeof BaseConfigService).s3config, Body: JSON.stringify(config), - }) - .promise(); + }).promise(); return config; } @@ -105,8 +102,10 @@ export default class BaseConfigService extends DependencyAwareClass { /** * Gets the service configuration. */ - async get() { - const response = await this.client.getObject(this.constructor.s3config).promise(); + async get(): Promise { + const response = await this.client.getObject( + (this.constructor as typeof BaseConfigService).s3config, + ).promise(); const body = String(response.Body); if (!body) { @@ -124,20 +123,19 @@ export default class BaseConfigService extends DependencyAwareClass { /** * Gets or creates the service configuration. * - * If the configuration is not found on S3 - * the default configuration - * is uploaded and returned instead. + * If the configuration is not found on S3 the default configuration is + * uploaded and returned instead. */ - async getOrCreate() { + async getOrCreate(): Promise { try { return await this.get(); - } catch (error) { + } catch (error: any) { if (error.code !== S3_NO_SUCH_KEY_ERROR_CODE) { // Throw directly any other error throw error; } - return this.put(this.constructor.defaultConfig); + return this.put((this.constructor as typeof BaseConfigService).defaultConfig); } } @@ -148,12 +146,12 @@ export default class BaseConfigService extends DependencyAwareClass { * * @param partialConfig */ - async patch(partialConfig) { - let base = this.constructor.defaultConfig; + async patch(partialConfig: any): Promise { + let base: any = (this.constructor as typeof BaseConfigService).defaultConfig; try { base = await this.get(); - } catch (error) { + } catch (error: any) { if (error.code !== S3_NO_SUCH_KEY_ERROR_CODE) { // Throw directly any other error throw error; @@ -170,27 +168,24 @@ export default class BaseConfigService extends DependencyAwareClass { } /** - * Performs a health check - * given the currentConfig. + * Performs a health check given the current config. * - * If currentConfig is not supplied - * it uses `getOrCreate` to fetch it. + * If `currentConfig` is not supplied it uses `getOrCreate` to fetch it. * * @param currentConfig */ - async healthCheck(currentConfig = null) { + async healthCheck(currentConfig?: any): Promise { const config = currentConfig || await this.getOrCreate(); return ServiceStatesHttpCodes[config.state] || 500; } /** - * Ensures that the application is healthy - * or throws a LambdaTermination + * Ensures that the application is healthy or throws a `LambdaTermination`. * * @param currentConfig */ - async ensureHealthy(currentConfig = null) { + async ensureHealthy(currentConfig: any = null) { const statusCode = await this.healthCheck(currentConfig); if (statusCode < 400) { diff --git a/src/Service/HTTP.service.js b/src/services/HTTPService.ts similarity index 55% rename from src/Service/HTTP.service.js rename to src/services/HTTPService.ts index ca6aabac..80898091 100644 --- a/src/Service/HTTP.service.js +++ b/src/services/HTTPService.ts @@ -1,15 +1,23 @@ import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'; -import DependencyAwareClass from '../DependencyInjection/DependencyAware.class'; +import DependencyAwareClass from '../core/DependencyAwareClass'; +import DependencyInjection from '../core/DependencyInjection'; +import RequestService from './RequestService'; export const COMICRELIEF_TEST_METADATA_HEADER = 'x-comicrelief-test-metadata'; export const DEFAULT_HTTP_TIMEOUT = 10 * 1000; /** - * HTTPService class + * Wrapper for `axios.request` that: + * + * - sets a default timeout of 10 seconds + * - forwards a `x-comicrelief-test-metadata` header if one was provided in + * the request from upstream */ export default class HTTPService extends DependencyAwareClass { - constructor(di) { + config: AxiosRequestConfig; + + constructor(di: DependencyInjection) { super(di); this.config = { @@ -18,16 +26,16 @@ export default class HTTPService extends DependencyAwareClass { } /** - * Sets the default timeout + * Set the default timeout. * - * @param {number} ms + * @param ms */ - setDefaultTimeout(ms) { + setDefaultTimeout(ms: number) { this.config.timeout = ms; } /** - * Performs and HTTP Request + * Perform an HTTP request. * * @param config */ @@ -38,8 +46,8 @@ export default class HTTPService extends DependencyAwareClass { ...config, }; - const lambdaRequest = this.getContainer().get(this.definitions.REQUEST); - const testMetadata = lambdaRequest.getHeader(COMICRELIEF_TEST_METADATA_HEADER); + const request = this.di.get(RequestService); + const testMetadata = request.getHeader(COMICRELIEF_TEST_METADATA_HEADER); if (testMetadata) { mergedConfig.headers = mergedConfig.headers || {}; diff --git a/src/Service/Logger.service.js b/src/services/LoggerService.ts similarity index 57% rename from src/Service/Logger.service.js rename to src/services/LoggerService.ts index 18eb144b..9d82b6cf 100644 --- a/src/Service/Logger.service.js +++ b/src/services/LoggerService.ts @@ -1,13 +1,14 @@ import * as Sentry from '@sentry/node'; +import { AxiosError } from 'axios'; import Epsagon from 'epsagon'; import Winston from 'winston'; -import DependencyAwareClass from '../DependencyInjection/DependencyAware.class'; -import DependencyInjection from '../DependencyInjection/DependencyInjection.class'; +import DependencyAwareClass from '../core/DependencyAwareClass'; +import DependencyInjection from '../core/DependencyInjection'; -// Instantiate the sentry client const sentryIsAvailable = typeof process.env.RAVEN_DSN !== 'undefined' && typeof process.env.RAVEN_DSN === 'string' && process.env.RAVEN_DSN !== 'undefined'; +// initialise the Sentry client if available if (sentryIsAvailable) { Sentry.init({ dsn: process.env.RAVEN_DSN, @@ -17,30 +18,36 @@ if (sentryIsAvailable) { } /** - * LoggerService class + * Provides logging and integrations with our monitoring tools. + * + * For logging we use [Winston](https://github.com/winstonjs/winston). + * Errors will also be sent to [Sentry](https://sentry.io/) and + * [Epsagon](https://epsagon.com/) if those are available. */ export default class LoggerService extends DependencyAwareClass { + private sentry: typeof Sentry | null; + + private winston: Winston.Logger | null; + constructor(di: DependencyInjection) { super(di); + this.sentry = null; this.winston = null; - const container = this.getContainer(); - const event = container.getEvent(); - const context = container.getContext(); + const { event, context } = this.di; - // Set sentry client context - if (sentryIsAvailable && !container.isOffline) { + if (sentryIsAvailable && !di.isOffline) { Sentry.configureScope((scope) => { scope.setTags({ Event: event, - Context: context, + Context: context as any, }); scope.setExtras({ lambda: context.functionName, memory_size: context.memoryLimitInMB, - log_group: context.log_group_name, - log_stream: context.log_stream_name, + log_group: context.logGroupName, + log_stream: context.logStreamName, stage: process.env.STAGE, path: event.path, httpMethod: event.httpMethod, @@ -52,14 +59,10 @@ export default class LoggerService extends DependencyAwareClass { } /** - * Returns a Winston logger object - * configured for our lambdas. + * Returns a Winston logger configured for our lambdas. * - * Note: - * - * If the lambda is executed - * in a `serverless-offline` context - * the log output to console will be pretty printed. + * Note: If the lambda is executed in a `serverless-offline` context, the + * log output to console will be pretty-printed. */ getLogger() { const loggerFormats = [ @@ -69,21 +72,17 @@ export default class LoggerService extends DependencyAwareClass { return value.toString('base64'); } if (value instanceof Error) { - const error = {}; - - Object.getOwnPropertyNames(value).forEach((objectKey) => { - error[objectKey] = value[objectKey]; - }); - - return error; + return Object.fromEntries( + Object.getOwnPropertyNames(value) + .map((errorKey) => [errorKey, (value as any)[errorKey]]), + ); } - return value; }, }), ]; - if (this.getContainer().isOffline) { + if (this.di.isOffline) { loggerFormats.push(Winston.format.prettyPrint()); } @@ -97,9 +96,8 @@ export default class LoggerService extends DependencyAwareClass { /** * Returns the logger. * - * Uses a cached `Winston` object - * if it has been already generated, - * otherwise it generates one. + * Uses a cached Winston logger if it has been already created, otherwise it + * creates one. */ get logger() { if (!this.winston) { @@ -110,8 +108,15 @@ export default class LoggerService extends DependencyAwareClass { } /** - * While handling an error, lambda wrapper should - * recognise axios errors and trim down the information. + * Get Sentry client. + */ + getSentry() { + return this.sentry; + } + + /** + * While logging an error, we should recognise axios errors and trim down the + * information to only what is useful for debugging. * * Keep the following keys: * - message.config @@ -121,16 +126,14 @@ export default class LoggerService extends DependencyAwareClass { * * @param {object} error */ - static processAxiosError(error) { - const processed = { + static processAxiosError(error: AxiosError) { + const processed: any = { config: error.config, message: error.message, }; - // It's pretty common for axios errors - // to not have.response e.g.when there's - // a network error or timeout. - // These errors will have .request but not .response. + // It's pretty common for axios errors to not have a `response`, + // for example if there was a network error or timeout. if (error.response) { processed.response = { status: error.response.status, @@ -142,16 +145,15 @@ export default class LoggerService extends DependencyAwareClass { } /** - * Transform the original message - * before it is passed to the winston logger + * Transform the original message before it is passed to the logger. * - * @param {string|object} message + * @param message */ - processMessage(message = '') { + static processMessage(message: any) { let processed = message; - if (processed && processed.isAxiosError) { - processed = this.constructor.processAxiosError(processed); + if (processed?.isAxiosError) { + processed = LoggerService.processAxiosError(processed); } return processed; @@ -163,7 +165,7 @@ export default class LoggerService extends DependencyAwareClass { * @param error object * @param message string */ - error(error, message = '') { + error(error: any, message = '') { if (sentryIsAvailable && error instanceof Error) { Sentry.captureException(error); } @@ -178,44 +180,34 @@ export default class LoggerService extends DependencyAwareClass { Epsagon.setError(error); } - this.logger.log('error', message, { error: this.processMessage(error) }); + this.logger.log('error', message, { error: LoggerService.processMessage(error) }); this.label('error', true); this.metric('error', 'error', true); } /** - * Get sentry client + * Log an informational message. * - * @returns {null|*} + * @param message */ - getSentry() { - return this.sentry; + info(message: any) { + this.logger.log('info', LoggerService.processMessage(message)); } /** - * Log Information Message + * Log an error, using `LoggerService.error` or `LoggerService.info` based + * on `process.env.LOGGER_SOFT_WARNING`. * - * @param message string - */ - info(message) { - this.logger.log('info', this.processMessage(message)); - } - - /** - * Logs an error, using `LoggerService.error` - * or `LoggerService.info` based on - * `process.env.LOGGER_SOFT_WARNING`. - * - * Please note that `LoggerService.error` and `LoggerService.info` - * have different signatures. The function uses the shared argument - * instead of introducing ambiguity. + * Please note that `LoggerService.error` and `LoggerService.info` have + * different signatures. The function uses the shared argument instead of + * introducing ambiguity. * * @param error */ - warning(error) { + warning(error: any) { const softWarningValues = ['true', '1']; - if (softWarningValues.includes(process.env.LOGGER_SOFT_WARNING)) { + if (softWarningValues.includes(process.env.LOGGER_SOFT_WARNING || '')) { return this.info(error); } @@ -223,12 +215,12 @@ export default class LoggerService extends DependencyAwareClass { } /** - * Add a label + * Add a label to the function's Epsagon trace. * - * @param descriptor string - * @param silent boolean + * @param descriptor + * @param silent If `false`, the label will also be logged. (default: false) */ - label(descriptor, silent = false) { + label(descriptor: string, silent = false) { if ( typeof process.env.EPSAGON_TOKEN === 'string' && process.env.EPSAGON_TOKEN !== 'undefined' @@ -238,19 +230,19 @@ export default class LoggerService extends DependencyAwareClass { Epsagon.label(descriptor, true); } - if (silent === false) { + if (!silent) { this.logger.log('info', `label - ${descriptor}`); } } /** - * Add a metric + * Add a metric to the function's Epsagon trace. * - * @param descriptor string - * @param stat integer | string - * @param silent boolean + * @param descriptor + * @param stat + * @param silent If `false`, the metric will also be logged. (default: false) */ - metric(descriptor, stat, silent = false) { + metric(descriptor: string, stat: number | string, silent = false) { if ( typeof process.env.EPSAGON_TOKEN === 'string' && process.env.EPSAGON_TOKEN !== 'undefined' @@ -266,13 +258,13 @@ export default class LoggerService extends DependencyAwareClass { } /** - * Logs an object so that it can be inspected + * Log an object so that it can be inspected. * - * @param action - What are we doing with the object, i.e. 'Processing' - * @param object - The object to be stored in logs - * @param level - 'error', 'warning' or 'info' + * @param action What are we doing with the object, e.g. 'Processing' + * @param object The object to be stored in logs + * @param level 'error', 'warning' or 'info' */ - object(action, object, level = 'info') { + object(action: string, object: any, level: 'error' | 'warning' | 'info' = 'info') { if (!(['error', 'warning', 'info'].includes(level))) { throw new Error('Unrecognised log level'); } diff --git a/src/Service/Request.service.js b/src/services/RequestService.ts similarity index 54% rename from src/Service/Request.service.js rename to src/services/RequestService.ts index b6ad5533..4070ffb7 100644 --- a/src/Service/Request.service.js +++ b/src/services/RequestService.ts @@ -1,16 +1,13 @@ -/* eslint-disable class-methods-use-this */ -/* eslint-disable sonarjs/no-duplicate-string */ -/* @flow */ - import QueryString from 'querystring'; +import { APIGatewayProxyEvent } from 'aws-lambda'; import useragent from 'useragent'; import validate from 'validate.js/validate'; import XML2JS from 'xml2js'; -import { DEFINITIONS } from '../Config/Dependencies'; -import DependencyAwareClass from '../DependencyInjection/DependencyAware.class'; -import ResponseModel from '../Model/Response.model'; +import DependencyAwareClass from '../core/DependencyAwareClass'; +import ResponseModel from '../models/ResponseModel'; +import LoggerService from './LoggerService'; export const REQUEST_TYPES = { DELETE: 'DELETE', @@ -40,34 +37,42 @@ export const ERROR_TYPES = { VALIDATION_ERROR: new ResponseModel({}, 400, 'required fields are missing'), }; +export type RequestFile = { + type: string; + filename: string; + contentType: string; + content: string | Buffer; +}; + /** - * RequestService class + * Provides access to components of the HTTP request being handled. */ export default class RequestService extends DependencyAwareClass { /** * Get a parameter from the request. * * @param parameter - * @param ifNull + * @param ifNull Value to return if the parameter is not set. * @param requestType */ - get(parameter: string, ifNull = null, requestType = null) { + get(parameter: string, ifNull?: string | null, requestType?: string): string | string[] | null { const queryParameters = this.getAll(requestType); if (queryParameters === null) { - return ifNull; + return ifNull ?? null; } - return typeof queryParameters[parameter] !== 'undefined' ? queryParameters[parameter] : ifNull; + return queryParameters[parameter] ?? ifNull ?? null; } /** * Get all HTTP headers included in the request. * - * @returns {object} An object with a key for each header. + * @returns An object with a key for each header. */ getAllHeaders() { - return { ...this.getContainer().getEvent().headers }; + const event = this.getContainer().getEvent() as APIGatewayProxyEvent; + return { ...event.headers }; } /** @@ -75,12 +80,11 @@ export default class RequestService extends DependencyAwareClass { * * The header name is case-insensitive. * - * @param {string} name The name of the header. - * @param {string} [whenMissing] Value to return if the header is missing. + * @param name The name of the header. + * @param [whenMissing] Value to return if the header is missing. * (default: empty string) - * @returns {string} */ - getHeader(name: string, whenMissing: string = '') { + getHeader(name: string, whenMissing = ''): string { const headers = this.getAllHeaders(); if (!headers) { return whenMissing; @@ -91,11 +95,9 @@ export default class RequestService extends DependencyAwareClass { } /** - * Get authorization token - * - * @returns {*} + * Get an authorization token from the `Authorization` header. */ - getAuthorizationToken() { + getAuthorizationToken(): string | null { const authorization = this.getHeader('Authorization'); if (!authorization) { return null; @@ -112,22 +114,22 @@ export default class RequestService extends DependencyAwareClass { } /** - * Get a path parameter + * Get a path parameter, or all path parameters if no `parameter` is given. * * @param parameter - * @param ifNull mixed + * @param ifNull Value to return if the parameter is not set. */ - getPathParameter(parameter: string = null, ifNull = {}) { - const event = this.getContainer().getEvent(); + getPathParameter(parameter?: string, ifNull = {}): any { + const event = this.getContainer().getEvent() as APIGatewayProxyEvent; // If no parameter has been requested, return all path parameters - if (parameter === null && typeof event.pathParameters === 'object') { + if (!parameter && typeof event.pathParameters === 'object') { return event.pathParameters; } // If a specifc parameter has been requested, return the parameter if it exists if ( - typeof parameter === 'string' + parameter && typeof event.pathParameters === 'object' && event.pathParameters !== null && typeof event.pathParameters[parameter] !== 'undefined' @@ -142,42 +144,50 @@ export default class RequestService extends DependencyAwareClass { * Get all request parameters * * @param requestType - * @returns {{}} */ - // eslint-disable-next-line sonarjs/cognitive-complexity - getAll(requestType = null) { - const event = this.getContainer().getEvent(); + getAll(requestType?: string): any { + const event = this.getContainer().getEvent() as APIGatewayProxyEvent; - if (HTTP_METHODS_WITHOUT_PAYLOADS.includes(event.httpMethod) || HTTP_METHODS_WITHOUT_PAYLOADS.includes(requestType)) { + if ( + HTTP_METHODS_WITHOUT_PAYLOADS.includes(event.httpMethod) + || HTTP_METHODS_WITHOUT_PAYLOADS.includes(requestType || '') + ) { // get simple parameters - const params = { ...event.queryStringParameters }; + const params: Record = { + ...event.queryStringParameters, + }; // add array parameters as arrays - Object.keys(params) - .filter((key) => key.endsWith('[]')) - .forEach((key) => { - params[key] = event.multiValueQueryStringParameters[key]; - }); + if (event.multiValueQueryStringParameters !== null) { + Object.keys(params) + .filter((key) => key.endsWith('[]')) + .forEach((key) => { + params[key] = event.multiValueQueryStringParameters?.[key]; + }); + } return params; } - if (HTTP_METHODS_WITH_PAYLOADS.includes(event.httpMethod) || HTTP_METHODS_WITH_PAYLOADS.includes(requestType)) { + if ( + HTTP_METHODS_WITH_PAYLOADS.includes(event.httpMethod) + || HTTP_METHODS_WITH_PAYLOADS.includes(requestType || '') + ) { const contentType = this.getHeader('Content-Type'); let queryParameters = {}; if (contentType.includes('application/x-www-form-urlencoded')) { - queryParameters = QueryString.parse(event.body); + queryParameters = QueryString.parse(event.body as string); } if (contentType.includes('application/json')) { try { - queryParameters = JSON.parse(event.body); + queryParameters = JSON.parse(event.body as string); } catch { queryParameters = {}; } } if (contentType.includes('text/xml')) { - XML2JS.parseString(event.body, (error, result) => { + XML2JS.parseString(event.body as string, (error, result) => { queryParameters = error ? {} : result; }); } @@ -194,10 +204,8 @@ export default class RequestService extends DependencyAwareClass { /** * Fetch the request IP address - * - * @returns {*} */ - getIp() { + getIp(): string | null { const event = this.getContainer().getEvent(); if ( @@ -212,13 +220,11 @@ export default class RequestService extends DependencyAwareClass { } /** - * Get user agent - * - * @returns {*} + * Get user agent details from the `User-Agent` header. */ getUserBrowserAndDevice() { - const userAgent = this.getHeader('user-agent', null); - if (userAgent === null) { + const userAgent = this.getHeader('user-agent'); + if (!userAgent) { return null; } @@ -234,20 +240,20 @@ export default class RequestService extends DependencyAwareClass { 'operating-system-version': agent.os.toVersion(), }; } catch { - this.getContainer().get(DEFINITIONS.LOGGER).label('user-agent-parsing-failed'); - + this.di.get(LoggerService).label('user-agent-parsing-failed'); return null; } } /** - * Test a request against validation constraints + * Test a request against validation constraints. + * + * See [validate.js](https://validatejs.org/) for how to write constraints. * * @param constraints - * @returns {Promise} */ - validateAgainstConstraints(constraints: object) { - const Logger = this.getContainer().get(DEFINITIONS.LOGGER); + validateAgainstConstraints(constraints: object): Promise { + const logger = this.di.get(LoggerService); return new Promise((resolve, reject) => { const validation = validate(this.getAll(), constraints); @@ -255,49 +261,54 @@ export default class RequestService extends DependencyAwareClass { if (typeof validation === 'undefined') { resolve(); } else { - Logger.label('request-validation-failed'); + logger.label('request-validation-failed'); const validationErrorResponse = ERROR_TYPES.VALIDATION_ERROR; validationErrorResponse.setBodyVariable('validation_errors', validation); - reject(validationErrorResponse); } }); } /** - * Fetch the request multipart form + * Fetch the request multipart form. * - * @param useBuffer - * @returns {*} + * @param useBuffer Whether to return file content as a `Buffer`. */ parseForm(useBuffer: boolean) { - const event = this.getContainer().getEvent(); - const boundary = this.getBoundary(event); + // todo: rewrite this to use a dedicated package and add error handling + /* eslint-disable @typescript-eslint/no-non-null-assertion */ + + const event = this.getContainer().getEvent() as APIGatewayProxyEvent; + const boundary = RequestService.getBoundary(event) as string; - const body = event.isBase64Encoded ? Buffer.from(event.body, 'base64').toString('binary').trim() : event.body; + const body = event.isBase64Encoded + ? Buffer.from(event.body as string, 'base64').toString('binary').trim() + : event.body as string; - const result = {}; + const result: Record = {}; body.split(boundary).forEach((item) => { if (/filename=".+"/g.test(item)) { - result[item.match(/name=".+";/g)[0].slice(6, -2)] = { + const name = item.match(/name=".+";/g)![0].slice(6, -2); + result[name] = { type: 'file', - filename: item.match(/filename=".+"/g)[0].slice(10, -1), - contentType: item.match(/Content-Type:\s.+/g)[0].slice(14), + filename: item.match(/filename=".+"/g)![0].slice(10, -1), + contentType: item.match(/Content-Type:\s.+/g)![0].slice(14), content: useBuffer - ? Buffer.from(item.slice(item.search(/Content-Type:\s.+/g) + item.match(/Content-Type:\s.+/g)[0].length + 4, -4), 'binary') - : item.slice(item.search(/Content-Type:\s.+/g) + item.match(/Content-Type:\s.+/g)[0].length + 4, -4), + ? Buffer.from(item.slice(item.search(/Content-Type:\s.+/g) + item.match(/Content-Type:\s.+/g)![0].length + 4, -4), 'binary') + : item.slice(item.search(/Content-Type:\s.+/g) + item.match(/Content-Type:\s.+/g)![0].length + 4, -4), }; } else if (/name=".+"/g.test(item)) { - result[item.match(/name=".+"/g)[0].slice(6, -1)] = item.slice(item.search(/name=".+"/g) + item.match(/name=".+"/g)[0].length + 4, -4); + result[item.match(/name=".+"/g)![0].slice(6, -1)] = item.slice(item.search(/name=".+"/g) + item.match(/name=".+"/g)![0].length + 4, -4); } }); + + /* eslint-enable @typescript-eslint/no-non-null-assertion */ + return result; } /** * Fetch the request AWS event Records - * - * @returns {*} */ getAWSRecords() { const event = this.getContainer().getEvent(); @@ -310,15 +321,15 @@ export default class RequestService extends DependencyAwareClass { } /** - * Gets a value independently from - * the case of the key + * Gets a value independently from the case of the key. * * @param object * @param key */ - getValueIgnoringKeyCase(object, key) { - const foundKey = Object.keys(object).find((currentKey) => currentKey.toLocaleLowerCase() === key.toLowerCase()); - return object[foundKey]; + static getValueIgnoringKeyCase(object: Record, key: string): string | undefined { + const foundKey = Object.keys(object) + .find((currentKey) => currentKey.toLocaleLowerCase() === key.toLowerCase()); + return foundKey && object[foundKey]; } /** @@ -327,7 +338,7 @@ export default class RequestService extends DependencyAwareClass { * * @param event */ - getBoundary(event) { - return this.getValueIgnoringKeyCase(event.headers, 'Content-Type').split('=')[1]; + static getBoundary(event: APIGatewayProxyEvent): string | undefined { + return this.getValueIgnoringKeyCase(event.headers, 'Content-Type')?.split('=')?.[1]; } } diff --git a/src/Service/SQS.service.js b/src/services/SQSService.ts similarity index 52% rename from src/Service/SQS.service.js rename to src/services/SQSService.ts index 1d4dbbf2..1144f016 100644 --- a/src/Service/SQS.service.js +++ b/src/services/SQSService.ts @@ -1,14 +1,52 @@ -/* @flow */ import alai from 'alai'; -import each from 'async/each'; +import { each } from 'async'; import AWS from 'aws-sdk'; -import { v4 as UUID } from 'uuid'; +import { v4 as uuid } from 'uuid'; -import { DEFINITIONS } from '../Config/Dependencies'; -import DependencyAwareClass from '../DependencyInjection/DependencyAware.class'; -import DependencyInjection from '../DependencyInjection/DependencyInjection.class'; -import SQSMessageModel from '../Model/SQS/Message.model'; -import StatusModel, { STATUS_TYPES } from '../Model/Status.model'; +import DependencyAwareClass from '../core/DependencyAwareClass'; +import DependencyInjection from '../core/DependencyInjection'; +import SQSMessageModel from '../models/SQSMessageModel'; +import StatusModel, { STATUS_TYPES } from '../models/StatusModel'; +import LoggerService from './LoggerService'; +import TimerService from './TimerService'; + +export interface SQSServiceConfig { + /** + * Maps short friendly queue names to the full SQS queue name. + * + * Usually we define queue names in our `serverless.yml` and provide them to + * the application via environment variables. Example: + * + * ```ts + * { + * queues: { + * submissions: process.env.SQS_QUEUE_SUBMISSIONS, + * } + * } + * ``` + */ + queues?: Record; + /** + * Maps short friendly queue names to the queue consumer function name, for + * use with offline SQS emulation. Example: + * + * ```ts + * { + * queueConsumers: { + * submissions: 'SubmissionConsumer', + * } + * } + * ``` + * + * See the [SQSService docs](../../docs/services/SQSService.md) for details + * about how this works. + */ + queueConsumers?: Record; +} + +export interface WithSQSServiceConfig { + sqs?: SQSServiceConfig; +} /** * Allowed values for `process.env.LAMBDA_WRAPPER_OFFLINE_SQS_MODE`. @@ -34,38 +72,72 @@ export const SQS_OFFLINE_MODES = { }; /** - * Defines the preferred behaviour - * for SQSService.prototype.publish - * should AWS SQS fail. + * Defines the preferred behaviour for `SQSService.prototype.send` in case the + * AWS SQS call fails. */ export const SQS_PUBLISH_FAILURE_MODES = { /** - * Catches the exception and logs it. - * This is the default behaviour - * for LambdaWrapper 1.8.0 and below - * and for LambdaWrapper 1.8.2 and above + * Catch the exception and logs it. + * + * This is the default behaviour for Lambda Wrapper v1.8.0 and below and for + * Lambda Wrapper v1.8.2 and above. */ CATCH: 'catch', /** - * Throws the exception so that the caller - * can handle it directly. + * Throw the exception so that the caller can handle it directly. */ THROW: 'throw', -}; +} as const; /** - * SQSService class + * Helper service for working with SQS. + * + * Config for this service goes in the `sqs` key of your Lambda Wrapper config. + * The `queues` key maps short friendly names to the full SQS queue name. + * Usually we define queue names in our `serverless.yml` and provide them to + * the application via environment variables. + * + * ```ts + * const lambdaWrapper = lw.configure({ + * sqs: { + * queues: { + * // add an entry for each queue mapping to its AWS name + * submissions: process.env.SQS_QUEUE_SUBMISSIONS, + * }, + * }, + * }); + * ``` + * + * You can then send messages to a queue within your Lambda handler using the + * `publish` method. + * + * ```ts + * export default lambdaWrapper.wrap(async (di) => { + * const sqs = di.get(SQSService); + * const message = { data: 'Hello SQS!' }; + * await sqs.publish('submissions', message); + * }); + * ``` */ export default class SQSService extends DependencyAwareClass { - /** - * SQSService constructor - * - * @param di DependencyInjection - */ + readonly queues: Record; + + readonly queueConsumers: Record; + + readonly queueUrls: Record; + + private $sqs?: AWS.SQS; + + private $lambda?: AWS.Lambda; + constructor(di: DependencyInjection) { super(di); + const config = (this.di.config as WithSQSServiceConfig).sqs; + this.queues = config?.queues || {}; + this.queueConsumers = config?.queueConsumers || {}; + const { LAMBDA_WRAPPER_OFFLINE_SQS_HOST: offlineHost = 'localhost', LAMBDA_WRAPPER_OFFLINE_SQS_PORT: offlinePort = '4576', @@ -74,33 +146,22 @@ export default class SQSService extends DependencyAwareClass { REGION, } = process.env; - const container = this.getContainer(); - const context = container.getContext(); - const queues = container.getConfiguration('QUEUES'); - const accountId = (context && context.invokedFunctionArn && alai.parse(context)) || AWS_ACCOUNT_ID; - - this.queues = {}; - - this.$lambda = null; - this.$sqs = null; + const accountId = (di.context.invokedFunctionArn && alai.parse(di.context)) + || AWS_ACCOUNT_ID; - if (container.isOffline && !Object.values(SQS_OFFLINE_MODES).includes(offlineMode)) { + if (di.isOffline && !Object.values(SQS_OFFLINE_MODES).includes(offlineMode)) { throw new Error(`Invalid LAMBDA_WRAPPER_OFFLINE_SQS_MODE: ${offlineMode}\n` + `Please use one of: ${Object.values(SQS_OFFLINE_MODES).join(', ')}`); } - // Add the queues from configuration - if (queues !== null && Object.keys(queues).length > 0) { - Object.keys(queues).forEach((queueDefinition) => { - if (container.isOffline && offlineMode === SQS_OFFLINE_MODES.LOCAL) { - // custom URL when using an offline SQS service such as Localstack - this.queues[queueDefinition] = `http://${offlineHost}:${offlinePort}/queue/${queues[queueDefinition]}`; - } else { - // default AWS queue URL - this.queues[queueDefinition] = `https://sqs.${REGION}.amazonaws.com/${accountId}/${queues[queueDefinition]}`; - } - }); - } + const useLocalQueues = di.isOffline && offlineMode === SQS_OFFLINE_MODES.LOCAL; + this.queueUrls = Object.fromEntries( + Object.entries(this.queues).map(( + ([key, queueName]) => [key, useLocalQueues + ? `http://${offlineHost}:${offlinePort}/queue/${queueName}` + : `https://sqs.${REGION}.amazonaws.com/${accountId}/${queueName}`] + )), + ); } /** @@ -154,23 +215,21 @@ export default class SQSService extends DependencyAwareClass { } /** - * Batch delete messages + * Batch delete messages. * - * @param queue strung - * @param messageModels [SQSMessageModel] - * @returns {Promise} + * @param queue + * @param messageModels */ - batchDelete(queue: string, messageModels: [SQSMessageModel]) { - const container = this.getContainer(); - const queueUrl = this.queues[queue]; - const Logger = container.get(DEFINITIONS.LOGGER); - const Timer = container.get(DEFINITIONS.TIMER); - const timerId = `sqs-batch-delete-${UUID()} - Queue: '${queueUrl}'`; + batchDelete(queue: string, messageModels: SQSMessageModel[]): Promise { + const queueUrl = this.queueUrls[queue]; + const logger = this.di.get(LoggerService); + const timer = this.di.get(TimerService); + const timerId = `sqs-batch-delete-${uuid()} - Queue: '${queueUrl}'`; - return new Promise((resolve) => { - const messagesForDeletion = []; + return new Promise((resolve) => { + const messagesForDeletion: { Id: string; ReceiptHandle: string }[] = []; - Timer.start(timerId); + timer.start(timerId); // assuming openFiles is an array of file names each( messageModels, @@ -185,7 +244,7 @@ export default class SQSService extends DependencyAwareClass { }, (loopError) => { if (loopError) { - Logger.error(loopError); + logger.error(loopError); resolve(); } @@ -195,10 +254,10 @@ export default class SQSService extends DependencyAwareClass { QueueUrl: queueUrl, }, (error) => { - Timer.stop(timerId); + timer.stop(timerId); if (error) { - Logger.error(error); + logger.error(error); } resolve(); @@ -210,26 +269,23 @@ export default class SQSService extends DependencyAwareClass { } /** - * Check SQS status - * - * @returns {Promise} + * Check SQS status. */ checkStatus() { - const container = this.getContainer(); - const Logger = container.get(DEFINITIONS.LOGGER); - const Timer = container.get(DEFINITIONS.TIMER); - const timerId = `sqs-list-queues-${UUID()}`; + const logger = this.di.get(LoggerService); + const timer = this.di.get(TimerService); + const timerId = `sqs-list-queues-${uuid()}`; return new Promise((resolve) => { - Timer.start(timerId); + timer.start(timerId); this.sqs.listQueues({}, (error, data) => { - Timer.stop(timerId); + timer.stop(timerId); const statusModel = new StatusModel('SQS', STATUS_TYPES.OK); if (error) { - Logger.error(error); + logger.error(error); statusModel.setStatus(STATUS_TYPES.APPLICATION_FAILURE); } @@ -243,20 +299,18 @@ export default class SQSService extends DependencyAwareClass { } /** - * Get number of messages in a queue + * Get the approximate number of messages in a queue. * * @param queue - * @returns {Promise} */ - getMessageCount(queue: string) { - const container = this.getContainer(); - const queueUrl = this.queues[queue]; - const Logger = container.get(DEFINITIONS.LOGGER); - const Timer = container.get(DEFINITIONS.TIMER); - const timerId = `sqs-get-queue-attributes-${UUID()} - Queue: '${queueUrl}'`; + getMessageCount(queue: string): Promise { + const queueUrl = this.queueUrls[queue]; + const logger = this.di.get(LoggerService); + const timer = this.di.get(TimerService); + const timerId = `sqs-get-queue-attributes-${uuid()} - Queue: '${queueUrl}'`; return new Promise((resolve) => { - Timer.start(timerId); + timer.start(timerId); this.sqs.getQueueAttributes( { @@ -264,21 +318,22 @@ export default class SQSService extends DependencyAwareClass { QueueUrl: queueUrl, }, (error, data) => { - Timer.stop(timerId); + timer.stop(timerId); if (error) { - Logger.error(error); + logger.error(error); resolve(0); } - resolve(Number.parseInt(data.Attributes.ApproximateNumberOfMessages, 10)); + const messageCount = data.Attributes?.ApproximateNumberOfMessages || '0'; + resolve(Number.parseInt(messageCount, 10)); }, ); }); } /** - * Publish to message queue + * Publish to message queue. * * When running within serverless-offline, messages can be published to a * local Lambda or SQS service instead of to AWS, depending on the offline @@ -287,42 +342,40 @@ export default class SQSService extends DependencyAwareClass { * @param queue string * @param messageObject object * @param messageGroupId string - * @param {'catch' | 'throw'} failureMode Choose how failures are handled: + * @param failureMode Choose how failures are handled: * - `catch`: errors will be caught and logged. This is the default. * - `throw`: errors will be thrown, causing promise to reject. - * @returns {Promise} */ - async publish(queue: string, messageObject: object, messageGroupId = null, failureMode = SQS_PUBLISH_FAILURE_MODES.CATCH) { + async publish(queue: string, messageObject: object, messageGroupId = null, failureMode: 'catch' | 'throw' = SQS_PUBLISH_FAILURE_MODES.CATCH) { if (!Object.values(SQS_PUBLISH_FAILURE_MODES).includes(failureMode)) { throw new Error(`Invalid value for 'failureMode': ${failureMode}`); } - const container = this.getContainer(); - const queueUrl = this.queues[queue]; - const Timer = container.get(DEFINITIONS.TIMER); - const timerId = `sqs-send-message-${UUID()} - Queue: '${queueUrl}'`; + const queueUrl = this.queueUrls[queue]; + const timer = this.di.get(TimerService); + const timerId = `sqs-send-message-${uuid()} - Queue: '${queueUrl}'`; - Timer.start(timerId); + timer.start(timerId); - const messageParameters = { + const messageParameters: AWS.SQS.SendMessageRequest = { MessageBody: JSON.stringify(messageObject), QueueUrl: queueUrl, }; if (queueUrl.includes('.fifo')) { - messageParameters.MessageDeduplicationId = UUID(); - messageParameters.MessageGroupId = messageGroupId !== null ? messageGroupId : UUID(); + messageParameters.MessageDeduplicationId = uuid(); + messageParameters.MessageGroupId = messageGroupId !== null ? messageGroupId : uuid(); } try { - if (container.isOffline && this.constructor.offlineMode === SQS_OFFLINE_MODES.DIRECT) { + if (this.di.isOffline && SQSService.offlineMode === SQS_OFFLINE_MODES.DIRECT) { await this.publishOffline(queue, messageParameters); } else { await this.sqs.sendMessage(messageParameters).promise(); } } catch (error) { if (failureMode === SQS_PUBLISH_FAILURE_MODES.CATCH) { - container.get(DEFINITIONS.LOGGER).error(error); + this.di.get(LoggerService).error(error); return null; } throw error; @@ -341,18 +394,18 @@ export default class SQSService extends DependencyAwareClass { * @param queue * @param messageParameters */ - async publishOffline(queue: string, messageParameters) { - const container = this.getContainer(); - - if (!container.isOffline) { + async publishOffline(queue: string, messageParameters: AWS.SQS.SendMessageRequest) { + if (!this.di.isOffline) { throw new Error('Can only publishOffline while running serverless offline.'); } - const consumers = container.getConfiguration('QUEUE_CONSUMERS') || {}; - const FunctionName = consumers[queue]; + const FunctionName = this.queueConsumers[queue]; if (!FunctionName) { - throw new Error(`Queue consumer for queue ${queue} was not found. Please configure your application's QUEUE_CONSUMERS.`); + throw new Error( + `Queue consumer for queue ${queue} was not found. Please add it to ` + + 'the sqs.queueConsumers key in your Lambda Wrapper config.', + ); } const InvocationType = 'RequestResponse'; @@ -375,17 +428,15 @@ export default class SQSService extends DependencyAwareClass { * * @param queue string * @param timeout number - * @returns {Promise} */ - receive(queue: string, timeout: number = 15) { - const container = this.getContainer(); - const queueUrl = this.queues[queue]; - const Logger = container.get(DEFINITIONS.LOGGER); - const Timer = container.get(DEFINITIONS.TIMER); - const timerId = `sqs-receive-message-${UUID()} - Queue: '${queueUrl}'`; + receive(queue: string, timeout = 15): Promise { + const queueUrl = this.queueUrls[queue]; + const logger = this.di.get(LoggerService); + const timer = this.di.get(TimerService); + const timerId = `sqs-receive-message-${uuid()} - Queue: '${queueUrl}'`; return new Promise((resolve, reject) => { - Timer.start(timerId); + timer.start(timerId); this.sqs.receiveMessage( { @@ -394,10 +445,10 @@ export default class SQSService extends DependencyAwareClass { MaxNumberOfMessages: 10, }, (error, data) => { - Timer.stop(timerId); + timer.stop(timerId); if (error) { - Logger.error(error); + logger.error(error); return reject(error); } diff --git a/src/services/TimerService.ts b/src/services/TimerService.ts new file mode 100644 index 00000000..962cbb20 --- /dev/null +++ b/src/services/TimerService.ts @@ -0,0 +1,33 @@ +import DependencyAwareClass from '../core/DependencyAwareClass'; +import LoggerService from './LoggerService'; + +/** + * Timer helper that can be used to measure how long operations take. + */ +export default class TimerService extends DependencyAwareClass { + timers: Record = {}; + + /** + * Start a timer. + * + * To stop the timer, call `stop()` with the same `identifier`. + * + * @param identifier + */ + start(identifier: string) { + this.timers[identifier] = Date.now(); + } + + /** + * Stop a timer and log the elapsed time. + * + * @param identifier + */ + stop(identifier: string) { + if (identifier in this.timers) { + const logger = this.di.get(LoggerService); + const duration = Date.now() - this.timers[identifier]; + logger.info(`Timing - ${identifier} took ${duration} ms to complete`); + } + } +} diff --git a/src/utils/LambdaTermination.ts b/src/utils/LambdaTermination.ts new file mode 100644 index 00000000..cda88e36 --- /dev/null +++ b/src/utils/LambdaTermination.ts @@ -0,0 +1,22 @@ +/** + * An error that triggers a Lambda termination. + * + * Offers developer details (that are logged), a code for the Lambda and a + * front-facing consumer message. + */ +export default class LambdaTermination extends Error { + constructor( + readonly internal: object | string, + readonly code = 500, + readonly body: object | string | null = null, + readonly details = 'unknown error', + ) { + const stringified = typeof internal === 'string' + ? internal + : JSON.stringify(internal); + + super(stringified); + + this.body = body || 'unknown error'; + } +} diff --git a/src/Wrapper/PromisifiedDelay.js b/src/utils/PromisifiedDelay.ts similarity index 81% rename from src/Wrapper/PromisifiedDelay.js rename to src/utils/PromisifiedDelay.ts index 94992ea9..d2c3cbee 100644 --- a/src/Wrapper/PromisifiedDelay.js +++ b/src/utils/PromisifiedDelay.ts @@ -18,31 +18,29 @@ const HIGH_LATENCY_DELAYS = { * PromisifiedDelay class */ export default class PromisifiedDelay { + readonly delays: number[] = []; + /** * PromisifiedDelay constructor * * @param highLatency */ constructor(highLatency = true) { - this.delays = []; - const delayArray = highLatency === true ? HIGH_LATENCY_DELAYS : STANDARD_LATENCY_DELAYS; Object.keys(delayArray).forEach((delayDuration) => { - const delayIterations = delayArray[delayDuration]; + const delayIterations = (delayArray as any)[delayDuration]; for (let i = 0; i < delayIterations; i += 1) { - this.delays.push(delayDuration); + this.delays.push(Number.parseInt(delayDuration, 10)); } }); } /** * Create a promisified delay - * - * @returns {Promise} */ - get() { + get(): Promise { return new Promise((resolve) => { setTimeout(() => { resolve(); diff --git a/tests/.eslintrc.yml b/tests/.eslintrc.yml index 446a915a..4f0ac1cc 100644 --- a/tests/.eslintrc.yml +++ b/tests/.eslintrc.yml @@ -1,2 +1,6 @@ extends: - '@comicrelief/eslint-config/mixins/jest' + +rules: + max-classes-per-file: off + '@typescript-eslint/no-non-null-assertion': off diff --git a/tests/lib/mocks.js b/tests/lib/mocks.js deleted file mode 100644 index 656e57ce..00000000 --- a/tests/lib/mocks.js +++ /dev/null @@ -1,49 +0,0 @@ -import { DEFINITIONS } from '../../src/Config/Dependencies'; - -/** - * Returns a mocked logger. - * - * You can pass an overrides object - * specifying the return values - * and/or behaviour for each property. - * - * @param {object} overrides - * @param {object} di - */ -export const getMockedLogger = (overrides = {}, di = null) => { - const logger = { - di, - error: jest.fn(() => overrides.error || null), - info: jest.fn(() => overrides.info || null), - metric: jest.fn(() => overrides.metric || null), - }; - - logger.getContainer = () => logger.di; - - return logger; -}; - -/** - * Returns a mocked di. - * - * You can pass an overrides object - * specifying the behaviour of a depedendency. - * - * @param {object} overrides - */ -export const getMockedDi = (overrides = {}) => { - const deps = { - [DEFINITIONS.LOGGER]: null, - ...overrides, - }; - const di = { - deps, - get: (key) => deps[key], - }; - - if (!deps[DEFINITIONS.LOGGER]) { - deps[DEFINITIONS.LOGGER] = getMockedLogger({}, di); - } - - return di; -}; diff --git a/tests/mocks/aws/index.ts b/tests/mocks/aws/index.ts new file mode 100644 index 00000000..adcaa6f5 --- /dev/null +++ b/tests/mocks/aws/index.ts @@ -0,0 +1,6 @@ +import { Context } from '@/src'; +import context from '@/tests/mocks/aws/context.json'; +import event from '@/tests/mocks/aws/event.json'; + +export const mockContext = context as Context; +export const mockEvent = event; diff --git a/tests/unit/DependencyInjection/DependencyAware.class.test.js b/tests/unit/DependencyInjection/DependencyAware.class.test.js deleted file mode 100644 index 3b151fde..00000000 --- a/tests/unit/DependencyInjection/DependencyAware.class.test.js +++ /dev/null @@ -1,32 +0,0 @@ -import DependencyInjection from '../../../src/DependencyInjection/DependencyInjection.class'; -// The import order is relevant here to avoid circular imports -// eslint-disable-next-line import/order -import DependencyAware from '../../../src/DependencyInjection/DependencyAware.class'; -import getContext from '../../mocks/aws/context.json'; -import getEvent from '../../mocks/aws/event.json'; - -describe('DependencyInjection/DependencyAwareClass', () => { - describe('getContainer', () => { - const dependencyInjectionClass = new DependencyInjection({}, getEvent, getContext); - const dependencyAwareClass = new DependencyAware(dependencyInjectionClass); - - it('should instantiate and be able to get the dependency injection container', () => { - expect(dependencyAwareClass.getContainer()).toEqual(dependencyInjectionClass); - }); - }); - - describe('definitions', () => { - describe('Returns the provided definitions', () => { - [ - [{}, undefined], - [{ DEFINITIONS: 1 }, 1], - ].forEach(([configuration, expected]) => { - it(`With configuration: ${configuration}`, () => { - const di = new DependencyInjection(configuration); - const service = new DependencyAware(di); - expect(service.definitions).toEqual(expected); - }); - }); - }); - }); -}); diff --git a/tests/unit/DependencyInjection/DependencyInjection.class.test.js b/tests/unit/DependencyInjection/DependencyInjection.class.test.js deleted file mode 100644 index 4304f7a0..00000000 --- a/tests/unit/DependencyInjection/DependencyInjection.class.test.js +++ /dev/null @@ -1,101 +0,0 @@ -import { DEFINITIONS } from '../../../src/Config/Dependencies'; -import DependencyInjection from '../../../src/DependencyInjection/DependencyInjection.class'; -import LoggerService from '../../../src/Service/Logger.service'; -import RequestService from '../../../src/Service/Request.service'; -import getContext from '../../mocks/aws/context.json'; -import getEvent from '../../mocks/aws/event.json'; - -describe('DependencyInjection/DependencyInjectionClass', () => { - describe('should instantiate', () => { - const configuration = { - test: 123, - }; - const dependencyInjection = new DependencyInjection(configuration, getEvent, getContext); - - it('should output the event that was provided to it', () => { - expect(dependencyInjection.getEvent()).toEqual(getEvent); - }); - - it('should output the context that was provided to it', () => { - expect(dependencyInjection.getContext()).toEqual(getContext); - }); - - it('should output the configuration that was provided to it', () => { - expect(dependencyInjection.getConfiguration()).toEqual(configuration); - }); - }); - - describe('should get dependencies', () => { - const dependencyInjection = new DependencyInjection({}, getEvent, getContext); - - it('Should throw validation errors when an non existent model is requested', () => { - expect(() => dependencyInjection.get('test')).toThrow('test does not exist in di container'); - }); - - it('should fetch an instance of the logger service', () => { - expect(dependencyInjection.get(DEFINITIONS.LOGGER) instanceof LoggerService).toEqual(true); - }); - - it('should fetch an instance of the request service', () => { - const requestService = dependencyInjection.get(DEFINITIONS.REQUEST); - expect(requestService instanceof RequestService).toEqual(true); - expect(requestService.di instanceof DependencyInjection).toEqual(true); - }); - }); - - describe('isOffline', () => { - let useServerlessOffline; - - beforeAll(() => { - useServerlessOffline = process.env.USE_SERVERLESS_OFFLINE; - process.env.USE_SERVERLESS_OFFLINE = ''; - }); - - afterEach(() => { - process.env.USE_SERVERLESS_OFFLINE = ''; - }); - - afterAll(() => { - process.env.USE_SERVERLESS_OFFLINE = useServerlessOffline; - }); - - describe('is true', () => { - it("when context doesn't define an invokedFunctionArn", () => { - const di = new DependencyInjection({}, getEvent, {}); - expect(di.isOffline).toEqual(true); - }); - - it('When the invokedFunctionArn includes `offline`', () => { - const di = new DependencyInjection({}, getEvent, { invokedFunctionArn: 'my-offline-function' }); - expect(di.isOffline).toEqual(true); - }); - - it('When process.env.USE_SERVERLESS_OFFLINE is defined', () => { - process.env.USE_SERVERLESS_OFFLINE = 'true'; - const di = new DependencyInjection({}, getEvent, { invokedFunctionArn: 'my-function' }); - expect(di.isOffline).toEqual(true); - }); - }); - - describe('is false`', () => { - it("When the invokedFunctionArn doesn't contain `offline", () => { - const di = new DependencyInjection({}, getEvent, { invokedFunctionArn: 'my-function' }); - expect(di.isOffline).toEqual(false); - }); - }); - }); - - describe('definitions', () => { - describe('Returns the provided definitions', () => { - [ - [{}, undefined], - [{ DEFINITIONS: 1 }, 1], - ].forEach(([configuration, expected]) => { - it(`With configuration: ${configuration}`, () => { - const di = new DependencyInjection(configuration); - expect(di.definitions).toEqual(expected); - }); - }); - }); - }); -}); diff --git a/tests/unit/Model/CloudEvent.model.test.js b/tests/unit/Model/CloudEvent.model.test.js deleted file mode 100644 index 9e00179c..00000000 --- a/tests/unit/Model/CloudEvent.model.test.js +++ /dev/null @@ -1,58 +0,0 @@ -import CloudEventModel from '../../../src/Model/CloudEvent.model'; - -// Test definitions. -describe('Model/CloudEventModel', () => { - describe('Ensure setting and getting of variables', () => { - const model = new CloudEventModel(); - - it('should get the cloud event version', () => { - expect(model.getCloudEventsVersion()).toEqual('0.1'); - }); - - it('should set and get the event type', () => { - expect(model.getEventType()).toEqual(''); - const eventType = 'test.event'; - model.setEventType(eventType); - expect(model.getEventType()).toEqual(eventType); - }); - - it('should set and get the source', () => { - expect(model.getSource()).toEqual(''); - const source = 'test'; - model.setSource(source); - expect(model.getSource()).toEqual(source); - }); - - it('should generate a uuid as the event id', () => { - expect(model.getEventID().length).toEqual(36); - }); - - it('should generate the current timestamp as the current time', () => { - expect(new CloudEventModel().getEventTime().replace(/:[^:]+$/, '')).toEqual(new Date().toISOString().replace(/:[^:]+$/, '')); - }); - - it('should set and get the extensions', () => { - expect(model.getExtensions()).toEqual({}); - - const extensions = { - test: 'test', - }; - model.setExtensions(extensions); - expect(model.getExtensions()).toEqual(extensions); - }); - - it('should get the content type', () => { - expect(model.getContentType()).toEqual('application/json'); - }); - - it('should set and get the extensions', () => { - expect(model.getData()).toEqual({}); - - const data = { - test: 'test', - }; - model.setData(data); - expect(model.getData()).toEqual(data); - }); - }); -}); diff --git a/tests/unit/Model/SQS/MarketingPreferences.model.test.js b/tests/unit/Model/SQS/MarketingPreferences.model.test.js deleted file mode 100644 index a85b0906..00000000 --- a/tests/unit/Model/SQS/MarketingPreferences.model.test.js +++ /dev/null @@ -1,460 +0,0 @@ -/* eslint-disable sonarjs/no-identical-functions */ -/* eslint-disable sonarjs/no-duplicate-string */ -import ResponseModel from '../../../../src/Model/Response.model'; -import MarketingPreferencesModel from '../../../../src/Model/SQS/MarketingPreference.model'; - -// Test definitions. -describe('Model/MarketingPreferencesModel', () => { - describe('Ensure setting and getting of variables', () => { - const mockedData = { - firstname: 'Tim', - lastname: 'Jones', - phone: '0208 254 3062', - mobile: '07917 321 492', - address1: '32-36', - address2: "St. Smith's Avenue", - address3: '', - town: 'London', - postcode: 'sw184bx', - country: 'United Kindgom', - campaign: 'sr18', - transactionId: 'AN129MNDJDJ', - transSource: 'giftaid-sportrelief', - transSourceUrl: 'https://giftaid.sportrelief.com/', - transType: 'prefs', - email: 'tim.jones@comicrelief.com', - permissionEmail: 1, - permissionPost: 0, - permissionPhone: 0, - permissionSMS: 0, - timestamp: '1550841771', - }; - - const model = new MarketingPreferencesModel(mockedData); - - it('should set and get the firstname', () => { - expect(model.getFirstName()).toEqual(mockedData.firstname); - }); - - it('should set and get the lastname', () => { - expect(model.getLastName()).toEqual(mockedData.lastname); - }); - - it('should set and get the phone', () => { - expect(model.getPhone()).toEqual(mockedData.phone); - }); - - it('should set and get the mobile', () => { - expect(model.getMobile()).toEqual(mockedData.mobile); - }); - - it('should set and get the address1', () => { - expect(model.getAddress1()).toEqual(mockedData.address1); - }); - - it('should set and get the address2', () => { - expect(model.getAddress2()).toEqual(mockedData.address2); - }); - - it('should set and get the address3', () => { - expect(model.getAddress3()).toEqual(null); - }); - - it('should set and get the town', () => { - expect(model.getTown()).toEqual(mockedData.town); - }); - - it('should set and get the postcode', () => { - expect(model.getPostcode()).toEqual(mockedData.postcode); - }); - - it('should set and get the country', () => { - expect(model.getCountry()).toEqual(mockedData.country); - }); - - it('should set and get the campaign', () => { - expect(model.getCampaign()).toEqual(mockedData.campaign); - }); - - it('should set and get the transaction id', () => { - expect(model.getTransactionId()).toEqual(mockedData.transactionId); - }); - - it('should set and get the transSource', () => { - expect(model.getTransSource()).toEqual(mockedData.transSource); - }); - - it('should set and get the transSourceUrl', () => { - expect(model.getTransSourceUrl()).toEqual(mockedData.transSourceUrl); - }); - - it('should set and get the transType', () => { - expect(model.getTransType()).toEqual(mockedData.transType); - }); - - it('should set and get the email', () => { - expect(model.getEmail()).toEqual(mockedData.email); - }); - - it('should set and get the permissionEmail', () => { - expect(model.getPermissionEmail()).toEqual(mockedData.permissionEmail); - }); - - it('should set and get the permissionPost', () => { - expect(model.getPermissionPost()).toEqual(mockedData.permissionPost); - }); - - it('should set and get the permissionPhone', () => { - expect(model.getPermissionPhone()).toEqual(mockedData.permissionPhone); - }); - - it('should set and get the permissionSMS', () => { - expect(model.getPermissionSMS()).toEqual(mockedData.permissionSMS); - }); - - it('should set and get the Timestamp', () => { - expect(model.getTimestamp()).toEqual(mockedData.timestamp); - }); - - it('should validate the model', (done) => { - model - .validate() - .then(() => { - expect(true).toEqual(true); - done(); - }) - .catch(() => { - expect(true).toEqual(false); - done(); - }); - }); - }); - - describe('Ensure validation fails when variables are not correctly set', () => { - const mockedData = {}; - - const model = new MarketingPreferencesModel(mockedData); - - it('should validate the model and return an error response', (done) => { - model - .validate() - .then(() => { - expect(true).toEqual(false); - done(); - }) - .catch((error) => { - expect(error instanceof ResponseModel).toEqual(true); - expect(error.getCode()).toEqual(400); - expect(error.body.message).toEqual('required fields are missing'); - expect(true).toEqual(true); - done(); - }); - }); - }); - - describe('Ensure validation fails when email permission is set and no email is provided', () => { - const mockedData = { - firstname: 'Tim', - lastname: 'Jones', - mobile: '07917 321 492', - address1: '32-36', - address2: "St. Smith's Avenue", - address3: '', - town: 'London', - postcode: 'sw184bx', - country: 'United Kindgom', - campaign: 'sr18', - transactionId: 'AN129MNDJDJ', - transSource: 'giftaid-sportrelief', - transSourceUrl: 'https://giftaid.sportrelief.com/', - transType: 'prefs', - permissionEmail: 1, - permissionPost: 0, - permissionPhone: 0, - permissionSMS: 0, - timestamp: '1550841771', - }; - - const model = new MarketingPreferencesModel(mockedData); - - it('should validate the model and return an error response', (done) => { - model - .validate() - .then(() => { - expect(true).toEqual(false); - done(); - }) - .catch((error) => { - expect(error instanceof ResponseModel).toEqual(true); - expect(error.getCode()).toEqual(400); - expect(error.body.validation_errors.email[0]).toEqual("Email can't be blank"); - expect(true).toEqual(true); - done(); - }); - }); - }); - - describe('Ensure validation fails when email permission is set and invalid email is provided', () => { - const mockedData = { - firstname: 'Tim', - lastname: 'Jones', - phone: '0208 254 3062', - mobile: '07917 321 492', - address1: '32-36', - address2: "St. Smith's Avenue", - address3: '', - town: 'London', - postcode: 'sw184bx', - country: 'United Kindgom', - campaign: 'sr18', - transactionId: 'AN129MNDJDJ', - transSource: 'giftaid-sportrelief', - transSourceUrl: 'https://giftaid.sportrelief.com/', - transType: 'prefs', - email: 'tim@', - permissionEmail: 1, - permissionPost: 0, - permissionPhone: 0, - permissionSMS: 0, - }; - - const model = new MarketingPreferencesModel(mockedData); - - it('should validate the model and return an error response', (done) => { - model - .validate() - .then(() => { - expect(true).toEqual(false); - done(); - }) - .catch((error) => { - expect(error instanceof ResponseModel).toEqual(true); - expect(error.getCode()).toEqual(400); - expect(error.body.validation_errors.email[0]).toEqual('Email is not a valid email'); - expect(true).toEqual(true); - done(); - }); - }); - }); - - describe('Ensure validation passes when permissions are not set', () => { - const mockedData = { - firstname: 'Kelvin', - lastname: 'James', - phone: '0208 254 3062', - mobile: '07425253522', - address1: 'COMIC RELIEF', - address2: 'CAMELFORD HOUSE 87-90', - address3: 'ALBERT EMBANKMENT', - town: 'LONDON', - postcode: 'SE1 7TP', - country: 'GB', - campaign: 'RND19', - transactionId: 'AN129MNDJDJ', - transSource: 'RND19_GiftAid', - transSourceUrl: 'https://giftaid.sportrelief.com/', - transType: 'prefs', - confirm: 1, - permissionEmail: null, - permissionPost: null, - permissionPhone: null, - permissionSMS: null, - timestamp: '1562165588', - }; - - const model = new MarketingPreferencesModel(mockedData); - - it('should validate the model', (done) => { - model - .validate() - .then(() => { - expect(true).toEqual(true); - done(); - }) - .catch((error) => { - console.log('Error:', error); - expect(true).toEqual(false); - done(); - }); - }); - }); - - describe('Ensure validation passes when email permission is NO', () => { - const mockedData = { - firstname: 'Kelvin', - lastname: 'James', - phone: '0208 254 3062', - mobile: '07425253522', - address1: 'COMIC RELIEF', - address2: 'CAMELFORD HOUSE 87-90', - address3: 'ALBERT EMBANKMENT', - town: 'LONDON', - postcode: 'SE1 7TP', - country: 'GB', - campaign: 'RND19', - transactionId: 'AN129MNDJDJ', - transSource: 'RND19_GiftAid', - transSourceUrl: 'https://giftaid.sportrelief.com/', - transType: 'prefs', - confirm: 1, - permissionEmail: 0, - permissionPost: null, - permissionPhone: null, - permissionSMS: null, - timestamp: '1562165588', - }; - - const model = new MarketingPreferencesModel(mockedData); - - it('should validate the model', (done) => { - model - .validate() - .then(() => { - expect(true).toEqual(true); - done(); - }) - .catch((error) => { - console.log('Error:', error); - expect(true).toEqual(false); - done(); - }); - }); - }); - - describe('Ensure generating of timestamp when not set', () => { - const mockedData = { - firstname: 'Tim', - lastname: 'Jones', - phone: '0208 254 3062', - mobile: '07917 321 492', - address1: '32-36', - address2: "St. Smith's Avenue", - address3: '', - town: 'London', - postcode: 'sw184bx', - country: 'United Kindgom', - campaign: 'sr18', - transactionId: 'AN129MNDJDJ', - transSource: 'giftaid-sportrelief', - transSourceUrl: 'https://giftaid.sportrelief.com/', - transType: 'prefs', - email: 'tim.jones@comicrelief.com', - permissionEmail: 1, - permissionPost: 0, - permissionPhone: 0, - permissionSMS: 0, - }; - - const model = new MarketingPreferencesModel(mockedData); - - it('should get a timestamp', () => { - expect(model.getTimestamp() > 0).toEqual(true); - }); - - it('should validate the model', (done) => { - model - .validate() - .then(() => { - expect(true).toEqual(true); - done(); - }) - .catch(() => { - expect(true).toEqual(false); - done(); - }); - }); - }); - - describe('Ensure validation passes when nullable fields are not present', () => { - const mockedData = { - firstname: 'Tim', - lastname: 'Jones', - phone: '0208 254 3062', - mobile: '07917 321 492', - address1: "32 Smith's Avenue", - town: 'London', - postcode: 'sw184bx', - country: 'United Kindgom', - campaign: 'sr18', - transSource: 'giftaid-sportrelief', - transSourceUrl: 'https://giftaid.sportrelief.com/', - transType: 'prefs', - email: '', - }; - - const model = new MarketingPreferencesModel(mockedData); - - it('should validate the model', (done) => { - model - .validate() - .then(() => { - expect(true).toEqual(true); - done(); - }) - .catch((error) => { - console.log('Error:', error); - expect(true).toEqual(false); - done(); - }); - }); - }); - - describe('Ensure model permission evaluates to false when no permission is set', () => { - const mockedData = { - firstname: 'Tim', - lastname: 'Jones', - phone: '0208 254 3062', - mobile: '07917 321 492', - address1: "32 Smith's Avenue", - town: 'London', - postcode: 'sw184bx', - country: 'United Kindgom', - campaign: 'sr18', - transSource: 'giftaid-sportrelief', - transSourceUrl: 'https://giftaid.sportrelief.com/', - transType: 'prefs', - email: '', - permissionEmail: '', - permissionPost: '', - permissionPhone: '', - permissionSMS: '', - }; - - const model = new MarketingPreferencesModel(mockedData); - - it('should evaluate model permissions to false', (done) => { - expect(model.isPermissionSet()).toEqual(false); - done(); - }); - }); - - describe('Ensure model permission evaluates to true when at least one permission is set', () => { - const mockedData = { - firstname: 'Tim', - lastname: 'Jones', - phone: '0208 254 3062', - mobile: '07917 321 492', - address1: "32 Smith's Avenue", - town: 'London', - postcode: 'sw184bx', - country: 'United Kindgom', - campaign: 'sr18', - transactionId: 'AN129MNDJDJ', - transSource: 'giftaid-sportrelief', - transSourceUrl: 'https://giftaid.sportrelief.com/', - transType: 'prefs', - email: 'tim@example.com', - permissionEmail: 1, - permissionPost: '', - permissionPhone: '', - permissionSMS: '', - }; - - const model = new MarketingPreferencesModel(mockedData); - - it('should evaluate model permissions to true', (done) => { - expect(model.isPermissionSet()).toEqual(true); - done(); - }); - }); -}); diff --git a/tests/unit/Model/SQS/Message.model.test.js b/tests/unit/Model/SQS/Message.model.test.js deleted file mode 100644 index e2d4615c..00000000 --- a/tests/unit/Model/SQS/Message.model.test.js +++ /dev/null @@ -1,46 +0,0 @@ -import Message from '../../../../src/Model/SQS/Message.model'; - -// Test definitions. -describe('Model/SQS/Message.model', () => { - describe('Ensure setting and getting of variables', () => { - const messageData = { - test: 123, - }; - - const mockedMessage = { - MessageId: 123, - ReceiptHandle: 123, - Body: JSON.stringify(messageData), - }; - - const messageModel = new Message(mockedMessage); - - it('should set and get the message id', () => { - expect(messageModel.getMessageId()).toEqual(mockedMessage.MessageId); - }); - - it('should set and get the receipt handle', () => { - expect(messageModel.getReceiptHandle()).toEqual(mockedMessage.ReceiptHandle); - }); - - it('should set, parse the JSON and get the body', () => { - expect(messageModel.getBody()).toEqual(messageData); - }); - - it('should default to having a for deletion status of false', () => { - expect(messageModel.isForDeletion()).toEqual(false); - }); - - it('should be able to change the for deletion status to true', () => { - messageModel.setForDeletion(true); - expect(messageModel.isForDeletion()).toEqual(true); - }); - - it('should be able to set metadata', () => { - messageModel.setMetaData('test', 123); - expect(messageModel.getMetaData()).toEqual({ - test: 123, - }); - }); - }); -}); diff --git a/tests/unit/Model/Status.model.test.js b/tests/unit/Model/Status.model.test.js deleted file mode 100644 index 62d34d72..00000000 --- a/tests/unit/Model/Status.model.test.js +++ /dev/null @@ -1,22 +0,0 @@ -import StatusModel, { STATUS_TYPES } from '../../../src/Model/Status.model'; - -// Test definitions. -describe('Model/StatusModel', () => { - describe('Ensure setting and getting of variables', () => { - const service = 'test'; - const status = STATUS_TYPES.OK; - const statusModel = new StatusModel(service, status); - - it('should set ang get the service', () => { - expect(statusModel.getService()).toEqual(service); - }); - - it('should set and get the status', () => { - expect(statusModel.getStatus()).toEqual(status); - }); - - it('should throw an error when trying to set an invalid status', () => { - expect(() => statusModel.setStatus('invalid')).toThrow('StatusModel - invalid is not a valid status type'); - }); - }); -}); diff --git a/tests/unit/Service/__snapshots__/BaseConfig.service.test.js.snap b/tests/unit/Service/__snapshots__/BaseConfig.service.test.js.snap deleted file mode 100644 index 37742866..00000000 --- a/tests/unit/Service/__snapshots__/BaseConfig.service.test.js.snap +++ /dev/null @@ -1,35 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Service/BaseConfigService ensureHealthy 400 throws a LambdaTermination 1`] = `"Application is not healthy."`; - -exports[`Service/BaseConfigService ensureHealthy 401 throws a LambdaTermination 1`] = `"Application is not healthy."`; - -exports[`Service/BaseConfigService ensureHealthy 403 throws a LambdaTermination 1`] = `"Application is not healthy."`; - -exports[`Service/BaseConfigService ensureHealthy 404 throws a LambdaTermination 1`] = `"Application is not healthy."`; - -exports[`Service/BaseConfigService ensureHealthy 409 throws a LambdaTermination 1`] = `"Application is not healthy."`; - -exports[`Service/BaseConfigService ensureHealthy 499 throws a LambdaTermination 1`] = `"Application is not healthy."`; - -exports[`Service/BaseConfigService ensureHealthy 500 throws a LambdaTermination 1`] = `"Application is not healthy."`; - -exports[`Service/BaseConfigService ensureHealthy 501 throws a LambdaTermination 1`] = `"Application is not healthy."`; - -exports[`Service/BaseConfigService ensureHealthy 502 throws a LambdaTermination 1`] = `"Application is not healthy."`; - -exports[`Service/BaseConfigService ensureHealthy 503 throws a LambdaTermination 1`] = `"Application is not healthy."`; - -exports[`Service/BaseConfigService ensureHealthy 504 throws a LambdaTermination 1`] = `"Application is not healthy."`; - -exports[`Service/BaseConfigService ensureHealthy Dante Alighieri throws a LambdaTermination 1`] = `"Application is not healthy."`; - -exports[`Service/BaseConfigService get propagates the 404 1`] = `"404"`; - -exports[`Service/BaseConfigService get refuses empty configurations 1`] = `"Configuration file is empty"`; - -exports[`Service/BaseConfigService get refuses invalid configurations 1`] = `"Invalid configuration file"`; - -exports[`Service/BaseConfigService getOrCreate throws any non-404 error 1`] = `"Bad error"`; - -exports[`Service/BaseConfigService patch throws any non-404 error 1`] = `"Bad error"`; diff --git a/tests/unit/Service/__snapshots__/Logger.service.test.js.snap b/tests/unit/Service/__snapshots__/Logger.service.test.js.snap deleted file mode 100644 index 2afbafa3..00000000 --- a/tests/unit/Service/__snapshots__/Logger.service.test.js.snap +++ /dev/null @@ -1,138 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Service/LoggerService error Trims down the axios error: EMPTY 1`] = ` -Object { - "config": Object { - "method": "get", - "url": "http://localhost:9999", - }, - "message": "some-message", - "response": Object { - "data": undefined, - "status": undefined, - }, -} -`; - -exports[`Service/LoggerService error Trims down the axios error: HTTP_417 1`] = ` -Object { - "config": Object { - "method": "get", - "url": "http://localhost:9999", - }, - "message": "some-message", - "response": Object { - "data": Object { - "data": 1, - }, - "status": 417, - }, -} -`; - -exports[`Service/LoggerService error Trims down the axios error: UNDEFINED 1`] = ` -Object { - "config": Object { - "method": "get", - "url": "http://localhost:9999", - }, - "message": "some-message", -} -`; - -exports[`Service/LoggerService info Trims down the axios error: EMPTY 1`] = ` -Object { - "config": Object { - "method": "get", - "url": "http://localhost:9999", - }, - "message": "some-message", - "response": Object { - "data": undefined, - "status": undefined, - }, -} -`; - -exports[`Service/LoggerService info Trims down the axios error: HTTP_417 1`] = ` -Object { - "config": Object { - "method": "get", - "url": "http://localhost:9999", - }, - "message": "some-message", - "response": Object { - "data": Object { - "data": 1, - }, - "status": 417, - }, -} -`; - -exports[`Service/LoggerService info Trims down the axios error: UNDEFINED 1`] = ` -Object { - "config": Object { - "method": "get", - "url": "http://localhost:9999", - }, - "message": "some-message", -} -`; - -exports[`Service/LoggerService object Logs a '"a string"' with level: 'error' 1`] = `"My action: '\\"a string\\"'"`; - -exports[`Service/LoggerService object Logs a '"a string"' with level: 'info' 1`] = `"My action: '\\"a string\\"'"`; - -exports[`Service/LoggerService object Logs a '"a string"' with level: 'warning' 1`] = `"My action: '\\"a string\\"'"`; - -exports[`Service/LoggerService object Logs a '{"a":{"b":null},"c":"a string"}' with level: 'error' 1`] = ` -"My action: '{ - \\"a\\": { - \\"b\\": null - }, - \\"c\\": \\"a string\\" -}'" -`; - -exports[`Service/LoggerService object Logs a '{"a":{"b":null},"c":"a string"}' with level: 'info' 1`] = ` -"My action: '{ - \\"a\\": { - \\"b\\": null - }, - \\"c\\": \\"a string\\" -}'" -`; - -exports[`Service/LoggerService object Logs a '{"a":{"b":null},"c":"a string"}' with level: 'warning' 1`] = ` -"My action: '{ - \\"a\\": { - \\"b\\": null - }, - \\"c\\": \\"a string\\" -}'" -`; - -exports[`Service/LoggerService object Logs a '{"a":1}' with level: 'error' 1`] = ` -"My action: '{ - \\"a\\": 1 -}'" -`; - -exports[`Service/LoggerService object Logs a '{"a":1}' with level: 'info' 1`] = ` -"My action: '{ - \\"a\\": 1 -}'" -`; - -exports[`Service/LoggerService object Logs a '{"a":1}' with level: 'warning' 1`] = ` -"My action: '{ - \\"a\\": 1 -}'" -`; - -exports[`Service/LoggerService object Logs a 'null' with level: 'error' 1`] = `"My action: 'null'"`; - -exports[`Service/LoggerService object Logs a 'null' with level: 'info' 1`] = `"My action: 'null'"`; - -exports[`Service/LoggerService object Logs a 'null' with level: 'warning' 1`] = `"My action: 'null'"`; diff --git a/tests/unit/Service/__snapshots__/SQS.service.test.js.snap b/tests/unit/Service/__snapshots__/SQS.service.test.js.snap deleted file mode 100644 index 82939d44..00000000 --- a/tests/unit/Service/__snapshots__/SQS.service.test.js.snap +++ /dev/null @@ -1,7 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Service/SQS publish failure modes throws an error with the invalid value: 1`] = `"Invalid value for 'failureMode': "`; - -exports[`Service/SQS publish failure modes throws an error with the invalid value: another-value 1`] = `"Invalid value for 'failureMode': another-value"`; - -exports[`Service/SQS publish failure modes throws an error with the invalid value: null 1`] = `"Invalid value for 'failureMode': null"`; diff --git a/tests/unit/Wrapper/LambdaWrapper.test.js b/tests/unit/Wrapper/LambdaWrapper.test.js deleted file mode 100644 index 0a7f4102..00000000 --- a/tests/unit/Wrapper/LambdaWrapper.test.js +++ /dev/null @@ -1,256 +0,0 @@ -/* eslint-disable sonarjs/no-duplicate-string */ -import { DEFINITIONS } from '../../../src/Config/Dependencies'; -import DependencyInjection from '../../../src/DependencyInjection/DependencyInjection.class'; -import ResponseModel from '../../../src/Model/Response.model'; -import RequestService, { REQUEST_TYPES } from '../../../src/Service/Request.service'; -import LambdaTermination from '../../../src/Wrapper/LambdaTermination'; -import LambdaWrapper, { handleError } from '../../../src/Wrapper/LambdaWrapper'; -import { getMockedDi } from '../../lib/mocks'; -import getContext from '../../mocks/aws/context.json'; -import getEvent from '../../mocks/aws/event.json'; - -const handlers = { - SYNC_SUCCESS: () => ResponseModel.generate({ x: 'success' }, 200, 'ok'), - SYNC_THROWING: (di) => { - jest.spyOn(di.dependencies[DEFINITIONS.LOGGER], 'error'); - jest.spyOn(di.dependencies[DEFINITIONS.LOGGER], 'metric'); - - throw new LambdaTermination('SYNC_THROWING', 403, 'external'); - }, - ASYNC_SUCCESS: () => Promise.resolve(ResponseModel.generate({ x: 'success' }, 200, 'ok')), - ASYNC_THROWING: (di) => new Promise(() => { - jest.spyOn(di.dependencies[DEFINITIONS.LOGGER], 'error'); - jest.spyOn(di.dependencies[DEFINITIONS.LOGGER], 'metric'); - - throw new LambdaTermination('ASYNC_THROWING', 403, 'external'); - }), -}; - -describe('Wrapper/LambdaWrapper', () => { - let dependencyInjection = {}; - let requestService = {}; - - const configuration = { - DEFINITIONS: {}, - DEPENDENCIES: {}, - }; - - beforeEach(() => { - // Mute Winston - // eslint-disable-next-line no-underscore-dangle - jest.spyOn(console._stdout, 'write').mockImplementation(() => {}); - }); - - afterEach(() => jest.resetAllMocks()); - - describe('handleError', () => { - [ - [undefined, 400, 0], - [false, 400, 0], - [true, 400, 1], - [undefined, undefined, 1], - [undefined, false, 1], - [undefined, 500, 1], - [true, 500, 1], - ].forEach(([raiseOnEpsagon, code, expected]) => { - it(`error.raiseOnEpsagon = '${raiseOnEpsagon}', code = '${code}' logger.error called ${expected} times`, () => { - const di = getMockedDi(); - const logger = di.get(DEFINITIONS.LOGGER); - const error = { raiseOnEpsagon, code }; - - handleError(di, error); - - expect(logger.error).toHaveBeenCalledTimes(expected); - }); - - [undefined, { data: 1 }].forEach((body) => { - it('Generates a response object', () => { - const di = getMockedDi(); - const error = { raiseOnEpsagon, code, body }; - - const response = handleError(di, error); - - expect(response).toMatchSnapshot(); - }); - }); - }); - }); - - describe('LambdaWrapper', () => { - describe('executes the wrapped function', () => { - it('when it is sync', () => { - const lambda = LambdaWrapper(configuration, handlers.SYNC_SUCCESS); - expect(lambda(getEvent, getContext)).toMatchSnapshot(); - }); - - it('when it is async', async () => { - const lambda = LambdaWrapper(configuration, handlers.ASYNC_SUCCESS); - await expect(lambda(getEvent, getContext)).resolves.toMatchSnapshot(); - }); - }); - - describe('should inject dependency injection into the function', () => { - LambdaWrapper(configuration, (di, request) => { - dependencyInjection = di; - requestService = request; - })(getEvent, getContext); - - it('dependency injection variables should be an instance of the dependency injection class', () => { - expect(dependencyInjection instanceof DependencyInjection).toEqual(true); - }); - - it('dependency injection should output the event that was provided to it', () => { - expect(dependencyInjection.getEvent()).toEqual(getEvent); - }); - - it('dependency injection should output the event that was provided to it', () => { - expect(dependencyInjection.getContext()).toEqual(getContext); - }); - }); - - describe('should inject the request service into the function', () => { - LambdaWrapper(configuration, (di, request) => { - dependencyInjection = di; - requestService = request; - })(getEvent, getContext); - - it('request service variables should be an instance of the dependency injection class', () => { - expect(requestService instanceof RequestService).toEqual(true); - }); - - it('request service should contain variables that were sent to it via the event', () => { - expect(requestService.get('test', null, REQUEST_TYPES.GET)).toEqual(getEvent.queryStringParameters.test); - }); - }); - - describe('should catch exceptions and generate appropriate responses', () => { - it('Logs.error the error without error code', () => { - let infoStub; - let errorStub; - let metricStub; - - const lambda = LambdaWrapper(configuration, (di) => { - infoStub = jest.spyOn(di.dependencies[DEFINITIONS.LOGGER], 'info'); - errorStub = jest.spyOn(di.dependencies[DEFINITIONS.LOGGER], 'error'); - metricStub = jest.spyOn(di.dependencies[DEFINITIONS.LOGGER], 'metric'); - throw new Error('Undefined error'); - }); - - lambda(getEvent, getContext); - - expect(infoStub).not.toHaveBeenCalled(); - expect(errorStub).toHaveBeenCalled(); - expect(metricStub).nthCalledWith(1, 'lambda.statusCode', 500); - }); - - [400, 401, 403, 404, 409, 419, 421, 423, 499].forEach((errorCode) => { - it(`Logs.info the error with code ${errorCode}`, () => { - let infoStub; - let errorStub; - let metricStub; - - const lambda = LambdaWrapper(configuration, (di) => { - infoStub = jest.spyOn(di.dependencies[DEFINITIONS.LOGGER], 'info'); - errorStub = jest.spyOn(di.dependencies[DEFINITIONS.LOGGER], 'error'); - metricStub = jest.spyOn(di.dependencies[DEFINITIONS.LOGGER], 'metric'); - - const error = new Error('4xx error'); - error.code = errorCode; - throw error; - }); - - lambda(getEvent, getContext); - - expect(infoStub).toHaveBeenCalled(); - expect(errorStub).not.toHaveBeenCalled(); - expect(metricStub).nthCalledWith(1, 'lambda.statusCode', errorCode); - }); - }); - - [500, 501, 502, 503].forEach((errorCode) => { - it(`Logs.error the error with code ${errorCode}`, () => { - let infoStub; - let errorStub; - let metricStub; - - const lambda = LambdaWrapper(configuration, (di) => { - infoStub = jest.spyOn(di.dependencies[DEFINITIONS.LOGGER], 'info'); - errorStub = jest.spyOn(di.dependencies[DEFINITIONS.LOGGER], 'error'); - metricStub = jest.spyOn(di.dependencies[DEFINITIONS.LOGGER], 'metric'); - - const error = new Error('5xx error'); - error.code = errorCode; - throw error; - }); - - lambda(getEvent, getContext); - - expect(infoStub).not.toHaveBeenCalled(); - expect(errorStub).toHaveBeenCalled(); - expect(metricStub).nthCalledWith(1, 'lambda.statusCode', errorCode); - }); - }); - - it('Returns 500 exception with a common error', () => { - const lambda = LambdaWrapper(configuration, (di) => { - jest.spyOn(di.dependencies[DEFINITIONS.LOGGER], 'error'); - jest.spyOn(di.dependencies[DEFINITIONS.LOGGER], 'metric'); - throw new Error('Some error'); - }); - - const response = lambda(getEvent, getContext); - const body = JSON.parse(response.body); - - expect(response.statusCode).toEqual(500); - expect(body.message).toEqual('unknown error'); - }); - - it('Returns a response generated by LambdaTermination', () => { - const lambda = LambdaWrapper(configuration, (di) => { - jest.spyOn(di.dependencies[DEFINITIONS.LOGGER], 'error'); - jest.spyOn(di.dependencies[DEFINITIONS.LOGGER], 'metric'); - throw new LambdaTermination('internal', 403, 'external', 'some message'); - }); - - const response = lambda(getEvent, getContext); - const body = JSON.parse(response.body); - - expect(response.statusCode).toEqual(403); - expect(body.data).toEqual('external'); - expect(body.message).toEqual('some message'); - }); - - describe('catches sync errors', () => { - it('returns an error http response with throwError === false', () => { - const lambda = LambdaWrapper(configuration, handlers.SYNC_THROWING, false); - const outcome = lambda(getEvent, getContext); - expect(outcome).toMatchSnapshot(); - }); - - it('returns a raw error with throwError === true', () => { - const lambda = LambdaWrapper(configuration, handlers.SYNC_THROWING, true); - const outcome = lambda(getEvent, getContext); - expect(outcome).toMatchSnapshot(); - - // Be absolutely sure we got an Error object or the lambda will not count as failed - expect(outcome instanceof LambdaTermination).toEqual(true); - expect(outcome instanceof Error).toEqual(true); - }); - }); - - describe('catches async errors', () => { - it('resolves an error http response with throwError === false', async () => { - const lambda = LambdaWrapper(configuration, handlers.ASYNC_THROWING, false); - await expect(lambda(getEvent, getContext)).resolves.toMatchSnapshot(); - }); - - it('rejects the promise with throwError === true', async () => { - const lambda = LambdaWrapper(configuration, handlers.ASYNC_THROWING, true); - - // Be absolutely sure we got a rejection or the lambda will not count as failed - await expect(lambda(getEvent, getContext)).rejects.toThrowErrorMatchingSnapshot(); - }); - }); - }); - }); -}); diff --git a/tests/unit/Wrapper/__snapshots__/LambdaWrapper.test.js.snap b/tests/unit/Wrapper/__snapshots__/LambdaWrapper.test.js.snap deleted file mode 100644 index 7003622e..00000000 --- a/tests/unit/Wrapper/__snapshots__/LambdaWrapper.test.js.snap +++ /dev/null @@ -1,221 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Wrapper/LambdaWrapper LambdaWrapper executes the wrapped function when it is async 1`] = ` -Object { - "body": "{\\"data\\":{\\"x\\":\\"success\\"},\\"message\\":\\"ok\\"}", - "headers": Object { - "Access-Control-Allow-Credentials": true, - "Access-Control-Allow-Origin": "*", - "Content-Type": "application/json", - }, - "statusCode": 200, -} -`; - -exports[`Wrapper/LambdaWrapper LambdaWrapper executes the wrapped function when it is sync 1`] = ` -Object { - "body": "{\\"data\\":{\\"x\\":\\"success\\"},\\"message\\":\\"ok\\"}", - "headers": Object { - "Access-Control-Allow-Credentials": true, - "Access-Control-Allow-Origin": "*", - "Content-Type": "application/json", - }, - "statusCode": 200, -} -`; - -exports[`Wrapper/LambdaWrapper LambdaWrapper should catch exceptions and generate appropriate responses catches async errors rejects the promise with throwError === true 1`] = `"ASYNC_THROWING"`; - -exports[`Wrapper/LambdaWrapper LambdaWrapper should catch exceptions and generate appropriate responses catches async errors resolves an error http response with throwError === false 1`] = ` -Object { - "body": "{\\"data\\":\\"external\\",\\"message\\":\\"unknown error\\"}", - "headers": Object { - "Access-Control-Allow-Credentials": true, - "Access-Control-Allow-Origin": "*", - "Content-Type": "application/json", - }, - "statusCode": 403, -} -`; - -exports[`Wrapper/LambdaWrapper LambdaWrapper should catch exceptions and generate appropriate responses catches sync errors returns a raw error with throwError === true 1`] = `[Error: SYNC_THROWING]`; - -exports[`Wrapper/LambdaWrapper LambdaWrapper should catch exceptions and generate appropriate responses catches sync errors returns an error http response with throwError === false 1`] = ` -Object { - "body": "{\\"data\\":\\"external\\",\\"message\\":\\"unknown error\\"}", - "headers": Object { - "Access-Control-Allow-Credentials": true, - "Access-Control-Allow-Origin": "*", - "Content-Type": "application/json", - }, - "statusCode": 403, -} -`; - -exports[`Wrapper/LambdaWrapper handleError Generates a response object 1`] = ` -Object { - "body": "{\\"data\\":{},\\"message\\":\\"unknown error\\"}", - "headers": Object { - "Access-Control-Allow-Credentials": true, - "Access-Control-Allow-Origin": "*", - "Content-Type": "application/json", - }, - "statusCode": 400, -} -`; - -exports[`Wrapper/LambdaWrapper handleError Generates a response object 2`] = ` -Object { - "body": "{\\"data\\":{\\"data\\":1},\\"message\\":\\"unknown error\\"}", - "headers": Object { - "Access-Control-Allow-Credentials": true, - "Access-Control-Allow-Origin": "*", - "Content-Type": "application/json", - }, - "statusCode": 400, -} -`; - -exports[`Wrapper/LambdaWrapper handleError Generates a response object 3`] = ` -Object { - "body": "{\\"data\\":{},\\"message\\":\\"unknown error\\"}", - "headers": Object { - "Access-Control-Allow-Credentials": true, - "Access-Control-Allow-Origin": "*", - "Content-Type": "application/json", - }, - "statusCode": 400, -} -`; - -exports[`Wrapper/LambdaWrapper handleError Generates a response object 4`] = ` -Object { - "body": "{\\"data\\":{\\"data\\":1},\\"message\\":\\"unknown error\\"}", - "headers": Object { - "Access-Control-Allow-Credentials": true, - "Access-Control-Allow-Origin": "*", - "Content-Type": "application/json", - }, - "statusCode": 400, -} -`; - -exports[`Wrapper/LambdaWrapper handleError Generates a response object 5`] = ` -Object { - "body": "{\\"data\\":{},\\"message\\":\\"unknown error\\"}", - "headers": Object { - "Access-Control-Allow-Credentials": true, - "Access-Control-Allow-Origin": "*", - "Content-Type": "application/json", - }, - "statusCode": 400, -} -`; - -exports[`Wrapper/LambdaWrapper handleError Generates a response object 6`] = ` -Object { - "body": "{\\"data\\":{\\"data\\":1},\\"message\\":\\"unknown error\\"}", - "headers": Object { - "Access-Control-Allow-Credentials": true, - "Access-Control-Allow-Origin": "*", - "Content-Type": "application/json", - }, - "statusCode": 400, -} -`; - -exports[`Wrapper/LambdaWrapper handleError Generates a response object 7`] = ` -Object { - "body": "{\\"data\\":{},\\"message\\":\\"unknown error\\"}", - "headers": Object { - "Access-Control-Allow-Credentials": true, - "Access-Control-Allow-Origin": "*", - "Content-Type": "application/json", - }, - "statusCode": 500, -} -`; - -exports[`Wrapper/LambdaWrapper handleError Generates a response object 8`] = ` -Object { - "body": "{\\"data\\":{\\"data\\":1},\\"message\\":\\"unknown error\\"}", - "headers": Object { - "Access-Control-Allow-Credentials": true, - "Access-Control-Allow-Origin": "*", - "Content-Type": "application/json", - }, - "statusCode": 500, -} -`; - -exports[`Wrapper/LambdaWrapper handleError Generates a response object 9`] = ` -Object { - "body": "{\\"data\\":{},\\"message\\":\\"unknown error\\"}", - "headers": Object { - "Access-Control-Allow-Credentials": true, - "Access-Control-Allow-Origin": "*", - "Content-Type": "application/json", - }, - "statusCode": 500, -} -`; - -exports[`Wrapper/LambdaWrapper handleError Generates a response object 10`] = ` -Object { - "body": "{\\"data\\":{\\"data\\":1},\\"message\\":\\"unknown error\\"}", - "headers": Object { - "Access-Control-Allow-Credentials": true, - "Access-Control-Allow-Origin": "*", - "Content-Type": "application/json", - }, - "statusCode": 500, -} -`; - -exports[`Wrapper/LambdaWrapper handleError Generates a response object 11`] = ` -Object { - "body": "{\\"data\\":{},\\"message\\":\\"unknown error\\"}", - "headers": Object { - "Access-Control-Allow-Credentials": true, - "Access-Control-Allow-Origin": "*", - "Content-Type": "application/json", - }, - "statusCode": 500, -} -`; - -exports[`Wrapper/LambdaWrapper handleError Generates a response object 12`] = ` -Object { - "body": "{\\"data\\":{\\"data\\":1},\\"message\\":\\"unknown error\\"}", - "headers": Object { - "Access-Control-Allow-Credentials": true, - "Access-Control-Allow-Origin": "*", - "Content-Type": "application/json", - }, - "statusCode": 500, -} -`; - -exports[`Wrapper/LambdaWrapper handleError Generates a response object 13`] = ` -Object { - "body": "{\\"data\\":{},\\"message\\":\\"unknown error\\"}", - "headers": Object { - "Access-Control-Allow-Credentials": true, - "Access-Control-Allow-Origin": "*", - "Content-Type": "application/json", - }, - "statusCode": 500, -} -`; - -exports[`Wrapper/LambdaWrapper handleError Generates a response object 14`] = ` -Object { - "body": "{\\"data\\":{\\"data\\":1},\\"message\\":\\"unknown error\\"}", - "headers": Object { - "Access-Control-Allow-Credentials": true, - "Access-Control-Allow-Origin": "*", - "Content-Type": "application/json", - }, - "statusCode": 500, -} -`; diff --git a/tests/unit/core/DependencyAwareClass.spec.ts b/tests/unit/core/DependencyAwareClass.spec.ts new file mode 100644 index 00000000..34fba6df --- /dev/null +++ b/tests/unit/core/DependencyAwareClass.spec.ts @@ -0,0 +1,19 @@ +import { Context, DependencyAwareClass, DependencyInjection } from '@/src'; + +describe('unit.core.DependencyAwareClass', () => { + describe('getContainer', () => { + it('should return the DependencyInjection instance', () => { + const di = new DependencyInjection({ dependencies: {} }, {}, {} as Context); + const dep = new DependencyAwareClass(di); + expect(dep.getContainer()).toBe(di); + }); + }); + + describe('di', () => { + it('should expose the DependencyInjection instance', () => { + const di = new DependencyInjection({ dependencies: {} }, {}, {} as Context); + const dep = new DependencyAwareClass(di); + expect(dep.di).toBe(di); + }); + }); +}); diff --git a/tests/unit/core/DependencyInjection.spec.ts b/tests/unit/core/DependencyInjection.spec.ts new file mode 100644 index 00000000..efdc8fcd --- /dev/null +++ b/tests/unit/core/DependencyInjection.spec.ts @@ -0,0 +1,69 @@ +import { DependencyAwareClass, DependencyInjection } from '@/src'; +import { mockContext, mockEvent } from '@/tests/mocks/aws'; + +class A extends DependencyAwareClass {} + +class B extends DependencyAwareClass {} + +class C extends DependencyAwareClass {} + +describe('unit.core.DependencyInjection', () => { + const mockConfig = { + dependencies: { + A, + B, + }, + }; + + const di = new DependencyInjection(mockConfig, mockEvent, mockContext); + + describe('event', () => { + it('should expose the event', () => { + expect(di.event).toBe(mockEvent); + }); + }); + + describe('context', () => { + it('should expose the Lambda context', () => { + expect(di.context).toBe(mockContext); + }); + }); + + describe('config', () => { + it('should expose the config object', () => { + expect(di.config).toBe(mockConfig); + }); + }); + + describe('get', () => { + it('should return an instance of A, given A', () => { + expect(di.get(A)).toBeInstanceOf(A); + }); + + it('should return an instance of B, given B', () => { + expect(di.get(B)).toBeInstanceOf(B); + }); + + it('should throw, given an unknown dependency', () => { + expect(() => di.get(C)).toThrow('C does not exist in dependency container'); + }); + }); + + describe('getEvent', () => { + it('should return the event', () => { + expect(di.getEvent()).toBe(mockEvent); + }); + }); + + describe('getContext', () => { + it('should return the Lambda context', () => { + expect(di.getContext()).toBe(mockContext); + }); + }); + + describe('getConfiguration', () => { + it('should return the config object', () => { + expect(di.getConfiguration()).toBe(mockConfig); + }); + }); +}); diff --git a/tests/unit/core/LambdaWrapper.spec.ts b/tests/unit/core/LambdaWrapper.spec.ts new file mode 100644 index 00000000..fe4542ea --- /dev/null +++ b/tests/unit/core/LambdaWrapper.spec.ts @@ -0,0 +1,306 @@ +import { RESPONSE_HEADERS } from '@/src/models/ResponseModel'; + +import { + DependencyInjection, + LambdaTermination, + LambdaWrapper, + LambdaWrapperConfig, + LoggerService, + RequestService, +} from '@/src'; +import { mockContext, mockEvent } from '@/tests/mocks/aws'; + +type ErrorWithCode = Error & { code?: number }; + +const config: LambdaWrapperConfig = { + dependencies: { + LoggerService, + RequestService, + }, +}; + +const getDi = () => new DependencyInjection(config, mockEvent, mockContext); + +describe('unit.core.LambdaWrapper', () => { + beforeAll(() => { + // mute log ouptut + const noop = () => { /* do nothing */ }; + jest.spyOn(LoggerService.prototype, 'info').mockImplementation(noop); + jest.spyOn(LoggerService.prototype, 'error').mockImplementation(noop); + jest.spyOn(LoggerService.prototype, 'metric').mockImplementation(noop); + jest.spyOn(LoggerService.prototype, 'label').mockImplementation(noop); + }); + + afterEach(() => jest.resetAllMocks()); + + describe('config', () => { + it('should expose the config object', () => { + const lw = new LambdaWrapper(config); + expect(lw.config).toBe(config); + }); + }); + + describe('configure', () => { + // see tests/unit/core/config.spec.ts for config merge tests + + const base = new LambdaWrapper({ + dependencies: {}, + }); + + const configured = base.configure({ + dependencies: { + LoggerService, + }, + }); + + it('should return a LambdaWrapper with the given config', () => { + expect(configured).toBeInstanceOf(LambdaWrapper); + expect(configured.config).toEqual({ + dependencies: { + LoggerService, + }, + }); + }); + + it('should not modify the original wrapper config', () => { + expect(base.config).toEqual({ + dependencies: {}, + }); + }); + }); + + describe('wrap', () => { + const lambdaWrapper = new LambdaWrapper(config); + + it('should return a wrapped handler function', async () => { + const wrapped = lambdaWrapper.wrap(jest.fn()); + + expect(typeof wrapped).toEqual('function'); + }); + + describe('the wrapped handler', () => { + it('should call the handler', async () => { + const fn = jest.fn(); + const wrapped = lambdaWrapper.wrap(fn); + + await wrapped(mockEvent, mockContext); + + expect(fn).toHaveBeenCalled(); + }); + + it('should forward the return value', async () => { + const result = Math.random(); + const fn = jest.fn().mockResolvedValue(result); + const wrapped = lambdaWrapper.wrap(fn); + + expect(await wrapped(mockEvent, mockContext)).toEqual(result); + }); + + it('should pass dependency injection to the handler', async () => { + const fn = jest.fn(); + const wrapped = lambdaWrapper.wrap(fn); + + await wrapped(mockEvent, mockContext); + + const callArgs: any[] = fn.mock.calls[0]; + expect(callArgs).toHaveLength(1); + expect(callArgs[0]).toBeInstanceOf(DependencyInjection); + }); + + it('should provide the Lambda event via di', async () => { + const fn = jest.fn(); + const wrapped = lambdaWrapper.wrap(fn); + + await wrapped(mockEvent, mockContext); + + const [di]: [DependencyInjection] = fn.mock.calls[0]; + expect(di.event).toEqual(mockEvent); + }); + + it('should provide the Lambda context via di', async () => { + const fn = jest.fn(); + const wrapped = lambdaWrapper.wrap(fn); + + await wrapped(mockEvent, mockContext); + + const [di]: [DependencyInjection] = fn.mock.calls[0]; + expect(di.context).toEqual(mockContext); + }); + }); + + describe('handleUncaughtErrors = true (default)', () => { + describe('when error has no code property', () => { + it('should pass it to logger.error', async () => { + let logger: LoggerService; + + const lambda = lambdaWrapper.wrap((di) => { + logger = di.get(LoggerService); + throw new Error('Undefined error'); + }); + + await lambda(mockEvent, mockContext); + + expect(logger!.info).not.toHaveBeenCalled(); + expect(logger!.error).toHaveBeenCalled(); + expect(logger!.metric).lastCalledWith('lambda.statusCode', 500); + }); + }); + + describe('when error has code 4xx', () => { + [400, 401, 403, 404, 409, 419, 421, 423, 499].forEach((errorCode) => { + it(`should call logger.info with code ${errorCode}`, async () => { + let logger: LoggerService; + + const lambda = lambdaWrapper.wrap((di) => { + logger = di.get(LoggerService); + + const error: ErrorWithCode = new Error('4xx error'); + error.code = errorCode; + throw error; + }); + + await lambda(mockEvent, mockContext); + + expect(logger!.info).toHaveBeenCalled(); + expect(logger!.error).not.toHaveBeenCalled(); + expect(logger!.metric).lastCalledWith('lambda.statusCode', errorCode); + }); + }); + }); + + describe('when error has code 5xx', () => { + [500, 501, 502, 503].forEach((errorCode) => { + it(`should call logger.error with code ${errorCode}`, async () => { + let logger: LoggerService; + + const lambda = lambdaWrapper.wrap((di) => { + logger = di.get(LoggerService); + + const error: ErrorWithCode = new Error('5xx error'); + error.code = errorCode; + throw error; + }); + + await lambda(mockEvent, mockContext); + + expect(logger!.info).not.toHaveBeenCalled(); + expect(logger!.error).toHaveBeenCalled(); + expect(logger!.metric).lastCalledWith('lambda.statusCode', errorCode); + }); + }); + }); + + describe('handler return value', () => { + describe('when a standard Error is thrown', () => { + it('should return status code 500 and message "unknown error"', async () => { + const lambda = lambdaWrapper.wrap(() => { + throw new Error('Some error'); + }); + + const response = await lambda(mockEvent, mockContext); + + expect(response.statusCode).toEqual(500); + expect(JSON.parse(response.body)).toHaveProperty('message', 'unknown error'); + }); + }); + + describe('when a LambdaTermination is thrown', () => { + it('should return status code 403', async () => { + const lambda = lambdaWrapper.wrap(() => { + throw new LambdaTermination('internal', 403, 'external', 'some message'); + }); + + const response = await lambda(mockEvent, mockContext); + + expect(response.statusCode).toEqual(403); + const body = JSON.parse(response.body); + expect(body).toHaveProperty('data', 'external'); + expect(body).toHaveProperty('message', 'some message'); + }); + }); + }); + }); + + describe('handleUncaughtErrors = false', () => { + describe('synchronous error', () => { + it('should return a promise that eventually rejects', async () => { + // note: handler function IS NOT async + const lambda = lambdaWrapper.wrap(() => { + throw new LambdaTermination('sync error message', 403, 'external'); + }, { + handleUncaughtErrors: false, + }); + + const promise = lambda(mockEvent, mockContext); + + // be absolutely sure we got a rejection or the lambda will not count as failed + await expect(promise).rejects.toThrowError(LambdaTermination); + }); + }); + + describe('asynchronous error', () => { + it('should return a promise that eventually rejects', async () => { + // note: handler function IS async + const lambda = lambdaWrapper.wrap(async () => { + throw new LambdaTermination('async error message', 403, 'external'); + }, { + handleUncaughtErrors: false, + }); + + const promise = lambda(mockEvent, mockContext); + + // be absolutely sure we got a rejection or the lambda will not count as failed + await expect(promise).rejects.toThrowError(LambdaTermination); + }); + }); + }); + }); + + describe('handleError', () => { + ([ + [undefined, 400, 0], + [false, 400, 0], + [true, 400, 1], + [undefined, undefined, 1], + [undefined, false, 1], + [undefined, 500, 1], + [true, 500, 1], + ] as const).forEach(([raiseOnEpsagon, code, expected]) => { + it(`should ${expected === 0 ? 'not ' : ''}call logger.error given { raiseOnEpsagon: ${raiseOnEpsagon}, code: ${code} }`, () => { + const di = getDi(); + const logger = di.get(LoggerService); + jest.spyOn(logger, 'error'); + + const error = { + name: 'Error', + message: 'error!', + raiseOnEpsagon, + code, + }; + + LambdaWrapper.handleError(di, error); + + expect(logger.error).toHaveBeenCalledTimes(expected); + }); + + it('should return an HTTP response object', () => { + const di = getDi(); + const error = { + name: 'Error', + message: 'error!', + raiseOnEpsagon, + code, + body: { key: 'value' }, + }; + + const response = LambdaWrapper.handleError(di, error); + + expect(response).toEqual({ + statusCode: code || 500, + body: JSON.stringify({ data: error.body, message: 'unknown error' }), + headers: RESPONSE_HEADERS, + }); + }); + }); + }); +}); diff --git a/tests/unit/core/config.spec.ts b/tests/unit/core/config.spec.ts new file mode 100644 index 00000000..4b302668 --- /dev/null +++ b/tests/unit/core/config.spec.ts @@ -0,0 +1,52 @@ +import DependencyAwareClass from '@/src/core/DependencyAwareClass'; +import { LambdaWrapperConfig, mergeConfig } from '@/src/core/config'; + +class A extends DependencyAwareClass {} + +class B extends DependencyAwareClass {} + +describe('unit.core.config', () => { + describe('mergeConfig', () => { + it('should return the config if no new config is given', () => { + const a: LambdaWrapperConfig = { + dependencies: { A, B }, + }; + const b = {}; + + expect(mergeConfig(a, b)).toEqual(a); + }); + + it('should combine dependencies', () => { + const a: LambdaWrapperConfig = { + dependencies: { A }, + }; + const b: LambdaWrapperConfig = { + dependencies: { B }, + }; + + expect(mergeConfig(a, b)).toEqual({ + dependencies: { A, B }, + }); + }); + + it('should override other keys', () => { + type WithOtherKeys = { test: string; another: string; }; + + const a: LambdaWrapperConfig & WithOtherKeys = { + dependencies: {}, + test: 'initial', + another: 'values', + }; + const b: Partial & WithOtherKeys = { + test: 'overridden', + another: 'here', + }; + + expect(mergeConfig(a, b)).toEqual({ + dependencies: {}, + test: 'overridden', + another: 'here', + }); + }); + }); +}); diff --git a/tests/unit/index.spec.ts b/tests/unit/index.spec.ts new file mode 100644 index 00000000..b2cef2a5 --- /dev/null +++ b/tests/unit/index.spec.ts @@ -0,0 +1,97 @@ +import DependencyAwareClass from '@/src/core/DependencyAwareClass'; +import DependencyInjection from '@/src/core/DependencyInjection'; +import LambdaWrapper from '@/src/core/LambdaWrapper'; +import ResponseModel from '@/src/models/ResponseModel'; +import SQSMessageModel from '@/src/models/SQSMessageModel'; +import StatusModel from '@/src/models/StatusModel'; +import BaseConfigService from '@/src/services/BaseConfigService'; +import HTTPService, { COMICRELIEF_TEST_METADATA_HEADER } from '@/src/services/HTTPService'; +import LoggerService from '@/src/services/LoggerService'; +import RequestService, { REQUEST_TYPES } from '@/src/services/RequestService'; +import SQSService, { SQS_OFFLINE_MODES, SQS_PUBLISH_FAILURE_MODES } from '@/src/services/SQSService'; +import TimerService from '@/src/services/TimerService'; +import LambdaTermination from '@/src/utils/LambdaTermination'; +import PromisifiedDelay from '@/src/utils/PromisifiedDelay'; + +import lambdaWrapper, * as lib from '@/src'; + +describe('unit.index', () => { + describe('default export', () => { + it('should be a LambdaWrapper instance', () => { + expect(lambdaWrapper).toBeInstanceOf(LambdaWrapper); + }); + + it('should be configured with SQSService', () => { + const deps = Object.values(lambdaWrapper.config.dependencies); + expect(deps).toContain(SQSService); + }); + }); + + // these tests prevent accidental removal of exports + + it('should export DependencyAwareClass', () => { + expect(lib.DependencyAwareClass).toBe(DependencyAwareClass); + }); + + it('should export DependencyInjection', () => { + expect(lib.DependencyInjection).toBe(DependencyInjection); + }); + + it('should export LambdaWrapper', () => { + expect(lib.LambdaWrapper).toBe(LambdaWrapper); + }); + + // models + + it('should export ResponseModel', () => { + expect(lib.ResponseModel).toBe(ResponseModel); + }); + + it('should export SQSMessageModel', () => { + expect(lib.SQSMessageModel).toBe(SQSMessageModel); + }); + + it('should export StatusModel', () => { + expect(lib.StatusModel).toBe(StatusModel); + }); + + // services + + it('should export BaseConfigService', () => { + expect(lib.BaseConfigService).toBe(BaseConfigService); + }); + + it('should export HTTPService', () => { + expect(lib.HTTPService).toBe(HTTPService); + expect(lib.COMICRELIEF_TEST_METADATA_HEADER).toBe(COMICRELIEF_TEST_METADATA_HEADER); + }); + + it('should export LoggerService', () => { + expect(lib.LoggerService).toBe(LoggerService); + }); + + it('should export RequestService', () => { + expect(lib.RequestService).toBe(RequestService); + expect(lib.REQUEST_TYPES).toBe(REQUEST_TYPES); + }); + + it('should export SQSService', () => { + expect(lib.SQSService).toBe(SQSService); + expect(lib.SQS_OFFLINE_MODES).toBe(SQS_OFFLINE_MODES); + expect(lib.SQS_PUBLISH_FAILURE_MODES).toBe(SQS_PUBLISH_FAILURE_MODES); + }); + + it('should export TimerService', () => { + expect(lib.TimerService).toBe(TimerService); + }); + + // utils + + it('should export LambdaTermination', () => { + expect(lib.LambdaTermination).toBe(LambdaTermination); + }); + + it('should export PromisifiedDelay', () => { + expect(lib.PromisifiedDelay).toBe(PromisifiedDelay); + }); +}); diff --git a/tests/unit/Model/Response.model.test.js b/tests/unit/models/ResponseModel.spec.ts similarity index 54% rename from tests/unit/Model/Response.model.test.js rename to tests/unit/models/ResponseModel.spec.ts index 2625e013..17442c52 100644 --- a/tests/unit/Model/Response.model.test.js +++ b/tests/unit/models/ResponseModel.spec.ts @@ -1,46 +1,50 @@ -import ResponseModel, { DEFAULT_MESSAGE, RESPONSE_HEADERS } from '../../../src/Model/Response.model'; +import { DEFAULT_MESSAGE, RESPONSE_HEADERS } from '@/src/models/ResponseModel'; -describe('Model/ResponseModel', () => { - it('should return the expected headers', () => { - const response = new ResponseModel({}, 500); - expect(response.generate().headers).toEqual(RESPONSE_HEADERS); +import { ResponseModel } from '@/src'; + +describe('unit.models.ResponseModel', () => { + describe('headers', () => { + it('should include the default response headers', () => { + const response = new ResponseModel({}, 500); + expect(response.generate().headers).toEqual(RESPONSE_HEADERS); + }); }); - describe('ensure body set correctly', () => { + describe('body', () => { it('should set the body data from the constructor', () => { const response = new ResponseModel({ test: 123 }, 500); const responseBody = response.generate(); expect(typeof responseBody.body).toEqual('string'); - expect(responseBody.body.indexOf('123')).not.toEqual(-1); - expect(JSON.parse(responseBody.body).data.test).toEqual(123); + expect(responseBody.body).toContain('123'); + expect(JSON.parse(responseBody.body)).toHaveProperty('data.test', 123); }); - it('should be able to modify the body data', () => { + it('should modify the body data using setData', () => { const response = new ResponseModel({ test: 123 }, 500); response.setData({ test: 234 }); const responseBody = response.generate(); expect(typeof responseBody.body).toEqual('string'); - expect(responseBody.body.indexOf('234')).not.toEqual(-1); - expect(JSON.parse(responseBody.body).data.test).toEqual(234); + expect(responseBody.body).toContain('234'); + expect(JSON.parse(responseBody.body)).toHaveProperty('data.test', 234); }); }); - describe('ensure status codes are set correctly', () => { - it('should return the 200 status code that is supplied to it', () => { + describe('status code', () => { + it('should return a 200 status code that is supplied to it', () => { const response = new ResponseModel({}, 200); expect(response.generate().statusCode).toEqual(200); }); - it('should return the 500 status code that is supplied to it', () => { + it('should return a 500 status code that is supplied to it', () => { const response = new ResponseModel({}, 500); expect(response.generate().statusCode).toEqual(500); }); - it('should allow the status code to be modified once set via the constructor', () => { + it('should modify the status code using setCode', () => { const response = new ResponseModel({}, 200); response.setCode(300); @@ -48,25 +52,25 @@ describe('Model/ResponseModel', () => { }); }); - describe('ensure messages are set correctly', () => { - it('should return a message field when a message is supplied to it', () => { + describe('message', () => { + it('should put a message field in the body', () => { const message = 'test 123'; const response = new ResponseModel({}, 500, message); - expect(JSON.parse(response.generate().body).message).toEqual(message); + expect(JSON.parse(response.generate().body)).toHaveProperty('message', message); }); - it('should be able to get the message using the message getter', () => { + it('should be able to get the message using getMessage', () => { const message = 'test 123'; const response = new ResponseModel({}, 500, message); expect(response.getMessage()).toEqual(message); }); - it('should return success message field when a message is not supplied to it', () => { + it('should return success message if no message is given', () => { const response = new ResponseModel({}, 500); expect(JSON.parse(response.generate().body).message).toEqual(DEFAULT_MESSAGE); }); - it('should allow the message supplied via the constructor to be overridden', () => { + it('should modify the message using setMessage', () => { const response = new ResponseModel({}, 200, 'replace-me'); response.setMessage('test'); @@ -75,7 +79,7 @@ describe('Model/ResponseModel', () => { }); describe('generate', () => { - it('static and instance method produce the same output', () => { + it('should output the same from the static and instance method', () => { const data = { a: 1, b: { c: 2 } }; const code = 201; const message = 'Some message'; diff --git a/tests/unit/models/SQSMessageModel.spec.ts b/tests/unit/models/SQSMessageModel.spec.ts new file mode 100644 index 00000000..3bcd60c0 --- /dev/null +++ b/tests/unit/models/SQSMessageModel.spec.ts @@ -0,0 +1,59 @@ +import { SQSMessageModel as Message } from '@/src'; + +describe('unit.models.SQSMessageModel', () => { + const messageData = { + test: 123, + }; + + const mockedMessage = { + MessageId: 'message-id-123', + ReceiptHandle: 'receipt-handle-123', + Body: JSON.stringify(messageData), + }; + + const messageModel = new Message(mockedMessage); + + describe('getMessageId', () => { + it('should return the message ID', () => { + expect(messageModel.getMessageId()).toEqual(mockedMessage.MessageId); + }); + }); + + describe('getReceiptHandle', () => { + it('should return the receipt handle', () => { + expect(messageModel.getReceiptHandle()).toEqual(mockedMessage.ReceiptHandle); + }); + }); + + describe('getBody', () => { + it('should parse and return the message body', () => { + expect(messageModel.getBody()).toEqual(messageData); + }); + }); + + describe('isForDeletion', () => { + it('should be false initially', () => { + expect(messageModel.isForDeletion()).toBe(false); + }); + }); + + describe('setForDeletion', () => { + it('should set the is-for-deletion status', () => { + messageModel.setForDeletion(true); + expect(messageModel.isForDeletion()).toBe(true); + }); + }); + + describe('getMetaData', () => { + it('should return an empty object initially', () => { + expect(messageModel.getMetaData()).toEqual({}); + }); + }); + + describe('setMetaData', () => { + it('should add a key to metadata', () => { + messageModel.setMetaData('test', 123); + expect(messageModel.getMetaData()).toEqual({ test: 123 }); + }); + }); +}); diff --git a/tests/unit/models/StatusModel.spec.ts b/tests/unit/models/StatusModel.spec.ts new file mode 100644 index 00000000..ad141b56 --- /dev/null +++ b/tests/unit/models/StatusModel.spec.ts @@ -0,0 +1,39 @@ +import StatusModel, { STATUS_TYPES } from '@/src/models/StatusModel'; + +describe('unit.models.StatusModel', () => { + describe('getService', () => { + it('should return the service name', () => { + const statusModel = new StatusModel('test', STATUS_TYPES.OK); + expect(statusModel.getService()).toEqual('test'); + }); + }); + + describe('setService', () => { + it('should set the service name', () => { + const statusModel = new StatusModel('test', STATUS_TYPES.OK); + statusModel.setService('other'); + expect(statusModel.getService()).toEqual('other'); + }); + }); + + describe('getStatus', () => { + it('should return the status', () => { + const statusModel = new StatusModel('test', STATUS_TYPES.OK); + expect(statusModel.getStatus()).toEqual(STATUS_TYPES.OK); + }); + }); + + describe('setStatus', () => { + it('should set the status', () => { + const statusModel = new StatusModel('test', STATUS_TYPES.OK); + statusModel.setStatus(STATUS_TYPES.ACCEPTABLE_FAILURE); + expect(statusModel.getStatus()).toEqual(STATUS_TYPES.ACCEPTABLE_FAILURE); + }); + + it('should throw an error when trying to set an invalid status', () => { + const statusModel = new StatusModel('test', STATUS_TYPES.OK); + expect(() => statusModel.setStatus('invalid')) + .toThrow('StatusModel - invalid is not a valid status type'); + }); + }); +}); diff --git a/tests/unit/Service/BaseConfig.service.test.js b/tests/unit/services/BaseConfigService.spec.ts similarity index 66% rename from tests/unit/Service/BaseConfig.service.test.js rename to tests/unit/services/BaseConfigService.spec.ts index d8b5496c..7a6166a8 100644 --- a/tests/unit/Service/BaseConfig.service.test.js +++ b/tests/unit/services/BaseConfigService.spec.ts @@ -1,9 +1,20 @@ import { S3 } from 'aws-sdk'; -import DependencyInjection from '../../../src/DependencyInjection/DependencyInjection.class'; -import BaseConfigService, { S3_NO_SUCH_KEY_ERROR_CODE, ServiceStates, ServiceStatesHttpCodes } from '../../../src/Service/BaseConfig.service'; +import { + S3_NO_SUCH_KEY_ERROR_CODE, + ServiceStates, + ServiceStatesHttpCodes, +} from '@/src/services/BaseConfigService'; -const createAsyncMock = (returnValue) => { +import { + BaseConfigService, + Context, + DependencyInjection, +} from '@/src'; + +type ErrorWithCode = Error & { code?: any }; + +const createAsyncMock = (returnValue: any) => { const mockedValue = returnValue instanceof Error ? Promise.reject(returnValue) : Promise.resolve(returnValue); @@ -12,40 +23,49 @@ const createAsyncMock = (returnValue) => { }; /** - * Generates a BaseConfigService - * - * @param {*} param0 - * @returns {BaseConfigService} + * Generate a BaseConfigService with mock S3 client. */ -const getService = ({ getObject = null, putObject = null, deleteObject = null } = {}) => { - const di = new DependencyInjection({}, {}, {}); - const service = new BaseConfigService(di); +const getService = ( + { + getObject = null, + putObject = null, + deleteObject = null, + }: any = {}, +): BaseConfigService & { constructor: typeof BaseConfigService; } => { + const di = new DependencyInjection({ + dependencies: { + BaseConfigService, + }, + }, {}, {} as Context); + + const service = di.get(BaseConfigService); + const client = { getObject: createAsyncMock(getObject), putObject: createAsyncMock(putObject), deleteObject: createAsyncMock(deleteObject), - }; + } as unknown as S3; jest.spyOn(service, 'client', 'get').mockReturnValue(client); - return service; + return service as any; }; -const BaseConfigUnitTests = (serviceGenerator: (...args) => BaseConfigService) => { +describe('unit.services.BaseConfigService', () => { afterEach(() => { jest.resetAllMocks(); }); describe('defaultConfig', () => { it('is a valid object', () => { - const service = serviceGenerator(); + const service = getService(); const isValidObject = typeof service.constructor.defaultConfig === 'object' && service.constructor.defaultConfig !== null; expect(isValidObject).toEqual(true); }); it('has state defined', () => { - const service = serviceGenerator(); + const service = getService(); const defaultConfig = service.constructor.defaultConfig; expect('state' in defaultConfig).toEqual(true); @@ -54,14 +74,14 @@ const BaseConfigUnitTests = (serviceGenerator: (...args) => BaseConfigService) = describe('s3config', () => { it('is a valid object', () => { - const service = serviceGenerator(); + const service = getService(); const isValidObject = typeof service.constructor.s3config === 'object' && service.constructor.s3config !== null; expect(isValidObject).toEqual(true); }); it('has Bucket and Key defined', () => { - const service = serviceGenerator(); + const service = getService(); const s3config = service.constructor.s3config; expect('Bucket' in s3config).toEqual(true); @@ -71,7 +91,7 @@ const BaseConfigUnitTests = (serviceGenerator: (...args) => BaseConfigService) = describe('delete', () => { it('calls client.deleteObject', async () => { - const service = serviceGenerator(); + const service = getService(); await service.delete(); expect(service.client.deleteObject).toHaveBeenCalledTimes(1); @@ -81,7 +101,7 @@ const BaseConfigUnitTests = (serviceGenerator: (...args) => BaseConfigService) = describe('put', () => { it('calls client.putObject', async () => { const expected = Symbol('put'); - const service = serviceGenerator(); + const service = getService(); await service.put(expected); expect(service.client.putObject).toHaveBeenCalledTimes(1); @@ -89,7 +109,7 @@ const BaseConfigUnitTests = (serviceGenerator: (...args) => BaseConfigService) = it('returns the provided config unchanged', async () => { const expected = Symbol('put'); - const service = serviceGenerator(); + const service = getService(); const config = await service.put(expected); expect(config).toEqual(expected); @@ -99,29 +119,29 @@ const BaseConfigUnitTests = (serviceGenerator: (...args) => BaseConfigService) = describe('get', () => { it('gets an existing config', async () => { const expected = { a: 1 }; - const service = serviceGenerator({ getObject: { Body: JSON.stringify(expected) } }); + const service = getService({ getObject: { Body: JSON.stringify(expected) } }); const config = await service.get(); expect(config).toEqual(expected); }); it('refuses empty configurations', async () => { - const service = serviceGenerator({ getObject: { Body: '' } }); + const service = getService({ getObject: { Body: '' } }); await expect(service.get()).rejects.toThrowErrorMatchingSnapshot(); }); it('refuses invalid configurations', async () => { - const service = serviceGenerator({ getObject: { Body: '{ "a": 1' } }); + const service = getService({ getObject: { Body: '{ "a": 1' } }); await expect(service.get()).rejects.toThrowErrorMatchingSnapshot(); }); it('propagates the 404', async () => { - const error = new Error('404'); + const error: ErrorWithCode = new Error('404'); error.code = S3_NO_SUCH_KEY_ERROR_CODE; - const service = serviceGenerator({ getObject: error }); + const service = getService({ getObject: error }); await expect(service.get()).rejects.toThrowErrorMatchingSnapshot(); }); @@ -129,20 +149,20 @@ const BaseConfigUnitTests = (serviceGenerator: (...args) => BaseConfigService) = describe('getOrCreate', () => { it('uploads the defaultConfig with a 404 error', async () => { - const error = new Error('404'); + const error: ErrorWithCode = new Error('404'); error.code = S3_NO_SUCH_KEY_ERROR_CODE; - const service = serviceGenerator({ getObject: error }); + const service = getService({ getObject: error }); const config = await service.getOrCreate(); expect(config).toEqual(service.constructor.defaultConfig); }); it('throws any non-404 error', async () => { - const error = new Error('Bad error'); + const error: ErrorWithCode = new Error('Bad error'); error.code = 'another'; - const service = serviceGenerator({ getObject: error }); + const service = getService({ getObject: error }); await expect(service.getOrCreate()).rejects.toThrowErrorMatchingSnapshot(); }); @@ -151,7 +171,7 @@ const BaseConfigUnitTests = (serviceGenerator: (...args) => BaseConfigService) = describe('patch', () => { it('uses the existing config if an existing config is found', async () => { const existing = { a: 1 }; - const service = serviceGenerator({ getObject: { Body: JSON.stringify(existing) } }); + const service = getService({ getObject: { Body: JSON.stringify(existing) } }); const additional = { b: 2 }; const expected = { ...existing, ...additional }; @@ -161,9 +181,9 @@ const BaseConfigUnitTests = (serviceGenerator: (...args) => BaseConfigService) = }); it('uses the base config if no existing config is found', async () => { - const error = new Error('404'); + const error: ErrorWithCode = new Error('404'); error.code = S3_NO_SUCH_KEY_ERROR_CODE; - const service = serviceGenerator({ getObject: error }); + const service = getService({ getObject: error }); const existing = service.constructor.defaultConfig; const additional = { b: 2 }; @@ -174,10 +194,10 @@ const BaseConfigUnitTests = (serviceGenerator: (...args) => BaseConfigService) = }); it('throws any non-404 error', async () => { - const error = new Error('Bad error'); + const error: ErrorWithCode = new Error('Bad error'); error.code = 'another'; - const service = serviceGenerator({ getObject: error }); + const service = getService({ getObject: error }); await expect(service.patch({ b: 1 })).rejects.toThrowErrorMatchingSnapshot(); }); @@ -186,18 +206,18 @@ const BaseConfigUnitTests = (serviceGenerator: (...args) => BaseConfigService) = describe('healthCheck', () => { Object.values(ServiceStates).forEach((state) => { describe(state, () => { - it('Returns the expected HTTP code with the given config', async () => { + it('returns the expected HTTP code with the given config', async () => { const config = { state }; - const service = serviceGenerator(); + const service = getService(); const statusCode = await service.healthCheck(config); const expected = ServiceStatesHttpCodes[state]; expect(statusCode).toEqual(expected); }); - it('Returns the expected HTTP code with the existing config', async () => { + it('returns the expected HTTP code with the existing config', async () => { const config = { state }; - const service = serviceGenerator({ getObject: { Body: JSON.stringify(config) } }); + const service = getService({ getObject: { Body: JSON.stringify(config) } }); const statusCode = await service.healthCheck(); const expected = ServiceStatesHttpCodes[state]; @@ -207,18 +227,18 @@ const BaseConfigUnitTests = (serviceGenerator: (...args) => BaseConfigService) = }); describe('Unknown state', () => { - it('Returns 500 with the given config', async () => { + it('returns 500 with the given config', async () => { const config = { state: 'Unknown' }; - const service = serviceGenerator(); + const service = getService(); const statusCode = await service.healthCheck(config); const expected = 500; expect(statusCode).toEqual(expected); }); - it('Returns 500 with the existing config', async () => { + it('returns 500 with the existing config', async () => { const config = { state: 'Unknown' }; - const service = serviceGenerator({ getObject: { Body: JSON.stringify(config) } }); + const service = getService({ getObject: { Body: JSON.stringify(config) } }); const statusCode = await service.healthCheck(); const expected = 500; @@ -231,7 +251,7 @@ const BaseConfigUnitTests = (serviceGenerator: (...args) => BaseConfigService) = [200, 201, 202, 204, 300, 301, 399].forEach((statusCode) => { describe(statusCode, () => { it('is healthy', async () => { - const service = serviceGenerator(); + const service = getService(); jest.spyOn(service, 'healthCheck').mockImplementation(() => Promise.resolve(statusCode)); await expect(service.ensureHealthy()).resolves.toEqual(statusCode); @@ -242,29 +262,29 @@ const BaseConfigUnitTests = (serviceGenerator: (...args) => BaseConfigService) = [400, 401, 403, 404, 409, 499, 500, 501, 502, 503, 504, 'Dante Alighieri'].forEach((statusCode) => { describe(statusCode, () => { it('throws a LambdaTermination', async () => { - const service = serviceGenerator(); - jest.spyOn(service, 'healthCheck').mockImplementation(() => Promise.resolve(statusCode)); + const service = getService(); + jest.spyOn(service, 'healthCheck').mockImplementation(() => Promise.resolve(statusCode as any)); await expect(service.ensureHealthy()).rejects.toThrowErrorMatchingSnapshot(); }); }); }); }); -}; - -describe('Service/BaseConfigService', () => { - BaseConfigUnitTests(getService); describe('client', () => { - it('Returns an s3 instance (static)', () => { - expect(BaseConfigService.client instanceof S3).toEqual(true); + it('should return an S3 instance (static method)', () => { + expect(BaseConfigService.client).toBeInstanceOf(S3); }); - it('Returns an s3 instance', () => { - const di = new DependencyInjection({}, {}, {}); - const service = new BaseConfigService(di); + it('should return an S3 instance (instance method)', () => { + const di = new DependencyInjection({ + dependencies: { + BaseConfigService, + }, + }, {}, {} as Context); + const service = di.get(BaseConfigService); - expect(service.client instanceof S3).toEqual(true); + expect(service.client).toBeInstanceOf(S3); }); }); }); diff --git a/tests/unit/Service/HTTP.service.test.js b/tests/unit/services/HTTPService.spec.ts similarity index 62% rename from tests/unit/Service/HTTP.service.test.js rename to tests/unit/services/HTTPService.spec.ts index e9ab0101..d50fe2d7 100644 --- a/tests/unit/Service/HTTP.service.test.js +++ b/tests/unit/services/HTTPService.spec.ts @@ -1,25 +1,37 @@ import axios from 'axios'; -import CONFIGURATION from '../../../src/Config/Dependencies'; -import DependencyInjection from '../../../src/DependencyInjection/DependencyInjection.class'; -import HTTPService, { COMICRELIEF_TEST_METADATA_HEADER } from '../../../src/Service/HTTP.service'; -import getEvent from '../../mocks/aws/event.json'; +import { + COMICRELIEF_TEST_METADATA_HEADER, + Context, + DependencyInjection, + HTTPService, + RequestService, +} from '@/src'; +import mockEvent from '@/tests/mocks/aws/event.json'; -const getContext = { invokedFunctionArn: 'my-function' }; +const mockContext = { invokedFunctionArn: 'my-function' } as Context; -const getService = (event = getEvent, context = getContext) => new HTTPService(new DependencyInjection(CONFIGURATION, event, context)); +const getService = (event = mockEvent, context = mockContext) => { + const di = new DependencyInjection({ + dependencies: { + HTTPService, + RequestService, + }, + }, event, context); + return di.get(HTTPService); +}; -describe('Service/HTTPService', () => { +describe('unit.services.HTTPService', () => { afterEach(() => jest.clearAllMocks()); describe('request', () => { const testCases = { - 'GET Request': { method: 'GET', url: '/' }, - 'POST Request': { method: 'POST', url: '/' }, - 'PUT Request': { method: 'PUT', url: '/' }, - 'PATCH Request': { method: 'PATCH', url: '/' }, - 'HEAD Request': { method: 'HEAD', url: '/' }, - 'DELETE Request': { method: 'DELETE', url: '/' }, + 'GET request': { method: 'GET', url: '/' }, + 'POST request': { method: 'POST', url: '/' }, + 'PUT request': { method: 'PUT', url: '/' }, + 'PATCH request': { method: 'PATCH', url: '/' }, + 'HEAD request': { method: 'HEAD', url: '/' }, + 'DELETE request': { method: 'DELETE', url: '/' }, 'with URL': { url: '/some/nested/path' }, 'with baseURL': { baseUrl: 'https://comicrelief.com/test', url: '/additional/url' }, 'overriding timeout': { timeout: 99 }, @@ -42,9 +54,9 @@ describe('Service/HTTPService', () => { it(`adds the test header, ${description}`, async () => { const metadata = JSON.stringify({ user: 'Dante Alighieri' }); const event = { - ...getEvent, + ...mockEvent, headers: { - ...getEvent.headers, + ...mockEvent.headers, [COMICRELIEF_TEST_METADATA_HEADER]: metadata, }, }; diff --git a/tests/unit/Service/Logger.service.test.js b/tests/unit/services/LoggerService.spec.ts similarity index 63% rename from tests/unit/Service/Logger.service.test.js rename to tests/unit/services/LoggerService.spec.ts index 492534ce..22259c49 100644 --- a/tests/unit/Service/Logger.service.test.js +++ b/tests/unit/services/LoggerService.spec.ts @@ -1,16 +1,21 @@ import Winston from 'winston'; -import CONFIGURATION from '../../../src/Config/Dependencies'; -import DependencyInjection from '../../../src/DependencyInjection/DependencyInjection.class'; -import LoggerService from '../../../src/Service/Logger.service'; -import getEvent from '../../mocks/aws/event.json'; +import { + Context, + DependencyInjection, + LoggerService, +} from '@/src'; +import mockEvent from '@/tests/mocks/aws/event.json'; -const getContext = { invokedFunctionArn: 'my-function' }; +const mockContext = { invokedFunctionArn: 'my-function' } as Context; -const getLogger = (event = getEvent, context = getContext) => new LoggerService(new DependencyInjection(CONFIGURATION, event, context)); +const getLogger = (event = mockEvent, context = mockContext) => { + const di = new DependencyInjection({ dependencies: { LoggerService } }, event, context); + return new LoggerService(di); +}; -describe('Service/LoggerService', () => { - const context = { invokedFunctionArn: 'my-function' }; +describe('unit.services.LoggerService', () => { + const context = { invokedFunctionArn: 'my-function' } as Context; const axiosResponses = { UNDEFINED: undefined, @@ -24,39 +29,19 @@ describe('Service/LoggerService', () => { afterEach(() => jest.clearAllMocks()); - describe('constructor', () => { - it('Creates a LoggerService instance', () => { - expect(getLogger()).toBeInstanceOf(LoggerService); - }); - }); - - describe('getLogger', () => { - it('Creates a logger instance', () => { - const logger = getLogger(undefined, context); - const winston = logger.getLogger(); - expect(winston.constructor.name).toEqual('DerivedLogger'); - }); - }); - describe('logger', () => { - it('Starts as null', () => { + it('should return a logger', () => { const logger = getLogger(undefined, context); - expect(logger.winston).toEqual(null); + expect(logger.logger.constructor.name).toEqual('DerivedLogger'); }); - it('Fetches a logger', () => { - const winston = Symbol('winston'); + it('should not call `Winston.createLogger` twice', () => { + const winston = Symbol('winston') as unknown as Winston.Logger; const logger = getLogger(undefined, context); - jest.spyOn(Winston, 'createLogger').mockImplementation(() => winston); - - expect(logger.logger).toEqual(winston); - }); - it("Doesn' call Winston.createLogger twice", () => { - const winston = Symbol('winston'); - const logger = getLogger(undefined, context); jest.spyOn(Winston, 'createLogger').mockImplementation(() => winston); + // use the getter several times expect(logger.logger).toEqual(winston); expect(logger.logger).toEqual(winston); expect(logger.logger).toEqual(winston); @@ -70,8 +55,9 @@ describe('Service/LoggerService', () => { it(`Trims down the axios error: ${key}`, () => { const logger = getLogger(); const log = jest.fn(); + const fakeLogger = { log } as unknown as Winston.Logger; - jest.spyOn(logger, 'logger', 'get').mockReturnValue({ log }); + jest.spyOn(logger, 'logger', 'get').mockReturnValue(fakeLogger); const error = { isAxiosError: true, @@ -104,8 +90,9 @@ describe('Service/LoggerService', () => { it(`Trims down the axios error: ${key}`, () => { const logger = getLogger(); const log = jest.fn(); + const fakeLogger = { log } as unknown as Winston.Logger; - jest.spyOn(logger, 'logger', 'get').mockReturnValue({ log }); + jest.spyOn(logger, 'logger', 'get').mockReturnValue(fakeLogger); const error = { isAxiosError: true, @@ -134,7 +121,7 @@ describe('Service/LoggerService', () => { }); describe('warning', () => { - let LOGGER_SOFT_WARNING; + let LOGGER_SOFT_WARNING: string | undefined; beforeAll(() => { LOGGER_SOFT_WARNING = process.env.LOGGER_SOFT_WARNING; @@ -144,21 +131,19 @@ describe('Service/LoggerService', () => { process.env.LOGGER_SOFT_WARNING = LOGGER_SOFT_WARNING; }); - [ + ([ ['', 'error'], ['some-value', 'error'], - [false, 'error'], ['false', 'error'], ['0', 'error'], ['1', 'info'], - [true, 'info'], ['true', 'info'], - ].forEach(([loggerSoftWarning, func]) => { + ] as [string, 'error' | 'info'][]).forEach(([loggerSoftWarning, func]) => { it(`uses 'this.logger.${func}' in ${loggerSoftWarning}`, () => { process.env.LOGGER_SOFT_WARNING = loggerSoftWarning; const logger = getLogger(); - jest.spyOn(logger, func).mockImplementation(() => {}); + jest.spyOn(logger, func).mockImplementation(() => { /* no-op */ }); logger.warning({}); @@ -168,7 +153,11 @@ describe('Service/LoggerService', () => { }); describe('object', () => { - ['error', 'warning', 'info'].forEach((level) => { + ([ + 'error', + 'warning', + 'info', + ] as const).forEach((level) => { [ null, 'a string', @@ -177,13 +166,15 @@ describe('Service/LoggerService', () => { ].forEach((object) => { it(`Logs a '${JSON.stringify(object)}' with level: '${level}'`, () => { const logger = getLogger(); - let message; + let calledArgs: any[] = []; + const fakeLog = (...args: any[]) => { calledArgs = args; }; - jest.spyOn(logger, level).mockImplementation((arg) => { message = arg; }); + jest.spyOn(logger, level).mockImplementation(fakeLog); logger.object('My action', object, level); + expect(logger[level]).toHaveBeenCalledTimes(1); - expect(message).toMatchSnapshot(); + expect(calledArgs[0]).toMatchSnapshot(); }); }); }); diff --git a/tests/unit/Service/Request.service.test.js b/tests/unit/services/RequestService.spec.ts similarity index 56% rename from tests/unit/Service/Request.service.test.js rename to tests/unit/services/RequestService.spec.ts index ccdbab53..82889252 100644 --- a/tests/unit/Service/Request.service.test.js +++ b/tests/unit/services/RequestService.spec.ts @@ -1,90 +1,112 @@ -/* eslint-disable sonarjs/no-duplicate-string */ import QueryString from 'querystring'; -import CONFIGURATION from '../../../src/Config/Dependencies'; -import DependencyInjection from '../../../src/DependencyInjection/DependencyInjection.class'; -import RequestService, { HTTP_METHODS_WITHOUT_PAYLOADS, HTTP_METHODS_WITH_PAYLOADS } from '../../../src/Service/Request.service'; -import getContext from '../../mocks/aws/context.json'; -import baseEvent from '../../mocks/aws/event.json'; +import { + HTTP_METHODS_WITHOUT_PAYLOADS, + HTTP_METHODS_WITH_PAYLOADS, +} from '@/src/services/RequestService'; + +import { + DependencyInjection, + LoggerService, + RequestService, +} from '@/src'; +import { mockContext, mockEvent } from '@/tests/mocks/aws'; const getEvent = (overrides = {}) => JSON.parse(JSON.stringify(({ - ...baseEvent, + ...mockEvent, ...overrides, }))); -describe('Service/RequestService', () => { +const getRequestService = (event: any) => { + const di = new DependencyInjection({ + dependencies: { + RequestService, + LoggerService, + }, + }, event, mockContext); + return new RequestService(di); +}; + +describe('unit.services.RequestService', () => { + beforeAll(() => { + // mute log ouptut + const noop = () => { /* do nothing */ }; + jest.spyOn(LoggerService.prototype, 'info').mockImplementation(noop); + jest.spyOn(LoggerService.prototype, 'error').mockImplementation(noop); + jest.spyOn(LoggerService.prototype, 'metric').mockImplementation(noop); + jest.spyOn(LoggerService.prototype, 'label').mockImplementation(noop); + }); + afterEach(() => jest.resetAllMocks()); HTTP_METHODS_WITHOUT_PAYLOADS.forEach((httpMethod) => { describe(`HTTP ${httpMethod}`, () => { - describe('.getAll', () => { - it('should return all get parameters as an object', () => { + describe('getAll', () => { + it('should return all query string parameters as an object', () => { const event = getEvent({ httpMethod }); - event.queryStringParameters.test = 123; - const request = new RequestService(new DependencyInjection(CONFIGURATION, event, getContext)); + event.queryStringParameters.test = '123'; + const request = getRequestService(event); const params = request.getAll(); - expect(params.test).toEqual(123); + expect(params.test).toEqual('123'); expect(params['array[]']).toEqual(['one', 'two', 'three']); }); }); - describe('.get', () => { - it('should fetch a query parameter from an AWS event', () => { + describe('get', () => { + it('should fetch a query parameter', () => { const event = getEvent({ httpMethod }); - event.queryStringParameters.test = 123; - const request = new RequestService(new DependencyInjection(CONFIGURATION, event, getContext)); + event.queryStringParameters.test = '123'; + const request = getRequestService(event); - expect(request.get('test')).toEqual(event.queryStringParameters.test); + expect(request.get('test')).toEqual('123'); }); - it('should fetch a query parameter from an AWS event when the request type is set', () => { + it('should fetch a query parameter when the request type is given', () => { const event = getEvent({ httpMethod }); event.queryStringParameters.test = 123; - const request = new RequestService(new DependencyInjection(CONFIGURATION, event, getContext)); + const request = getRequestService(event); - expect(request.get('test', null, httpMethod)).toEqual(event.queryStringParameters.test); + const param = request.get('test', null, httpMethod); + expect(param).toEqual(event.queryStringParameters.test); }); - it(`should return null from a non existent ${httpMethod} parameter from an AWS event`, () => { + it(`should return null from a nonexistent ${httpMethod} parameter`, () => { const event = getEvent({ httpMethod }); - const request = new RequestService(new DependencyInjection(CONFIGURATION, event, getContext)); + const request = getRequestService(event); - expect(request.get('fake')).toEqual(null); + const param = request.get('fake'); + expect(param).toBeNull(); }); - it(`should return null from a non existent ${httpMethod} parameter from an AWS event when the request type is set`, () => { + it(`should return null from a nonexistent ${httpMethod} parameter when the request type is given`, () => { const event = getEvent({ httpMethod }); - const request = new RequestService(new DependencyInjection(CONFIGURATION, event, getContext)); + const request = getRequestService(event); - expect(request.get('fake', null, httpMethod)).toEqual(null); + const param = request.get('fake', null, httpMethod); + expect(param).toBeNull(); }); it('should return an array-type query parameter if its name ends []', () => { const event = getEvent({ httpMethod }); - const request = new RequestService(new DependencyInjection(CONFIGURATION, event, getContext)); + const request = getRequestService(event); - expect(request.get('array[]')).toEqual(['one', 'two', 'three']); + const param = request.get('array[]'); + expect(param).toEqual(['one', 'two', 'three']); }); }); - describe('.validateAgainstConstraints', () => { + describe('validateAgainstConstraints', () => { const constraints = { giftaid: { numericality: true, }, }; - beforeEach(() => { - // Mute Winston - // eslint-disable-next-line no-underscore-dangle - jest.spyOn(console._stdout, 'write').mockImplementation(() => {}); - }); - it('should resolve if there are no validation errors', async () => { const event = getEvent({ httpMethod }); event.queryStringParameters.giftaid = 123; - const request = new RequestService(new DependencyInjection(CONFIGURATION, event, getContext)); + const request = getRequestService(event); await expect(request.validateAgainstConstraints(constraints)).resolves.toEqual(undefined); }); @@ -92,30 +114,31 @@ describe('Service/RequestService', () => { it('should return a response containing validation errors if the data provided is incorrect', async () => { const event = getEvent({ httpMethod }); event.queryStringParameters.giftaid = 'abc'; - const request = new RequestService(new DependencyInjection(CONFIGURATION, event, getContext)); + const request = getRequestService(event); await expect(request.validateAgainstConstraints(constraints)).rejects.toMatchSnapshot(); }); }); - describe('.getUserBrowserAndDevice', () => { + describe('getUserBrowserAndDevice', () => { it('should return null with `headers === undefined`', () => { const event = getEvent({ httpMethod, headers: undefined }); - const request = new RequestService(new DependencyInjection(CONFIGURATION, event, getContext)); + const request = getRequestService(event); expect(request.getUserBrowserAndDevice()).toEqual(null); }); it('should return null with `headers === null`', () => { const event = getEvent({ httpMethod, headers: null }); - const request = new RequestService(new DependencyInjection(CONFIGURATION, event, getContext)); + const request = getRequestService(event); expect(request.getUserBrowserAndDevice()).toEqual(null); }); it('should return a prettified user agent', () => { const event = getEvent({ httpMethod }); - const request = new RequestService(new DependencyInjection(CONFIGURATION, event, getContext)); + const request = getRequestService(event); + expect(request.getUserBrowserAndDevice()).toEqual({ 'browser-type': 'Safari', 'browser-version': '9.1.1', @@ -133,75 +156,68 @@ describe('Service/RequestService', () => { const event = getEvent({ httpMethod }); event.headers['Content-Type'] = 'application/x-www-form-urlencoded'; event.body = 'grant_type=client_credentials&response_type=token&token_format=opaque'; - return { ...event, ...overrides }; }; const queryParameters = QueryString.parse(getPayloadEvent().body); describe(`HTTP ${httpMethod}`, () => { - describe('.getAll', () => { + describe('getAll', () => { it('should return all post parameters as an array', () => { const event = getPayloadEvent(); - const request = new RequestService(new DependencyInjection(CONFIGURATION, event, getContext)); + const request = getRequestService(event); expect(request.getAll()).toEqual(queryParameters); }); }); - describe('.get', () => { + describe('get', () => { it('should fetch a request body parameter from an AWS event', () => { const event = getPayloadEvent(); - const request = new RequestService(new DependencyInjection(CONFIGURATION, event, getContext)); + const request = getRequestService(event); expect(request.get('grant_type')).toEqual(queryParameters.grant_type); }); it('should fetch a request body parameter from an AWS event when the request type is set', () => { const event = getPayloadEvent(); - const request = new RequestService(new DependencyInjection(CONFIGURATION, event, getContext)); + const request = getRequestService(event); expect(request.get('grant_type', null, httpMethod)).toEqual(queryParameters.grant_type); }); it('should return null from a non existent request body parameter from an AWS event', () => { const event = getPayloadEvent(); - const request = new RequestService(new DependencyInjection(CONFIGURATION, event, getContext)); + const request = getRequestService(event); expect(request.get('fake')).toEqual(null); }); it('should return null from a non existent request body parameter from an AWS event when the request type is set', () => { const event = getPayloadEvent(); - const request = new RequestService(new DependencyInjection(CONFIGURATION, event, getContext), getEvent); + const request = getRequestService(event); expect(request.get('fake', null, httpMethod)).toEqual(null); }); }); - describe('.validateAgainstConstraints', () => { + describe('validateAgainstConstraints', () => { const constraints = { giftaid: { numericality: true, }, }; - beforeEach(() => { - // Mute Winston - // eslint-disable-next-line no-underscore-dangle - jest.spyOn(console._stdout, 'write').mockImplementation(() => {}); - }); - it('should resolve if there are no validation errors', async () => { const event = getPayloadEvent({ body: 'giftaid=123' }); - const request = new RequestService(new DependencyInjection(CONFIGURATION, event, getContext)); + const request = getRequestService(event); await expect(request.validateAgainstConstraints(constraints)).resolves.toEqual(undefined); }); it('should return a response containing validation errors if the data provided is incorrect', async () => { const event = getPayloadEvent({ body: 'giftaid=abc' }); - const request = new RequestService(new DependencyInjection(CONFIGURATION, event, getContext)); + const request = getRequestService(event); await expect(request.validateAgainstConstraints(constraints)).rejects.toMatchSnapshot(); }); @@ -209,20 +225,18 @@ describe('Service/RequestService', () => { }); }); - describe('getAllHeaders()', () => { + describe('getAllHeaders', () => { const event = getEvent(); - const di = new DependencyInjection(CONFIGURATION, event, getContext); - const request = new RequestService(di); + const request = getRequestService(event); it('should return all headers from the event', () => { expect(request.getAllHeaders()).toStrictEqual(event.headers); }); }); - describe('getHeader()', () => { + describe('getHeader', () => { const event = getEvent(); - const di = new DependencyInjection(CONFIGURATION, event, getContext); - const request = new RequestService(di); + const request = getRequestService(event); it('should return the specified header', () => { expect(request.getHeader('Accept')).toEqual(event.headers.Accept); diff --git a/tests/unit/Service/SQS.service.test.js b/tests/unit/services/SQSService.spec.ts similarity index 84% rename from tests/unit/Service/SQS.service.test.js rename to tests/unit/services/SQSService.spec.ts index 8ffd210a..7e6838cd 100644 --- a/tests/unit/Service/SQS.service.test.js +++ b/tests/unit/services/SQSService.spec.ts @@ -1,8 +1,33 @@ -import { DEFINITIONS } from '../../../src/Config/Dependencies'; -import DependencyInjection from '../../../src/DependencyInjection/DependencyInjection.class'; -import { SQS_PUBLISH_FAILURE_MODES } from '../../../src/Service/SQS.service'; +import { + Context, + DependencyInjection, + LambdaWrapperConfig, + LoggerService, + SQSService, + SQS_PUBLISH_FAILURE_MODES, + TimerService, + WithSQSServiceConfig, +} from '@/src'; -const createAsyncMock = (returnValue) => { +const TEST_QUEUE = 'TEST_QUEUE'; + +const config: LambdaWrapperConfig & WithSQSServiceConfig = { + dependencies: { + SQSService, + LoggerService, + TimerService, + }, + sqs: { + queues: { + [TEST_QUEUE]: 'QueueName', + }, + queueConsumers: { + [TEST_QUEUE]: 'ConsumerFunctionName', + }, + }, +}; + +const createAsyncMock = (returnValue: any) => { const mockedValue = returnValue instanceof Error ? Promise.reject(returnValue) : Promise.resolve(returnValue); @@ -10,48 +35,45 @@ const createAsyncMock = (returnValue) => { return jest.fn().mockReturnValue({ promise: () => mockedValue }); }; -const TEST_QUEUE = 'TEST_QUEUE'; - /** * Generates a SQSService * - * @param {*} param0 + * @param param0 * @param isOffline - * @returns {SQSService} */ -const getService = ({ sendMessage = null, invoke = null } = {}, isOffline = false) => { - const di = new DependencyInjection({ - QUEUES: { [TEST_QUEUE]: 'QueueName' }, - QUEUE_CONSUMERS: { TEST_QUEUE }, - }, {}, { +const getService = ( + { + sendMessage = null, + invoke = null, + }: any = {}, + isOffline = false, +): SQSService & { sqs: { sendMessage: jest.Mock }; lambda: { invoke: jest.Mock } } => { + const di = new DependencyInjection(config, {}, { invokedFunctionArn: isOffline ? 'offline' : 'arn:aws:lambda:eu-west-1:0123456789:test', - }); - - const logger = di.get(DEFINITIONS.LOGGER); + } as Context); + const logger = di.get(LoggerService); jest.spyOn(logger, 'error').mockImplementation(); - const service = di.get(DEFINITIONS.SQS); + const service = di.get(SQSService); const sqs = { sendMessage: createAsyncMock(sendMessage), - }; - + } as unknown as AWS.SQS; const lambda = { invoke: createAsyncMock(invoke), - }; - + } as unknown as AWS.Lambda; jest.spyOn(service, 'sqs', 'get').mockReturnValue(sqs); jest.spyOn(service, 'lambda', 'get').mockReturnValue(lambda); - return service; + return service as any; }; -describe('Service/SQS', () => { - let envAccountId; - let envOfflineSqsMode; - let envOfflineSqsHost; - let envOfflineSqsPort; - let envRegion; +describe('unit.services.SQSService', () => { + let envAccountId: string | undefined; + let envOfflineSqsMode: string | undefined; + let envOfflineSqsHost: string | undefined; + let envOfflineSqsPort: string | undefined; + let envRegion: string | undefined; beforeAll(() => { envAccountId = process.env.AWS_ACCOUNT_ID; @@ -73,15 +95,23 @@ describe('Service/SQS', () => { jest.resetAllMocks(); }); + it('should load config from the `sqs` key', () => { + const di = new DependencyInjection(config, {}, {} as Context); + const sqs = di.get(SQSService); + + expect(sqs.queues).toEqual(config.sqs?.queues); + expect(sqs.queueConsumers).toEqual(config.sqs?.queueConsumers); + }); + describe('publish', () => { describe('when container.isOffline === false', () => { - [ + ([ ['sends to SQS', undefined], ['sends to SQS, even in "direct" offline mode', 'direct'], ['sends to SQS, even in "local" offline mode', 'local'], ['sends to SQS, even in "aws" offline mode', 'aws'], ['sends to SQS, even in "invalid" offline mode', 'invalid'], - ].forEach(([description, offlineMode]) => { + ] as const).forEach(([description, offlineMode]) => { it(description, async () => { process.env.LAMBDA_WRAPPER_OFFLINE_SQS_MODE = offlineMode; const service = getService({}, false); @@ -255,7 +285,7 @@ describe('Service/SQS', () => { '', null, 'another-value', - ].forEach((invalidValue) => { + ].forEach((invalidValue: any) => { it(`throws an error with the invalid value: ${invalidValue}`, async () => { const service = getService(); diff --git a/tests/unit/services/TimerService.spec.ts b/tests/unit/services/TimerService.spec.ts new file mode 100644 index 00000000..c4dcf82e --- /dev/null +++ b/tests/unit/services/TimerService.spec.ts @@ -0,0 +1,36 @@ +import { + Context, + DependencyInjection, + LoggerService, + TimerService, +} from '@/src'; + +describe('unit.services.TimerService', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it('should measure time between start and stop', () => { + const di = new DependencyInjection({ + dependencies: { + TimerService, + LoggerService, + }, + }, {}, {} as Context); + const timer = di.get(TimerService); + const logger = di.get(LoggerService); + + let info = 'logger.info not called!'; + jest.spyOn(logger, 'info').mockImplementation((msg: any) => { info = msg; }); + + timer.start('test'); + jest.advanceTimersByTime(12345); + timer.stop('test'); + + expect(info).toContain('test took 12345 ms to complete'); + }); +}); diff --git a/tests/unit/services/__snapshots__/BaseConfigService.spec.ts.snap b/tests/unit/services/__snapshots__/BaseConfigService.spec.ts.snap new file mode 100644 index 00000000..5f6fec23 --- /dev/null +++ b/tests/unit/services/__snapshots__/BaseConfigService.spec.ts.snap @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`unit.services.BaseConfigService ensureHealthy 400 throws a LambdaTermination 1`] = `"Application is not healthy."`; + +exports[`unit.services.BaseConfigService ensureHealthy 401 throws a LambdaTermination 1`] = `"Application is not healthy."`; + +exports[`unit.services.BaseConfigService ensureHealthy 403 throws a LambdaTermination 1`] = `"Application is not healthy."`; + +exports[`unit.services.BaseConfigService ensureHealthy 404 throws a LambdaTermination 1`] = `"Application is not healthy."`; + +exports[`unit.services.BaseConfigService ensureHealthy 409 throws a LambdaTermination 1`] = `"Application is not healthy."`; + +exports[`unit.services.BaseConfigService ensureHealthy 499 throws a LambdaTermination 1`] = `"Application is not healthy."`; + +exports[`unit.services.BaseConfigService ensureHealthy 500 throws a LambdaTermination 1`] = `"Application is not healthy."`; + +exports[`unit.services.BaseConfigService ensureHealthy 501 throws a LambdaTermination 1`] = `"Application is not healthy."`; + +exports[`unit.services.BaseConfigService ensureHealthy 502 throws a LambdaTermination 1`] = `"Application is not healthy."`; + +exports[`unit.services.BaseConfigService ensureHealthy 503 throws a LambdaTermination 1`] = `"Application is not healthy."`; + +exports[`unit.services.BaseConfigService ensureHealthy 504 throws a LambdaTermination 1`] = `"Application is not healthy."`; + +exports[`unit.services.BaseConfigService ensureHealthy Dante Alighieri throws a LambdaTermination 1`] = `"Application is not healthy."`; + +exports[`unit.services.BaseConfigService get propagates the 404 1`] = `"404"`; + +exports[`unit.services.BaseConfigService get refuses empty configurations 1`] = `"Configuration file is empty"`; + +exports[`unit.services.BaseConfigService get refuses invalid configurations 1`] = `"Invalid configuration file"`; + +exports[`unit.services.BaseConfigService getOrCreate throws any non-404 error 1`] = `"Bad error"`; + +exports[`unit.services.BaseConfigService patch throws any non-404 error 1`] = `"Bad error"`; diff --git a/tests/unit/Service/__snapshots__/HTTP.service.test.js.snap b/tests/unit/services/__snapshots__/HTTPService.spec.ts.snap similarity index 68% rename from tests/unit/Service/__snapshots__/HTTP.service.test.js.snap rename to tests/unit/services/__snapshots__/HTTPService.spec.ts.snap index 0b933d4d..2328d61f 100644 --- a/tests/unit/Service/__snapshots__/HTTP.service.test.js.snap +++ b/tests/unit/services/__snapshots__/HTTPService.spec.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Service/HTTPService request DELETE Request: config 1`] = ` +exports[`unit.services.HTTPService request DELETE request: config 1`] = ` Array [ Array [ Object { @@ -13,7 +13,7 @@ Array [ ] `; -exports[`Service/HTTPService request GET Request: config 1`] = ` +exports[`unit.services.HTTPService request GET request: config 1`] = ` Array [ Array [ Object { @@ -26,7 +26,7 @@ Array [ ] `; -exports[`Service/HTTPService request HEAD Request: config 1`] = ` +exports[`unit.services.HTTPService request HEAD request: config 1`] = ` Array [ Array [ Object { @@ -39,7 +39,7 @@ Array [ ] `; -exports[`Service/HTTPService request PATCH Request: config 1`] = ` +exports[`unit.services.HTTPService request PATCH request: config 1`] = ` Array [ Array [ Object { @@ -52,7 +52,7 @@ Array [ ] `; -exports[`Service/HTTPService request POST Request: config 1`] = ` +exports[`unit.services.HTTPService request POST request: config 1`] = ` Array [ Array [ Object { @@ -65,7 +65,7 @@ Array [ ] `; -exports[`Service/HTTPService request PUT Request: config 1`] = ` +exports[`unit.services.HTTPService request PUT request: config 1`] = ` Array [ Array [ Object { @@ -78,7 +78,7 @@ Array [ ] `; -exports[`Service/HTTPService request adds the test header, DELETE Request: config 1`] = ` +exports[`unit.services.HTTPService request adds the test header, DELETE request: config 1`] = ` Array [ Array [ Object { @@ -93,7 +93,7 @@ Array [ ] `; -exports[`Service/HTTPService request adds the test header, GET Request: config 1`] = ` +exports[`unit.services.HTTPService request adds the test header, GET request: config 1`] = ` Array [ Array [ Object { @@ -108,7 +108,7 @@ Array [ ] `; -exports[`Service/HTTPService request adds the test header, HEAD Request: config 1`] = ` +exports[`unit.services.HTTPService request adds the test header, HEAD request: config 1`] = ` Array [ Array [ Object { @@ -123,7 +123,7 @@ Array [ ] `; -exports[`Service/HTTPService request adds the test header, PATCH Request: config 1`] = ` +exports[`unit.services.HTTPService request adds the test header, PATCH request: config 1`] = ` Array [ Array [ Object { @@ -138,7 +138,7 @@ Array [ ] `; -exports[`Service/HTTPService request adds the test header, POST Request: config 1`] = ` +exports[`unit.services.HTTPService request adds the test header, POST request: config 1`] = ` Array [ Array [ Object { @@ -153,7 +153,7 @@ Array [ ] `; -exports[`Service/HTTPService request adds the test header, PUT Request: config 1`] = ` +exports[`unit.services.HTTPService request adds the test header, PUT request: config 1`] = ` Array [ Array [ Object { @@ -168,7 +168,7 @@ Array [ ] `; -exports[`Service/HTTPService request adds the test header, overriding timeout: config 1`] = ` +exports[`unit.services.HTTPService request adds the test header, overriding timeout: config 1`] = ` Array [ Array [ Object { @@ -181,7 +181,7 @@ Array [ ] `; -exports[`Service/HTTPService request adds the test header, with URL: config 1`] = ` +exports[`unit.services.HTTPService request adds the test header, with URL: config 1`] = ` Array [ Array [ Object { @@ -195,7 +195,7 @@ Array [ ] `; -exports[`Service/HTTPService request adds the test header, with baseURL: config 1`] = ` +exports[`unit.services.HTTPService request adds the test header, with baseURL: config 1`] = ` Array [ Array [ Object { @@ -210,7 +210,7 @@ Array [ ] `; -exports[`Service/HTTPService request adds the test header, with headers: config 1`] = ` +exports[`unit.services.HTTPService request adds the test header, with headers: config 1`] = ` Array [ Array [ Object { @@ -224,7 +224,7 @@ Array [ ] `; -exports[`Service/HTTPService request adds the test header, with undefined headers: config 1`] = ` +exports[`unit.services.HTTPService request adds the test header, with undefined headers: config 1`] = ` Array [ Array [ Object { @@ -237,7 +237,7 @@ Array [ ] `; -exports[`Service/HTTPService request overriding timeout: config 1`] = ` +exports[`unit.services.HTTPService request overriding timeout: config 1`] = ` Array [ Array [ Object { @@ -248,7 +248,7 @@ Array [ ] `; -exports[`Service/HTTPService request with URL: config 1`] = ` +exports[`unit.services.HTTPService request with URL: config 1`] = ` Array [ Array [ Object { @@ -260,7 +260,7 @@ Array [ ] `; -exports[`Service/HTTPService request with baseURL: config 1`] = ` +exports[`unit.services.HTTPService request with baseURL: config 1`] = ` Array [ Array [ Object { @@ -273,7 +273,7 @@ Array [ ] `; -exports[`Service/HTTPService request with headers: config 1`] = ` +exports[`unit.services.HTTPService request with headers: config 1`] = ` Array [ Array [ Object { @@ -286,7 +286,7 @@ Array [ ] `; -exports[`Service/HTTPService request with undefined headers: config 1`] = ` +exports[`unit.services.HTTPService request with undefined headers: config 1`] = ` Array [ Array [ Object { diff --git a/tests/unit/services/__snapshots__/LoggerService.spec.ts.snap b/tests/unit/services/__snapshots__/LoggerService.spec.ts.snap new file mode 100644 index 00000000..5033ec6a --- /dev/null +++ b/tests/unit/services/__snapshots__/LoggerService.spec.ts.snap @@ -0,0 +1,138 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`unit.services.LoggerService error Trims down the axios error: EMPTY 1`] = ` +Object { + "config": Object { + "method": "get", + "url": "http://localhost:9999", + }, + "message": "some-message", + "response": Object { + "data": undefined, + "status": undefined, + }, +} +`; + +exports[`unit.services.LoggerService error Trims down the axios error: HTTP_417 1`] = ` +Object { + "config": Object { + "method": "get", + "url": "http://localhost:9999", + }, + "message": "some-message", + "response": Object { + "data": Object { + "data": 1, + }, + "status": 417, + }, +} +`; + +exports[`unit.services.LoggerService error Trims down the axios error: UNDEFINED 1`] = ` +Object { + "config": Object { + "method": "get", + "url": "http://localhost:9999", + }, + "message": "some-message", +} +`; + +exports[`unit.services.LoggerService info Trims down the axios error: EMPTY 1`] = ` +Object { + "config": Object { + "method": "get", + "url": "http://localhost:9999", + }, + "message": "some-message", + "response": Object { + "data": undefined, + "status": undefined, + }, +} +`; + +exports[`unit.services.LoggerService info Trims down the axios error: HTTP_417 1`] = ` +Object { + "config": Object { + "method": "get", + "url": "http://localhost:9999", + }, + "message": "some-message", + "response": Object { + "data": Object { + "data": 1, + }, + "status": 417, + }, +} +`; + +exports[`unit.services.LoggerService info Trims down the axios error: UNDEFINED 1`] = ` +Object { + "config": Object { + "method": "get", + "url": "http://localhost:9999", + }, + "message": "some-message", +} +`; + +exports[`unit.services.LoggerService object Logs a '"a string"' with level: 'error' 1`] = `"My action: '\\"a string\\"'"`; + +exports[`unit.services.LoggerService object Logs a '"a string"' with level: 'info' 1`] = `"My action: '\\"a string\\"'"`; + +exports[`unit.services.LoggerService object Logs a '"a string"' with level: 'warning' 1`] = `"My action: '\\"a string\\"'"`; + +exports[`unit.services.LoggerService object Logs a '{"a":{"b":null},"c":"a string"}' with level: 'error' 1`] = ` +"My action: '{ + \\"a\\": { + \\"b\\": null + }, + \\"c\\": \\"a string\\" +}'" +`; + +exports[`unit.services.LoggerService object Logs a '{"a":{"b":null},"c":"a string"}' with level: 'info' 1`] = ` +"My action: '{ + \\"a\\": { + \\"b\\": null + }, + \\"c\\": \\"a string\\" +}'" +`; + +exports[`unit.services.LoggerService object Logs a '{"a":{"b":null},"c":"a string"}' with level: 'warning' 1`] = ` +"My action: '{ + \\"a\\": { + \\"b\\": null + }, + \\"c\\": \\"a string\\" +}'" +`; + +exports[`unit.services.LoggerService object Logs a '{"a":1}' with level: 'error' 1`] = ` +"My action: '{ + \\"a\\": 1 +}'" +`; + +exports[`unit.services.LoggerService object Logs a '{"a":1}' with level: 'info' 1`] = ` +"My action: '{ + \\"a\\": 1 +}'" +`; + +exports[`unit.services.LoggerService object Logs a '{"a":1}' with level: 'warning' 1`] = ` +"My action: '{ + \\"a\\": 1 +}'" +`; + +exports[`unit.services.LoggerService object Logs a 'null' with level: 'error' 1`] = `"My action: 'null'"`; + +exports[`unit.services.LoggerService object Logs a 'null' with level: 'info' 1`] = `"My action: 'null'"`; + +exports[`unit.services.LoggerService object Logs a 'null' with level: 'warning' 1`] = `"My action: 'null'"`; diff --git a/tests/unit/Service/__snapshots__/Request.service.test.js.snap b/tests/unit/services/__snapshots__/RequestService.spec.ts.snap similarity index 58% rename from tests/unit/Service/__snapshots__/Request.service.test.js.snap rename to tests/unit/services/__snapshots__/RequestService.spec.ts.snap index 868fb53d..5eaa46b7 100644 --- a/tests/unit/Service/__snapshots__/Request.service.test.js.snap +++ b/tests/unit/services/__snapshots__/RequestService.spec.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Service/RequestService HTTP DELETE .validateAgainstConstraints should return a response containing validation errors if the data provided is incorrect 1`] = ` +exports[`unit.services.RequestService HTTP DELETE validateAgainstConstraints should return a response containing validation errors if the data provided is incorrect 1`] = ` ResponseModel { "body": Object { "data": Object {}, @@ -15,7 +15,7 @@ ResponseModel { } `; -exports[`Service/RequestService HTTP GET .validateAgainstConstraints should return a response containing validation errors if the data provided is incorrect 1`] = ` +exports[`unit.services.RequestService HTTP GET validateAgainstConstraints should return a response containing validation errors if the data provided is incorrect 1`] = ` ResponseModel { "body": Object { "data": Object {}, @@ -30,7 +30,7 @@ ResponseModel { } `; -exports[`Service/RequestService HTTP HEAD .validateAgainstConstraints should return a response containing validation errors if the data provided is incorrect 1`] = ` +exports[`unit.services.RequestService HTTP HEAD validateAgainstConstraints should return a response containing validation errors if the data provided is incorrect 1`] = ` ResponseModel { "body": Object { "data": Object {}, @@ -45,7 +45,7 @@ ResponseModel { } `; -exports[`Service/RequestService HTTP OPTIONS .validateAgainstConstraints should return a response containing validation errors if the data provided is incorrect 1`] = ` +exports[`unit.services.RequestService HTTP OPTIONS validateAgainstConstraints should return a response containing validation errors if the data provided is incorrect 1`] = ` ResponseModel { "body": Object { "data": Object {}, @@ -60,7 +60,7 @@ ResponseModel { } `; -exports[`Service/RequestService HTTP PATCH .validateAgainstConstraints should return a response containing validation errors if the data provided is incorrect 1`] = ` +exports[`unit.services.RequestService HTTP PATCH validateAgainstConstraints should return a response containing validation errors if the data provided is incorrect 1`] = ` ResponseModel { "body": Object { "data": Object {}, @@ -75,7 +75,7 @@ ResponseModel { } `; -exports[`Service/RequestService HTTP POST .validateAgainstConstraints should return a response containing validation errors if the data provided is incorrect 1`] = ` +exports[`unit.services.RequestService HTTP POST validateAgainstConstraints should return a response containing validation errors if the data provided is incorrect 1`] = ` ResponseModel { "body": Object { "data": Object {}, @@ -90,7 +90,7 @@ ResponseModel { } `; -exports[`Service/RequestService HTTP PUT .validateAgainstConstraints should return a response containing validation errors if the data provided is incorrect 1`] = ` +exports[`unit.services.RequestService HTTP PUT validateAgainstConstraints should return a response containing validation errors if the data provided is incorrect 1`] = ` ResponseModel { "body": Object { "data": Object {}, diff --git a/tests/unit/services/__snapshots__/SQSService.spec.ts.snap b/tests/unit/services/__snapshots__/SQSService.spec.ts.snap new file mode 100644 index 00000000..be64da51 --- /dev/null +++ b/tests/unit/services/__snapshots__/SQSService.spec.ts.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`unit.services.SQSService publish failure modes throws an error with the invalid value: 1`] = `"Invalid value for 'failureMode': "`; + +exports[`unit.services.SQSService publish failure modes throws an error with the invalid value: another-value 1`] = `"Invalid value for 'failureMode': another-value"`; + +exports[`unit.services.SQSService publish failure modes throws an error with the invalid value: null 1`] = `"Invalid value for 'failureMode': null"`; diff --git a/tests/unit/Wrapper/LambdaTermination.test.js b/tests/unit/utils/LambdaTermination.spec.ts similarity index 55% rename from tests/unit/Wrapper/LambdaTermination.test.js rename to tests/unit/utils/LambdaTermination.spec.ts index dee0d9c6..607ba577 100644 --- a/tests/unit/Wrapper/LambdaTermination.test.js +++ b/tests/unit/utils/LambdaTermination.spec.ts @@ -1,7 +1,7 @@ -import LambdaTermination from '../../../src/Wrapper/LambdaTermination'; +import { LambdaTermination } from '@/src'; -describe('Wrapper/LambdaTermination', () => { - describe('Stores the custom fields', () => { +describe('unit.utils.LambdaTermination', () => { + describe('custom fields', () => { const properties = { internal: 'INTERNAL', code: 401, @@ -11,24 +11,24 @@ describe('Wrapper/LambdaTermination', () => { const lt = new LambdaTermination(properties.internal, properties.code, properties.body); Object.entries(properties).forEach(([key, value]) => { - it(`Exposes '${key}'`, () => { - expect(lt[key]).toEqual(value); + it(`should set and expose '${key}'`, () => { + expect(lt[key as keyof LambdaTermination]).toEqual(value); }); }); }); - it('Generates an error', () => { + it('should create an instance of error', () => { const lt = new LambdaTermination('internal'); - expect(lt instanceof Error).toEqual(true); + expect(lt).toBeInstanceOf(Error); }); - describe('Passes a prop to the superclass that', () => { - it('Becomes Error.message', () => { + describe('error message', () => { + it('should use `internal` param when a string', () => { const lt = new LambdaTermination('abc'); expect(lt.message).toEqual('abc'); }); - it('Is stringified when an object', () => { + it('should use stringified `internal` param when an object', () => { const details = { a: 1 }; const stringified = JSON.stringify(details); const lt = new LambdaTermination(details); diff --git a/tsconfig-base.json b/tsconfig-base.json new file mode 100644 index 00000000..70011e4e --- /dev/null +++ b/tsconfig-base.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + "strict": true, + "target": "es2020", + "module": "commonjs", + "esModuleInterop": true, + "lib": ["es2020"], + "sourceMap": true, + "declaration": true, + "declarationMap": true, + "outDir": "dist", + "baseUrl": ".", + "paths": { + "@/*": ["./*"], + }, + /* Additional Checks */ + "noUnusedLocals": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + }, + "ts-node": { + "files": true, + "require": [ + "tsconfig-paths/register", + ], + }, +} diff --git a/tsconfig-build.json b/tsconfig-build.json new file mode 100644 index 00000000..3b71185b --- /dev/null +++ b/tsconfig-build.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig-base.json", + "compilerOptions": { + "rootDir": "src", + "types": ["node"], + }, + "include": [ + "src/**/*.ts", + "types/*.d.ts", + ], +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..1d67ec3f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig-base.json", + "compilerOptions": { + "rootDir": ".", + "resolveJsonModule": true, + }, + "include": [ + "src/**/*.ts", + "tests/**/*.ts", + "types/*.d.ts", + ], +} diff --git a/types/alai.d.ts b/types/alai.d.ts new file mode 100644 index 00000000..ab06dcbe --- /dev/null +++ b/types/alai.d.ts @@ -0,0 +1,3 @@ +declare module 'alai' { + export function parse(ctx: import('aws-lambda').Context): string; +} diff --git a/yarn.lock b/yarn.lock index e8b95ca8..75682aad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -649,22 +649,6 @@ "@aws-sdk/util-buffer-from" "3.55.0" tslib "^2.3.1" -"@babel/cli@^7.18.10": - version "7.18.10" - resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.18.10.tgz#4211adfc45ffa7d4f3cee6b60bb92e9fe68fe56a" - integrity sha512-dLvWH+ZDFAkd2jPBSghrsFBuXrREvFwjpDycXbmUoeochqKYe4zNSLEJYErpLg8dvxvZYe79/MkN461XCwpnGw== - dependencies: - "@jridgewell/trace-mapping" "^0.3.8" - commander "^4.0.1" - convert-source-map "^1.1.0" - fs-readdir-recursive "^1.1.0" - glob "^7.2.0" - make-dir "^2.1.0" - slash "^2.0.0" - optionalDependencies: - "@nicolo-ribaudo/chokidar-2" "2.1.8-no-fsevents.3" - chokidar "^3.4.0" - "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" @@ -672,12 +656,12 @@ dependencies: "@babel/highlight" "^7.18.6" -"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.18.8": +"@babel/compat-data@^7.18.8": version "7.18.8" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.18.8.tgz#2483f565faca607b8535590e84e7de323f27764d" integrity sha512-HSmX4WZPPK3FUxYp7g2T6EyO8j96HlZJlxmKPSh6KAcqwyDrfx7hKjXpAW/0FhFfTJsR0Yt4lAjLI2coMptIHQ== -"@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.18.10", "@babel/core@^7.7.5": +"@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.7.5": version "7.18.10" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.10.tgz#39ad504991d77f1f3da91be0b8b949a5bc466fb8" integrity sha512-JQM6k6ENcBFKVtWvLavlvi/mPcpYZ3+R+2EySDEMSMbp7Mn4FexlbbJVrx2R7Ijhr01T8gyqrOaABWIOgxeUyw== @@ -698,15 +682,6 @@ json5 "^2.2.1" semver "^6.3.0" -"@babel/eslint-parser@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.18.9.tgz#255a63796819a97b7578751bb08ab9f2a375a031" - integrity sha512-KzSGpMBggz4fKbRbWLNyPVTuQr6cmCcBhOyXTw/fieOVaw5oYAwcAj4a7UKcDYCPxQq+CG1NCDZH9e2JTXquiQ== - dependencies: - eslint-scope "^5.1.1" - eslint-visitor-keys "^2.1.0" - semver "^6.3.0" - "@babel/generator@^7.18.10", "@babel/generator@^7.7.2": version "7.18.12" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.12.tgz#fa58daa303757bd6f5e4bbca91b342040463d9f4" @@ -716,22 +691,7 @@ "@jridgewell/gen-mapping" "^0.3.2" jsesc "^2.5.1" -"@babel/helper-annotate-as-pure@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb" - integrity sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-builder-binary-assignment-operator-visitor@^7.18.6": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz#acd4edfd7a566d1d51ea975dff38fd52906981bb" - integrity sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw== - dependencies: - "@babel/helper-explode-assignable-expression" "^7.18.6" - "@babel/types" "^7.18.9" - -"@babel/helper-compilation-targets@^7.17.7", "@babel/helper-compilation-targets@^7.18.9": +"@babel/helper-compilation-targets@^7.18.9": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.9.tgz#69e64f57b524cde3e5ff6cc5a9f4a387ee5563bf" integrity sha512-tzLCyVmqUiFlcFoAPLA/gL9TeYrF61VLNtb+hvkuVaB5SUjW7jcfrglBIX1vUIoT7CLP3bBlIMeyEsIl2eFQNg== @@ -741,51 +701,11 @@ browserslist "^4.20.2" semver "^6.3.0" -"@babel/helper-create-class-features-plugin@^7.18.6": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.18.9.tgz#d802ee16a64a9e824fcbf0a2ffc92f19d58550ce" - integrity sha512-WvypNAYaVh23QcjpMR24CwZY2Nz6hqdOcFdPbNpV56hL5H6KiFheO7Xm1aPdlLQ7d5emYZX7VZwPp9x3z+2opw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.18.9" - "@babel/helper-member-expression-to-functions" "^7.18.9" - "@babel/helper-optimise-call-expression" "^7.18.6" - "@babel/helper-replace-supers" "^7.18.9" - "@babel/helper-split-export-declaration" "^7.18.6" - -"@babel/helper-create-regexp-features-plugin@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.18.6.tgz#3e35f4e04acbbf25f1b3534a657610a000543d3c" - integrity sha512-7LcpH1wnQLGrI+4v+nPp+zUvIkF9x0ddv1Hkdue10tg3gmRnLy97DXh4STiOf1qeIInyD69Qv5kKSZzKD8B/7A== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - regexpu-core "^5.1.0" - -"@babel/helper-define-polyfill-provider@^0.3.2": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.2.tgz#bd10d0aca18e8ce012755395b05a79f45eca5073" - integrity sha512-r9QJJ+uDWrd+94BSPcP6/de67ygLtvVy6cK4luE6MOuDsZIdoaPBnfSpbO/+LTifjPckbKXRuI9BB/Z2/y3iTg== - dependencies: - "@babel/helper-compilation-targets" "^7.17.7" - "@babel/helper-plugin-utils" "^7.16.7" - debug "^4.1.1" - lodash.debounce "^4.0.8" - resolve "^1.14.2" - semver "^6.1.2" - "@babel/helper-environment-visitor@^7.18.9": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== -"@babel/helper-explode-assignable-expression@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz#41f8228ef0a6f1a036b8dfdfec7ce94f9a6bc096" - integrity sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg== - dependencies: - "@babel/types" "^7.18.6" - "@babel/helper-function-name@^7.18.9": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.18.9.tgz#940e6084a55dee867d33b4e487da2676365e86b0" @@ -801,13 +721,6 @@ dependencies: "@babel/types" "^7.18.6" -"@babel/helper-member-expression-to-functions@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.9.tgz#1531661e8375af843ad37ac692c132841e2fd815" - integrity sha512-RxifAh2ZoVU67PyKIO4AMi1wTenGfMR/O/ae0CCRqwgBAt5v7xjdtRw7UoSbsreKrQn5t7r89eruK/9JjYHuDg== - dependencies: - "@babel/types" "^7.18.9" - "@babel/helper-module-imports@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e" @@ -815,7 +728,7 @@ dependencies: "@babel/types" "^7.18.6" -"@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.18.9": +"@babel/helper-module-transforms@^7.18.9": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.18.9.tgz#5a1079c005135ed627442df31a42887e80fcb712" integrity sha512-KYNqY0ICwfv19b31XzvmI/mfcylOzbLtowkw+mfvGPAQ3kfCnMLYbED3YecL5tPd8nAYFQFAd6JHp2LxZk/J1g== @@ -829,39 +742,11 @@ "@babel/traverse" "^7.18.9" "@babel/types" "^7.18.9" -"@babel/helper-optimise-call-expression@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz#9369aa943ee7da47edab2cb4e838acf09d290ffe" - integrity sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.18.9", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.8.0": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.9.tgz#4b8aea3b069d8cb8a72cdfe28ddf5ceca695ef2f" integrity sha512-aBXPT3bmtLryXaoJLyYPXPlSD4p1ld9aYeR+sJNOZjJJGiOpb+fKfh3NkcCu7J54nUJwCERPBExCCpyCOHnu/w== -"@babel/helper-remap-async-to-generator@^7.18.6", "@babel/helper-remap-async-to-generator@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz#997458a0e3357080e54e1d79ec347f8a8cd28519" - integrity sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-wrap-function" "^7.18.9" - "@babel/types" "^7.18.9" - -"@babel/helper-replace-supers@^7.18.6", "@babel/helper-replace-supers@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.18.9.tgz#1092e002feca980fbbb0bd4d51b74a65c6a500e6" - integrity sha512-dNsWibVI4lNT6HiuOIBr1oyxo40HvIVmbwPUm3XZ7wMh4k2WxrxTqZwSqw/eEmXDS9np0ey5M2bz9tBmO9c+YQ== - dependencies: - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-member-expression-to-functions" "^7.18.9" - "@babel/helper-optimise-call-expression" "^7.18.6" - "@babel/traverse" "^7.18.9" - "@babel/types" "^7.18.9" - "@babel/helper-simple-access@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz#d6d8f51f4ac2978068df934b569f08f29788c7ea" @@ -869,13 +754,6 @@ dependencies: "@babel/types" "^7.18.6" -"@babel/helper-skip-transparent-expression-wrappers@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.18.9.tgz#778d87b3a758d90b471e7b9918f34a9a02eb5818" - integrity sha512-imytd2gHi3cJPsybLRbmFrF7u5BIEuI2cNheyKi3/iOBC63kNn3q8Crn2xVuESli0aM4KYsyEqKyS7lFL8YVtw== - dependencies: - "@babel/types" "^7.18.9" - "@babel/helper-split-export-declaration@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075" @@ -888,7 +766,7 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz#181f22d28ebe1b3857fa575f5c290b1aaf659b56" integrity sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw== -"@babel/helper-validator-identifier@^7.15.7", "@babel/helper-validator-identifier@^7.18.6": +"@babel/helper-validator-identifier@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076" integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g== @@ -898,16 +776,6 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8" integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw== -"@babel/helper-wrap-function@^7.18.9": - version "7.18.11" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.18.11.tgz#bff23ace436e3f6aefb61f85ffae2291c80ed1fb" - integrity sha512-oBUlbv+rjZLh2Ks9SKi4aL7eKaAXBWleHzU89mP0G6BMUlRxSckk9tSIkgDGydhgFxHuGSlBQZfnaD47oBEB7w== - dependencies: - "@babel/helper-function-name" "^7.18.9" - "@babel/template" "^7.18.10" - "@babel/traverse" "^7.18.11" - "@babel/types" "^7.18.10" - "@babel/helpers@^7.18.9": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.18.9.tgz#4bef3b893f253a1eced04516824ede94dcfe7ff9" @@ -926,168 +794,11 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/node@^7.18.10": - version "7.18.10" - resolved "https://registry.yarnpkg.com/@babel/node/-/node-7.18.10.tgz#ab2be57785346b5bf0721c3d17572402419d9d8a" - integrity sha512-VbqzK6QXfQVi4Bpk6J7XqHXKFNbG2j3rdIdx68+/14GDU7jXDOSyUU/cwqCM1fDwCdxp37pNV/ToSCXsNChcyA== - dependencies: - "@babel/register" "^7.18.9" - commander "^4.0.1" - core-js "^3.22.1" - node-environment-flags "^1.0.5" - regenerator-runtime "^0.13.4" - v8flags "^3.1.1" - "@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.18.10", "@babel/parser@^7.18.11": version "7.18.11" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.11.tgz#68bb07ab3d380affa9a3f96728df07969645d2d9" integrity sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ== -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2" - integrity sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.18.9.tgz#a11af19aa373d68d561f08e0a57242350ed0ec50" - integrity sha512-AHrP9jadvH7qlOj6PINbgSuphjQUAK7AOT7DPjBo9EHoLhQTnnK5u45e1Hd4DbSQEO9nqPWtQ89r+XEOWFScKg== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - "@babel/helper-skip-transparent-expression-wrappers" "^7.18.9" - "@babel/plugin-proposal-optional-chaining" "^7.18.9" - -"@babel/plugin-proposal-async-generator-functions@^7.18.10": - version "7.18.10" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.18.10.tgz#85ea478c98b0095c3e4102bff3b67d306ed24952" - integrity sha512-1mFuY2TOsR1hxbjCo4QL+qlIjV07p4H4EUYw2J/WCqsvFV6V9X9z9YhXbWndc/4fw+hYGlDT7egYxliMp5O6Ew== - dependencies: - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-plugin-utils" "^7.18.9" - "@babel/helper-remap-async-to-generator" "^7.18.9" - "@babel/plugin-syntax-async-generators" "^7.8.4" - -"@babel/plugin-proposal-class-properties@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz#b110f59741895f7ec21a6fff696ec46265c446a3" - integrity sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-proposal-class-static-block@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.18.6.tgz#8aa81d403ab72d3962fc06c26e222dacfc9b9020" - integrity sha512-+I3oIiNxrCpup3Gi8n5IGMwj0gOCAjcJUSQEcotNnCCPMEnixawOQ+KeJPlgfjzx+FKQ1QSyZOWe7wmoJp7vhw== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-class-static-block" "^7.14.5" - -"@babel/plugin-proposal-dynamic-import@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz#72bcf8d408799f547d759298c3c27c7e7faa4d94" - integrity sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - -"@babel/plugin-proposal-export-namespace-from@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz#5f7313ab348cdb19d590145f9247540e94761203" - integrity sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - -"@babel/plugin-proposal-json-strings@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz#7e8788c1811c393aff762817e7dbf1ebd0c05f0b" - integrity sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-json-strings" "^7.8.3" - -"@babel/plugin-proposal-logical-assignment-operators@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.18.9.tgz#8148cbb350483bf6220af06fa6db3690e14b2e23" - integrity sha512-128YbMpjCrP35IOExw2Fq+x55LMP42DzhOhX2aNNIdI9avSWl2PI0yuBWarr3RYpZBSPtabfadkH2yeRiMD61Q== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - -"@babel/plugin-proposal-nullish-coalescing-operator@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz#fdd940a99a740e577d6c753ab6fbb43fdb9467e1" - integrity sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - -"@babel/plugin-proposal-numeric-separator@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz#899b14fbafe87f053d2c5ff05b36029c62e13c75" - integrity sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - -"@babel/plugin-proposal-object-rest-spread@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.18.9.tgz#f9434f6beb2c8cae9dfcf97d2a5941bbbf9ad4e7" - integrity sha512-kDDHQ5rflIeY5xl69CEqGEZ0KY369ehsCIEbTGb4siHG5BE9sga/T0r0OUwyZNLMmZE79E1kbsqAjwFCW4ds6Q== - dependencies: - "@babel/compat-data" "^7.18.8" - "@babel/helper-compilation-targets" "^7.18.9" - "@babel/helper-plugin-utils" "^7.18.9" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.18.8" - -"@babel/plugin-proposal-optional-catch-binding@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz#f9400d0e6a3ea93ba9ef70b09e72dd6da638a2cb" - integrity sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - -"@babel/plugin-proposal-optional-chaining@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.18.9.tgz#e8e8fe0723f2563960e4bf5e9690933691915993" - integrity sha512-v5nwt4IqBXihxGsW2QmCWMDS3B3bzGIk/EQVZz2ei7f3NJl8NzAJVvUmpDW5q1CRNY+Beb/k58UAH1Km1N411w== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - "@babel/helper-skip-transparent-expression-wrappers" "^7.18.9" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - -"@babel/plugin-proposal-private-methods@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz#5209de7d213457548a98436fa2882f52f4be6bea" - integrity sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-proposal-private-property-in-object@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.18.6.tgz#a64137b232f0aca3733a67eb1a144c192389c503" - integrity sha512-9Rysx7FOctvT5ouj5JODjAFAkgGoudQuLPamZb0v1TGLpapdNaftzifU8NTWQm0IRjqoYypdrSmyWgkocDQ8Dw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-create-class-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" - -"@babel/plugin-proposal-unicode-property-regex@^7.18.6", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz#af613d2cd5e643643b65cded64207b15c85cb78e" - integrity sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" @@ -1102,48 +813,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-class-properties@^7.12.13", "@babel/plugin-syntax-class-properties@^7.8.3": +"@babel/plugin-syntax-class-properties@^7.8.3": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== dependencies: "@babel/helper-plugin-utils" "^7.12.13" -"@babel/plugin-syntax-class-static-block@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" - integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-dynamic-import@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" - integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-export-namespace-from@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" - integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - -"@babel/plugin-syntax-flow@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.18.6.tgz#774d825256f2379d06139be0c723c4dd444f3ca1" - integrity sha512-LUbR+KNTBWCUAqRG9ex5Gnzu2IOkt8jRJbHHXFT9q+L9zm7M/QQbEqXyw1n1pohYvOyWC8CjeyjrSaIwiYjK7A== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-syntax-import-assertions@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.18.6.tgz#cd6190500a4fa2fe31990a963ffab4b63e4505e4" - integrity sha512-/DU3RXad9+bZwrgWJQKbr39gYbJpLJHezqEzRzi/BHRlJ9zsQb4CK2CA/5apllXNomwA1qHwzvHl+AdEmC5krQ== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-import-meta@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" @@ -1158,14 +834,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-jsx@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz#a8feef63b010150abd97f1649ec296e849943ca0" - integrity sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-syntax-logical-assignment-operators@^7.10.4", "@babel/plugin-syntax-logical-assignment-operators@^7.8.3": +"@babel/plugin-syntax-logical-assignment-operators@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== @@ -1179,7 +848,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-numeric-separator@^7.10.4", "@babel/plugin-syntax-numeric-separator@^7.8.3": +"@babel/plugin-syntax-numeric-separator@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== @@ -1202,412 +871,24 @@ "@babel/plugin-syntax-optional-chaining@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" - integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-private-property-in-object@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" - integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-top-level-await@^7.14.5", "@babel/plugin-syntax-top-level-await@^7.8.3": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" - integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-typescript@^7.7.2": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.18.6.tgz#1c09cd25795c7c2b8a4ba9ae49394576d4133285" - integrity sha512-mAWAuq4rvOepWCBid55JuRNvpTNf2UGVgoz4JV0fXEKolsVZDzsa4NqCef758WZJj/GDu0gVGItjKFiClTAmZA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-arrow-functions@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.18.6.tgz#19063fcf8771ec7b31d742339dac62433d0611fe" - integrity sha512-9S9X9RUefzrsHZmKMbDXxweEH+YlE8JJEuat9FdvW9Qh1cw7W64jELCtWNkPBPX5En45uy28KGvA/AySqUh8CQ== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-async-to-generator@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.18.6.tgz#ccda3d1ab9d5ced5265fdb13f1882d5476c71615" - integrity sha512-ARE5wZLKnTgPW7/1ftQmSi1CmkqqHo2DNmtztFhvgtOWSDfq0Cq9/9L+KnZNYSNrydBekhW3rwShduf59RoXag== - dependencies: - "@babel/helper-module-imports" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/helper-remap-async-to-generator" "^7.18.6" - -"@babel/plugin-transform-block-scoped-functions@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz#9187bf4ba302635b9d70d986ad70f038726216a8" - integrity sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-block-scoping@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.18.9.tgz#f9b7e018ac3f373c81452d6ada8bd5a18928926d" - integrity sha512-5sDIJRV1KtQVEbt/EIBwGy4T01uYIo4KRB3VUqzkhrAIOGx7AoctL9+Ux88btY0zXdDyPJ9mW+bg+v+XEkGmtw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-classes@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.18.9.tgz#90818efc5b9746879b869d5ce83eb2aa48bbc3da" - integrity sha512-EkRQxsxoytpTlKJmSPYrsOMjCILacAjtSVkd4gChEe2kXjFCun3yohhW5I7plXJhCemM0gKsaGMcO8tinvCA5g== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.18.9" - "@babel/helper-optimise-call-expression" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.9" - "@babel/helper-replace-supers" "^7.18.9" - "@babel/helper-split-export-declaration" "^7.18.6" - globals "^11.1.0" - -"@babel/plugin-transform-computed-properties@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.18.9.tgz#2357a8224d402dad623caf6259b611e56aec746e" - integrity sha512-+i0ZU1bCDymKakLxn5srGHrsAPRELC2WIbzwjLhHW9SIE1cPYkLCL0NlnXMZaM1vhfgA2+M7hySk42VBvrkBRw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-destructuring@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.18.9.tgz#68906549c021cb231bee1db21d3b5b095f8ee292" - integrity sha512-p5VCYNddPLkZTq4XymQIaIfZNJwT9YsjkPOhkVEqt6QIpQFZVM9IltqqYpOEkJoN1DPznmxUDyZ5CTZs/ZCuHA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-dotall-regex@^7.18.6", "@babel/plugin-transform-dotall-regex@^7.4.4": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz#b286b3e7aae6c7b861e45bed0a2fafd6b1a4fef8" - integrity sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-duplicate-keys@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz#687f15ee3cdad6d85191eb2a372c4528eaa0ae0e" - integrity sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-exponentiation-operator@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz#421c705f4521888c65e91fdd1af951bfefd4dacd" - integrity sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw== - dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-flow-strip-types@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.18.9.tgz#5b4cc521426263b5ce08893a2db41097ceba35bf" - integrity sha512-+G6rp2zRuOAInY5wcggsx4+QVao1qPM0osC9fTUVlAV3zOrzTCnrMAFVnR6+a3T8wz1wFIH7KhYMcMB3u1n80A== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - "@babel/plugin-syntax-flow" "^7.18.6" - -"@babel/plugin-transform-for-of@^7.18.8": - version "7.18.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.18.8.tgz#6ef8a50b244eb6a0bdbad0c7c61877e4e30097c1" - integrity sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-function-name@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz#cc354f8234e62968946c61a46d6365440fc764e0" - integrity sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ== - dependencies: - "@babel/helper-compilation-targets" "^7.18.9" - "@babel/helper-function-name" "^7.18.9" - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-literals@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz#72796fdbef80e56fba3c6a699d54f0de557444bc" - integrity sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-member-expression-literals@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz#ac9fdc1a118620ac49b7e7a5d2dc177a1bfee88e" - integrity sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-modules-amd@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.18.6.tgz#8c91f8c5115d2202f277549848874027d7172d21" - integrity sha512-Pra5aXsmTsOnjM3IajS8rTaLCy++nGM4v3YR4esk5PCsyg9z8NA5oQLwxzMUtDBd8F+UmVza3VxoAaWCbzH1rg== - dependencies: - "@babel/helper-module-transforms" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - babel-plugin-dynamic-import-node "^2.3.3" - -"@babel/plugin-transform-modules-commonjs@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.18.6.tgz#afd243afba166cca69892e24a8fd8c9f2ca87883" - integrity sha512-Qfv2ZOWikpvmedXQJDSbxNqy7Xr/j2Y8/KfijM0iJyKkBTmWuvCA1yeH1yDM7NJhBW/2aXxeucLj6i80/LAJ/Q== - dependencies: - "@babel/helper-module-transforms" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/helper-simple-access" "^7.18.6" - babel-plugin-dynamic-import-node "^2.3.3" - -"@babel/plugin-transform-modules-systemjs@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.18.9.tgz#545df284a7ac6a05125e3e405e536c5853099a06" - integrity sha512-zY/VSIbbqtoRoJKo2cDTewL364jSlZGvn0LKOf9ntbfxOvjfmyrdtEEOAdswOswhZEb8UH3jDkCKHd1sPgsS0A== - dependencies: - "@babel/helper-hoist-variables" "^7.18.6" - "@babel/helper-module-transforms" "^7.18.9" - "@babel/helper-plugin-utils" "^7.18.9" - "@babel/helper-validator-identifier" "^7.18.6" - babel-plugin-dynamic-import-node "^2.3.3" - -"@babel/plugin-transform-modules-umd@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz#81d3832d6034b75b54e62821ba58f28ed0aab4b9" - integrity sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ== - dependencies: - "@babel/helper-module-transforms" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-named-capturing-groups-regex@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.18.6.tgz#c89bfbc7cc6805d692f3a49bc5fc1b630007246d" - integrity sha512-UmEOGF8XgaIqD74bC8g7iV3RYj8lMf0Bw7NJzvnS9qQhM4mg+1WHKotUIdjxgD2RGrgFLZZPCFPFj3P/kVDYhg== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-new-target@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz#d128f376ae200477f37c4ddfcc722a8a1b3246a8" - integrity sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-object-super@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz#fb3c6ccdd15939b6ff7939944b51971ddc35912c" - integrity sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/helper-replace-supers" "^7.18.6" - -"@babel/plugin-transform-parameters@^7.18.8": - version "7.18.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.18.8.tgz#ee9f1a0ce6d78af58d0956a9378ea3427cccb48a" - integrity sha512-ivfbE3X2Ss+Fj8nnXvKJS6sjRG4gzwPMsP+taZC+ZzEGjAYlvENixmt1sZ5Ca6tWls+BlKSGKPJ6OOXvXCbkFg== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-property-literals@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz#e22498903a483448e94e032e9bbb9c5ccbfc93a3" - integrity sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-react-jsx@^7.18.10": - version "7.18.10" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.18.10.tgz#ea47b2c4197102c196cbd10db9b3bb20daa820f1" - integrity sha512-gCy7Iikrpu3IZjYZolFE4M1Sm+nrh1/6za2Ewj77Z+XirT4TsbJcvOFOyF+fRPwU6AKKK136CZxx6L8AbSFG6A== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-module-imports" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.9" - "@babel/plugin-syntax-jsx" "^7.18.6" - "@babel/types" "^7.18.10" - -"@babel/plugin-transform-regenerator@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.18.6.tgz#585c66cb84d4b4bf72519a34cfce761b8676ca73" - integrity sha512-poqRI2+qiSdeldcz4wTSTXBRryoq3Gc70ye7m7UD5Ww0nE29IXqMl6r7Nd15WBgRd74vloEMlShtH6CKxVzfmQ== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - regenerator-transform "^0.15.0" - -"@babel/plugin-transform-reserved-words@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz#b1abd8ebf8edaa5f7fe6bbb8d2133d23b6a6f76a" - integrity sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-shorthand-properties@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz#6d6df7983d67b195289be24909e3f12a8f664dc9" - integrity sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-spread@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.18.9.tgz#6ea7a6297740f381c540ac56caf75b05b74fb664" - integrity sha512-39Q814wyoOPtIB/qGopNIL9xDChOE1pNU0ZY5dO0owhiVt/5kFm4li+/bBtwc7QotG0u5EPzqhZdjMtmqBqyQA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - "@babel/helper-skip-transparent-expression-wrappers" "^7.18.9" - -"@babel/plugin-transform-sticky-regex@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz#c6706eb2b1524028e317720339583ad0f444adcc" - integrity sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-template-literals@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz#04ec6f10acdaa81846689d63fae117dd9c243a5e" - integrity sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-typeof-symbol@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz#c8cea68263e45addcd6afc9091429f80925762c0" - integrity sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-unicode-escapes@^7.18.10": - version "7.18.10" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz#1ecfb0eda83d09bbcb77c09970c2dd55832aa246" - integrity sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-unicode-regex@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz#194317225d8c201bbae103364ffe9e2cea36cdca" - integrity sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/preset-env@^7.18.10": - version "7.18.10" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.18.10.tgz#83b8dfe70d7eea1aae5a10635ab0a5fe60dfc0f4" - integrity sha512-wVxs1yjFdW3Z/XkNfXKoblxoHgbtUF7/l3PvvP4m02Qz9TZ6uZGxRVYjSQeR87oQmHco9zWitW5J82DJ7sCjvA== - dependencies: - "@babel/compat-data" "^7.18.8" - "@babel/helper-compilation-targets" "^7.18.9" - "@babel/helper-plugin-utils" "^7.18.9" - "@babel/helper-validator-option" "^7.18.6" - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.18.6" - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.18.9" - "@babel/plugin-proposal-async-generator-functions" "^7.18.10" - "@babel/plugin-proposal-class-properties" "^7.18.6" - "@babel/plugin-proposal-class-static-block" "^7.18.6" - "@babel/plugin-proposal-dynamic-import" "^7.18.6" - "@babel/plugin-proposal-export-namespace-from" "^7.18.9" - "@babel/plugin-proposal-json-strings" "^7.18.6" - "@babel/plugin-proposal-logical-assignment-operators" "^7.18.9" - "@babel/plugin-proposal-nullish-coalescing-operator" "^7.18.6" - "@babel/plugin-proposal-numeric-separator" "^7.18.6" - "@babel/plugin-proposal-object-rest-spread" "^7.18.9" - "@babel/plugin-proposal-optional-catch-binding" "^7.18.6" - "@babel/plugin-proposal-optional-chaining" "^7.18.9" - "@babel/plugin-proposal-private-methods" "^7.18.6" - "@babel/plugin-proposal-private-property-in-object" "^7.18.6" - "@babel/plugin-proposal-unicode-property-regex" "^7.18.6" - "@babel/plugin-syntax-async-generators" "^7.8.4" - "@babel/plugin-syntax-class-properties" "^7.12.13" - "@babel/plugin-syntax-class-static-block" "^7.14.5" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - "@babel/plugin-syntax-import-assertions" "^7.18.6" - "@babel/plugin-syntax-json-strings" "^7.8.3" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" - "@babel/plugin-syntax-top-level-await" "^7.14.5" - "@babel/plugin-transform-arrow-functions" "^7.18.6" - "@babel/plugin-transform-async-to-generator" "^7.18.6" - "@babel/plugin-transform-block-scoped-functions" "^7.18.6" - "@babel/plugin-transform-block-scoping" "^7.18.9" - "@babel/plugin-transform-classes" "^7.18.9" - "@babel/plugin-transform-computed-properties" "^7.18.9" - "@babel/plugin-transform-destructuring" "^7.18.9" - "@babel/plugin-transform-dotall-regex" "^7.18.6" - "@babel/plugin-transform-duplicate-keys" "^7.18.9" - "@babel/plugin-transform-exponentiation-operator" "^7.18.6" - "@babel/plugin-transform-for-of" "^7.18.8" - "@babel/plugin-transform-function-name" "^7.18.9" - "@babel/plugin-transform-literals" "^7.18.9" - "@babel/plugin-transform-member-expression-literals" "^7.18.6" - "@babel/plugin-transform-modules-amd" "^7.18.6" - "@babel/plugin-transform-modules-commonjs" "^7.18.6" - "@babel/plugin-transform-modules-systemjs" "^7.18.9" - "@babel/plugin-transform-modules-umd" "^7.18.6" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.18.6" - "@babel/plugin-transform-new-target" "^7.18.6" - "@babel/plugin-transform-object-super" "^7.18.6" - "@babel/plugin-transform-parameters" "^7.18.8" - "@babel/plugin-transform-property-literals" "^7.18.6" - "@babel/plugin-transform-regenerator" "^7.18.6" - "@babel/plugin-transform-reserved-words" "^7.18.6" - "@babel/plugin-transform-shorthand-properties" "^7.18.6" - "@babel/plugin-transform-spread" "^7.18.9" - "@babel/plugin-transform-sticky-regex" "^7.18.6" - "@babel/plugin-transform-template-literals" "^7.18.9" - "@babel/plugin-transform-typeof-symbol" "^7.18.9" - "@babel/plugin-transform-unicode-escapes" "^7.18.10" - "@babel/plugin-transform-unicode-regex" "^7.18.6" - "@babel/preset-modules" "^0.1.5" - "@babel/types" "^7.18.10" - babel-plugin-polyfill-corejs2 "^0.3.2" - babel-plugin-polyfill-corejs3 "^0.5.3" - babel-plugin-polyfill-regenerator "^0.4.0" - core-js-compat "^3.22.1" - semver "^6.3.0" - -"@babel/preset-modules@^0.1.5": - version "0.1.5" - resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.5.tgz#ef939d6e7f268827e1841638dc6ff95515e115d9" - integrity sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA== + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" - "@babel/plugin-transform-dotall-regex" "^7.4.4" - "@babel/types" "^7.4.4" - esutils "^2.0.2" + "@babel/helper-plugin-utils" "^7.8.0" -"@babel/register@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.18.9.tgz#1888b24bc28d5cc41c412feb015e9ff6b96e439c" - integrity sha512-ZlbnXDcNYHMR25ITwwNKT88JiaukkdVj/nG7r3wnuXkOTHc60Uy05PwMCPre0hSkY68E6zK3xz+vUJSP2jWmcw== +"@babel/plugin-syntax-top-level-await@^7.8.3": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== dependencies: - clone-deep "^4.0.1" - find-cache-dir "^2.0.0" - make-dir "^2.1.0" - pirates "^4.0.5" - source-map-support "^0.5.16" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/runtime@^7.8.4": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.9.tgz#b4fcfce55db3d2e5e080d2490f608a3b9f407f4a" - integrity sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw== +"@babel/plugin-syntax-typescript@^7.7.2": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.18.6.tgz#1c09cd25795c7c2b8a4ba9ae49394576d4133285" + integrity sha512-mAWAuq4rvOepWCBid55JuRNvpTNf2UGVgoz4JV0fXEKolsVZDzsa4NqCef758WZJj/GDu0gVGItjKFiClTAmZA== dependencies: - regenerator-runtime "^0.13.4" + "@babel/helper-plugin-utils" "^7.18.6" "@babel/template@^7.18.10", "@babel/template@^7.18.6", "@babel/template@^7.3.3": version "7.18.10" @@ -1618,7 +899,7 @@ "@babel/parser" "^7.18.10" "@babel/types" "^7.18.10" -"@babel/traverse@^7.18.10", "@babel/traverse@^7.18.11", "@babel/traverse@^7.18.9", "@babel/traverse@^7.7.2": +"@babel/traverse@^7.18.10", "@babel/traverse@^7.18.9", "@babel/traverse@^7.7.2": version "7.18.11" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.11.tgz#3d51f2afbd83ecf9912bcbb5c4d94e3d2ddaa16f" integrity sha512-TG9PiM2R/cWCAy6BPJKeHzNbu4lPzOSZpeMfeNErskGpTJx6trEvFaVCbDvpcxwy49BKWmEPwiW8mrysNiDvIQ== @@ -1634,7 +915,7 @@ debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": +"@babel/types@^7.0.0", "@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.3.0", "@babel/types@^7.3.3": version "7.18.10" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.10.tgz#4908e81b6b339ca7c6b7a555a5fc29446f26dde6" integrity sha512-MJvnbEiiNkpjo+LknnmRrqbY1GPUUggjv+wQVjetM/AONoupqRALB7I6jGqNUAZsKcRIEu2J6FRFvsczljjsaQ== @@ -1661,6 +942,13 @@ eslint-config-airbnb "^19.0.4" eslint-config-airbnb-base "^15.0.0" +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + "@dabh/diagnostics@^2.0.2": version "2.0.3" resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.3.tgz#7f7e97ee9a725dffc7808d93668cc984e1dc477a" @@ -1734,6 +1022,13 @@ js-yaml "^3.13.1" resolve-from "^5.0.0" +"@istanbuljs/nyc-config-typescript@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@istanbuljs/nyc-config-typescript/-/nyc-config-typescript-1.0.2.tgz#1f5235b28540a07219ae0dd42014912a0b19cf89" + integrity sha512-iKGIyMoyJuFnJRSVTZ78POIRvNnwZaWIf8vG4ZS3rQq58MMDrqEX2nnzx0R28V2X8JvmKYiqY9FP2hlJsm8A0w== + dependencies: + "@istanbuljs/schema" "^0.1.2" + "@istanbuljs/schema@^0.1.2": version "0.1.3" resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" @@ -1964,7 +1259,15 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== -"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.13", "@jridgewell/trace-mapping@^0.3.8", "@jridgewell/trace-mapping@^0.3.9": +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.13", "@jridgewell/trace-mapping@^0.3.9": version "0.3.15" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz#aba35c48a38d3fd84b37e66c9c0423f9744f9774" integrity sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g== @@ -1972,11 +1275,6 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" -"@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3": - version "2.1.8-no-fsevents.3" - resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz#323d72dd25103d0c4fbdce89dadf574a787b1f9b" - integrity sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ== - "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -2430,6 +1728,36 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== +"@tsconfig/node10@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" + integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" + integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== + +"@types/async@^3.2.15": + version "3.2.15" + resolved "https://registry.yarnpkg.com/@types/async/-/async-3.2.15.tgz#26d4768fdda0e466f18d6c9918ca28cc89a4e1fe" + integrity sha512-PAmPfzvFA31mRoqZyTVsgJMsvbynR429UTTxhmfsUCrWGh3/fxOrzqBtaTPJsn4UtzTv4Vb0+/O7CARWb69N4g== + +"@types/aws-lambda@^8.10.102": + version "8.10.102" + resolved "https://registry.yarnpkg.com/@types/aws-lambda/-/aws-lambda-8.10.102.tgz#d2402224ec30cdddfb669005c25b6ee01fd6f5be" + integrity sha512-BT05v46n9KtSHa9SgGuOvm49eSruJ9utD8iNXpdpuUVYk8wOcqmm1LEzpNRkrXxD0CULc38sdLpk6q3Wa2WOwg== + "@types/babel__core@^7.1.14": version "7.1.19" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.19.tgz#7b497495b7d1b4812bdb9d02804d0576f43ee460" @@ -2497,6 +1825,11 @@ jest-matcher-utils "^28.0.0" pretty-format "^28.0.0" +"@types/json-schema@^7.0.9": + version "7.0.11" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" + integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== + "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" @@ -2512,6 +1845,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.3.tgz#432c89796eab539b7a30b7b8801a727b585238a4" integrity sha512-LJgzOEwWuMTBxHzgBR/fhhBOWrvBjvO+zPteUgbbuQi80rYIZHrk1mNbRUqPZqSLP2H7Rwt1EFLL/tNLD1Xx/w== +"@types/node@14": + version "14.18.23" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.23.tgz#70f5f20b0b1b38f696848c1d3647bb95694e615e" + integrity sha512-MhbCWN18R4GhO8ewQWAFK4TGQdBpXWByukz7cWyJmXhvRuCIaM/oWytGPqVmDzgEnnaIc9ss6HbU5mUi+vyZPA== + "@types/normalize-package-data@^2.4.0": version "2.4.1" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" @@ -2537,6 +1875,23 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== +"@types/useragent@^2.3.1": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@types/useragent/-/useragent-2.3.1.tgz#c971243faa04f50df399da35d77538ab5fabae20" + integrity sha512-w70ziElAVDD8lEOQ2Id3YBDE0sn2DTVA1zLB59H4kFngYoOJAIlnMkndiZFrzzHE0jmFDZ9AEWNvmeTm6Rvj9A== + +"@types/uuid@^8.3.4": + version "8.3.4" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" + integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== + +"@types/xml2js@^0.4.11": + version "0.4.11" + resolved "https://registry.yarnpkg.com/@types/xml2js/-/xml2js-0.4.11.tgz#bf46a84ecc12c41159a7bd9cf51ae84129af0e79" + integrity sha512-JdigeAKmCyoJUiQljjr7tQG3if9NkqGUgwEUqBvV0N7LM4HyQk7UXCnusRa1lnvXAEYJ8mw8GtZWioagNztOwA== + dependencies: + "@types/node" "*" + "@types/yargs-parser@*": version "21.0.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" @@ -2549,6 +1904,86 @@ dependencies: "@types/yargs-parser" "*" +"@typescript-eslint/eslint-plugin@^5.33.0": + version "5.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.33.0.tgz#059798888720ec52ffa96c5f868e31a8f70fa3ec" + integrity sha512-jHvZNSW2WZ31OPJ3enhLrEKvAZNyAFWZ6rx9tUwaessTc4sx9KmgMNhVcqVAl1ETnT5rU5fpXTLmY9YvC1DCNg== + dependencies: + "@typescript-eslint/scope-manager" "5.33.0" + "@typescript-eslint/type-utils" "5.33.0" + "@typescript-eslint/utils" "5.33.0" + debug "^4.3.4" + functional-red-black-tree "^1.0.1" + ignore "^5.2.0" + regexpp "^3.2.0" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/parser@^5.33.0": + version "5.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.33.0.tgz#26ec3235b74f0667414613727cb98f9b69dc5383" + integrity sha512-cgM5cJrWmrDV2KpvlcSkelTBASAs1mgqq+IUGKJvFxWrapHpaRy5EXPQz9YaKF3nZ8KY18ILTiVpUtbIac86/w== + dependencies: + "@typescript-eslint/scope-manager" "5.33.0" + "@typescript-eslint/types" "5.33.0" + "@typescript-eslint/typescript-estree" "5.33.0" + debug "^4.3.4" + +"@typescript-eslint/scope-manager@5.33.0": + version "5.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.33.0.tgz#509d7fa540a2c58f66bdcfcf278a3fa79002e18d" + integrity sha512-/Jta8yMNpXYpRDl8EwF/M8It2A9sFJTubDo0ATZefGXmOqlaBffEw0ZbkbQ7TNDK6q55NPHFshGBPAZvZkE8Pw== + dependencies: + "@typescript-eslint/types" "5.33.0" + "@typescript-eslint/visitor-keys" "5.33.0" + +"@typescript-eslint/type-utils@5.33.0": + version "5.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.33.0.tgz#92ad1fba973c078d23767ce2d8d5a601baaa9338" + integrity sha512-2zB8uEn7hEH2pBeyk3NpzX1p3lF9dKrEbnXq1F7YkpZ6hlyqb2yZujqgRGqXgRBTHWIUG3NGx/WeZk224UKlIA== + dependencies: + "@typescript-eslint/utils" "5.33.0" + debug "^4.3.4" + tsutils "^3.21.0" + +"@typescript-eslint/types@5.33.0": + version "5.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.33.0.tgz#d41c584831805554b063791338b0220b613a275b" + integrity sha512-nIMt96JngB4MYFYXpZ/3ZNU4GWPNdBbcB5w2rDOCpXOVUkhtNlG2mmm8uXhubhidRZdwMaMBap7Uk8SZMU/ppw== + +"@typescript-eslint/typescript-estree@5.33.0": + version "5.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.33.0.tgz#02d9c9ade6f4897c09e3508c27de53ad6bfa54cf" + integrity sha512-tqq3MRLlggkJKJUrzM6wltk8NckKyyorCSGMq4eVkyL5sDYzJJcMgZATqmF8fLdsWrW7OjjIZ1m9v81vKcaqwQ== + dependencies: + "@typescript-eslint/types" "5.33.0" + "@typescript-eslint/visitor-keys" "5.33.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/utils@5.33.0": + version "5.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.33.0.tgz#46797461ce3146e21c095d79518cc0f8ec574038" + integrity sha512-JxOAnXt9oZjXLIiXb5ZIcZXiwVHCkqZgof0O8KPgz7C7y0HS42gi75PdPlqh1Tf109M0fyUw45Ao6JLo7S5AHw== + dependencies: + "@types/json-schema" "^7.0.9" + "@typescript-eslint/scope-manager" "5.33.0" + "@typescript-eslint/types" "5.33.0" + "@typescript-eslint/typescript-estree" "5.33.0" + eslint-scope "^5.1.1" + eslint-utils "^3.0.0" + +"@typescript-eslint/visitor-keys@5.33.0": + version "5.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.33.0.tgz#fbcbb074e460c11046e067bc3384b5d66b555484" + integrity sha512-/XsqCzD4t+Y9p5wd9HZiptuGKBlaZO5showwqODii5C0nZawxWLF+Q6k5wYHBrQv96h6GYKyqqMHCSTqta8Kiw== + dependencies: + "@typescript-eslint/types" "5.33.0" + eslint-visitor-keys "^3.3.0" + JSONStream@^1.0.4: version "1.3.5" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" @@ -2567,7 +2002,12 @@ acorn-jsx@^5.3.2: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn@^8.8.0: +acorn-walk@^8.1.1: + version "8.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" + integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + +acorn@^8.4.1, acorn@^8.8.0: version "8.8.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== @@ -2654,7 +2094,7 @@ ansicolors@~0.3.2: resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979" integrity sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg== -anymatch@^3.0.3, anymatch@~3.1.2: +anymatch@^3.0.3: version "3.1.2" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== @@ -2687,6 +2127,11 @@ are-we-there-yet@^3.0.0: delegates "^1.0.0" readable-stream "^3.6.0" +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -2735,17 +2180,6 @@ array.prototype.flat@^1.2.5: es-abstract "^1.19.2" es-shim-unscopables "^1.0.0" -array.prototype.reduce@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/array.prototype.reduce/-/array.prototype.reduce-1.0.4.tgz#8167e80089f78bff70a99e20bd4201d4663b0a6f" - integrity sha512-WnM+AjG/DvLRLo4DDl+r+SvCzYtD2Jd9oeBYMcEaI7t3fFrHY9M53/wdLcTvmZNQ70IU6Htj0emFkZ5TS+lrdw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.2" - es-array-method-boxes-properly "^1.0.0" - is-string "^1.0.7" - arrify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" @@ -2815,13 +2249,6 @@ babel-jest@^28.1.3: graceful-fs "^4.2.9" slash "^3.0.0" -babel-plugin-dynamic-import-node@^2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3" - integrity sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ== - dependencies: - object.assign "^4.1.0" - babel-plugin-istanbul@^6.1.1: version "6.1.1" resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" @@ -2843,30 +2270,6 @@ babel-plugin-jest-hoist@^28.1.3: "@types/babel__core" "^7.1.14" "@types/babel__traverse" "^7.0.6" -babel-plugin-polyfill-corejs2@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.2.tgz#e4c31d4c89b56f3cf85b92558954c66b54bd972d" - integrity sha512-LPnodUl3lS0/4wN3Rb+m+UK8s7lj2jcLRrjho4gLw+OJs+I4bvGXshINesY5xx/apM+biTnQ9reDI8yj+0M5+Q== - dependencies: - "@babel/compat-data" "^7.17.7" - "@babel/helper-define-polyfill-provider" "^0.3.2" - semver "^6.1.1" - -babel-plugin-polyfill-corejs3@^0.5.3: - version "0.5.3" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.3.tgz#d7e09c9a899079d71a8b670c6181af56ec19c5c7" - integrity sha512-zKsXDh0XjnrUEW0mxIHLfjBfnXSMr5Q/goMe/fxpQnLm07mcOZiIZHBNWCMx60HmdvjxfXcalac0tfFg0wqxyw== - dependencies: - "@babel/helper-define-polyfill-provider" "^0.3.2" - core-js-compat "^3.21.0" - -babel-plugin-polyfill-regenerator@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.0.tgz#8f51809b6d5883e07e71548d75966ff7635527fe" - integrity sha512-RW1cnryiADFeHmfLS+WW/G431p1PsW5qdRdz0SDRi7TKcUgc7Oh/uXkT7MZ/+tGsT1BkczEAmD5XjUyJ5SWDTw== - dependencies: - "@babel/helper-define-polyfill-provider" "^0.3.2" - babel-preset-current-node-syntax@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b" @@ -2920,7 +2323,7 @@ bin-links@^3.0.0: rimraf "^3.0.0" write-file-atomic "^4.0.0" -binary-extensions@^2.0.0, binary-extensions@^2.2.0: +binary-extensions@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== @@ -2950,14 +2353,14 @@ brace-expansion@^2.0.1: dependencies: balanced-match "^1.0.0" -braces@^3.0.2, braces@~3.0.2: +braces@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== dependencies: fill-range "^7.0.1" -browserslist@^4.20.2, browserslist@^4.21.3: +browserslist@^4.20.2: version "4.21.3" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.3.tgz#5df277694eb3c48bc5c4b05af3e8b7e09c5a6d1a" integrity sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ== @@ -2967,6 +2370,13 @@ browserslist@^4.20.2, browserslist@^4.21.3: node-releases "^2.0.6" update-browserslist-db "^1.0.5" +bs-logger@0.x: + version "0.2.6" + resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" + integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== + dependencies: + fast-json-stable-stringify "2.x" + bser@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" @@ -2988,11 +2398,6 @@ buffer@4.9.2: ieee754 "^1.1.4" isarray "^1.0.0" -builtin-modules@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" - integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== - builtins@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/builtins/-/builtins-5.0.1.tgz#87f6db9ab0458be728564fa81d876d8d74552fa9" @@ -3111,27 +2516,12 @@ charenc@0.0.2: resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" integrity sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA== -chokidar@^3.4.0: - version "3.5.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" - integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - chownr@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== -ci-info@^3.2.0, ci-info@^3.3.0: +ci-info@^3.2.0: version "3.3.2" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.3.2.tgz#6d2967ffa407466481c6c90b6e16b3098f080128" integrity sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg== @@ -3148,13 +2538,6 @@ cjs-module-lexer@^1.0.0: resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40" integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== -clean-regexp@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/clean-regexp/-/clean-regexp-1.0.0.tgz#8df7c7aae51fd36874e8f8d05b9180bc11a3fed7" - integrity sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw== - dependencies: - escape-string-regexp "^1.0.5" - clean-stack@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" @@ -3195,15 +2578,6 @@ cliui@^7.0.2: strip-ansi "^6.0.0" wrap-ansi "^7.0.0" -clone-deep@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" - integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== - dependencies: - is-plain-object "^2.0.4" - kind-of "^6.0.2" - shallow-clone "^3.0.0" - clone@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" @@ -3294,11 +2668,6 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" -commander@^4.0.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" - integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== - comment-parser@1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-1.3.1.tgz#3d7ea3adaf9345594aedee6563f422348f165c1b" @@ -3380,7 +2749,7 @@ conventional-commits-parser@^3.2.3: split2 "^3.0.0" through2 "^4.0.0" -convert-source-map@^1.1.0, convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: +convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: version "1.8.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== @@ -3392,19 +2761,6 @@ cookie@^0.4.1: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== -core-js-compat@^3.21.0, core-js-compat@^3.22.1: - version "3.24.1" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.24.1.tgz#d1af84a17e18dfdd401ee39da9996f9a7ba887de" - integrity sha512-XhdNAGeRnTpp8xbD+sR/HFDK9CbeeeqXT6TuofXh3urqEevzkWmLRgrVoykodsw8okqo2pu1BOmuCKrHx63zdw== - dependencies: - browserslist "^4.21.3" - semver "7.0.0" - -core-js@^3.22.1: - version "3.24.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.24.1.tgz#cf7724d41724154010a6576b7b57d94c5d66e64f" - integrity sha512-0QTBSYSUZ6Gq21utGzkfITDylE8jWC9Ne1D2MrhvlsZBI1x39OdDIVbzSqtgMndIy6BlHxBXpMGqzZmnztg2rg== - core-util-is@~1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" @@ -3421,6 +2777,11 @@ cosmiconfig@^7.0.0: path-type "^4.0.0" yaml "^1.10.0" +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -3593,6 +2954,11 @@ diff-sequences@^28.1.1: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-28.1.1.tgz#9989dc731266dc2903457a70e996f3a041913ac6" integrity sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw== +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + diff@^5.0.0: version "5.1.0" resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40" @@ -3708,7 +3074,7 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19.5, es-abstract@^1.20.0, es-abstract@^1.20.1: +es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19.5, es-abstract@^1.20.0: version "1.20.1" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.1.tgz#027292cd6ef44bd12b1913b828116f54787d1814" integrity sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA== @@ -3737,11 +3103,6 @@ es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19 string.prototype.trimstart "^1.0.5" unbox-primitive "^1.0.2" -es-array-method-boxes-properly@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" - integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== - es-shim-unscopables@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241" @@ -3817,14 +3178,6 @@ eslint-module-utils@^2.7.3: dependencies: debug "^3.2.7" -eslint-plugin-flowtype@^8.0.3: - version "8.0.3" - resolved "https://registry.yarnpkg.com/eslint-plugin-flowtype/-/eslint-plugin-flowtype-8.0.3.tgz#e1557e37118f24734aa3122e7536a038d34a4912" - integrity sha512-dX8l6qUL6O+fYPtpNRideCFSpmWOUVx5QcaGLVqe/vlDiBSe4vYljDWDETwnyFzpl7By/WVIu6rcrniCgH9BqQ== - dependencies: - lodash "^4.17.21" - string-natural-compare "^3.0.1" - eslint-plugin-import@^2.25.2: version "2.26.0" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz#f812dc47be4f2b72b478a021605a59fc6fe8b88b" @@ -3857,31 +3210,6 @@ eslint-plugin-jsdoc@^39.3.2: semver "^7.3.7" spdx-expression-parse "^3.0.1" -eslint-plugin-sonarjs@^0.13.0: - version "0.13.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.13.0.tgz#c34d140cc90abaaed38f5a5201a2ccdebe398862" - integrity sha512-t3m7ta0EspzDxSOZh3cEOJIJVZgN/TlJYaBGnQlK6W/PZNbWep8q4RQskkJkA7/zwNpX0BaoEOSUUrqaADVoqA== - -eslint-plugin-unicorn@^42.0.0: - version "42.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-unicorn/-/eslint-plugin-unicorn-42.0.0.tgz#47d60c00c263ad743403b052db689e39acbacff1" - integrity sha512-ixBsbhgWuxVaNlPTT8AyfJMlhyC5flCJFjyK3oKE8TRrwBnaHvUbuIkCM1lqg8ryYrFStL/T557zfKzX4GKSlg== - dependencies: - "@babel/helper-validator-identifier" "^7.15.7" - ci-info "^3.3.0" - clean-regexp "^1.0.0" - eslint-utils "^3.0.0" - esquery "^1.4.0" - indent-string "^4.0.0" - is-builtin-module "^3.1.0" - lodash "^4.17.21" - pluralize "^8.0.0" - read-pkg-up "^7.0.1" - regexp-tree "^0.1.24" - safe-regex "^2.1.1" - semver "^7.3.5" - strip-indent "^3.0.0" - eslint-scope@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" @@ -3905,7 +3233,7 @@ eslint-utils@^3.0.0: dependencies: eslint-visitor-keys "^2.0.0" -eslint-visitor-keys@^2.0.0, eslint-visitor-keys@^2.1.0: +eslint-visitor-keys@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== @@ -4055,7 +3383,7 @@ fast-glob@^3.2.9: merge2 "^1.3.0" micromatch "^4.0.4" -fast-json-stable-stringify@^2.0.0: +fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== @@ -4122,15 +3450,6 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" -find-cache-dir@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" - integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ== - dependencies: - commondir "^1.0.1" - make-dir "^2.0.0" - pkg-dir "^3.0.0" - find-cache-dir@^3.2.0: version "3.3.2" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b" @@ -4147,13 +3466,6 @@ find-up@^2.0.0: dependencies: locate-path "^2.0.0" -find-up@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" - integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== - dependencies: - locate-path "^3.0.0" - find-up@^4.0.0, find-up@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" @@ -4253,17 +3565,12 @@ fs-minipass@^2.0.0, fs-minipass@^2.1.0: dependencies: minipass "^3.0.0" -fs-readdir-recursive@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz#e32fc030a2ccee44a6b5371308da54be0b397d27" - integrity sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA== - fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@^2.3.2, fsevents@~2.3.2: +fsevents@^2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== @@ -4356,7 +3663,7 @@ git-log-parser@^1.2.0: through2 "~2.0.0" traverse "~0.6.6" -glob-parent@^5.1.2, glob-parent@~5.1.2: +glob-parent@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -4370,7 +3677,7 @@ glob-parent@^6.0.1: dependencies: is-glob "^4.0.3" -glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.0: +glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -4503,13 +3810,6 @@ hasha@^5.0.0: is-stream "^2.0.0" type-fest "^0.8.0" -homedir-polyfill@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" - integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA== - dependencies: - parse-passwd "^1.0.0" - hook-std@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/hook-std/-/hook-std-2.0.0.tgz#ff9aafdebb6a989a354f729bb6445cf4a3a7077c" @@ -4726,13 +4026,6 @@ is-bigint@^1.0.1: dependencies: has-bigints "^1.0.1" -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - is-boolean-object@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" @@ -4746,13 +4039,6 @@ is-buffer@~1.1.6: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== -is-builtin-module@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-3.2.0.tgz#bb0310dfe881f144ca83f30100ceb10cf58835e0" - integrity sha512-phDA4oSGt7vl1n5tJvTWooWWAsXLY+2xCnxNqvKhGEzujg+A43wPlPOyDg3C8XQHN+6k/JTQWJ/j0dQh/qr+Hw== - dependencies: - builtin-modules "^3.3.0" - is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" @@ -4801,7 +4087,7 @@ is-generator-function@^1.0.7: dependencies: has-tostringtag "^1.0.0" -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== @@ -4850,13 +4136,6 @@ is-plain-obj@^1.1.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" integrity sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg== -is-plain-object@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" - integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== - dependencies: - isobject "^3.0.1" - is-plain-object@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" @@ -4941,11 +4220,6 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== -isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== - issue-parser@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/issue-parser/-/issue-parser-6.0.0.tgz#b1edd06315d4f2044a9755daf85fdafde9b4014a" @@ -5334,7 +4608,7 @@ jest-snapshot@^28.1.3: pretty-format "^28.1.3" semver "^7.3.5" -jest-util@^28.1.3: +jest-util@^28.0.0, jest-util@^28.1.3: version "28.1.3" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-28.1.3.tgz#f4f932aa0074f0679943220ff9cbba7e497028b0" integrity sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ== @@ -5426,11 +4700,6 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== -jsesc@~0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" - integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== - json-parse-better-errors@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" @@ -5497,7 +4766,7 @@ just-diff@^5.0.1: resolved "https://registry.yarnpkg.com/just-diff/-/just-diff-5.1.1.tgz#8da6414342a5ed6d02ccd64f5586cbbed3146202" integrity sha512-u8HXJ3HlNrTzY7zrYYKjNEfBlyjqhdBkoyTVdjtn7p02RJD5NvR8rIClzeGA7t+UYP1/7eAkWNLU0+P3QrEqKQ== -kind-of@^6.0.2, kind-of@^6.0.3: +kind-of@^6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== @@ -5661,14 +4930,6 @@ locate-path@^2.0.0: p-locate "^2.0.0" path-exists "^3.0.0" -locate-path@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" - integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== - dependencies: - p-locate "^3.0.0" - path-exists "^3.0.0" - locate-path@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" @@ -5688,11 +4949,6 @@ lodash.capitalize@^4.2.1: resolved "https://registry.yarnpkg.com/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz#f826c9b4e2a8511d84e3aca29db05e1a4f3b72a9" integrity sha512-kZzYOKspf8XVX5AvmQF94gQW0lejFVgb80G85bU4ZWzoJ6C03PQg3coYAUpSTpQWelrZELd3XWgHzw4Ck5kaIw== -lodash.debounce@^4.0.8: - version "4.0.8" - resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" - integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== - lodash.escaperegexp@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347" @@ -5718,6 +4974,11 @@ lodash.isstring@^4.0.1: resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== +lodash.memoize@4.x: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" @@ -5769,14 +5030,6 @@ lru_map@^0.3.3: resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd" integrity sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ== -make-dir@^2.0.0, make-dir@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" - integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== - dependencies: - pify "^4.0.1" - semver "^5.6.0" - make-dir@^3.0.0, make-dir@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" @@ -5784,6 +5037,11 @@ make-dir@^3.0.0, make-dir@^3.0.2: dependencies: semver "^6.0.0" +make-error@1.x, make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + make-fetch-happen@^10.0.3, make-fetch-happen@^10.0.6, make-fetch-happen@^10.2.0: version "10.2.0" resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-10.2.0.tgz#0bde3914f2f82750b5d48c6d2294d2c74f985e5b" @@ -6072,14 +5330,6 @@ node-emoji@^1.11.0: dependencies: lodash "^4.17.21" -node-environment-flags@^1.0.5: - version "1.0.6" - resolved "https://registry.yarnpkg.com/node-environment-flags/-/node-environment-flags-1.0.6.tgz#a30ac13621f6f7d674260a54dede048c3982c088" - integrity sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw== - dependencies: - object.getownpropertydescriptors "^2.0.3" - semver "^5.7.0" - node-fetch@^2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" @@ -6164,7 +5414,7 @@ normalize-package-data@^4.0.0: semver "^7.3.5" validate-npm-package-license "^3.0.4" -normalize-path@^3.0.0, normalize-path@~3.0.0: +normalize-path@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== @@ -6393,7 +5643,7 @@ object-keys@^1.1.1: resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== -object.assign@^4.1.0, object.assign@^4.1.2: +object.assign@^4.1.2: version "4.1.3" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.3.tgz#d36b7700ddf0019abb6b1df1bb13f6445f79051f" integrity sha512-ZFJnX3zltyjcYJL0RoCJuzb+11zWGyaDbjgxZbdV7rFEcHQuYxrZqhow67aA7xpes6LhojyFDaBKAFfogQrikA== @@ -6412,16 +5662,6 @@ object.entries@^1.1.5: define-properties "^1.1.3" es-abstract "^1.19.1" -object.getownpropertydescriptors@^2.0.3: - version "2.1.4" - resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.4.tgz#7965e6437a57278b587383831a9b829455a4bc37" - integrity sha512-sccv3L/pMModT6dJAYF3fzGMVcb38ysQ0tEE6ixv2yXJDtEIPph268OlAdJj5/qZMZDq2g/jqvwppt36uS/uQQ== - dependencies: - array.prototype.reduce "^1.0.4" - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.1" - object.values@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.5.tgz#959f63e3ce9ef108720333082131e4a459b716ac" @@ -6498,7 +5738,7 @@ p-limit@^1.1.0: dependencies: p-try "^1.0.0" -p-limit@^2.0.0, p-limit@^2.2.0: +p-limit@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== @@ -6519,13 +5759,6 @@ p-locate@^2.0.0: dependencies: p-limit "^1.1.0" -p-locate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" - integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== - dependencies: - p-limit "^2.0.0" - p-locate@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" @@ -6653,11 +5886,6 @@ parse-json@^5.0.0, parse-json@^5.2.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" -parse-passwd@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" - integrity sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q== - path-exists@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" @@ -6693,7 +5921,7 @@ picocolors@^1.0.0: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: +picomatch@^2.0.4, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== @@ -6703,12 +5931,7 @@ pify@^3.0.0: resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" integrity sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg== -pify@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" - integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== - -pirates@^4.0.4, pirates@^4.0.5: +pirates@^4.0.4: version "4.0.5" resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== @@ -6721,13 +5944,6 @@ pkg-conf@^2.1.0: find-up "^2.0.0" load-json-file "^4.0.0" -pkg-dir@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" - integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw== - dependencies: - find-up "^3.0.0" - pkg-dir@^4.1.0, pkg-dir@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" @@ -6735,11 +5951,6 @@ pkg-dir@^4.1.0, pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" -pluralize@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" - integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== - postcss-selector-parser@^6.0.10: version "6.0.10" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d" @@ -6954,13 +6165,6 @@ readdir-scoped-modules@^1.1.0: graceful-fs "^4.1.2" once "^1.3.0" -readdirp@~3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" - integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== - dependencies: - picomatch "^2.2.1" - redent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" @@ -6976,35 +6180,6 @@ redeyed@~2.1.0: dependencies: esprima "~4.0.0" -regenerate-unicode-properties@^10.0.1: - version "10.0.1" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.0.1.tgz#7f442732aa7934a3740c779bb9b3340dccc1fb56" - integrity sha512-vn5DU6yg6h8hP/2OkQo3K7uVILvY4iu0oI4t3HFa81UPkhGJwkRwM10JEc3upjdhHjs/k8GJY1sRBhk5sr69Bw== - dependencies: - regenerate "^1.4.2" - -regenerate@^1.4.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" - integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== - -regenerator-runtime@^0.13.4: - version "0.13.9" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" - integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== - -regenerator-transform@^0.15.0: - version "0.15.0" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.0.tgz#cbd9ead5d77fae1a48d957cf889ad0586adb6537" - integrity sha512-LsrGtPmbYg19bcPHwdtmXwbW+TqNvtY4riE3P83foeHRroMbH6/2ddFBfab3t7kbzc7v7p4wbkIecHImqt0QNg== - dependencies: - "@babel/runtime" "^7.8.4" - -regexp-tree@^0.1.24, regexp-tree@~0.1.1: - version "0.1.24" - resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.24.tgz#3d6fa238450a4d66e5bc9c4c14bb720e2196829d" - integrity sha512-s2aEVuLhvnVJW6s/iPgEGK6R+/xngd2jNQ+xy4bXNDKxZKJH6jpPHY6kVeVv1IeLCHgswRj+Kl3ELaDjG6V1iw== - regexp.prototype.flags@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" @@ -7019,18 +6194,6 @@ regexpp@^3.2.0: resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== -regexpu-core@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.1.0.tgz#2f8504c3fd0ebe11215783a41541e21c79942c6d" - integrity sha512-bb6hk+xWd2PEOkj5It46A16zFMs2mv86Iwpdu94la4S3sJ7C973h2dHpYKwIBGaWSO7cIRJ+UX0IeMaWcO4qwA== - dependencies: - regenerate "^1.4.2" - regenerate-unicode-properties "^10.0.1" - regjsgen "^0.6.0" - regjsparser "^0.8.2" - unicode-match-property-ecmascript "^2.0.0" - unicode-match-property-value-ecmascript "^2.0.0" - registry-auth-token@^4.0.0: version "4.2.2" resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.2.2.tgz#f02d49c3668884612ca031419491a13539e21fac" @@ -7038,18 +6201,6 @@ registry-auth-token@^4.0.0: dependencies: rc "1.2.8" -regjsgen@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.6.0.tgz#83414c5354afd7d6627b16af5f10f41c4e71808d" - integrity sha512-ozE883Uigtqj3bx7OhL1KNbCzGyW2NQZPl6Hs09WTvCuZD5sTI4JY58bkbQWa/Y9hxIsvJ3M8Nbf7j54IqeZbA== - -regjsparser@^0.8.2: - version "0.8.4" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.8.4.tgz#8a14285ffcc5de78c5b95d62bbf413b6bc132d5f" - integrity sha512-J3LABycON/VNEu3abOviqGHuB/LOtOQj8SKmfP9anY5GfAVw/SPjwzSjxGjbZXIxbGfqTHtJw58C2Li/WkStmA== - dependencies: - jsesc "~0.5.0" - release-zalgo@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/release-zalgo/-/release-zalgo-1.0.0.tgz#09700b7e5074329739330e535c5a90fb67851730" @@ -7098,7 +6249,7 @@ resolve.exports@^1.1.0: resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.0.tgz#5ce842b94b05146c0e03076985d1d0e7e48c90c9" integrity sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ== -resolve@^1.10.0, resolve@^1.14.2, resolve@^1.20.0, resolve@^1.22.0, resolve@^1.22.1: +resolve@^1.10.0, resolve@^1.20.0, resolve@^1.22.0, resolve@^1.22.1: version "1.22.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== @@ -7146,13 +6297,6 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-regex@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-2.1.1.tgz#f7128f00d056e2fe5c11e81a1324dd974aadced2" - integrity sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A== - dependencies: - regexp-tree "~0.1.1" - safe-stable-stringify@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz#ab67cbe1fe7d40603ca641c5e765cb942d04fc73" @@ -7219,40 +6363,28 @@ semver-regex@^3.1.2: resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-3.1.4.tgz#13053c0d4aa11d070a2f2872b6b1e3ae1e1971b4" integrity sha512-6IiqeZNgq01qGf0TId0t3NvKzSvUsjcpdEO3AQNeIjR6A2+ckTnQlDpl4qu1bjRv0RzN3FP9hzFmws3lKqRWkA== -"semver@2 || 3 || 4 || 5", semver@^5.6.0, semver@^5.7.0: +"semver@2 || 3 || 4 || 5": version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -semver@7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" - integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== - -semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - -semver@^7.0.0, semver@^7.1.1, semver@^7.1.2, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7: +semver@7.x, semver@^7.0.0, semver@^7.1.1, semver@^7.1.2, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7: version "7.3.7" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== dependencies: lru-cache "^6.0.0" +semver@^6.0.0, semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== -shallow-clone@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" - integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== - dependencies: - kind-of "^6.0.2" - shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -7305,11 +6437,6 @@ sisteransi@^1.0.5: resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== -slash@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" - integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== - slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" @@ -7354,14 +6481,6 @@ source-map-support@0.5.13: buffer-from "^1.0.0" source-map "^0.6.0" -source-map-support@^0.5.16: - version "0.5.21" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" - integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - source-map@^0.6.0, source-map@^0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" @@ -7471,11 +6590,6 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -string-natural-compare@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" - integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== - "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -7724,6 +6838,39 @@ triple-beam@^1.3.0: resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9" integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw== +ts-jest@^28.0.8: + version "28.0.8" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-28.0.8.tgz#cd204b8e7a2f78da32cf6c95c9a6165c5b99cc73" + integrity sha512-5FaG0lXmRPzApix8oFG8RKjAz4ehtm8yMKOTy5HX3fY6W8kmvOrmcY0hKDElW52FJov+clhUbrKAqofnj4mXTg== + dependencies: + bs-logger "0.x" + fast-json-stable-stringify "2.x" + jest-util "^28.0.0" + json5 "^2.2.1" + lodash.memoize "4.x" + make-error "1.x" + semver "7.x" + yargs-parser "^21.0.1" + +ts-node@^10.9.1: + version "10.9.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" + integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + tsconfig-paths@^3.14.1: version "3.14.1" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a" @@ -7734,7 +6881,16 @@ tsconfig-paths@^3.14.1: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^1.11.1, tslib@^1.9.3: +tsconfig-paths@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.1.0.tgz#f8ef7d467f08ae3a695335bf1ece088c5538d2c1" + integrity sha512-AHx4Euop/dXFC+Vx589alFba8QItjF+8hf8LtmuiCwHyI4rHXQtOOENaM8kvYf5fR0dRChy3wzWIZ9WbB7FWow== + dependencies: + json5 "^2.2.1" + minimist "^1.2.6" + strip-bom "^3.0.0" + +tslib@^1.11.1, tslib@^1.8.1, tslib@^1.9.3: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== @@ -7744,6 +6900,13 @@ tslib@^2.3.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== +tsutils@^3.21.0: + version "3.21.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" + integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== + dependencies: + tslib "^1.8.1" + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -7798,6 +6961,11 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" +typescript@^4.7.4: + version "4.7.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" + integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== + uglify-js@^3.1.4: version "3.16.3" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.16.3.tgz#94c7a63337ee31227a18d03b8a3041c210fd1f1d" @@ -7813,29 +6981,6 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" -unicode-canonical-property-names-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" - integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== - -unicode-match-property-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" - integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== - dependencies: - unicode-canonical-property-names-ecmascript "^2.0.0" - unicode-property-aliases-ecmascript "^2.0.0" - -unicode-match-property-value-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.0.0.tgz#1a01aa57247c14c568b89775a54938788189a714" - integrity sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw== - -unicode-property-aliases-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz#0a36cb9a585c4f6abd51ad1deddb285c165297c8" - integrity sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ== - unique-filename@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" @@ -7940,6 +7085,11 @@ uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + v8-compile-cache@^2.0.3: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" @@ -7954,13 +7104,6 @@ v8-to-istanbul@^9.0.1: "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^1.6.0" -v8flags@^3.1.1: - version "3.2.0" - resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-3.2.0.tgz#b243e3b4dfd731fa774e7492128109a0fe66d656" - integrity sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg== - dependencies: - homedir-polyfill "^1.0.1" - validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" @@ -8200,7 +7343,7 @@ yargs-parser@^20.2.2, yargs-parser@^20.2.3: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== -yargs-parser@^21.0.0: +yargs-parser@^21.0.0, yargs-parser@^21.0.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== @@ -8248,6 +7391,11 @@ yargs@^17.3.1: y18n "^5.0.5" yargs-parser "^21.0.0" +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"