diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..07764a7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text eol=lf \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..b90a48f --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,110 @@ +name: Functionly build and publish + +on: + workflow_dispatch: + +jobs: + dev: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v3 + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '8' + - name: Install dependencies + run: npm i --no-save -q + - name: Setup .npmrc for npmjs + run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Auto Increment Version, and Publish to npmjs + run: | + git config --global user.email "jaystack-ci@jaystack.com" + git config --global user.name "jaystack-ci" + git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }} + npm version patch -m "Auto-increment version: %s" --force + npm publish --tag dev + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Git operations + run: | + git tag -f dev + git push -f --tags + git push + stage: + needs: dev + runs-on: ubuntu-latest + environment: stage + permissions: + contents: write + steps: + - uses: actions/checkout@v3 + with: + ref: 'refs/tags/dev' + + - name: Configure npm + run: echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" > .npmrc + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Extract package version + id: package_version + run: echo "::set-output name=VERSION::$(jq -r .version package.json)" + shell: bash + + - name: Configure Git + run: | + git config --global user.email "jaystack-ci@jaystack.com" + git config --global user.name "jaystack-ci" + git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }} + + - name: Create and push git tag + run: | + git tag -f stage + git push -f --tags + + - name: Publish to npm + run: npm dist-tag add functionly@${{ steps.package_version.outputs.VERSION }} stage + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + live: + needs: stage + runs-on: ubuntu-latest + environment: live + permissions: + contents: write + steps: + - uses: actions/checkout@v3 + with: + ref: 'refs/tags/stage' + + - name: Configure npm + run: echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" > .npmrc + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Extract package version + id: package_version + run: echo "::set-output name=VERSION::$(jq -r .version package.json)" + shell: bash + + - name: Configure Git + run: | + git config --global user.email "jaystack-ci@jaystack.com" + git config --global user.name "jaystack-ci" + git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }} + + - name: Create and push git tag + run: | + git tag -f latest + git push -f --tags + + - name: Publish to npm + run: npm dist-tag add functionly@${{ steps.package_version.outputs.VERSION }} latest + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + diff --git a/.gitignore b/.gitignore index e69be72..5b293df 100644 --- a/.gitignore +++ b/.gitignore @@ -45,4 +45,7 @@ __test/**/* typings -.vscode \ No newline at end of file +.vscode + +test-results.xml +version.txt diff --git a/.npmignore b/.npmignore index 1aa3797..cbb8866 100644 --- a/.npmignore +++ b/.npmignore @@ -41,6 +41,7 @@ test /src main.js main.js.map +tsconfig.json __test/**/* diff --git a/.npmrc b/.npmrc index bc9dcc1..d511702 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,2 @@ -@types:registry=https://registry.npmjs.org \ No newline at end of file +@types:registry=https://registry.npmjs.org +//SECRET diff --git a/README.md b/README.md index cd77534..9158bd6 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,367 @@ # functionly -The `functionly library` lets you build `serverless` nodejs applications on an innovative, functional, and fun by abstraction way. -Use the JavaScript language and the JSON syntax to describe infrastructure and entities, service dependencies, and to implement service code. Deploy your solution to cloud providers, or run containerized on your onprem servers, or locally during development time using the `functionly CLI`. +The `functionly library` lets you build `serverless` nodejs applications in an innovative, functional, and fun by abstraction way. +Use the JavaScript language and the JSON syntax to describe infrastructure and entities, service dependencies, and to implement service code. Deploy your solution to cloud providers, or run containerized on your onprem servers, or locally during development time using the `functionly CLI`. -### install from npm: +Defining a rest service which listens on `/hello-world`: +```js +import { FunctionalService, rest, description, param } from 'functionly' -#### functionly CLI -- npm install functionly -g +@rest({ path: '/hello-world' }) +@description('hello world service') +export class HelloWorld extends FunctionalService { + static async handle(@param name = 'world') { + return `hello ${name}` + } +} -#### functionly library -- npm install functionly +export const helloworld = HelloWorld.createInvoker() +``` +Running on localhost: +```sh +functionly start +``` +Try it on http://localhost:3000/hello-world?name=Joe +--- -## Examples +- [Install from npm](#install-from-npm) +- [Getting started](#getting-started) +- [Examples](#examples) -#### Serverless Hello World +# Install from npm: -#### Dependency injection: Service To Service references +## functionly CLI +```sh +npm install functionly -g +``` -#### Dependency injection: Database reference +## functionly library +```sh +npm install functionly +``` -## examples +# Getting started + +* [Create an empty Functionly project](#create-an-empty-functionly-project) +* [Create a Hello world service](#create-a-hello-world-service) + * [Resolve parameter values](#resolve-parameter-values) +* [Create a Todo application with DynamoDB](#create-a-tododb-application-with-dynamodb) + * [Create a dynamo table](#create-a-dynamo-table) + * [Read todos](#read-todos) + * [Create todo](#create-todo) + * *[Extend with Services](#extend-the-example-with-Services) - optional* +* [Run and Deploy with CLI](#run-and-deploy-with-cli) +* [AWS deployment](#aws-deployment) +* [Examples](#examples) + +## Create an empty Functionly project +It's a simple npm project. +### Dependencies +- functionly +```sh +npm install --save functionly +``` + +### Dev dependencies +Functionly uses webpack with babel for compile the code. +- babel-core +- babel-loader +- babel-preset-functionly-aws +```sh +npm install --save-dev babel-core babel-loader babel-preset-functionly-aws +``` + +### Babel configuration +Default `.babelrc` + +```js +{ + "presets": [ "functionly-aws" ] +} +``` + +### Functionly configuration +Default `functionly.json` +```js +{ + "awsRegion": "us-east-1", + "main": "./src/index.js", + "deployTarget": "aws", + "localPort": 3000, + "stage": "dev", + "watch": true, + "compile": "babel-loader" +} +``` + +## Create a Hello world service +We need to create a `FunctionalService` to implement the business logic of hello world application +```js +import { FunctionalService } from 'functionly' + +export class HelloWorld extends FunctionalService { + static async handle() {} +} +``` +If you want your service to be accessible with a web request over a rest interface then you have to decorate it with the [rest]() decorator. We have to set the `path` property to define the rest endpoint. +If we do not set the `methods` property that means it will accept `GET` requests. (default: `methods: ['get']`) +```js +@rest({ path: '/hello-world' }) +``` +Define a [description]() for the `HelloWorld`, which will make it easier to find in the AWS Lambda list. +```js +@description('hello world service') +``` +Now we have to create the business logic. +```js +import { FunctionalService, rest, description } from 'functionly' + +@rest({ path: '/hello-world' }) +@description('hello world service') +export class HelloWorld extends FunctionalService { + static async handle() { + return `hello world` + } +} +``` +We are almost done, we just have to export our service from the main file. +```js +export const helloworld = HelloWorld.createInvoker() +``` +### Resolve parameter values +In the `handle` method if you use the `@param` property decorator for a parameter then it resolves the value from a request context. +```js +import { FunctionalService, rest, description, param } from 'functionly' + +@rest({ path: '/hello-world' }) +@description('hello world service') +export class HelloWorld extends FunctionalService { + static async handle(@param name = 'world') { + return `hello ${name}` + } +} + +export const helloworld = HelloWorld.createInvoker() +``` + +## Create a TodoDB application with DynamoDB +Define a base class for FunctionalService to set basic Lambda settings in the AWS environment. +```js +import { FunctionalService, aws } from 'functionly' + +@aws({ type: 'nodejs20.x', memorySize: 512, timeout: 3 }) +export class TodoService extends FunctionalService { } +``` + +### Create a dynamo table +We need a DynamoTable, called `TodoTable` because we want to store todo items. +```js +import { DynamoTable, dynamoTable, injectable } from 'functionly' + +@injectable() +@dynamo() +export class TodoTable extends DynamoTable { } +``` + +### Read todos +We need to create a service to read todo items. +```js +export class GetAllTodos extends TodoService { + static async handle() {} +} +``` +If you want your service to be accessible with a web request over a rest interface then you have to decorate it with the [rest]() decorator. We have to set the `path` property to define the rest endpoint. +If we do not set the `methods` property that means it will accept `GET` requests. (default: `methods: ['get']`) +```js +@rest({ path: '/getAllTodos' }) +``` +Define a [description]() for the `TodoService`, which will make it easier to find in the AWS Lambda list. +```js +@description('get all Todo service') +``` +Now we have to create the business logic. We want to read the todo items, so we need to inject the `TodoTable`. Get the items from it and return from our service. +```js +import { rest, description, inject } from 'functionly' + +@rest({ path: '/getAllTodos' }) +@description('get all Todo service') +export class GetAllTodos extends TodoService { + static async handle(@inject(TodoTable) db) { + let items = await db.scan() + return { ok: 1, items } + } +} +``` +We are almost done, we just have to export our service from the main file. +```js +export const getAllTodos = GetAllTodos.createInvoker() +``` + +### Create todo +We need a service to create todo items, so let's do this. We will also define a [rest]() endpoint and a [description](). +```js +import { rest, description } from 'functionly' + +@rest({ path: '/createTodo', methods: ['post'] }) +@description('create Todo service') +export class CreateTodo extends TodoService { + static async handle() {} +} +``` +We need some values to create a new todo item: `name`, `description` and `status`. Expect these with the [param]() decorator, and it will resolve them from the invocation context. +```js +import { rest, description, param } from 'functionly' + +@rest({ path: '/createTodo', methods: ['post'] }) +@description('create Todo service') +export class CreateTodo extends TodoService { + static async handle(@param name, @param description, @param staus) {} +} +``` +The business logic: save a new todo item. [Inject]() the `TodoTable` and save a new todo item with the `put` function. We need an id for the new todo, in the example, we'll use [shortid](https://www.npmjs.com/package/shortid) to generate them. +```js +import { generate } from 'shortid' +import { rest, description, param } from 'functionly' + +@rest({ path: '/createTodo', methods: ['post'] }) +@description('create Todo service') +export class CreateTodo extends TodoService { + static async handle(@param name, @param description, @param status, @inject(TodoTable) db) { + let item = { + id: generate(), + name, + description, + status + } + + await db.put({ Item: item }) + + return { ok: 1, item } + } +} + +export const createTodo = CreateTodo.createInvoker() +``` + +## Extend the example with Services +> **Optional** + +Create two services: validate and persist todo items. Then the CreateTodo has only to call these services. + +### Validate todo +It will be an [injectable]() service and expect the three todo values, then implement a validation logic in the service. +```js +import { injectable, param } from 'functionly' + +@injectable() +export class ValidateTodo extends Service { + static async handle( @param name, @param description, @param status) { + const isValid = true + return { isValid } + } +} +``` + +### Persist todo +It will be an [injectable]() service and expect the three todo values and [inject]() a `TodoTable` then implement a persist logic in the service. +```js +import { injectable, param, inject } from 'functionly' + +@injectable() +export class PersistTodo extends Service { + static async handle( @param name, @param description, @param status, @inject(TodoTable) db) { + let item = { + id: generate(), + name, + description, + status + } + await db.put({ Item: item }) + return item + } +} +``` + +### Changed CreateTodo FunctionalService +[inject]() the two new services(`ValidateTodo`, `PersistTodo`) and change the business logic +```js +import { rest, description, param, inject } from 'functionly' + +@rest({ path: '/createTodo', methods: ['post'] }) +@description('create Todo service') +export class CreateTodo extends TodoService { + static async handle( + @param name, + @param description, + @param status, + @inject(ValidateTodo) validateTodo, + @inject(PersistTodo) persistTodo + ) { + let validateResult = await validateTodo({ name, description, status }) + if (!validateResult.isValid) { + throw new Error('Todo validation error') + } + let persistTodoResult = await persistTodo({ name, description, status }) + return { ok: 1, persistTodoResult } + } +} +``` + +### The source code of this example is available [here](https://github.com/jaystack/functionly-examples/tree/master/todoDB-es6) + +# Install +```sh +npm install +``` + +# Run and Deploy with CLI +The CLI helps you to deploy and run the application. +1. CLI install +```sh +npm install functionly -g +``` + +## Local deployment +1. Create DynamoDB with Docker +```sh +docker run -d --name dynamodb -p 8000:8000 peopleperhour/dynamodb +``` +2. Deploy will create the tables in DynamoDB +> Note: Create the [functionly.json](#functionly-configuration) in the project for short commands. Also, you don't have to pass all arguments. +```sh +functionly deploy local +``` +## Run in local environment +During development, you can run the application on your local machine. +```sh +functionly start +``` + +## AWS deployment +> Disclaimer: As functionly provisions AWS services, charges may apply to your AWS account. We suggest you to visit https://aws.amazon.com/pricing/services/ to revise the possible AWS costs. + +> [Set up](http://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/setup-credentials.html) AWS Credentials before deployment. + +> Note: Create the [functionly.json](#functionly-configuration) in the project for short commands. Also, you don't have to pass all arguments. As the `deployTarget` is configured as `aws` (the default value configured) then the deploy command will use this as deployment target. + +Functionly will create the package and deploy the application to AWS. The package is a [CloudFormation](https://aws.amazon.com/cloudformation/) template, it contains all the AWS resources so AWS can create or update the application's resources based on the template. +```sh +functionly deploy +``` + +> Congratulations! You have just created and deployed your first `functionly` application! + +# Examples - https://github.com/jaystack/functionly-examples + +## Javascript +- [greeter](https://github.com/jaystack/functionly-examples/tree/master/greeter) +- [todoDB-es6](https://github.com/jaystack/functionly-examples/tree/master/todoDB-es6) + +## Typescript +- [todoDB](https://github.com/jaystack/functionly-examples/tree/master/todoDB) +- [todoDB-mongo](https://github.com/jaystack/functionly-examples/tree/master/todoDB-mongo) +- [todoDBAdvanced](https://github.com/jaystack/functionly-examples/tree/master/todoDBAdvanced) +- [eventSource](https://github.com/jaystack/functionly-examples/tree/master/eventSource) diff --git a/cli.js b/cli.js index ed89af5..acc6dd3 100644 --- a/cli.js +++ b/cli.js @@ -1 +1 @@ -module.exports = require('./lib/src/cli') \ No newline at end of file +module.exports = require('./lib/src/cli/cli') \ No newline at end of file diff --git a/config/default.js b/config/default.js index dcb355e..6422d16 100644 --- a/config/default.js +++ b/config/default.js @@ -6,6 +6,7 @@ module.exports = { Lambda: { apiVersion: '2015-03-31', signatureVersion: 'v4' }, DynamoDB: { apiVersion: '2012-08-10', signatureVersion: 'v4' }, CloudFormation: { apiVersion: '2010-05-15', signatureVersion: 'v4' }, + SNS: { apiVersion: '2010-03-31', signatureVersion: 'v4' }, }, webpack: { output: { @@ -15,6 +16,7 @@ module.exports = { }, target: 'node' }, + tempDirectory: path.join(process.cwd(), 'dist'), S3: { ACL: 'public-read' } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a9a6296 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,9130 @@ +{ + "name": "functionly", + "version": "0.6.3", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "functionly", + "version": "0.4.0", + "license": "ISC", + "dependencies": { + "async": "^2.1.2", + "aws-sdk": "^2.49.0", + "body-parser": "^1.17.1", + "commander": "^2.9.0", + "config": "^1.26.1", + "cors": "^2.8.3", + "decache": "^4.3.0", + "express": "^4.15.2", + "fs-extra": "^3.0.1", + "lodash": "^4.14.1", + "mongodb": "^2.2.31", + "node-zip": "^1.1.1", + "reflect-metadata": "^0.1.10", + "request": "^2.81.0", + "webpack": "^2.5.1", + "winston": "^2.3.0", + "yamljs": "^0.2.10" + }, + "bin": { + "functionly": "lib/src/cli/cli.js" + }, + "devDependencies": { + "@types/async": "^2.0.33", + "@types/chai": "^4.0.1", + "@types/lodash": "^4.14.38", + "@types/mocha": "^2.2.41", + "@types/mongodb": "^2.2.11", + "@types/node": "^6.0.46", + "@types/winston": "0.0.30", + "chai": "^4.0.2", + "istanbul": "^0.4.5", + "mocha": "^3.4.2", + "mocha-junit-reporter": "^1.15.0", + "typescript": "^2.3.0" + } + }, + "node_modules/@types/async": { + "version": "2.0.42", + "resolved": "https://registry.npmjs.org/@types/async/-/async-2.0.42.tgz", + "integrity": "sha512-rmsnoIPcAHn9k0HtBktG0vFQIJlQQvLofo70pWly8itQzqG5c/ILSZqmXLsCdJpT1X5wpxO0F3rb3GB5xCiDAA==", + "dev": true + }, + "node_modules/@types/bson": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/bson/-/bson-1.0.4.tgz", + "integrity": "sha512-/nysVvxwup1WniGHIM31UZXM+6727h4FAa2tZpFSQBooBcl2Bh1N9oQmVVg8QYnjchN/DOGi7UvVN0jpzWL6sw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/chai": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.0.4.tgz", + "integrity": "sha512-cvU0HomQ7/aGDQJZsbtJXqBQ7w4J4TqLB0Z/h8mKrpRjfeZEvTbygkfJEb7fWdmwpIeDeFmIVwAEqS0OYuUv3Q==", + "dev": true + }, + "node_modules/@types/lodash": { + "version": "4.14.74", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.74.tgz", + "integrity": "sha512-BZknw3E/z3JmCLqQVANcR17okqVTPZdlxvcIz0fJiJVLUCbSH1hK3zs9r634PVSmrzAxN+n/fxlVRiYoArdOIQ==", + "dev": true + }, + "node_modules/@types/mocha": { + "version": "2.2.43", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-2.2.43.tgz", + "integrity": "sha512-xNlAmH+lRJdUMXClMTI9Y0pRqIojdxfm7DHsIxoB2iTzu3fnPmSMEN8SsSx0cdwV36d02PWCWaDUoZPDSln+xw==", + "dev": true + }, + "node_modules/@types/mongodb": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-2.2.11.tgz", + "integrity": "sha512-kksYkn34+ENpZqUvGYq0IGOOa4SOhFnMfqCepF789CP+QBgeWok9osTCiAStbc8n4Cv3QSXGmpB5T3PGzL50bQ==", + "dev": true, + "dependencies": { + "@types/bson": "*", + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "6.0.88", + "resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.88.tgz", + "integrity": "sha512-bYDPZTX0/s1aihdjLuAgogUAT5M+TpoWChEMea2p0yOcfn5bu3k6cJb9cp6nw268XeSNIGGr+4+/8V5K6BGzLQ==", + "dev": true + }, + "node_modules/@types/winston": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/winston/-/winston-0.0.30.tgz", + "integrity": "sha1-eyo5hwv/iWPbWHY/HlNnV6mssTQ=", + "deprecated": "This is a stub types definition. winston provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/abbrev": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", + "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", + "dev": true + }, + "node_modules/accepts": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz", + "integrity": "sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=", + "dependencies": { + "mime-types": "~2.1.16", + "negotiator": "0.6.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.1.2.tgz", + "integrity": "sha512-o96FZLJBPY1lvTuJylGA9Bk3t/GKPPJG8H0ydQQl01crzwJgspa4AEIq/pVTXigmK0PHVQhiAtn8WMBLL9D2WA==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-dynamic-import": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz", + "integrity": "sha1-x1K9IQvvZ5UBtsbLf8hPj0cVjMQ=", + "deprecated": "This is probably built in to whatever tool you're using. If you still need it... idk", + "dependencies": { + "acorn": "^4.0.3" + } + }, + "node_modules/acorn-dynamic-import/node_modules/acorn": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "dependencies": { + "co": "^4.6.0", + "json-stable-stringify": "^1.0.1" + } + }, + "node_modules/ajv-keywords": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz", + "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=", + "peerDependencies": { + "ajv": ">=4.10.0" + } + }, + "node_modules/align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "dependencies": { + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true, + "engines": { + "node": ">=0.4.2" + } + }, + "node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/anymatch": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", + "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "dependencies": { + "micromatch": "^2.1.5", + "normalize-path": "^2.0.0" + } + }, + "node_modules/argparse": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", + "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dependencies": { + "arr-flatten": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "node_modules/array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + }, + "node_modules/asn1.js": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.9.1.tgz", + "integrity": "sha1-SLokC0WpKA6UdImQull9IWYX/UA=", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/assert": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", + "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", + "dependencies": { + "util": "0.10.3" + } + }, + "node_modules/assert-plus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/assertion-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.2.tgz", + "integrity": "sha1-E8pRXYYgbaC6xm6DTdOX2HWBCUw=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/async": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/async/-/async-2.5.0.tgz", + "integrity": "sha512-e+lJAJeNWuPCNyxZKOBdaJGyLGHugXVQtrAwtuAe2vhxTYxFTKE73p8JuTmdH0qdQZtDvI4dhJwjZc5zsfIsYw==", + "dependencies": { + "lodash": "^4.14.0" + } + }, + "node_modules/async-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", + "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "node_modules/aws-sdk": { + "version": "2.112.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.112.0.tgz", + "integrity": "sha1-mgSMDdWnQHoh9uhiNx0gSanPs1A=", + "dependencies": { + "buffer": "4.9.1", + "crypto-browserify": "1.0.9", + "events": "^1.1.1", + "jmespath": "0.15.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "uuid": "3.0.1", + "xml2js": "0.4.17", + "xmlbuilder": "4.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" + }, + "node_modules/balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "node_modules/base64-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.1.tgz", + "integrity": "sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw==" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "optional": true, + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/big.js": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.1.3.tgz", + "integrity": "sha1-TK2iGTZS6zyp7I5VyQFWacmAaXg=", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.10.0.tgz", + "integrity": "sha1-muuabF6IY4qtFx4Wf1kAq+JINdA=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/bn.js": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" + }, + "node_modules/body-parser": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.0.tgz", + "integrity": "sha1-07Ik1Gf6LOjUNYnAJFBDJnwJNjQ=", + "dependencies": { + "bytes": "3.0.0", + "content-type": "~1.0.2", + "debug": "2.6.8", + "depd": "~1.1.1", + "http-errors": "~1.6.2", + "iconv-lite": "0.4.18", + "on-finished": "~2.3.0", + "qs": "6.5.0", + "raw-body": "2.3.1", + "type-is": "~1.6.15" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "deprecated": "This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial).", + "dependencies": { + "hoek": "2.x.x" + }, + "engines": { + "node": ">=0.10.40" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", + "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dependencies": { + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" + }, + "node_modules/browser-stdout": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", + "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", + "dev": true + }, + "node_modules/browserify-aes": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.0.8.tgz", + "integrity": "sha512-WYCMOT/PtGTlpOKFht0YJFYcPy6pLCR98CtWfzK13zoynLlBMvAdEMSRGmgnJCw2M2j/5qxBkinZQFobieM8dQ==", + "dependencies": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/browserify-cipher": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.0.tgz", + "integrity": "sha1-mYgkSHS/XtTijalWZtzWasj8Njo=", + "dependencies": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "node_modules/browserify-des": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.0.tgz", + "integrity": "sha1-2qJ3cXRwki7S/hhZQRihdUOXId0=", + "dependencies": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1" + } + }, + "node_modules/browserify-rsa": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "dependencies": { + "bn.js": "^4.1.0", + "randombytes": "^2.0.1" + } + }, + "node_modules/browserify-sign": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", + "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "dependencies": { + "bn.js": "^4.1.1", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.2", + "elliptic": "^6.0.0", + "inherits": "^2.0.1", + "parse-asn1": "^5.0.0" + } + }, + "node_modules/browserify-zlib": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", + "integrity": "sha1-uzX4pRn2AOD6a4SFJByXnQFB+y0=", + "dependencies": { + "pako": "~0.2.0" + } + }, + "node_modules/bson": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-1.0.4.tgz", + "integrity": "sha1-k8ENOeqltYQVy8QFLz5T5WKwtyw=", + "deprecated": "Fixed a critical issue with BSON serialization documented in CVE-2019-2391, see https://bit.ly/2KcpXdo for more details", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/buffer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "deprecated": "This version of 'buffer' is out-of-date. You must update to v4.9.2 or newer", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "node_modules/buffer-shims": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", + "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=" + }, + "node_modules/buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" + }, + "node_modules/builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=" + }, + "node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=", + "engines": { + "node": "*" + } + }, + "node_modules/camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "node_modules/center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "dependencies": { + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/chai": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz", + "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=", + "dev": true, + "dependencies": { + "assertion-error": "^1.0.1", + "check-error": "^1.0.1", + "deep-eql": "^3.0.0", + "get-func-name": "^2.0.0", + "pathval": "^1.0.0", + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "deprecated": "Chokidar 2 will break on node v14+. Upgrade to chokidar 3 with 15x less dependencies.", + "dependencies": { + "anymatch": "^1.3.0", + "async-each": "^1.0.0", + "glob-parent": "^2.0.0", + "inherits": "^2.0.1", + "is-binary-path": "^1.0.0", + "is-glob": "^2.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.0.0" + }, + "optionalDependencies": { + "fsevents": "^1.0.0" + } + }, + "node_modules/cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "dependencies": { + "center-align": "^0.1.1", + "right-align": "^0.1.1", + "wordwrap": "0.0.2" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/combined-stream": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", + "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", + "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "node_modules/config": { + "version": "1.26.2", + "resolved": "https://registry.npmjs.org/config/-/config-1.26.2.tgz", + "integrity": "sha1-JGYpEWjYr64Kroq5nqTUJy9SDK4=", + "dependencies": { + "json5": "0.4.0", + "os-homedir": "1.0.2" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/console-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "dependencies": { + "date-now": "^0.1.4" + } + }, + "node_modules/constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=" + }, + "node_modules/content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.3.tgz", + "integrity": "sha1-2hjvL7ZMpqzJBcxyAX0/OBhbkdE=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "node_modules/cors": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.4.tgz", + "integrity": "sha1-K9OB8usgECAQXNUOpZ2mMJBpRoY=", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/create-ecdh": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.0.tgz", + "integrity": "sha1-iIxyNZbN92EvZJgjPuvXo1MBc30=", + "dependencies": { + "bn.js": "^4.1.0", + "elliptic": "^6.0.0" + } + }, + "node_modules/create-hash": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.1.3.tgz", + "integrity": "sha1-YGBCrIuSYnUPSDyt2rD1gZFy2P0=", + "dependencies": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "sha.js": "^2.4.0" + } + }, + "node_modules/create-hmac": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.6.tgz", + "integrity": "sha1-rLniIaThe9sHbpBlfEK5PjcmzwY=", + "dependencies": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "deprecated": "This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial).", + "dependencies": { + "boom": "2.x.x" + }, + "engines": { + "node": ">=0.10.40" + } + }, + "node_modules/crypto-browserify": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-1.0.9.tgz", + "integrity": "sha1-zFRJaF37hesRyYKKzHy4erW7/MA=", + "engines": { + "node": "*" + } + }, + "node_modules/cycle": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", + "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dashdash/node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/date-now": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=" + }, + "node_modules/debug": { + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", + "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decache": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/decache/-/decache-4.3.0.tgz", + "integrity": "sha1-o5XkBwlWmKyKbe8B8qaky3Y49jU=", + "dependencies": { + "callsite": "^1.0.0" + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", + "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/des.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", + "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", + "dependencies": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "node_modules/diff": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz", + "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diffie-hellman": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.2.tgz", + "integrity": "sha1-tYNXOScM/ias9jIJn97SoH8gnl4=", + "dependencies": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "node_modules/domain-browser": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.1.7.tgz", + "integrity": "sha1-hnqksJP6oF8d4IwG9NeyH9+GmLw=", + "engines": { + "node": ">=0.4", + "npm": ">=1.2" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "optional": true, + "dependencies": { + "jsbn": "~0.1.0" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "node_modules/elliptic": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz", + "integrity": "sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=", + "dependencies": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + } + }, + "node_modules/emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/encodeurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz", + "integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz", + "integrity": "sha1-BCHjOf1xQZs9oT0Smzl5BAIwR24=", + "dependencies": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.4.0", + "object-assign": "^4.0.1", + "tapable": "^0.2.7" + }, + "engines": { + "node": ">=4.3.0 <5.0.0 || >=5.10" + } + }, + "node_modules/errno": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.4.tgz", + "integrity": "sha1-uJbiOp5ei6M4cfyZar02NfyaHH0=", + "dependencies": { + "prr": "~0.0.0" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/error-ex": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", + "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es6-promise": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.2.1.tgz", + "integrity": "sha1-7FYjOGgDKQkgcXDDlEjiREndH8Q=" + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/escodegen": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", + "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", + "dev": true, + "dependencies": { + "esprima": "^2.7.1", + "estraverse": "^1.9.1", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=0.12.0" + }, + "optionalDependencies": { + "source-map": "~0.2.0" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", + "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", + "dev": true, + "optional": true, + "dependencies": { + "amdefine": ">=0.0.4" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/estraverse": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", + "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.0.tgz", + "integrity": "sha1-b2Ma7zNtbEY2K1F2QETOIWvjwFE=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dependencies": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dependencies": { + "is-posix-bracket": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "dependencies": { + "fill-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/express": { + "version": "4.15.4", + "resolved": "https://registry.npmjs.org/express/-/express-4.15.4.tgz", + "integrity": "sha1-Ay4iU0ic+PzgJma+yj0R7XotrtE=", + "dependencies": { + "accepts": "~1.3.3", + "array-flatten": "1.1.1", + "content-disposition": "0.5.2", + "content-type": "~1.0.2", + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "debug": "2.6.8", + "depd": "~1.1.1", + "encodeurl": "~1.0.1", + "escape-html": "~1.0.3", + "etag": "~1.8.0", + "finalhandler": "~1.0.4", + "fresh": "0.5.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.1", + "path-to-regexp": "0.1.7", + "proxy-addr": "~1.1.5", + "qs": "6.5.0", + "range-parser": "~1.2.0", + "send": "0.15.4", + "serve-static": "1.12.4", + "setprototypeof": "1.0.3", + "statuses": "~1.3.1", + "type-is": "~1.6.15", + "utils-merge": "1.0.0", + "vary": "~1.1.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + }, + "node_modules/extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=", + "engines": { + "node": "> 0.1.90" + } + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "node_modules/filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fill-range": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz", + "integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=", + "dependencies": { + "is-number": "^2.1.0", + "isobject": "^2.0.0", + "randomatic": "^1.1.3", + "repeat-element": "^1.1.2", + "repeat-string": "^1.5.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/finalhandler": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.0.4.tgz", + "integrity": "sha512-16l/r8RgzlXKmFOhZpHBztvye+lAhC5SU7hXavnerC9UfZqZxxXl3BzL8MhffPT3kF61lj9Oav2LKEzh0ei7tg==", + "dependencies": { + "debug": "2.6.8", + "encodeurl": "~1.0.1", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.1", + "statuses": "~1.3.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dependencies": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "dependencies": { + "for-in": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.5", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/forwarded": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.1.tgz", + "integrity": "sha1-ik4wxkCwU5U5mjVJxzAldygEiWE=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.0.tgz", + "integrity": "sha1-9HTKXmqSRtb9jglTz6m5yAWvp44=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", + "integrity": "sha1-N5TzeMWLNC6n27sjCVEJxLO2IpE=", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^3.0.0", + "universalify": "^0.1.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "node_modules/fsevents": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.3.tgz", + "integrity": "sha512-WIr7iDkdmdbxu/Gh6eKEZJL6KPE74/5MEsf2whTOFNxbIoIixogroLdKYqB6FDav4Wavh/lZdzzd3b2KxIXC5Q==", + "bundleDependencies": [ + "node-pre-gyp" + ], + "deprecated": "fsevents 1 will break on node v14+ and could be using insecure binaries. Upgrade to fsevents 2.", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "nan": "^2.3.0", + "node-pre-gyp": "^0.6.39" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fsevents/node_modules/abbrev": { + "version": "1.1.0", + "inBundle": true, + "license": "ISC", + "optional": true + }, + "node_modules/fsevents/node_modules/ajv": { + "version": "4.11.8", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "co": "^4.6.0", + "json-stable-stringify": "^1.0.1" + } + }, + "node_modules/fsevents/node_modules/ansi-regex": { + "version": "2.1.1", + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fsevents/node_modules/aproba": { + "version": "1.1.1", + "inBundle": true, + "license": "ISC", + "optional": true + }, + "node_modules/fsevents/node_modules/are-we-there-yet": { + "version": "1.1.4", + "inBundle": true, + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "node_modules/fsevents/node_modules/asn1": { + "version": "0.2.3", + "inBundle": true, + "license": "MIT", + "optional": true + }, + "node_modules/fsevents/node_modules/assert-plus": { + "version": "0.2.0", + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/fsevents/node_modules/asynckit": { + "version": "0.4.0", + "inBundle": true, + "license": "MIT", + "optional": true + }, + "node_modules/fsevents/node_modules/aws-sign2": { + "version": "0.6.0", + "inBundle": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/fsevents/node_modules/aws4": { + "version": "1.6.0", + "inBundle": true, + "license": "MIT", + "optional": true + }, + "node_modules/fsevents/node_modules/balanced-match": { + "version": "0.4.2", + "inBundle": true, + "license": "MIT", + "optional": true + }, + "node_modules/fsevents/node_modules/bcrypt-pbkdf": { + "version": "1.0.1", + "inBundle": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/fsevents/node_modules/block-stream": { + "version": "0.0.9", + "inBundle": true, + "license": "ISC", + "optional": true, + "dependencies": { + "inherits": "~2.0.0" + }, + "engines": { + "node": "0.4 || >=0.5.8" + } + }, + "node_modules/fsevents/node_modules/boom": { + "version": "2.10.1", + "inBundle": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "hoek": "2.x.x" + }, + "engines": { + "node": ">=0.10.40" + } + }, + "node_modules/fsevents/node_modules/brace-expansion": { + "version": "1.1.7", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^0.4.1", + "concat-map": "0.0.1" + } + }, + "node_modules/fsevents/node_modules/buffer-shims": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT", + "optional": true + }, + "node_modules/fsevents/node_modules/caseless": { + "version": "0.12.0", + "inBundle": true, + "license": "Apache-2.0", + "optional": true + }, + "node_modules/fsevents/node_modules/co": { + "version": "4.6.0", + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/fsevents/node_modules/code-point-at": { + "version": "1.1.0", + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fsevents/node_modules/combined-stream": { + "version": "1.0.5", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents/node_modules/concat-map": { + "version": "0.0.1", + "inBundle": true, + "license": "MIT", + "optional": true + }, + "node_modules/fsevents/node_modules/console-control-strings": { + "version": "1.1.0", + "inBundle": true, + "license": "ISC", + "optional": true + }, + "node_modules/fsevents/node_modules/core-util-is": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT", + "optional": true + }, + "node_modules/fsevents/node_modules/cryptiles": { + "version": "2.0.5", + "inBundle": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boom": "2.x.x" + }, + "engines": { + "node": ">=0.10.40" + } + }, + "node_modules/fsevents/node_modules/dashdash": { + "version": "1.14.1", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/fsevents/node_modules/dashdash/node_modules/assert-plus": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/fsevents/node_modules/debug": { + "version": "2.6.8", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/fsevents/node_modules/deep-extend": { + "version": "0.4.2", + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.12.0" + } + }, + "node_modules/fsevents/node_modules/delayed-stream": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/fsevents/node_modules/delegates": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT", + "optional": true + }, + "node_modules/fsevents/node_modules/detect-libc": { + "version": "1.0.2", + "inBundle": true, + "license": "Apache-2.0", + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/fsevents/node_modules/ecc-jsbn": { + "version": "0.1.1", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "jsbn": "~0.1.0" + } + }, + "node_modules/fsevents/node_modules/extend": { + "version": "3.0.1", + "inBundle": true, + "license": "MIT", + "optional": true + }, + "node_modules/fsevents/node_modules/extsprintf": { + "version": "1.0.2", + "engines": [ + "node >=0.6.0" + ], + "inBundle": true, + "optional": true + }, + "node_modules/fsevents/node_modules/forever-agent": { + "version": "0.6.1", + "inBundle": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/fsevents/node_modules/form-data": { + "version": "2.1.4", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.5", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/fsevents/node_modules/fs.realpath": { + "version": "1.0.0", + "inBundle": true, + "license": "ISC", + "optional": true + }, + "node_modules/fsevents/node_modules/fstream": { + "version": "1.0.11", + "inBundle": true, + "license": "ISC", + "optional": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/fsevents/node_modules/fstream-ignore": { + "version": "1.0.5", + "inBundle": true, + "license": "ISC", + "optional": true, + "dependencies": { + "fstream": "^1.0.0", + "inherits": "2", + "minimatch": "^3.0.0" + } + }, + "node_modules/fsevents/node_modules/gauge": { + "version": "2.7.4", + "inBundle": true, + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "node_modules/fsevents/node_modules/getpass": { + "version": "0.1.7", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/fsevents/node_modules/getpass/node_modules/assert-plus": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/fsevents/node_modules/glob": { + "version": "7.1.2", + "inBundle": true, + "license": "ISC", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/fsevents/node_modules/graceful-fs": { + "version": "4.1.11", + "inBundle": true, + "license": "ISC", + "optional": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/fsevents/node_modules/har-schema": { + "version": "1.0.5", + "inBundle": true, + "license": "ISC", + "optional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/fsevents/node_modules/har-validator": { + "version": "4.2.1", + "inBundle": true, + "license": "ISC", + "optional": true, + "dependencies": { + "ajv": "^4.9.1", + "har-schema": "^1.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fsevents/node_modules/has-unicode": { + "version": "2.0.1", + "inBundle": true, + "license": "ISC", + "optional": true + }, + "node_modules/fsevents/node_modules/hawk": { + "version": "3.1.3", + "inBundle": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boom": "2.x.x", + "cryptiles": "2.x.x", + "hoek": "2.x.x", + "sntp": "1.x.x" + }, + "engines": { + "node": ">=0.10.32" + } + }, + "node_modules/fsevents/node_modules/hoek": { + "version": "2.16.3", + "inBundle": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.40" + } + }, + "node_modules/fsevents/node_modules/http-signature": { + "version": "1.1.1", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^0.2.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/fsevents/node_modules/inflight": { + "version": "1.0.6", + "inBundle": true, + "license": "ISC", + "optional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/fsevents/node_modules/inherits": { + "version": "2.0.3", + "inBundle": true, + "license": "ISC", + "optional": true + }, + "node_modules/fsevents/node_modules/ini": { + "version": "1.3.4", + "inBundle": true, + "license": "ISC", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/fsevents/node_modules/is-fullwidth-code-point": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "number-is-nan": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fsevents/node_modules/is-typedarray": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT", + "optional": true + }, + "node_modules/fsevents/node_modules/isarray": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT", + "optional": true + }, + "node_modules/fsevents/node_modules/isstream": { + "version": "0.1.2", + "inBundle": true, + "license": "MIT", + "optional": true + }, + "node_modules/fsevents/node_modules/jodid25519": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "jsbn": "~0.1.0" + } + }, + "node_modules/fsevents/node_modules/jsbn": { + "version": "0.1.1", + "inBundle": true, + "license": "MIT", + "optional": true + }, + "node_modules/fsevents/node_modules/json-schema": { + "version": "0.2.3", + "inBundle": true, + "optional": true + }, + "node_modules/fsevents/node_modules/json-stable-stringify": { + "version": "1.0.1", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "jsonify": "~0.0.0" + } + }, + "node_modules/fsevents/node_modules/json-stringify-safe": { + "version": "5.0.1", + "inBundle": true, + "license": "ISC", + "optional": true + }, + "node_modules/fsevents/node_modules/jsonify": { + "version": "0.0.0", + "inBundle": true, + "license": "Public Domain", + "optional": true + }, + "node_modules/fsevents/node_modules/jsprim": { + "version": "1.4.0", + "engines": [ + "node >=0.6.0" + ], + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.0.2", + "json-schema": "0.2.3", + "verror": "1.3.6" + } + }, + "node_modules/fsevents/node_modules/jsprim/node_modules/assert-plus": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/fsevents/node_modules/mime-db": { + "version": "1.27.0", + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents/node_modules/mime-types": { + "version": "2.1.15", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "mime-db": "~1.27.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents/node_modules/minimatch": { + "version": "3.0.4", + "inBundle": true, + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/fsevents/node_modules/minimist": { + "version": "0.0.8", + "inBundle": true, + "license": "MIT", + "optional": true + }, + "node_modules/fsevents/node_modules/mkdirp": { + "version": "0.5.1", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "minimist": "0.0.8" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/fsevents/node_modules/ms": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "optional": true + }, + "node_modules/fsevents/node_modules/node-pre-gyp": { + "version": "0.6.39", + "inBundle": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "detect-libc": "^1.0.2", + "hawk": "3.1.3", + "mkdirp": "^0.5.1", + "nopt": "^4.0.1", + "npmlog": "^4.0.2", + "rc": "^1.1.7", + "request": "2.81.0", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^2.2.1", + "tar-pack": "^3.4.0" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/fsevents/node_modules/nopt": { + "version": "4.0.1", + "inBundle": true, + "license": "ISC", + "optional": true, + "dependencies": { + "abbrev": "1", + "osenv": "^0.1.4" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, + "node_modules/fsevents/node_modules/npmlog": { + "version": "4.1.0", + "inBundle": true, + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "node_modules/fsevents/node_modules/number-is-nan": { + "version": "1.0.1", + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fsevents/node_modules/oauth-sign": { + "version": "0.8.2", + "inBundle": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/fsevents/node_modules/object-assign": { + "version": "4.1.1", + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fsevents/node_modules/once": { + "version": "1.4.0", + "inBundle": true, + "license": "ISC", + "optional": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/fsevents/node_modules/os-homedir": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fsevents/node_modules/os-tmpdir": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fsevents/node_modules/osenv": { + "version": "0.1.4", + "inBundle": true, + "license": "ISC", + "optional": true, + "dependencies": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "node_modules/fsevents/node_modules/path-is-absolute": { + "version": "1.0.1", + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fsevents/node_modules/performance-now": { + "version": "0.2.0", + "inBundle": true, + "license": "MIT", + "optional": true + }, + "node_modules/fsevents/node_modules/process-nextick-args": { + "version": "1.0.7", + "inBundle": true, + "license": "MIT", + "optional": true + }, + "node_modules/fsevents/node_modules/punycode": { + "version": "1.4.1", + "inBundle": true, + "license": "MIT", + "optional": true + }, + "node_modules/fsevents/node_modules/qs": { + "version": "6.4.0", + "inBundle": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/fsevents/node_modules/rc": { + "version": "1.2.1", + "inBundle": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "optional": true, + "dependencies": { + "deep-extend": "~0.4.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "index.js" + } + }, + "node_modules/fsevents/node_modules/rc/node_modules/minimist": { + "version": "1.2.0", + "inBundle": true, + "license": "MIT", + "optional": true + }, + "node_modules/fsevents/node_modules/readable-stream": { + "version": "2.2.9", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer-shims": "~1.0.0", + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "string_decoder": "~1.0.0", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/fsevents/node_modules/request": { + "version": "2.81.0", + "inBundle": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "aws-sign2": "~0.6.0", + "aws4": "^1.2.1", + "caseless": "~0.12.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.0", + "forever-agent": "~0.6.1", + "form-data": "~2.1.1", + "har-validator": "~4.2.1", + "hawk": "~3.1.3", + "http-signature": "~1.1.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.7", + "oauth-sign": "~0.8.1", + "performance-now": "^0.2.0", + "qs": "~6.4.0", + "safe-buffer": "^5.0.1", + "stringstream": "~0.0.4", + "tough-cookie": "~2.3.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.0.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/fsevents/node_modules/rimraf": { + "version": "2.6.1", + "inBundle": true, + "license": "ISC", + "optional": true, + "dependencies": { + "glob": "^7.0.5" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/fsevents/node_modules/safe-buffer": { + "version": "5.0.1", + "inBundle": true, + "license": "MIT", + "optional": true + }, + "node_modules/fsevents/node_modules/semver": { + "version": "5.3.0", + "inBundle": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/fsevents/node_modules/set-blocking": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "optional": true + }, + "node_modules/fsevents/node_modules/signal-exit": { + "version": "3.0.2", + "inBundle": true, + "license": "ISC", + "optional": true + }, + "node_modules/fsevents/node_modules/sntp": { + "version": "1.0.9", + "inBundle": true, + "optional": true, + "dependencies": { + "hoek": "2.x.x" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fsevents/node_modules/sshpk": { + "version": "1.13.0", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "dashdash": "^1.12.0", + "getpass": "^0.1.1" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + }, + "optionalDependencies": { + "bcrypt-pbkdf": "^1.0.0", + "ecc-jsbn": "~0.1.1", + "jodid25519": "^1.0.0", + "jsbn": "~0.1.0", + "tweetnacl": "~0.14.0" + } + }, + "node_modules/fsevents/node_modules/sshpk/node_modules/assert-plus": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/fsevents/node_modules/string_decoder": { + "version": "1.0.1", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/fsevents/node_modules/string-width": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fsevents/node_modules/stringstream": { + "version": "0.0.5", + "inBundle": true, + "license": "MIT", + "optional": true + }, + "node_modules/fsevents/node_modules/strip-ansi": { + "version": "3.0.1", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fsevents/node_modules/strip-json-comments": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fsevents/node_modules/tar": { + "version": "2.2.1", + "inBundle": true, + "license": "ISC", + "optional": true, + "dependencies": { + "block-stream": "*", + "fstream": "^1.0.2", + "inherits": "2" + } + }, + "node_modules/fsevents/node_modules/tar-pack": { + "version": "3.4.0", + "inBundle": true, + "license": "BSD-2-Clause", + "optional": true, + "dependencies": { + "debug": "^2.2.0", + "fstream": "^1.0.10", + "fstream-ignore": "^1.0.5", + "once": "^1.3.3", + "readable-stream": "^2.1.4", + "rimraf": "^2.5.1", + "tar": "^2.2.1", + "uid-number": "^0.0.6" + } + }, + "node_modules/fsevents/node_modules/tough-cookie": { + "version": "2.3.2", + "inBundle": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "punycode": "^1.4.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/fsevents/node_modules/tunnel-agent": { + "version": "0.6.0", + "inBundle": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/fsevents/node_modules/tweetnacl": { + "version": "0.14.5", + "inBundle": true, + "license": "Unlicense", + "optional": true + }, + "node_modules/fsevents/node_modules/uid-number": { + "version": "0.0.6", + "inBundle": true, + "license": "ISC", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/fsevents/node_modules/util-deprecate": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT", + "optional": true + }, + "node_modules/fsevents/node_modules/uuid": { + "version": "3.0.1", + "inBundle": true, + "license": "MIT", + "optional": true, + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/fsevents/node_modules/verror": { + "version": "1.3.6", + "engines": [ + "node >=0.6.0" + ], + "inBundle": true, + "optional": true, + "dependencies": { + "extsprintf": "1.0.2" + } + }, + "node_modules/fsevents/node_modules/wide-align": { + "version": "1.1.2", + "inBundle": true, + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^1.0.2" + } + }, + "node_modules/fsevents/node_modules/wrappy": { + "version": "1.0.2", + "inBundle": true, + "license": "ISC", + "optional": true + }, + "node_modules/get-caller-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", + "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=" + }, + "node_modules/get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/getpass/node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "dependencies": { + "glob-parent": "^2.0.0", + "is-glob": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dependencies": { + "is-glob": "^2.0.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", + "dev": true + }, + "node_modules/growl": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", + "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=", + "dev": true + }, + "node_modules/handlebars": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.10.tgz", + "integrity": "sha1-PTDHGLCaPZbyPqTMH0A8TTup/08=", + "dev": true, + "dependencies": { + "async": "^1.4.0", + "optimist": "^0.6.1", + "source-map": "^0.4.4" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^2.6" + } + }, + "node_modules/handlebars/node_modules/async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "node_modules/handlebars/node_modules/source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "dependencies": { + "amdefine": ">=0.0.4" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/har-schema": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", + "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=", + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", + "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", + "deprecated": "this library is no longer supported", + "dependencies": { + "ajv": "^4.9.1", + "har-schema": "^1.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hash-base": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz", + "integrity": "sha1-ZuodhW206KVHDK32/OI65SRO8uE=", + "dependencies": { + "inherits": "^2.0.1" + } + }, + "node_modules/hash.js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.3.tgz", + "integrity": "sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "deprecated": "This module moved to @hapi/hawk. Please make sure to switch over as this distribution is no longer supported and may contain bugs and critical security issues.", + "dependencies": { + "boom": "2.x.x", + "cryptiles": "2.x.x", + "hoek": "2.x.x", + "sntp": "1.x.x" + }, + "engines": { + "node": ">=0.10.32" + } + }, + "node_modules/he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", + "deprecated": "This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial).", + "engines": { + "node": ">=0.10.40" + } + }, + "node_modules/hosted-git-info": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", + "integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==" + }, + "node_modules/http-errors": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", + "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", + "dependencies": { + "depd": "1.1.1", + "inherits": "2.0.3", + "setprototypeof": "1.0.3", + "statuses": ">= 1.3.1 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-signature": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "dependencies": { + "assert-plus": "^0.2.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/https-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-0.0.1.tgz", + "integrity": "sha1-P5E2XKvmC3ftDruiS0VOPgnZWoI=" + }, + "node_modules/iconv-lite": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.18.tgz", + "integrity": "sha512-sr1ZQph3UwHTR0XftSbK85OvBbxe/abLGzEnPENCQwmHf7sck8Oyu4ob3LgBxWWxRoM+QszeUyl7jbqapu2TqA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", + "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=" + }, + "node_modules/indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "node_modules/interpret": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.0.3.tgz", + "integrity": "sha1-y8NcYu7uc/Gat7EKgBURQBr8D5A=" + }, + "node_modules/invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ipaddr.js": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.4.0.tgz", + "integrity": "sha1-KWrKh4qCGBbluF0KKFqZvP9FgvA=", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, + "node_modules/is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dependencies": { + "binary-extensions": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-buffer": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz", + "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=", + "deprecated": "This version of 'is-buffer' is out-of-date. You must update to v1.1.6 or newer" + }, + "node_modules/is-builtin-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "dependencies": { + "builtin-modules": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "dependencies": { + "is-primitive": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dependencies": { + "number-is-nan": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "node_modules/is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "node_modules/isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dependencies": { + "isarray": "1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "node_modules/istanbul": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", + "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", + "deprecated": "This module is no longer maintained, try this instead:\n npm i nyc\nVisit https://istanbul.js.org/integrations for other alternatives.", + "dev": true, + "dependencies": { + "abbrev": "1.0.x", + "async": "1.x", + "escodegen": "1.8.x", + "esprima": "2.7.x", + "glob": "^5.0.15", + "handlebars": "^4.0.1", + "js-yaml": "3.x", + "mkdirp": "0.5.x", + "nopt": "3.x", + "once": "1.x", + "resolve": "1.1.x", + "supports-color": "^3.1.0", + "which": "^1.1.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "istanbul": "lib/cli.js" + } + }, + "node_modules/istanbul/node_modules/async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "node_modules/istanbul/node_modules/glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true, + "dependencies": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/istanbul/node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "node_modules/jmespath": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", + "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/js-yaml": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz", + "integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/js-yaml/node_modules/esprima": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", + "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, + "node_modules/json-loader": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/json-loader/-/json-loader-0.5.7.tgz", + "integrity": "sha512-QLPs8Dj7lnf3e3QYS1zkCo+4ZwqOiF9d/nZnYozTISxXWCfNs9yuky5rJw4/W34s7POaNlbZmQGaB5NiXCbP4w==" + }, + "node_modules/json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "node_modules/json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dependencies": { + "jsonify": "~0.0.0" + } + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "node_modules/json3": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", + "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", + "deprecated": "Please use the native JSON object instead of JSON 3", + "dev": true + }, + "node_modules/json5": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.4.0.tgz", + "integrity": "sha1-BUNS5MTIDIbAkjh31EneF2pzLI0=", + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsonfile": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", + "integrity": "sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "engines": { + "node": "*" + } + }, + "node_modules/jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "node_modules/jsprim/node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/jszip": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-2.5.0.tgz", + "integrity": "sha1-dET9hVHd8+XacZj+oMkbyDCMwnQ=", + "dependencies": { + "pako": "~0.2.5" + } + }, + "node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "dependencies": { + "invert-kv": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loader-runner": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.3.0.tgz", + "integrity": "sha1-9IKuqC1UPgeSFwDVpG7yb9rGuKI=", + "engines": { + "node": ">=4.3.0 <5.0.0 || >=5.10" + } + }, + "node_modules/loader-utils": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", + "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", + "dependencies": { + "big.js": "^3.1.3", + "emojis-list": "^2.0.0", + "json5": "^0.5.0", + "object-assign": "^4.0.1" + } + }, + "node_modules/loader-utils/node_modules/json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/lodash": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" + }, + "node_modules/lodash._baseassign": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", + "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", + "dev": true, + "dependencies": { + "lodash._basecopy": "^3.0.0", + "lodash.keys": "^3.0.0" + } + }, + "node_modules/lodash._basecopy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", + "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", + "dev": true + }, + "node_modules/lodash._basecreate": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz", + "integrity": "sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE=", + "dev": true + }, + "node_modules/lodash._getnative": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", + "dev": true + }, + "node_modules/lodash._isiterateecall": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", + "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", + "dev": true + }, + "node_modules/lodash.create": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz", + "integrity": "sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=", + "dev": true, + "dependencies": { + "lodash._baseassign": "^3.0.0", + "lodash._basecreate": "^3.0.0", + "lodash._isiterateecall": "^3.0.0" + } + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", + "dev": true + }, + "node_modules/lodash.isarray": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", + "dev": true + }, + "node_modules/lodash.keys": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", + "dev": true, + "dependencies": { + "lodash._getnative": "^3.0.0", + "lodash.isarguments": "^3.0.0", + "lodash.isarray": "^3.0.0" + } + }, + "node_modules/longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/md5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz", + "integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=", + "dev": true, + "dependencies": { + "charenc": "~0.0.1", + "crypt": "~0.0.1", + "is-buffer": "~1.1.1" + } + }, + "node_modules/md5.js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", + "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "node_modules/md5.js/node_modules/hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "dependencies": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dependencies": { + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/miller-rabin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.0.tgz", + "integrity": "sha1-SmL7HUKTPAVYOYL0xxb2+55sbT0=", + "dependencies": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + }, + "bin": { + "miller-rabin": "bin/miller-rabin" + } + }, + "node_modules/mime": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz", + "integrity": "sha1-EV+eO2s9rylZmDyzjxSaLUDrXVM=", + "bin": { + "mime": "cli.js" + } + }, + "node_modules/mime-db": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", + "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.17", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", + "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", + "dependencies": { + "mime-db": "~1.30.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz", + "integrity": "sha1-cCvi3aazf0g2vLP121ZkG2Sh09M=" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" + }, + "node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "node_modules/mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", + "dependencies": { + "minimist": "0.0.8" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mocha": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-3.5.2.tgz", + "integrity": "sha512-iH5zl7afRZl1GvD0pnrRlazgc9Z/o34pXWmTFi8xNIMFKXgNL1SoBTDDb9sJfbV/sJV/j8X+0gvwY1QS1He7Nw==", + "dev": true, + "dependencies": { + "browser-stdout": "1.3.0", + "commander": "2.9.0", + "debug": "2.6.8", + "diff": "3.2.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.1", + "growl": "1.9.2", + "he": "1.1.1", + "json3": "3.3.2", + "lodash.create": "3.1.1", + "mkdirp": "0.5.1", + "supports-color": "3.1.2" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha" + }, + "engines": { + "node": ">= 0.10.x", + "npm": ">= 1.4.x" + } + }, + "node_modules/mocha-junit-reporter": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/mocha-junit-reporter/-/mocha-junit-reporter-1.15.0.tgz", + "integrity": "sha1-MJ9LeiD82ibQrWnJt9CAjXcjAsI=", + "dev": true, + "dependencies": { + "debug": "^2.2.0", + "md5": "^2.1.0", + "mkdirp": "~0.5.1", + "xml": "^1.0.0" + }, + "peerDependencies": { + "mocha": "^4 || ^3 || ^2.2.5" + } + }, + "node_modules/mocha/node_modules/commander": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", + "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", + "dev": true, + "dependencies": { + "graceful-readlink": ">= 1.0.0" + }, + "engines": { + "node": ">= 0.6.x" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz", + "integrity": "sha1-gFIR3wT6rxxjo2ADBs31reULLsg=", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.2", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz", + "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=", + "dev": true, + "dependencies": { + "has-flag": "^1.0.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/mongodb": { + "version": "2.2.31", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.2.31.tgz", + "integrity": "sha1-GUBEXGYeGSF7s7+CRdmFSq71SNs=", + "dependencies": { + "es6-promise": "3.2.1", + "mongodb-core": "2.1.15", + "readable-stream": "2.2.7" + }, + "engines": { + "node": ">=0.10.3" + } + }, + "node_modules/mongodb-core": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-2.1.15.tgz", + "integrity": "sha1-hB9TuH//9MdFgYnDXIroJ+EWl2Q=", + "dependencies": { + "bson": "~1.0.4", + "require_optional": "~1.0.0" + } + }, + "node_modules/mongodb/node_modules/readable-stream": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.7.tgz", + "integrity": "sha1-BwV6y+JGeyIELTb5jFrVBwVOlbE=", + "dependencies": { + "buffer-shims": "~1.0.0", + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "string_decoder": "~1.0.0", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/nan": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz", + "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=", + "optional": true + }, + "node_modules/negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-libs-browser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.0.0.tgz", + "integrity": "sha1-o6WeyXAkmFtG6Vg3lkb5bEthZkY=", + "dependencies": { + "assert": "^1.1.1", + "browserify-zlib": "^0.1.4", + "buffer": "^4.3.0", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.11.0", + "domain-browser": "^1.1.1", + "events": "^1.0.0", + "https-browserify": "0.0.1", + "os-browserify": "^0.2.0", + "path-browserify": "0.0.0", + "process": "^0.11.0", + "punycode": "^1.2.4", + "querystring-es3": "^0.2.0", + "readable-stream": "^2.0.5", + "stream-browserify": "^2.0.1", + "stream-http": "^2.3.1", + "string_decoder": "^0.10.25", + "timers-browserify": "^2.0.2", + "tty-browserify": "0.0.0", + "url": "^0.11.0", + "util": "^0.10.3", + "vm-browserify": "0.0.4" + } + }, + "node_modules/node-libs-browser/node_modules/crypto-browserify": { + "version": "3.11.1", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.11.1.tgz", + "integrity": "sha512-Na7ZlwCOqoaW5RwUK1WpXws2kv8mNhWdTlzob0UXulk6G9BDbyiJaGTYBIX61Ozn9l1EPPJpICZb4DaOpT9NlQ==", + "dependencies": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/node-libs-browser/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "node_modules/node-libs-browser/node_modules/url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/node-zip": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/node-zip/-/node-zip-1.1.1.tgz", + "integrity": "sha1-lNGtZ0o81GoViN1zb0qaeMdX62I=", + "dependencies": { + "jszip": "2.5.0" + }, + "bin": { + "nodezip": "bin/nodezip" + }, + "engines": { + "node": "*" + } + }, + "node_modules/nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, + "node_modules/normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "dependencies": { + "hosted-git-info": "^2.1.4", + "is-builtin-module": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", + "engines": { + "node": "*" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "dependencies": { + "for-own": "^0.1.4", + "is-extendable": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "dependencies": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + } + }, + "node_modules/optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true, + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.4", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "wordwrap": "~1.0.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/optionator/node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "node_modules/os-browserify": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.2.1.tgz", + "integrity": "sha1-Y/xMzuXS13Y9Jrv4YBB45sLgBE8=" + }, + "node_modules/os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "dependencies": { + "lcid": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=" + }, + "node_modules/parse-asn1": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.0.tgz", + "integrity": "sha1-N8T5t+06tlx0gXtfJICTf7+XxxI=", + "dependencies": { + "asn1.js": "^4.0.0", + "browserify-aes": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3" + } + }, + "node_modules/parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "dependencies": { + "glob-base": "^0.3.0", + "is-dotfile": "^1.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dependencies": { + "error-ex": "^1.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", + "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=" + }, + "node_modules/path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dependencies": { + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "node_modules/path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dependencies": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/pbkdf2": { + "version": "3.0.14", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.14.tgz", + "integrity": "sha512-gjsZW9O34fm0R7PaLHRJmLLVfSoesxztjPjE9o6R+qtVJij90ltg1joIovN9GKrRW3t1PzhDDG3UMEMFfZ+1wA==", + "dependencies": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/performance-now": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", + "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=" + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + }, + "node_modules/proxy-addr": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.1.5.tgz", + "integrity": "sha1-ccDuOxAt4/IC87ZPYI0XP8uhqRg=", + "dependencies": { + "forwarded": "~0.1.0", + "ipaddr.js": "1.4.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/prr": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/prr/-/prr-0.0.0.tgz", + "integrity": "sha1-GoS4WQgyVQFBGFPQCB7j+obikmo=" + }, + "node_modules/public-encrypt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.0.tgz", + "integrity": "sha1-OfaZ86RlYN1eusvKaTyvfGXBjMY=", + "dependencies": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1" + } + }, + "node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + }, + "node_modules/qs": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.0.tgz", + "integrity": "sha512-fjVFjW9yhqMhVGwRExCXLhJKrLlkYSaxNWdyc9rmHlrVZbk35YHH312dFd7191uQeXkI3mKLZTIbSvIeFwFemg==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/randomatic": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", + "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==", + "dependencies": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/randomatic/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/randomatic/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/randomatic/node_modules/kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/randombytes": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.5.tgz", + "integrity": "sha512-8T7Zn1AhMsQ/HI1SjcCfT/t4ii3eAqco3yOcSzS4mozsOz69lHLsoMXmF9nZgnFanYscnSlUSgs8uZyKzpE6kg==", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.1.tgz", + "integrity": "sha512-sxkd1uqaSj41SG5Vet9sNAxBMCMsmZ3LVhRkDlK8SbCpelTUB7JiMGHG70AZS6cFiCRgfNQhU2eLnTHYRFf7LA==", + "dependencies": { + "bytes": "3.0.0", + "http-errors": "1.6.2", + "iconv-lite": "0.4.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dependencies": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dependencies": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.0.3", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readdirp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", + "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", + "dependencies": { + "graceful-fs": "^4.1.2", + "minimatch": "^3.0.2", + "readable-stream": "^2.0.2", + "set-immediate-shim": "^1.0.1" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/reflect-metadata": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.10.tgz", + "integrity": "sha1-tPg3BEFqytiZiMmxVjXUfgO5NEo=" + }, + "node_modules/regex-cache": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", + "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "dependencies": { + "is-equal-shallow": "^0.1.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" + }, + "node_modules/repeat-element": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/request": { + "version": "2.81.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", + "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "dependencies": { + "aws-sign2": "~0.6.0", + "aws4": "^1.2.1", + "caseless": "~0.12.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.0", + "forever-agent": "~0.6.1", + "form-data": "~2.1.1", + "har-validator": "~4.2.1", + "hawk": "~3.1.3", + "http-signature": "~1.1.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.7", + "oauth-sign": "~0.8.1", + "performance-now": "^0.2.0", + "qs": "~6.4.0", + "safe-buffer": "^5.0.1", + "stringstream": "~0.0.4", + "tough-cookie": "~2.3.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.0.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/request/node_modules/qs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", + "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/require_optional": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", + "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", + "dependencies": { + "resolve-from": "^2.0.0", + "semver": "^5.1.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" + }, + "node_modules/resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + }, + "node_modules/resolve-from": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", + "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "dependencies": { + "align-text": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ripemd160": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz", + "integrity": "sha1-D0WEKVxTo2KK9+bXmsohzlfRxuc=", + "dependencies": { + "hash-base": "^2.0.0", + "inherits": "^2.0.1" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + }, + "node_modules/sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" + }, + "node_modules/semver": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", + "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/send": { + "version": "0.15.4", + "resolved": "https://registry.npmjs.org/send/-/send-0.15.4.tgz", + "integrity": "sha1-mF+qPihLAnPHkzZKNcZze9k5Bbk=", + "dependencies": { + "debug": "2.6.8", + "depd": "~1.1.1", + "destroy": "~1.0.4", + "encodeurl": "~1.0.1", + "escape-html": "~1.0.3", + "etag": "~1.8.0", + "fresh": "0.5.0", + "http-errors": "~1.6.2", + "mime": "1.3.4", + "ms": "2.0.0", + "on-finished": "~2.3.0", + "range-parser": "~1.2.0", + "statuses": "~1.3.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static": { + "version": "1.12.4", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.12.4.tgz", + "integrity": "sha1-m2qpjutyU8Tu3Ewfb9vKYJkBqWE=", + "dependencies": { + "encodeurl": "~1.0.1", + "escape-html": "~1.0.3", + "parseurl": "~1.3.1", + "send": "0.15.4" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "node_modules/set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" + }, + "node_modules/setprototypeof": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" + }, + "node_modules/sha.js": { + "version": "2.4.8", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.8.tgz", + "integrity": "sha1-NwaMLEdra69ALRSknGf1l5IfY08=", + "dependencies": { + "inherits": "^2.0.1" + }, + "bin": { + "sha.js": "bin.js" + } + }, + "node_modules/sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "deprecated": "This module moved to @hapi/sntp. Please make sure to switch over as this distribution is no longer supported and may contain bugs and critical security issues.", + "dependencies": { + "hoek": "2.x.x" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/source-list-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.0.tgz", + "integrity": "sha512-I2UmuJSRr/T8jisiROLU3A3ltr+swpniSmNPI4Ml3ZCX6tVnDsuZzK7F2hl5jTqbZBWCEKlj5HRQiPExXLgE8A==" + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdx-correct": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz", + "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=", + "dependencies": { + "spdx-license-ids": "^1.0.2" + } + }, + "node_modules/spdx-expression-parse": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz", + "integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=" + }, + "node_modules/spdx-license-ids": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz", + "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=" + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "node_modules/sshpk": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", + "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "dashdash": "^1.12.0", + "getpass": "^0.1.1" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + }, + "optionalDependencies": { + "bcrypt-pbkdf": "^1.0.0", + "ecc-jsbn": "~0.1.1", + "jsbn": "~0.1.0", + "tweetnacl": "~0.14.0" + } + }, + "node_modules/sshpk/node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", + "engines": { + "node": "*" + } + }, + "node_modules/statuses": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stream-browserify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", + "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", + "dependencies": { + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" + } + }, + "node_modules/stream-http": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.7.2.tgz", + "integrity": "sha512-c0yTD2rbQzXtSsFSVhtpvY/vS6u066PcXOX9kBB3mSO76RiUQzL340uJkGBWnlBg4/HZzqiUXtaVA7wcRcJgEw==", + "dependencies": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.2.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dependencies": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" + }, + "node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dependencies": { + "is-utf8": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dependencies": { + "has-flag": "^1.0.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/tapable": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.2.8.tgz", + "integrity": "sha1-mTcqXJmb8t8WCvwNdL7U9HlIzSI=", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/timers-browserify": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.4.tgz", + "integrity": "sha512-uZYhyU3EX8O7HQP+J9fTVYwsq90Vr68xPEFo7yrVImIxYvHgukBEgOB/SgGoorWVTzGM/3Z+wUNnboA4M8jWrg==", + "dependencies": { + "setimmediate": "^1.0.4" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=" + }, + "node_modules/tough-cookie": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz", + "integrity": "sha1-8IH3bkyFcg5sN6X6ztc3FQ2EByo=", + "dependencies": { + "punycode": "^1.4.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tough-cookie/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "node_modules/tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + }, + "node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.3.tgz", + "integrity": "sha1-Dj8mcLRAmbC0bChNE2p+9Jx0wuo=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/type-is": { + "version": "1.6.15", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", + "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.15" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.5.2.tgz", + "integrity": "sha1-A4qV99m7tCCxvzW6MdTFwd0//jQ=", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "dependencies": { + "source-map": "~0.5.1", + "yargs": "~3.10.0" + }, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + }, + "optionalDependencies": { + "uglify-to-browserify": "~1.0.0" + } + }, + "node_modules/uglify-js/node_modules/yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "dependencies": { + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", + "window-size": "0.1.0" + } + }, + "node_modules/uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "optional": true + }, + "node_modules/universalify": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz", + "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dependencies": { + "inherits": "2.0.1" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "node_modules/util/node_modules/inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" + }, + "node_modules/utils-merge": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz", + "integrity": "sha1-ApT7kiu5N1FTVBxPcJYjHyh8ivg=", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.1.tgz", + "integrity": "sha1-ZUS7ot/ajBzxfmKaOjBeK7H+5sE=", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz", + "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=", + "dependencies": { + "spdx-correct": "~1.0.0", + "spdx-expression-parse": "~1.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.1.tgz", + "integrity": "sha1-Z1Neu2lMHVIldFeYRmUyP1h+jTc=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/verror/node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/vm-browserify": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", + "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", + "dependencies": { + "indexof": "0.0.1" + } + }, + "node_modules/watchpack": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.4.0.tgz", + "integrity": "sha1-ShRyvLuVK9Cpu0A2gB+VTfs5+qw=", + "dependencies": { + "async": "^2.1.2", + "chokidar": "^1.7.0", + "graceful-fs": "^4.1.2" + } + }, + "node_modules/webpack": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-2.7.0.tgz", + "integrity": "sha512-MjAA0ZqO1ba7ZQJRnoCdbM56mmFpipOPUv/vQpwwfSI42p5PVDdoiuK2AL2FwFUVgT859Jr43bFZXRg/LNsqvg==", + "dependencies": { + "acorn": "^5.0.0", + "acorn-dynamic-import": "^2.0.0", + "ajv": "^4.7.0", + "ajv-keywords": "^1.1.1", + "async": "^2.1.2", + "enhanced-resolve": "^3.3.0", + "interpret": "^1.0.0", + "json-loader": "^0.5.4", + "json5": "^0.5.1", + "loader-runner": "^2.3.0", + "loader-utils": "^0.2.16", + "memory-fs": "~0.4.1", + "mkdirp": "~0.5.0", + "node-libs-browser": "^2.0.0", + "source-map": "^0.5.3", + "supports-color": "^3.1.0", + "tapable": "~0.2.5", + "uglify-js": "^2.8.27", + "watchpack": "^1.3.1", + "webpack-sources": "^1.0.1", + "yargs": "^6.0.0" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=4.3.0 <5.0.0 || >=5.10" + } + }, + "node_modules/webpack-sources": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.0.1.tgz", + "integrity": "sha512-05tMxipUCwHqYaVS8xc7sYPTly8PzXayRCB4dTxLhWTqlKUiwH6ezmEe0OSreL1c30LAuA3Zqmc+uEBUGFJDjw==", + "dependencies": { + "source-list-map": "^2.0.0", + "source-map": "~0.5.3" + } + }, + "node_modules/webpack/node_modules/json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/which": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", + "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/which-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", + "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=" + }, + "node_modules/window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/winston": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/winston/-/winston-2.3.1.tgz", + "integrity": "sha1-C0hCDZeMAYBM8CMLZIhhWYIloRk=", + "dependencies": { + "async": "~1.0.0", + "colors": "1.0.x", + "cycle": "1.0.x", + "eyes": "0.1.x", + "isstream": "0.1.x", + "stack-trace": "0.0.x" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/winston/node_modules/async": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz", + "integrity": "sha1-+PwEyjoTeErenhZBr5hXjPvWR6k=" + }, + "node_modules/wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "dependencies": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "node_modules/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=", + "dev": true + }, + "node_modules/xml2js": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.17.tgz", + "integrity": "sha1-F76T6q4/O3eTWceVtBlwWogX6Gg=", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "^4.1.0" + } + }, + "node_modules/xmlbuilder": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-4.2.1.tgz", + "integrity": "sha1-qlijBBoGb5DqoWwvU4n/GfP0YaU=", + "dependencies": { + "lodash": "^4.0.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" + }, + "node_modules/yamljs": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.2.10.tgz", + "integrity": "sha1-SBzHwlynOvWfWR8MluPOVsdXpA8=", + "dependencies": { + "argparse": "^1.0.7", + "glob": "^7.0.5" + }, + "bin": { + "json2yaml": "bin/json2yaml", + "yaml2json": "bin/yaml2json" + } + }, + "node_modules/yargs": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz", + "integrity": "sha1-eC7CHvQDNF+DCoCMo9UTr1YGUgg=", + "dependencies": { + "camelcase": "^3.0.0", + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "os-locale": "^1.4.0", + "read-pkg-up": "^1.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^1.0.2", + "which-module": "^1.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^4.2.0" + } + }, + "node_modules/yargs-parser": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-4.2.1.tgz", + "integrity": "sha1-KczqwNxPA8bIe0qfIX3RjJ90hxw=", + "dependencies": { + "camelcase": "^3.0.0" + } + }, + "node_modules/yargs-parser/node_modules/camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yargs/node_modules/camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yargs/node_modules/cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "dependencies": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + } + } + }, + "dependencies": { + "@types/async": { + "version": "2.0.42", + "resolved": "https://registry.npmjs.org/@types/async/-/async-2.0.42.tgz", + "integrity": "sha512-rmsnoIPcAHn9k0HtBktG0vFQIJlQQvLofo70pWly8itQzqG5c/ILSZqmXLsCdJpT1X5wpxO0F3rb3GB5xCiDAA==", + "dev": true + }, + "@types/bson": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/bson/-/bson-1.0.4.tgz", + "integrity": "sha512-/nysVvxwup1WniGHIM31UZXM+6727h4FAa2tZpFSQBooBcl2Bh1N9oQmVVg8QYnjchN/DOGi7UvVN0jpzWL6sw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/chai": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.0.4.tgz", + "integrity": "sha512-cvU0HomQ7/aGDQJZsbtJXqBQ7w4J4TqLB0Z/h8mKrpRjfeZEvTbygkfJEb7fWdmwpIeDeFmIVwAEqS0OYuUv3Q==", + "dev": true + }, + "@types/lodash": { + "version": "4.14.74", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.74.tgz", + "integrity": "sha512-BZknw3E/z3JmCLqQVANcR17okqVTPZdlxvcIz0fJiJVLUCbSH1hK3zs9r634PVSmrzAxN+n/fxlVRiYoArdOIQ==", + "dev": true + }, + "@types/mocha": { + "version": "2.2.43", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-2.2.43.tgz", + "integrity": "sha512-xNlAmH+lRJdUMXClMTI9Y0pRqIojdxfm7DHsIxoB2iTzu3fnPmSMEN8SsSx0cdwV36d02PWCWaDUoZPDSln+xw==", + "dev": true + }, + "@types/mongodb": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-2.2.11.tgz", + "integrity": "sha512-kksYkn34+ENpZqUvGYq0IGOOa4SOhFnMfqCepF789CP+QBgeWok9osTCiAStbc8n4Cv3QSXGmpB5T3PGzL50bQ==", + "dev": true, + "requires": { + "@types/bson": "*", + "@types/node": "*" + } + }, + "@types/node": { + "version": "6.0.88", + "resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.88.tgz", + "integrity": "sha512-bYDPZTX0/s1aihdjLuAgogUAT5M+TpoWChEMea2p0yOcfn5bu3k6cJb9cp6nw268XeSNIGGr+4+/8V5K6BGzLQ==", + "dev": true + }, + "@types/winston": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/winston/-/winston-0.0.30.tgz", + "integrity": "sha1-eyo5hwv/iWPbWHY/HlNnV6mssTQ=", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "abbrev": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", + "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", + "dev": true + }, + "accepts": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz", + "integrity": "sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=", + "requires": { + "mime-types": "~2.1.16", + "negotiator": "0.6.1" + } + }, + "acorn": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.1.2.tgz", + "integrity": "sha512-o96FZLJBPY1lvTuJylGA9Bk3t/GKPPJG8H0ydQQl01crzwJgspa4AEIq/pVTXigmK0PHVQhiAtn8WMBLL9D2WA==" + }, + "acorn-dynamic-import": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz", + "integrity": "sha1-x1K9IQvvZ5UBtsbLf8hPj0cVjMQ=", + "requires": { + "acorn": "^4.0.3" + }, + "dependencies": { + "acorn": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" + } + } + }, + "ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "requires": { + "co": "^4.6.0", + "json-stable-stringify": "^1.0.1" + } + }, + "ajv-keywords": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz", + "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=", + "requires": {} + }, + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "requires": { + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" + } + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "anymatch": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", + "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "requires": { + "micromatch": "^2.1.5", + "normalize-path": "^2.0.0" + } + }, + "argparse": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", + "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "requires": { + "arr-flatten": "^1.0.1" + } + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=" + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + }, + "asn1.js": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.9.1.tgz", + "integrity": "sha1-SLokC0WpKA6UdImQull9IWYX/UA=", + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "assert": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", + "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", + "requires": { + "util": "0.10.3" + } + }, + "assert-plus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=" + }, + "assertion-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.2.tgz", + "integrity": "sha1-E8pRXYYgbaC6xm6DTdOX2HWBCUw=", + "dev": true + }, + "async": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/async/-/async-2.5.0.tgz", + "integrity": "sha512-e+lJAJeNWuPCNyxZKOBdaJGyLGHugXVQtrAwtuAe2vhxTYxFTKE73p8JuTmdH0qdQZtDvI4dhJwjZc5zsfIsYw==", + "requires": { + "lodash": "^4.14.0" + } + }, + "async-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", + "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sdk": { + "version": "2.112.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.112.0.tgz", + "integrity": "sha1-mgSMDdWnQHoh9uhiNx0gSanPs1A=", + "requires": { + "buffer": "4.9.1", + "crypto-browserify": "1.0.9", + "events": "^1.1.1", + "jmespath": "0.15.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "uuid": "3.0.1", + "xml2js": "0.4.17", + "xmlbuilder": "4.2.1" + } + }, + "aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=" + }, + "aws4": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base64-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.1.tgz", + "integrity": "sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw==" + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "optional": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "big.js": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.1.3.tgz", + "integrity": "sha1-TK2iGTZS6zyp7I5VyQFWacmAaXg=" + }, + "binary-extensions": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.10.0.tgz", + "integrity": "sha1-muuabF6IY4qtFx4Wf1kAq+JINdA=" + }, + "bn.js": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" + }, + "body-parser": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.0.tgz", + "integrity": "sha1-07Ik1Gf6LOjUNYnAJFBDJnwJNjQ=", + "requires": { + "bytes": "3.0.0", + "content-type": "~1.0.2", + "debug": "2.6.8", + "depd": "~1.1.1", + "http-errors": "~1.6.2", + "iconv-lite": "0.4.18", + "on-finished": "~2.3.0", + "qs": "6.5.0", + "raw-body": "2.3.1", + "type-is": "~1.6.15" + } + }, + "boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "requires": { + "hoek": "2.x.x" + } + }, + "brace-expansion": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", + "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "requires": { + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" + }, + "browser-stdout": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", + "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", + "dev": true + }, + "browserify-aes": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.0.8.tgz", + "integrity": "sha512-WYCMOT/PtGTlpOKFht0YJFYcPy6pLCR98CtWfzK13zoynLlBMvAdEMSRGmgnJCw2M2j/5qxBkinZQFobieM8dQ==", + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "browserify-cipher": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.0.tgz", + "integrity": "sha1-mYgkSHS/XtTijalWZtzWasj8Njo=", + "requires": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "browserify-des": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.0.tgz", + "integrity": "sha1-2qJ3cXRwki7S/hhZQRihdUOXId0=", + "requires": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1" + } + }, + "browserify-rsa": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "requires": { + "bn.js": "^4.1.0", + "randombytes": "^2.0.1" + } + }, + "browserify-sign": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", + "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "requires": { + "bn.js": "^4.1.1", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.2", + "elliptic": "^6.0.0", + "inherits": "^2.0.1", + "parse-asn1": "^5.0.0" + } + }, + "browserify-zlib": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", + "integrity": "sha1-uzX4pRn2AOD6a4SFJByXnQFB+y0=", + "requires": { + "pako": "~0.2.0" + } + }, + "bson": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-1.0.4.tgz", + "integrity": "sha1-k8ENOeqltYQVy8QFLz5T5WKwtyw=" + }, + "buffer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "buffer-shims": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", + "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=" + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=" + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=" + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + }, + "callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" + }, + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=" + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "requires": { + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" + } + }, + "chai": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz", + "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=", + "dev": true, + "requires": { + "assertion-error": "^1.0.1", + "check-error": "^1.0.1", + "deep-eql": "^3.0.0", + "get-func-name": "^2.0.0", + "pathval": "^1.0.0", + "type-detect": "^4.0.0" + } + }, + "charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=", + "dev": true + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, + "chokidar": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "requires": { + "anymatch": "^1.3.0", + "async-each": "^1.0.0", + "fsevents": "^1.0.0", + "glob-parent": "^2.0.0", + "inherits": "^2.0.1", + "is-binary-path": "^1.0.0", + "is-glob": "^2.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.0.0" + } + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "requires": { + "center-align": "^0.1.1", + "right-align": "^0.1.1", + "wordwrap": "0.0.2" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=" + }, + "combined-stream": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", + "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", + "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "config": { + "version": "1.26.2", + "resolved": "https://registry.npmjs.org/config/-/config-1.26.2.tgz", + "integrity": "sha1-JGYpEWjYr64Kroq5nqTUJy9SDK4=", + "requires": { + "json5": "0.4.0", + "os-homedir": "1.0.2" + } + }, + "console-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "requires": { + "date-now": "^0.1.4" + } + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=" + }, + "content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" + }, + "content-type": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.3.tgz", + "integrity": "sha1-2hjvL7ZMpqzJBcxyAX0/OBhbkdE=" + }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cors": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.4.tgz", + "integrity": "sha1-K9OB8usgECAQXNUOpZ2mMJBpRoY=", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, + "create-ecdh": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.0.tgz", + "integrity": "sha1-iIxyNZbN92EvZJgjPuvXo1MBc30=", + "requires": { + "bn.js": "^4.1.0", + "elliptic": "^6.0.0" + } + }, + "create-hash": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.1.3.tgz", + "integrity": "sha1-YGBCrIuSYnUPSDyt2rD1gZFy2P0=", + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.6.tgz", + "integrity": "sha1-rLniIaThe9sHbpBlfEK5PjcmzwY=", + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=", + "dev": true + }, + "cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "requires": { + "boom": "2.x.x" + } + }, + "crypto-browserify": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-1.0.9.tgz", + "integrity": "sha1-zFRJaF37hesRyYKKzHy4erW7/MA=" + }, + "cycle": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", + "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=" + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "date-now": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=" + }, + "debug": { + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", + "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", + "requires": { + "ms": "2.0.0" + } + }, + "decache": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/decache/-/decache-4.3.0.tgz", + "integrity": "sha1-o5XkBwlWmKyKbe8B8qaky3Y49jU=", + "requires": { + "callsite": "^1.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "depd": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", + "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" + }, + "des.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", + "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", + "requires": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "diff": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz", + "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=", + "dev": true + }, + "diffie-hellman": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.2.tgz", + "integrity": "sha1-tYNXOScM/ias9jIJn97SoH8gnl4=", + "requires": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "domain-browser": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.1.7.tgz", + "integrity": "sha1-hnqksJP6oF8d4IwG9NeyH9+GmLw=" + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "optional": true, + "requires": { + "jsbn": "~0.1.0" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "elliptic": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz", + "integrity": "sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=", + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + } + }, + "emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=" + }, + "encodeurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz", + "integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA=" + }, + "enhanced-resolve": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz", + "integrity": "sha1-BCHjOf1xQZs9oT0Smzl5BAIwR24=", + "requires": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.4.0", + "object-assign": "^4.0.1", + "tapable": "^0.2.7" + } + }, + "errno": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.4.tgz", + "integrity": "sha1-uJbiOp5ei6M4cfyZar02NfyaHH0=", + "requires": { + "prr": "~0.0.0" + } + }, + "error-ex": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", + "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es6-promise": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.2.1.tgz", + "integrity": "sha1-7FYjOGgDKQkgcXDDlEjiREndH8Q=" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "escodegen": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", + "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", + "dev": true, + "requires": { + "esprima": "^2.7.1", + "estraverse": "^1.9.1", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.2.0" + }, + "dependencies": { + "source-map": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", + "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", + "dev": true, + "optional": true, + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true + }, + "estraverse": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", + "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "etag": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.0.tgz", + "integrity": "sha1-b2Ma7zNtbEY2K1F2QETOIWvjwFE=" + }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "requires": { + "is-posix-bracket": "^0.1.0" + } + }, + "expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "requires": { + "fill-range": "^2.1.0" + } + }, + "express": { + "version": "4.15.4", + "resolved": "https://registry.npmjs.org/express/-/express-4.15.4.tgz", + "integrity": "sha1-Ay4iU0ic+PzgJma+yj0R7XotrtE=", + "requires": { + "accepts": "~1.3.3", + "array-flatten": "1.1.1", + "content-disposition": "0.5.2", + "content-type": "~1.0.2", + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "debug": "2.6.8", + "depd": "~1.1.1", + "encodeurl": "~1.0.1", + "escape-html": "~1.0.3", + "etag": "~1.8.0", + "finalhandler": "~1.0.4", + "fresh": "0.5.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.1", + "path-to-regexp": "0.1.7", + "proxy-addr": "~1.1.5", + "qs": "6.5.0", + "range-parser": "~1.2.0", + "send": "0.15.4", + "serve-static": "1.12.4", + "setprototypeof": "1.0.3", + "statuses": "~1.3.1", + "type-is": "~1.6.15", + "utils-merge": "1.0.0", + "vary": "~1.1.1" + } + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "requires": { + "is-extglob": "^1.0.0" + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=" + }, + "fill-range": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz", + "integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=", + "requires": { + "is-number": "^2.1.0", + "isobject": "^2.0.0", + "randomatic": "^1.1.3", + "repeat-element": "^1.1.2", + "repeat-string": "^1.5.2" + } + }, + "finalhandler": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.0.4.tgz", + "integrity": "sha512-16l/r8RgzlXKmFOhZpHBztvye+lAhC5SU7hXavnerC9UfZqZxxXl3BzL8MhffPT3kF61lj9Oav2LKEzh0ei7tg==", + "requires": { + "debug": "2.6.8", + "encodeurl": "~1.0.1", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.1", + "statuses": "~1.3.1", + "unpipe": "~1.0.0" + } + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" + }, + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "requires": { + "for-in": "^1.0.1" + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.5", + "mime-types": "^2.1.12" + } + }, + "forwarded": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.1.tgz", + "integrity": "sha1-ik4wxkCwU5U5mjVJxzAldygEiWE=" + }, + "fresh": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.0.tgz", + "integrity": "sha1-9HTKXmqSRtb9jglTz6m5yAWvp44=" + }, + "fs-extra": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", + "integrity": "sha1-N5TzeMWLNC6n27sjCVEJxLO2IpE=", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^3.0.0", + "universalify": "^0.1.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fsevents": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.3.tgz", + "integrity": "sha512-WIr7iDkdmdbxu/Gh6eKEZJL6KPE74/5MEsf2whTOFNxbIoIixogroLdKYqB6FDav4Wavh/lZdzzd3b2KxIXC5Q==", + "optional": true, + "requires": { + "nan": "^2.3.0", + "node-pre-gyp": "^0.6.39" + }, + "dependencies": { + "abbrev": { + "version": "1.1.0", + "bundled": true, + "optional": true + }, + "ajv": { + "version": "4.11.8", + "bundled": true, + "optional": true, + "requires": { + "co": "^4.6.0", + "json-stable-stringify": "^1.0.1" + } + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "optional": true + }, + "aproba": { + "version": "1.1.1", + "bundled": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "bundled": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "asn1": { + "version": "0.2.3", + "bundled": true, + "optional": true + }, + "assert-plus": { + "version": "0.2.0", + "bundled": true, + "optional": true + }, + "asynckit": { + "version": "0.4.0", + "bundled": true, + "optional": true + }, + "aws-sign2": { + "version": "0.6.0", + "bundled": true, + "optional": true + }, + "aws4": { + "version": "1.6.0", + "bundled": true, + "optional": true + }, + "balanced-match": { + "version": "0.4.2", + "bundled": true, + "optional": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "bundled": true, + "optional": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "block-stream": { + "version": "0.0.9", + "bundled": true, + "optional": true, + "requires": { + "inherits": "~2.0.0" + } + }, + "boom": { + "version": "2.10.1", + "bundled": true, + "optional": true, + "requires": { + "hoek": "2.x.x" + } + }, + "brace-expansion": { + "version": "1.1.7", + "bundled": true, + "optional": true, + "requires": { + "balanced-match": "^0.4.1", + "concat-map": "0.0.1" + } + }, + "buffer-shims": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "caseless": { + "version": "0.12.0", + "bundled": true, + "optional": true + }, + "co": { + "version": "4.6.0", + "bundled": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "optional": true + }, + "combined-stream": { + "version": "1.0.5", + "bundled": true, + "optional": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "optional": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "optional": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "cryptiles": { + "version": "2.0.5", + "bundled": true, + "optional": true, + "requires": { + "boom": "2.x.x" + } + }, + "dashdash": { + "version": "1.14.1", + "bundled": true, + "optional": true, + "requires": { + "assert-plus": "^1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "optional": true + } + } + }, + "debug": { + "version": "2.6.8", + "bundled": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.4.2", + "bundled": true, + "optional": true + }, + "delayed-stream": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "ecc-jsbn": { + "version": "0.1.1", + "bundled": true, + "optional": true, + "requires": { + "jsbn": "~0.1.0" + } + }, + "extend": { + "version": "3.0.1", + "bundled": true, + "optional": true + }, + "extsprintf": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "forever-agent": { + "version": "0.6.1", + "bundled": true, + "optional": true + }, + "form-data": { + "version": "2.1.4", + "bundled": true, + "optional": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.5", + "mime-types": "^2.1.12" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "fstream": { + "version": "1.0.11", + "bundled": true, + "optional": true, + "requires": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + } + }, + "fstream-ignore": { + "version": "1.0.5", + "bundled": true, + "optional": true, + "requires": { + "fstream": "^1.0.0", + "inherits": "2", + "minimatch": "^3.0.0" + } + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "getpass": { + "version": "0.1.7", + "bundled": true, + "optional": true, + "requires": { + "assert-plus": "^1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "optional": true + } + } + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.1.11", + "bundled": true, + "optional": true + }, + "har-schema": { + "version": "1.0.5", + "bundled": true, + "optional": true + }, + "har-validator": { + "version": "4.2.1", + "bundled": true, + "optional": true, + "requires": { + "ajv": "^4.9.1", + "har-schema": "^1.0.5" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "optional": true + }, + "hawk": { + "version": "3.1.3", + "bundled": true, + "optional": true, + "requires": { + "boom": "2.x.x", + "cryptiles": "2.x.x", + "hoek": "2.x.x", + "sntp": "1.x.x" + } + }, + "hoek": { + "version": "2.16.3", + "bundled": true, + "optional": true + }, + "http-signature": { + "version": "1.1.1", + "bundled": true, + "optional": true, + "requires": { + "assert-plus": "^0.2.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "optional": true + }, + "ini": { + "version": "1.3.4", + "bundled": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "optional": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-typedarray": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "isstream": { + "version": "0.1.2", + "bundled": true, + "optional": true + }, + "jodid25519": { + "version": "1.0.2", + "bundled": true, + "optional": true, + "requires": { + "jsbn": "~0.1.0" + } + }, + "jsbn": { + "version": "0.1.1", + "bundled": true, + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "bundled": true, + "optional": true + }, + "json-stable-stringify": { + "version": "1.0.1", + "bundled": true, + "optional": true, + "requires": { + "jsonify": "~0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "bundled": true, + "optional": true + }, + "jsonify": { + "version": "0.0.0", + "bundled": true, + "optional": true + }, + "jsprim": { + "version": "1.4.0", + "bundled": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.0.2", + "json-schema": "0.2.3", + "verror": "1.3.6" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "optional": true + } + } + }, + "mime-db": { + "version": "1.27.0", + "bundled": true, + "optional": true + }, + "mime-types": { + "version": "2.1.15", + "bundled": true, + "optional": true, + "requires": { + "mime-db": "~1.27.0" + } + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "optional": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "optional": true + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "optional": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "node-pre-gyp": { + "version": "0.6.39", + "bundled": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "hawk": "3.1.3", + "mkdirp": "^0.5.1", + "nopt": "^4.0.1", + "npmlog": "^4.0.2", + "rc": "^1.1.7", + "request": "2.81.0", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^2.2.1", + "tar-pack": "^3.4.0" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npmlog": { + "version": "4.1.0", + "bundled": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "optional": true + }, + "oauth-sign": { + "version": "0.8.2", + "bundled": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "optional": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "osenv": { + "version": "0.1.4", + "bundled": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "optional": true + }, + "performance-now": { + "version": "0.2.0", + "bundled": true, + "optional": true + }, + "process-nextick-args": { + "version": "1.0.7", + "bundled": true, + "optional": true + }, + "punycode": { + "version": "1.4.1", + "bundled": true, + "optional": true + }, + "qs": { + "version": "6.4.0", + "bundled": true, + "optional": true + }, + "rc": { + "version": "1.2.1", + "bundled": true, + "optional": true, + "requires": { + "deep-extend": "~0.4.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.2.9", + "bundled": true, + "optional": true, + "requires": { + "buffer-shims": "~1.0.0", + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "string_decoder": "~1.0.0", + "util-deprecate": "~1.0.1" + } + }, + "request": { + "version": "2.81.0", + "bundled": true, + "optional": true, + "requires": { + "aws-sign2": "~0.6.0", + "aws4": "^1.2.1", + "caseless": "~0.12.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.0", + "forever-agent": "~0.6.1", + "form-data": "~2.1.1", + "har-validator": "~4.2.1", + "hawk": "~3.1.3", + "http-signature": "~1.1.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.7", + "oauth-sign": "~0.8.1", + "performance-now": "^0.2.0", + "qs": "~6.4.0", + "safe-buffer": "^5.0.1", + "stringstream": "~0.0.4", + "tough-cookie": "~2.3.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.0.0" + } + }, + "rimraf": { + "version": "2.6.1", + "bundled": true, + "optional": true, + "requires": { + "glob": "^7.0.5" + } + }, + "safe-buffer": { + "version": "5.0.1", + "bundled": true, + "optional": true + }, + "semver": { + "version": "5.3.0", + "bundled": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "optional": true + }, + "sntp": { + "version": "1.0.9", + "bundled": true, + "optional": true, + "requires": { + "hoek": "2.x.x" + } + }, + "sshpk": { + "version": "1.13.0", + "bundled": true, + "optional": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jodid25519": "^1.0.0", + "jsbn": "~0.1.0", + "tweetnacl": "~0.14.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "optional": true + } + } + }, + "string_decoder": { + "version": "1.0.1", + "bundled": true, + "optional": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "optional": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "stringstream": { + "version": "0.0.5", + "bundled": true, + "optional": true + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "optional": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "optional": true + }, + "tar": { + "version": "2.2.1", + "bundled": true, + "optional": true, + "requires": { + "block-stream": "*", + "fstream": "^1.0.2", + "inherits": "2" + } + }, + "tar-pack": { + "version": "3.4.0", + "bundled": true, + "optional": true, + "requires": { + "debug": "^2.2.0", + "fstream": "^1.0.10", + "fstream-ignore": "^1.0.5", + "once": "^1.3.3", + "readable-stream": "^2.1.4", + "rimraf": "^2.5.1", + "tar": "^2.2.1", + "uid-number": "^0.0.6" + } + }, + "tough-cookie": { + "version": "2.3.2", + "bundled": true, + "optional": true, + "requires": { + "punycode": "^1.4.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "bundled": true, + "optional": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "bundled": true, + "optional": true + }, + "uid-number": { + "version": "0.0.6", + "bundled": true, + "optional": true + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "uuid": { + "version": "3.0.1", + "bundled": true, + "optional": true + }, + "verror": { + "version": "1.3.6", + "bundled": true, + "optional": true, + "requires": { + "extsprintf": "1.0.2" + } + }, + "wide-align": { + "version": "1.1.2", + "bundled": true, + "optional": true, + "requires": { + "string-width": "^1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "optional": true + } + } + }, + "get-caller-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", + "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=" + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "requires": { + "glob-parent": "^2.0.0", + "is-glob": "^2.0.0" + } + }, + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "requires": { + "is-glob": "^2.0.0" + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + }, + "graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", + "dev": true + }, + "growl": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", + "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=", + "dev": true + }, + "handlebars": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.10.tgz", + "integrity": "sha1-PTDHGLCaPZbyPqTMH0A8TTup/08=", + "dev": true, + "requires": { + "async": "^1.4.0", + "optimist": "^0.6.1", + "source-map": "^0.4.4", + "uglify-js": "^2.6" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "har-schema": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", + "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=" + }, + "har-validator": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", + "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", + "requires": { + "ajv": "^4.9.1", + "har-schema": "^1.0.5" + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "hash-base": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz", + "integrity": "sha1-ZuodhW206KVHDK32/OI65SRO8uE=", + "requires": { + "inherits": "^2.0.1" + } + }, + "hash.js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.3.tgz", + "integrity": "sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==", + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.0" + } + }, + "hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "requires": { + "boom": "2.x.x", + "cryptiles": "2.x.x", + "hoek": "2.x.x", + "sntp": "1.x.x" + } + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" + }, + "hosted-git-info": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", + "integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==" + }, + "http-errors": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", + "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", + "requires": { + "depd": "1.1.1", + "inherits": "2.0.3", + "setprototypeof": "1.0.3", + "statuses": ">= 1.3.1 < 2" + } + }, + "http-signature": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "requires": { + "assert-plus": "^0.2.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "https-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-0.0.1.tgz", + "integrity": "sha1-P5E2XKvmC3ftDruiS0VOPgnZWoI=" + }, + "iconv-lite": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.18.tgz", + "integrity": "sha512-sr1ZQph3UwHTR0XftSbK85OvBbxe/abLGzEnPENCQwmHf7sck8Oyu4ob3LgBxWWxRoM+QszeUyl7jbqapu2TqA==" + }, + "ieee754": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", + "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=" + }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "interpret": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.0.3.tgz", + "integrity": "sha1-y8NcYu7uc/Gat7EKgBURQBr8D5A=" + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" + }, + "ipaddr.js": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.4.0.tgz", + "integrity": "sha1-KWrKh4qCGBbluF0KKFqZvP9FgvA=" + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-buffer": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz", + "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=" + }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "requires": { + "builtin-modules": "^1.0.0" + } + }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=" + }, + "is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "requires": { + "is-primitive": "^2.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=" + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "requires": { + "is-extglob": "^1.0.0" + } + }, + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=" + }, + "is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=" + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "requires": { + "isarray": "1.0.0" + } + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "istanbul": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", + "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", + "dev": true, + "requires": { + "abbrev": "1.0.x", + "async": "1.x", + "escodegen": "1.8.x", + "esprima": "2.7.x", + "glob": "^5.0.15", + "handlebars": "^4.0.1", + "js-yaml": "3.x", + "mkdirp": "0.5.x", + "nopt": "3.x", + "once": "1.x", + "resolve": "1.1.x", + "supports-color": "^3.1.0", + "which": "^1.1.1", + "wordwrap": "^1.0.0" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + } + } + }, + "jmespath": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", + "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" + }, + "js-yaml": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz", + "integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "dependencies": { + "esprima": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", + "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", + "dev": true + } + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, + "json-loader": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/json-loader/-/json-loader-0.5.7.tgz", + "integrity": "sha512-QLPs8Dj7lnf3e3QYS1zkCo+4ZwqOiF9d/nZnYozTISxXWCfNs9yuky5rJw4/W34s7POaNlbZmQGaB5NiXCbP4w==" + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "requires": { + "jsonify": "~0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "json3": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", + "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", + "dev": true + }, + "json5": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.4.0.tgz", + "integrity": "sha1-BUNS5MTIDIbAkjh31EneF2pzLI0=" + }, + "jsonfile": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", + "integrity": "sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=", + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "jszip": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-2.5.0.tgz", + "integrity": "sha1-dET9hVHd8+XacZj+oMkbyDCMwnQ=", + "requires": { + "pako": "~0.2.5" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=" + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "requires": { + "invert-kv": "^1.0.0" + } + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + } + }, + "loader-runner": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.3.0.tgz", + "integrity": "sha1-9IKuqC1UPgeSFwDVpG7yb9rGuKI=" + }, + "loader-utils": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", + "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", + "requires": { + "big.js": "^3.1.3", + "emojis-list": "^2.0.0", + "json5": "^0.5.0", + "object-assign": "^4.0.1" + }, + "dependencies": { + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=" + } + } + }, + "lodash": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" + }, + "lodash._baseassign": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", + "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", + "dev": true, + "requires": { + "lodash._basecopy": "^3.0.0", + "lodash.keys": "^3.0.0" + } + }, + "lodash._basecopy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", + "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", + "dev": true + }, + "lodash._basecreate": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz", + "integrity": "sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE=", + "dev": true + }, + "lodash._getnative": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", + "dev": true + }, + "lodash._isiterateecall": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", + "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", + "dev": true + }, + "lodash.create": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz", + "integrity": "sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=", + "dev": true, + "requires": { + "lodash._baseassign": "^3.0.0", + "lodash._basecreate": "^3.0.0", + "lodash._isiterateecall": "^3.0.0" + } + }, + "lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", + "dev": true + }, + "lodash.isarray": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", + "dev": true + }, + "lodash.keys": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", + "dev": true, + "requires": { + "lodash._getnative": "^3.0.0", + "lodash.isarguments": "^3.0.0", + "lodash.isarray": "^3.0.0" + } + }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" + }, + "md5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz", + "integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=", + "dev": true, + "requires": { + "charenc": "~0.0.1", + "crypt": "~0.0.1", + "is-buffer": "~1.1.1" + } + }, + "md5.js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", + "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + }, + "dependencies": { + "hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + } + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "requires": { + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" + } + }, + "miller-rabin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.0.tgz", + "integrity": "sha1-SmL7HUKTPAVYOYL0xxb2+55sbT0=", + "requires": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + } + }, + "mime": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz", + "integrity": "sha1-EV+eO2s9rylZmDyzjxSaLUDrXVM=" + }, + "mime-db": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", + "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" + }, + "mime-types": { + "version": "2.1.17", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", + "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", + "requires": { + "mime-db": "~1.30.0" + } + }, + "minimalistic-assert": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz", + "integrity": "sha1-cCvi3aazf0g2vLP121ZkG2Sh09M=" + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } + }, + "mocha": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-3.5.2.tgz", + "integrity": "sha512-iH5zl7afRZl1GvD0pnrRlazgc9Z/o34pXWmTFi8xNIMFKXgNL1SoBTDDb9sJfbV/sJV/j8X+0gvwY1QS1He7Nw==", + "dev": true, + "requires": { + "browser-stdout": "1.3.0", + "commander": "2.9.0", + "debug": "2.6.8", + "diff": "3.2.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.1", + "growl": "1.9.2", + "he": "1.1.1", + "json3": "3.3.2", + "lodash.create": "3.1.1", + "mkdirp": "0.5.1", + "supports-color": "3.1.2" + }, + "dependencies": { + "commander": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", + "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", + "dev": true, + "requires": { + "graceful-readlink": ">= 1.0.0" + } + }, + "glob": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz", + "integrity": "sha1-gFIR3wT6rxxjo2ADBs31reULLsg=", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.2", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "supports-color": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz", + "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=", + "dev": true, + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "mocha-junit-reporter": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/mocha-junit-reporter/-/mocha-junit-reporter-1.15.0.tgz", + "integrity": "sha1-MJ9LeiD82ibQrWnJt9CAjXcjAsI=", + "dev": true, + "requires": { + "debug": "^2.2.0", + "md5": "^2.1.0", + "mkdirp": "~0.5.1", + "xml": "^1.0.0" + } + }, + "mongodb": { + "version": "2.2.31", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.2.31.tgz", + "integrity": "sha1-GUBEXGYeGSF7s7+CRdmFSq71SNs=", + "requires": { + "es6-promise": "3.2.1", + "mongodb-core": "2.1.15", + "readable-stream": "2.2.7" + }, + "dependencies": { + "readable-stream": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.7.tgz", + "integrity": "sha1-BwV6y+JGeyIELTb5jFrVBwVOlbE=", + "requires": { + "buffer-shims": "~1.0.0", + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "string_decoder": "~1.0.0", + "util-deprecate": "~1.0.1" + } + } + } + }, + "mongodb-core": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-2.1.15.tgz", + "integrity": "sha1-hB9TuH//9MdFgYnDXIroJ+EWl2Q=", + "requires": { + "bson": "~1.0.4", + "require_optional": "~1.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "nan": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz", + "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=", + "optional": true + }, + "negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" + }, + "node-libs-browser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.0.0.tgz", + "integrity": "sha1-o6WeyXAkmFtG6Vg3lkb5bEthZkY=", + "requires": { + "assert": "^1.1.1", + "browserify-zlib": "^0.1.4", + "buffer": "^4.3.0", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.11.0", + "domain-browser": "^1.1.1", + "events": "^1.0.0", + "https-browserify": "0.0.1", + "os-browserify": "^0.2.0", + "path-browserify": "0.0.0", + "process": "^0.11.0", + "punycode": "^1.2.4", + "querystring-es3": "^0.2.0", + "readable-stream": "^2.0.5", + "stream-browserify": "^2.0.1", + "stream-http": "^2.3.1", + "string_decoder": "^0.10.25", + "timers-browserify": "^2.0.2", + "tty-browserify": "0.0.0", + "url": "^0.11.0", + "util": "^0.10.3", + "vm-browserify": "0.0.4" + }, + "dependencies": { + "crypto-browserify": { + "version": "3.11.1", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.11.1.tgz", + "integrity": "sha512-Na7ZlwCOqoaW5RwUK1WpXws2kv8mNhWdTlzob0UXulk6G9BDbyiJaGTYBIX61Ozn9l1EPPJpICZb4DaOpT9NlQ==", + "requires": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + } + } + }, + "node-zip": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/node-zip/-/node-zip-1.1.1.tgz", + "integrity": "sha1-lNGtZ0o81GoViN1zb0qaeMdX62I=", + "requires": { + "jszip": "2.5.0" + } + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "requires": { + "hosted-git-info": "^2.1.4", + "is-builtin-module": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "requires": { + "for-own": "^0.1.4", + "is-extendable": "^0.1.1" + } + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "requires": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.4", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "wordwrap": "~1.0.0" + }, + "dependencies": { + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + } + } + }, + "os-browserify": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.2.1.tgz", + "integrity": "sha1-Y/xMzuXS13Y9Jrv4YBB45sLgBE8=" + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + }, + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "requires": { + "lcid": "^1.0.0" + } + }, + "pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=" + }, + "parse-asn1": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.0.tgz", + "integrity": "sha1-N8T5t+06tlx0gXtfJICTf7+XxxI=", + "requires": { + "asn1.js": "^4.0.0", + "browserify-aes": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3" + } + }, + "parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "requires": { + "glob-base": "^0.3.0", + "is-dotfile": "^1.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.0" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "requires": { + "error-ex": "^1.2.0" + } + }, + "parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" + }, + "path-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", + "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=" + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "requires": { + "pinkie-promise": "^2.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, + "pbkdf2": { + "version": "3.0.14", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.14.tgz", + "integrity": "sha512-gjsZW9O34fm0R7PaLHRJmLLVfSoesxztjPjE9o6R+qtVJij90ltg1joIovN9GKrRW3t1PzhDDG3UMEMFfZ+1wA==", + "requires": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "performance-now": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", + "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=" + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "requires": { + "pinkie": "^2.0.0" + } + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=" + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + }, + "proxy-addr": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.1.5.tgz", + "integrity": "sha1-ccDuOxAt4/IC87ZPYI0XP8uhqRg=", + "requires": { + "forwarded": "~0.1.0", + "ipaddr.js": "1.4.0" + } + }, + "prr": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/prr/-/prr-0.0.0.tgz", + "integrity": "sha1-GoS4WQgyVQFBGFPQCB7j+obikmo=" + }, + "public-encrypt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.0.tgz", + "integrity": "sha1-OfaZ86RlYN1eusvKaTyvfGXBjMY=", + "requires": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1" + } + }, + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + }, + "qs": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.0.tgz", + "integrity": "sha512-fjVFjW9yhqMhVGwRExCXLhJKrLlkYSaxNWdyc9rmHlrVZbk35YHH312dFd7191uQeXkI3mKLZTIbSvIeFwFemg==" + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=" + }, + "randomatic": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", + "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==", + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "randombytes": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.5.tgz", + "integrity": "sha512-8T7Zn1AhMsQ/HI1SjcCfT/t4ii3eAqco3yOcSzS4mozsOz69lHLsoMXmF9nZgnFanYscnSlUSgs8uZyKzpE6kg==", + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" + }, + "raw-body": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.1.tgz", + "integrity": "sha512-sxkd1uqaSj41SG5Vet9sNAxBMCMsmZ3LVhRkDlK8SbCpelTUB7JiMGHG70AZS6cFiCRgfNQhU2eLnTHYRFf7LA==", + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.2", + "iconv-lite": "0.4.18", + "unpipe": "1.0.0" + } + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + } + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.0.3", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", + "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", + "requires": { + "graceful-fs": "^4.1.2", + "minimatch": "^3.0.2", + "readable-stream": "^2.0.2", + "set-immediate-shim": "^1.0.1" + } + }, + "reflect-metadata": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.10.tgz", + "integrity": "sha1-tPg3BEFqytiZiMmxVjXUfgO5NEo=" + }, + "regex-cache": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", + "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "requires": { + "is-equal-shallow": "^0.1.3" + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" + }, + "repeat-element": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=" + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + }, + "request": { + "version": "2.81.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", + "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", + "requires": { + "aws-sign2": "~0.6.0", + "aws4": "^1.2.1", + "caseless": "~0.12.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.0", + "forever-agent": "~0.6.1", + "form-data": "~2.1.1", + "har-validator": "~4.2.1", + "hawk": "~3.1.3", + "http-signature": "~1.1.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.7", + "oauth-sign": "~0.8.1", + "performance-now": "^0.2.0", + "qs": "~6.4.0", + "safe-buffer": "^5.0.1", + "stringstream": "~0.0.4", + "tough-cookie": "~2.3.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.0.0" + }, + "dependencies": { + "qs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", + "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=" + } + } + }, + "require_optional": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", + "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", + "requires": { + "resolve-from": "^2.0.0", + "semver": "^5.1.0" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" + }, + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + }, + "resolve-from": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", + "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" + }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "requires": { + "align-text": "^0.1.1" + } + }, + "ripemd160": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz", + "integrity": "sha1-D0WEKVxTo2KK9+bXmsohzlfRxuc=", + "requires": { + "hash-base": "^2.0.0", + "inherits": "^2.0.1" + } + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + }, + "sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" + }, + "semver": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", + "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" + }, + "send": { + "version": "0.15.4", + "resolved": "https://registry.npmjs.org/send/-/send-0.15.4.tgz", + "integrity": "sha1-mF+qPihLAnPHkzZKNcZze9k5Bbk=", + "requires": { + "debug": "2.6.8", + "depd": "~1.1.1", + "destroy": "~1.0.4", + "encodeurl": "~1.0.1", + "escape-html": "~1.0.3", + "etag": "~1.8.0", + "fresh": "0.5.0", + "http-errors": "~1.6.2", + "mime": "1.3.4", + "ms": "2.0.0", + "on-finished": "~2.3.0", + "range-parser": "~1.2.0", + "statuses": "~1.3.1" + } + }, + "serve-static": { + "version": "1.12.4", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.12.4.tgz", + "integrity": "sha1-m2qpjutyU8Tu3Ewfb9vKYJkBqWE=", + "requires": { + "encodeurl": "~1.0.1", + "escape-html": "~1.0.3", + "parseurl": "~1.3.1", + "send": "0.15.4" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=" + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" + }, + "setprototypeof": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" + }, + "sha.js": { + "version": "2.4.8", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.8.tgz", + "integrity": "sha1-NwaMLEdra69ALRSknGf1l5IfY08=", + "requires": { + "inherits": "^2.0.1" + } + }, + "sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "requires": { + "hoek": "2.x.x" + } + }, + "source-list-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.0.tgz", + "integrity": "sha512-I2UmuJSRr/T8jisiROLU3A3ltr+swpniSmNPI4Ml3ZCX6tVnDsuZzK7F2hl5jTqbZBWCEKlj5HRQiPExXLgE8A==" + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "spdx-correct": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz", + "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=", + "requires": { + "spdx-license-ids": "^1.0.2" + } + }, + "spdx-expression-parse": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz", + "integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=" + }, + "spdx-license-ids": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz", + "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=" + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "sshpk": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", + "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "tweetnacl": "~0.14.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" + }, + "statuses": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" + }, + "stream-browserify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", + "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", + "requires": { + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" + } + }, + "stream-http": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.7.2.tgz", + "integrity": "sha512-c0yTD2rbQzXtSsFSVhtpvY/vS6u066PcXOX9kBB3mSO76RiUQzL340uJkGBWnlBg4/HZzqiUXtaVA7wcRcJgEw==", + "requires": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.2.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "requires": { + "is-utf8": "^0.2.0" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + }, + "tapable": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.2.8.tgz", + "integrity": "sha1-mTcqXJmb8t8WCvwNdL7U9HlIzSI=" + }, + "timers-browserify": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.4.tgz", + "integrity": "sha512-uZYhyU3EX8O7HQP+J9fTVYwsq90Vr68xPEFo7yrVImIxYvHgukBEgOB/SgGoorWVTzGM/3Z+wUNnboA4M8jWrg==", + "requires": { + "setimmediate": "^1.0.4" + } + }, + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=" + }, + "tough-cookie": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz", + "integrity": "sha1-8IH3bkyFcg5sN6X6ztc3FQ2EByo=", + "requires": { + "punycode": "^1.4.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + } + } + }, + "tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=" + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-detect": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.3.tgz", + "integrity": "sha1-Dj8mcLRAmbC0bChNE2p+9Jx0wuo=", + "dev": true + }, + "type-is": { + "version": "1.6.15", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", + "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.15" + } + }, + "typescript": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.5.2.tgz", + "integrity": "sha1-A4qV99m7tCCxvzW6MdTFwd0//jQ=", + "dev": true + }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "requires": { + "source-map": "~0.5.1", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.10.0" + }, + "dependencies": { + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "requires": { + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", + "window-size": "0.1.0" + } + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "optional": true + }, + "universalify": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz", + "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=" + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "requires": { + "inherits": "2.0.1" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" + } + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "utils-merge": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz", + "integrity": "sha1-ApT7kiu5N1FTVBxPcJYjHyh8ivg=" + }, + "uuid": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.1.tgz", + "integrity": "sha1-ZUS7ot/ajBzxfmKaOjBeK7H+5sE=" + }, + "validate-npm-package-license": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz", + "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=", + "requires": { + "spdx-correct": "~1.0.0", + "spdx-expression-parse": "~1.0.0" + } + }, + "vary": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.1.tgz", + "integrity": "sha1-Z1Neu2lMHVIldFeYRmUyP1h+jTc=" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "vm-browserify": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", + "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", + "requires": { + "indexof": "0.0.1" + } + }, + "watchpack": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.4.0.tgz", + "integrity": "sha1-ShRyvLuVK9Cpu0A2gB+VTfs5+qw=", + "requires": { + "async": "^2.1.2", + "chokidar": "^1.7.0", + "graceful-fs": "^4.1.2" + } + }, + "webpack": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-2.7.0.tgz", + "integrity": "sha512-MjAA0ZqO1ba7ZQJRnoCdbM56mmFpipOPUv/vQpwwfSI42p5PVDdoiuK2AL2FwFUVgT859Jr43bFZXRg/LNsqvg==", + "requires": { + "acorn": "^5.0.0", + "acorn-dynamic-import": "^2.0.0", + "ajv": "^4.7.0", + "ajv-keywords": "^1.1.1", + "async": "^2.1.2", + "enhanced-resolve": "^3.3.0", + "interpret": "^1.0.0", + "json-loader": "^0.5.4", + "json5": "^0.5.1", + "loader-runner": "^2.3.0", + "loader-utils": "^0.2.16", + "memory-fs": "~0.4.1", + "mkdirp": "~0.5.0", + "node-libs-browser": "^2.0.0", + "source-map": "^0.5.3", + "supports-color": "^3.1.0", + "tapable": "~0.2.5", + "uglify-js": "^2.8.27", + "watchpack": "^1.3.1", + "webpack-sources": "^1.0.1", + "yargs": "^6.0.0" + }, + "dependencies": { + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=" + } + } + }, + "webpack-sources": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.0.1.tgz", + "integrity": "sha512-05tMxipUCwHqYaVS8xc7sYPTly8PzXayRCB4dTxLhWTqlKUiwH6ezmEe0OSreL1c30LAuA3Zqmc+uEBUGFJDjw==", + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.5.3" + } + }, + "which": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", + "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", + "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=" + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=" + }, + "winston": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/winston/-/winston-2.3.1.tgz", + "integrity": "sha1-C0hCDZeMAYBM8CMLZIhhWYIloRk=", + "requires": { + "async": "~1.0.0", + "colors": "1.0.x", + "cycle": "1.0.x", + "eyes": "0.1.x", + "isstream": "0.1.x", + "stack-trace": "0.0.x" + }, + "dependencies": { + "async": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz", + "integrity": "sha1-+PwEyjoTeErenhZBr5hXjPvWR6k=" + } + } + }, + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=" + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=", + "dev": true + }, + "xml2js": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.17.tgz", + "integrity": "sha1-F76T6q4/O3eTWceVtBlwWogX6Gg=", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "^4.1.0" + } + }, + "xmlbuilder": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-4.2.1.tgz", + "integrity": "sha1-qlijBBoGb5DqoWwvU4n/GfP0YaU=", + "requires": { + "lodash": "^4.0.0" + } + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" + }, + "yamljs": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.2.10.tgz", + "integrity": "sha1-SBzHwlynOvWfWR8MluPOVsdXpA8=", + "requires": { + "argparse": "^1.0.7", + "glob": "^7.0.5" + } + }, + "yargs": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz", + "integrity": "sha1-eC7CHvQDNF+DCoCMo9UTr1YGUgg=", + "requires": { + "camelcase": "^3.0.0", + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "os-locale": "^1.4.0", + "read-pkg-up": "^1.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^1.0.2", + "which-module": "^1.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^4.2.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=" + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + } + } + } + }, + "yargs-parser": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-4.2.1.tgz", + "integrity": "sha1-KczqwNxPA8bIe0qfIX3RjJ90hxw=", + "requires": { + "camelcase": "^3.0.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=" + } + } + } + } +} diff --git a/package.json b/package.json index 17f199e..7f3b876 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "functionly", - "version": "0.0.3", + "version": "0.6.3", "description": "", "main": "lib/src/index", "types": "lib/src/index.d.ts", @@ -9,7 +9,8 @@ "build:watch": "node_modules/.bin/tsc -w", "test": "npm run build && mocha --recursive lib/test/**/*.tests.js", "test:fast": "mocha --recursive lib/test/**/*.tests.js", - "metadata": "node ./lib/src/cli/cli.js metadata aws --" + "metadata": "node ./lib/src/cli/cli.js metadata aws --", + "coverage": "node_modules/.bin/istanbul cover --report cobertura ./node_modules/mocha/bin/_mocha -- -R spec lib/test/**/*.tests.js" }, "repository": { "type": "git", @@ -23,13 +24,16 @@ "homepage": "https://github.com/jaystack/functionly#readme", "devDependencies": { "@types/async": "^2.0.33", - "@types/chai": "^3.4.34", + "@types/chai": "^4.0.1", "@types/lodash": "^4.14.38", - "@types/mocha": "^2.2.33", + "@types/mocha": "^2.2.41", + "@types/mongodb": "^2.2.11", "@types/node": "^6.0.46", "@types/winston": "0.0.30", - "chai": "^3.5.0", - "mocha": "^3.2.0", + "chai": "^4.0.2", + "istanbul": "^0.4.5", + "mocha": "^3.4.2", + "mocha-junit-reporter": "^1.15.0", "typescript": "^2.3.0" }, "dependencies": { @@ -39,21 +43,19 @@ "commander": "^2.9.0", "config": "^1.26.1", "cors": "^2.8.3", + "decache": "^4.3.0", "express": "^4.15.2", + "fs-extra": "^3.0.1", "lodash": "^4.14.1", + "mongodb": "^2.2.31", "node-zip": "^1.1.1", "reflect-metadata": "^0.1.10", "request": "^2.81.0", "webpack": "^2.5.1", - "winston": "^2.3.0" + "winston": "^2.3.0", + "yamljs": "^0.2.10" }, "bin": { "functionly": "./lib/src/cli/cli.js" - }, - "CloudFormation": { - "StackName": "TestStack", - "OnFailure": "ROLLBACK", - "TimeoutInMinutes": 10, - "Capabilities": ["CAPABILITY_NAMED_IAM"] } -} \ No newline at end of file +} diff --git a/src/annotations/classes/apiGateway.ts b/src/annotations/classes/apiGateway.ts deleted file mode 100644 index 6942375..0000000 --- a/src/annotations/classes/apiGateway.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { CLASS_APIGATEWAYKEY } from '../constants' -import { getMetadata, defineMetadata } from '../metadata' -import { defaults } from 'lodash' - -export const defaultEndpoint = { - method: 'get', - cors: false -} - -export const apiGateway = (endpoint: { path: string, method?: string, cors?: boolean }) => { - return (target: Function) => { - let metadata = getMetadata(CLASS_APIGATEWAYKEY, target) || [] - metadata.push(defaults({}, endpoint, defaultEndpoint)) - defineMetadata(CLASS_APIGATEWAYKEY, [...metadata], target); - } -} diff --git a/src/annotations/classes/aws/apiGateway.ts b/src/annotations/classes/aws/apiGateway.ts new file mode 100644 index 0000000..8b64d1c --- /dev/null +++ b/src/annotations/classes/aws/apiGateway.ts @@ -0,0 +1,33 @@ +import { CLASS_APIGATEWAYKEY } from '../../constants' +import { getMetadata, defineMetadata } from '../../metadata' +import { rest, CorsConfig } from '../rest' + +export const defaultEndpoint = { + method: 'get', + cors: true, + authorization: 'NONE' +} + +export const apiGateway = (endpoint: { + path: string, method?: string, cors?: boolean, corsConfig?: CorsConfig, + authorization?: 'AWS_IAM' | 'NONE' | 'CUSTOM' | 'COGNITO_USER_POOLS' +}) => { + return (target: Function) => { + let metadata = getMetadata(CLASS_APIGATEWAYKEY, target) || [] + metadata.push({ ...defaultEndpoint, ...endpoint }) + defineMetadata(CLASS_APIGATEWAYKEY, [...metadata], target); + } +} + +rest.extension('aws', (target, config) => { + for(const method of config.methods){ + const decorator = apiGateway({ + path: config.path, + method, + cors: config.cors, + corsConfig: config.corsConfig, + authorization: config.authenticated ? 'AWS_IAM' : 'NONE' + }) + decorator(target) + } +}) \ No newline at end of file diff --git a/src/annotations/classes/aws/aws.ts b/src/annotations/classes/aws/aws.ts new file mode 100644 index 0000000..9241e20 --- /dev/null +++ b/src/annotations/classes/aws/aws.ts @@ -0,0 +1,18 @@ +import { CLASS_AWSMEMORYSIZEKEY, CLASS_AWSTIMEOUTKEY, CLASS_AWSRUNTIMEKEY } from '../../constants' +import { defineMetadata } from '../../metadata' + +export const aws = (config: { + type?: 'nodejs16.x' | 'nodejs18.x' | 'nodejs20.x', + memorySize?: number, + timeout?: number +}) => (target: Function) => { + if (typeof config.type === 'string') { + defineMetadata(CLASS_AWSRUNTIMEKEY, config.type, target); + } + if (typeof config.memorySize === 'number') { + defineMetadata(CLASS_AWSMEMORYSIZEKEY, config.memorySize, target); + } + if (typeof config.timeout === 'number') { + defineMetadata(CLASS_AWSTIMEOUTKEY, config.timeout, target); + } +} diff --git a/src/annotations/classes/aws/cloudFormation.ts b/src/annotations/classes/aws/cloudFormation.ts new file mode 100644 index 0000000..9312d64 --- /dev/null +++ b/src/annotations/classes/aws/cloudFormation.ts @@ -0,0 +1,18 @@ +import { CLASS_CLOUDFORMATION } from '../../constants'; +import { getMetadata, defineMetadata } from '../../metadata'; +import { rest, CorsConfig } from '../rest'; + +export const defaultConfig = { + stack: null, + resourceName: null +}; + +export const cloudFormation = (config?: { stack?: string; resourceName?: string }) => { + return (target: Function) => { + let metadata = getMetadata(CLASS_CLOUDFORMATION, target); + if (!metadata) { + metadata = { ...defaultConfig }; + } + defineMetadata(CLASS_CLOUDFORMATION, { ...metadata, ...config }, target); + }; +}; diff --git a/src/annotations/classes/aws/cloudWatchEvent.ts b/src/annotations/classes/aws/cloudWatchEvent.ts new file mode 100644 index 0000000..37c6cd4 --- /dev/null +++ b/src/annotations/classes/aws/cloudWatchEvent.ts @@ -0,0 +1,14 @@ +import { CLASS_CLOUDWATCHEVENT } from '../../constants' +import { getMetadata, defineMetadata } from '../../metadata' +import { rest } from '../rest' + +export const cloudWatchEvent = (config: { scheduleExpression?: string, eventPattern?: any }) => { + return (target: Function) => { + let metadata = getMetadata(CLASS_CLOUDWATCHEVENT, target) || [] + metadata.push({ + ...config, + definedBy: target.name + }) + defineMetadata(CLASS_CLOUDWATCHEVENT, [...metadata], target); + } +} \ No newline at end of file diff --git a/src/annotations/classes/azure/azure.ts b/src/annotations/classes/azure/azure.ts new file mode 100644 index 0000000..eca9e28 --- /dev/null +++ b/src/annotations/classes/azure/azure.ts @@ -0,0 +1,10 @@ +import { CLASS_AZURENODEKEY } from '../../constants' +import { defineMetadata } from '../../metadata' + +export const azure = (config: { + node?: '6.5.0' +}) => (target: Function) => { + if (typeof config.node === 'string') { + defineMetadata(CLASS_AZURENODEKEY, config.node, target); + } +} \ No newline at end of file diff --git a/src/annotations/classes/azure/httpTrigger.ts b/src/annotations/classes/azure/httpTrigger.ts new file mode 100644 index 0000000..94b902d --- /dev/null +++ b/src/annotations/classes/azure/httpTrigger.ts @@ -0,0 +1,34 @@ +import { CLASS_HTTPTRIGGER } from '../../constants' +import { getMetadata, defineMetadata } from '../../metadata' +import { rest, CorsConfig } from '../rest' + +export const defaultEndpoint = { + methods: ['get'], + cors: true, + authLevel: 'anonymous' +} + +export const httpTrigger = (endpoint: { + route: string, + methods?: string[], + cors?: boolean, + corsConfig?: CorsConfig, + authLevel?: 'anonymous' | 'function' | 'admin' +}) => { + return (target: Function) => { + let metadata = getMetadata(CLASS_HTTPTRIGGER, target) || [] + metadata.push({ ...defaultEndpoint, ...endpoint }) + defineMetadata(CLASS_HTTPTRIGGER, [...metadata], target); + } +} + +rest.extension('azure', (target, config) => { + const decorator = httpTrigger({ + route: config.path, + methods: config.methods, + cors: config.cors, + corsConfig: config.corsConfig, + authLevel: config.authenticated ? 'function' : 'anonymous' + }) + decorator(target) +}) \ No newline at end of file diff --git a/src/annotations/classes/classConfig.ts b/src/annotations/classes/classConfig.ts new file mode 100644 index 0000000..884fa5f --- /dev/null +++ b/src/annotations/classes/classConfig.ts @@ -0,0 +1,13 @@ +import { CLASS_CLASSCONFIGKEY } from '../constants' +import { getMetadata, defineMetadata } from '../metadata' + +export const classConfig = (config: { [key: string]: any }) => { + return (target: Function) => { + let metadata = getMetadata(CLASS_CLASSCONFIGKEY, target) || {} + defineMetadata(CLASS_CLASSCONFIGKEY, { ...metadata, ...config }, target); + } +} + +export const getClassConfigValue = (key: string, target) => { + return (getMetadata(CLASS_CLASSCONFIGKEY, target) || {})[key] +} \ No newline at end of file diff --git a/src/annotations/classes/dynamoTable.ts b/src/annotations/classes/dynamoTable.ts index 0113163..9b0c68e 100644 --- a/src/annotations/classes/dynamoTable.ts +++ b/src/annotations/classes/dynamoTable.ts @@ -1,8 +1,7 @@ -import { merge } from 'lodash' - import { CLASS_DYNAMOTABLECONFIGURATIONKEY } from '../constants' import { getMetadata, defineMetadata } from '../metadata' -import { applyTemplates, environment } from './environment' +import { applyTemplates } from '../templates' +import { environment } from './environment' export const __dynamoDBDefaults = { AttributeDefinitions: [ @@ -23,27 +22,31 @@ export const __dynamoDBDefaults = { } } - - -export const dynamoTable = (tableConfig: { - tableName: string, +export const dynamoTable = (tableConfig?: { + tableName?: string, environmentKey?: string, - nativeConfig?: any + nativeConfig?: any, + exists?: boolean }) => (target: Function) => { let tableDefinitions = getMetadata(CLASS_DYNAMOTABLECONFIGURATIONKEY, target) || []; - tableConfig.environmentKey = tableConfig.environmentKey || '%ClassName%_TABLE_NAME' - tableConfig.nativeConfig = merge({}, __dynamoDBDefaults, tableConfig.nativeConfig) + tableConfig = tableConfig || {} + tableConfig.tableName = tableConfig.tableName || `%ClassName%-table` + tableConfig.environmentKey = tableConfig.environmentKey || `%ClassName%_TABLE_NAME` + tableConfig.nativeConfig = { ...__dynamoDBDefaults, ...tableConfig.nativeConfig } const { templatedKey, templatedValue } = applyTemplates(tableConfig.environmentKey, tableConfig.tableName, target) - tableDefinitions.push(merge({}, tableConfig, { + tableDefinitions.push({ + ...tableConfig, environmentKey: templatedKey, tableName: templatedValue, - definedBy: target.name - })) + definedBy: target + }) defineMetadata(CLASS_DYNAMOTABLECONFIGURATIONKEY, [...tableDefinitions], target) const environmentSetter = environment(templatedKey, templatedValue) environmentSetter(target) } + +export const dynamo = dynamoTable diff --git a/src/annotations/classes/environment.ts b/src/annotations/classes/environment.ts index 5f06a07..75dbea5 100644 --- a/src/annotations/classes/environment.ts +++ b/src/annotations/classes/environment.ts @@ -1,5 +1,6 @@ import { CLASS_ENVIRONMENTKEY } from '../constants' import { getMetadata, defineMetadata } from '../metadata' +import { applyTemplates } from '../templates' export const environment = (key: string, value: string) => { return (target: Function) => { @@ -11,26 +12,3 @@ export const environment = (key: string, value: string) => { defineMetadata(CLASS_ENVIRONMENTKEY, { ...metadata }, target); } } - -export const applyTemplates = (key, value, target) => { - let templatedKey = key; - let templatedValue = value; - for (let template of environmentTemplates) { - if (template.regexp.test(templatedKey)) { - templatedKey = templatedKey.replace(template.regexp, template.resolution(target)) - } - if (template.regexp.test(templatedValue)) { - templatedValue = templatedValue.replace(template.regexp, template.resolution(target)) - } - } - - return { templatedKey, templatedValue } -} - -export const environmentTemplates = [ - { - name: 'ClassName', - regexp: /%ClassName%/g, - resolution: (target) => target.name - } -] diff --git a/src/annotations/classes/eventSource.ts b/src/annotations/classes/eventSource.ts new file mode 100644 index 0000000..9d23e1f --- /dev/null +++ b/src/annotations/classes/eventSource.ts @@ -0,0 +1,13 @@ +import { CLASS_CLASSCONFIGKEY } from '../constants' +import { getMetadata, defineMetadata } from '../metadata' +import { simpleClassAnnotation } from './simpleAnnotation' + +export const eventSource = (...eventSourceTargets) => { + return (target: Function) => { + for (const eventSourceTarget of eventSourceTargets) { + if (eventSourceTarget && typeof eventSourceTarget.toEventSource === 'function') { + eventSourceTarget.toEventSource(target) + } + } + } +} diff --git a/src/annotations/classes/expandableDecorator.ts b/src/annotations/classes/expandableDecorator.ts new file mode 100644 index 0000000..35e4ab6 --- /dev/null +++ b/src/annotations/classes/expandableDecorator.ts @@ -0,0 +1,47 @@ +import { getMetadata, defineMetadata } from '../metadata' +import { CLASS_KEY_PREFIX } from '../constants' + +export const expandableDecorator = function (decoratorConfig: { + name: string, + defaultValues?: Partial + environmentKey?: string +}) { + const environmentKey = decoratorConfig.environmentKey || `${CLASS_KEY_PREFIX}${decoratorConfig.name}` + const environmentExtensions = new Map void)[]>() + + const decorator = Object.assign( + (config: T) => { + const _dv: any = decoratorConfig.defaultValues || {}; + const _c: any = config + const handlerConfig = { ..._dv, ..._c } + + return (target: Function) => { + if (environmentKey) { + let metadata = getMetadata(environmentKey, target) || [] + metadata.push(handlerConfig) + defineMetadata(environmentKey, [...metadata], target); + } + + const environment = process.env.FUNCTIONAL_ENVIRONMENT + if (!environment || !environmentExtensions.has(environment)) { + return; + } + + const handlers = environmentExtensions.get(environment) + for (const handler of handlers) { + handler(target, handlerConfig) + } + } + }, + { + extension: (environmentMode: string, handler: (target: Function, config: T) => any) => { + const handlers = environmentExtensions.get(environmentMode) || [] + environmentExtensions.set(environmentMode, [...handlers, handler]) + }, + environmentKey + } + ); + + return decorator + +} \ No newline at end of file diff --git a/src/annotations/classes/injectable.ts b/src/annotations/classes/injectable.ts index c7ed4db..eee5d55 100644 --- a/src/annotations/classes/injectable.ts +++ b/src/annotations/classes/injectable.ts @@ -1,6 +1,13 @@ import { CLASS_INJECTABLEKEY } from '../constants' import { defineMetadata } from '../metadata' -export const injectable = (target: Function) => { - defineMetadata(CLASS_INJECTABLEKEY, true, target); +export enum InjectionScope { + Transient = 1, + Singleton = 2 +} + +export const injectable = (scope: InjectionScope = InjectionScope.Transient) => { + return (target) => { + defineMetadata(CLASS_INJECTABLEKEY, scope, target); + } } diff --git a/src/annotations/classes/rest.ts b/src/annotations/classes/rest.ts new file mode 100644 index 0000000..52c2eb5 --- /dev/null +++ b/src/annotations/classes/rest.ts @@ -0,0 +1,37 @@ +import { expandableDecorator } from './expandableDecorator' + +export interface CorsConfig { + headers?: string[], + methods?: string[], + origin?: string, + credentials?: boolean +} + +export const rest = expandableDecorator<{ path: string, methods?: string[], cors?: boolean, corsConfig?: CorsConfig, authenticated?: boolean }>({ + name: 'rest', + defaultValues: { + methods: ['get'], + cors: true, + authenticated: false + } +}) + +export interface IHttpMethod { + (path: string): Function + (config: { path: string, cors?: boolean, corsConfig?: CorsConfig, authenticated?: boolean }): Function +} + +export const resolveParam = (p: any, defaults) => { + if (typeof p === 'string') { + return { ...defaults, path: p } + } else { + return { ...defaults, ...p } + } +} + + +export const httpGet: IHttpMethod = (p) => rest(resolveParam(p, { methods: ['get'] })) +export const httpPost: IHttpMethod = (p) => rest(resolveParam(p, { methods: ['post'] })) +export const httpPut: IHttpMethod = (p) => rest(resolveParam(p, { methods: ['put'] })) +export const httpPatch: IHttpMethod = (p) => rest(resolveParam(p, { methods: ['patch'] })) +export const httpDelete: IHttpMethod = (p) => rest(resolveParam(p, { methods: ['delete'] })) diff --git a/src/annotations/classes/runtime.ts b/src/annotations/classes/runtime.ts deleted file mode 100644 index 22cc596..0000000 --- a/src/annotations/classes/runtime.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { CLASS_MEMORYSIZEKEY, CLASS_TIMEOUTKEY, CLASS_RUNTIMEKEY } from '../constants' -import { defineMetadata } from '../metadata' - -export const runtime = (config: { - type?: 'nodejs6.10', - memorySize?: number, - timeout?: number -}) => (target: Function) => { - if (typeof config.type === 'string') { - defineMetadata(CLASS_RUNTIMEKEY, config.type, target); - } - if (typeof config.memorySize === 'number') { - defineMetadata(CLASS_MEMORYSIZEKEY, config.memorySize, target); - } - if (typeof config.timeout === 'number') { - defineMetadata(CLASS_TIMEOUTKEY, config.timeout, target); - } -} \ No newline at end of file diff --git a/src/annotations/classes/s3Storage.ts b/src/annotations/classes/s3Storage.ts new file mode 100644 index 0000000..7612615 --- /dev/null +++ b/src/annotations/classes/s3Storage.ts @@ -0,0 +1,42 @@ +import { CLASS_S3CONFIGURATIONKEY } from '../constants' +import { getMetadata, defineMetadata } from '../metadata' +import { applyTemplates } from '../templates' +import { environment } from './environment' + +export const S3_BUCKET_SUFFIX = '_S3_BUCKET' +export const S3_BUCKET_NAME_REGEXP = /^[a-z0-9][a-z0-9-.]{1,61}[a-z0-9]$/ + +export const s3Storage = (s3Config?: { + bucketName?: string, + environmentKey?: string, + eventSourceConfiguration?: { + Event?: any, + Filter?: any + }, + exists?: boolean +}) => (target: Function) => { + let s3Definitions = getMetadata(CLASS_S3CONFIGURATIONKEY, target) || []; + + s3Config = s3Config || {} + s3Config.bucketName = s3Config.bucketName || `%ClassName%-bucket` + s3Config.environmentKey = s3Config.environmentKey || `%ClassName%${S3_BUCKET_SUFFIX}` + + const { templatedKey, templatedValue } = applyTemplates(s3Config.environmentKey, s3Config.bucketName, target) + const lowerCaseTemplatedValue = templatedValue ? templatedValue.toLowerCase() : templatedValue + + if (!S3_BUCKET_NAME_REGEXP.test(lowerCaseTemplatedValue)) { + throw new Error(`invalid bucket name '${lowerCaseTemplatedValue}' validator: ${S3_BUCKET_NAME_REGEXP}`) + } + + s3Definitions.push({ + ...s3Config, + environmentKey: templatedKey, + bucketName: lowerCaseTemplatedValue, + definedBy: target.name + }) + + defineMetadata(CLASS_S3CONFIGURATIONKEY, [...s3Definitions], target) + + const environmentSetter = environment(templatedKey, lowerCaseTemplatedValue) + environmentSetter(target) +} diff --git a/src/annotations/classes/sns.ts b/src/annotations/classes/sns.ts new file mode 100644 index 0000000..30b1a3d --- /dev/null +++ b/src/annotations/classes/sns.ts @@ -0,0 +1,29 @@ +import { CLASS_SNSCONFIGURATIONKEY } from '../constants' +import { getMetadata, defineMetadata } from '../metadata' +import { applyTemplates } from '../templates' +import { environment } from './environment' + +export const sns = (snsConfig?: { + topicName?: string, + environmentKey?: string, + exists?: boolean +}) => (target: Function) => { + let snsDefinitions = getMetadata(CLASS_SNSCONFIGURATIONKEY, target) || []; + + snsConfig = snsConfig || {} + snsConfig.topicName = snsConfig.topicName || `%ClassName%-topic` + snsConfig.environmentKey = snsConfig.environmentKey || `%ClassName%_SNS_TOPICNAME` + + const { templatedKey, templatedValue } = applyTemplates(snsConfig.environmentKey, snsConfig.topicName, target) + snsDefinitions.push({ + ...snsConfig, + environmentKey: templatedKey, + topicName: templatedValue, + definedBy: target.name + }) + + defineMetadata(CLASS_SNSCONFIGURATIONKEY, [...snsDefinitions], target) + + const environmentSetter = environment(templatedKey, templatedValue) + environmentSetter(target) +} diff --git a/src/annotations/classes/use.ts b/src/annotations/classes/use.ts new file mode 100644 index 0000000..b162109 --- /dev/null +++ b/src/annotations/classes/use.ts @@ -0,0 +1,20 @@ +import { CLASS_MIDDLEWAREKEY } from '../constants' +import { getMetadata, defineMetadata } from '../metadata' +import { applyTemplates } from '../templates' + +export const use = (...middleware) => { + return (target: Function) => { + const metadata = getMiddlewares(target) + defineMetadata(CLASS_MIDDLEWAREKEY, [...middleware, ...metadata], target); + + for (const mw of middleware) { + if (typeof mw.onDefineMiddlewareTo === 'function') { + mw.onDefineMiddlewareTo(target) + } + } + } +} + +export const getMiddlewares = (target) => { + return getMetadata(CLASS_MIDDLEWAREKEY, target) || [] +} diff --git a/src/annotations/constants.ts b/src/annotations/constants.ts index 281f023..b31e6b2 100644 --- a/src/annotations/constants.ts +++ b/src/annotations/constants.ts @@ -1,13 +1,27 @@ +export const CLASS_KEY_PREFIX = 'functionly:class:' + export const PARAMETER_PARAMKEY = 'functionly:param:parameters' export const CLASS_DESCRIPTIONKEY = 'functionly:class:description' export const CLASS_ROLEKEY = 'functionly:class:role' -export const CLASS_RUNTIMEKEY = 'functionly:class:runtime' +export const CLASS_GROUP = 'functionly:class:group' export const CLASS_ENVIRONMENTKEY = 'functionly:class:environment' -export const CLASS_MEMORYSIZEKEY = 'functionly:class:memorysize' -export const CLASS_TIMEOUTKEY = 'functionly:class:timeout' +export const CLASS_MIDDLEWAREKEY = 'functionly:class:middleware' export const CLASS_APIGATEWAYKEY = 'functionly:class:apigateway' +export const CLASS_CLOUDFORMATION = 'functionly:class:cloudformation' +export const CLASS_CLOUDWATCHEVENT = 'functionly:class:cloudwatchevent' export const CLASS_TAGKEY = 'functionly:class:tag' export const CLASS_LOGKEY = 'functionly:class:log' export const CLASS_INJECTABLEKEY = 'functionly:class:injectable' export const CLASS_NAMEKEY = 'functionly:class:name' -export const CLASS_DYNAMOTABLECONFIGURATIONKEY = 'functionly:class:dynamoTableConfiguration' \ No newline at end of file +export const CLASS_DYNAMOTABLECONFIGURATIONKEY = 'functionly:class:dynamoTableConfiguration' +export const CLASS_SNSCONFIGURATIONKEY = 'functionly:class:snsConfiguration' +export const CLASS_S3CONFIGURATIONKEY = 'functionly:class:s3Configuration' +export const CLASS_CLASSCONFIGKEY = 'functionly:class:classConfig' + +export const CLASS_HTTPTRIGGER = 'functionly:class:httpTrigger' + +export const CLASS_AZURENODEKEY = 'functionly:class:azurenode' + +export const CLASS_AWSRUNTIMEKEY = 'functionly:class:awsruntime' +export const CLASS_AWSMEMORYSIZEKEY = 'functionly:class:awsmemorysize' +export const CLASS_AWSTIMEOUTKEY = 'functionly:class:awstimeout' diff --git a/src/annotations/index.ts b/src/annotations/index.ts index 6f8feee..4671247 100644 --- a/src/annotations/index.ts +++ b/src/annotations/index.ts @@ -1,23 +1,38 @@ -export { injectable } from './classes/injectable' -export { apiGateway } from './classes/apiGateway' -export { environment, environmentTemplates } from './classes/environment' +export { templates, applyTemplates } from './templates' +export { injectable, InjectionScope } from './classes/injectable' +export { apiGateway } from './classes/aws/apiGateway' +export { cloudFormation } from './classes/aws/cloudFormation' +export { cloudWatchEvent } from './classes/aws/cloudWatchEvent' +export { httpTrigger } from './classes/azure/httpTrigger' +export { rest, httpGet, httpPost, httpPut, httpPatch, httpDelete, IHttpMethod } from './classes/rest' +export { environment } from './classes/environment' export { tag } from './classes/tag' export { log } from './classes/log' -export { runtime } from './classes/runtime' export { functionName, getFunctionName } from './classes/functionName' -export { dynamoTable, __dynamoDBDefaults } from './classes/dynamoTable' +export { dynamoTable, dynamo, __dynamoDBDefaults } from './classes/dynamoTable' +export { sns } from './classes/sns' +export { s3Storage } from './classes/s3Storage' +export { eventSource } from './classes/eventSource' +export { classConfig, getClassConfigValue } from './classes/classConfig' +export { simpleClassAnnotation } from './classes/simpleAnnotation' +export { expandableDecorator } from './classes/expandableDecorator' +export { use } from './classes/use' import { simpleClassAnnotation } from './classes/simpleAnnotation' -import { CLASS_DESCRIPTIONKEY, CLASS_ROLEKEY, CLASS_RUNTIMEKEY } from './constants' +import { CLASS_DESCRIPTIONKEY, CLASS_ROLEKEY, CLASS_GROUP } from './constants' export const description = simpleClassAnnotation(CLASS_DESCRIPTIONKEY) export const role = simpleClassAnnotation(CLASS_ROLEKEY) +export const group = simpleClassAnnotation(CLASS_GROUP) +export { aws } from './classes/aws/aws' -export { param } from './parameters/param' +export { azure } from './classes/azure/azure' + +export { param, serviceParams, createParameterDecorator, request, error, result, functionalServiceName, provider, stage } from './parameters/param' export { inject } from './parameters/inject' import * as _constants from './constants' export const constants = _constants -export { defineMetadata, getMetadata, getMetadataKeys, getOwnMetadata } from './metadata' \ No newline at end of file +export { defineMetadata, getMetadata, getMetadataKeys, getOwnMetadata, getOverridableMetadata } from './metadata' \ No newline at end of file diff --git a/src/annotations/metadata.ts b/src/annotations/metadata.ts index 1bff7b8..1dda595 100644 --- a/src/annotations/metadata.ts +++ b/src/annotations/metadata.ts @@ -4,8 +4,8 @@ export const defineMetadata = (metadataKey, metadataValue, target, propertyKey?) return Reflect.defineMetadata(metadataKey, metadataValue, target.prototype ? target.prototype : target, propertyKey) } -export const getMetadata = (metadataKey, target) => { - return Reflect.getMetadata(metadataKey, target.prototype ? target.prototype : target) +export const getMetadata = (metadataKey, target, propertyKey?) => { + return Reflect.getMetadata(metadataKey, target.prototype ? target.prototype : target, propertyKey) } export const getMetadataKeys = (target) => { @@ -14,4 +14,31 @@ export const getMetadataKeys = (target) => { export const getOwnMetadata = (metadataKey, target, propertyKey?) => { return Reflect.getOwnMetadata(metadataKey, target.prototype ? target.prototype : target, propertyKey) +} + +export const getOverridableMetadata = (metadataKey, target, propertyKey) => { + if (!propertyKey) { + // parameter decorators in constructors + } else { + // parameter decorators in static methods + if (target.hasOwnProperty(propertyKey)) { + return getOwnMetadata(metadataKey, target, propertyKey) + } + } + + return getParentMetadata(metadataKey, target, propertyKey) +} + +export const getParentMetadata = (metadataKey, target, propertyKey) => { + if (target === Function) return + const value = getOwnMetadata(metadataKey, target, propertyKey) + if (typeof value === 'undefined') { + if (target.prototype && target.__proto__) { + return getParentMetadata(metadataKey, target.__proto__, propertyKey) + } + if (!target.prototype && target.constructor) { + return getParentMetadata(metadataKey, target.constructor, propertyKey) + } + } + return value } \ No newline at end of file diff --git a/src/annotations/parameters/inject.ts b/src/annotations/parameters/inject.ts index 0680227..5215555 100644 --- a/src/annotations/parameters/inject.ts +++ b/src/annotations/parameters/inject.ts @@ -1,11 +1,10 @@ -import { PARAMETER_PARAMKEY, CLASS_ENVIRONMENTKEY, CLASS_INJECTABLEKEY, CLASS_DYNAMOTABLECONFIGURATIONKEY } from '../constants' -import { getOwnMetadata, defineMetadata, getMetadata } from '../metadata' -import { getFunctionParameters } from '../utils' +import { PARAMETER_PARAMKEY, CLASS_ENVIRONMENTKEY, CLASS_INJECTABLEKEY } from '../constants' +import { defineMetadata, getOwnMetadata } from '../metadata' import { getFunctionName } from '../classes/functionName' export const inject = (type: any, ...params): any => { return (target, targetKey, parameterIndex: number) => { - if (!getMetadata(CLASS_INJECTABLEKEY, type)) { + if (!getOwnMetadata(CLASS_INJECTABLEKEY, type)) { throw new Error(`type '${getFunctionName(type)}' not marked as injectable`) } @@ -14,24 +13,15 @@ export const inject = (type: any, ...params): any => { existingParameters.push({ serviceType: type, parameterIndex, + targetKey, type: 'inject', params }); defineMetadata(PARAMETER_PARAMKEY, [...existingParameters], target, targetKey); - const injectTarget = type - const injectMetadata = getMetadata(CLASS_ENVIRONMENTKEY, injectTarget) || {} - const metadata = getMetadata(CLASS_ENVIRONMENTKEY, target) || {} - if (injectMetadata) { - Object.keys(injectMetadata).forEach((key) => { - metadata[key] = injectMetadata[key] - }) - defineMetadata(CLASS_ENVIRONMENTKEY, { ...metadata }, target); + if (typeof type.onDefineInjectTo === 'function') { + type.onDefineInjectTo(target, targetKey, parameterIndex) } - - const injectTableConfig = getMetadata(CLASS_DYNAMOTABLECONFIGURATIONKEY, injectTarget) || [] - const tableConfig = getMetadata(CLASS_DYNAMOTABLECONFIGURATIONKEY, target) || [] - defineMetadata(CLASS_DYNAMOTABLECONFIGURATIONKEY, [ ...tableConfig, ...injectTableConfig ], target); } } diff --git a/src/annotations/parameters/param.ts b/src/annotations/parameters/param.ts index c9e853b..d16d449 100644 --- a/src/annotations/parameters/param.ts +++ b/src/annotations/parameters/param.ts @@ -1,9 +1,10 @@ import { PARAMETER_PARAMKEY } from '../constants' -import { getOwnMetadata, defineMetadata } from '../metadata' +import { getOwnMetadata, defineMetadata, getMetadata } from '../metadata' import { getFunctionParameters } from '../utils' export const param = (target: any, targetKey?: string, parameterIndex?: number): any => { let name; + let config = {}; let decorator = function (target, targetKey, parameterIndex: number) { let parameterNames = getFunctionParameters(target, targetKey); @@ -11,8 +12,10 @@ export const param = (target: any, targetKey?: string, parameterIndex?: number): let paramName = parameterNames[parameterIndex]; existingParameters.push({ + ...config, from: name || paramName, parameterIndex, + targetKey, type: 'param' }); @@ -22,7 +25,48 @@ export const param = (target: any, targetKey?: string, parameterIndex?: number): if (typeof target == "string" || typeof target == "undefined" || !target) { name = target; return decorator; + } else if (typeof target === 'object' && target && !targetKey) { + name = target.name; + config = target + return decorator } else { return decorator(target, targetKey, parameterIndex); } -} \ No newline at end of file +} + +export const createParameterDecorator = (type: string, defaultConfig?: any) => (target?, targetKey?, parameterIndex?: number): any => { + let config = { ...(defaultConfig || {}) }; + + const decorator = function (target, targetKey, parameterIndex: number) { + const parameterNames = getFunctionParameters(target, targetKey); + + const existingParameters: any[] = getOwnMetadata(PARAMETER_PARAMKEY, target, targetKey) || []; + const paramName = parameterNames[parameterIndex]; + existingParameters.push({ + from: paramName, + parameterIndex, + targetKey, + type, + config, + target + }); + defineMetadata(PARAMETER_PARAMKEY, existingParameters, target, targetKey); + } + + if (typeof target == "undefined" || !target) { + return decorator + } else if (typeof target === 'object' && target && !targetKey) { + config = { ...config, ...target } + return decorator + } else { + return decorator(target, targetKey, parameterIndex); + } +} + +export const serviceParams = createParameterDecorator('serviceParams') +export const request = createParameterDecorator('request') +export const error = createParameterDecorator('error') +export const result = createParameterDecorator('result') +export const functionalServiceName = createParameterDecorator('functionalServiceName') +export const provider = createParameterDecorator('provider') +export const stage = createParameterDecorator('stage') diff --git a/src/annotations/templates.ts b/src/annotations/templates.ts new file mode 100644 index 0000000..005a2d0 --- /dev/null +++ b/src/annotations/templates.ts @@ -0,0 +1,22 @@ +export const applyTemplates = (key, value, target) => { + let templatedKey = key; + let templatedValue = value; + for (let template of templates) { + if (template.regexp.test(templatedKey)) { + templatedKey = templatedKey.replace(template.regexp, template.resolution(target)) + } + if (template.regexp.test(templatedValue)) { + templatedValue = templatedValue.replace(template.regexp, template.resolution(target)) + } + } + + return { templatedKey, templatedValue } +} + +export const templates = [ + { + name: 'ClassName', + regexp: /%ClassName%/g, + resolution: (target) => target.name + } +] \ No newline at end of file diff --git a/src/classes/api.ts b/src/classes/api.ts new file mode 100644 index 0000000..4291ff3 --- /dev/null +++ b/src/classes/api.ts @@ -0,0 +1,47 @@ +import { Resource } from './resource' +import { getFunctionName } from '../annotations/classes/functionName' +import { defineMetadata, getMetadata, constants, getClassConfigValue } from '../annotations' +const { CLASS_ENVIRONMENTKEY, CLASS_CLASSCONFIGKEY, PARAMETER_PARAMKEY } = constants + +export class Api extends Resource { + constructor(...params) { + super(); + } + public async init(): Promise { + + } + + public static onDefineInjectTo(target, targetKey, parameterIndex: number) { + super.onDefineInjectTo(target, targetKey, parameterIndex) + + const configEnvironmentKey = getClassConfigValue('injectServiceCopyMetadataKey', this) + if (configEnvironmentKey) { + const injectKeyConfig = (getMetadata(configEnvironmentKey, this) || []) + .map(c => { return { ...c, injected: true } }) + const keyConfig = getMetadata(configEnvironmentKey, target) || [] + defineMetadata(configEnvironmentKey, [...keyConfig, ...injectKeyConfig], target); + } + + const targetTkey = 'handle' + const injectedDefinitions: any[] = (getMetadata(PARAMETER_PARAMKEY, this) || []) + .filter(p => p.type === 'inject') + + for (const { serviceType, targetKey, parameterIndex } of injectedDefinitions) { + if (typeof serviceType.onDefineInjectTo === 'function') { + serviceType.onDefineInjectTo(target, targetTkey, parameterIndex) + } + } + } + + public static toEventSource(target: Function) { + const environmentKey = getClassConfigValue('injectServiceEventSourceKey', this) + if (environmentKey) { + const metadataCollection = getMetadata(environmentKey, target) || [] + + const eventSourceConfigs = (getMetadata(environmentKey, this) || []) + .map(config => { return { ...config, eventSource: true } }) + + defineMetadata(environmentKey, [...metadataCollection, ...eventSourceConfigs], target) + } + } +} \ No newline at end of file diff --git a/src/classes/api/aws/apiGateway.ts b/src/classes/api/aws/apiGateway.ts new file mode 100644 index 0000000..587be3e --- /dev/null +++ b/src/classes/api/aws/apiGateway.ts @@ -0,0 +1,9 @@ + +import { Api } from '../../api' +import { constants, classConfig } from '../../../annotations' +const { CLASS_APIGATEWAYKEY } = constants + +@classConfig({ + injectServiceEventSourceKey: CLASS_APIGATEWAYKEY +}) +export class ApiGateway extends Api { } diff --git a/src/classes/api/aws/cloudWatchEvent.ts b/src/classes/api/aws/cloudWatchEvent.ts new file mode 100644 index 0000000..7d46958 --- /dev/null +++ b/src/classes/api/aws/cloudWatchEvent.ts @@ -0,0 +1,9 @@ + +import { Api } from '../../api' +import { constants, classConfig } from '../../../annotations' +const { CLASS_CLOUDWATCHEVENT } = constants + +@classConfig({ + injectServiceEventSourceKey: CLASS_CLOUDWATCHEVENT +}) +export class CloudWatchEvent extends Api { } diff --git a/src/classes/externals/dynamoDB.ts b/src/classes/api/aws/dynamoTable.ts similarity index 62% rename from src/classes/externals/dynamoDB.ts rename to src/classes/api/aws/dynamoTable.ts index 009fe63..e3635f1 100644 --- a/src/classes/externals/dynamoDB.ts +++ b/src/classes/api/aws/dynamoTable.ts @@ -1,32 +1,54 @@ import * as AWS from 'aws-sdk' -import { merge } from 'lodash' import { DocumentClient } from 'aws-sdk/lib/dynamodb/document_client' -import { Service } from '../service' -import { constants, getMetadata } from '../../annotations' +import { Api } from '../../api' +import { constants, getMetadata, classConfig, inject, injectable, InjectionScope } from '../../../annotations' -let dynamoDB = null; -const initAWSSDK = () => { - if (!dynamoDB) { +const { CLASS_DYNAMOTABLECONFIGURATIONKEY } = constants + +@injectable(InjectionScope.Singleton) +export class DocumentClientApi extends Api { + private dynamoDB = null + public constructor() { + super(); + } + + public async init() { let awsConfig: any = {} if (process.env.FUNCTIONAL_ENVIRONMENT === 'local') { awsConfig.apiVersion = '2012-08-10' - awsConfig.region = process.env.AWS_REGION || 'eu-central-1' - awsConfig.endpoint = process.env.DYNAMODB_LOCAL_ENDPOINT || 'http://localhost:8000' + awsConfig.region = process.env.AWS_REGION || 'us-east-1' + if (process.env.DYNAMODB_LOCAL !== 'false') { + awsConfig.endpoint = 'DYNAMODB_LOCAL_ENDPOINT' in process.env ? process.env.DYNAMODB_LOCAL_ENDPOINT : 'http://localhost:8000' + } + + console.log('Local DynamoDB configuration') + console.log(JSON.stringify({ + apiVersion: awsConfig.apiVersion, + 'region (process.env.AWS_REGION)': awsConfig.region, + 'endpoint (process.env.DYNAMODB_LOCAL_ENDPOINT)': awsConfig.endpoint, + }, null, 2)) } - dynamoDB = new AWS.DynamoDB(awsConfig); + this.dynamoDB = new AWS.DynamoDB(awsConfig); + } + + public getDocumentClient() { + return new AWS.DynamoDB.DocumentClient({ service: this.dynamoDB }) } - return dynamoDB } -export class DynamoDB extends Service { +@classConfig({ + injectServiceCopyMetadataKey: CLASS_DYNAMOTABLECONFIGURATIONKEY, + injectServiceEventSourceKey: CLASS_DYNAMOTABLECONFIGURATIONKEY +}) +export class DynamoTable extends Api { private _documentClient: DocumentClient - constructor() { - initAWSSDK() - + public constructor( @inject(DocumentClientApi) private documentClientApi: DocumentClientApi) { super() - this._documentClient = new AWS.DynamoDB.DocumentClient({ service: dynamoDB }) + } + public async init() { + this._documentClient = this.documentClientApi.getDocumentClient() } public getDocumentClient() { @@ -103,12 +125,15 @@ export class DynamoDB extends Service { } protected setDefaultValues(params, command) { - const tableConfig = getMetadata(constants.CLASS_DYNAMOTABLECONFIGURATIONKEY, this) || {} - const tableName = (tableConfig[this.constructor.name] && tableConfig[this.constructor.name].TableName) + const tableConfig = (getMetadata(CLASS_DYNAMOTABLECONFIGURATIONKEY, this) || [])[0] || {} + const tableName = ({ TableName: tableConfig.tableName, ...tableConfig.nativeConfig }).TableName + + const calcTableName = tableConfig.environmentKey && process.env[tableConfig.environmentKey] ? process.env[tableConfig.environmentKey] : '' + const suffix = tableConfig.exists ? '' : `-${process.env.FUNCTIONAL_STAGE}` const initParams = { - TableName: process.env[`${this.constructor.name}_TABLE_NAME`] || tableName + TableName: (calcTableName || tableName) + suffix } - return merge({}, initParams, params) + return { ...initParams, ...params } } } \ No newline at end of file diff --git a/src/classes/api/aws/s3Storage.ts b/src/classes/api/aws/s3Storage.ts new file mode 100644 index 0000000..0369266 --- /dev/null +++ b/src/classes/api/aws/s3Storage.ts @@ -0,0 +1,146 @@ +import { S3 } from 'aws-sdk' +import { Api } from '../../api' +import { constants, getMetadata, classConfig, inject, injectable, InjectionScope } from '../../../annotations' +import { S3_BUCKET_SUFFIX } from '../../../annotations/classes/s3Storage' +const { CLASS_S3CONFIGURATIONKEY } = constants + +export { S3 } from 'aws-sdk' + +@injectable(InjectionScope.Singleton) +export class S3Api extends Api { + private s3 = null + public constructor() { + super(); + } + + public async init() { + let awsConfig: any = {} + if (process.env.FUNCTIONAL_ENVIRONMENT === 'local') { + awsConfig.apiVersion = '2006-03-01' + awsConfig.region = process.env.AWS_REGION || 'us-east-1' + if (process.env.S3_LOCAL !== 'false') { + awsConfig.endpoint = 'S3_LOCAL_ENDPOINT' in process.env ? process.env.S3_LOCAL_ENDPOINT : 'http://localhost:4572' + } + + console.log('Local S3 configuration') + console.log(JSON.stringify({ + apiVersion: awsConfig.apiVersion, + 'region (process.env.AWS_REGION)': awsConfig.region, + 'endpoint (process.env.S3_LOCAL_ENDPOINT)': awsConfig.endpoint, + }, null, 2)) + } + + this.s3 = new S3(awsConfig); + } + + public getS3() { + return this.s3 + } +} + +@classConfig({ + injectServiceCopyMetadataKey: CLASS_S3CONFIGURATIONKEY, + injectServiceEventSourceKey: CLASS_S3CONFIGURATIONKEY +}) +export class S3Storage extends Api { + private _s3Client: S3 + public constructor( @inject(S3Api) private s3Api: S3Api) { + super() + } + public async init() { + this._s3Client = this.s3Api.getS3() + } + + public getS3() { + return this._s3Client + } + + public async putObject(params: Partial) { + return new Promise((resolve, reject) => { + if (typeof params.Key === 'string') { + params = { + ...params, + Key: /^\//.test(params.Key) ? params.Key.substring(1, params.Key.length) : params.Key + } + } + this._s3Client.putObject(this.setDefaultValues(params, 'putObject'), (err, result) => { + if (err) reject(err) + else resolve(result) + }) + }) + } + + public async getObject(params: Partial) { + return new Promise((resolve, reject) => { + if (typeof params.Key === 'string') { + params = { + ...params, + Key: /^\//.test(params.Key) ? params.Key.substring(1, params.Key.length) : params.Key + } + } + this._s3Client.getObject(this.setDefaultValues(params, 'getObject'), (err, result) => { + if (err) reject(err) + else resolve(result) + }) + }) + } + + public async deleteObject(params: Partial) { + return new Promise((resolve, reject) => { + if (typeof params.Key === 'string') { + params = { + ...params, + Key: /^\//.test(params.Key) ? params.Key.substring(1, params.Key.length) : params.Key + } + } + this._s3Client.deleteObject(this.setDefaultValues(params, 'deleteObject'), (err, result) => { + if (err) reject(err) + else resolve(result) + }) + }) + } + + public async listObjectsV2(params: Partial) { + return new Promise((resolve, reject) => { + this._s3Client.listObjectsV2(this.setDefaultValues(params, 'listObjectsV2'), (err, result) => { + if (err) reject(err) + else resolve(result) + }) + }) + } + + public async getSignedUrl(operation: string, params: any) { + return new Promise((resolve, reject) => { + if (typeof params.Key === 'string') { + params = { + ...params, + Key: /^\//.test(params.Key) ? params.Key.substring(1, params.Key.length) : params.Key + } + } + this._s3Client.getSignedUrl(operation, this.setDefaultValues(params, 'getSignedUrl'), (err, result) => { + if (err) reject(err) + else resolve(result) + }) + }) + } + + public upload( + params: Partial, + options?: S3.ManagedUpload.ManagedUploadOptions, + callback?: (err: Error, data: S3.ManagedUpload.SendData) => void): S3.ManagedUpload { + return this._s3Client.upload(this.setDefaultValues(params, 'upload'), options, callback) + } + + protected setDefaultValues(params, command) { + const bucketConfig = (getMetadata(CLASS_S3CONFIGURATIONKEY, this) || [])[0] || {} + const bucketName = bucketConfig.bucketName + + const calcBucketName = bucketConfig.environmentKey && process.env[bucketConfig.environmentKey] ? process.env[bucketConfig.environmentKey] : '' + const suffix = bucketConfig.exists ? '' : `-${process.env.FUNCTIONAL_STAGE}` + const initParams = { + Bucket: (calcBucketName || bucketName) + suffix + } + + return { ...initParams, ...params } + } +} \ No newline at end of file diff --git a/src/classes/api/aws/sns.ts b/src/classes/api/aws/sns.ts new file mode 100644 index 0000000..c909ace --- /dev/null +++ b/src/classes/api/aws/sns.ts @@ -0,0 +1,75 @@ +import { SNS } from 'aws-sdk' +export { SNS } from 'aws-sdk' + +import { Api } from '../../api' +import { constants, getMetadata, classConfig, inject, injectable, InjectionScope } from '../../../annotations' +const { CLASS_SNSCONFIGURATIONKEY } = constants + +@injectable(InjectionScope.Singleton) +export class SNSApi extends Api { + private sns = null + public constructor() { + super(); + } + + public async init() { + let awsConfig: any = {} + if (process.env.FUNCTIONAL_ENVIRONMENT === 'local') { + awsConfig.apiVersion = '2010-03-31' + awsConfig.region = process.env.AWS_REGION || 'us-east-1' + if (process.env.SNS_LOCAL !== 'false') { + awsConfig.endpoint = 'SNS_LOCAL_ENDPOINT' in process.env ? process.env.SNS_LOCAL_ENDPOINT : 'http://localhost:4100' + } + + console.log('Local SNS configuration') + console.log(JSON.stringify({ + apiVersion: awsConfig.apiVersion, + 'region (process.env.AWS_REGION)': awsConfig.region, + 'endpoint (process.env.SNS_LOCAL_ENDPOINT)': awsConfig.endpoint, + }, null, 2)) + } + + this.sns = new SNS(awsConfig); + } + + public getSNS() { + return this.sns + } +} + +@classConfig({ + injectServiceCopyMetadataKey: CLASS_SNSCONFIGURATIONKEY, + injectServiceEventSourceKey: CLASS_SNSCONFIGURATIONKEY +}) +export class SimpleNotificationService extends Api { + private _snsClient: SNS + public constructor( @inject(SNSApi) private snsApi: SNSApi) { + super() + } + public async init() { + this._snsClient = this.snsApi.getSNS() + } + + public getSNS() { + return this._snsClient + } + + public async publish(params: SNS.PublishInput) { + return new Promise((resolve, reject) => { + this._snsClient.publish(this.setDefaultValues(params, 'publish'), (err, result) => { + if (err) reject(err) + else resolve(result) + }) + }) + } + + protected setDefaultValues(params, command) { + const snsConfig = (getMetadata(CLASS_SNSCONFIGURATIONKEY, this) || [])[0] || {} + const envKey = snsConfig.environmentKey + '_ARN' + const TopicArn = process.env[envKey] ? process.env[envKey] : '' + + const initParams = { TopicArn } + + return { ...initParams, ...params } + } +} \ No newline at end of file diff --git a/src/classes/core/callExtension.ts b/src/classes/core/callExtension.ts new file mode 100644 index 0000000..1881ecc --- /dev/null +++ b/src/classes/core/callExtension.ts @@ -0,0 +1,7 @@ + + +export const callExtension = async (target: any, method: string, ...params) => { + if (typeof target[method] === 'function') { + return await target[method](...params) + } +} \ No newline at end of file diff --git a/src/classes/functionalApi.ts b/src/classes/functionalApi.ts deleted file mode 100644 index 32842c0..0000000 --- a/src/classes/functionalApi.ts +++ /dev/null @@ -1,2 +0,0 @@ -export class FunctionalApi { -} \ No newline at end of file diff --git a/src/classes/functionalService.ts b/src/classes/functionalService.ts index e70d756..cc18702 100644 --- a/src/classes/functionalService.ts +++ b/src/classes/functionalService.ts @@ -1,12 +1,17 @@ -import { Service } from './service' +import { Resource } from './resource' import { getInvoker, invoke } from '../providers' +import { defineMetadata, getMetadata, constants, getFunctionName } from '../annotations' +const { CLASS_ENVIRONMENTKEY } = constants +import { container } from '../helpers/ioc' -export class FunctionalService extends Service { - public handle(...params) { +export const FUNCTIONAL_SERVICE_PREFIX = 'FUNCTIONAL_SERVICE_' + +export class FunctionalService extends Resource { + public static handle(...params) { } - public async invoke(params?, invokeConfig?) { + public static async invoke(params?, invokeConfig?) { return await invoke(this, params, invokeConfig) } @@ -14,4 +19,16 @@ export class FunctionalService extends Service { let invoker = getInvoker(this, params) return invoker } + + public static async onInject({ parameter, context }): Promise { + const injectableType = container.resolveType(this) + return (params?, invokeConfig?) => injectableType.invoke(params, { ...invokeConfig, context: context.context }) + } + + public static onDefineInjectTo(target) { + const metadata = getMetadata(CLASS_ENVIRONMENTKEY, target) || {} + const funcName = getFunctionName(this) || 'undefined' + metadata[`${FUNCTIONAL_SERVICE_PREFIX}${funcName.toUpperCase()}`] = funcName + defineMetadata(CLASS_ENVIRONMENTKEY, { ...metadata }, target); + } } \ No newline at end of file diff --git a/src/classes/index.ts b/src/classes/index.ts index 5f73f03..2728f57 100644 --- a/src/classes/index.ts +++ b/src/classes/index.ts @@ -1,4 +1,13 @@ -export { Service } from './service' -export { FunctionalApi } from './functionalApi' +export { Resource } from './resource' +export { Api } from './api' export { FunctionalService } from './functionalService' -export { DynamoDB } from './externals/dynamoDB' \ No newline at end of file +export { DynamoTable, DocumentClientApi } from './api/aws/dynamoTable' +export { SimpleNotificationService, SNSApi } from './api/aws/sns' +export { S3Storage, S3Api } from './api/aws/s3Storage' +export { ApiGateway } from './api/aws/apiGateway' +export { CloudWatchEvent } from './api/aws/cloudWatchEvent' +export { callExtension } from './core/callExtension' +export { Service } from './service' + +export { PreHook } from './middleware/preHook' +export { PostHook } from './middleware/postHook' \ No newline at end of file diff --git a/src/classes/middleware/hook.ts b/src/classes/middleware/hook.ts new file mode 100644 index 0000000..e4c7b0d --- /dev/null +++ b/src/classes/middleware/hook.ts @@ -0,0 +1,47 @@ +import { getMetadata, defineMetadata, constants, result } from '../../annotations' +import { getMiddlewares } from '../../annotations/classes/use' +const { PARAMETER_PARAMKEY, CLASS_ENVIRONMENTKEY } = constants + +export class Hook { + public static handle(...params); + public static handle( @result res) { + return res + } + + public static onDefineMiddlewareTo(target) { + const targetTkey = 'handle' + + const injectedDefinitions: any[] = (getMetadata(PARAMETER_PARAMKEY, this, targetTkey) || []) + .filter(p => p.type === 'inject') + + for (const { serviceType, targetKey, parameterIndex } of injectedDefinitions) { + if (typeof serviceType.onDefineInjectTo === 'function') { + serviceType.onDefineInjectTo(target, targetTkey, parameterIndex) + } + } + + const middlewares = getMiddlewares(this) + for (const middleware of middlewares) { + if (typeof middleware.onDefineMiddlewareTo === 'function') { + middleware.onDefineMiddlewareTo(target) + } + } + + const metadata = getMetadata(CLASS_ENVIRONMENTKEY, target) || {} + const injectMetadata = getMetadata(CLASS_ENVIRONMENTKEY, this) || {} + if (injectMetadata) { + Object.keys(injectMetadata).forEach((key) => { + metadata[key] = injectMetadata[key] + }) + defineMetadata(CLASS_ENVIRONMENTKEY, { ...metadata }, target); + } + } + + public static onInject({ parameter, context }) { + const key = parameter.serviceType && parameter.serviceType.name + if (key && context.context && typeof context.context[key] !== 'undefined') { + return context.context[key] + } + return null + } +} \ No newline at end of file diff --git a/src/classes/middleware/postHook.ts b/src/classes/middleware/postHook.ts new file mode 100644 index 0000000..85c806c --- /dev/null +++ b/src/classes/middleware/postHook.ts @@ -0,0 +1,26 @@ +import { Hook } from './hook' +import { error, getMetadata, constants } from '../../annotations' +const { PARAMETER_PARAMKEY } = constants + +export class PostHook extends Hook { + public static catch(...params); + public static catch( @error error) { + throw error + } + + + public static onDefineMiddlewareTo(target) { + super.onDefineMiddlewareTo(target) + + const targetTkey = 'catch' + + const injectedDefinitions: any[] = (getMetadata(PARAMETER_PARAMKEY, this, targetTkey) || []) + .filter(p => p.type === 'inject') + + for (const { serviceType, targetKey, parameterIndex } of injectedDefinitions) { + if (typeof serviceType.onDefineInjectTo === 'function') { + serviceType.onDefineInjectTo(target, targetTkey, parameterIndex) + } + } + } +} diff --git a/src/classes/middleware/preHook.ts b/src/classes/middleware/preHook.ts new file mode 100644 index 0000000..8ee56f4 --- /dev/null +++ b/src/classes/middleware/preHook.ts @@ -0,0 +1,3 @@ +import { Hook } from './hook' + +export class PreHook extends Hook { } \ No newline at end of file diff --git a/src/classes/resource.ts b/src/classes/resource.ts new file mode 100644 index 0000000..878e6ec --- /dev/null +++ b/src/classes/resource.ts @@ -0,0 +1,18 @@ +import { defineMetadata, getMetadata, constants } from '../annotations' +const { CLASS_ENVIRONMENTKEY } = constants + +export class Resource { + public constructor(...params) { } + public static factory(...params) { return new this(...params) } + + public static onDefineInjectTo(target, targetKey, parameterIndex: number) { + const metadata = getMetadata(CLASS_ENVIRONMENTKEY, target) || {} + const injectMetadata = getMetadata(CLASS_ENVIRONMENTKEY, this) || {} + if (injectMetadata) { + Object.keys(injectMetadata).forEach((key) => { + metadata[key] = injectMetadata[key] + }) + defineMetadata(CLASS_ENVIRONMENTKEY, { ...metadata }, target); + } + } +} \ No newline at end of file diff --git a/src/classes/service.ts b/src/classes/service.ts index 46203db..f3bf466 100644 --- a/src/classes/service.ts +++ b/src/classes/service.ts @@ -1,4 +1,55 @@ -export class Service { - constructor(...params) { } - public static factory(...params) { return new this(...params) } +import { Resource } from './resource' +import { callExtension } from '../classes/core/callExtension' +import { constants, getMetadata, getOverridableMetadata } from '../annotations' +const { PARAMETER_PARAMKEY } = constants + +import { InProcProvider } from '../providers/inProc' +import { container } from '../helpers/ioc' + +const provider = container.resolve(InProcProvider) + +export class Service extends Resource { + public static handle(...params) { + + } + + public static async invoke(params?, invokeConfig?) { + const availableParams = {} + const parameterMapping = (getOverridableMetadata(PARAMETER_PARAMKEY, this, 'handle') || []) + parameterMapping.forEach((target) => { + if (params && target && target.type === 'param') { + availableParams[target.from] = params[target.from] + } + }) + + await callExtension(this, `onInvoke`, { + invokeParams: params, + params: availableParams, + invokeConfig, + parameterMapping + }) + + const invoker = provider.getInvoker(this, undefined, invokeConfig && invokeConfig.context) + + return await invoker(availableParams) + } + + public static async onInject({ parameter, context }): Promise { + const injectableType = container.resolveType(this) + return (params?, invokeConfig?) => injectableType.invoke(params, { ...(invokeConfig || {}), context: context.context }) + } + + public static onDefineInjectTo(target, targetKey, parameterIndex: number) { + super.onDefineInjectTo(target, targetKey, parameterIndex) + + const targetTkey = 'handle' + const injectedDefinitions: any[] = (getMetadata(PARAMETER_PARAMKEY, this, targetTkey) || []) + .filter(p => p.type === 'inject') + + for (const { serviceType, targetKey, parameterIndex } of injectedDefinitions) { + if (typeof serviceType.onDefineInjectTo === 'function') { + serviceType.onDefineInjectTo(target, targetTkey, parameterIndex) + } + } + } } \ No newline at end of file diff --git a/src/cli/commands/deploy.ts b/src/cli/commands/deploy.ts index 6ed76ea..2c8dfcd 100644 --- a/src/cli/commands/deploy.ts +++ b/src/cli/commands/deploy.ts @@ -1,27 +1,41 @@ -import { deploy } from '../utilities/deploy' -import { createContext } from '../context' +export default ({ createContext, executor, ExecuteStep, projectConfig, requireValue }) => { + return { + commands({ commander }) { + commander + .command('deploy [target] [path]') + .description('deploy functional services') + .option('--aws-region ', 'AWS_REGION') + .option('--aws-bucket ', 'aws bucket') + .option('--stage ', 'stage') + .action(async (target, path, command) => { + try { + const entryPoint = requireValue(path || projectConfig.main, 'entry point') + const deployTarget = requireValue(target || projectConfig.deployTarget, 'missing deploy target') + const awsRegion = requireValue(command.awsRegion || projectConfig.awsRegion, 'awsRegion') + const awsBucket = command.awsBucket || projectConfig.awsBucket + const stage = command.stage || projectConfig.stage || 'dev' -export const init = (commander) => { - commander - .command('deploy ') - .description('deploy functional services') - .option('--aws-region ', 'AWS_REGION') - .option('--aws-bucket ', 'aws bucket') - .action(async (target, path, command) => { - process.env.FUNCTIONAL_ENVIRONMENT = 'deploy' + process.env.FUNCTIONAL_ENVIRONMENT = deployTarget + process.env.FUNCTIONAL_STAGE = stage - const context = await createContext(path, { - deployTarget: target, - awsRegion: command.awsRegion, - awsBucket: command.awsBucket - }) + const context = await createContext(entryPoint, { + deployTarget, + awsRegion, + awsBucket, + version: projectConfig.version, + projectName: projectConfig.name, + stage + }) + await context.init() - try { - await deploy(context) + await executor(context, ExecuteStep.get('CreateEnvironment')) - console.log(`done`) - } catch (e) { - console.log(`error`, e) - } - }); + console.log(`done`) + } catch (e) { + console.log(`error`, e) + process.exit(1) + } + }); + } + } } \ No newline at end of file diff --git a/src/cli/commands/local.ts b/src/cli/commands/local.ts deleted file mode 100644 index 9cff99d..0000000 --- a/src/cli/commands/local.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { local } from '../utilities/local' -import { createContext } from '../context' - -export const init = (commander) => { - commander - .command('local ') - .description('run functional service local') - .action(async (port, path, command) => { - process.env.FUNCTIONAL_ENVIRONMENT = 'local' - - const context = await createContext(path, { - deployTarget: 'local', - localPort: port - }) - - try { - await local(context) - - console.log(`done`) - } catch (e) { - console.log(`error`, e) - } - }); -} \ No newline at end of file diff --git a/src/cli/commands/metadata.ts b/src/cli/commands/metadata.ts index c6e8dca..9cf7928 100644 --- a/src/cli/commands/metadata.ts +++ b/src/cli/commands/metadata.ts @@ -1,36 +1,36 @@ -import { local } from '../utilities/local' -import { createContext } from '../context' -import { getMetadata, getMetadataKeys } from '../../annotations' +export default ({ createContext, executor, ExecuteStep, annotations: { getMetadata, getMetadataKeys }, projectConfig, requireValue }) => { -export const init = (commander) => { - commander - .command('metadata ') - .description('service metadata') - .action(async (target, path, command) => { - process.env.FUNCTIONAL_ENVIRONMENT = 'deploy' + return { + commands({ commander }) { + commander + .command('metadata [target] [path]') + .option('--stage ', 'stage') + .description('service metadata') + .action(async (target, path, command) => { - const context = await createContext(path, { - deployTarget: target - }) + try { + const entryPoint = requireValue(path || projectConfig.main, 'entry point') + const deployTarget = requireValue(target || projectConfig.deployTarget, 'missing deploy target') + const stage = command.stage || projectConfig.stage || 'dev' - try { + process.env.FUNCTIONAL_ENVIRONMENT = deployTarget + process.env.FUNCTIONAL_STAGE = stage - console.log(JSON.stringify(context, null, 4)) + const context = await createContext(entryPoint, { + deployTarget + }) + await context.init() - for (let serviceDefinitions of context.publishedFunctions) { - let keys = getMetadataKeys(serviceDefinitions.service) - let metadata = {} - for (let key of keys) { - metadata[key] = getMetadata(key, serviceDefinitions.service) - } - - console.log(serviceDefinitions.handler, JSON.stringify(metadata, null, 4)) - } + await executor(context, ExecuteStep.get('ServiceMetadata')) + console.log(JSON.stringify(context.serviceMetadata, null, 4)) - console.log(`done`) - } catch (e) { - console.log(`error`, e) - } - }); -} \ No newline at end of file + console.log(`done`) + } catch (e) { + console.log(`error`, e) + process.exit(1) + } + }); + } + } +} diff --git a/src/cli/commands/package.ts b/src/cli/commands/package.ts new file mode 100644 index 0000000..9860e9c --- /dev/null +++ b/src/cli/commands/package.ts @@ -0,0 +1,42 @@ +export default ({ createContext, executor, ExecuteStep, projectConfig, requireValue }) => { + return { + commands({ commander }) { + commander + .command('package [target] [path]') + .description('package functional services') + .option('--aws-region ', 'AWS_REGION') + .option('--aws-bucket ', 'aws bucket') + .option('--stage ', 'stage') + .action(async (target, path, command) => { + try { + const entryPoint = requireValue(path || projectConfig.main, 'entry point') + const deployTarget = requireValue(target || projectConfig.deployTarget, 'missing deploy target') + const awsRegion = requireValue(command.awsRegion || projectConfig.awsRegion, 'awsRegion') + const awsBucket = command.awsBucket || projectConfig.awsBucket + const stage = command.stage || projectConfig.stage || 'dev' + + process.env.FUNCTIONAL_ENVIRONMENT = deployTarget + process.env.FUNCTIONAL_STAGE = stage + + const context = await createContext(entryPoint, { + deployTarget, + awsRegion, + awsBucket, + version: projectConfig.version, + projectName: projectConfig.name, + packageOnly: true, + stage + }) + await context.init() + + await executor(context, ExecuteStep.get('CreateEnvironment')) + + console.log(`done`) + } catch (e) { + console.log(`error`, e) + process.exit(1) + } + }); + } + } +} \ No newline at end of file diff --git a/src/cli/commands/serverless.ts b/src/cli/commands/serverless.ts new file mode 100644 index 0000000..13ef3af --- /dev/null +++ b/src/cli/commands/serverless.ts @@ -0,0 +1,244 @@ +import { writeFileSync } from 'fs' +import { extname } from 'path' +import { stringify } from 'yamljs' + +export const FUNCTIONAL_ENVIRONMENT = 'aws' + +export default (api) => { + const { + createContext, + annotations: { + getMetadata, getMetadataKeys, getFunctionName, + constants: { + CLASS_AWSRUNTIMEKEY, + CLASS_ENVIRONMENTKEY, + CLASS_APIGATEWAYKEY + }, + __dynamoDBDefaults + }, + projectConfig, + requireValue, resolvePath, + ExecuteStep, executor + } = api + + const serverlessConfig = async (context) => { + context.serverless = { + service: projectConfig.name || 'unknown' + } + } + + const providerConfig = async (context) => { + context.serverless.provider = { + name: FUNCTIONAL_ENVIRONMENT, + region: context.awsRegion, + stage: context.stage, + environment: {} + } + await executor({ context, name: 'iamRoleConfig', method: iamRoleConfig }) + } + + const iamRoleConfig = async (context) => { + context.serverless.provider.iamRoleStatements = [ + { + Effect: 'Allow', + Action: [ + 'lambda:InvokeAsync', + 'lambda:InvokeFunction' + ], + Resource: ['*'] + } + ] + if (context.tableConfigs && context.tableConfigs.length) { + const dynamoStatement = { + Effect: 'Allow', + Action: [ + 'dynamodb:Query', + 'dynamodb:Scan', + 'dynamodb:GetItem', + 'dynamodb:PutItem', + 'dynamodb:UpdateItem' + ], + Resource: [] + } + + for (const tableConfig of context.tableConfigs) { + const properties = { TableName: tableConfig.tableName, ...tableConfig.nativeConfig } + dynamoStatement.Resource.push("arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/" + properties.TableName + '-' + context.stage) + } + + context.serverless.provider.iamRoleStatements.push(dynamoStatement) + } + } + + const functionsConfig = async (context) => { + context.serverless.functions = {} + + for (const serviceDefinition of context.publishedFunctions) { + await executor({ + context: { ...context, serviceDefinition }, + name: 'functionExport', + method: functionExport + }) + } + } + + const functionExport = async (context) => { + const { serviceDefinition, serverless } = context + + const functionName = getFunctionName(serviceDefinition.service) + const executePath = process.cwd() + let handler: string = serviceDefinition.file.replace(executePath, '') + const ext = extname(handler) + const nameKey = handler.substring(1, handler.length - ext.length).replace(/\\/g, '/') + + const def = serverless.functions[functionName] = { + handler: `${nameKey}.${serviceDefinition.exportName}`, + runtime: getMetadata(CLASS_AWSRUNTIMEKEY, serviceDefinition.service) || "nodejs20.x" + } + + await executor({ context, name: 'funtionEnvironments', method: funtionEnvironments }) + await executor({ context, name: 'funtionEvents', method: funtionEvents }) + } + + const functional_service_regexp = /^FUNCTIONAL_SERVICE_/ + const funtionEnvironments = async ({ serviceDefinition, serverless }) => { + const functionName = getFunctionName(serviceDefinition.service) + const environments = getMetadata(CLASS_ENVIRONMENTKEY, serviceDefinition.service) + if (environments) { + serverless.functions[functionName].environment = {} + for (const key in environments) { + let environmentValue = environments[key] + if (functional_service_regexp.test(key)) { + environmentValue = `${serverless.service}-${serverless.provider.stage}-${environmentValue}` + } + serverless.functions[functionName].environment[key] = environmentValue + } + } + + } + + const funtionEvents = ({ serviceDefinition, serverless }) => { + const functionName = getFunctionName(serviceDefinition.service) + let httpMetadata = getMetadata(CLASS_APIGATEWAYKEY, serviceDefinition.service) || [] + for (const { method, path, cors } of httpMetadata) { + const resourcePath = /^\//.test(path) ? path.substring(1, path.length) : path + const defs = serverless.functions[functionName].events = serverless.functions[functionName].events || [] + const def: any = { + http: { + method, + path: resourcePath + } + } + + if (cors) { + def.http.cors = { + origins: '*' + } + } + defs.push(def) + } + } + + const resourcesConfig = async (context) => { + context.serverless.resources = { + Resources: {} + } + + for (const tableDefinition of context.tableConfigs) { + await executor({ + context: { ...context, tableConfig: tableDefinition }, + name: 'tableConfig', + method: tableConfiguration + }) + } + + } + + const tableConfiguration = async ({ stage, tableConfig, serverless }) => { + const properties = { + ...__dynamoDBDefaults, + TableName: tableConfig.tableName, + ...tableConfig.nativeConfig + } + + const resName = properties.TableName + properties.TableName += '-' + stage + + const tableResource = { + "Type": "AWS::DynamoDB::Table", + "Properties": properties + } + + const name = normalizeName(resName) + + serverless.resources.Resources[name] = tableResource + } + + const saveConfig = async ({ serverless }) => { + writeFileSync('serverless.yml', stringify(serverless, Number.MAX_SAFE_INTEGER)) + } + + const build = async (context) => { + await executor({ context, name: 'serverlessConfig', method: serverlessConfig }) + await executor({ context, name: 'providerConfig', method: providerConfig }) + await executor({ context, name: 'functionsConfig', method: functionsConfig }) + await executor({ context, name: 'resourcesConfig', method: resourcesConfig }) + await executor({ context, name: 'saveConfig', method: saveConfig }) + } + + + + return { + commands({ commander }) { + commander + .command('serverless [path]') + .alias('sls') + .option('--aws-region ', 'AWS_REGION') + .option('--stage ', 'stage') + .description('serverless config') + .action(async (path, command) => { + + try { + const entryPoint = requireValue(path || projectConfig.main, 'entry point') + const awsRegion = requireValue(command.awsRegion || projectConfig.awsRegion, 'awsRegion') + const stage = command.stage || projectConfig.stage || 'dev' + + process.env.FUNCTIONAL_ENVIRONMENT = FUNCTIONAL_ENVIRONMENT + process.env.FUNCTIONAL_STAGE = stage + + const context = await createContext(entryPoint, { + deployTarget: FUNCTIONAL_ENVIRONMENT, + awsRegion, + FUNCTIONAL_ENVIRONMENT, + stage + }) + await context.init() + + await executor(context, ExecuteStep.get('SetFunctionalEnvironment')) + + await build(context) + + console.log(`done`) + } catch (e) { + console.log(`error`, e) + process.exit(1) + } + }); + }, + deployProviders: { + sls: { + FUNCTIONAL_ENVIRONMENT, + createEnvironment: build + } + } + } +} + +export const nameReplaceRegexp = /[^a-zA-Z0-9]/g +export const normalizeName = (name: string) => { + const result = name.replace(nameReplaceRegexp, '') + if (!result) { + throw new Error(`'invalid name '${name}'`) + } + return result +} diff --git a/src/cli/commands/start.ts b/src/cli/commands/start.ts new file mode 100644 index 0000000..5db3269 --- /dev/null +++ b/src/cli/commands/start.ts @@ -0,0 +1,161 @@ +import * as express from 'express' +import * as bodyParser from 'body-parser' +import * as cors from 'cors' + +export default ({ createContext, annotations: { getMetadata, constants, getFunctionName, rest }, projectConfig, requireValue, executor }) => { + + const pathPattern = /\{([^+\}]+)(\+?)\}/ + const pathTransform = (path, middlewares, idx = 0) => { + const match = pathPattern.exec(path) + if (match) { + if (match[2]) { + path = path.replace(match[0], '*') + middlewares.push((req, res, next) => { + req.params[match[1]] = req.params[idx] + next() + }) + return pathTransform(path, middlewares, idx + 1) + } else { + path = path.replace(match[0], `:${match[1]}`) + return pathTransform(path, middlewares, idx) + } + } + + return path; + } + + const startLocal = async (context) => { + let app = express() + app.use(bodyParser.json({ limit: '10mb' })) + app.use(bodyParser.urlencoded({ extended: true })); // support encoded bodies + console.log("") + for (let serviceDefinition of context.publishedFunctions) { + let httpMetadata = getMetadata(rest.environmentKey, serviceDefinition.service) || [] + + for (let event of httpMetadata) { + const isLoggingEnabled = getMetadata(constants.CLASS_LOGKEY, serviceDefinition.service) + const transformMiddlewares = [] + const path = pathTransform(event.path, transformMiddlewares) + if (event.cors) { + app.use(path, cors()) + } + + for (const method of event.methods) { + console.log(`${method.toUpperCase()}\t${event.path}\t${getFunctionName(serviceDefinition.service)}`) + + const expressMethod = method.toLowerCase() === 'any' ? 'use' : method.toLowerCase() + app[expressMethod]( + path, + logMiddleware(isLoggingEnabled, serviceDefinition.service), + environmentConfigMiddleware(serviceDefinition.service), + ...transformMiddlewares, + serviceDefinition.invoker + ) + } + } + } + console.log("") + + return app.listen(context.localPort, function () { + process.env.FUNCTIONAL_LOCAL_PORT = context.localPort + console.log(`App listening on port ${process.env.FUNCTIONAL_LOCAL_PORT}!`) + }) + } + + const logMiddleware = (enabled, serviceType) => { + return (req, res, next) => { + if (!enabled) return next() + + console.log(`${new Date().toISOString()} ${getFunctionName(serviceType)}`, JSON.stringify({ + url: req.url, + query: req.query, + params: req.params, + body: req.body, + headers: req.headers + }, null, 2)) + + next() + } + } + + const environmentConfigMiddleware = (serviceType) => { + return (req, res, next) => { + const environmentVariables = getMetadata(constants.CLASS_ENVIRONMENTKEY, serviceType) || {} + + let originals = {} + Object.keys(environmentVariables).forEach((key) => { + if (key in process.env) { + originals[key] = process.env[key] + } + + process.env[key] = environmentVariables[key] + }) + + + req.on("end", function () { + Object.keys(environmentVariables).forEach((key) => { + if (key in originals) { + process.env[key] = originals[key] + } else { + delete process.env[key] + } + }) + }); + + next() + } + } + + const startServer = async (context) => { + try { + await context.init() + return await executor(context, { name: 'startLocal', method: startLocal }) + } catch (e) { + console.log(`error`, e) + } + } + + return { + commands({ commander }) { + commander + .command('start [port] [path]') + .description('run functional service local') + .option('--stage ', 'stage') + .action(async (port, path, command) => { + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + + try { + const entryPoint = requireValue(path || projectConfig.main, 'entry point') + const localPort = requireValue(port || projectConfig.localPort, 'localPort') + const stage = command.stage || projectConfig.stage || 'dev' + + process.env.FUNCTIONAL_STAGE = stage + + let server = null + const context = await createContext(entryPoint, { + deployTarget: 'local', + localPort, + stage, + watchCallback: async (ctx) => { + if (server) { + server.close() + } + + server = await startServer(ctx) + + console.log(`Compilation complete. Watching for file changes.`) + + } + }) + + await startServer(context) + + console.log(`Compilation complete.`) + } catch (e) { + console.log(`error`, e) + process.exit(1) + } + }); + } + } +} \ No newline at end of file diff --git a/src/cli/context/core/executeStep.ts b/src/cli/context/core/executeStep.ts new file mode 100644 index 0000000..fb22fa5 --- /dev/null +++ b/src/cli/context/core/executeStep.ts @@ -0,0 +1,37 @@ +export const executeSteppes = new Map any }>() + +export class ExecuteStep { + public name: string + public constructor(name) { + this.name = name + + if (executeSteppes.has(this.name)) { + throw new Error(`step name '${this.name}' already defined`) + } + executeSteppes.set(this.name, this) + } + + public method(context) { + + } + + public static register(name, method) { + if (executeSteppes.get(name)) { + throw new Error(`step name '${name}' already defined`) + } + + const step = { + name, + method + } + + executeSteppes.set(name, step) + return step + } + + public static get(name: string) { + if (executeSteppes.has(name)) { + return executeSteppes.get(name) + } + } +} \ No newline at end of file diff --git a/src/cli/context/core/executor.ts b/src/cli/context/core/executor.ts new file mode 100644 index 0000000..923cf4e --- /dev/null +++ b/src/cli/context/core/executor.ts @@ -0,0 +1,61 @@ + +import { logger } from '../../utilities/logger' +import { projectConfig } from '../../project/config' +import { + cliCallContextHooks, hasCliCallContextHooks, CONTEXT_HOOK_MODIFIER_BEFORE, CONTEXT_HOOK_MODIFIER_AFTER, + callContextHookStep +} from '../../extensions/hooks' + +let depth = 0 +export const executor = async (context, step?: Function | any) => { + if (!step) { + step = context; + context = context.context + } + + let result = undefined + if (typeof step === 'function') { + result = await step(context) + } else if (step && step.name && typeof step.method === 'function') { + const tab = depth++ + const separator = (s = ' ') => { + let result = '' + for (var i = 0; i < tab; i++) { + result += s + } + return result + } + + try { + + if (projectConfig.debug) logger.debug(`executor | run -----------${separator('--')}> ${step.name}`) + if (projectConfig.debug) logger.debug(`executor | before start ${separator()} ${step.name}`) + await cliCallContextHooks(step.name, context, CONTEXT_HOOK_MODIFIER_BEFORE) + if (projectConfig.debug) logger.debug(`executor | before end ${separator()} ${step.name}`) + + if (projectConfig.debug) logger.debug(`executor | start ${separator()} ${step.name}`) + if (hasCliCallContextHooks(step.name, context)) { + result = await cliCallContextHooks(step.name, context) + } else { + result = await step.method(context) + } + if (projectConfig.debug) logger.debug(`executor | end ${separator()} ${step.name}`) + + if (projectConfig.debug) logger.debug(`executor | after start ${separator()} ${step.name}`) + await cliCallContextHooks(step.name, context, CONTEXT_HOOK_MODIFIER_AFTER) + if (projectConfig.debug) logger.debug(`executor | after end ${separator()} ${step.name}`) + if (projectConfig.debug) logger.debug(`executor | complete ------${separator('--')}> ${step.name}`) + } + catch (e) { + if (projectConfig.debug) logger.debug(`executor | exited --------${separator('--')}> ${step.name}`) + depth-- + throw e + } + depth-- + } else { + throw new Error(`runStep has invalid parameter '${step}'`) + } + return result +} + + diff --git a/src/cli/context/core/serviceDiscovery.ts b/src/cli/context/core/serviceDiscovery.ts deleted file mode 100644 index ca472c5..0000000 --- a/src/cli/context/core/serviceDiscovery.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { lstat, readdirSync } from 'fs' -import { FunctionalService } from '../../../classes/functionalService' - -import { join, basename, extname } from 'path' -import { set } from 'lodash' - - - -export const serviceDiscovery = async (context) => { - context.files = context.files || [] - context.publishedFunctions = context.publishedFunctions || [] - - const path = context.serviceRoot - - let isDir = await isDirectory(path) - - let files = [path] - if (isDir) { - files = await getJsFiles(path) - } - - for (let file of files) { - collectFromFile(file, context) - } - - return context -} - -export const collectFromFile = (file, context) => { - const module = require(file) - - const name = basename(file) - const ext = extname(name) - const nameKey = name.substring(0, name.length - ext.length) - - Object.keys(module).forEach((key) => { - let exportItem = module[key] - - if (exportItem.serviceType && exportItem.serviceType.createInvoker) { - const item = { - service: exportItem.serviceType, - exportName: key, - invoker: exportItem, - handler: `${nameKey}.${key}`, - } - - if (context.files.indexOf(file) < 0) { - context.files.push(file) - } - - context.publishedFunctions.push(item) - } - - }) -} - -export const getJsFiles = (folder) => { - let files = readdirSync(folder); - let filter = /\.js$/ - return files.filter(name => filter.test(name)).map(file => join(folder, file)) -} - - - -export const isDirectory = (path) => { - return new Promise((resolve, reject) => { - lstat(path, (err, stats) => { - if (err) return reject(err); - return resolve(stats.isDirectory()) - }); - }) -} \ No newline at end of file diff --git a/src/cli/context/core/setFunctionalEnvironment.ts b/src/cli/context/core/setFunctionalEnvironment.ts deleted file mode 100644 index ba600ad..0000000 --- a/src/cli/context/core/setFunctionalEnvironment.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { environment } from '../../../annotations' - -export const setFunctionalEnvironment = async (context) => { - for (let serviceDefinition of context.publishedFunctions) { - const setEnvAttrib = environment('FUNCTIONAL_ENVIRONMENT', context.FUNCTIONAL_ENVIRONMENT) - setEnvAttrib(serviceDefinition.service) - } -} \ No newline at end of file diff --git a/src/cli/context/core/tableDiscovery.ts b/src/cli/context/core/tableDiscovery.ts deleted file mode 100644 index ed09770..0000000 --- a/src/cli/context/core/tableDiscovery.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { merge } from 'lodash' -import { getMetadata, constants, __dynamoDBDefaults } from '../../../annotations' - -export const tableNameEnvRegexp = /_TABLE_NAME$/ -export const tableDiscovery = async (context) => { - let tablesToCreate = new Map() - - for (let serviceDefinition of context.publishedFunctions) { - let tableConfigs = getMetadata(constants.CLASS_DYNAMOTABLECONFIGURATIONKEY, serviceDefinition.service) || [] - for (const tableConfig of tableConfigs) { - if (tablesToCreate.has(tableConfig.tableName)) { - continue - } - - tablesToCreate.set(tableConfig.tableName, tableConfig) - } - - let metadata = getMetadata(constants.CLASS_ENVIRONMENTKEY, serviceDefinition.service) - if (metadata) { - let keys = Object.keys(metadata) - for (const key of keys) { - if (tableNameEnvRegexp.test(key) && !tablesToCreate.has(metadata[key])) { - tablesToCreate.set(metadata[key], { - TableName: metadata[key] - }) - } - } - } - } - - context.tableConfigs = Array.from(tablesToCreate.values()) -} \ No newline at end of file diff --git a/src/cli/context/index.ts b/src/cli/context/index.ts index 5ab08b8..709b7b9 100644 --- a/src/cli/context/index.ts +++ b/src/cli/context/index.ts @@ -1,27 +1,50 @@ -export { serviceDiscovery } from './core/serviceDiscovery' -export { tableDiscovery } from './core/tableDiscovery' -export { setFunctionalEnvironment } from './core/setFunctionalEnvironment' +export { ExecuteStep } from './core/executeStep' +export { executor } from './core/executor' +import { serviceDiscovery } from './steppes/serviceDiscovery' +import { tableDiscovery } from './steppes/tableDiscovery' +import './steppes/setFunctionalEnvironment' +import './steppes/metadata/serviceMetadata' import { resolvePath } from '../utilities/cli' -import { defaults } from 'lodash' +import { callContextHookStep } from '../extensions/hooks' +import { executor } from './core/executor' +import { codeCompile } from './steppes/codeCompile' -import { serviceDiscovery } from './core/serviceDiscovery' -import { tableDiscovery } from './core/tableDiscovery' +export const getDefaultSteppes = (): any[] => { + return [ + callContextHookStep, + codeCompile + ] +} -export const tasks = [ - serviceDiscovery, - tableDiscovery -] +export const getInitSteppes = (): any[] => { + return [ + serviceDiscovery, + tableDiscovery + ] +} export const createContext = async (path, defaultValues) => { - const context = defaults({}, { + const context = { + ...defaultValues, serviceRoot: resolvePath(path), - date: new Date() - }, defaultValues) + date: new Date(), + runStep: async function (step) { return await executor(this, step) }, + - for (const t of tasks) { - await t(context) + init: async () => { + const initSteppes = getInitSteppes() + for (const step of initSteppes) { + await executor(context, step) + } + } + } + + const defaultSteppes = getDefaultSteppes() + for (const step of defaultSteppes) { + await executor(context, step) } return context } + diff --git a/src/cli/context/steppes/codeCompile.ts b/src/cli/context/steppes/codeCompile.ts new file mode 100644 index 0000000..f81d81f --- /dev/null +++ b/src/cli/context/steppes/codeCompile.ts @@ -0,0 +1,183 @@ +import * as webpack from 'webpack' +import { config } from '../../utilities/config' +import { basename, extname, join } from 'path' +import { lstat, readdirSync } from 'fs' +import { ExecuteStep } from '../core/executeStep' +import { executor } from '../core/executor' + +import { projectConfig } from '../../project/config' + +export class CodeCompile extends ExecuteStep { + public async method(context) { + const path = context.serviceRoot + + let isDir = await this.isDirectory(path) + + let files = [path] + if (isDir) { + files = await this.getJsFiles(path) + } + context.files = files + + await executor(context, bundle) + + return context + } + + private getJsFiles(folder) { + let files = readdirSync(folder); + let filter = /\.js|\.ts$/ + return files.filter(name => filter.test(name)).map(file => join(folder, file)) + } + private isDirectory(path) { + return new Promise((resolve, reject) => { + lstat(path, (err, stats) => { + if (err) return reject(err); + return resolve(stats.isDirectory()) + }); + }) + } + +} + +export const codeCompile = new CodeCompile('CodeCompile') + +export const bundle = ExecuteStep.register('WebpackBundle', (context) => { + return new Promise(async (resolve, reject) => { + const webpackConfig = await executor(context, bundleConfig) + let buildHash = '' + + const done = (err?, res?) => { + const isWatch = context.watchCallback && webpackConfig.watch + if (err) { + if (isWatch) { + // watch mode, wait to fix + return + } + + return reject(err) + } + + if (isWatch) { + context.watchCallback(context) + } else { + return resolve(res) + } + } + + try { + webpack(webpackConfig, function (err, stats) { + if (err) return done(err) + + if (buildHash === stats.hash) return + if (buildHash && context.watchCallback) context.clearRequireCache = true + buildHash = stats.hash + + let jsonStats = stats.toJson(); + if (jsonStats.errors.length > 0) { + console.log('WEBPACK ERROR') + jsonStats.errors.forEach(msg => console.log(msg)) + return done(jsonStats.errors); + } + // if (jsonStats.warnings.length > 0) { + // console.log('WEBPACK WARNINGS') + // console.log(jsonStats.warnings) + // } + + context.originalFiles = context.files + context.files = [] + Object.keys(jsonStats.entrypoints).forEach((entryKey) => { + let entry = jsonStats.entrypoints[entryKey] + let assets = entry.assets.map((file) => join(webpackConfig.output.path, file)) + context.files.push(...assets) + }) + + done() + }); + } catch (e) { + return done(e) + } + }) +}) + +export const bundleConfig = ExecuteStep.register('WebpackBundleConfig', async (context) => { + let entry = {} + context.files.forEach((file) => { + let name = basename(file) + const ext = extname(name) + const nameKey = name.substring(0, name.length - ext.length) + entry[nameKey] = file + }) + + const externals = {} + + let compile = {} + if (projectConfig.compile) { + compile = await executor({ context, projectConfig }, bundleCompileConfig) + } + let watch = {} + if (context.watchCallback && projectConfig.watch) { + watch = await executor({ context, projectConfig }, watchConfig) + } + + const webpackConfig = { + ...config.webpack, + ...compile, + ...watch, + ...projectConfig.webpack, + entry + } + + webpackConfig.externals = { ...webpackConfig.externals, ...externals } + + if (!context.watchCallback && webpackConfig.watch) { + delete webpackConfig.watch + } + + return webpackConfig +}) + +export const bundleCompileConfig = ExecuteStep.register('WebpackCompileConfig', (context) => { + switch (context.projectConfig.compile) { + case "ts-loader": + return { + module: { + rules: [ + { + test: /\.tsx?$/, + use: 'ts-loader', + exclude: /node_modules/ + } + ] + }, + resolve: { + extensions: ['.tsx', '.ts', '.js'] + } + } + case "babel-loader": + return { + module: { + rules: [ + { + test: /\.jsx?$/, + use: 'babel-loader', + exclude: /node_modules/ + } + ] + } + } + default: + return {} + } +}) + +export const watchConfig = ExecuteStep.register('WebpackWatchConfig', (context) => { + return { + watch: true, + watchOptions: { + aggregateTimeout: 300, + poll: 1000, + ignored: /dist/ + } + } +}) diff --git a/src/cli/context/steppes/metadata/serviceMetadata.ts b/src/cli/context/steppes/metadata/serviceMetadata.ts new file mode 100644 index 0000000..2356e1e --- /dev/null +++ b/src/cli/context/steppes/metadata/serviceMetadata.ts @@ -0,0 +1,24 @@ +import { getMetadata, constants, __dynamoDBDefaults, getMetadataKeys } from '../../../../annotations' +import { ExecuteStep } from '../../core/executeStep' + +export class ServiceMetadata extends ExecuteStep { + public async method(context) { + context.serviceMetadata = [] + + for (const serviceDefinition of context.publishedFunctions) { + const metadataKeys = getMetadataKeys(serviceDefinition.service) + const metadata = {} + for (const key of metadataKeys) { + metadata[key] = getMetadata(key, serviceDefinition.service) + } + + context.serviceMetadata.push({ + serviceDefinition, + metadata + }) + } + } +} + + +export const serviceMetadata = new ServiceMetadata('ServiceMetadata') diff --git a/src/cli/context/steppes/serviceDiscovery.ts b/src/cli/context/steppes/serviceDiscovery.ts new file mode 100644 index 0000000..569ef1a --- /dev/null +++ b/src/cli/context/steppes/serviceDiscovery.ts @@ -0,0 +1,55 @@ +import { lstat, readdirSync } from 'fs' +import { FunctionalService } from '../../../classes/functionalService' +import { ExecuteStep } from '../core/executeStep' +import * as decache from 'decache' + +import { join, basename, extname } from 'path' + +export class ServiceDiscoveryStep extends ExecuteStep { + public async method(context) { + + context.publishedFunctions = [] + + for (let file of context.files) { + this.collectFromFile(file, context) + } + + return context + } + + private collectFromFile(file, context) { + if (context.clearRequireCache) { + decache(file); + } + + const module = require(file) + + const name = basename(file) + const ext = extname(name) + const nameKey = name.substring(0, name.length - ext.length) + + Object.keys(module).forEach((key) => { + let exportItem = module[key] + + if (exportItem.serviceType && exportItem.serviceType.createInvoker) { + const item = { + service: exportItem.serviceType, + exportName: key, + fileName: nameKey, + invoker: exportItem, + handler: `${nameKey}.${key}`, + file + } + + if (context.files.indexOf(file) < 0) { + context.files.push(file) + } + + context.publishedFunctions.push(item) + } + + }) + } +} + +export const serviceDiscovery = new ServiceDiscoveryStep('ServiceDiscovery') diff --git a/src/cli/context/steppes/setFunctionalEnvironment.ts b/src/cli/context/steppes/setFunctionalEnvironment.ts new file mode 100644 index 0000000..193486b --- /dev/null +++ b/src/cli/context/steppes/setFunctionalEnvironment.ts @@ -0,0 +1,16 @@ +import { environment } from '../../../annotations' +import { ExecuteStep } from '../core/executeStep' +export class SetFunctionalEnvironmentStep extends ExecuteStep { + public async method(context) { + for (let serviceDefinition of context.publishedFunctions) { + const setFuncEnvEnvAttrib = environment('FUNCTIONAL_ENVIRONMENT', context.FUNCTIONAL_ENVIRONMENT) + const setStageEnvAttrib = environment('FUNCTIONAL_STAGE', context.stage) + const setProjectNameEnvAttrib = environment('FUNCTIONAL_PROJECTNAME', context.projectName) + setFuncEnvEnvAttrib(serviceDefinition.service) + setStageEnvAttrib(serviceDefinition.service) + setProjectNameEnvAttrib(serviceDefinition.service) + } + } +} + +export const setFunctionalEnvironment = new SetFunctionalEnvironmentStep('SetFunctionalEnvironment') diff --git a/src/cli/context/steppes/tableDiscovery.ts b/src/cli/context/steppes/tableDiscovery.ts new file mode 100644 index 0000000..21aff7a --- /dev/null +++ b/src/cli/context/steppes/tableDiscovery.ts @@ -0,0 +1,16 @@ +import { getMetadata, constants } from '../../../annotations' +import { ExecuteStep } from '../core/executeStep' +import { collectMetadata } from '../../utilities/collectMetadata' +const { CLASS_DYNAMOTABLECONFIGURATIONKEY, CLASS_ENVIRONMENTKEY } = constants + +export class TableDiscoveryStep extends ExecuteStep { + public async method(context) { + context.tableConfigs = collectMetadata(context, { + metadataKey: CLASS_DYNAMOTABLECONFIGURATIONKEY, + selector: (c) => c.tableName + }) + } +} + + +export const tableDiscovery = new TableDiscoveryStep('TableDiscovery') diff --git a/src/cli/extensions/classExtensions.ts b/src/cli/extensions/classExtensions.ts new file mode 100644 index 0000000..54420f4 --- /dev/null +++ b/src/cli/extensions/classExtensions.ts @@ -0,0 +1,14 @@ +import { callExtension } from '../../classes' +import { getPluginDefinitions } from '../project/config' + +export const cliCallExtension = (target: any, method: string, ...params) => { + callExtension(target, method, ...params) + + const pluginDefinitions = getPluginDefinitions() + for (const plugin of pluginDefinitions) { + if (plugin.config.extensions && typeof plugin.config.extensions[method] === 'function') { + + plugin.config.extensions[method](...params) + } + } +} \ No newline at end of file diff --git a/src/cli/extensions/hooks.ts b/src/cli/extensions/hooks.ts new file mode 100644 index 0000000..4ebb0d7 --- /dev/null +++ b/src/cli/extensions/hooks.ts @@ -0,0 +1,54 @@ +import { getPluginDefinitions, projectConfig } from '../project/config' +import { logger } from '../utilities/logger' + +export const CONTEXT_HOOK_MODIFIER_BEFORE = 'before' +export const CONTEXT_HOOK_MODIFIER_AFTER = 'after' +export const CONTEXT_HOOK_PROPERTIES = '__hooks' +export const CONTEXT_HOOK_DEFAULT_MODIFIER = 'default' +export const CONTEXT_HOOK_SEPARATOR = ':' + +export const hasCliCallContextHooks = (name, context, modifier = CONTEXT_HOOK_DEFAULT_MODIFIER) => { + return context && + context[CONTEXT_HOOK_PROPERTIES] && + context[CONTEXT_HOOK_PROPERTIES][modifier] && + typeof context[CONTEXT_HOOK_PROPERTIES][modifier][name] === 'function' ? true : false +} + +export const cliCallContextHooks = async (name, context, modifier = CONTEXT_HOOK_DEFAULT_MODIFIER) => { + if (context && + context[CONTEXT_HOOK_PROPERTIES] && + context[CONTEXT_HOOK_PROPERTIES][modifier] && + typeof context[CONTEXT_HOOK_PROPERTIES][modifier][name] === 'function' + ) { + return await context[CONTEXT_HOOK_PROPERTIES][modifier][name](context) + } +} + +export const callContextHookStep = (context) => { + context[CONTEXT_HOOK_PROPERTIES] = {} + + const pluginDefinitions = getPluginDefinitions() + for (const plugin of pluginDefinitions) { + if (plugin.config.hooks && typeof plugin.config.hooks === 'object') { + const keys = Object.keys(plugin.config.hooks) + + for (const key of keys) { + if (typeof plugin.config.hooks[key] !== 'function') { + logger.warn(`hook '${key}' in '${plugin.pluginInfo.path}' is invalid`) + continue + } + + let [modifier, stepName] = key.split(CONTEXT_HOOK_SEPARATOR) + if (!stepName) { + stepName = modifier + modifier = CONTEXT_HOOK_DEFAULT_MODIFIER + } + if (!stepName) continue + + context[CONTEXT_HOOK_PROPERTIES][modifier] = context[CONTEXT_HOOK_PROPERTIES][modifier] || {} + context[CONTEXT_HOOK_PROPERTIES][modifier][stepName] = plugin.config.hooks[key] + } + + } + } +} diff --git a/src/cli/index.ts b/src/cli/index.ts index e59dac5..b860d31 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -1,12 +1,22 @@ import { normalize, join } from 'path' import './utilities/config' +import './providers' -import { init as initDeployCommand } from './commands/deploy' -import { init as initLocalCommand } from './commands/local' -import { init as initMetadataCommand } from './commands/metadata' +//built-in commands +import * as deploy from './commands/deploy' +import * as deployPackage from './commands/package' +import * as start from './commands/start' +import * as metadata from './commands/metadata' +import * as serverless from './commands/serverless' + +import { init as initProjectConfig, internalPluginLoad } from './project/init' export const init = (commander) => { - initDeployCommand(commander) - initLocalCommand(commander) - initMetadataCommand(commander) + internalPluginLoad(deploy) + internalPluginLoad(deployPackage) + internalPluginLoad(start) + internalPluginLoad(metadata) + internalPluginLoad(serverless) + + initProjectConfig(commander) } \ No newline at end of file diff --git a/src/cli/project/config.ts b/src/cli/project/config.ts new file mode 100644 index 0000000..f860e71 --- /dev/null +++ b/src/cli/project/config.ts @@ -0,0 +1,19 @@ +import { merge } from 'lodash' +import { join } from 'path' + +export const projectConfig: any = {} +export const updateConfig = (values) => merge(projectConfig, values) + +export const pluginDefinitions = [] +export const setPluginDefinitions = (pluginDefinition) => pluginDefinitions.push(pluginDefinition) +export const getPluginDefinitions = () => pluginDefinitions + +try { + const packageJson = require(join(process.cwd(), 'package')) + if (packageJson && packageJson.version) { + projectConfig.version = packageJson.version + } + if (!projectConfig.name && packageJson && packageJson.name) { + projectConfig.name = packageJson.name + } +} catch (e) { } diff --git a/src/cli/project/core/loadPlugins.ts b/src/cli/project/core/loadPlugins.ts new file mode 100644 index 0000000..21b2eca --- /dev/null +++ b/src/cli/project/core/loadPlugins.ts @@ -0,0 +1,66 @@ +import { readFileSync } from 'fs' +import { logger } from '../../utilities/logger' +import { resolvePath } from '../../utilities/cli' + +export const PLUGIN_FOLDER = './node_modules/' +export const PLUGIN_PREFIX = 'functionly-plugin-' +export const PLUGIN_PATH_REGEXP = /^\./ + +const pluginToLoad = [] + +export const loadPlugins = (config) => { + const pluginInitializers: any[] = [] + + const cwd = process.cwd() + + for (const plugin of pluginToLoad) { + createPluginInitializer({ + pluginInitializers, + pluginInit: plugin, + pluginPath: null, + resolvedPluginPath: null + }) + } + + if (Array.isArray(config.plugins)) { + for (const pluginPath of config.plugins) { + + let resolvedPluginPath = null + if (PLUGIN_PATH_REGEXP.test(pluginPath)) { + resolvedPluginPath = resolvePath(pluginPath) + } else { + resolvedPluginPath = resolvePath(`${PLUGIN_FOLDER}${PLUGIN_PREFIX}${pluginPath}`) + } + + try { + const pluginInit = require(resolvedPluginPath) + + createPluginInitializer({ + pluginInitializers, + pluginInit, + pluginPath, + resolvedPluginPath + }) + + } catch (e) { + logger.error(`Plugin '${pluginPath}' not exists`, e) + } + } + } + + return pluginInitializers; +} + +export const createPluginInitializer = ({ + pluginInitializers, pluginInit, pluginPath, resolvedPluginPath +}) => { + pluginInitializers.push({ + init: pluginInit.default, + path: pluginPath, + resolvedPath: resolvedPluginPath + }) +} + +export const internalPluginLoad = (plugin) => { + pluginToLoad.push(plugin) +} \ No newline at end of file diff --git a/src/cli/project/core/readConfig.ts b/src/cli/project/core/readConfig.ts new file mode 100644 index 0000000..33e08a1 --- /dev/null +++ b/src/cli/project/core/readConfig.ts @@ -0,0 +1,17 @@ +import { join } from 'path' +import { readFileSync } from 'fs' +import { logger } from '../../utilities/logger' + +export const projectConfig = './functionly' + +export const readConfig = () => { + const cwd = process.cwd() + const configPath = join(cwd, projectConfig) + + try { + return require(configPath) + } catch (e) { + return {} + } + +} \ No newline at end of file diff --git a/src/cli/project/init.ts b/src/cli/project/init.ts new file mode 100644 index 0000000..899def2 --- /dev/null +++ b/src/cli/project/init.ts @@ -0,0 +1,59 @@ +export { internalPluginLoad } from './core/loadPlugins' +import { readConfig } from './core/readConfig' +import { loadPlugins } from './core/loadPlugins' +import { projectConfig, updateConfig, setPluginDefinitions, getPluginDefinitions } from './config' + +import { logger } from '../utilities/logger' +import { resolvePath, requireValue } from '../utilities/cli' +import { createContext, ExecuteStep, executor } from '../context' +import * as annotations from '../../annotations' + +export const pluginInitParam: any = { + logger, + resolvePath, requireValue, + createContext, ExecuteStep, executor, + annotations, + projectConfig, + getPluginDefinitions +} + +export const init = (commander) => { + + const configJson = readConfig() + updateConfig(configJson) + const plugins = loadPlugins(projectConfig) + + // init plugins + for (const pluginInfo of plugins) { + try { + + const pluginConfig = pluginInfo.init(pluginInitParam) + + setPluginDefinitions({ + pluginInfo, + config: pluginConfig || {} + }) + + } catch (e) { + logger.error(`Plugin '${pluginInfo.path}' not loaded correctly`, e) + } + } + + + // init commands + const pluginDefinitions = getPluginDefinitions() + for (const plugin of pluginDefinitions) { + try { + if (plugin.config.commands) { + + plugin.config.commands({ + commander + }) + } + } catch (e) { + logger.error(`Plugin '${plugin.pluginInfo.path}' commands not loaded correctly`, e) + } + } + +} + diff --git a/src/cli/providers/aws.ts b/src/cli/providers/aws.ts index bb2e39b..0ba84bd 100644 --- a/src/cli/providers/aws.ts +++ b/src/cli/providers/aws.ts @@ -1,7 +1,6 @@ import { getFunctionName } from '../../annotations' -import { bundle } from '../utilities/webpack' import { zip } from '../utilities/compress' -import { uploadZip } from '../utilities/aws/s3Upload' +import { uploadZipStep } from '../utilities/aws/s3Upload' import { createTables } from '../utilities/aws/dynamoDB' import { getLambdaFunction, @@ -12,37 +11,38 @@ import { updateLambdaFunctionConfiguration, updateLambdaFunctionTags } from '../utilities/aws/lambda' +import { ExecuteStep, executor } from '../context' +import { projectConfig } from '../project/config' -export const FUNCTIONAL_ENVIRONMENT = 'aws' +export const aws = { + FUNCTIONAL_ENVIRONMENT: 'aws', + createEnvironment: ExecuteStep.register('CreateEnvironment_aws', async (context) => { + await executor(context, zip) + const fileName = projectConfig.name ? `${projectConfig.name}.zip` : 'project.zip' + await executor(context, uploadZipStep(fileName, context.zipData())) + await executor(context, createTables) -export const createEnvironment = async (context) => { - const date = new Date() - await bundle(context) - await zip(context) - await uploadZip(context, `services-${date.toISOString()}.zip`, context.zipData()) - await createTables(context) - - for (let serviceDefinition of context.publishedFunctions) { - const serviceName = getFunctionName(serviceDefinition.service) - if (serviceName) { - - console.log(`${serviceName} deploying...`) - try { - await getLambdaFunction(serviceDefinition, context) - await updateLambdaFunctionCode(serviceDefinition, context) - await updateLambdaFunctionConfiguration(serviceDefinition, context) - await updateLambdaFunctionTags(serviceDefinition, context) - } catch (e) { - if (e.code === "ResourceNotFoundException") { - await createLambdaFunction(serviceDefinition, context) - } else { - throw e + for (let serviceDefinition of context.publishedFunctions) { + const serviceName = getFunctionName(serviceDefinition.service) + if (serviceName) { + console.log(`${serviceName} deploying...`) + context.serviceDefinition = serviceDefinition + try { + await executor(context, getLambdaFunction) + await executor(context, updateLambdaFunctionCode) + await executor(context, updateLambdaFunctionConfiguration) + await executor(context, updateLambdaFunctionTags) + } catch (e) { + if (e.code === "ResourceNotFoundException") { + await executor(context, createLambdaFunction) + } else { + throw e + } } - } + delete context.serviceDefinition - console.log('completed') + console.log('completed') + } } - } + }) } - - diff --git a/src/cli/providers/azureARM/context/functions.ts b/src/cli/providers/azureARM/context/functions.ts new file mode 100644 index 0000000..9d6e449 --- /dev/null +++ b/src/cli/providers/azureARM/context/functions.ts @@ -0,0 +1,154 @@ +import { basename, join } from 'path' +import { readFileSync } from 'fs' +import { getFunctionName, getMetadata, constants } from '../../../../annotations' +const { CLASS_HTTPTRIGGER, CLASS_ENVIRONMENTKEY, CLASS_AZURENODEKEY } = constants +import { ExecuteStep, executor } from '../../../context' +import { writeFile, copyFile, removePath } from '../../../utilities/local/file' +import { addEnvironmentSetting } from './init' + +export const azureFunctions = ExecuteStep.register('AzureFunctions', async (context) => { + const site = context.ARMTemplate.resources.find(r => r.type === "Microsoft.Web/sites") + if (site) { + const config = site.resources.find(r => r.type === "config") || { properties: {} } + const routePrefix = context.ARMHost.http.routePrefix ? `/${context.ARMHost.http.routePrefix}` : '' + + addEnvironmentSetting('FUNCION_APP_BASEURL', `[concat('https://', toLower(variables('functionAppName')), '.azurewebsites.net${routePrefix}')]`, site) + + for (const serviceDefinition of context.publishedFunctions) { + const funcname = getFunctionName(serviceDefinition.service) + await executor({ + context: { ...context, serviceDefinition, site }, + name: `Azure-ARM-Function-${funcname}`, + method: azureFunction + }) + } + } +}) + +export const azureFunction = async (context) => { + const { serviceDefinition, site } = context + + const environmentVariables = getMetadata(CLASS_ENVIRONMENTKEY, serviceDefinition.service) || {} + for (const key in environmentVariables) { + addEnvironmentSetting(key, environmentVariables[key], site) + } + + const nodeVersion = getMetadata(CLASS_AZURENODEKEY, serviceDefinition.service) + if (nodeVersion) { + addEnvironmentSetting('WEBSITE_NODE_DEFAULT_VERSION', nodeVersion, site) + } + + const httpMetadata = getMetadata(CLASS_HTTPTRIGGER, serviceDefinition.service) || [] + for (let metadata of httpMetadata) { + await executor({ + context: { ...context, metadata }, + name: `Azure-ARM-Function-Endpoint-${metadata.method}-${metadata.path}`, + method: azureFunctionEndpoint + }) + } +} +export const azureFunctionEndpoint = async (context) => { + const { serviceDefinition, site, metadata } = context + + const funcname = getFunctionName(serviceDefinition.service) + + + const resourceDefinition = { + "apiVersion": "2015-08-01", + "name": funcname, + "type": "functions", + "dependsOn": [ + "[resourceId('Microsoft.Web/Sites', variables('functionAppName'))]" + ], + "properties": { + "config": { + "bindings": [] + }, + "files": { + } + } + } + + await executor({ + context: { ...context, serviceDefinition, resourceDefinition }, + name: `Azure-ARM-Function-HttpBindings-${metadata.method}-${metadata.path}-${funcname}`, + method: functionBindings + }) + + await executor({ + context: { ...context, serviceDefinition, resourceDefinition }, + name: `Azure-ARM-Function-HttpHandler-${metadata.method}-${metadata.path}-${funcname}`, + method: functionFiles + }) + + site.resources.push(resourceDefinition) +} + +export const functionBindings = async (context) => { + const { serviceDefinition, metadata, resourceDefinition } = context + + const route = metadata.route.split('/').filter(p => p).join('/') + + resourceDefinition.properties.config.bindings = [ + ...resourceDefinition.properties.config.bindings, + { + "authLevel": metadata.authLevel, + "name": "req", + "type": "httpTrigger", + "direction": "in", + "methods": metadata.methods, + "route": route + }, + { + "name": "res", + "type": "http", + "direction": "out" + } + ] +} + +export const functionFiles = async (context) => { + const { serviceDefinition, resourceDefinition } = context + + resourceDefinition.properties.files = { + ...resourceDefinition.properties.files, + "index.js": `module.exports = require('../${serviceDefinition.fileName}')['${serviceDefinition.exportName}']` + } +} + +export const persistAzureGithubRepo = ExecuteStep.register('PersistAzureGithubRepo', async (context) => { + const { deploymentFolder } = context + const site = context.ARMTemplate.resources.find(r => r.type === "Microsoft.Web/sites") + if (site) { + context.projectFolder = `${context.projectName || 'functionly'}-${context.stage}` + removePath(context.projectFolder) + + for (const file of context.files) { + copyFile(file, join(context.projectFolder, basename(file)), deploymentFolder) + } + + const functions = site.resources.filter(f => f.type === 'functions') + for (const functionResource of functions) { + await executor({ + context: { ...context, site, functionResource }, + name: `PersistAzureGithubRepo-${functionResource.name}`, + method: persistFunction + }) + } + } +}) + +export const persistFunction = (context) => { + const { functionResource, site, deploymentFolder } = context + const basePath = join(context.projectFolder, functionResource.name) + + for (const file in functionResource.properties.files) { + writeFile(join(basePath, file), Buffer.from(functionResource.properties.files[file], 'utf8'), deploymentFolder) + } + + const bindings = JSON.stringify(functionResource.properties.config, null, 4) + writeFile(join(basePath, 'function.json'), Buffer.from(bindings, 'utf8'), deploymentFolder) + + const idx = site.resources.indexOf(functionResource) + site.resources.splice(idx, 1) +} \ No newline at end of file diff --git a/src/cli/providers/azureARM/context/init.ts b/src/cli/providers/azureARM/context/init.ts new file mode 100644 index 0000000..853e62f --- /dev/null +++ b/src/cli/providers/azureARM/context/init.ts @@ -0,0 +1,184 @@ +import { resolvePath } from '../../../utilities/cli' +import { projectConfig } from '../../../project/config' +import { ExecuteStep } from '../../../context' + +import { writeFile } from '../../../utilities/local/file' +import { defaultsDeep } from 'lodash' +import { join } from 'path' + +export const ARMInit = ExecuteStep.register('ARMInit', async (context) => { + context.ARMConfig = { + ...projectConfig.ARM + } + + context.ARMTemplate = { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "functionAppName": { + "type": "string", + "defaultValue": `${context.projectName || 'functionly'}-${context.stage}`, + "metadata": { + "description": "The name of the function app that you wish to create." + } + }, + "storageAccountType": { + "type": "string", + "defaultValue": "Standard_LRS", + "allowedValues": [ + "Standard_LRS", + "Standard_GRS", + "Standard_ZRS", + "Premium_LRS" + ], + "metadata": { + "description": "Storage Account type" + } + } + }, + "variables": { + "functionAppName": "[parameters('functionAppName')]", + "hostingPlanName": "[parameters('functionAppName')]", + "storageAccountName": "[concat(uniquestring(resourceGroup().id), 'azfunctions')]", + "storageAccountid": "[concat(resourceGroup().id,'/providers/','Microsoft.Storage/storageAccounts/', variables('storageAccountName'))]" + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "name": "[variables('storageAccountName')]", + "apiVersion": "2015-06-15", + "location": "[resourceGroup().location]", + "properties": { + "accountType": "[parameters('storageAccountType')]" + } + }, + { + "type": "Microsoft.Web/serverfarms", + "apiVersion": "2015-04-01", + "name": "[variables('hostingPlanName')]", + "location": "[resourceGroup().location]", + "properties": { + "name": "[variables('hostingPlanName')]", + "computeMode": "Dynamic", + "sku": "Dynamic" + } + }, + { + "apiVersion": "2015-08-01", + "type": "Microsoft.Web/sites", + "name": "[variables('functionAppName')]", + "location": "[resourceGroup().location]", + "kind": "functionapp", + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]", + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]" + ], + "properties": { + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]", + "siteConfig": { + "appSettings": [ + { + "name": "AzureWebJobsDashboard", + "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountid'),'2015-05-01-preview').key1)]" + }, + { + "name": "AzureWebJobsStorage", + "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountid'),'2015-05-01-preview').key1)]" + }, + { + "name": "FUNCTIONS_EXTENSION_VERSION", + "value": "~1" + }, + { + "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING", + "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountid'),'2015-05-01-preview').key1)]" + }, + { + "name": "WEBSITE_CONTENTSHARE", + "value": "[toLower(variables('functionAppName'))]" + }, + { + "name": "WEBSITE_NODE_DEFAULT_VERSION", + "value": "6.5.0" + } + ] + } + }, + "resources": [] + } + ] + } + context.ARMStacks = {} + context.deploymentResources = [] + context.ARMHost = { + "http": { + "routePrefix": "" + } + } +}) + +export const ARMMerge = ExecuteStep.register('ARMAfterCreateDefaults', async (context) => { + defaultsDeep(context.ARMTemplate, projectConfig.ARMTemplate || {}) + defaultsDeep(context.ARMStacks, projectConfig.ARMStacksTemplate || {}) + defaultsDeep(context.ARMHost, projectConfig.ARMHost || {}) +}) + +export const initGitTemplate = (context) => { + const { site } = context + + if (context.ARMConfig.deploymentOptions === 'git') { + const gitUrl = context.ARMTemplate.parameters.gitUrl || {} + context.ARMTemplate.parameters.gitUrl = { + ...gitUrl, + "type": "string", + // "defaultValue": "https://github.com/borzav/azure-function-test", + "metadata": { + "description": "Git repository URL" + } + } + + const branch = context.ARMTemplate.parameters.branch || {} + context.ARMTemplate.parameters.branch = { + ...branch, + "type": "string", + // "defaultValue": "master", + "metadata": { + "description": "Git repository branch" + } + } + + context.ARMTemplate.variables.gitRepoUrl = "[parameters('gitUrl')]" + context.ARMTemplate.variables.gitBranch = "[parameters('branch')]" + + const site = context.ARMTemplate.resources.find(r => r.type === 'Microsoft.Web/sites') + if (site) { + site.resources.push({ + "apiVersion": "2015-08-01", + "name": "web", + "type": "sourcecontrols", + "dependsOn": [ + "[resourceId('Microsoft.Web/Sites', parameters('functionAppName'))]" + ], + "properties": { + "RepoUrl": "[variables('gitRepoUrl')]", + "branch": "[variables('gitBranch')]", + "IsManualIntegration": true + } + }) + } + } +} + +export const addEnvironmentSetting = (name, value, site) => { + const exists = site.properties.siteConfig.appSettings.find(s => s.name === name && s.value === value) + if (!exists) { + site.properties.siteConfig.appSettings.push({ name, value }) + } +} + + +export const persistHostJson = async (context) => { + const { deploymentFolder } = context + const host = JSON.stringify(context.ARMHost, null, 4) + writeFile(join(`${context.projectName || 'functionly'}-${context.stage}`, 'host.json'), Buffer.from(host, 'utf8'), deploymentFolder) +} \ No newline at end of file diff --git a/src/cli/providers/azureARM/index.ts b/src/cli/providers/azureARM/index.ts new file mode 100644 index 0000000..0d719fb --- /dev/null +++ b/src/cli/providers/azureARM/index.ts @@ -0,0 +1,60 @@ +import { getFunctionName } from '../../../annotations' +import { logger } from '../../utilities/logger' +import { zip } from '../../utilities/compress' +import { writeFile } from '../../utilities/local/file' +import { projectConfig } from '../../project/config' +import { executor } from '../../context' + +import { ARMInit, ARMMerge, initGitTemplate, persistHostJson } from './context/init' +import { azureFunctions, persistAzureGithubRepo } from './context/functions' + + +export const azure = { + FUNCTIONAL_ENVIRONMENT: 'azure', + createEnvironment: async (context) => { + logger.info(`Functionly: Packaging...`) + await executor(context, zip) + + await executor(context, ARMInit) + + + await executor(context, ARMMerge) + await executor(context, azureFunctions) + await executor(context, initGitTemplate) + + + logger.info(`Functionly: Create project...`) + await executor({ ...context, deploymentFolder: projectConfig.ARM.deploymentFolder }, persistAzureGithubRepo) + await executor({ ...context, deploymentFolder: projectConfig.ARM.deploymentFolder }, persistHostJson) + logger.info(`Functionly: Save template...`) + await executor(context, persistFile) + + logger.info(`Functionly: Complete`) + }, + package: async (context) => { + logger.info(`Functionly: Packaging...`) + await executor(context, zip) + + await executor(context, ARMInit) + + + await executor(context, ARMMerge) + await executor(context, azureFunctions) + await executor(context, initGitTemplate) + + + logger.info(`Functionly: Create project...`) + await executor(context, persistAzureGithubRepo) + await executor(context, persistHostJson) + logger.info(`Functionly: Save template...`) + await executor(context, persistFile) + + logger.info(`Functionly: Complete`) + } +} + +export const persistFile = async (context) => { + const fileName = projectConfig.name ? `${projectConfig.name}.template.json` : 'azurearm.template.json' + const templateData = JSON.stringify(context.ARMTemplate, null, 2); + writeFile(fileName, Buffer.from(templateData, 'binary')) +} \ No newline at end of file diff --git a/src/cli/providers/cloudFormation/context/apiGateway.ts b/src/cli/providers/cloudFormation/context/apiGateway.ts new file mode 100644 index 0000000..13d0b4e --- /dev/null +++ b/src/cli/providers/cloudFormation/context/apiGateway.ts @@ -0,0 +1,394 @@ +import { getMetadata, constants } from '../../../../annotations' +const { CLASS_APIGATEWAYKEY } = constants +import { ExecuteStep, executor } from '../../../context' +import { setResource } from '../utils' +import { setStackParameter, getStackName, createStack } from './stack' + +import { projectConfig } from '../../../project/config' + +export const API_GATEWAY_REST_API = 'ApiGatewayRestApi' + +export const apiGateway = ExecuteStep.register('ApiGateway', async (context) => { + await executor(context, gatewayResources) +}) + +export const gatewayResources = ExecuteStep.register('ApiGateway-Resources', async (context) => { + const endpointsCors = new Map() + const endpoints = new Map() + for (const serviceDefinition of context.publishedFunctions) { + await executor({ + context: { ...context, serviceDefinition, endpointsCors, endpoints }, + name: `ApiGateway-Methods-${serviceDefinition.service.name}`, + method: apiGatewayMethods + }) + } + + for (const [endpointResourceName, { serviceDefinition, methods, headers, credentials, origin }] of endpointsCors) { + await executor({ + context: { ...context, endpointResourceName, serviceDefinition, methods, headers, credentials, origin }, + name: `ApiGateway-Method-Options-${endpointResourceName}`, + method: setOptionsMethodResource + }) + } + + if (endpoints.size) { + await executor(context, gatewayRestApi) + await executor(context, gatewayDeployment) + } +}) + +export const gatewayRestApi = ExecuteStep.register('ApiGateway-RestApi', async (context) => { + const RestApi = { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Name": `${context.CloudFormationConfig.StackName}-${context.stage}` + } + } + + context.ApiGatewayStackName = projectConfig.ApiGatewayStackName || null + if (context.ApiGatewayStackName) { + await executor({ + context: { ...context, stackName: context.ApiGatewayStackName }, + name: `CloudFormation-Stack-init-${context.ApiGatewayStackName}`, + method: createStack + }) + } + + const resourceName = setResource(context, API_GATEWAY_REST_API, RestApi, context.ApiGatewayStackName, true) + + await setStackParameter({ + ...context, + resourceName, + sourceStackName: context.ApiGatewayStackName + }) + + await setStackParameter({ + ...context, + resourceName, + attr: 'RootResourceId', + sourceStackName: context.ApiGatewayStackName + }) + + context.CloudFormationTemplate.Outputs[`ServiceEndpoint`] = { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + context.ApiGatewayStackName ? { + "Fn::GetAtt": [ + context.ApiGatewayStackName, + "Outputs." + resourceName + ] + } : { + "Ref": resourceName + }, + ".execute-api.", + context.awsRegion, + ".amazonaws.com/", + context.stage + ] + ] + } + } + +}) + +export const apiGatewayMethods = async (context) => { + const { serviceDefinition } = context + + let httpMetadata = getMetadata(CLASS_APIGATEWAYKEY, serviceDefinition.service) || [] + for (let metadata of httpMetadata) { + await executor({ + context: { ...context, ...metadata }, + name: `ApiGateway-Method-${metadata.method}-${metadata.path}`, + method: apiGatewayMethod + }) + } +} + +export const apiGatewayMethod = async (context) => { + const { path, cors, corsConfig, authorization, endpoints, endpointsCors, serviceDefinition } = context + const method = context.method.toUpperCase() + + const pathParts = path.split('/') + let pathFragment = '' + let endpoint; + for (const pathPart of pathParts) { + if (!pathPart) continue + + const rootPathFragment = pathFragment + pathFragment += `/${pathPart}` + + if (endpoints.has(pathFragment)) { + endpoint = endpoints.get(pathFragment) + } else { + endpoint = await executor({ + context: { ...context, pathFragment, rootPathFragment, pathPart }, + name: `ApiGateway-ResourcePath-${pathFragment}`, + method: apiGatewayPathPart + }) + } + } + + if (!endpoint) { + // handle / (root path) + throw new Error('TODO missing endpoint') + } + + if (endpoint.serviceDefinition !== serviceDefinition) { + await setStackParameter({ + ...context, + resourceName: endpoint.endpointResourceName, + sourceStackName: getStackName(endpoint.serviceDefinition), + targetStackName: getStackName(serviceDefinition) + }) + } + + if (cors) { + let value = { + serviceDefinition, + methods: new Set(['OPTIONS']), + headers: ['Content-Type', 'Authorization', 'X-Requested-With', ...((corsConfig || {}).headers || [])], + origin: (corsConfig || {}).origin || '*', + credentials: (corsConfig || {}).credentials + } + + if (typeof value.credentials === 'undefined') value.credentials = true + + if (endpointsCors.has(endpoint.endpointResourceName)) { + value = endpointsCors.get(endpoint.endpointResourceName) + } + + const corsMethods = ((corsConfig || {}).methods || []) + if (corsMethods.length) { + value.methods = new Set([...value.methods, ...corsMethods]) + } else { + if (method.toLowerCase() === 'any') { + value.methods = new Set([...value.methods, 'DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT']) + } else { + value.methods.add(method) + } + } + endpointsCors.set(endpoint.endpointResourceName, value) + } + + const properties = { + "HttpMethod": method, + "RequestParameters": {}, + "ResourceId": { + "Ref": endpoint.endpointResourceName + }, + "RestApiId": { + "Ref": API_GATEWAY_REST_API + }, + "AuthorizationType": authorization, + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:aws:apigateway:", + { + "Ref": "AWS::Region" + }, + ":lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + serviceDefinition.resourceName, + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "MethodResponses": [] + } + + const methodConfig = { + "Type": "AWS::ApiGateway::Method", + "Properties": properties + } + const resourceName = `ApiGateway${pathFragment}${method}` + const name = setResource(context, resourceName, methodConfig, getStackName(serviceDefinition)) + + await executor({ + context, + name: `ApiGateway-Method-Permission-${pathFragment}`, + method: setGatewayPermissions + }) +} + +export const apiGatewayPathPart = async (context) => { + const { pathFragment, rootPathFragment, endpoints, pathPart, serviceDefinition } = context + + let parentId + if (rootPathFragment && endpoints.has(rootPathFragment)) { + const endpoint = endpoints.get(rootPathFragment) + if (endpoint.serviceDefinition !== serviceDefinition) { + await setStackParameter({ + ...context, + resourceName: endpoint.endpointResourceName, + sourceStackName: getStackName(endpoint.serviceDefinition), + targetStackName: getStackName(serviceDefinition) + }) + } + + parentId = { + "Ref": endpoint.endpointResourceName + } + } else { + parentId = { + "Ref": `${API_GATEWAY_REST_API}RootResourceId` + } + } + + const properties = { + "ParentId": parentId, + "PathPart": pathPart, + "RestApiId": { + "Ref": API_GATEWAY_REST_API + } + } + + + const resourceConfig = { + "Type": "AWS::ApiGateway::Resource", + "Properties": properties + } + const resourceName = `ApiGateway${pathFragment}` + const endpointResourceName = setResource(context, resourceName, resourceConfig, getStackName(serviceDefinition)) + endpoints.set(pathFragment, { endpointResourceName, serviceDefinition }) + return { endpointResourceName, serviceDefinition } +} + +export const gatewayDeployment = ExecuteStep.register('ApiGateway-Deployment', async (context) => { + + const lambdaDependsOn = context.deploymentResources + .filter(r => !r.stackName && r.type === 'AWS::ApiGateway::Method') + .map(r => r.resourceName) + + const stackDependsOn = context.deploymentResources + .filter(r => r.stackName && r.type === 'AWS::ApiGateway::Method') + .map(r => r.stackName) + + const ApiGatewayDeployment = { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": context.ApiGatewayStackName ? { + "Fn::GetAtt": [ + context.ApiGatewayStackName, + "Outputs." + API_GATEWAY_REST_API + ] + } : { + "Ref": API_GATEWAY_REST_API + }, + "StageName": context.stage + }, + "DependsOn": [ + ...[...lambdaDependsOn, ...stackDependsOn].filter((v, i, self) => self.indexOf(v) === i) + ] + } + + const deploymentResourceName = `ApiGateway${context.date.valueOf()}` + const resourceName = setResource(context, deploymentResourceName, ApiGatewayDeployment) +}) + +export const setOptionsMethodResource = async (context) => { + const { endpointResourceName, serviceDefinition, methods, headers, origin, credentials } = context + const properties = { + "AuthorizationType": "NONE", + "HttpMethod": "OPTIONS", + "MethodResponses": [ + { + "StatusCode": "200", + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Methods": true, + "method.response.header.Access-Control-Allow-Credentials": true + }, + "ResponseModels": {} + } + ], + "RequestParameters": {}, + "Integration": { + "Type": "MOCK", + "RequestTemplates": { + "application/json": "{statusCode:200}" + }, + "IntegrationResponses": [ + { + "StatusCode": "200", + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Origin": `'${origin}'`, + "method.response.header.Access-Control-Allow-Headers": `'${headers.join(',')}'`, + "method.response.header.Access-Control-Allow-Methods": `'${[...methods].join(',')}'`, + "method.response.header.Access-Control-Allow-Credentials": `'${credentials}'` + }, + "ResponseTemplates": { + "application/json": "" + } + } + ] + }, + "ResourceId": { + "Ref": endpointResourceName + }, + "RestApiId": { + "Ref": API_GATEWAY_REST_API + } + } + + const methodConfig = { + "Type": "AWS::ApiGateway::Method", + "Properties": properties + } + const resourceName = `${endpointResourceName}Options` + setResource(context, resourceName, methodConfig, getStackName(serviceDefinition)) +} + +export const setGatewayPermissions = (context) => { + const { serviceDefinition } = context + const properties = { + "FunctionName": { + "Fn::GetAtt": [ + serviceDefinition.resourceName, + "Arn" + ] + }, + "Action": "lambda:InvokeFunction", + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:aws:execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": API_GATEWAY_REST_API + }, + "/*/*" + ] + ] + } + } + + const methodConfig = { + "Type": "AWS::Lambda::Permission", + "Properties": properties + } + const resourceName = `ApiGateway${serviceDefinition.resourceName}Permission` + setResource(context, resourceName, methodConfig, getStackName(serviceDefinition), false, true) +} diff --git a/src/cli/providers/cloudFormation/context/cloudFormationInit.ts b/src/cli/providers/cloudFormation/context/cloudFormationInit.ts index 6cc31d7..1fddb53 100644 --- a/src/cli/providers/cloudFormation/context/cloudFormationInit.ts +++ b/src/cli/providers/cloudFormation/context/cloudFormationInit.ts @@ -1,16 +1,27 @@ import { resolvePath } from '../../../utilities/cli' +import { projectConfig } from '../../../project/config' +import { ExecuteStep } from '../../../context' -export const cloudFormationInit = async (context) => { - const path = resolvePath('./package.json') - try { - const packageJson = require(path) - context.CloudFormationConfig = packageJson.CloudFormation || {} - } catch (e) { - context.CloudFormationConfig = {} +import { defaultsDeep } from 'lodash' + +export const cloudFormationInit = ExecuteStep.register('CloudFormationInit', async (context) => { + context.CloudFormationConfig = { + StackName: projectConfig.name, + OnFailure: "ROLLBACK", + TimeoutInMinutes: 10, + ...projectConfig.cloudFormation } context.CloudFormationTemplate = { "AWSTemplateFormatVersion": "2010-09-09", - "Resources": {} + "Resources": {}, + "Outputs": {} } -} \ No newline at end of file + context.CloudFormationStacks = {} + context.deploymentResources = [] +}) + +export const cloudFormationMerge = ExecuteStep.register('CloudFormationAfterCreateDefaults', async (context) => { + defaultsDeep(context.CloudFormationTemplate, projectConfig.cloudFormationTemplate || {}) + defaultsDeep(context.CloudFormationStacks, projectConfig.cloudFormationStacksTemplate || {}) +}) \ No newline at end of file diff --git a/src/cli/providers/cloudFormation/context/cloudWatchEvent.ts b/src/cli/providers/cloudFormation/context/cloudWatchEvent.ts new file mode 100644 index 0000000..c26e032 --- /dev/null +++ b/src/cli/providers/cloudFormation/context/cloudWatchEvent.ts @@ -0,0 +1,95 @@ +import { getMetadata, constants, getFunctionName } from '../../../../annotations' +const { CLASS_CLOUDWATCHEVENT } = constants +import { ExecuteStep, executor } from '../../../context' +import { setResource } from '../utils' +import { setStackParameter, getStackName } from './stack' + +export const cloudWatchEvent = ExecuteStep.register('CloudWatchEvent', async (context) => { + await executor(context, cloudWatchResources) +}) + +export const cloudWatchResources = ExecuteStep.register('CloudWatchEvent-Resources', async (context) => { + for (const serviceDefinition of context.publishedFunctions) { + await executor({ + context: { ...context, serviceDefinition }, + name: `CloudWatchEvent-${serviceDefinition.service.name}`, + method: cloudWatchEventItems + }) + } +}) + +export const cloudWatchEventItems = async (context) => { + const { serviceDefinition } = context + + let cloudWatches = getMetadata(CLASS_CLOUDWATCHEVENT, serviceDefinition.service) || [] + for (let [ruleIndex, rule] of cloudWatches.entries()) { + await executor({ + context: { ...context, rule, ruleIndex }, + name: `CloudWatchEvent-Rule-${serviceDefinition.service.name}`, + method: cloudWatchEventItem + }) + } +} + +export const cloudWatchEventItem = async (context) => { + const { serviceDefinition, rule, ruleIndex } = context + + const properties: any = { + "State": "ENABLED", + "Targets": [{ + "Arn": { "Fn::GetAtt": [serviceDefinition.resourceName, "Arn"] }, + "Id": `${getFunctionName(serviceDefinition.service)}Schedule${ruleIndex}` + }] + } + + if (rule.scheduleExpression) { + properties.ScheduleExpression = rule.scheduleExpression + } else if (rule.eventPattern) { + properties.EventPattern = rule.eventPattern + } else { + // invalid event configuration + return + } + + const methodConfig = { + "Type": "AWS::Events::Rule", + "Properties": properties, + "DependsOn": [ + serviceDefinition.resourceName + ] + } + const resourceName = `CloudWatchEvent${getFunctionName(serviceDefinition.service)}` + const name = setResource(context, resourceName, methodConfig, getStackName(serviceDefinition)) + + await executor({ + context: { ...context, cloudWatchResourceName: resourceName }, + name: `CloudWatchEvent-Rule-Permission-${serviceDefinition.service.name}`, + method: cloudWatchEvenetPermission + }) +} + +export const cloudWatchEvenetPermission = (context) => { + const { serviceDefinition, cloudWatchResourceName } = context + const properties = { + "FunctionName": { + "Fn::GetAtt": [ + serviceDefinition.resourceName, + "Arn" + ] + }, + "Action": "lambda:InvokeFunction", + "Principal": "events.amazonaws.com", + "SourceArn": { "Fn::GetAtt": [cloudWatchResourceName, "Arn"] } + } + + const methodConfig = { + "Type": "AWS::Lambda::Permission", + "Properties": properties, + "DependsOn": [ + serviceDefinition.resourceName, + cloudWatchResourceName + ] + } + const resourceName = `CloudWatchEvent${serviceDefinition.resourceName}Permission` + setResource(context, resourceName, methodConfig, getStackName(serviceDefinition), false, true) +} diff --git a/src/cli/providers/cloudFormation/context/dynamoTable.ts b/src/cli/providers/cloudFormation/context/dynamoTable.ts new file mode 100644 index 0000000..351597c --- /dev/null +++ b/src/cli/providers/cloudFormation/context/dynamoTable.ts @@ -0,0 +1,182 @@ +import { defaultsDeep } from 'lodash' +import { __dynamoDBDefaults, getMetadata, constants } from '../../../../annotations' +import { ExecuteStep, executor } from '../../../context' +import { setResource } from '../utils' +import { createStack, setStackParameter, getStackName } from './stack' +import { cloudFormation } from '../../../../annotations/classes/aws/cloudFormation'; + +export const DYNAMODB_TABLE_STACK = 'DynamoDBTableStack' + +const { CLASS_CLOUDFORMATION } = constants + +export const tableResources = ExecuteStep.register('DynamoDB-Tables', async (context) => { + await executor({ + context: { ...context, stackName: DYNAMODB_TABLE_STACK }, + name: `CloudFormation-Stack-init-${DYNAMODB_TABLE_STACK}`, + method: createStack + }) + + for (const tableConfig of context.tableConfigs) { + await executor({ + context: { ...context, tableConfig }, + name: `DynamoDB-Table-${tableConfig.tableName}`, + method: tableResource + }) + } +}) + +export const tableResource = async (context) => { + const { tableConfig } = context + + tableConfig.AWSTableName = tableConfig.tableName + + if (tableConfig.exists) return + + tableConfig.AWSTableName = `${tableConfig.tableName}-${context.stage}` + + const properties = { + ...__dynamoDBDefaults, + TableName: tableConfig.AWSTableName, + ...tableConfig.nativeConfig + }; + + + const hasSubscribers = tableConfig.services.some(s => s.serviceConfig.eventSource) + if (hasSubscribers) { + defaultsDeep(properties, { + StreamSpecification: { + "StreamViewType": "NEW_AND_OLD_IMAGES" + } + }) + } + + const dynamoDb = { + "Type": "AWS::DynamoDB::Table", + "Properties": properties + } + + tableConfig.tableStackName = DYNAMODB_TABLE_STACK + let tableResourceName = `Dynamo${tableConfig.tableName}` + + const cloudFormationConfig = getMetadata(CLASS_CLOUDFORMATION, tableConfig.definedBy) + if (cloudFormationConfig) { + tableConfig.tableStackName = cloudFormationConfig.stack + tableResourceName = cloudFormationConfig.resourceName || tableResourceName + + if (tableConfig.tableStackName) { + await executor({ + context: { ...context, stackName: tableConfig.tableStackName }, + name: `CloudFormation-Stack-init-${tableConfig.tableStackName}`, + method: createStack + }) + } + } + + const resourceName = setResource(context, tableResourceName, dynamoDb, tableConfig.tableStackName, true) + + await setStackParameter({ + ...context, + resourceName, + sourceStackName: tableConfig.tableStackName + }) + + tableConfig.resourceName = resourceName + +} + +export const tableSubscribers = ExecuteStep.register('DynamoDB-Table-Subscriptions', async (context) => { + for (const tableConfig of context.tableConfigs) { + const subscribers = tableConfig.services.filter(s => s.serviceConfig.eventSource) + for (const subscriber of subscribers) { + await executor({ + context: { ...context, tableConfig, subscriber }, + name: `DynamoDB-Table-Subscriptions-${tableConfig.tableName}-${subscriber.serviceDefinition.service.name}`, + method: tableSubscriber + }) + } + } +}) + +export const tableSubscriber = async (context) => { + const { tableConfig, subscriber } = context + + if (tableConfig.exists) return + + const properties = { + "BatchSize": 1, + "EventSourceArn": { + "Ref": `${tableConfig.resourceName}StreamArn` + }, + "FunctionName": { + "Fn::GetAtt": [ + subscriber.serviceDefinition.resourceName, + "Arn" + ] + }, + "StartingPosition": "TRIM_HORIZON" + }; + + const dynamoDb = { + "Type": "AWS::Lambda::EventSourceMapping", + "Properties": properties + } + + await setStackParameter({ + ...context, + resourceName: tableConfig.resourceName, + sourceStackName: tableConfig.tableStackName, + targetStackName: getStackName(subscriber.serviceDefinition), + attr: 'StreamArn' + }) + + const eventSourceResourceName = `EventSource${tableConfig.tableName}${subscriber.serviceDefinition.resourceName}Subscription` + const resourceName = setResource(context, eventSourceResourceName, dynamoDb, getStackName(subscriber.serviceDefinition)) + + await executor({ + context: { ...context, tableConfig, serviceDefinition: subscriber.serviceDefinition }, + name: `DynamoDB-Table-Streaming-Policy-${tableConfig.tableName}-${subscriber.serviceDefinition.service.name}`, + method: dynamoStreamingPolicy + }) +} + +export const dynamoStreamingPolicy = async (context) => { + const { tableConfig, serviceDefinition } = context + + if (tableConfig.exists) return + + let policy = serviceDefinition.roleResource.Properties.Policies.find(p => p.PolicyDocument.Statement[0].Action.includes('dynamodb:GetRecords')) + if (!policy) { + policy = { + "PolicyName": { + "Fn::Join": [ + "-", + [ + "functionly", + serviceDefinition.roleName, + "dynamo", + "streams" + ] + ] + }, + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [{ + "Effect": "Allow", + "Action": [ + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:DescribeStream", + "dynamodb:ListStreams" + ], + "Resource": [] + }] + } + } + serviceDefinition.roleResource.Properties.Policies = serviceDefinition.roleResource.Properties.Policies || [] + serviceDefinition.roleResource.Properties.Policies.push(policy) + } + + policy.PolicyDocument.Statement[0].Resource.push({ + "Fn::Sub": "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/" + tableConfig.AWSTableName + "/stream/*" + }) +} diff --git a/src/cli/providers/cloudFormation/context/resources.ts b/src/cli/providers/cloudFormation/context/resources.ts index 1123137..1937662 100644 --- a/src/cli/providers/cloudFormation/context/resources.ts +++ b/src/cli/providers/cloudFormation/context/resources.ts @@ -1,186 +1,364 @@ -import { merge } from 'lodash' +import { intersection } from 'lodash' + import { getMetadata, constants, getFunctionName, __dynamoDBDefaults } from '../../../../annotations' -const { CLASS_DESCRIPTIONKEY, CLASS_ROLEKEY, CLASS_MEMORYSIZEKEY, CLASS_RUNTIMEKEY, CLASS_TIMEOUTKEY, - CLASS_ENVIRONMENTKEY, CLASS_TAGKEY } = constants - -export const nameReplaceRegexp = /[^a-zA-Z0-9]/g -export const normalizeName = (name: string) => { - const result = name.replace(nameReplaceRegexp, '') - if (!result) { - throw new Error(`'invalid name '${name}'`) - } - return result -} +const { CLASS_DESCRIPTIONKEY, CLASS_ROLEKEY, CLASS_AWSMEMORYSIZEKEY, CLASS_AWSRUNTIMEKEY, CLASS_AWSTIMEOUTKEY, + CLASS_ENVIRONMENTKEY, CLASS_TAGKEY, CLASS_APIGATEWAYKEY } = constants +import { ExecuteStep, executor } from '../../../context' +import { setResource } from '../utils' +import { createStack, setStackParameter, getStackName } from './stack' +import { getBucketReference } from './s3StorageDeployment' + +export { s3DeploymentBucket, s3DeploymentBucketParameter, s3 } from './s3Storage' +export { getDeploymentBucketResourceName } from './s3StorageDeployment' +export { apiGateway } from './apiGateway' +export { sns } from './sns' +export { cloudWatchEvent } from './cloudWatchEvent' +export { tableResources, tableSubscribers } from './dynamoTable' -export const setResource = (context, name, resource) => { - if (!name) { - throw new Error(`invalid resource name '${name}'`) + +export const initStacks = ExecuteStep.register('CloudFormation-Stack-init', async (context) => { + for (const serviceDefinition of context.publishedFunctions) { + const stackName = getStackName(serviceDefinition) + await executor({ + context: { ...context, stackName }, + name: `CloudFormation-Stack-init-${stackName}`, + method: createStack + }) } - name = normalizeName(name) - if (context.CloudFormationTemplate.Resources[name]) { - throw new Error(`resource name '${name}' already exists`) +}) + +export const roleResources = ExecuteStep.register('IAM-Role', async (context) => { + const roleMap = new Map() + for (const serviceDefinition of context.publishedFunctions) { + let role = getMetadata(CLASS_ROLEKEY, serviceDefinition.service) + if (typeof role === 'string' && /^arn:/.test(role)) { + continue + } + if (!role) { + role = context.CloudFormationConfig.StackName + } + if (!roleMap.has(role)) { + roleMap.set(role, [serviceDefinition]) + } + + const roleDef = roleMap.get(role) + roleDef.push(serviceDefinition) } - context.usedAwsResources = context.usedAwsResources || []; - if (context.usedAwsResources.indexOf(resource.Type) < 0) { - context.usedAwsResources.push(resource.type) + for (const [roleName, serviceDefinitions] of roleMap) { + await executor({ + context: { ...context, roleName, serviceDefinitions }, + name: `IAM-Role-${roleName}`, + method: roleResource + }) } +}) - context.CloudFormationTemplate.Resources[name] = resource - return name; -} +export const roleResource = async (context) => { -export const roleResources = async (context) => { - const roleMap = new Map() + const { roleName, serviceDefinitions } = context + const roleProperties = { + "RoleName": `${roleName}-${context.stage}`, + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [{ + "Effect": "Allow", + "Principal": { + "Service": ["lambda.amazonaws.com"] + }, + "Action": ["sts:AssumeRole"] + }] + }, + "Path": "/", + "Policies": [] + } - for (const serviceDefinition of context.publishedFunctions) { + await executor({ + context: { ...context, roleName, serviceDefinitions, roleProperties }, + name: `IAM-Lambda-Policy-${roleName}`, + method: lambdaPolicy + }) + await executor({ + context: { ...context, roleName, serviceDefinitions, roleProperties }, + name: `IAM-Log-Policy-${roleName}`, + method: logPolicy + }) - const role = getMetadata(CLASS_ROLEKEY, serviceDefinition.service) - if (typeof role === 'string' && /^arn:/.test(role)) { - continue + await executor({ + context: { ...context, roleName, serviceDefinitions, roleProperties }, + name: `IAM-Dynamo-Policy-${roleName}`, + method: dynamoPolicy + }) + + if (roleProperties.Policies.length) { + const iamRole = { + "Type": "AWS::IAM::Role", + "Properties": roleProperties } - if (!roleMap.has(role)) { - const properties = { - "RoleName": role, - "AssumeRolePolicyDocument": { - "Version": "2012-10-17", - "Statement": [{ - "Effect": "Allow", - "Principal": { - "Service": ["lambda.amazonaws.com"] - }, - "Action": ["sts:AssumeRole"] - }] - }, - "Path": "/", - "Policies": [{ - "PolicyName": { - "Fn::Join": [ - "-", - [ - "functionly", - role, - "lambda" - ] - ] - }, - "PolicyDocument": { - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "logs:CreateLogGroup", - "logs:CreateLogStream", - "logs:PutLogEvents" - ], - "Resource": ["*"] - }, { - "Effect": "Allow", - "Action": [ - "lambda:InvokeAsync", - "lambda:InvokeFunction" - ], - "Resource": ["*"] - }, { - "Effect": "Allow", - "Action": [ - "dynamodb:Query", - "dynamodb:Scan", - "dynamodb:GetItem", - "dynamodb:PutItem", - "dynamodb:UpdateItem" - ], - "Resource": [ - { - "Fn::Sub": "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/*" - } - ] - }] - } - }] - } + const roleResourceName = `IAM${roleName}` + const resourceName = setResource(context, roleResourceName, iamRole) + + await setStackParameter({ + ...context, + resourceName, + attr: 'Arn' + }) + + context.CloudFormationConfig.Capabilities = context.CloudFormationConfig.Capabilities || [ + "CAPABILITY_NAMED_IAM" + ] - const tableResource = { - "Type": "AWS::IAM::Role", - "Properties": properties + for (const serviceDefinition of serviceDefinitions) { + serviceDefinition[CLASS_ROLEKEY] = { + "Ref": `${resourceName}Arn` } + serviceDefinition.roleResource = iamRole + serviceDefinition.roleName = roleName + } + } +} +export const lambdaPolicy = async (context) => { + const { roleName, roleProperties } = context + const policy = { + "PolicyName": { + "Fn::Join": [ + "-", + [ + "functionly", + roleName, + "lambda" + ] + ] + }, + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [{ + "Effect": "Allow", + "Action": [ + "lambda:InvokeAsync", + "lambda:InvokeFunction", + "sns:Publish" + ], + "Resource": ["*"] + }] + } + } + roleProperties.Policies.push(policy) +} - const roleResourceName = `iam_${properties.RoleName}` - const resourceName = setResource(context, roleResourceName, tableResource) - roleMap.set(role, { - ref: { - "Fn::GetAtt": [ - resourceName, - "Arn" - ] - }, - resourceName - }) +export const logPolicy = async (context) => { + const { roleName, roleProperties, serviceDefinitions } = context + + const logGroupResourceNames = serviceDefinitions.map(s => s.logGroupResourceName) + const logGroupNames = context.deploymentResources + .filter(r => r.type === 'AWS::Logs::LogGroup' && logGroupResourceNames.indexOf(r.resourceName) >= 0) + .map(r => r.resource.Properties.LogGroupName) + + const policy = { + "PolicyName": { + "Fn::Join": [ + "-", + [ + "functionly", + roleName, + "logs" + ] + ] + }, + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [{ + "Effect": "Allow", + "Action": [ + "logs:CreateLogStream", + ], + "Resource": [ + { + "Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/" + context.projectName + "-*-" + context.stage + ":*" + } + ] + }, { + "Effect": "Allow", + "Action": [ + "logs:PutLogEvents" + ], + "Resource": [ + { + "Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/" + context.projectName + "-*-" + context.stage + ":*:*" + } + ] + }] } + } + roleProperties.Policies.push(policy) +} - const iam_role = roleMap.get(role) - serviceDefinition[CLASS_ROLEKEY] = iam_role.ref +export const dynamoPolicy = async (context) => { + const { roleName, serviceDefinitions, roleProperties } = context + const tables = [] + const services = serviceDefinitions.map(s => s.service) + const usedTableConfigs = context.tableConfigs.filter(t => intersection(t.services.map(s => s.serviceDefinition.service), services).length) + const policy = { + "PolicyName": { + "Fn::Join": [ + "-", + [ + "functionly", + roleName, + "dynamo" + ] + ] + }, + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [{ + "Effect": "Allow", + "Action": [ + "dynamodb:Query", + "dynamodb:Scan", + "dynamodb:GetItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem", + "dynamodb:DescribeStream", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:ListStreams" + ], + "Resource": [ + ...usedTableConfigs.map(t => { + return { + "Fn::Sub": "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/" + t.AWSTableName + } + }), + ...usedTableConfigs + .filter(t => t.nativeConfig && (t.nativeConfig.GlobalSecondaryIndexes || t.nativeConfig.LocalSecondaryIndexes)) + .map(t => { + return { + "Fn::Sub": "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/" + t.AWSTableName + "/*" + } + }) + ] + }] + } } + if (usedTableConfigs.length) { + roleProperties.Policies.push(policy) + } } -export const tableResources = async (context) => { +export const lambdaResources = ExecuteStep.register('Lambda-Functions', async (context) => { + const awsBucket = await getBucketReference(context) - for (const tableConfig of context.tableConfigs) { - - const properties = merge({}, { - TableName: tableConfig.tableName - }, tableConfig.nativeConfig, __dynamoDBDefaults); + for (const serviceDefinition of context.publishedFunctions) { + await executor({ + context: { ...context, serviceDefinition, awsBucket }, + name: `Lambda-Function-${serviceDefinition.service.name}`, + method: lambdaResource + }) + /* await executor({ + context: { ...context, serviceDefinition }, + name: `Lambda-Version-${serviceDefinition.service.name}`, + method: lambdaVersionResource + }) */ + } +}) +export const lambdaResource = async (context) => { + const { serviceDefinition, awsBucket } = context - const tableResource = { - "Type": "AWS::DynamoDB::Table", - "Properties": properties - } + const properties: any = { + Code: { + S3Bucket: awsBucket, + S3Key: context.S3Zip + }, + Description: serviceDefinition[CLASS_DESCRIPTIONKEY] || getMetadata(CLASS_DESCRIPTIONKEY, serviceDefinition.service), + FunctionName: `${context.projectName}-${getFunctionName(serviceDefinition.service)}-${context.stage}`, + Handler: serviceDefinition.handler, + MemorySize: serviceDefinition[CLASS_AWSMEMORYSIZEKEY] || getMetadata(CLASS_AWSMEMORYSIZEKEY, serviceDefinition.service), + Role: serviceDefinition[CLASS_ROLEKEY] || getMetadata(CLASS_ROLEKEY, serviceDefinition.service), + Runtime: serviceDefinition[CLASS_AWSRUNTIMEKEY] || getMetadata(CLASS_AWSRUNTIMEKEY, serviceDefinition.service) || "nodejs20.x", + Timeout: serviceDefinition[CLASS_AWSTIMEOUTKEY] || getMetadata(CLASS_AWSTIMEOUTKEY, serviceDefinition.service), + Environment: { + Variables: serviceDefinition[CLASS_ENVIRONMENTKEY] || getMetadata(CLASS_ENVIRONMENTKEY, serviceDefinition.service) + }, + Tags: serviceDefinition[CLASS_TAGKEY] || getMetadata(CLASS_TAGKEY, serviceDefinition.service) + }; + updateEnvironmentVariable({ ...context, environments: properties.Environment.Variables }) - const resourceName = `dynamo_${properties.TableName}` - setResource(context, resourceName, tableResource) + const lambdaResource = { + "Type": "AWS::Lambda::Function", + "Properties": properties, + "DependsOn": [ + serviceDefinition.logGroupResourceName + ] } + const resourceName = `Lambda${getFunctionName(serviceDefinition.service)}` + const name = setResource(context, resourceName, lambdaResource, getStackName(serviceDefinition)) + serviceDefinition.resourceName = name } -export const lambdaResources = async (context) => { +export const lambdaVersionResource = async (context) => { + const { serviceDefinition } = context + const versionResource = { + "Type": "AWS::Lambda::Version", + "DeletionPolicy": "Retain", + "Properties": { + "FunctionName": { + "Ref": serviceDefinition.resourceName + }, + "CodeSha256": context.zipCodeSha256 + } + } + setResource(context, `${serviceDefinition.resourceName}${context.zipCodeSha256}`, versionResource, getStackName(serviceDefinition)) +} + +export const lambdaLogResources = ExecuteStep.register('Lambda-LogGroups', async (context) => { for (const serviceDefinition of context.publishedFunctions) { + await executor({ + context: { ...context, serviceDefinition }, + name: `Lambda-Function-${serviceDefinition.service.name}`, + method: lambdaLogResource + }) + } +}) - const properties: any = { - Code: { - S3Bucket: context.awsBucket, - S3Key: context.S3Zip - }, - Description: serviceDefinition[CLASS_DESCRIPTIONKEY] || getMetadata(CLASS_DESCRIPTIONKEY, serviceDefinition.service), - FunctionName: getFunctionName(serviceDefinition.service), - Handler: serviceDefinition.handler, - MemorySize: serviceDefinition[CLASS_MEMORYSIZEKEY] || getMetadata(CLASS_MEMORYSIZEKEY, serviceDefinition.service), - Role: serviceDefinition[CLASS_ROLEKEY] || getMetadata(CLASS_ROLEKEY, serviceDefinition.service), - Runtime: serviceDefinition[CLASS_RUNTIMEKEY] || getMetadata(CLASS_RUNTIMEKEY, serviceDefinition.service) || "nodejs6.10", - Timeout: serviceDefinition[CLASS_TIMEOUTKEY] || getMetadata(CLASS_TIMEOUTKEY, serviceDefinition.service), - Environment: { - Variables: serviceDefinition[CLASS_ENVIRONMENTKEY] || getMetadata(CLASS_ENVIRONMENTKEY, serviceDefinition.service) - }, - Tags: serviceDefinition[CLASS_TAGKEY] || getMetadata(CLASS_TAGKEY, serviceDefinition.service) - }; +export const lambdaLogResource = async (context) => { + const { serviceDefinition } = context - const lambdaResource = { - "Type": "AWS::Lambda::Function", - "Properties": properties - } + const functionName = getFunctionName(serviceDefinition.service) + + const properties: any = { + "LogGroupName": `/aws/lambda/${context.projectName}-${functionName}-${context.stage}` + }; - const resourceName = `lambda_${properties.FunctionName}` - setResource(context, resourceName, lambdaResource) + const lambdaResource = { + "Type": "AWS::Logs::LogGroup", + "Properties": properties } + const resourceName = `${functionName}LogGroup` + const name = setResource(context, resourceName, lambdaResource, getStackName(serviceDefinition)) + serviceDefinition.logGroupResourceName = name +} + +const UPDATEABLE_ENVIRONMENT_REGEXP = /^FUNCTIONAL_SERVICE_/ +const updateEnvironmentVariable = async ({ environments, stage }) => { + if (environments) { + for (const key in environments) { + if (UPDATEABLE_ENVIRONMENT_REGEXP.test(key)) { + environments[key] = `${environments[key]}-${stage}` + } + } + } } diff --git a/src/cli/providers/cloudFormation/context/s3Storage.ts b/src/cli/providers/cloudFormation/context/s3Storage.ts new file mode 100644 index 0000000..120ea88 --- /dev/null +++ b/src/cli/providers/cloudFormation/context/s3Storage.ts @@ -0,0 +1,231 @@ +import { getMetadata, constants, defineMetadata, getFunctionName } from '../../../../annotations' +const { CLASS_S3CONFIGURATIONKEY, CLASS_ENVIRONMENTKEY } = constants +import { ExecuteStep, executor } from '../../../context' +import { collectMetadata } from '../../../utilities/collectMetadata' +import { setResource } from '../utils' +import { createStack, setStackParameter, getStackName } from './stack' +import { getDeploymentBucketResourceName } from './s3StorageDeployment' + +export const S3_STORAGE_STACK = 'S3Stack' + +export const s3DeploymentBucket = ExecuteStep.register('S3-Deployment-Bucket', async (context) => { + if (context.awsBucket) { + context.__userAWSBucket = true + } + + const s3BucketResource = { + "Type": "AWS::S3::Bucket" + } + + const bucketResourceName = await getDeploymentBucketResourceName() + const resourceName = setResource(context, bucketResourceName, s3BucketResource) + + context.CloudFormationTemplate.Outputs[`${resourceName}Name`] = { + "Value": { + "Ref": bucketResourceName + } + } + +}) + + +export const s3DeploymentBucketParameter = ExecuteStep.register('S3-Deployment-Bucket-Parameter', async (context) => { + const resourceName = await getDeploymentBucketResourceName() + await setStackParameter({ + ...context, + resourceName + }) +}) + + + +export const s3 = ExecuteStep.register('S3', async (context) => { + await executor({ + context: { ...context, stackName: S3_STORAGE_STACK }, + name: `CloudFormation-Stack-init-${S3_STORAGE_STACK}`, + method: createStack + }) + + await executor(context, s3Storages) +}) + +export const s3Storages = ExecuteStep.register('S3-Storages', async (context) => { + const configs = collectMetadata(context, { + metadataKey: CLASS_S3CONFIGURATIONKEY, + selector: (c) => c.bucketName + }) + + for (const s3Config of configs) { + await executor({ + context: { ...context, s3Config }, + name: `S3-Storage-${s3Config.bucketName}`, + method: s3Storage + }) + } +}) + +export const s3Storage = async (context) => { + const { s3Config } = context + + s3Config.AWSBucketName = s3Config.bucketName + + if (!s3Config.exists) { + s3Config.AWSBucketName = `${s3Config.bucketName}-${context.stage}` + + const s3Properties = { + "BucketName": s3Config.AWSBucketName + } + + const s3Bucket = { + "Type": "AWS::S3::Bucket", + "Properties": s3Properties + } + + const resourceName = `S3${s3Config.bucketName}` + const bucketResourceName = setResource(context, resourceName, s3Bucket, S3_STORAGE_STACK, true) + s3Config.resourceName = bucketResourceName + + await executor({ + context: { ...context, s3BucketDefinition: s3Bucket }, + name: `S3-Storage-Subscription-${s3Config.bucketName}`, + method: s3StorageSubscriptions + }) + } + + for (const { serviceDefinition, serviceConfig } of s3Config.services) { + if (!serviceConfig.injected) continue + + await executor({ + context: { ...context, serviceDefinition, serviceConfig }, + name: `S3-Storage-Policy-${s3Config.bucketName}-${serviceDefinition.service.name}`, + method: s3StoragePolicy + }) + } +} + + +export const s3StoragePolicy = async (context) => { + const { s3Config, serviceDefinition, serviceConfig } = context + + let policy = serviceDefinition.roleResource.Properties.Policies.find(p => p.PolicyDocument.Statement[0].Action.includes('s3:PutObject')) + if (!policy) { + policy = { + "PolicyName": { + "Fn::Join": [ + "-", + [ + "functionly", + serviceDefinition.roleName, + "s3" + ] + ] + }, + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [{ + "Effect": "Allow", + "Action": [ + "s3:GetObject", + "s3:PutObject", + "s3:PutObjectAcl", + "s3:PutObjectTagging", + "s3:PutObjectVersionAcl", + "s3:PutObjectVersionTagging", + "s3:DeleteObject" + ], + "Resource": [] + }, + { + "Effect": "Allow", + "Action": [ + "s3:ListBucket" + ], + "Resource": [] + }] + } + } + serviceDefinition.roleResource.Properties.Policies = serviceDefinition.roleResource.Properties.Policies || [] + serviceDefinition.roleResource.Properties.Policies.push(policy) + } + + const s3Arn = `arn:aws:s3:::${s3Config.AWSBucketName}/*` + if (!policy.PolicyDocument.Statement[0].Resource.includes(s3Arn)) { + policy.PolicyDocument.Statement[0].Resource.push(s3Arn) + policy.PolicyDocument.Statement[1].Resource.push(`arn:aws:s3:::${s3Config.AWSBucketName}`) + } +} + +export const s3StorageSubscriptions = async (context) => { + const { s3Config } = context + + if (s3Config.exists) return + + for (const { serviceDefinition, serviceConfig } of s3Config.services) { + if (!serviceConfig.eventSource) continue + + await executor({ + context: { ...context, serviceDefinition, serviceConfig }, + name: `S3-Storage-Subscription-${s3Config.bucketName}-${serviceDefinition.service.name}`, + method: s3BucketSubscription + }) + + await executor({ + context: { ...context, serviceDefinition, serviceConfig }, + name: `S3-Storage-Permission-${s3Config.bucketName}-${serviceDefinition.service.name}`, + method: s3Permissions + }) + } +} + +export const s3BucketSubscription = async (context) => { + const { serviceDefinition, serviceConfig, s3Config, s3BucketDefinition } = context + + if (s3Config.exists) return + + await setStackParameter({ + ...context, + sourceStackName: getStackName(serviceDefinition), + resourceName: serviceDefinition.resourceName, + targetStackName: S3_STORAGE_STACK, + attr: 'Arn' + }) + + s3BucketDefinition.Properties.NotificationConfiguration = + s3BucketDefinition.Properties.NotificationConfiguration || {} + s3BucketDefinition.Properties.NotificationConfiguration.LambdaConfigurations = + s3BucketDefinition.Properties.NotificationConfiguration.LambdaConfigurations || [] + + s3BucketDefinition.Properties.NotificationConfiguration.LambdaConfigurations.push({ + "Event": "s3:ObjectCreated:*", + ...serviceConfig.eventSourceConfiguration, + "Function": { + // "Fn::Sub": "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:" + serviceDefinition.resourceName + "Ref": `${serviceDefinition.resourceName}Arn` + } + }) +} + +export const s3Permissions = (context) => { + const { serviceDefinition, serviceConfig, s3Config, s3BucketDefinition } = context + + if (s3Config.exists) return + + const properties = { + "FunctionName": { + "Ref": serviceDefinition.resourceName + }, + "Action": "lambda:InvokeFunction", + "Principal": "s3.amazonaws.com", + "SourceArn": { + "Fn::Join": [":", [ + "arn", "aws", "s3", "", "", `${serviceConfig.bucketName}-${context.stage}`]] + } + } + + const s3Permission = { + "Type": "AWS::Lambda::Permission", + "Properties": properties + } + const resourceName = `${s3Config.resourceName}Permission` + const permissionResourceName = setResource(context, resourceName, s3Permission, getStackName(serviceDefinition), false, true) +} diff --git a/src/cli/providers/cloudFormation/context/s3StorageDeployment.ts b/src/cli/providers/cloudFormation/context/s3StorageDeployment.ts new file mode 100644 index 0000000..951ad30 --- /dev/null +++ b/src/cli/providers/cloudFormation/context/s3StorageDeployment.ts @@ -0,0 +1,13 @@ +import { projectConfig } from '../../../project/config' + +const S3_DEPLOYMENT_BUCKET_RESOURCE_NAME = 'FunctionlyDeploymentBucket' + +export const getDeploymentBucketResourceName = async () => { + return projectConfig.awsBucketResourceName || S3_DEPLOYMENT_BUCKET_RESOURCE_NAME +} + +export const getBucketReference = async (context) => { + return context.__userAWSBucket ? context.awsBucket : { + "Ref": await getDeploymentBucketResourceName() + } +} \ No newline at end of file diff --git a/src/cli/providers/cloudFormation/context/sns.ts b/src/cli/providers/cloudFormation/context/sns.ts new file mode 100644 index 0000000..ea46eae --- /dev/null +++ b/src/cli/providers/cloudFormation/context/sns.ts @@ -0,0 +1,178 @@ +import { getMetadata, constants, defineMetadata } from '../../../../annotations' +const { CLASS_SNSCONFIGURATIONKEY, CLASS_ENVIRONMENTKEY } = constants +import { ExecuteStep, executor } from '../../../context' +import { collectMetadata } from '../../../utilities/collectMetadata' +import { setResource } from '../utils' +import { createStack, setStackParameter, getStackName } from './stack' + +export const SNS_TABLE_STACK = 'SNSStack' + +export const sns = ExecuteStep.register('SNS', async (context) => { + await executor({ + context: { ...context, stackName: SNS_TABLE_STACK }, + name: `CloudFormation-Stack-init-${SNS_TABLE_STACK}`, + method: createStack + }) + + await executor(context, snsTopics) +}) + +export const snsTopics = ExecuteStep.register('SNS-Topics', async (context) => { + const configs = collectMetadata(context, { + metadataKey: CLASS_SNSCONFIGURATIONKEY, + selector: (c) => c.topicName + }) + + for (const snsConfig of configs) { + await executor({ + context: { ...context, snsConfig }, + name: `SNS-Topic-${snsConfig.topicName}`, + method: snsTopic + }) + + await executor({ + context: { ...context, snsConfig }, + name: `SNS-Topic-Subscription-${snsConfig.topicName}`, + method: snsTopicSubscriptions + }) + } +}) + +export const snsTopic = async (context) => { + const { snsConfig } = context + + snsConfig.ADVTopicName = snsConfig.topicName + snsConfig.AWSTopicName = snsConfig.topicName + + if (!snsConfig.exists) { + + snsConfig.ADVTopicName = `${snsConfig.topicName}${context.date.valueOf()}` + snsConfig.AWSTopicName = `${snsConfig.ADVTopicName}-${context.stage}` + + const snsProperties = { + "TopicName": snsConfig.AWSTopicName + } + + const snsTopic = { + "Type": "AWS::SNS::Topic", + "Properties": snsProperties + } + + const resourceName = `SNS${snsConfig.ADVTopicName}` + const topicResourceName = setResource(context, resourceName, snsTopic, SNS_TABLE_STACK, true) + snsConfig.resourceName = topicResourceName + } + + await executor({ + context, + name: `SNS-Topic-${snsConfig.topicName}-DynamicName`, + method: updateSNSEnvironmentVariables + }) +} + +export const snsTopicSubscriptions = async (context) => { + const { snsConfig } = context + + if (snsConfig.exists) return + + for (const { serviceDefinition, serviceConfig } of snsConfig.services) { + if (!serviceConfig.eventSource) continue + + await executor({ + context: { ...context, serviceDefinition }, + name: `SNS-Topic-Subscription-${snsConfig.topicName}-${serviceDefinition.service.name}`, + method: snsTopicSubscription + }) + + await executor({ + context: { ...context, serviceDefinition }, + name: `SNS-Topic-Permission-${snsConfig.topicName}-${serviceDefinition.service.name}`, + method: snsPermissions + }) + } +} + +export const snsTopicSubscription = async (context) => { + const { serviceDefinition, snsConfig } = context + + if (snsConfig.exists) return + + await setStackParameter({ + ...context, + sourceStackName: SNS_TABLE_STACK, + resourceName: snsConfig.resourceName, + targetStackName: getStackName(serviceDefinition) + }) + + const snsProperties = { + "Endpoint": { + "Fn::GetAtt": [serviceDefinition.resourceName, "Arn"] + }, + "Protocol": "lambda", + "TopicArn": { + "Ref": snsConfig.resourceName + } + } + + const snsTopic = { + "Type": "AWS::SNS::Subscription", + "Properties": snsProperties + } + + const resourceName = `SNS${serviceDefinition.resourceName}${snsConfig.resourceName}` + const topicResourceName = setResource(context, resourceName, snsTopic, getStackName(serviceDefinition)) +} + +export const snsPermissions = (context) => { + const { serviceDefinition, snsConfig } = context + + if (snsConfig.exists) return + + const properties = { + "FunctionName": { + "Fn::GetAtt": [ + serviceDefinition.resourceName, + "Arn" + ] + }, + "Action": "lambda:InvokeFunction", + "Principal": "sns.amazonaws.com", + "SourceArn": { "Ref": snsConfig.resourceName } + } + + const snsPermission = { + "Type": "AWS::Lambda::Permission", + "Properties": properties + } + const resourceName = `${snsConfig.resourceName}Permission` + setResource(context, resourceName, snsPermission, getStackName(serviceDefinition), false, true) +} + +const updateSNSEnvironmentVariables = async (context) => { + const { snsConfig } = context + + for (const { serviceDefinition, serviceConfig } of snsConfig.services) { + const environmentVariables = getMetadata(CLASS_ENVIRONMENTKEY, serviceDefinition.service) || {} + environmentVariables[serviceConfig.environmentKey] = `${snsConfig.ADVTopicName}` + + if (snsConfig.exists) { + environmentVariables[`${serviceConfig.environmentKey}_ARN`] = { + "Fn::Sub": "arn:aws:sns:${AWS::Region}:${AWS::AccountId}:" + snsConfig.AWSTopicName + } + } else { + await setStackParameter({ + ...context, + sourceStackName: SNS_TABLE_STACK, + resourceName: snsConfig.resourceName, + targetStackName: getStackName(serviceDefinition) + }) + + environmentVariables[`${serviceConfig.environmentKey}_ARN`] = { + "Ref": snsConfig.resourceName + } + } + + defineMetadata(CLASS_ENVIRONMENTKEY, { ...environmentVariables }, serviceDefinition.service) + } + +} \ No newline at end of file diff --git a/src/cli/providers/cloudFormation/context/stack.ts b/src/cli/providers/cloudFormation/context/stack.ts new file mode 100644 index 0000000..ed81eb2 --- /dev/null +++ b/src/cli/providers/cloudFormation/context/stack.ts @@ -0,0 +1,117 @@ +import { ExecuteStep, executor } from '../../../context' +import { setResource, getResourceName } from '../utils' +import { getFunctionName, getMetadata, constants } from '../../../../annotations' +import { getBucketReference } from './s3StorageDeployment' +const { CLASS_GROUP } = constants + +import { defaultsDeep } from 'lodash' +export const __createdStackNames = [] + +export const createStack = async (context) => { + const { stackName } = context + + if (__createdStackNames.indexOf(stackName) >= 0) return + + __createdStackNames.push(stackName) + context.CloudFormationStacks[stackName] = defaultsDeep({ + "AWSTemplateFormatVersion": "2010-09-09", + "Parameters": {}, + "Resources": {}, + "Outputs": {} + }, context.CloudFormationStacks[stackName] || {}) + + const folderPah = context.version ? `${context.version}/${context.date.toISOString()}` : `${context.date.toISOString()}` + const awsBucket = await getBucketReference(context) + + const properties = { + "Parameters": {}, + "TemplateURL": { + "Fn::Join": [ + "/", + [ + "https://s3.amazonaws.com", + awsBucket, + context.projectName || `functionly`, + context.stage, + folderPah, + `${stackName}.template.json` + ] + ] + } + } + + + const stack = { + "Type": "AWS::CloudFormation::Stack", + "Properties": properties, + "DependsOn": [] + } + + return setResource(context, stackName, stack) +} + +export const setStackParameter = (context) => { + const { sourceStackName, resourceName, targetStackName, attr } = context + + let parameterReference: any = { + "Ref": resourceName + } + let attrName = '' + + if (sourceStackName) { + parameterReference = { + "Fn::GetAtt": [ + sourceStackName, + `Outputs.${resourceName}` + ] + } + if (attr) { + attrName = attr + parameterReference = { + "Fn::GetAtt": [ + sourceStackName, + `Outputs.${resourceName}${attr}` + ] + } + context.CloudFormationStacks[sourceStackName].Outputs[`${resourceName}${attr}`] = { + "Value": { + "Fn::GetAtt": [ + resourceName, + attr + ] + } + } + } + } else if (attr) { + attrName = attr + parameterReference = { + "Fn::GetAtt": [ + resourceName, + attr + ] + } + } + + for (const stackName of __createdStackNames) { + const template = context.CloudFormationStacks[stackName] + const stackDefinition = context.CloudFormationTemplate.Resources[stackName] + if (stackName !== sourceStackName && (!targetStackName || targetStackName === stackName)) { + template.Parameters[`${resourceName}${attrName}`] = { + "Type": "String" + } + stackDefinition.Properties.Parameters[`${resourceName}${attrName}`] = parameterReference + + + const dependsOnResourceName = sourceStackName ? sourceStackName : resourceName + if (stackDefinition.DependsOn.indexOf(dependsOnResourceName) < 0) { + stackDefinition.DependsOn.push(dependsOnResourceName) + } + } + } + +} + +export const getStackName = (serviceDefinition) => { + const groupName = getMetadata(CLASS_GROUP, serviceDefinition.service) || getFunctionName(serviceDefinition.service) + return getResourceName(`Stack${groupName}`) +} \ No newline at end of file diff --git a/src/cli/providers/cloudFormation/context/uploadTemplate.ts b/src/cli/providers/cloudFormation/context/uploadTemplate.ts index 1b4d25b..8a8aa4e 100644 --- a/src/cli/providers/cloudFormation/context/uploadTemplate.ts +++ b/src/cli/providers/cloudFormation/context/uploadTemplate.ts @@ -1,8 +1,33 @@ -import { upload } from '../../../utilities/aws/s3Upload' +import { uploaderStep } from '../../../utilities/aws/s3Upload' +import { writeFile } from '../../../utilities/local/file' +import { ExecuteStep, executor } from '../../../context' +import { projectConfig } from '../../../project/config' -export const uploadTemplate = async (context) => { +export const persistCreateTemplate = ExecuteStep.register('PersistCreateTemplate', async (context) => { const templateData = JSON.stringify(context.CloudFormationTemplate, null, 2); - const uploadresult = await upload(context, `services-${context.date.toISOString()}.template`, templateData, 'application/octet-stream') + const fileName = projectConfig.name ? `${projectConfig.name}.create.template.json` : 'cloudformation.create.template.json' + writeFile(fileName, Buffer.from(templateData, 'binary')) +}) + +export const uploadTemplate = ExecuteStep.register('UploadTemplate', async (context) => { + for (const stackName in context.CloudFormationStacks) { + const stack = context.CloudFormationStacks[stackName] + if (!stack.Resources || !Object.keys(stack.Resources).length) { + delete context.CloudFormationStacks[stackName] + delete context.CloudFormationTemplate.Resources[stackName] + } + } + + const templateData = JSON.stringify(context.CloudFormationTemplate, null, 2); + const fileName = projectConfig.name ? `${projectConfig.name}.template.json` : 'cloudformation.template.json' + const uploadresult = await executor(context, uploaderStep(fileName, templateData, 'application/octet-stream')) context.S3CloudFormationTemplate = uploadresult.Key + + for (const stackName in context.CloudFormationStacks) { + const templateData = JSON.stringify(context.CloudFormationStacks[stackName], null, 2); + const templateFileName = `${stackName}.template.json` + const uploadresult = await executor(context, uploaderStep(templateFileName, templateData, 'application/octet-stream')) + } + return uploadresult -} \ No newline at end of file +}) \ No newline at end of file diff --git a/src/cli/providers/cloudFormation/index.ts b/src/cli/providers/cloudFormation/index.ts index e3922bd..7a33abd 100644 --- a/src/cli/providers/cloudFormation/index.ts +++ b/src/cli/providers/cloudFormation/index.ts @@ -1,41 +1,110 @@ import { getFunctionName } from '../../../annotations' -import { bundle } from '../../utilities/webpack' +import { logger } from '../../utilities/logger' import { zip } from '../../utilities/compress' -import { uploadZip } from '../../utilities/aws/s3Upload' -import { createStack, updateStack, getTemplate } from '../../utilities/aws/cloudFormation' - -import { cloudFormationInit } from './context/cloudFormationInit' -import { tableResources, lambdaResources, roleResources } from './context/resources' -import { uploadTemplate } from './context/uploadTemplate' - -export const FUNCTIONAL_ENVIRONMENT = 'aws' - -export const createEnvironment = async (context) => { - await bundle(context) - await zip(context) - await uploadZip(context, `services-${context.date.toISOString()}.zip`, context.zipData()) - - - await cloudFormationInit(context) - await roleResources(context) - await tableResources(context) - await lambdaResources(context) - - await uploadTemplate(context) - - try { - await getTemplate(context) - await updateStack(context) - console.log('updated') - } catch (e) { - if (/^Stack with id .* does not exist$/.test(e.message)) { - await createStack(context) - console.log('created') - } else { - console.log(e) - throw e +import { uploadZipStep } from '../../utilities/aws/s3Upload' +import { createStack, updateStack, getTemplate, describeStackResouce, describeStacks } from '../../utilities/aws/cloudFormation' +import { projectConfig } from '../../project/config' +import { executor } from '../../context' + +import { cloudFormationInit, cloudFormationMerge } from './context/cloudFormationInit' +import { + tableResources, lambdaResources, roleResources, s3DeploymentBucket, s3DeploymentBucketParameter, + apiGateway, sns, s3, cloudWatchEvent, initStacks, lambdaLogResources, getDeploymentBucketResourceName, tableSubscribers +} from './context/resources' +import { uploadTemplate, persistCreateTemplate } from './context/uploadTemplate' + +export const cloudFormation = { + FUNCTIONAL_ENVIRONMENT: 'aws', + createEnvironment: async (context) => { + logger.info(`Functionly: Packaging...`) + await executor(context, zip) + + await executor(context, cloudFormationInit) + await executor(context, s3DeploymentBucket) + + logger.info(`Functionly: Save create template...`) + await executor(context, persistCreateTemplate) + + try { + await executor(context, getTemplate) + } catch (e) { + if (/^Stack with id .* does not exist$/.test(e.message)) { + logger.info(`Functionly: Creating stack...`) + + await executor(context, createStack) + } else { + console.log(e) + throw e + } } + if (!context.awsBucket) { + const logicalResourceId = await getDeploymentBucketResourceName() + const bucketData = await executor({ ...context, LogicalResourceId: logicalResourceId }, describeStackResouce) + context.awsBucket = bucketData.StackResourceDetail.PhysicalResourceId + } + + logger.info(`Functionly: Uploading binary...`) + const fileName = projectConfig.name ? `${projectConfig.name}.zip` : 'project.zip' + await executor(context, uploadZipStep(fileName, context.zipData())) + + await executor(context, cloudFormationMerge) + + await executor(context, initStacks) + await executor(context, s3DeploymentBucketParameter) + + await executor(context, tableResources) + await executor(context, lambdaLogResources) + await executor(context, roleResources) + await executor(context, lambdaResources) + await executor(context, apiGateway) + await executor(context, sns) + await executor(context, s3) + await executor(context, cloudWatchEvent) + await executor(context, tableSubscribers) + + logger.info(`Functionly: Uploading template...`) + await executor(context, uploadTemplate) + logger.info(`Functionly: Updating stack...`) + const stackData = await executor(context, updateStack) + logger.info(`Functionly: Complete`) + + logger.info('') + for (const {OutputKey, OutputValue} of stackData.Outputs) { + logger.info(`${OutputKey}: ${OutputValue}`) + } + }, + package: async (context) => { + logger.info(`Functionly: Packaging...`) + await executor(context, zip) + + await executor(context, cloudFormationInit) + await executor(context, s3DeploymentBucket) + + logger.info(`Functionly: Save create template...`) + await executor(context, persistCreateTemplate) + + logger.info(`Functionly: Save binary...`) + const fileName = projectConfig.name ? `${projectConfig.name}.zip` : 'project.zip' + await executor({ ...context, skipUpload: true }, uploadZipStep(fileName, context.zipData())) + + await executor(context, cloudFormationMerge) + + await executor(context, initStacks) + await executor(context, s3DeploymentBucketParameter) + + await executor(context, tableResources) + await executor(context, lambdaLogResources) + await executor(context, roleResources) + await executor(context, lambdaResources) + await executor(context, apiGateway) + await executor(context, sns) + await executor(context, s3) + await executor(context, cloudWatchEvent) + await executor(context, tableSubscribers) + + logger.info(`Functionly: Save template...`) + await executor({ ...context, skipUpload: true }, uploadTemplate) + logger.info(`Functionly: Complete`) } } - diff --git a/src/cli/providers/cloudFormation/utils.ts b/src/cli/providers/cloudFormation/utils.ts new file mode 100644 index 0000000..533e5ea --- /dev/null +++ b/src/cli/providers/cloudFormation/utils.ts @@ -0,0 +1,63 @@ +export const nameReplaceRegexp = /[^a-zA-Z0-9]/g +export const normalizeName = (name: string) => { + const result = name.replace(nameReplaceRegexp, '') + if (!result) { + throw new Error(`'invalid name '${name}'`) + } + return result +} + +export const getResourceName = (name) => { + if (!name) { + throw new Error(`invalid resource name '${name}'`) + } + return normalizeName(name) +} + +export const setResource = (context: any, name: string, resource: any, stackName: string = null, output = false, allowOverride = false) => { + const resourceName = getResourceName(name) + + let resources + let outputs + if (stackName) { + if (context.CloudFormationStacks[stackName]) { + resources = context.CloudFormationStacks[stackName].Resources + outputs = context.CloudFormationStacks[stackName].Outputs + } + } else { + resources = context.CloudFormationTemplate.Resources + } + + if (!resources) { + throw new Error(`Stack with name '${stackName}' not defined`) + } + + if (allowOverride === false && resources[resourceName]) { + throw new Error(`resource name '${resourceName}' already exists`) + } + + context.usedAwsResources = context.usedAwsResources || []; + if (context.usedAwsResources.indexOf(resource.Type) < 0) { + context.usedAwsResources.push(resource.Type) + } + + resources[resourceName] = resource + if (output && outputs) { + outputs[resourceName] = { + "Value": { + "Ref": resourceName + } + } + } + + if (Array.isArray(context.deploymentResources)) { + context.deploymentResources.push({ + resourceName, + type: resource.Type, + stackName, + resource + }) + } + + return resourceName; +} diff --git a/src/cli/providers/index.ts b/src/cli/providers/index.ts index 9da9552..1a24068 100644 --- a/src/cli/providers/index.ts +++ b/src/cli/providers/index.ts @@ -1,22 +1,49 @@ -import * as aws from './aws' -import * as local from './local' -import * as cf from './cloudFormation' -import { setFunctionalEnvironment } from '../context' +import { aws as awssdk } from './aws' +import { local } from './local' +import { cloudFormation as aws } from './cloudFormation' +import { azure } from './azureARM' +import { ExecuteStep, executor } from '../context' +import { getPluginDefinitions } from '../project/config' -let environments = { aws, local, cf } +let environments = { aws, local, awssdk, azure } -export const createEnvironment = async (context) => { +export class CreateEnvironmentStep extends ExecuteStep { + public async method(context) { + let currentEnvironment = environments[context.deployTarget] - let currentEnvironment = environments[context.deployTarget] + if (!currentEnvironment) { + const plugins = getPluginDefinitions() + for (const plugin of plugins) { + if (plugin.config.deployProviders && + plugin.config.deployProviders[context.deployTarget] && + plugin.config.deployProviders[context.deployTarget].createEnvironment + ) { + currentEnvironment = plugin.config.deployProviders[context.deployTarget] + break; + } + } + } - if (!currentEnvironment) { - throw new Error(`unhandled deploy target: '${context.deployTarget}'`) - } - if (currentEnvironment.FUNCTIONAL_ENVIRONMENT) { - context.FUNCTIONAL_ENVIRONMENT = currentEnvironment.FUNCTIONAL_ENVIRONMENT - setFunctionalEnvironment(context) + if (!currentEnvironment) { + throw new Error(`unhandled deploy target: '${context.deployTarget}'`) + } + + if (currentEnvironment.FUNCTIONAL_ENVIRONMENT) { + context.FUNCTIONAL_ENVIRONMENT = currentEnvironment.FUNCTIONAL_ENVIRONMENT + await executor(context, ExecuteStep.get('SetFunctionalEnvironment')) + } + + if (context.packageOnly) { + if (currentEnvironment.package) { + await executor(context, currentEnvironment.package) + } else { + throw new Error('package creation not implemented') + } + } else { + await executor(context, currentEnvironment.createEnvironment) + } } +} - await currentEnvironment.createEnvironment(context) -} \ No newline at end of file +export const createEnvironment = new CreateEnvironmentStep('CreateEnvironment') \ No newline at end of file diff --git a/src/cli/providers/local.ts b/src/cli/providers/local.ts index e151625..eeeb95e 100644 --- a/src/cli/providers/local.ts +++ b/src/cli/providers/local.ts @@ -1,8 +1,10 @@ import { createTables } from '../utilities/aws/dynamoDB' import { getMetadata, constants } from '../../annotations' +import { ExecuteStep, executor } from '../context' -export const FUNCTIONAL_ENVIRONMENT = 'local' - -export const createEnvironment = async (context) => { - await createTables(context) +export const local = { + FUNCTIONAL_ENVIRONMENT: 'local', + createEnvironment: ExecuteStep.register('CreateEnvironment_local', async (context) => { + await executor(context, createTables) + }) } diff --git a/src/cli/utilities/aws/cloudFormation.ts b/src/cli/utilities/aws/cloudFormation.ts index 28c6745..7da203b 100644 --- a/src/cli/utilities/aws/cloudFormation.ts +++ b/src/cli/utilities/aws/cloudFormation.ts @@ -1,6 +1,7 @@ import { CloudFormation } from 'aws-sdk' -import { merge, pick } from 'lodash' +import { pick } from 'lodash' import { config } from '../../utilities/config' +import { ExecuteStep } from '../../context' export const UPDATE_STACK_PRPERTIES = ['StackName', 'Capabilities', 'ClientRequestToken', 'Parameters', 'ResourceTypes', 'RoleARN', 'StackPolicyBody', @@ -11,7 +12,7 @@ export const CREATE_STACK_PRPERTIES = ['StackName', 'Capabilities', 'ClientReque let cloudFormation = null; const initAWSSDK = (context) => { if (!cloudFormation) { - let awsConfig = merge({}, config.aws.CloudFormation) + let awsConfig = { ...config.aws.CloudFormation } if (context.awsRegion) { awsConfig.region = context.awsRegion } @@ -23,42 +24,60 @@ const initAWSSDK = (context) => { -export const createStack = (context) => { +export const createStack = ExecuteStep.register('CloudFormation-CreateStack', (context) => { initAWSSDK(context) return new Promise((resolve, reject) => { - const cfConfig = pick(context.CloudFormationConfig, CREATE_STACK_PRPERTIES) - const params = merge({}, cfConfig, { - TemplateURL: `https://s3.eu-central-1.amazonaws.com/${context.awsBucket}/${context.S3CloudFormationTemplate}` - }) + const cfConfig: any = pick(context.CloudFormationConfig, CREATE_STACK_PRPERTIES) + const params = { + ...cfConfig, + StackName: `${cfConfig.StackName}-${context.stage}`, + TemplateBody: JSON.stringify(context.CloudFormationTemplate, null, 2) + } + + cloudFormation.createStack(params, async (err, data) => { + if (err) return reject(err) + + try { + const result = await stackStateWaiter('CREATE_COMPLETE', context) + resolve(result) + } catch (e) { + reject(e) + } - cloudFormation.createStack(params, function (err, data) { - if (err) reject(err) - else resolve(data); }); }) -} +}) -export const updateStack = (context) => { +export const updateStack = ExecuteStep.register('CloudFormation-UpdateStack', (context) => { initAWSSDK(context) return new Promise((resolve, reject) => { - const cfConfig = pick(context.CloudFormationConfig, UPDATE_STACK_PRPERTIES) - const params = merge({}, cfConfig, { - TemplateURL: `https://s3.eu-central-1.amazonaws.com/${context.awsBucket}/${context.S3CloudFormationTemplate}`, + const cfConfig: any = pick(context.CloudFormationConfig, UPDATE_STACK_PRPERTIES) + const params = { + ...cfConfig, + StackName: `${cfConfig.StackName}-${context.stage}`, + TemplateURL: `https://s3.${context.awsRegion}.amazonaws.com/${context.awsBucket}/${context.S3CloudFormationTemplate}`, UsePreviousTemplate: false - }) + } + + cloudFormation.updateStack(params, async (err, data) => { + if (err) return reject(err) + + try { + const result = await stackStateWaiter('UPDATE_COMPLETE', context, 1000) + resolve(result) + } catch (e) { + reject(e) + } - cloudFormation.updateStack(params, function (err, data) { - if (err) reject(err) - else resolve(data); }); }) -} +}) -export const getTemplate = (context) => { +export const getTemplate = ExecuteStep.register('CloudFormation-GetTemplate', (context) => { initAWSSDK(context) return new Promise((resolve, reject) => { let params = { - StackName: context.CloudFormationConfig.StackName + StackName: `${context.CloudFormationConfig.StackName}-${context.stage}` } cloudFormation.getTemplate(params, function (err, data) { @@ -66,4 +85,60 @@ export const getTemplate = (context) => { else resolve(data); }); }) +}) + +export const describeStackResouce = ExecuteStep.register('CloudFormation-DescribeStackResouce', (context) => { + initAWSSDK(context) + return new Promise((resolve, reject) => { + let params = { + StackName: `${context.CloudFormationConfig.StackName}-${context.stage}`, + LogicalResourceId: context.LogicalResourceId + } + + cloudFormation.describeStackResource(params, function (err, data) { + if (err) return reject(err) + return resolve(data); + }); + }) +}) + +export const describeStacks = ExecuteStep.register('CloudFormation-DescribeStacks', (context) => { + initAWSSDK(context) + return new Promise((resolve, reject) => { + const stackName = `${context.CloudFormationConfig.StackName}-${context.stage}` + let params = { + StackName: stackName + } + + cloudFormation.describeStacks(params, function (err, data) { + if (err) return reject(err) + const result = data.Stacks.find(s => s.StackName === stackName) + return resolve(result); + }); + }) +}) + +export const stackStateWaiter = (status, context, tries = 20, timeout = 5000) => { + const waitregexp = /_PROGRESS$/ + return new Promise((resolve, reject) => { + let iteration = 0 + const waiter = () => { + if (iteration >= tries) return reject(new Error(`stackStateWaiter timeout '${status}'`)) + iteration++ + setTimeout(async () => { + const stack = await describeStacks.method(context) + console.log(' >', stack.StackStatus) + if (waitregexp.test(stack.StackStatus)) { + return waiter() + } + + if (stack.StackStatus === status) { + return resolve(stack) + } + return reject(stack) + }, timeout) + } + + waiter() + }) } \ No newline at end of file diff --git a/src/cli/utilities/aws/dynamoDB.ts b/src/cli/utilities/aws/dynamoDB.ts index e73d65b..dfef836 100644 --- a/src/cli/utilities/aws/dynamoDB.ts +++ b/src/cli/utilities/aws/dynamoDB.ts @@ -1,12 +1,12 @@ import { DynamoDB } from 'aws-sdk' -import { merge } from 'lodash' import { config } from '../config' import { __dynamoDBDefaults } from '../../../annotations' +import { ExecuteStep, executor } from '../../context' let dynamoDB = null; const initAWSSDK = (context) => { if (!dynamoDB) { - let awsConfig = merge({}, config.aws.DynamoDB) + let awsConfig = { ...config.aws.DynamoDB } if (context.awsRegion) { awsConfig.region = context.awsRegion } @@ -21,12 +21,16 @@ const initAWSSDK = (context) => { } -export const createTables = async (context) => { +export const createTables = ExecuteStep.register('CreateTables', async (context) => { initAWSSDK(context) for (let tableConfig of context.tableConfigs) { try { - let data = await createTable(tableConfig, context) + let data = await executor({ + context: { ...context, tableConfig }, + name: `CreateTable-${tableConfig.tableName}`, + method: createTable + }) console.log(`${data.TableDescription.TableName} DynamoDB table created.`) } catch (e) { if (e.code !== 'ResourceInUseException') { @@ -34,15 +38,18 @@ export const createTables = async (context) => { } } } -} +}) -export const createTable = (tableConfig, context) => { +export const createTable = (context) => { initAWSSDK(context) + const { tableConfig } = context return new Promise((resolve, reject) => { - let params = merge({}, { - TableName: tableConfig.tableName - }, tableConfig.nativeConfig, __dynamoDBDefaults); + let params = { + ...__dynamoDBDefaults, + TableName: tableConfig.tableName + `-${process.env.FUNCTIONAL_STAGE}`, + ...tableConfig.nativeConfig + }; dynamoDB.createTable(params, function (err, data) { if (err) reject(err) diff --git a/src/cli/utilities/aws/lambda.ts b/src/cli/utilities/aws/lambda.ts index 246d0be..fe2d97d 100644 --- a/src/cli/utilities/aws/lambda.ts +++ b/src/cli/utilities/aws/lambda.ts @@ -1,13 +1,14 @@ import { Lambda } from 'aws-sdk' -import { merge, difference } from 'lodash' +import { difference } from 'lodash' import { config } from '../../utilities/config' import { getMetadata, constants, getFunctionName } from '../../../annotations' +import { ExecuteStep, executor } from '../../context' let lambda = null; const initAWSSDK = (context) => { if (!lambda) { - let awsConfig = merge({}, config.aws.Lambda) + let awsConfig = { ...config.aws.Lambda } if (context.awsRegion) { awsConfig.region = context.awsRegion } @@ -19,7 +20,7 @@ const initAWSSDK = (context) => { -export const createLambdaFunction = (serviceDefinition, context) => { +export const createLambdaFunction = ExecuteStep.register('CreateLambdaFunction', (context) => { initAWSSDK(context) return new Promise((resolve, reject) => { let params = { @@ -27,18 +28,18 @@ export const createLambdaFunction = (serviceDefinition, context) => { S3Bucket: context.awsBucket, S3Key: context.S3Zip }, - Description: getMetadata(constants.CLASS_DESCRIPTIONKEY, serviceDefinition.service), - FunctionName: getFunctionName(serviceDefinition.service), - Handler: serviceDefinition.handler, - MemorySize: getMetadata(constants.CLASS_MEMORYSIZEKEY, serviceDefinition.service), + Description: getMetadata(constants.CLASS_DESCRIPTIONKEY, context.serviceDefinition.service), + FunctionName: getFunctionName(context.serviceDefinition.service), + Handler: context.serviceDefinition.handler, + MemorySize: getMetadata(constants.CLASS_AWSMEMORYSIZEKEY, context.serviceDefinition.service), Publish: true, - Role: getMetadata(constants.CLASS_ROLEKEY, serviceDefinition.service), - Runtime: getMetadata(constants.CLASS_RUNTIMEKEY, serviceDefinition.service) || "nodejs6.10", - Timeout: getMetadata(constants.CLASS_TIMEOUTKEY, serviceDefinition.service), + Role: getMetadata(constants.CLASS_ROLEKEY, context.serviceDefinition.service), + Runtime: getMetadata(constants.CLASS_AWSRUNTIMEKEY, context.serviceDefinition.service) || "nodejs20.x", + Timeout: getMetadata(constants.CLASS_AWSTIMEOUTKEY, context.serviceDefinition.service), Environment: { - Variables: getMetadata(constants.CLASS_ENVIRONMENTKEY, serviceDefinition.service) + Variables: getMetadata(constants.CLASS_ENVIRONMENTKEY, context.serviceDefinition.service) }, - Tags: getMetadata(constants.CLASS_TAGKEY, serviceDefinition.service), + Tags: getMetadata(constants.CLASS_TAGKEY, context.serviceDefinition.service), VpcConfig: { } }; @@ -48,52 +49,52 @@ export const createLambdaFunction = (serviceDefinition, context) => { else resolve(data); }); }) -} +}) -export const getLambdaFunction = (serviceDefinition, context) => { +export const getLambdaFunction = ExecuteStep.register('GetLambdaFunction', (context) => { initAWSSDK(context) return new Promise((resolve, reject) => { let params = { - FunctionName: getFunctionName(serviceDefinition.service) + FunctionName: getFunctionName(context.serviceDefinition.service) }; lambda.getFunction(params, function (err, data) { if (err) reject(err) else resolve(data); }); }) -} +}) -export const deleteLambdaFunction = (serviceDefinition, context) => { +export const deleteLambdaFunction = ExecuteStep.register('DeleteLambdaFunction', (context) => { initAWSSDK(context) return new Promise((resolve, reject) => { let params = { - FunctionName: getFunctionName(serviceDefinition.service) + FunctionName: getFunctionName(context.serviceDefinition.service) }; lambda.deleteFunction(params, function (err, data) { if (err) reject(err) else resolve(data); }); }) -} +}) -export const publishLambdaFunction = (serviceDefinition, context) => { +export const publishLambdaFunction = ExecuteStep.register('PublishLambdaFunction', (context) => { initAWSSDK(context) return new Promise((resolve, reject) => { let params = { - FunctionName: getFunctionName(serviceDefinition.service) + FunctionName: getFunctionName(context.serviceDefinition.service) }; lambda.publishVersion(params, function (err, data) { if (err) reject(err) else resolve(data); }); }) -} +}) -export const updateLambdaFunctionCode = (serviceDefinition, context) => { +export const updateLambdaFunctionCode = ExecuteStep.register('UpdateLambdaFunctionCode', (context) => { initAWSSDK(context) return new Promise((resolve, reject) => { let params = { - FunctionName: getFunctionName(serviceDefinition.service), + FunctionName: getFunctionName(context.serviceDefinition.service), S3Bucket: context.awsBucket, S3Key: context.S3Zip, Publish: true @@ -103,22 +104,22 @@ export const updateLambdaFunctionCode = (serviceDefinition, context) => { else resolve(data); }); }) -} +}) -export const updateLambdaFunctionConfiguration = (serviceDefinition, context) => { +export const updateLambdaFunctionConfiguration = ExecuteStep.register('UpdateLambdaFunctionConfiguration', (context) => { initAWSSDK(context) return new Promise((resolve, reject) => { let params = { - FunctionName: getFunctionName(serviceDefinition.service), - Description: getMetadata(constants.CLASS_DESCRIPTIONKEY, serviceDefinition.service), + FunctionName: getFunctionName(context.serviceDefinition.service), + Description: getMetadata(constants.CLASS_DESCRIPTIONKEY, context.serviceDefinition.service), Environment: { - Variables: getMetadata(constants.CLASS_ENVIRONMENTKEY, serviceDefinition.service) + Variables: getMetadata(constants.CLASS_ENVIRONMENTKEY, context.serviceDefinition.service) }, - Handler: serviceDefinition.handler, - MemorySize: getMetadata(constants.CLASS_MEMORYSIZEKEY, serviceDefinition.service), - Role: getMetadata(constants.CLASS_ROLEKEY, serviceDefinition.service), - Runtime: getMetadata(constants.CLASS_RUNTIMEKEY, serviceDefinition.service) || "nodejs6.10", - Timeout: getMetadata(constants.CLASS_TIMEOUTKEY, serviceDefinition.service), + Handler: context.serviceDefinition.handler, + MemorySize: getMetadata(constants.CLASS_AWSMEMORYSIZEKEY, context.serviceDefinition.service), + Role: getMetadata(constants.CLASS_ROLEKEY, context.serviceDefinition.service), + Runtime: getMetadata(constants.CLASS_AWSRUNTIMEKEY, context.serviceDefinition.service) || "nodejs20.x", + Timeout: getMetadata(constants.CLASS_AWSTIMEOUTKEY, context.serviceDefinition.service), VpcConfig: { } }; @@ -127,12 +128,12 @@ export const updateLambdaFunctionConfiguration = (serviceDefinition, context) => else resolve(data); }); }) -} +}) -export const updateLambdaFunctionTags = async (serviceDefinition, context) => { +export const updateLambdaFunctionTags = ExecuteStep.register('UpdateLambdaFunctionTags', async (context) => { initAWSSDK(context) - const getLambdaFunctionResult = await getLambdaFunction(serviceDefinition, context) - const Tags = getMetadata(constants.CLASS_TAGKEY, serviceDefinition.service) || {} + const getLambdaFunctionResult = await executor(context, getLambdaFunction) + const Tags = getMetadata(constants.CLASS_TAGKEY, context.serviceDefinition.service) || {} const listTagParams = { Resource: getLambdaFunctionResult.Configuration.FunctionArn @@ -156,7 +157,7 @@ export const updateLambdaFunctionTags = async (serviceDefinition, context) => { await tagResource(tagResourceParams, context) } -} +}) export const listTags = (params, context) => { initAWSSDK(context) diff --git a/src/cli/utilities/aws/s3Upload.ts b/src/cli/utilities/aws/s3Upload.ts index b351cf1..9999870 100644 --- a/src/cli/utilities/aws/s3Upload.ts +++ b/src/cli/utilities/aws/s3Upload.ts @@ -1,11 +1,12 @@ import { S3 } from 'aws-sdk' -import { merge } from 'lodash' import { config } from '../config' +import { ExecuteStep, executor } from '../../context' +import { writeFile } from '../local/file' let s3 = null; const initAWSSDK = (context) => { if (!s3) { - let awsConfig = merge({}, config.aws.S3) + let awsConfig = { ...config.aws.S3 } if (context.awsRegion) { awsConfig.region = context.awsRegion } @@ -15,26 +16,53 @@ const initAWSSDK = (context) => { return s3 } -export const uploadZip = async (context, name, data) => { - const uploadResult = await upload(context, name, data, 'application/zip') - context.S3Zip = uploadResult.Key - return uploadResult +export const uploadZipStep = (name, data) => { + return async (context) => { + const step = uploaderStep(name, data, 'application/zip') + const uploadResult = await executor(context, step) + context.S3Zip = uploadResult.Key + return uploadResult + } } +export const uploaderStep = (name, data, contentType) => { + return async (context) => { + context.upload = { + name, + data, + contentType + } + const uploadResult = await executor(context, uploadToAws) + delete context.upload + return uploadResult + } +} -export const upload = (context, name, data, contentType) => { +export const uploadToAws = ExecuteStep.register('S3-Upload', async (context) => { initAWSSDK(context) return new Promise((resolve, reject) => { - let params = merge({}, config.S3, { + const version = context.version ? `${context.version}/` : '' + const folderPah = context.version ? `${context.version}/${context.date.toISOString()}` : `${context.date.toISOString()}` + const binary = Buffer.from(context.upload.data, 'binary') + let params = { + ...config.S3, Bucket: context.awsBucket, - Body: new Buffer(data, 'binary'), - Key: name, - ContentType: contentType - }) + Body: binary, + Key: `${context.projectName || 'functionly'}/${context.stage}/${folderPah}/${context.upload.name}`, + ContentType: context.upload.contentType + } + + if (context.skipUpload) { + writeFile(context.upload.name, binary) + return resolve(params) + } s3.putObject(params, (err, res) => { if (err) return reject(err) + + writeFile(context.upload.name, binary) + return resolve(params) }) }) -} \ No newline at end of file +}) diff --git a/src/cli/utilities/aws/sns.ts b/src/cli/utilities/aws/sns.ts new file mode 100644 index 0000000..92bb0a5 --- /dev/null +++ b/src/cli/utilities/aws/sns.ts @@ -0,0 +1,46 @@ +import { SNS } from 'aws-sdk' +import { config } from '../config' +import { __dynamoDBDefaults } from '../../../annotations' +import { ExecuteStep, executor } from '../../context' + +let sns = null; +const initAWSSDK = (context) => { + if (!sns) { + let awsConfig = { ...config.aws.SNS } + if (context.awsRegion) { + awsConfig.region = context.awsRegion + } + + if (context.deployTarget === 'local') { + awsConfig.endpoint = process.env.DYNAMODB_LOCAL_ENDPOINT || 'http://localhost:8000' + } + + sns = new SNS(awsConfig); + } + return sns +} + +export const getTopics = async (context) => { + let data = await getTopicPage(context) + const topics = [...data.Topics] + while (data.NextToken) { + data = await getTopicPage({ context, NextToken: data.NextToken }) + topics.push(...data.Topics) + } + return topics +} + + +export const getTopicPage = (context) => { + const { NextToken } = context + initAWSSDK(context) + return new Promise((resolve, reject) => { + let params = { + NextToken + }; + sns.listTopics(params, function (err, data) { + if (err) reject(err) + else resolve(data); + }); + }) +} diff --git a/src/cli/utilities/cli.ts b/src/cli/utilities/cli.ts index 38dee5c..d189a0e 100644 --- a/src/cli/utilities/cli.ts +++ b/src/cli/utilities/cli.ts @@ -6,4 +6,10 @@ export let resolvePath = (path) => { } return path; -} \ No newline at end of file +} + +export const requireValue = (value, msg) => { + if(typeof value !== 'undefined') return value + + throw new Error(`missing value '${msg}'`) +} diff --git a/src/cli/utilities/collectMetadata.ts b/src/cli/utilities/collectMetadata.ts new file mode 100644 index 0000000..e96aa37 --- /dev/null +++ b/src/cli/utilities/collectMetadata.ts @@ -0,0 +1,31 @@ +import { getMetadata, constants } from '../../annotations' +const { CLASS_ENVIRONMENTKEY } = constants + +export const collectMetadata = (context, config: { + metadataKey?: string, + selector?: (c) => string +}) => { + const result = new Map() + for (const serviceDefinition of context.publishedFunctions) { + if (config.metadataKey && config.selector) { + let partialConfigs = (getMetadata(config.metadataKey, serviceDefinition.service) || []) + for (const serviceConfig of partialConfigs) { + const hash = config.selector(serviceConfig) + + if (result.has(hash)) { + const item = result.get(hash) + item.services.push({ serviceDefinition, serviceConfig }) + continue + } + + result.set(hash, { + ...serviceConfig, + hash, + services: [{ serviceDefinition, serviceConfig }] + }) + } + } + } + + return Array.from(result.values()) +} diff --git a/src/cli/utilities/compress.ts b/src/cli/utilities/compress.ts index 0b01a21..cd30ca9 100644 --- a/src/cli/utilities/compress.ts +++ b/src/cli/utilities/compress.ts @@ -1,8 +1,10 @@ import * as nodeZip from 'node-zip' import { basename, join } from 'path' +import { createHash } from 'crypto' import { readFileSync, writeFileSync } from 'fs' +import { ExecuteStep } from '../context' -export const zip = (context) => { +export const zip = ExecuteStep.register('Compress-zip', (context) => { let compressor = new nodeZip() for (const file of context.files) { @@ -10,5 +12,12 @@ export const zip = (context) => { } let zipData = compressor.generate({ base64: false, compression: 'DEFLATE' }); + + const hash = createHash('sha256'); + hash.setEncoding('base64'); + hash.write(zipData); + hash.end(); + context.zipData = () => zipData -} \ No newline at end of file + context.zipCodeSha256 = hash.read() +}) \ No newline at end of file diff --git a/src/cli/utilities/deploy.ts b/src/cli/utilities/deploy.ts deleted file mode 100644 index 191c0b9..0000000 --- a/src/cli/utilities/deploy.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { createEnvironment } from '../providers' - -export const deploy = async (context) => { - await createEnvironment(context) -} \ No newline at end of file diff --git a/src/cli/utilities/local.ts b/src/cli/utilities/local.ts deleted file mode 100644 index e2112b3..0000000 --- a/src/cli/utilities/local.ts +++ /dev/null @@ -1,80 +0,0 @@ -import * as express from 'express' -import * as bodyParser from 'body-parser' -import * as cors from 'cors' -import { getMetadata, constants, getFunctionName } from '../../annotations' -import { config } from './config' - -const environmentConfigMiddleware = (serviceType) => { - return (req, res, next) => { - const environmentVariables = getMetadata(constants.CLASS_ENVIRONMENTKEY, serviceType) || {} - - let originals = {} - Object.keys(environmentVariables).forEach((key) => { - if (key in process.env) { - originals[key] = process.env[key] - } - - process.env[key] = environmentVariables[key] - }) - - - req.on("end", function () { - Object.keys(environmentVariables).forEach((key) => { - if (key in originals) { - process.env[key] = originals[key] - } else { - delete process.env[key] - } - }) - }); - - next() - } -} - -const logMiddleware = (enabled, serviceType) => { - return (req, res, next) => { - if (!enabled) return next() - - console.log(`${new Date().toISOString()} ${getFunctionName(serviceType)}`, JSON.stringify({ - url: req.url, - query: req.query, - params: req.params, - body: req.body, - headers: req.headers - }, null, 2)) - - next() - } -} - - -export const local = async (context) => { - let app = express() - app.use(bodyParser.json()) - - for (let serviceDefinition of context.publishedFunctions) { - let httpMetadata = getMetadata(constants.CLASS_APIGATEWAYKEY, serviceDefinition.service) - - for (let event of httpMetadata) { - const isLoggingEnabled = getMetadata(constants.CLASS_LOGKEY, serviceDefinition.service) - console.log(`${new Date().toISOString()} ${getFunctionName(serviceDefinition.service)} listening { path: '${event.path}', method: '${event.method}', cors: ${event.cors ? true : false} }`) - - if (event.cors) { - app.use(event.path, cors()) - } - - app[event.method]( - event.path, - logMiddleware(isLoggingEnabled, serviceDefinition.service), - environmentConfigMiddleware(serviceDefinition.service), - serviceDefinition.invoker - ) - } - } - - app.listen(context.localPort, function () { - process.env.FUNCTIONAL_LOCAL_PORT = context.localPort - console.log(`Example app listening on port ${process.env.FUNCTIONAL_LOCAL_PORT}!`) - }) -} diff --git a/src/cli/utilities/local/file.ts b/src/cli/utilities/local/file.ts new file mode 100644 index 0000000..ecd7bbd --- /dev/null +++ b/src/cli/utilities/local/file.ts @@ -0,0 +1,54 @@ +import { config } from '../config' + +import { writeFileSync, mkdirSync, existsSync } from 'fs' +import { ensureDirSync, copySync, removeSync } from 'fs-extra' +import { join, normalize, dirname } from 'path' + +export const writeFile = (fileName, binary, basePath?) => { + if (!basePath) { + basePath = config.tempDirectory + const mode = process.env.FUNCTIONAL_ENVIRONMENT + if (mode) { + basePath = join(basePath, mode) + } + } + + if (basePath) { + const filePath = join(basePath, fileName) + const dirPath = dirname(filePath) + ensureDirSync(dirPath) + writeFileSync(filePath, binary) + } +} + +export const copyFile = (from, to, basePath?) => { + if (!basePath) { + basePath = config.tempDirectory + const mode = process.env.FUNCTIONAL_ENVIRONMENT + if (mode) { + basePath = join(basePath, mode) + } + } + + if (basePath) { + const destinationFilePath = join(basePath, to) + const dirPath = dirname(destinationFilePath) + ensureDirSync(dirPath) + copySync(from, destinationFilePath) + } +} + +export const removePath = (path, basePath?) => { + if (!basePath) { + basePath = config.tempDirectory + const mode = process.env.FUNCTIONAL_ENVIRONMENT + if (mode) { + basePath = join(basePath, mode) + } + } + + if (basePath) { + const targetFilePath = join(basePath, path) + removeSync(targetFilePath) + } +} diff --git a/src/cli/utilities/logger.ts b/src/cli/utilities/logger.ts new file mode 100644 index 0000000..4ac379c --- /dev/null +++ b/src/cli/utilities/logger.ts @@ -0,0 +1,6 @@ +export const logger = { + warn(msg?, ...params) { console.log(msg, ...params) }, + error(msg?, ...params) { console.log(msg, ...params) }, + debug(msg?, ...params) { console.log(msg, ...params) }, + info(msg?, ...params) { console.log(msg, ...params) }, +} \ No newline at end of file diff --git a/src/cli/utilities/webpack.ts b/src/cli/utilities/webpack.ts deleted file mode 100644 index 21fcdad..0000000 --- a/src/cli/utilities/webpack.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { merge } from 'lodash' -import * as webpack from 'webpack' -import { config } from './config' -import { basename, extname, join } from 'path' - - -export const bundle = (context) => { - - return new Promise((resolve, reject) => { - const webpackConfig = createConfig(context) - - webpack(webpackConfig, function (err, stats) { - if (err) return reject() - - let jsonStats = stats.toJson(); - if (jsonStats.errors.length > 0) { - console.log('WEBPACK ERROR') - console.log(jsonStats.errors) - return reject(jsonStats.errors); - } - // if (jsonStats.warnings.length > 0) { - // console.log('WEBPACK WARNINGS') - // console.log(jsonStats.warnings) - // } - - context.originalFiles = context.files - context.files = [] - Object.keys(jsonStats.entrypoints).forEach((entryKey) => { - let entry = jsonStats.entrypoints[entryKey] - let assets = entry.assets.map((file) => join(webpackConfig.output.path, file)) - context.files.push(...assets) - }) - - resolve() - }); - }) -} - -export const createConfig = (context) => { - let entry = {} - context.files.forEach((file) => { - let name = basename(file) - const ext = extname(name) - const nameKey = name.substring(0, name.length - ext.length) - entry[nameKey] = file - }) - - const webpackConfig = merge({}, config.webpack, { - entry: entry, - externals: [ - { - 'aws-sdk': 'commonjs aws-sdk' - } - ] - }) - - return webpackConfig -} \ No newline at end of file diff --git a/src/helpers/ioc.ts b/src/helpers/ioc.ts new file mode 100644 index 0000000..b4d48e5 --- /dev/null +++ b/src/helpers/ioc.ts @@ -0,0 +1,63 @@ + +import { getOwnMetadata } from '../annotations/metadata' +import { CLASS_INJECTABLEKEY } from '../annotations/constants' +import { InjectionScope } from '../annotations/classes/injectable' + +export interface ClassFunction { + new(...args): T; +} + +export class IOC { + private instances: Map, any> + private classes: Map, ClassFunction> + public constructor() { + this.instances = new Map, any>() + this.classes = new Map, ClassFunction>() + } + + public registerType(from: ClassFunction, to: ClassFunction) { + this.classes.set(from, to) + } + + public clearType(type: ClassFunction) { + if (this.classes.has(type)) + this.classes.delete(type) + } + + public registerInstance(type: ClassFunction, instance) { + this.instances.set(type, instance) + } + public resolve(type: ClassFunction, ...params): T { + const resolveType = this.resolveType(type) + const scope = getOwnMetadata(CLASS_INJECTABLEKEY, resolveType) || InjectionScope.Transient + switch (scope) { + case InjectionScope.Singleton: + if (!this.instances.has(resolveType)) { + this.instances.set(resolveType, new resolveType(...params)) + } + + return this.instances.get(resolveType) + case InjectionScope.Transient: + default: + return new resolveType(...params) + } + } + public containsInstance(type: ClassFunction): boolean { + const resolveType = this.resolveType(type) + const scope = getOwnMetadata(CLASS_INJECTABLEKEY, resolveType) || InjectionScope.Transient + switch (scope) { + case InjectionScope.Singleton: + return this.instances.has(resolveType) + case InjectionScope.Transient: + default: + return false + } + } + + public resolveType(type: ClassFunction) { + return this.classes.has(type) ? this.resolveType(this.classes.get(type)) : type + } + +} + +export const container = new IOC() diff --git a/src/helpers/property.ts b/src/helpers/property.ts new file mode 100644 index 0000000..0648b96 --- /dev/null +++ b/src/helpers/property.ts @@ -0,0 +1,10 @@ +export const get = (obj, path) => { + if (typeof path === 'function') { + return path(obj) + } + if (typeof path === 'string') { + return path.split('.').reduce(function (prev, curr) { + return prev ? prev[curr] : undefined; + }, obj) + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 3020dec..e56fd2d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,34 @@ -export * from './classes' -import * as _annotations from './annotations' -export const annotations = _annotations + +/* Classes */ +export { FunctionalService, Api, Service, PreHook, PostHook, Resource } from './classes' + +/* Apis */ +export { DynamoTable, DocumentClientApi, SimpleNotificationService, SNSApi, S3Storage, S3Api, ApiGateway, CloudWatchEvent } from './classes' +export { MongoCollection, MongoConnection } from './plugins/mongo' + +/* Hooks */ +export { NoCallbackWaitsForEmptyEventLoop } from './plugins/mongo' + +/* Providers */ +export { Provider, AWSProvider, LocalProvider } from './providers' +export { addProvider, removeProvider } from './providers' + +/* Decorators */ +export { + injectable, apiGateway, httpTrigger, rest, httpGet, httpPost, httpPut, httpPatch, httpDelete, IHttpMethod, environment, + tag, log, functionName, dynamoTable, sns, s3Storage, eventSource, classConfig, use, description, role, group, aws, azure, + param, serviceParams, request, error, result, functionalServiceName, provider, stage, inject, cloudWatchEvent, dynamo, + cloudFormation +} from './annotations' +export { mongoCollection, mongoConnection } from './plugins/mongo' + +/* Enums */ +export { InjectionScope } from './annotations' + +/* Helpers */ +export { callExtension } from './classes' +export { + templates, applyTemplates, getFunctionName, __dynamoDBDefaults, getClassConfigValue, simpleClassAnnotation, expandableDecorator, + createParameterDecorator, constants, defineMetadata, getMetadata, getMetadataKeys, getOwnMetadata, getOverridableMetadata +} from './annotations' + export { container, IOC } from './helpers/ioc' \ No newline at end of file diff --git a/src/plugins/mongo.ts b/src/plugins/mongo.ts new file mode 100644 index 0000000..4e26b36 --- /dev/null +++ b/src/plugins/mongo.ts @@ -0,0 +1,538 @@ +import * as MongoDB from 'mongodb'; + +import { + getMetadata, + defineMetadata, + templates, + applyTemplates, + param, + environment, + injectable, + InjectionScope, + inject, + simpleClassAnnotation, + classConfig, + serviceParams, + provider +} from '../annotations'; + +import { Service, Api, PreHook } from '../classes'; + + +export const CLASS_MONGODB_TABLECONFIGURATIONKEY = 'functionly:class:mongoTableConfiguration'; +export const MONGO_TABLE_NAME_SUFFIX = '_TABLE_NAME'; +export const MONGO_CONNECTION_URL = 'MONGO_CONNECTION_URL'; + +export const __mongoDefaults = {}; +export const mongoConnection = (url: string) => environment(MONGO_CONNECTION_URL, url); +export const mongoCollection = (tableConfig: { collectionName: string; url?: string; environmentKey?: string }) => ( + target: Function +) => { + let tableDefinitions = getMetadata(CLASS_MONGODB_TABLECONFIGURATIONKEY, target) || []; + + tableConfig.environmentKey = tableConfig.environmentKey || `%ClassName%${MONGO_TABLE_NAME_SUFFIX}`; + const { templatedKey, templatedValue } = applyTemplates( + tableConfig.environmentKey, + tableConfig.collectionName, + target + ); + tableDefinitions.push({ + ...tableConfig, + environmentKey: templatedKey, + collectionName: templatedValue, + definedBy: target.name + }); + + defineMetadata(CLASS_MONGODB_TABLECONFIGURATIONKEY, [ ...tableDefinitions ], target); + + const environmentSetter = environment(templatedKey, templatedValue); + environmentSetter(target); +}; + +export class NoCallbackWaitsForEmptyEventLoop extends PreHook { + public async handle(@serviceParams p, @provider prov) { + if (prov === 'aws') { + p.context.callbackWaitsForEmptyEventLoop = false; + } + } +} + +@injectable(InjectionScope.Singleton) +export class MongoConnection extends Api { + private _connections: Map; + private _connectionPromises: Map; + + constructor() { + super(); + this._connections = new Map(); + this._connectionPromises = new Map(); + } + + public async connect(url): Promise { + const connectionUrl = url || process.env.MONGO_CONNECTION_URL || 'mongodb://localhost:27017/test'; + + if (this._connections.has(connectionUrl)) { + const connection = this._connections.get(connectionUrl); + if (!connection.serverConfig.isConnected()) { + this._connections.delete(connectionUrl); + if (this._connectionPromises.has(connectionUrl)) { + this._connectionPromises.delete(connectionUrl); + } + } else { + return connection; + } + } + + if (this._connectionPromises.has(connectionUrl)) { + return await this._connectionPromises.get(connectionUrl); + } + + const connectionPromise = MongoDB.MongoClient.connect(connectionUrl); + this._connectionPromises.set(connectionUrl, connectionPromise); + + try { + const connection = await connectionPromise; + this._connections.set(connectionUrl, connection); + + return connection; + } catch (e) { + this._connectionPromises.delete(connectionUrl); + throw e + } + } +} + +export class MongoCollection extends Api implements MongoDB.Collection { + /* MongoDB.Collection */ + get collectionName() { + return this._collection.collectionName; + } + get namespace() { + return this._collection.namespace; + } + get writeConcern() { + return this._collection.writeConcern; + } + get readConcern() { + return this._collection.readConcern; + } + get hint() { + return this._collection.hint; + } + aggregate(pipeline: Object[], callback: MongoDB.MongoCallback): MongoDB.AggregationCursor; + aggregate( + pipeline: Object[], + options?: MongoDB.CollectionAggregationOptions, + callback?: MongoDB.MongoCallback + ): MongoDB.AggregationCursor; + aggregate(pipeline: any, options?: any, callback?: any) { + return this._collection.aggregate(pipeline, options, callback) as any; + } + bulkWrite(operations: Object[], callback: MongoDB.MongoCallback): void; + bulkWrite( + operations: Object[], + options?: MongoDB.CollectionBluckWriteOptions + ): Promise; + bulkWrite( + operations: Object[], + options: MongoDB.CollectionBluckWriteOptions, + callback: MongoDB.MongoCallback + ): void; + bulkWrite(operations: any, options?: any, callback?: any) { + return this._collection.bulkWrite(operations, options, callback) as any; + } + count(query: Object, callback: MongoDB.MongoCallback): void; + count(query: Object, options?: MongoDB.MongoCountPreferences): Promise; + count(query: Object, options: MongoDB.MongoCountPreferences, callback: MongoDB.MongoCallback): void; + count(query: any, options?: any, callback?: any) { + return this._collection.count(query, options, callback) as any; + } + createIndex(fieldOrSpec: any, callback: MongoDB.MongoCallback): void; + createIndex(fieldOrSpec: any, options?: MongoDB.IndexOptions): Promise; + createIndex(fieldOrSpec: any, options: MongoDB.IndexOptions, callback: MongoDB.MongoCallback): void; + createIndex(fieldOrSpec: any, options?: any, callback?: any) { + return this._collection.createIndex(fieldOrSpec, options, callback) as any; + } + createIndexes(indexSpecs: Object[]): Promise; + createIndexes(indexSpecs: Object[], callback: MongoDB.MongoCallback): void; + createIndexes(indexSpecs: any, callback?: any) { + return this._collection.createIndexes(indexSpecs, callback) as any; + } + deleteMany(filter: Object, callback: MongoDB.MongoCallback): void; + deleteMany(filter: Object, options?: MongoDB.CollectionOptions): Promise; + deleteMany( + filter: Object, + options: MongoDB.CollectionOptions, + callback: MongoDB.MongoCallback + ): void; + deleteMany(filter: any, options?: any, callback?: any) { + return this._collection.deleteMany(filter, options, callback) as any; + } + deleteOne(filter: Object, callback: MongoDB.MongoCallback): void; + deleteOne( + filter: Object, + options?: { w?: string | number; wtimmeout?: number; j?: boolean; bypassDocumentValidation?: boolean } + ): Promise; + deleteOne( + filter: Object, + options: { w?: string | number; wtimmeout?: number; j?: boolean; bypassDocumentValidation?: boolean }, + callback: MongoDB.MongoCallback + ): void; + deleteOne(filter: any, options?: any, callback?: any) { + return this._collection.deleteOne(filter, options, callback) as any; + } + distinct(key: string, query: Object, callback: MongoDB.MongoCallback): void; + distinct(key: string, query: Object, options?: { readPreference?: string | MongoDB.ReadPreference }): Promise; + distinct( + key: string, + query: Object, + options: { readPreference?: string | MongoDB.ReadPreference }, + callback: MongoDB.MongoCallback + ): void; + distinct(key: any, query: any, options?: any, callback?: any) { + return this._collection.distinct(key, query, options, callback) as any; + } + drop(): Promise; + drop(callback: MongoDB.MongoCallback): void; + drop(callback?: any) { + return this._collection.drop(callback) as any; + } + dropIndex(indexName: string, callback: MongoDB.MongoCallback): void; + dropIndex(indexName: string, options?: MongoDB.CollectionOptions): Promise; + dropIndex(indexName: string, options: MongoDB.CollectionOptions, callback: MongoDB.MongoCallback): void; + dropIndex(indexName: any, options?: any, callback?: any) { + return this._collection.dropIndex(indexName, options, callback) as any; + } + dropIndexes(): Promise; + dropIndexes(callback?: MongoDB.MongoCallback): void; + dropIndexes(callback?: any) { + return this._collection.dropIndexes(callback) as any; + } + find(query?: Object): MongoDB.Cursor; + find(query: Object, fields?: Object, skip?: number, limit?: number, timeout?: number): MongoDB.Cursor; + find(query?: any, fields?: any, skip?: any, limit?: any, timeout?: any) { + return this._collection.find(query, fields, skip, limit, timeout); + } + findOne(filter: Object, callback: MongoDB.MongoCallback): void; + findOne(filter: Object, options?: MongoDB.FindOneOptions): Promise; + findOne(filter: Object, options: MongoDB.FindOneOptions, callback: MongoDB.MongoCallback): void; + findOne(filter: any, options?: any, callback?: any) { + return this._collection.findOne(filter, options, callback) as any; + } + findOneAndDelete(filter: Object, callback: MongoDB.MongoCallback>): void; + findOneAndDelete( + filter: Object, + options?: { projection?: Object; sort?: Object; maxTimeMS?: number } + ): Promise>; + findOneAndDelete( + filter: Object, + options: { projection?: Object; sort?: Object; maxTimeMS?: number }, + callback: MongoDB.MongoCallback> + ): void; + findOneAndDelete(filter: any, options?: any, callback?: any) { + return this._collection.findOneAndDelete(filter, options, callback) as any; + } + findOneAndReplace( + filter: Object, + replacement: Object, + callback: MongoDB.MongoCallback> + ): void; + findOneAndReplace( + filter: Object, + replacement: Object, + options?: MongoDB.FindOneAndReplaceOption + ): Promise>; + findOneAndReplace( + filter: Object, + replacement: Object, + options: MongoDB.FindOneAndReplaceOption, + callback: MongoDB.MongoCallback> + ): void; + findOneAndReplace(filter: any, replacement: any, options?: any, callback?: any) { + return this._collection.findOneAndReplace(filter, replacement, options, callback) as any; + } + findOneAndUpdate( + filter: Object, + update: Object, + callback: MongoDB.MongoCallback> + ): void; + findOneAndUpdate( + filter: Object, + update: Object, + options?: MongoDB.FindOneAndReplaceOption + ): Promise>; + findOneAndUpdate( + filter: Object, + update: Object, + options: MongoDB.FindOneAndReplaceOption, + callback: MongoDB.MongoCallback> + ): void; + findOneAndUpdate(filter: any, update: any, options?: any, callback?: any) { + return this._collection.findOneAndUpdate(filter, update, options, callback) as any; + } + geoHaystackSearch(x: number, y: number, callback: MongoDB.MongoCallback): void; + geoHaystackSearch(x: number, y: number, options?: MongoDB.GeoHaystackSearchOptions): Promise; + geoHaystackSearch( + x: number, + y: number, + options: MongoDB.GeoHaystackSearchOptions, + callback: MongoDB.MongoCallback + ): void; + geoHaystackSearch(x: any, y: any, options?: any, callback?: any) { + return this._collection.geoHaystackSearch(x, y, options, callback) as any; + } + geoNear(x: number, y: number, callback: MongoDB.MongoCallback): void; + geoNear(x: number, y: number, options?: MongoDB.GeoNearOptions): Promise; + geoNear(x: number, y: number, options: MongoDB.GeoNearOptions, callback: MongoDB.MongoCallback): void; + geoNear(x: any, y: any, options?: any, callback?: any) { + return this._collection.geoNear(x, y, options, callback) as any; + } + group( + keys: Object | Function | any[] | MongoDB.Code, + condition: Object, + initial: Object, + reduce: Function | MongoDB.Code, + finalize: Function | MongoDB.Code, + command: boolean, + callback: MongoDB.MongoCallback + ): void; + group( + keys: Object | Function | any[] | MongoDB.Code, + condition: Object, + initial: Object, + reduce: Function | MongoDB.Code, + finalize: Function | MongoDB.Code, + command: boolean, + options?: { readPreference?: string | MongoDB.ReadPreference } + ): Promise; + group( + keys: Object | Function | any[] | MongoDB.Code, + condition: Object, + initial: Object, + reduce: Function | MongoDB.Code, + finalize: Function | MongoDB.Code, + command: boolean, + options: { readPreference?: string | MongoDB.ReadPreference }, + callback: MongoDB.MongoCallback + ): void; + group( + keys: any, + condition: any, + initial: any, + reduce: any, + finalize: any, + command: any, + options?: any, + callback?: any + ) { + return this._collection.group(keys, condition, initial, reduce, finalize, command, options, callback) as any; + } + indexes(): Promise; + indexes(callback: MongoDB.MongoCallback): void; + indexes(callback?: any) { + return this._collection.indexes(callback) as any; + } + indexExists(indexes: string | string[]): Promise; + indexExists(indexes: string | string[], callback: MongoDB.MongoCallback): void; + indexExists(indexes: any, callback?: any) { + return this._collection.indexExists(callback) as any; + } + indexInformation(callback: MongoDB.MongoCallback): void; + indexInformation(options?: { full: boolean }): Promise; + indexInformation(options: { full: boolean }, callback: MongoDB.MongoCallback): void; + indexInformation(options?: any, callback?: any) { + return this._collection.indexInformation(options, callback) as any; + } + initializeOrderedBulkOp(options?: MongoDB.CollectionOptions): MongoDB.OrderedBulkOperation { + return this._collection.initializeOrderedBulkOp(options); + } + initializeUnorderedBulkOp(options?: MongoDB.CollectionOptions): MongoDB.UnorderedBulkOperation { + return this._collection.initializeUnorderedBulkOp(options); + } + insert(docs: Object, callback: MongoDB.MongoCallback): void; + insert(docs: Object, options?: MongoDB.CollectionInsertOneOptions): Promise; + insert( + docs: Object, + options: MongoDB.CollectionInsertOneOptions, + callback: MongoDB.MongoCallback + ): void; + insert(docs: any, options?: any, callback?: any) { + return this._collection.insert(docs, options, callback) as any; + } + insertMany(docs: Object[], callback: MongoDB.MongoCallback): void; + insertMany(docs: Object[], options?: MongoDB.CollectionInsertManyOptions): Promise; + insertMany( + docs: Object[], + options: MongoDB.CollectionInsertManyOptions, + callback: MongoDB.MongoCallback + ): void; + insertMany(docs: any, options?: any, callback?: any) { + return this._collection.insertMany(docs, options, callback) as any; + } + insertOne(docs: Object, callback: MongoDB.MongoCallback): void; + insertOne(docs: Object, options?: MongoDB.CollectionInsertOneOptions): Promise; + insertOne( + docs: Object, + options: MongoDB.CollectionInsertOneOptions, + callback: MongoDB.MongoCallback + ): void; + insertOne(docs: any, options?: any, callback?: any) { + return this._collection.insertOne(docs, options, callback) as any; + } + isCapped(): Promise; + isCapped(callback: MongoDB.MongoCallback): void; + isCapped(callback?: any) { + return this._collection.isCapped(callback) as any; + } + listIndexes(options?: { + batchSize?: number; + readPreference?: string | MongoDB.ReadPreference; + }): MongoDB.CommandCursor { + return this._collection.listIndexes(options); + } + mapReduce(map: string | Function, reduce: string | Function, callback: MongoDB.MongoCallback): void; + mapReduce(map: string | Function, reduce: string | Function, options?: MongoDB.MapReduceOptions): Promise; + mapReduce( + map: string | Function, + reduce: string | Function, + options: MongoDB.MapReduceOptions, + callback: MongoDB.MongoCallback + ): void; + mapReduce(map: any, reduce: any, options?: any, callback?: any) { + return this._collection.mapReduce(map, reduce, options, callback) as any; + } + options(): Promise; + options(callback: MongoDB.MongoCallback): void; + options(callback?: any) { + return this._collection.options(callback) as any; + } + parallelCollectionScan(callback: MongoDB.MongoCallback[]>): void; + parallelCollectionScan(options?: MongoDB.ParallelCollectionScanOptions): Promise[]>; + parallelCollectionScan( + options: MongoDB.ParallelCollectionScanOptions, + callback: MongoDB.MongoCallback[]> + ): void; + parallelCollectionScan(options?: any, callback?: any) { + return this._collection.parallelCollectionScan(options, callback) as any; + } + reIndex(): Promise; + reIndex(callback: MongoDB.MongoCallback): void; + reIndex(callback?: any) { + return this._collection.reIndex(callback) as any; + } + remove(selector: Object, callback: MongoDB.MongoCallback): void; + remove(selector: Object, options?: MongoDB.CollectionOptions & { single?: boolean }): Promise; + remove( + selector: Object, + options?: MongoDB.CollectionOptions & { single?: boolean }, + callback?: MongoDB.MongoCallback + ): void; + remove(selector: any, options?: any, callback?: any) { + return this._collection.remove(selector, options, callback) as any; + } + rename(newName: string, callback: MongoDB.MongoCallback>): void; + rename(newName: string, options?: { dropTarget?: boolean }): Promise>; + rename( + newName: string, + options: { dropTarget?: boolean }, + callback: MongoDB.MongoCallback> + ): void; + rename(newName: any, options?: any, callback?: any) { + return this._collection.rename(newName, options, callback) as any; + } + replaceOne( + filter: Object, + doc: Object, + callback: MongoDB.MongoCallback + ): void; + replaceOne( + filter: Object, + doc: Object, + options?: MongoDB.ReplaceOneOptions + ): Promise; + replaceOne( + filter: Object, + doc: Object, + options: MongoDB.ReplaceOneOptions, + callback: MongoDB.MongoCallback + ): void; + replaceOne(filter: any, doc: any, options?: any, callback?: any) { + return this._collection.replaceOne(filter, doc, options, callback) as any; + } + save(doc: Object, callback: MongoDB.MongoCallback): void; + save(doc: Object, options?: MongoDB.CollectionOptions): Promise; + save(doc: Object, options: MongoDB.CollectionOptions, callback: MongoDB.MongoCallback): void; + save(doc: any, options?: any, callback?: any) { + return this._collection.save(doc, options, callback) as any; + } + stats(callback: MongoDB.MongoCallback): void; + stats(options?: { scale: number }): Promise; + stats(options: { scale: number }, callback: MongoDB.MongoCallback): void; + stats(options?: any, callback?: any) { + return this._collection.stats(options, callback) as any; + } + update(filter: Object, update: Object, callback: MongoDB.MongoCallback): void; + update( + filter: Object, + update: Object, + options?: MongoDB.ReplaceOneOptions & { multi?: boolean } + ): Promise; + update( + filter: Object, + update: Object, + options: MongoDB.ReplaceOneOptions & { multi?: boolean }, + callback: MongoDB.MongoCallback + ): void; + update(filter: any, update: any, options?: any, callback?: any) { + return this._collection.update(filter, update, options, callback) as any; + } + updateMany(filter: Object, update: Object, callback: MongoDB.MongoCallback): void; + updateMany( + filter: Object, + update: Object, + options?: { upsert?: boolean; w?: any; wtimeout?: number; j?: boolean } + ): Promise; + updateMany( + filter: Object, + update: Object, + options: { upsert?: boolean; w?: any; wtimeout?: number; j?: boolean }, + callback: MongoDB.MongoCallback + ): void; + updateMany(filter: any, update: any, options?: any, callback?: any) { + return this._collection.updateMany(filter, update, options, callback) as any; + } + updateOne(filter: Object, update: Object, callback: MongoDB.MongoCallback): void; + updateOne(filter: Object, update: Object, options?: MongoDB.ReplaceOneOptions): Promise; + updateOne( + filter: Object, + update: Object, + options: MongoDB.ReplaceOneOptions, + callback: MongoDB.MongoCallback + ): void; + updateOne(filter: any, update: any, options?: any, callback?: any) { + return this._collection.updateMany(filter, update, options, callback) as any; + } + + /* Api */ + private _collection: MongoDB.Collection; + private connection: MongoConnection; + constructor(@inject(MongoConnection) connection: MongoConnection) { + super(); + this.connection = connection; + } + + public async init() { + const tableConfig = (getMetadata(CLASS_MONGODB_TABLECONFIGURATIONKEY, this) || [])[0]; + + const tableName = + (process.env[`${this.constructor.name}${MONGO_TABLE_NAME_SUFFIX}`] || tableConfig.collectionName) + + `-${process.env.FUNCTIONAL_STAGE}`; + + const db = await this.connection.connect(tableConfig.url); + this._collection = db.collection(tableName); + } + + public getCollection() { + return this._collection; + } +} diff --git a/src/providers/aws.ts b/src/providers/aws.ts deleted file mode 100644 index c09a5e4..0000000 --- a/src/providers/aws.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Lambda } from 'aws-sdk' -import { constants, getOwnMetadata, getMetadata, getFunctionName } from '../annotations' - -let lambda = new Lambda(); - -export const getInvoker = (serviceType, params) => { - const serviceInstance = new serviceType(...params) - const parameterMapping = (getOwnMetadata(constants.PARAMETER_PARAMKEY, serviceType, 'handle') || []) - .filter(t => t && typeof t.parameterIndex === 'number'); - - const invoker = async (event, context, cb) => { - try { - const params = [] - parameterMapping.forEach((target) => { - params[target.parameterIndex] = parameterResolver(event, context, target) - }) - - const r = await serviceInstance.handle.apply(serviceInstance, params) - cb(null, r) - return r - } catch (e) { - cb(e) - } - } - return invoker -} - -const parameterResolver = (event, context, target) => { - switch (target.type) { - case 'inject': - let serviceType = target.serviceType - return new serviceType(...target.params.map((p) => typeof p === 'function' ? p() : p)) - default: - return event[target.from] - } -} - -export const invoke = async (serviceInstance, params?, invokeConfig?) => { - return new Promise((resolve, reject) => { - let lambdaParams = {} - - let parameterMapping = getOwnMetadata(constants.PARAMETER_PARAMKEY, serviceInstance.constructor, 'handle') || []; - parameterMapping.forEach((target) => { - if (params && target && target.type === 'param') { - lambdaParams[target.from] = params[target.from] - } - }) - - let invokeParams = { - FunctionName: getFunctionName(serviceInstance), - Payload: JSON.stringify(lambdaParams) - }; - - lambda.invoke(invokeParams, function (err, data) { - if (err) reject(err) - else resolve(JSON.parse(data.Payload.toString())); - }); - }) -} \ No newline at end of file diff --git a/src/providers/aws/eventSources/apiGateway.ts b/src/providers/aws/eventSources/apiGateway.ts new file mode 100644 index 0000000..6c92c57 --- /dev/null +++ b/src/providers/aws/eventSources/apiGateway.ts @@ -0,0 +1,85 @@ +import { EventSource } from '../../core/eventSource' +import { get } from '../../../helpers/property' +import { CLASS_APIGATEWAYKEY } from '../../../annotations/constants' +import { getMetadata, defineMetadata } from '../../../annotations/metadata' + +export class ApiGateway extends EventSource { + public available(eventContext: any): boolean { + const { event } = eventContext + return event && event.requestContext && event.requestContext.apiId ? true : false + } + + public async parameterResolver(parameter, context) { + const body = context.event.event.body + const query = context.event.event.queryStringParameters + const params = context.event.event.pathParameters + const headers = context.event.event.headers + + switch (parameter.type) { + case 'param': + const source = parameter.source; + if (typeof source !== 'undefined') { + const holder = !source ? context : get(context, source) + if (holder) { + return get(holder, parameter.from) + } + } else { + let value + if (typeof body === 'string') { + try { + const parsedBody = JSON.parse(body) + if (typeof (value = get(parsedBody, parameter.from)) !== 'undefined') return value + // fallback to other options + } catch(e) { + // fallback to other options + } + } else if (typeof (value = get(body, parameter.from)) !== 'undefined') return value + if (typeof (value = get(query, parameter.from)) !== 'undefined') return value + if (typeof (value = get(params, parameter.from)) !== 'undefined') return value + if (typeof (value = get(headers, parameter.from)) !== 'undefined') return value + return value; + } + return undefined + default: + return await super.parameterResolver(parameter, context) + } + } + + public async resultTransform(err, result, event, serviceType) { + let headers = {} + const metadata = getMetadata(CLASS_APIGATEWAYKEY, serviceType) || [] + if (serviceType && metadata.find(m => m.cors === true)) { + headers = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': 'Content-Type,Authorization,X-Requested-With', + 'Access-Control-Allow-Credentials': 'true' + } + } + + if (err) { + return { + statusCode: 500, + headers, + body: JSON.stringify(err) + } + } + + if (result && typeof result.status === 'number' && result.hasOwnProperty('data')) { + return { + statusCode: result.status, + headers: { ...headers, ...result.headers }, + body: typeof result.data === 'string' ? result.data : JSON.stringify(result.data) + } + } + + if (result && typeof result.statusCode === 'number' && typeof result.body === 'string') { + return result + } + + return { + statusCode: 200, + headers, + body: JSON.stringify(result) + } + } +} \ No newline at end of file diff --git a/src/providers/aws/eventSources/dynamoTable.ts b/src/providers/aws/eventSources/dynamoTable.ts new file mode 100644 index 0000000..802d240 --- /dev/null +++ b/src/providers/aws/eventSources/dynamoTable.ts @@ -0,0 +1,30 @@ +import { EventSource } from '../../core/eventSource' +import { get } from '../../../helpers/property' + +export class DynamoTable extends EventSource { + public available(eventContext: any): boolean { + const { event } = eventContext + return event && Array.isArray(event.Records) && event.Records.length && event.Records[0].eventSource === "aws:dynamodb" ? true : false + } + + public async parameterResolver(parameter, context) { + + switch (parameter.type) { + case 'param': + const source = parameter.source; + if (typeof source !== 'undefined') { + const holder = !source ? context : get(context, source) + if (holder) { + return get(holder, parameter.from) + } + } else { + let value + if (typeof (value = get(context.event.event.Records[0], parameter.from)) !== 'undefined') return value + return value; + } + return undefined + default: + return await super.parameterResolver(parameter, context) + } + } +} \ No newline at end of file diff --git a/src/providers/aws/eventSources/lambdaCall.ts b/src/providers/aws/eventSources/lambdaCall.ts new file mode 100644 index 0000000..3835e44 --- /dev/null +++ b/src/providers/aws/eventSources/lambdaCall.ts @@ -0,0 +1,24 @@ +import { EventSource } from '../../core/eventSource' +import { get } from '../../../helpers/property' + +export class LambdaCall extends EventSource { + public async parameterResolver(parameter, context) { + switch (parameter.type) { + case 'param': + const source = parameter.source; + if (typeof source !== 'undefined') { + const holder = !source ? context : get(context, source) + if (holder) { + return get(holder, parameter.from) + } + } else { + let value + if (typeof (value = get(context.event.event, parameter.from)) !== 'undefined') return value + return value; + } + return undefined + default: + return await super.parameterResolver(parameter, context) + } + } +} \ No newline at end of file diff --git a/src/providers/aws/eventSources/s3.ts b/src/providers/aws/eventSources/s3.ts new file mode 100644 index 0000000..acadd11 --- /dev/null +++ b/src/providers/aws/eventSources/s3.ts @@ -0,0 +1,29 @@ +import { EventSource } from '../../core/eventSource' +import { get } from '../../../helpers/property' + +export class S3 extends EventSource { + public available(eventContext: any): boolean { + const { event } = eventContext + return event && Array.isArray(event.Records) && event.Records.length && event.Records[0].eventSource === "aws:s3" ? true : false + } + + public async parameterResolver(parameter, context) { + switch (parameter.type) { + case 'param': + const source = parameter.source; + if (typeof source !== 'undefined') { + const holder = !source ? context : get(context, source) + if (holder) { + return get(holder, parameter.from) + } + } else { + let value + if (typeof (value = get(context.event.event.Records[0], parameter.from)) !== 'undefined') return value + return value; + } + return undefined + default: + return await super.parameterResolver(parameter, context) + } + } +} \ No newline at end of file diff --git a/src/providers/aws/eventSources/sns.ts b/src/providers/aws/eventSources/sns.ts new file mode 100644 index 0000000..4a48bf2 --- /dev/null +++ b/src/providers/aws/eventSources/sns.ts @@ -0,0 +1,29 @@ +import { EventSource } from '../../core/eventSource' +import { get } from '../../../helpers/property' + +export class SNS extends EventSource { + public available(eventContext: any): boolean { + const { event } = eventContext + return event && Array.isArray(event.Records) && event.Records.length && event.Records[0].EventSource === "aws:sns" ? true : false + } + + public async parameterResolver(parameter, context) { + switch (parameter.type) { + case 'param': + const source = parameter.source; + if (typeof source !== 'undefined') { + const holder = !source ? context : get(context, source) + if (holder) { + return get(holder, parameter.from) + } + } else { + let value + if (typeof (value = get(context.event.event.Records[0], parameter.from)) !== 'undefined') return value + return value; + } + return undefined + default: + return await super.parameterResolver(parameter, context) + } + } +} \ No newline at end of file diff --git a/src/providers/aws/index.ts b/src/providers/aws/index.ts new file mode 100644 index 0000000..3f6cf30 --- /dev/null +++ b/src/providers/aws/index.ts @@ -0,0 +1,103 @@ +import { Lambda } from 'aws-sdk' +import { Provider } from '../core/provider' +import { getFunctionName } from '../../annotations' +import { ApiGateway } from './eventSources/apiGateway' +import { LambdaCall } from './eventSources/lambdaCall' +import { SNS } from './eventSources/sns' +import { S3 } from './eventSources/s3' +import { DynamoTable } from './eventSources/dynamoTable' +import { parse } from 'url' +import { container } from '../../helpers/ioc' + +let lambda = null; +const initAWSSDK = () => { + if (!lambda) { + lambda = new Lambda(); + } + return lambda +} + + +const eventSourceHandlers = [ + container.resolve(ApiGateway), + container.resolve(SNS), + container.resolve(S3), + container.resolve(DynamoTable), + container.resolve(LambdaCall) +] + + +export class AWSProvider extends Provider { + public getInvoker(serviceType, params, initContext): Function { + const callContext = this.createCallContext(serviceType, 'handle', initContext) + + const invoker = async (event, context, cb) => { + try { + const eventContext = { event, context, cb } + + const eventSourceHandler = eventSourceHandlers.find(h => h.available(eventContext)) + + let result + let error + try { + result = await callContext({ eventSourceHandler, event: eventContext, serviceType }) + } catch (err) { + error = err + } + const response = await eventSourceHandler.resultTransform(error, result, eventContext, serviceType) + + cb(null, response) + return response + } catch (e) { + cb(e) + } + } + return invoker + } + + public async invoke(serviceType, params, invokeConfig?) { + initAWSSDK() + + const funcName = getFunctionName(serviceType) + const resolvedFuncName = process.env[`FUNCTIONAL_SERVICE_${funcName.toUpperCase()}`] || funcName + + const invokeParams = { + FunctionName: `${process.env.FUNCTIONAL_PROJECTNAME}-${resolvedFuncName}`, + Payload: JSON.stringify(params) + }; + + return await this.invokeExec(invokeParams) + } + + public async invokeExec(config: any): Promise { + return new Promise((resolve, reject) => { + lambda.invoke(config, function (err, data) { + if (err) reject(err) + else resolve(JSON.parse(data.Payload.toString())); + }); + }) + } +} + +AWSProvider.addParameterDecoratorImplementation("param", async (parameter, context, provider) => { + return await context.eventSourceHandler.parameterResolver(parameter, context) +}) +AWSProvider.addParameterDecoratorImplementation("request", async (parameter, context, provider) => { + if (context.eventSourceHandler.constructor.name === 'ApiGateway') { + let body = context.event.event.body + try { + body = JSON.parse(body) + } catch (e) { } + + return { + url: parse(context.event.event.path), + method: context.event.event.httpMethod, + body, + query: context.event.event.queryStringParameters, + params: context.event.event.pathParameters, + headers: context.event.event.headers + } + } +}) + +export const provider = container.resolve(AWSProvider) diff --git a/src/providers/azure/eventSources/httpTrigger.ts b/src/providers/azure/eventSources/httpTrigger.ts new file mode 100644 index 0000000..5a299ac --- /dev/null +++ b/src/providers/azure/eventSources/httpTrigger.ts @@ -0,0 +1,58 @@ +import { EventSource } from '../../core/eventSource' +import { get } from '../../../helpers/property' + +export class HttpTrigger extends EventSource { + public async parameterResolver(parameter, context) { + const body = context.event.req.body + const query = context.event.req.query + const params = context.event.req.params + const headers = context.event.req.headers + + switch (parameter.type) { + case 'param': + const source = parameter.source; + if (typeof source !== 'undefined') { + const holder = !source ? context : get(context, source) + if (holder) { + return get(holder, parameter.from) + } + } else { + let value + if (typeof (value = get(body, parameter.from)) !== 'undefined') return value + if (typeof (value = get(query, parameter.from)) !== 'undefined') return value + if (typeof (value = get(params, parameter.from)) !== 'undefined') return value + if (typeof (value = get(headers, parameter.from)) !== 'undefined') return value + return value; + } + return undefined + default: + return await super.parameterResolver(parameter, context) + } + } + + public async resultTransform(error, result, eventContext, serviceType) { + if (error) { + return { + status: 500, + body: `${error.message} - ${error.stack}` + } + } + + if (result && typeof result.status === 'number' && result.hasOwnProperty('data')) { + return { + status: result.status, + headers: result.headers, + body: result.data + } + } + + if (result && typeof result.status === 'number' && result.hasOwnProperty && result.hasOwnProperty('body')) { + return result + } + + return { + status: 200, + body: result + } + } +} \ No newline at end of file diff --git a/src/providers/azure/index.ts b/src/providers/azure/index.ts new file mode 100644 index 0000000..ca4381a --- /dev/null +++ b/src/providers/azure/index.ts @@ -0,0 +1,119 @@ +import { Provider } from '../core/provider' +import { getFunctionName, constants, getMetadata } from '../../annotations' +const { CLASS_HTTPTRIGGER } = constants +import { HttpTrigger } from './eventSources/httpTrigger' +import { container } from '../../helpers/ioc' +import * as request from 'request' +import { parse } from 'url' + +const eventSourceHandlers = [ + container.resolve(HttpTrigger) +] + +export const FUNCTIONLY_FUNCTION_KEY = 'FUNCTIONLY_FUNCTION_KEY' + + +export class AzureProvider extends Provider { + public getInvoker(serviceType, params, initContext): Function { + const callContext = this.createCallContext(serviceType, 'handle', initContext) + + const invoker = async (context, req) => { + try { + const eventContext = { context, req } + + const eventSourceHandler = eventSourceHandlers.find(h => h.available(eventContext)) + + let result + let error + try { + result = await callContext({ eventSourceHandler, event: eventContext, serviceType }) + } catch (err) { + error = err + } + const response = await eventSourceHandler.resultTransform(error, result, eventContext, serviceType) + + context.res = response + return response + } catch (e) { + context.res = { + status: 500, + body: `${e.message} - ${e.stack}` + } + } + } + return invoker + } + + public async invoke(serviceType, params, invokeConfig?) { + + const httpAttr = (getMetadata(CLASS_HTTPTRIGGER, serviceType) || [])[0] + if (!httpAttr) { + throw new Error('missing http configuration') + } + + const method = httpAttr.methods[0] || 'GET' + const invokeParams: any = { + method, + url: `${process.env.FUNCION_APP_BASEURL}${httpAttr.route}`, + }; + + if (method.toLowerCase() === 'get') { + invokeParams.qs = params + } else { + invokeParams.body = params + invokeParams.json = true + } + + if (httpAttr.authLevel !== 'anonymous') { + if (!process.env.FUNCTIONLY_FUNCTION_KEY) { + throw new Error(`process.env.FUNCTIONLY_FUNCTION_KEY is not set, create host key to all functions`) + } + invokeParams.qs = { ...(invokeParams.qs || {}), code: process.env.FUNCTIONLY_FUNCTION_KEY } + } + + return await this.invokeExec(invokeParams) + } + + public async invokeExec(config: any): Promise { + return new Promise((resolve, reject) => { + try { + request(config, (error, response, body) => { + + if (error) return reject(error) + + let result = body + try { + result = JSON.parse(result) + } + catch (e) { } + + return resolve(result) + + }) + + } catch (e) { + return reject(e); + } + }) + } +} + +AzureProvider.addParameterDecoratorImplementation("param", async (parameter, context, provider) => { + return await context.eventSourceHandler.parameterResolver(parameter, context) +}) + + +AzureProvider.addParameterDecoratorImplementation("request", async (parameter, context, provider) => { + if (context.eventSourceHandler.constructor.name === 'HttpTrigger') { + return { + url: parse(context.event.req.originalUrl), + method: context.event.req.method, + body: context.event.req.body, + query: context.event.req.query, + params: context.event.req.params, + headers: context.event.req.headers + } + } +}) + +export const provider = container.resolve(AzureProvider) diff --git a/src/providers/core/eventSource.ts b/src/providers/core/eventSource.ts new file mode 100644 index 0000000..251681f --- /dev/null +++ b/src/providers/core/eventSource.ts @@ -0,0 +1,21 @@ +import { get } from '../../helpers/property' + +export abstract class EventSource { + public available(event: any) { + return true + } + + public async parameterResolver(parameter, context: any) { + return undefined + } + + public async resultTransform(err, result, event: any, serviceType) { + if (err) throw err + + if (result && typeof result.status === 'number' && result.hasOwnProperty('data')) { + return result.data + } + + return result + } +} \ No newline at end of file diff --git a/src/providers/core/provider.ts b/src/providers/core/provider.ts new file mode 100644 index 0000000..0609e22 --- /dev/null +++ b/src/providers/core/provider.ts @@ -0,0 +1,161 @@ +import { constants, getMetadata, getOverridableMetadata, getFunctionName, getOwnMetadata } from '../../annotations' +const { PARAMETER_PARAMKEY, CLASS_INJECTABLEKEY } = constants +import { getMiddlewares } from '../../annotations/classes/use' +import { callExtension } from '../../classes/core/callExtension' +import { PreHook } from '../../classes/middleware/preHook' +import { PostHook } from '../../classes/middleware/postHook' +import { container } from '../../helpers/ioc' + +export abstract class Provider { + public getInvoker(serviceType, params, initContext): Function { + const invoker = () => { } + return invoker + } + + public async invoke(serviceType, params, invokeConfig?): Promise { + + } + + public async invokeExec(config: any): Promise { + + } + + public async createInstance(type, context) { + if (container.containsInstance(type)) { + return container.resolve(type) + } + + const parameters = this.getParameters(type, undefined) + + const params = [] + for (const parameter of parameters) { + params[parameter.parameterIndex] = await this.parameterResolver(parameter, context) + } + + const instance = container.resolve(type, ...params) + + if (typeof instance.init === 'function') { + await instance.init() + } + + return instance + } + + protected async parameterResolver(parameter, context): Promise { + const implementation = this.getParameterDecoratorImplementation(parameter.type) || (() => { }) + return implementation(parameter, context, this) + } + + protected createCallContext(target, method, initContext) { + const hooks = getMiddlewares(target) + const parameters = this.getParameters(target, method) + + const preHooks = hooks.filter(h => h.prototype instanceof PreHook) + .map(h => ({ hookKey: h.name, hook: this.createCallContext(h, 'handle', initContext) })) + const postHookInstances = hooks.filter(h => h.prototype instanceof PostHook) + const postHooks = postHookInstances.map(h => ({ hookKey: h.name, hook: this.createCallContext(h, 'handle', initContext) })) + const catchHooks = postHookInstances.map(h => ({ hookKey: h.name, hook: this.createCallContext(h, 'catch', initContext) })) + + return async (context) => { + const preic: any = { ...(initContext || {}) } + const preContext = { context: preic, ...context } + + try { + for (const { hookKey, hook } of preHooks) { + const result = await hook(preContext) + if (hookKey) { + preic[hookKey] = result + } + } + + const params = [] + for (const parameter of parameters) { + params[parameter.parameterIndex] = await this.parameterResolver(parameter, preContext) + } + + preic.result = await target[method](...params) + preic.error = undefined + } catch (e) { + preic.error = e + preic.result = undefined + } + + const ic = { ...preic } + const postContext = { ...preContext, context: ic } + for (let hookIndex = 0; hookIndex < postHookInstances.length; hookIndex++) { + try { + if (ic.error) { + ic.result = await catchHooks[hookIndex].hook(postContext) + } else { + ic.result = await postHooks[hookIndex].hook(postContext) + } + ic.error = undefined + } catch (e) { + ic.error = e + } + } + + if (ic.error) throw ic.error + return ic.result + } + } + + protected getParameters(target, method) { + return (getOverridableMetadata(PARAMETER_PARAMKEY, target, method) || []) + .filter(t => t && typeof t.parameterIndex === 'number'); + } + + public static __supportedDecorators: { [key: string]: Function } + public static __getDecoratorHolder() { + return this.__supportedDecorators = this.hasOwnProperty('__supportedDecorators') ? this.__supportedDecorators : {} + } + public static addParameterDecoratorImplementation(parameterType: string, implementation: Function) { + this.__getDecoratorHolder()[parameterType] = implementation + } + + public static getParameterDecoratorImplementation(parameterType: string) { + return this.__getDecoratorHolder()[parameterType] || + (this['__proto__'].getParameterDecoratorImplementation && this['__proto__'].getParameterDecoratorImplementation(parameterType)) + } + protected getParameterDecoratorImplementation(parameterType: string) { + return this.constructor['getParameterDecoratorImplementation'](parameterType) + } +} + +Provider.addParameterDecoratorImplementation("inject", async (parameter, context, provider) => { + const serviceType = container.resolveType(parameter.serviceType) + + if (!getOwnMetadata(CLASS_INJECTABLEKEY, serviceType)) { + throw new Error(`type '${getFunctionName(serviceType)}' not marked as injectable`) + } + + const staticInjectValue = await callExtension(serviceType, 'onInject', { parameter, context, provider }) + if (typeof staticInjectValue !== 'undefined') { + return staticInjectValue + } + + const instance = await provider.createInstance(serviceType, context) + + await callExtension(instance, 'onInject', { parameter, context, provider }) + return instance +}) + +Provider.addParameterDecoratorImplementation("serviceParams", async (parameter, context, provider) => { + return context.event +}) + +Provider.addParameterDecoratorImplementation("error", async (parameter, context, provider) => { + return parameter.targetKey === 'catch' ? (context.context && context.context.error) : undefined +}) +Provider.addParameterDecoratorImplementation("result", async (parameter, context, provider) => { + return context.context && context.context.result +}) +Provider.addParameterDecoratorImplementation("functionalServiceName", async (parameter, context, provider) => { + return context.serviceType && getFunctionName(context.serviceType) +}) +Provider.addParameterDecoratorImplementation("provider", async (parameter, context, provider) => { + return process.env.FUNCTIONAL_ENVIRONMENT +}) +Provider.addParameterDecoratorImplementation("stage", async (parameter, context, provider) => { + return process.env.FUNCTIONAL_STAGE +}) \ No newline at end of file diff --git a/src/providers/deploy.ts b/src/providers/deploy.ts deleted file mode 100644 index 2b4f658..0000000 --- a/src/providers/deploy.ts +++ /dev/null @@ -1,9 +0,0 @@ - -export const getInvoker = (serviceType, params) => { - let invoker = () => {} - return invoker -} - -export const invoke = async (serviceInstance, params?, invokeConfig?) => { - -} \ No newline at end of file diff --git a/src/providers/inProc.ts b/src/providers/inProc.ts new file mode 100644 index 0000000..2e03603 --- /dev/null +++ b/src/providers/inProc.ts @@ -0,0 +1,59 @@ +import * as request from 'request' +import { Provider } from './core/provider' +import { constants, getMetadata, getFunctionName, rest } from '../annotations' +const { CLASS_LOGKEY } = constants +import { get } from '../helpers/property' +import { container } from '../helpers/ioc' +import { parse } from 'url' + +export class InProcProvider extends Provider { + public getInvoker(serviceType, params, initContext) { + const callContext = this.createCallContext(serviceType, 'handle', initContext) + + const invoker = async (invokeParams) => { + const eventContext = { params: invokeParams } + + let result + let error + try { + result = await callContext({ event: eventContext, serviceType }) + } catch (err) { + error = err + } + const response = await this.resultTransform(error, result, eventContext, serviceType) + + return response + } + return invoker + } + + protected resultTransform(error, result, eventContext, serviceType) { + if (error) throw error + + if (result && typeof result.status === 'number' && result.hasOwnProperty('data')) { + return result.data + } + + return result + } + +} + +InProcProvider.addParameterDecoratorImplementation("param", async (parameter, context, provider) => { + const req = context.event.params + const source = parameter.source; + if (typeof source !== 'undefined') { + const holder = !source ? context : get(context, source) + if (holder) { + return get(holder, parameter.from) + } + } else { + let value = undefined + if (typeof (value = get(req, parameter.from)) !== 'undefined') return value + if (typeof (value = get(context.context, parameter.from)) !== 'undefined') return value + return value + } + return undefined +}) + +export const provider = container.resolve(InProcProvider) diff --git a/src/providers/index.ts b/src/providers/index.ts index 020e65d..bf01dc2 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -1,33 +1,71 @@ -import { get } from 'lodash' +import { constants, getMetadata, getOverridableMetadata } from '../annotations' +const { PARAMETER_PARAMKEY } = constants +import { callExtension } from '../classes/core/callExtension' +import { container } from '../helpers/ioc' -import * as aws from './aws' -import * as local from './local' -import * as deploy from './deploy' +export { Provider } from './core/provider' +export { AWSProvider } from './aws' +export { LocalProvider } from './local' +export { AzureProvider } from './azure' -let environments = { aws, local, deploy } -let invokeEnvironments = { aws, local } +import { Provider } from './core/provider' +import { provider as aws } from './aws' +import { provider as local } from './local' +import { provider as azure } from './azure' + +const environments = {} +const invokeEnvironments = {} + +export const addProvider = (name, provider: Provider) => { + environments[name] = provider + invokeEnvironments[name] = provider +} + +export const removeProvider = (name) => { + delete environments[name] + delete invokeEnvironments[name] +} + +addProvider('aws', aws) +addProvider('local', local) +addProvider('azure', azure) export const getInvoker = (serviceType, params) => { + const environment = process.env.FUNCTIONAL_ENVIRONMENT; - if (!process.env.FUNCTIONAL_ENVIRONMENT || !environments[process.env.FUNCTIONAL_ENVIRONMENT]) { - throw new Error(`missing environment: process.env.FUNCTIONAL_ENVIRONMENT`) + if (!environment || !environments[environment]) { + throw new Error(`missing environment: '${environment}'`) } - let currentEnvironment = environments[process.env.FUNCTIONAL_ENVIRONMENT] + const currentEnvironment = environments[environment] - let invoker = currentEnvironment.getInvoker(serviceType, params) + const resolvedServiceType = container.resolveType(serviceType) + const invoker = currentEnvironment.getInvoker(resolvedServiceType, params) + + const invokeHandler = async (...params) => { + const onHandleResult = await callExtension(resolvedServiceType, `onHandle_${environment}`, ...params) + if (typeof onHandleResult !== 'undefined') { + return onHandleResult + } + return await invoker(...params) + } - Object.defineProperty(invoker, 'serviceType', { + Object.defineProperty(invokeHandler, 'serviceType', { enumerable: false, configurable: false, writable: false, - value: serviceType, + value: resolvedServiceType, }) - return invoker + return invokeHandler } -export const invoke = async (serviceInstance, params?, invokeConfig?) => { +export const invoke = async (serviceType, params?, invokeConfig?) => { + await callExtension(serviceType, 'onInvoke', { + params, + invokeConfig, + }) + const environmentMode = (invokeConfig && invokeConfig.mode) || process.env.FUNCTIONAL_ENVIRONMENT if (!environmentMode || !invokeEnvironments[environmentMode]) { @@ -36,5 +74,22 @@ export const invoke = async (serviceInstance, params?, invokeConfig?) => { let currentEnvironment = invokeEnvironments[environmentMode] - return await currentEnvironment.invoke(serviceInstance, params, invokeConfig) -} \ No newline at end of file + const availableParams = {} + const parameterMapping = (getOverridableMetadata(PARAMETER_PARAMKEY, serviceType, 'handle') || []) + parameterMapping.forEach((target) => { + if (params && target && target.type === 'param') { + availableParams[target.from] = params[target.from] + } + }) + + await callExtension(serviceType, `onInvoke_${environmentMode}`, { + invokeParams: params, + params: availableParams, + invokeConfig, + parameterMapping, + currentEnvironment, + environmentMode + }) + + return await currentEnvironment.invoke(serviceType, availableParams, invokeConfig) +} diff --git a/src/providers/local.ts b/src/providers/local.ts index f12ec4b..34884fb 100644 --- a/src/providers/local.ts +++ b/src/providers/local.ts @@ -1,90 +1,136 @@ import * as request from 'request' -import { constants, getOwnMetadata, getMetadata, getFunctionName } from '../annotations' - -export const getInvoker = (serviceType, params) => { - const serviceInstance = new serviceType(...params) - const parameterMapping = (getOwnMetadata(constants.PARAMETER_PARAMKEY, serviceType, 'handle') || []) - .filter(t => t && typeof t.parameterIndex === 'number'); - - const invoker = async (req, res, next) => { - try { - const params = [] - parameterMapping.forEach((target) => { - params[target.parameterIndex] = parameterResolver(req, target) - }) - - const r = await serviceInstance.handle.apply(serviceInstance, params) - res.send(r) - return r - } catch (e) { - next(e) +import { Provider } from './core/provider' +import { constants, getMetadata, getFunctionName, rest } from '../annotations' +const { CLASS_LOGKEY } = constants +import { get } from '../helpers/property' +import { container } from '../helpers/ioc' +import { parse } from 'url' + +export class LocalProvider extends Provider { + public getInvoker(serviceType, params, initContext) { + const callContext = this.createCallContext(serviceType, 'handle', initContext) + + const invoker = async (req, res, next) => { + try { + const eventContext = { req, res, next } + + let result + let error + try { + result = await callContext({ event: eventContext, serviceType }) + } catch (err) { + error = err + } + const response = await this.resultTransform(error, result, eventContext, serviceType) + + res.send(response) + return response + } catch (e) { + next(e) + } } + return invoker } - return invoker -} -const parameterResolver = (req, target) => { - switch (target.type) { - case 'inject': - let serviceType = target.serviceType - return new serviceType(...target.params.map((p) => typeof p === 'function' ? p() : p)) - default: - if (req.body && req.body[target.from]) return req.body[target.from] - if (req.query && req.query[target.from]) return req.query[target.from] - if (req.params && req.params[target.from]) return req.params[target.from] - } -} + protected resultTransform(error, result, eventContext, serviceType) { + if (error) throw error -export const invoke = async (serviceInstance, params?, invokeConfig?) => { - return new Promise((resolve, reject) => { - let lambdaParams = {} + if (result && typeof result.status === 'number' && result.hasOwnProperty('data')) { + const { res } = eventContext - let parameterMapping = getOwnMetadata(constants.PARAMETER_PARAMKEY, serviceInstance.constructor, 'handle') || []; - parameterMapping.forEach((target) => { - if (params && target && target.type === 'param') { - lambdaParams[target.from] = params[target.from] + res.status(result.status) + + if (typeof result.headers === 'object' && result.headers) { + res.set(result.headers) } - }) - let httpAttr = getMetadata(constants.CLASS_APIGATEWAYKEY, serviceInstance)[0] + return result.data + } + + return result + } + + public async invoke(serviceType, params, invokeConfig?) { + + const httpAttr = (getMetadata(rest.environmentKey, serviceType) || [])[0] if (!httpAttr) { - return reject(new Error('missing http configuration')) + throw new Error('missing http configuration') } - let invokeParams: any = { - method: httpAttr.method || 'GET', + let method = (httpAttr.methods[0] || 'GET').toString().toLowerCase() + method = method === 'any' ? 'post' : method + const invokeParams: any = { + method, url: `http://localhost:${process.env.FUNCTIONAL_LOCAL_PORT}${httpAttr.path}`, }; - if (!httpAttr.method || httpAttr.method.toLowerCase() === 'get') { - invokeParams.qs = lambdaParams + if (method === 'get') { + invokeParams.qs = params } else { - invokeParams.body = lambdaParams + invokeParams.body = params invokeParams.json = true } - try { - const isLoggingEnabled = getMetadata(constants.CLASS_LOGKEY, serviceInstance) - if (isLoggingEnabled) { - console.log(`${new Date().toISOString()} request to ${getFunctionName(serviceInstance)}`, JSON.stringify(invokeParams, null, 2)) - } + const isLoggingEnabled = getMetadata(CLASS_LOGKEY, serviceType) + if (isLoggingEnabled) { + console.log(`${new Date().toISOString()} request to ${getFunctionName(serviceType)}`, JSON.stringify(invokeParams, null, 2)) + } + + return await this.invokeExec(invokeParams) + } - request(invokeParams, (error, response, body) => { + public async invokeExec(config: any): Promise { + return new Promise((resolve, reject) => { + try { + request(config, (error, response, body) => { - if (error) return reject(error) + if (error) return reject(error) - let result = body - try { - result = JSON.parse(result) - } - catch (e) { } + let result = body + try { + result = JSON.parse(result) + } + catch (e) { } + + return resolve(result) - return resolve(result) + }) + } catch (e) { + return reject(e); + } + }) + } +} - }) - } catch (e) { - return reject(e); +LocalProvider.addParameterDecoratorImplementation("param", async (parameter, context, provider) => { + const req = context.event.req + const source = parameter.source; + if (typeof source !== 'undefined') { + const holder = !source ? context : get(context, source) + if (holder) { + return get(holder, parameter.from) } - }) -} \ No newline at end of file + } else { + let value = undefined + if (typeof (value = get(req.body, parameter.from)) !== 'undefined') return value + if (typeof (value = get(req.query, parameter.from)) !== 'undefined') return value + if (typeof (value = get(req.params, parameter.from)) !== 'undefined') return value + if (typeof (value = get(req.headers, parameter.from)) !== 'undefined') return value + return value + } + return undefined +}) + +LocalProvider.addParameterDecoratorImplementation("request", async (parameter, context, provider) => { + return { + url: context.event.req._parsedUrl || parse(context.event.req.originalUrl), + method: context.event.req.method, + body: context.event.req.body, + query: context.event.req.query, + params: context.event.req.params, + headers: context.event.req.headers + } +}) + +export const provider = container.resolve(LocalProvider) diff --git a/test/annotation.tests.ts b/test/annotation.tests.ts new file mode 100644 index 0000000..0680fc8 --- /dev/null +++ b/test/annotation.tests.ts @@ -0,0 +1,2342 @@ +import 'mocha' +import { expect } from 'chai' + +import { + CLASS_APIGATEWAYKEY, CLASS_DYNAMOTABLECONFIGURATIONKEY, CLASS_ENVIRONMENTKEY, CLASS_NAMEKEY, + CLASS_INJECTABLEKEY, CLASS_LOGKEY, CLASS_AWSRUNTIMEKEY, CLASS_AWSMEMORYSIZEKEY, CLASS_AWSTIMEOUTKEY, + CLASS_S3CONFIGURATIONKEY, CLASS_SNSCONFIGURATIONKEY, CLASS_TAGKEY, CLASS_ROLEKEY, CLASS_DESCRIPTIONKEY, + PARAMETER_PARAMKEY, CLASS_CLASSCONFIGKEY, CLASS_HTTPTRIGGER, CLASS_AZURENODEKEY, CLASS_CLOUDWATCHEVENT +} from '../src/annotations/constants' +import { applyTemplates, templates } from '../src/annotations/templates' +import { getFunctionParameters } from '../src/annotations/utils' +import { getMetadata, getOverridableMetadata } from '../src/annotations/metadata' +import { expandableDecorator } from '../src/annotations/classes/expandableDecorator' +import { apiGateway } from '../src/annotations/classes/aws/apiGateway' +import { httpTrigger } from '../src/annotations/classes/azure/httpTrigger' +import { rest, httpGet, httpPost, httpPut, httpPatch, httpDelete } from '../src/annotations/classes/rest' +import { dynamoTable, dynamo, __dynamoDBDefaults } from '../src/annotations/classes/dynamoTable' +import { environment } from '../src/annotations/classes/environment' +import { functionName, getFunctionName } from '../src/annotations/classes/functionName' +import { injectable, InjectionScope } from '../src/annotations/classes/injectable' +import { log } from '../src/annotations/classes/log' +import { s3Storage } from '../src/annotations/classes/s3Storage' +import { sns } from '../src/annotations/classes/sns' +import { cloudWatchEvent } from '../src/annotations/classes/aws/cloudWatchEvent' +import { tag } from '../src/annotations/classes/tag' +import { eventSource } from '../src/annotations/classes/eventSource' +import { classConfig } from '../src/annotations/classes/classConfig' +import { role, description } from '../src/annotations' + +import { aws } from '../src/annotations/classes/aws/aws' +import { azure } from '../src/annotations/classes/azure/azure' + + +import { inject } from '../src/annotations/parameters/inject' +import { param, request, serviceParams, createParameterDecorator } from '../src/annotations/parameters/param' +import { FunctionalService, Resource, DynamoTable, SimpleNotificationService, S3Storage, Api, Service } from '../src/classes' + + + +describe('annotations', () => { + + describe("templates", () => { + class TemplateTestClass { } + + it("simple", () => { + + const { templatedKey, templatedValue } = applyTemplates('key', 'value', TemplateTestClass) + + expect(templatedKey).to.equal('key') + expect(templatedValue).to.equal('value') + }) + it("key param", () => { + const { templatedKey, templatedValue } = applyTemplates('%ClassName%_key', 'value', TemplateTestClass) + + expect(templatedKey).to.equal('TemplateTestClass_key') + expect(templatedValue).to.equal('value') + }) + it("value param", () => { + const { templatedKey, templatedValue } = applyTemplates('key', '%ClassName%_value', TemplateTestClass) + + expect(templatedKey).to.equal('key') + expect(templatedValue).to.equal('TemplateTestClass_value') + }) + it("key-value param", () => { + const { templatedKey, templatedValue } = applyTemplates('%ClassName%_key', '%ClassName%_value', TemplateTestClass) + + expect(templatedKey).to.equal('TemplateTestClass_key') + expect(templatedValue).to.equal('TemplateTestClass_value') + }) + + describe("custom templates", () => { + + before(() => { + templates.push({ + name: 'templateName', + regexp: /%myClassName%/g, + resolution: (target) => 'my' + target.name + }) + }) + + after(() => { + templates.length = 1 + }) + + it("simple", () => { + + const { templatedKey, templatedValue } = applyTemplates('key', 'value', TemplateTestClass) + + expect(templatedKey).to.equal('key') + expect(templatedValue).to.equal('value') + }) + it("key param", () => { + const { templatedKey, templatedValue } = applyTemplates('%myClassName%_key', 'value', TemplateTestClass) + + expect(templatedKey).to.equal('myTemplateTestClass_key') + expect(templatedValue).to.equal('value') + }) + it("value param", () => { + const { templatedKey, templatedValue } = applyTemplates('key', '%myClassName%_value', TemplateTestClass) + + expect(templatedKey).to.equal('key') + expect(templatedValue).to.equal('myTemplateTestClass_value') + }) + it("key-value param", () => { + const { templatedKey, templatedValue } = applyTemplates('%myClassName%_key', '%myClassName%_value', TemplateTestClass) + + expect(templatedKey).to.equal('myTemplateTestClass_key') + expect(templatedValue).to.equal('myTemplateTestClass_value') + }) + }) + }) + + describe("utils", () => { + it("getFunctionParameters", () => { + class GetFunctionParameterClass { + method(p1, p2, p3) { } + } + + const parameters = getFunctionParameters(GetFunctionParameterClass, 'method') + + expect(parameters).to.have.ordered.members(['p1', 'p2', 'p3']) + expect(parameters).to.be.an('array').that.does.not.include('p4'); + }) + }) + + describe("classes", () => { + describe("expandableDecorator", () => { + it("interface", () => { + const mockDecorator = expandableDecorator<{ name: string, p1?: number }>({ name: 'mockDecorator', defaultValues: { p1: 2 } }) + + expect(mockDecorator).to.be.a('function') + expect(mockDecorator).to.have.property('environmentKey', 'functionly:class:mockDecorator') + expect(mockDecorator).to.have.property('extension').to.be.a('function') + }) + it("environmentKey", () => { + const mockDecorator = expandableDecorator<{ name: string, p1?: number }>({ name: 'mockDecorator', defaultValues: { p1: 2 }, environmentKey: 'mockDecorator_environment_key' }) + + expect(mockDecorator).to.be.a('function') + expect(mockDecorator).to.have.property('environmentKey', 'mockDecorator_environment_key') + expect(mockDecorator).to.have.property('extension').to.be.a('function') + }) + it("default value", () => { + const mockDecorator = expandableDecorator<{ name: string, p1?: number }>({ name: 'mockDecorator', defaultValues: { p1: 2 } }) + + @mockDecorator({ name: 'n1' }) + class TestClass { } + + const value = getMetadata(mockDecorator.environmentKey, TestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('name', 'n1') + expect(metadata).to.have.property('p1', 2) + }) + it("configured value", () => { + const mockDecorator = expandableDecorator<{ name: string, p1?: number }>({ name: 'mockDecorator', defaultValues: { p1: 2 } }) + + @mockDecorator({ name: 'n1', p1: 3 }) + class TestClass { } + + const value = getMetadata(mockDecorator.environmentKey, TestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('name', 'n1') + expect(metadata).to.have.property('p1', 3) + }) + describe("extension", () => { + afterEach(() => { + delete process.env.FUNCTIONAL_ENVIRONMENT + }) + it("environment extension", () => { + let counter = 0 + const mockDecorator = expandableDecorator<{ name: string, p1?: number }>({ name: 'mockDecorator', defaultValues: { p1: 2 } }) + mockDecorator.extension('custom', (target, config) => { + expect(target).to.equal(TestClass) + expect(config).to.deep.equal({ name: 'n1', p1: 2 }) + counter++ + }) + + process.env.FUNCTIONAL_ENVIRONMENT = 'custom' + @mockDecorator({ name: 'n1' }) + class TestClass { } + + const value = getMetadata(mockDecorator.environmentKey, TestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('name', 'n1') + expect(metadata).to.have.property('p1', 2) + expect(counter).to.equal(1) + }) + }) + }) + describe("apiGateway", () => { + it("path", () => { + @apiGateway({ path: '/v1/test' }) + class ApiGatewayTestClass { } + + const value = getMetadata(CLASS_APIGATEWAYKEY, ApiGatewayTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('path', '/v1/test') + expect(metadata).to.have.property('method', 'get') + expect(metadata).to.have.property('cors', true) + expect(metadata).to.have.property('authorization', 'NONE') + }) + it("method", () => { + @apiGateway({ path: '/v1/test', method: 'post' }) + class ApiGatewayTestClass { } + + const value = getMetadata(CLASS_APIGATEWAYKEY, ApiGatewayTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('path', '/v1/test') + expect(metadata).to.have.property('method', 'post') + expect(metadata).to.have.property('cors', true) + expect(metadata).to.have.property('authorization', 'NONE') + }) + it("cors", () => { + @apiGateway({ path: '/v1/test', cors: false }) + class ApiGatewayTestClass { } + + const value = getMetadata(CLASS_APIGATEWAYKEY, ApiGatewayTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('path', '/v1/test') + expect(metadata).to.have.property('method', 'get') + expect(metadata).to.have.property('cors', false) + expect(metadata).to.have.property('authorization', 'NONE') + }) + it("corsConfig", () => { + @apiGateway({ path: '/v1/test', corsConfig: { headers: ['X-test'] } }) + class ApiGatewayTestClass { } + + const value = getMetadata(CLASS_APIGATEWAYKEY, ApiGatewayTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('path', '/v1/test') + expect(metadata).to.have.property('method', 'get') + expect(metadata).to.have.property('cors', true) + expect(metadata).to.have.deep.property('corsConfig', { headers: ['X-test'] }) + expect(metadata).to.have.property('authorization', 'NONE') + }) + it("authorization", () => { + @apiGateway({ path: '/v1/test', authorization: 'AWS_IAM' }) + class ApiGatewayTestClass { } + + const value = getMetadata(CLASS_APIGATEWAYKEY, ApiGatewayTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('path', '/v1/test') + expect(metadata).to.have.property('method', 'get') + expect(metadata).to.have.property('cors', true) + expect(metadata).to.have.property('authorization', 'AWS_IAM') + }) + }) + describe("httpTrigger", () => { + it("path", () => { + @httpTrigger({ route: '/v1/test' }) + class HttpTriggerTestClass { } + + const value = getMetadata(CLASS_HTTPTRIGGER, HttpTriggerTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('route', '/v1/test') + expect(metadata).to.have.deep.property('methods', ['get']) + expect(metadata).to.have.property('cors', true) + expect(metadata).to.have.property('authLevel', 'anonymous') + }) + it("method", () => { + @httpTrigger({ route: '/v1/test', methods: ['post'] }) + class HttpTriggerTestClass { } + + const value = getMetadata(CLASS_HTTPTRIGGER, HttpTriggerTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('route', '/v1/test') + expect(metadata).to.have.deep.property('methods', ['post']) + expect(metadata).to.have.property('cors', true) + expect(metadata).to.have.property('authLevel', 'anonymous') + }) + it("cors", () => { + @httpTrigger({ route: '/v1/test', cors: false }) + class HttpTriggerTestClass { } + + const value = getMetadata(CLASS_HTTPTRIGGER, HttpTriggerTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('route', '/v1/test') + expect(metadata).to.have.deep.property('methods', ['get']) + expect(metadata).to.have.property('cors', false) + expect(metadata).to.have.property('authLevel', 'anonymous') + }) + + it("corsConfig", () => { + @httpTrigger({ route: '/v1/test', corsConfig: { headers: ['X-test'] } }) + class HttpTriggerTestClass { } + + const value = getMetadata(CLASS_HTTPTRIGGER, HttpTriggerTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('route', '/v1/test') + expect(metadata).to.have.deep.property('methods', ['get']) + expect(metadata).to.have.property('cors', true) + expect(metadata).to.have.deep.property('corsConfig', { headers: ['X-test'] }) + expect(metadata).to.have.property('authLevel', 'anonymous') + }) + it("authorization", () => { + @httpTrigger({ route: '/v1/test', authLevel: 'function' }) + class HttpTriggerTestClass { } + + const value = getMetadata(CLASS_HTTPTRIGGER, HttpTriggerTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + + expect(metadata).to.have.property('route', '/v1/test') + expect(metadata).to.have.deep.property('methods', ['get']) + expect(metadata).to.have.property('cors', true) + expect(metadata).to.have.property('authLevel', 'function') + }) + }) + describe("rest", () => { + it("path", () => { + @rest({ path: '/v1/test' }) + class ApiGatewayTestClass { } + + const value = getMetadata(rest.environmentKey, ApiGatewayTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('path', '/v1/test') + expect(metadata).to.have.deep.property('methods', ['get']) + expect(metadata).to.have.property('cors', true) + expect(metadata).to.have.property('authenticated', false) + }) + it("method", () => { + @rest({ path: '/v1/test', methods: ['post'] }) + class ApiGatewayTestClass { } + + const value = getMetadata(rest.environmentKey, ApiGatewayTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('path', '/v1/test') + expect(metadata).to.have.deep.property('methods', ['post']) + expect(metadata).to.have.property('cors', true) + expect(metadata).to.have.property('authenticated', false) + }) + it("cors", () => { + @rest({ path: '/v1/test', cors: false }) + class ApiGatewayTestClass { } + + const value = getMetadata(rest.environmentKey, ApiGatewayTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('path', '/v1/test') + expect(metadata).to.have.deep.property('methods', ['get']) + expect(metadata).to.have.property('cors', false) + expect(metadata).to.have.property('authenticated', false) + }) + + it("corsConfig", () => { + @rest({ path: '/v1/test', corsConfig: { headers: ['X-test'] } }) + class ApiGatewayTestClass { } + + const value = getMetadata(rest.environmentKey, ApiGatewayTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('path', '/v1/test') + expect(metadata).to.have.deep.property('methods', ['get']) + expect(metadata).to.have.property('cors', true) + expect(metadata).to.have.deep.property('corsConfig', { headers: ['X-test'] }) + expect(metadata).to.have.property('authenticated', false) + }) + it("authorization", () => { + @rest({ path: '/v1/test', authenticated: true }) + class ApiGatewayTestClass { } + + const value = getMetadata(rest.environmentKey, ApiGatewayTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('path', '/v1/test') + expect(metadata).to.have.deep.property('methods', ['get']) + expect(metadata).to.have.property('cors', true) + expect(metadata).to.have.property('authenticated', true) + }) + }) + describe("httpGet", () => { + it("path", () => { + @httpGet('/v1/test') + class ApiGatewayTestClass { } + + const value = getMetadata(rest.environmentKey, ApiGatewayTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('path', '/v1/test') + expect(metadata).to.have.deep.property('methods', ['get']) + expect(metadata).to.have.property('cors', true) + expect(metadata).to.have.property('authenticated', false) + }) + it("config", () => { + @httpGet({ path: '/v1/test', authenticated: true }) + class ApiGatewayTestClass { } + + const value = getMetadata(rest.environmentKey, ApiGatewayTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('path', '/v1/test') + expect(metadata).to.have.deep.property('methods', ['get']) + expect(metadata).to.have.property('cors', true) + expect(metadata).to.have.property('authenticated', true) + }) + }) + describe("httpPost", () => { + it("path", () => { + @httpPost('/v1/test') + class ApiGatewayTestClass { } + + const value = getMetadata(rest.environmentKey, ApiGatewayTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('path', '/v1/test') + expect(metadata).to.have.deep.property('methods', ['post']) + expect(metadata).to.have.property('cors', true) + expect(metadata).to.have.property('authenticated', false) + }) + it("config", () => { + @httpPost({ path: '/v1/test', authenticated: true }) + class ApiGatewayTestClass { } + + const value = getMetadata(rest.environmentKey, ApiGatewayTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('path', '/v1/test') + expect(metadata).to.have.deep.property('methods', ['post']) + expect(metadata).to.have.property('cors', true) + expect(metadata).to.have.property('authenticated', true) + }) + }) + describe("httpPut", () => { + it("path", () => { + @httpPut('/v1/test') + class ApiGatewayTestClass { } + + const value = getMetadata(rest.environmentKey, ApiGatewayTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('path', '/v1/test') + expect(metadata).to.have.deep.property('methods', ['put']) + expect(metadata).to.have.property('cors', true) + expect(metadata).to.have.property('authenticated', false) + }) + it("config", () => { + @httpPut({ path: '/v1/test', authenticated: true }) + class ApiGatewayTestClass { } + + const value = getMetadata(rest.environmentKey, ApiGatewayTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('path', '/v1/test') + expect(metadata).to.have.deep.property('methods', ['put']) + expect(metadata).to.have.property('cors', true) + expect(metadata).to.have.property('authenticated', true) + }) + }) + describe("httpPatch", () => { + it("path", () => { + @httpPatch('/v1/test') + class ApiGatewayTestClass { } + + const value = getMetadata(rest.environmentKey, ApiGatewayTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('path', '/v1/test') + expect(metadata).to.have.deep.property('methods', ['patch']) + expect(metadata).to.have.property('cors', true) + expect(metadata).to.have.property('authenticated', false) + }) + it("config", () => { + @httpPatch({ path: '/v1/test', authenticated: true }) + class ApiGatewayTestClass { } + + const value = getMetadata(rest.environmentKey, ApiGatewayTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('path', '/v1/test') + expect(metadata).to.have.deep.property('methods', ['patch']) + expect(metadata).to.have.property('cors', true) + expect(metadata).to.have.property('authenticated', true) + }) + }) + describe("httpDelete", () => { + it("path", () => { + @httpDelete('/v1/test') + class ApiGatewayTestClass { } + + const value = getMetadata(rest.environmentKey, ApiGatewayTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('path', '/v1/test') + expect(metadata).to.have.deep.property('methods', ['delete']) + expect(metadata).to.have.property('cors', true) + expect(metadata).to.have.property('authenticated', false) + }) + it("config", () => { + @httpDelete({ path: '/v1/test', authenticated: true }) + class ApiGatewayTestClass { } + + const value = getMetadata(rest.environmentKey, ApiGatewayTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('path', '/v1/test') + expect(metadata).to.have.deep.property('methods', ['delete']) + expect(metadata).to.have.property('cors', true) + expect(metadata).to.have.property('authenticated', true) + }) + }) + describe("dynamoTable", () => { + it("no param", () => { + @dynamoTable() + class DynamoTableTestClass { } + + const value = getMetadata(CLASS_DYNAMOTABLECONFIGURATIONKEY, DynamoTableTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('tableName', 'DynamoTableTestClass-table') + expect(metadata).to.have.property('environmentKey', 'DynamoTableTestClass_TABLE_NAME') + expect(metadata).to.have.property('definedBy', DynamoTableTestClass) + expect(metadata).to.have.deep.property('nativeConfig').that.deep.equal(__dynamoDBDefaults); + }) + it("empty", () => { + @dynamoTable({}) + class DynamoTableTestClass { } + + const value = getMetadata(CLASS_DYNAMOTABLECONFIGURATIONKEY, DynamoTableTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('tableName', 'DynamoTableTestClass-table') + expect(metadata).to.have.property('environmentKey', 'DynamoTableTestClass_TABLE_NAME') + expect(metadata).to.have.property('definedBy', DynamoTableTestClass) + expect(metadata).to.have.deep.property('nativeConfig').that.deep.equal(__dynamoDBDefaults); + }) + it("tableName", () => { + @dynamoTable({ tableName: 'mytablename' }) + class DynamoTableTestClass { } + + const value = getMetadata(CLASS_DYNAMOTABLECONFIGURATIONKEY, DynamoTableTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('tableName', 'mytablename') + expect(metadata).to.have.property('environmentKey', 'DynamoTableTestClass_TABLE_NAME') + expect(metadata).to.have.property('definedBy', DynamoTableTestClass) + expect(metadata).to.have.deep.property('nativeConfig').that.deep.equal(__dynamoDBDefaults); + }) + it("environmentKey", () => { + @dynamoTable({ tableName: 'mytablename', environmentKey: 'myenvkey' }) + class DynamoTableTestClass { } + + const value = getMetadata(CLASS_DYNAMOTABLECONFIGURATIONKEY, DynamoTableTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('tableName', 'mytablename') + expect(metadata).to.have.property('environmentKey', 'myenvkey') + expect(metadata).to.have.property('definedBy', DynamoTableTestClass) + expect(metadata).to.have.deep.property('nativeConfig').that.deep.equal(__dynamoDBDefaults); + }) + it("nativeConfig", () => { + @dynamoTable({ + tableName: 'mytablename', nativeConfig: { + ProvisionedThroughput: { + ReadCapacityUnits: 4, + WriteCapacityUnits: 4 + } + } + }) + class DynamoTableTestClass { } + + const value = getMetadata(CLASS_DYNAMOTABLECONFIGURATIONKEY, DynamoTableTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('tableName', 'mytablename') + expect(metadata).to.have.property('environmentKey', 'DynamoTableTestClass_TABLE_NAME') + expect(metadata).to.have.property('definedBy', DynamoTableTestClass) + expect(metadata).to.have.deep.property('nativeConfig').that.deep.equal({ + ...__dynamoDBDefaults, + ProvisionedThroughput: { + ReadCapacityUnits: 4, + WriteCapacityUnits: 4 + } + }); + }) + }) + describe("dynamo", () => { + it("no param", () => { + @dynamo() + class DynamoTableTestClass { } + + const value = getMetadata(CLASS_DYNAMOTABLECONFIGURATIONKEY, DynamoTableTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('tableName', 'DynamoTableTestClass-table') + expect(metadata).to.have.property('environmentKey', 'DynamoTableTestClass_TABLE_NAME') + expect(metadata).to.have.property('definedBy', DynamoTableTestClass) + expect(metadata).to.have.deep.property('nativeConfig').that.deep.equal(__dynamoDBDefaults); + }) + it("empty", () => { + @dynamo({}) + class DynamoTableTestClass { } + + const value = getMetadata(CLASS_DYNAMOTABLECONFIGURATIONKEY, DynamoTableTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('tableName', 'DynamoTableTestClass-table') + expect(metadata).to.have.property('environmentKey', 'DynamoTableTestClass_TABLE_NAME') + expect(metadata).to.have.property('definedBy', DynamoTableTestClass) + expect(metadata).to.have.deep.property('nativeConfig').that.deep.equal(__dynamoDBDefaults); + }) + it("tableName", () => { + @dynamo({ tableName: 'mytablename' }) + class DynamoTableTestClass { } + + const value = getMetadata(CLASS_DYNAMOTABLECONFIGURATIONKEY, DynamoTableTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('tableName', 'mytablename') + expect(metadata).to.have.property('environmentKey', 'DynamoTableTestClass_TABLE_NAME') + expect(metadata).to.have.property('definedBy', DynamoTableTestClass) + expect(metadata).to.have.deep.property('nativeConfig').that.deep.equal(__dynamoDBDefaults); + }) + it("environmentKey", () => { + @dynamo({ tableName: 'mytablename', environmentKey: 'myenvkey' }) + class DynamoTableTestClass { } + + const value = getMetadata(CLASS_DYNAMOTABLECONFIGURATIONKEY, DynamoTableTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('tableName', 'mytablename') + expect(metadata).to.have.property('environmentKey', 'myenvkey') + expect(metadata).to.have.property('definedBy', DynamoTableTestClass) + expect(metadata).to.have.deep.property('nativeConfig').that.deep.equal(__dynamoDBDefaults); + }) + it("nativeConfig", () => { + @dynamo({ + tableName: 'mytablename', nativeConfig: { + ProvisionedThroughput: { + ReadCapacityUnits: 4, + WriteCapacityUnits: 4 + } + } + }) + class DynamoTableTestClass { } + + const value = getMetadata(CLASS_DYNAMOTABLECONFIGURATIONKEY, DynamoTableTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('tableName', 'mytablename') + expect(metadata).to.have.property('environmentKey', 'DynamoTableTestClass_TABLE_NAME') + expect(metadata).to.have.property('definedBy', DynamoTableTestClass) + expect(metadata).to.have.deep.property('nativeConfig').that.deep.equal({ + ...__dynamoDBDefaults, + ProvisionedThroughput: { + ReadCapacityUnits: 4, + WriteCapacityUnits: 4 + } + }); + }) + }) + describe("environment", () => { + it("key - value", () => { + @environment('key', 'value') + @environment('key2', 'value2') + class EnvironmentTestClass { } + + const metadata = getMetadata(CLASS_ENVIRONMENTKEY, EnvironmentTestClass) + + expect(metadata).to.have.all.keys('key', 'key2') + expect(metadata).to.have.property('key', 'value') + expect(metadata).to.have.property('key2', 'value2') + expect(metadata).to.not.have.property('key3') + }) + it("template", () => { + @environment('%ClassName%_env', 'value') + @environment('key', '%ClassName%_value') + @environment('%ClassName%_env2', '%ClassName%_value2') + class EnvironmentTestClass { } + + const metadata = getMetadata(CLASS_ENVIRONMENTKEY, EnvironmentTestClass) + + expect(metadata).to.have.all.keys('EnvironmentTestClass_env', 'key', 'EnvironmentTestClass_env2') + expect(metadata).to.have.property('EnvironmentTestClass_env', 'value') + expect(metadata).to.have.property('key', 'EnvironmentTestClass_value') + expect(metadata).to.have.property('EnvironmentTestClass_env2', 'EnvironmentTestClass_value2') + }) + }) + describe("functionName", () => { + it("metadata", () => { + @functionName('fnName') + class FunctionNameTestClass { } + + const name = getMetadata(CLASS_NAMEKEY, FunctionNameTestClass) + + expect(name).to.equal('fnName') + }) + it("functionName", () => { + class FunctionNameTestClass { } + + const name = getFunctionName(FunctionNameTestClass) + + expect(name).to.equal(FunctionNameTestClass.name) + }) + it("attribute", () => { + @functionName('fnName') + class FunctionNameTestClass { } + + const name = getFunctionName(FunctionNameTestClass) + + expect(name).to.equal('fnName') + }) + it("instance functionName", () => { + class FunctionNameTestClass { } + + const name = getFunctionName(new FunctionNameTestClass()) + + expect(name).to.equal(FunctionNameTestClass.name) + }) + it("instance attribute", () => { + @functionName('fnName') + class FunctionNameTestClass { } + + const name = getFunctionName(new FunctionNameTestClass()) + + expect(name).to.equal('fnName') + }) + }) + describe("injectable", () => { + it("injectable", () => { + @injectable() + class InjectableTestClass { } + + const value = getMetadata(CLASS_INJECTABLEKEY, InjectableTestClass) + + expect(value).to.equal(1) + }) + it("injectable", () => { + @injectable(InjectionScope.Transient) + class InjectableTestClass { } + + const value = getMetadata(CLASS_INJECTABLEKEY, InjectableTestClass) + + expect(value).to.equal(1) + }) + it("injectable", () => { + @injectable(InjectionScope.Singleton) + class InjectableTestClass { } + + const value = getMetadata(CLASS_INJECTABLEKEY, InjectableTestClass) + + expect(value).to.equal(2) + }) + it("non injectable", () => { + class InjectableTestClass { } + + const value = getMetadata(CLASS_INJECTABLEKEY, InjectableTestClass) + + expect(value).to.undefined + }) + }) + describe("log", () => { + it("log", () => { + @log() + class LogTestClass { } + + const value = getMetadata(CLASS_LOGKEY, LogTestClass) + + expect(value).to.equal(true) + }) + it("non log", () => { + class LogTestClass { } + + const value = getMetadata(CLASS_LOGKEY, LogTestClass) + + expect(value).to.undefined + }) + }) + describe("s3Storage", () => { + it("no param", () => { + @s3Storage() + class S3StorageTestClass { } + + const value = getMetadata(CLASS_S3CONFIGURATIONKEY, S3StorageTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('bucketName', 's3storagetestclass-bucket') + expect(metadata).to.have.property('environmentKey', 'S3StorageTestClass_S3_BUCKET') + expect(metadata).to.have.property('definedBy', S3StorageTestClass.name) + expect(metadata).to.not.have.property('eventSourceConfiguration') + }) + it("empty", () => { + @s3Storage({}) + class S3StorageTestClass { } + + const value = getMetadata(CLASS_S3CONFIGURATIONKEY, S3StorageTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('bucketName', 's3storagetestclass-bucket') + expect(metadata).to.have.property('environmentKey', 'S3StorageTestClass_S3_BUCKET') + expect(metadata).to.have.property('definedBy', S3StorageTestClass.name) + expect(metadata).to.not.have.property('eventSourceConfiguration') + }) + it("bucketName", () => { + @s3Storage({ bucketName: 'mybucketname' }) + class S3StorageTestClass { } + + const value = getMetadata(CLASS_S3CONFIGURATIONKEY, S3StorageTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('bucketName', 'mybucketname') + expect(metadata).to.have.property('environmentKey', 'S3StorageTestClass_S3_BUCKET') + expect(metadata).to.have.property('definedBy', S3StorageTestClass.name) + expect(metadata).to.not.have.property('eventSourceConfiguration') + }) + it("bucketName LowerCase", () => { + @s3Storage({ bucketName: 'myBucketName' }) + class S3StorageTestClass { } + + const value = getMetadata(CLASS_S3CONFIGURATIONKEY, S3StorageTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('bucketName').that.not.equal('myBucketName') + expect(metadata).to.have.property('bucketName', 'mybucketname') + expect(metadata).to.have.property('environmentKey', 'S3StorageTestClass_S3_BUCKET') + expect(metadata).to.have.property('definedBy', S3StorageTestClass.name) + expect(metadata).to.not.have.property('eventSourceConfiguration') + }) + it("environmentKey", () => { + @s3Storage({ bucketName: 'mybucketname', environmentKey: 'myenvkey' }) + class S3StorageTestClass { } + + const value = getMetadata(CLASS_S3CONFIGURATIONKEY, S3StorageTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('bucketName', 'mybucketname') + expect(metadata).to.have.property('environmentKey', 'myenvkey') + expect(metadata).to.have.property('definedBy', S3StorageTestClass.name) + expect(metadata).to.not.have.property('eventSourceConfiguration') + }) + it("eventSourceConfiguration", () => { + @s3Storage({ + bucketName: 'mybucketname', + eventSourceConfiguration: { + Event: 'eventName', + Filter: {} + } + }) + class S3StorageTestClass { } + + const value = getMetadata(CLASS_S3CONFIGURATIONKEY, S3StorageTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('bucketName', 'mybucketname') + expect(metadata).to.have.property('environmentKey', 'S3StorageTestClass_S3_BUCKET') + expect(metadata).to.have.property('definedBy', S3StorageTestClass.name) + expect(metadata).to.have.deep.property('eventSourceConfiguration').that.deep.equal({ + Event: 'eventName', + Filter: {} + }); + }) + }) + describe("sns", () => { + it("no parm", () => { + @sns() + class SNSTestClass { } + + const value = getMetadata(CLASS_SNSCONFIGURATIONKEY, SNSTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('topicName', 'SNSTestClass-topic') + expect(metadata).to.have.property('environmentKey', 'SNSTestClass_SNS_TOPICNAME') + expect(metadata).to.have.property('definedBy', SNSTestClass.name) + }) + it("topicName", () => { + @sns({}) + class SNSTestClass { } + + const value = getMetadata(CLASS_SNSCONFIGURATIONKEY, SNSTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('topicName', 'SNSTestClass-topic') + expect(metadata).to.have.property('environmentKey', 'SNSTestClass_SNS_TOPICNAME') + expect(metadata).to.have.property('definedBy', SNSTestClass.name) + }) + it("topicName", () => { + @sns({ topicName: 'myTopicName' }) + class SNSTestClass { } + + const value = getMetadata(CLASS_SNSCONFIGURATIONKEY, SNSTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('topicName', 'myTopicName') + expect(metadata).to.have.property('environmentKey', 'SNSTestClass_SNS_TOPICNAME') + expect(metadata).to.have.property('definedBy', SNSTestClass.name) + }) + it("environmentKey", () => { + @sns({ topicName: 'myTopicName', environmentKey: 'myenvkey' }) + class SNSTestClass { } + + const value = getMetadata(CLASS_SNSCONFIGURATIONKEY, SNSTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('topicName', 'myTopicName') + expect(metadata).to.have.property('environmentKey', 'myenvkey') + expect(metadata).to.have.property('definedBy', SNSTestClass.name) + }) + }) + describe("scheduleEvent", () => { + it("missing config", () => { + @cloudWatchEvent({}) + class ScheduleEventTestClass { } + + const value = getMetadata(CLASS_CLOUDWATCHEVENT, ScheduleEventTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.not.have.property('scheduleExpression') + expect(metadata).to.not.have.property('eventPattern') + expect(metadata).to.have.property('definedBy', ScheduleEventTestClass.name) + }) + it("scheduleExpression", () => { + @cloudWatchEvent({ scheduleExpression: 'rate(10 minutes)' }) + class ScheduleEventTestClass { } + + const value = getMetadata(CLASS_CLOUDWATCHEVENT, ScheduleEventTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('scheduleExpression', 'rate(10 minutes)') + expect(metadata).to.not.have.property('eventPattern') + expect(metadata).to.have.property('definedBy', ScheduleEventTestClass.name) + }) + it("eventPattern", () => { + @cloudWatchEvent({ eventPattern: {} }) + class ScheduleEventTestClass { } + + const value = getMetadata(CLASS_CLOUDWATCHEVENT, ScheduleEventTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.not.have.property('scheduleExpression') + expect(metadata).to.have.deep.property('eventPattern', {}) + expect(metadata).to.have.property('definedBy', ScheduleEventTestClass.name) + }) + it("scheduleExpression and eventPattern", () => { + @cloudWatchEvent({ scheduleExpression: 'rate(10 minutes)', eventPattern: {} }) + class ScheduleEventTestClass { } + + const value = getMetadata(CLASS_CLOUDWATCHEVENT, ScheduleEventTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('scheduleExpression', 'rate(10 minutes)') + expect(metadata).to.have.deep.property('eventPattern', {}) + expect(metadata).to.have.property('definedBy', ScheduleEventTestClass.name) + }) + }) + describe("tag", () => { + it("key - value", () => { + @tag('key', 'value') + @tag('key2', 'value2') + class TagTestClass { } + + const metadata = getMetadata(CLASS_TAGKEY, TagTestClass) + + expect(metadata).to.have.all.keys('key', 'key2') + expect(metadata).to.have.property('key', 'value') + expect(metadata).to.have.property('key2', 'value2') + expect(metadata).to.not.have.property('key3') + }) + }) + describe("role", () => { + it("role", () => { + @role('rolename') + class RoleTestClass { } + + const roleValue = getMetadata(CLASS_ROLEKEY, RoleTestClass) + + expect(roleValue).to.equal('rolename') + }) + }) + describe("description", () => { + it("description", () => { + @description('desc...') + class DescriptionTestClass { } + + const descriptionValue = getMetadata(CLASS_DESCRIPTIONKEY, DescriptionTestClass) + + expect(descriptionValue).to.equal('desc...') + }) + }) + describe("classConfig", () => { + it("classConfig", () => { + @classConfig({ customValue: 'v1' }) + class EventSourceTestClass { } + + const config = getMetadata(CLASS_CLASSCONFIGKEY, EventSourceTestClass) + + expect(config).to.deep.equal({ customValue: 'v1' }) + }) + + it("classConfig inherited", () => { + @classConfig({ customValue: 'v1' }) + class ClassConfigTestClass { } + + class ClassConfigTestClassInherited extends ClassConfigTestClass { } + + const config = getMetadata(CLASS_CLASSCONFIGKEY, ClassConfigTestClassInherited) + + expect(config).to.deep.equal({ customValue: 'v1' }) + }) + + it("classConfig extended", () => { + @classConfig({ customValue: 'v1' }) + class ClassConfigTestClass { } + + @classConfig({ customValue2: 'v2' }) + class ClassConfigTestClassInherited extends ClassConfigTestClass { } + + const config = getMetadata(CLASS_CLASSCONFIGKEY, ClassConfigTestClassInherited) + + expect(config).to.deep.equal({ customValue: 'v1', customValue2: 'v2' }) + }) + + it("classConfig override", () => { + @classConfig({ customValue: 'v1' }) + class ClassConfigTestClass { } + + @classConfig({ customValue: 'v2' }) + class ClassConfigTestClassInherited extends ClassConfigTestClass { } + + const config = getMetadata(CLASS_CLASSCONFIGKEY, ClassConfigTestClassInherited) + + expect(config).to.deep.equal({ customValue: 'v2' }) + }) + }) + describe("eventSource", () => { + it("eventSource", () => { + let counter = 0 + + class ATestClass { + public static toEventSource(target: Function, definitionConfig: any) { + counter++ + } + } + + @eventSource(ATestClass) + class BTestClass { } + + expect(counter).to.equal(1) + }) + + it("multiple eventSource", () => { + let counter = 0 + + class ATestClass { + public static toEventSource(target: Function) { + counter++ + } + } + + class AATestClass extends ATestClass { + public static toEventSource(target: Function) { + counter++ + } + } + + @eventSource(ATestClass) + @eventSource(AATestClass) + @eventSource(AATestClass) + class BTestClass { } + + expect(counter).to.equal(3) + }) + + it("multiple eventSource single definition", () => { + let counter = 0 + + class ATestClass { + public static toEventSource(target: Function) { + counter++ + } + } + + class AATestClass extends ATestClass { + public static toEventSource(target: Function) { + counter++ + } + } + + @eventSource(ATestClass, AATestClass, AATestClass) + class BTestClass { } + + expect(counter).to.equal(3) + }) + + it("eventSource inject service", () => { + @dynamoTable({ tableName: 't1' }) + @classConfig({ injectServiceEventSourceKey: CLASS_DYNAMOTABLECONFIGURATIONKEY }) + class ATestClass extends Api { } + + @eventSource(ATestClass) + class BTestClass extends FunctionalService { } + + const value = getMetadata(CLASS_DYNAMOTABLECONFIGURATIONKEY, BTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('tableName', 't1') + expect(metadata).to.have.property('eventSource', true) + }) + }) + describe("aws", () => { + it("type", () => { + @aws({ type: 'nodejs20.x' }) + class RuntimeTestClass { } + + const runtimeValue = getMetadata(CLASS_AWSRUNTIMEKEY, RuntimeTestClass) + const memoryValue = getMetadata(CLASS_AWSMEMORYSIZEKEY, RuntimeTestClass) + const timeoutValue = getMetadata(CLASS_AWSTIMEOUTKEY, RuntimeTestClass) + + expect(runtimeValue).to.equal('nodejs20.x') + expect(memoryValue).to.undefined + expect(timeoutValue).to.undefined + }) + it("memorySize", () => { + @aws({ memorySize: 100 }) + class RuntimeTestClass { } + + const runtimeValue = getMetadata(CLASS_AWSRUNTIMEKEY, RuntimeTestClass) + const memoryValue = getMetadata(CLASS_AWSMEMORYSIZEKEY, RuntimeTestClass) + const timeoutValue = getMetadata(CLASS_AWSTIMEOUTKEY, RuntimeTestClass) + + expect(runtimeValue).to.undefined + expect(memoryValue).to.equal(100) + expect(timeoutValue).to.undefined + }) + it("timeout", () => { + @aws({ timeout: 3 }) + class RuntimeTestClass { } + + const runtimeValue = getMetadata(CLASS_AWSRUNTIMEKEY, RuntimeTestClass) + const memoryValue = getMetadata(CLASS_AWSMEMORYSIZEKEY, RuntimeTestClass) + const timeoutValue = getMetadata(CLASS_AWSTIMEOUTKEY, RuntimeTestClass) + + expect(runtimeValue).to.undefined + expect(memoryValue).to.undefined + expect(timeoutValue).to.equal(3) + }) + it("all", () => { + @aws({ type: 'nodejs20.x', memorySize: 100, timeout: 3 }) + class RuntimeTestClass { } + + const runtimeValue = getMetadata(CLASS_AWSRUNTIMEKEY, RuntimeTestClass) + const memoryValue = getMetadata(CLASS_AWSMEMORYSIZEKEY, RuntimeTestClass) + const timeoutValue = getMetadata(CLASS_AWSTIMEOUTKEY, RuntimeTestClass) + + expect(runtimeValue).to.equal('nodejs20.x') + expect(memoryValue).to.equal(100) + expect(timeoutValue).to.equal(3) + }) + }) + describe("azure", () => { + it("node", () => { + @azure({ node: '6.5.0' }) + class RuntimeTestClass { } + + const nodeValue = getMetadata(CLASS_AZURENODEKEY, RuntimeTestClass) + + expect(nodeValue).to.equal('6.5.0') + }) + }) + }) + + describe("parameters", () => { + describe("inject", () => { + it("inject", () => { + @injectable() + class ATestClass { } + class BTestClass { + method( @inject(ATestClass) a) { } + } + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, BTestClass, 'method') + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('serviceType', ATestClass) + expect(metadata).to.have.property('parameterIndex', 0) + expect(metadata).to.have.property('type', 'inject') + }) + + it("functional service inject", () => { + @injectable() + class ATestClass extends FunctionalService { } + class BTestClass { + method( @inject(ATestClass) a) { } + } + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, BTestClass, 'method') + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('serviceType', ATestClass) + expect(metadata).to.have.property('parameterIndex', 0) + expect(metadata).to.have.property('type', 'inject') + + const environmentMetadata = getMetadata(CLASS_ENVIRONMENTKEY, BTestClass) + expect(environmentMetadata).to.have + .property(`FUNCTIONAL_SERVICE_${ATestClass.name.toUpperCase()}`, getFunctionName(ATestClass)) + }) + + it("resource inject", () => { + @injectable() + @environment('%ClassName%_defined_environment', 'value') + class ATestClass extends Resource { } + class BTestClass { + method( @inject(ATestClass) a) { } + } + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, BTestClass, 'method') + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('serviceType', ATestClass) + expect(metadata).to.have.property('parameterIndex', 0) + expect(metadata).to.have.property('type', 'inject') + + const environmentMetadata = getMetadata(CLASS_ENVIRONMENTKEY, BTestClass) + expect(environmentMetadata).to.have + .property(`ATestClass_defined_environment`, 'value') + }) + + it("injected DynamoTable", () => { + @injectable() + @dynamoTable({ tableName: 'ATable' }) + class ATestClass extends DynamoTable { } + class BTestClass { + method( @inject(ATestClass) a) { } + } + + const value = getMetadata(CLASS_DYNAMOTABLECONFIGURATIONKEY, BTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('tableName', 'ATable') + }) + + it("injected SimpleNotificationService", () => { + @injectable() + @sns({ topicName: 'ATopic' }) + class ATestClass extends SimpleNotificationService { } + class BTestClass { + method( @inject(ATestClass) a) { } + } + + const value = getMetadata(CLASS_SNSCONFIGURATIONKEY, BTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('topicName', 'ATopic') + }) + + it("injected S3Storage", () => { + @injectable() + @s3Storage({ bucketName: 'ABucket' }) + class ATestClass extends S3Storage { } + class BTestClass { + method( @inject(ATestClass) a) { } + } + + const value = getMetadata(CLASS_S3CONFIGURATIONKEY, BTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('bucketName', 'abucket') + }) + + it("overrided class method", () => { + @injectable() + class ATestClass { } + + class BaseBTestClass { + method( @inject(ATestClass) p1, @inject(ATestClass) p2) { } + } + class BTestClass extends BaseBTestClass { + method( @inject(ATestClass) a) { } + } + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, BTestClass, 'method') + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('serviceType', ATestClass) + expect(metadata).to.have.property('parameterIndex', 0) + expect(metadata).to.have.property('type', 'inject') + }) + + it("overrided class constructor", () => { + @injectable() + class ATestClass { } + + class BaseBTestClass { + constructor( @inject(ATestClass) p1, @inject(ATestClass) p2) { } + } + class BTestClass extends BaseBTestClass { + constructor( @inject(ATestClass) a) { super(a, a) } + } + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, BTestClass, undefined) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('serviceType', ATestClass) + expect(metadata).to.have.property('parameterIndex', 0) + expect(metadata).to.have.property('type', 'inject') + }) + + it("overrided static class method", () => { + @injectable() + class ATestClass { } + + class BaseBTestClass { + static method( @inject(ATestClass) p1, @inject(ATestClass) p2) { } + } + class BTestClass extends BaseBTestClass { + static method( @inject(ATestClass) a) { } + } + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, BTestClass, 'method') + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('serviceType', ATestClass) + expect(metadata).to.have.property('parameterIndex', 0) + expect(metadata).to.have.property('type', 'inject') + }) + + // non static methods are not supported + // it("overrided class method no inject", () => { + // @injectable() + // class ATestClass { } + + // class BaseBTestClass { + // method( @inject(ATestClass) p1, @inject(ATestClass) p2) { } + // } + // class BTestClass extends BaseBTestClass { + // method() { } + // } + + // const value = getOverridableMetadata(PARAMETER_PARAMKEY, BTestClass, 'method') + // expect(value).to.undefined + // }) + + it("overrided class constructor no inject", () => { + @injectable() + class ATestClass { } + + class BaseBTestClass { + constructor( @inject(ATestClass) p1, @inject(ATestClass) p2) { } + } + class BTestClass extends BaseBTestClass { + constructor() { super(null, null) } + } + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, BTestClass, undefined) + // pass the base class params if there is not defined inject in the new ctor + // expect(value).to.undefined + + expect(value).to.have.lengthOf(2); + }) + + it("overrided static class method no inject", () => { + @injectable() + class ATestClass { } + + class BaseBTestClass { + static method( @inject(ATestClass) p1, @inject(ATestClass) p2) { } + } + class BTestClass extends BaseBTestClass { + static method() { } + } + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, BTestClass, 'method') + expect(value).to.undefined + }) + + it("not overrided class method", () => { + @injectable() + class ATestClass { } + + class BaseBTestClass { + method( @inject(ATestClass) a) { } + } + class BTestClass extends BaseBTestClass { + + } + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, BTestClass, 'method') + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('serviceType', ATestClass) + expect(metadata).to.have.property('parameterIndex', 0) + expect(metadata).to.have.property('type', 'inject') + }) + + it("not overrided class constructor", () => { + @injectable() + class ATestClass { } + + class BaseBTestClass { + constructor( @inject(ATestClass) a) { } + } + class BTestClass extends BaseBTestClass { + + } + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, BTestClass, undefined) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('serviceType', ATestClass) + expect(metadata).to.have.property('parameterIndex', 0) + expect(metadata).to.have.property('type', 'inject') + }) + + it("not overrided static class method", () => { + @injectable() + class ATestClass { } + + class BaseBTestClass { + static method( @inject(ATestClass) a) { } + } + class BTestClass extends BaseBTestClass { + + } + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, BTestClass, 'method') + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('serviceType', ATestClass) + expect(metadata).to.have.property('parameterIndex', 0) + expect(metadata).to.have.property('type', 'inject') + }) + + it("service inject", () => { + @injectable() + @environment('%ClassName%_defined_environment', 'value') + class ATestClass extends Service { } + class BTestClass { + method( @inject(ATestClass) a) { } + } + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, BTestClass, 'method') + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('serviceType', ATestClass) + expect(metadata).to.have.property('parameterIndex', 0) + expect(metadata).to.have.property('type', 'inject') + + const environmentMetadata = getMetadata(CLASS_ENVIRONMENTKEY, BTestClass) + expect(environmentMetadata).to.have + .property(`ATestClass_defined_environment`, 'value') + }) + }) + describe("param", () => { + it("param", () => { + class ParamClass { + method( @param name) { } + } + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, ParamClass, 'method') + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('from', 'name') + expect(metadata).to.have.property('parameterIndex', 0) + expect(metadata).to.have.property('type', 'param') + }) + it("custom name", () => { + class ParamClass { + method( @param('fullName') name) { } + } + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, ParamClass, 'method') + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('from', 'fullName') + expect(metadata).to.have.property('parameterIndex', 0) + expect(metadata).to.have.property('type', 'param') + }) + it("param index", () => { + class ParamClass { + method( @param name, @param fullName) { } + } + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, ParamClass, 'method') + + expect(value).to.have.lengthOf(2); + + const matadata1 = value[0] + + expect(matadata1).to.have.property('from', 'fullName') + expect(matadata1).to.have.property('parameterIndex', 1) + expect(matadata1).to.have.property('type', 'param') + + const matadata2 = value[1] + + expect(matadata2).to.have.property('from', 'name') + expect(matadata2).to.have.property('parameterIndex', 0) + expect(matadata2).to.have.property('type', 'param') + }) + it("with config", () => { + class ParamClass { + method( @param({ name: 'fullName', p1: 1, p2: 'p2' }) name) { } + } + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, ParamClass, 'method') + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('from', 'fullName') + expect(metadata).to.have.property('parameterIndex', 0) + expect(metadata).to.have.property('type', 'param') + expect(metadata).to.have.property('p1', 1) + expect(metadata).to.have.property('p2', 'p2') + }) + it("with config without name", () => { + class ParamClass { + method( @param({ p1: 1, p2: 'p2' }) shortName) { } + } + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, ParamClass, 'method') + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('from', 'shortName') + expect(metadata).to.have.property('parameterIndex', 0) + expect(metadata).to.have.property('type', 'param') + expect(metadata).to.have.property('p1', 1) + expect(metadata).to.have.property('p2', 'p2') + }) + + describe('createParameterDecorator', () => { + it("inject", () => { + + const myDecorator = createParameterDecorator('myDecorator') + + class ParamClass { + method( @myDecorator p1) { } + } + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, ParamClass, 'method') + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('from', 'p1') + expect(metadata).to.have.property('parameterIndex', 0) + expect(metadata).to.have.property('type', 'myDecorator') + }) + it("inject call", () => { + + const myDecorator = createParameterDecorator('myDecorator') + + class ParamClass { + method( @myDecorator() p1) { } + } + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, ParamClass, 'method') + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('from', 'p1') + expect(metadata).to.have.property('parameterIndex', 0) + expect(metadata).to.have.property('type', 'myDecorator') + }) + it("inject default config", () => { + const defaultConfig = { c: 1 } + const myDecorator = createParameterDecorator('myDecorator', defaultConfig) + + class ParamClass { + method( @myDecorator p1) { } + } + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, ParamClass, 'method') + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('from', 'p1') + expect(metadata).to.have.property('parameterIndex', 0) + expect(metadata).to.have.property('type', 'myDecorator') + expect(metadata).to.have.property('config').that.deep.equal(defaultConfig) + }) + it("inject call default config", () => { + const defaultConfig = { c: 1 } + const myDecorator = createParameterDecorator('myDecorator', defaultConfig) + + class ParamClass { + method( @myDecorator() p1) { } + } + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, ParamClass, 'method') + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('from', 'p1') + expect(metadata).to.have.property('parameterIndex', 0) + expect(metadata).to.have.property('type', 'myDecorator') + expect(metadata).to.have.property('config').that.deep.equal(defaultConfig) + }) + it("inject config", () => { + const defaultConfig = { c: 1 } + const myDecorator = createParameterDecorator('myDecorator', defaultConfig) + + class ParamClass { + method( @myDecorator({ d: 3 }) p1) { } + } + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, ParamClass, 'method') + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('from', 'p1') + expect(metadata).to.have.property('parameterIndex', 0) + expect(metadata).to.have.property('type', 'myDecorator') + expect(metadata).to.have.property('config').that.deep.equal({ c: 1, d: 3 }) + }) + it("inject config override", () => { + const defaultConfig = { c: 1 } + const myDecorator = createParameterDecorator('myDecorator', defaultConfig) + + class ParamClass { + method( @myDecorator({ c: 2 }) p1) { } + } + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, ParamClass, 'method') + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('from', 'p1') + expect(metadata).to.have.property('parameterIndex', 0) + expect(metadata).to.have.property('type', 'myDecorator') + expect(metadata).to.have.property('config').that.deep.equal({ c: 2 }) + }) + }) + + describe('request', () => { + it("inject", () => { + class ParamClass { + method( @request p1) { } + } + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, ParamClass, 'method') + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('from', 'p1') + expect(metadata).to.have.property('parameterIndex', 0) + expect(metadata).to.have.property('type', 'request') + }) + it("inject call", () => { + class ParamClass { + method( @request() p1) { } + } + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, ParamClass, 'method') + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('from', 'p1') + expect(metadata).to.have.property('parameterIndex', 0) + expect(metadata).to.have.property('type', 'request') + }) + }) + + describe('serviceParams', () => { + it("inject", () => { + class ParamClass { + method( @serviceParams p1) { } + } + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, ParamClass, 'method') + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('from', 'p1') + expect(metadata).to.have.property('parameterIndex', 0) + expect(metadata).to.have.property('type', 'serviceParams') + }) + it("inject call", () => { + class ParamClass { + method( @serviceParams() p1) { } + } + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, ParamClass, 'method') + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('from', 'p1') + expect(metadata).to.have.property('parameterIndex', 0) + expect(metadata).to.have.property('type', 'serviceParams') + }) + }) + + it("overrided class method", () => { + + class BaseParamClass { + method( @param p1, @param p2) { } + } + + class ParamClass extends BaseParamClass { + method( @param name) { } + } + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, ParamClass, 'method') + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('from', 'name') + expect(metadata).to.have.property('parameterIndex', 0) + expect(metadata).to.have.property('type', 'param') + }) + + it("overrided class constructor", () => { + + class BaseParamClass { + constructor( @param p1, @param p2) { } + } + + class ParamClass extends BaseParamClass { + constructor( @param name) { super(name, name) } + } + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, ParamClass, undefined) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('from', 'name') + expect(metadata).to.have.property('parameterIndex', 0) + expect(metadata).to.have.property('type', 'param') + }) + + it("overrided static class method", () => { + + class BaseParamClass { + static method( @param p1, @param p2) { } + } + + class ParamClass extends BaseParamClass { + static method( @param name) { } + } + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, ParamClass, 'method') + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('from', 'name') + expect(metadata).to.have.property('parameterIndex', 0) + expect(metadata).to.have.property('type', 'param') + }) + + // non static methods are not supported + // it("overrided class method no param", () => { + + // class BaseParamClass { + // method( @param p1, @param p2) { } + // } + + // class ParamClass extends BaseParamClass { + // method() { } + // } + + // const value = getOverridableMetadata(PARAMETER_PARAMKEY, ParamClass, 'method') + // expect(value).to.undefined + // }) + + + it("overrided class constructor no param", () => { + + class BaseParamClass { + constructor( @param p1, @param p2) { } + } + + class ParamClass extends BaseParamClass { + constructor() { super(null, null) } + } + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, ParamClass, undefined) + // pass the base class params if there is not defined inject in the new ctor + // expect(value).to.undefined + + expect(value).to.have.lengthOf(2); + }) + + it("overrided static class method no param", () => { + + class BaseParamClass { + static method( @param p1, @param p2) { } + } + + class ParamClass extends BaseParamClass { + static method() { } + } + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, ParamClass, 'method') + expect(value).to.undefined + }) + + it("not overrided class method", () => { + + class BaseParamClass { + method( @param name) { } + } + + class ParamClass extends BaseParamClass { + + } + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, ParamClass, 'method') + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('from', 'name') + expect(metadata).to.have.property('parameterIndex', 0) + expect(metadata).to.have.property('type', 'param') + }) + + it("not overrided class constructor", () => { + + class BaseParamClass { + constructor( @param name) { } + } + + class ParamClass extends BaseParamClass { + + } + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, ParamClass, undefined) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('from', 'name') + expect(metadata).to.have.property('parameterIndex', 0) + expect(metadata).to.have.property('type', 'param') + }) + + it("not overrided static class method", () => { + + class BaseParamClass { + static method( @param name) { } + } + + class ParamClass extends BaseParamClass { + + } + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, ParamClass, 'method') + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('from', 'name') + expect(metadata).to.have.property('parameterIndex', 0) + expect(metadata).to.have.property('type', 'param') + }) + }) + }) + + describe("service inject", () => { + it("environment param", () => { + + @environment('p1', 'v1') + @injectable() + class Service1 extends Service { + } + + class FSTest1 extends FunctionalService { + public async handle( @inject(Service1) s1) { } + } + + const environmentMetadata = getMetadata(CLASS_ENVIRONMENTKEY, FSTest1) + + expect(environmentMetadata).to.have.property('p1', 'v1') + }) + + it("dynamo param", () => { + @dynamo({ tableName: 't1', nativeConfig: { any: 'value' } }) + @injectable() + class DTable1 extends DynamoTable { } + + @injectable() + class Service1 extends Service { + public async handle( @inject(DTable1) table1) { } + } + + class FSTest1 extends FunctionalService { + public async handle( @inject(Service1) s1) { } + } + + const value = getMetadata(CLASS_DYNAMOTABLECONFIGURATIONKEY, FSTest1) + + expect(value).to.be.not.undefined; + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('tableName', 't1') + expect(metadata).to.have.property('nativeConfig').to.have.property('any', 'value') + }) + + it("dynamo param", () => { + @dynamoTable({ tableName: 't1', nativeConfig: { any: 'value' } }) + @injectable() + class DTable1 extends DynamoTable { } + + @injectable() + class Service1 extends Service { + public async handle( @inject(DTable1) table1) { } + } + + class FSTest1 extends FunctionalService { + public async handle( @inject(Service1) s1) { } + } + + const value = getMetadata(CLASS_DYNAMOTABLECONFIGURATIONKEY, FSTest1) + + expect(value).to.be.not.undefined; + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('tableName', 't1') + expect(metadata).to.have.property('nativeConfig').to.have.property('any', 'value') + }) + + it("s3 param", () => { + @s3Storage({ bucketName: 's3-bucket' }) + @injectable() + class S3Storage1 extends S3Storage { } + + @injectable() + class Service1 extends Service { + public async handle( @inject(S3Storage1) s1) { } + } + + class FSTest1 extends FunctionalService { + public async handle( @inject(Service1) s1) { } + } + + const value = getMetadata(CLASS_S3CONFIGURATIONKEY, FSTest1) + + expect(value).to.be.not.undefined; + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('bucketName', 's3-bucket') + }) + + it("dynamo param from api", () => { + @dynamoTable({ tableName: 't1', nativeConfig: { any: 'value' } }) + @injectable() + class DTable1 extends DynamoTable { } + + @injectable() + class Api1 extends Api { + public constructor( @inject(DTable1) private table1) { super() } + } + + @injectable() + class Service1 extends Service { + public async handle( @inject(Api1) a1) { } + } + + class FSTest1 extends FunctionalService { + public async handle( @inject(Service1) s1) { } + } + + const value = getMetadata(CLASS_DYNAMOTABLECONFIGURATIONKEY, FSTest1) + + expect(value).to.be.not.undefined; + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('tableName', 't1') + expect(metadata).to.have.property('nativeConfig').to.have.property('any', 'value') + }) + + it("dynamo param from api and service", () => { + @dynamoTable({ tableName: 't1', nativeConfig: { any: 'value' } }) + @injectable() + class DTable1 extends DynamoTable { } + + @injectable() + class Api1 extends Api { + public constructor( @inject(DTable1) private table1) { super() } + } + + @injectable() + class Service1 extends Service { + public async handle( @inject(Api1) a1, @inject(DTable1) table1) { } + } + + class FSTest1 extends FunctionalService { + public async handle( @inject(Service1) s1) { } + } + + const value = getMetadata(CLASS_DYNAMOTABLECONFIGURATIONKEY, FSTest1) + + expect(value).to.be.not.undefined; + expect(value).to.have.lengthOf(2); + + const metadata1 = value[0] + + expect(metadata1).to.have.property('tableName', 't1') + expect(metadata1).to.have.property('nativeConfig').to.have.property('any', 'value') + + const metadata2 = value[1] + + expect(metadata2).to.have.property('tableName', 't1') + expect(metadata2).to.have.property('nativeConfig').to.have.property('any', 'value') + }) + + it("dynamo param from api and service different", () => { + @dynamoTable({ tableName: 't1', nativeConfig: { any: 'value1' } }) + @injectable() + class DTable1 extends DynamoTable { } + + @dynamoTable({ tableName: 't2', nativeConfig: { any: 'value2' } }) + @injectable() + class DTable2 extends DynamoTable { } + + @injectable() + class Api1 extends Api { + public constructor( @inject(DTable1) private table1) { super() } + } + + @injectable() + class Service1 extends Service { + public async handle( @inject(Api1) a1, @inject(DTable2) table2) { } + } + + class FSTest1 extends FunctionalService { + public async handle( @inject(Service1) s1) { } + } + + const value = getMetadata(CLASS_DYNAMOTABLECONFIGURATIONKEY, FSTest1) + + expect(value).to.be.not.undefined; + expect(value).to.have.lengthOf(2); + + const metadata1 = value[0] + expect(metadata1).to.have.property('tableName', 't2') + expect(metadata1).to.have.property('nativeConfig').to.have.property('any', 'value2') + + const metadata2 = value[1] + expect(metadata2).to.have.property('tableName', 't1') + expect(metadata2).to.have.property('nativeConfig').to.have.property('any', 'value1') + }) + + it("environment variable chain", () => { + @environment('p1', 'v1') + @injectable() + class Api2 extends Api { } + + @injectable() + @environment('p2', 'v2') + class Api1 extends Api { + public constructor( @inject(Api2) private a2) { super() } + } + + @injectable() + @environment('p3', 'v3') + class Service1 extends Service { + public async handle( @inject(Api1) a1) { } + } + + @environment('p4', 'v4') + class FSTest1 extends FunctionalService { + public async handle( @inject(Service1) s1) { } + } + + + const api2Env = getMetadata(CLASS_ENVIRONMENTKEY, Api2) + expect(api2Env).to.have.all.keys('p1'); + expect(api2Env).to.deep.equal({ + p1: 'v1', + }) + + const api1Env = getMetadata(CLASS_ENVIRONMENTKEY, Api1) + expect(api1Env).to.have.all.keys('p1', 'p2'); + expect(api1Env).to.deep.equal({ + p1: 'v1', + p2: 'v2', + }) + + const service1Env = getMetadata(CLASS_ENVIRONMENTKEY, Service1) + expect(service1Env).to.have.all.keys('p1', 'p2', 'p3'); + expect(service1Env).to.deep.equal({ + p1: 'v1', + p2: 'v2', + p3: 'v3', + }) + + const fsTest1Env = getMetadata(CLASS_ENVIRONMENTKEY, FSTest1) + expect(fsTest1Env).to.have.all.keys('p1', 'p2', 'p3', 'p4'); + expect(fsTest1Env).to.deep.equal({ + p1: 'v1', + p2: 'v2', + p3: 'v3', + p4: 'v4', + }) + }) + }) +}) diff --git a/test/api.tests.ts b/test/api.tests.ts new file mode 100644 index 0000000..ed26d84 --- /dev/null +++ b/test/api.tests.ts @@ -0,0 +1,1060 @@ +import { expect } from 'chai' +import { FunctionalService, DynamoTable, DocumentClientApi, S3Storage, S3Api, SimpleNotificationService, SNSApi } from '../src/classes' +import { injectable, dynamoTable, inject, s3Storage, sns } from '../src/annotations' +import { container } from '../src/helpers/ioc' + +describe('api', () => { + describe('DynamoTable', () => { + afterEach(() => { + delete process.env.FUNCTIONAL_ENVIRONMENT + delete process.env.FUNCTIONAL_STAGE + delete process.env.DTClass_TABLE_NAME + delete process.env.DTCLASS_ENV_NAME + container.clearType(DocumentClientApi) + }) + + it("empty table name", (done) => { + let counter = 0 + + @injectable() + @dynamoTable() + class DTClass extends DynamoTable { } + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + process.env.FUNCTIONAL_STAGE = 'customstage' + process.env.DTClass_TABLE_NAME = 'DTClass-table' + class Home extends FunctionalService { + public static async handle( @inject(DTClass) a: DTClass) { + counter++ + + await a.put({ + Item: { _id: 1 } + }) + + return { ok: 1 } + } + } + + @injectable() + class DCL extends DocumentClientApi { + public async init() { } + public getDocumentClient() { + return { + put(params, cb) { + counter++ + try { + expect(params).to.deep.equal({ + Item: { _id: 1 }, + TableName: `DTClass-table-customstage` + }) + cb() + } catch (e) { + cb(e) + } + } + } + } + } + + container.registerType(DocumentClientApi, DCL) + + const invoker = Home.createInvoker() + invoker({}, { + send: (result) => { + expect(counter).to.equal(2) + expect(result).to.deep.equal({ ok: 1 }) + done() + } + }, (e) => { e && done(e) }) + .catch((e) => { e && done(e) }) + }) + + it("empty table name no env", (done) => { + let counter = 0 + + @injectable() + @dynamoTable() + class DTClass extends DynamoTable { } + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + process.env.FUNCTIONAL_STAGE = 'customstage' + class Home extends FunctionalService { + public static async handle( @inject(DTClass) a: DTClass) { + counter++ + + await a.put({ + Item: { _id: 1 } + }) + + return { ok: 1 } + } + } + + @injectable() + class DCL extends DocumentClientApi { + public async init() { } + public getDocumentClient() { + return { + put(params, cb) { + counter++ + try { + expect(params).to.deep.equal({ + Item: { _id: 1 }, + TableName: `DTClass-table-customstage` + }) + cb() + } catch (e) { + cb(e) + } + } + } + } + } + + container.registerType(DocumentClientApi, DCL) + + const invoker = Home.createInvoker() + invoker({}, { + send: (result) => { + expect(counter).to.equal(2) + expect(result).to.deep.equal({ ok: 1 }) + done() + } + }, (e) => { e && done(e) }) + .catch((e) => { e && done(e) }) + }) + + it("empty table name different env", (done) => { + let counter = 0 + + @injectable() + @dynamoTable() + class DTClass extends DynamoTable { } + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + process.env.FUNCTIONAL_STAGE = 'customstage' + process.env.DTClass_TABLE_NAME = 'DTClass-table-other' + class Home extends FunctionalService { + public static async handle( @inject(DTClass) a: DTClass) { + counter++ + + await a.put({ + Item: { _id: 1 } + }) + + return { ok: 1 } + } + } + + @injectable() + class DCL extends DocumentClientApi { + public async init() { } + public getDocumentClient() { + return { + put(params, cb) { + counter++ + try { + expect(params).to.deep.equal({ + Item: { _id: 1 }, + TableName: `DTClass-table-other-customstage` + }) + cb() + } catch (e) { + cb(e) + } + } + } + } + } + + container.registerType(DocumentClientApi, DCL) + + const invoker = Home.createInvoker() + invoker({}, { + send: (result) => { + expect(counter).to.equal(2) + expect(result).to.deep.equal({ ok: 1 }) + done() + } + }, (e) => { e && done(e) }) + .catch((e) => { e && done(e) }) + }) + + it("custom table name", (done) => { + let counter = 0 + + @injectable() + @dynamoTable({ tableName: 'ATable' }) + class DTClass extends DynamoTable { } + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + process.env.FUNCTIONAL_STAGE = 'customstage' + process.env.DTClass_TABLE_NAME = 'ATable' + class Home extends FunctionalService { + public static async handle( @inject(DTClass) a: DTClass) { + counter++ + + await a.put({ + Item: { _id: 1 } + }) + + return { ok: 1 } + } + } + + @injectable() + class DCL extends DocumentClientApi { + public async init() { } + public getDocumentClient() { + return { + put(params, cb) { + counter++ + try { + expect(params).to.deep.equal({ + Item: { _id: 1 }, + TableName: `ATable-customstage` + }) + cb() + } catch (e) { + cb(e) + } + } + } + } + } + + container.registerType(DocumentClientApi, DCL) + + const invoker = Home.createInvoker() + invoker({}, { + send: (result) => { + expect(counter).to.equal(2) + expect(result).to.deep.equal({ ok: 1 }) + done() + } + }, (e) => { e && done(e) }) + .catch((e) => { e && done(e) }) + }) + + it("custom environmentKey", (done) => { + let counter = 0 + + @injectable() + @dynamoTable({ environmentKey: 'DTCLASS_ENV_NAME' }) + class DTClass extends DynamoTable { } + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + process.env.FUNCTIONAL_STAGE = 'customstage' + process.env.DTCLASS_ENV_NAME = 'DTClass-table' + class Home extends FunctionalService { + public static async handle( @inject(DTClass) a: DTClass) { + counter++ + + await a.put({ + Item: { _id: 1 } + }) + + return { ok: 1 } + } + } + + @injectable() + class DCL extends DocumentClientApi { + public async init() { } + public getDocumentClient() { + return { + put(params, cb) { + counter++ + try { + expect(params).to.deep.equal({ + Item: { _id: 1 }, + TableName: `DTClass-table-customstage` + }) + cb() + } catch (e) { + cb(e) + } + } + } + } + } + + container.registerType(DocumentClientApi, DCL) + + const invoker = Home.createInvoker() + invoker({}, { + send: (result) => { + expect(counter).to.equal(2) + expect(result).to.deep.equal({ ok: 1 }) + done() + } + }, (e) => { e && done(e) }) + .catch((e) => { e && done(e) }) + }) + + it("existing table", (done) => { + let counter = 0 + + @injectable() + @dynamoTable({ tableName: 'custom-table-name', exists: true }) + class DTClass extends DynamoTable { } + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + process.env.FUNCTIONAL_STAGE = 'customstage' + process.env.DTClass_TABLE_NAME = 'custom-table-name' + class Home extends FunctionalService { + public static async handle( @inject(DTClass) a: DTClass) { + counter++ + + await a.put({ + Item: { _id: 1 } + }) + + return { ok: 1 } + } + } + + @injectable() + class DCL extends DocumentClientApi { + public async init() { } + public getDocumentClient() { + return { + put(params, cb) { + counter++ + try { + expect(params).to.deep.equal({ + Item: { _id: 1 }, + TableName: `custom-table-name` + }) + cb() + } catch (e) { + cb(e) + } + } + } + } + } + + container.registerType(DocumentClientApi, DCL) + + const invoker = Home.createInvoker() + invoker({}, { + send: (result) => { + expect(counter).to.equal(2) + expect(result).to.deep.equal({ ok: 1 }) + done() + } + }, (e) => { e && done(e) }) + .catch((e) => { e && done(e) }) + }) + }) + + describe('S3Storage', () => { + afterEach(() => { + delete process.env.FUNCTIONAL_ENVIRONMENT + delete process.env.FUNCTIONAL_STAGE + delete process.env.S3StorageClass_S3_BUCKET + delete process.env.S3StorageClass_ENV_NAME + container.clearType(S3Api) + }) + + it("empty table name", (done) => { + let counter = 0 + + @injectable() + @s3Storage() + class S3StorageClass extends S3Storage { } + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + process.env.FUNCTIONAL_STAGE = 'customstage' + process.env.S3StorageClass_S3_BUCKET = 's3storageclass-bucket' + class Home extends FunctionalService { + public static async handle( @inject(S3StorageClass) a: S3StorageClass) { + counter++ + + await a.getObject({ + Key: 'a' + }) + + return { ok: 1 } + } + } + + @injectable() + class S3A extends S3Api { + public async init() { } + public getS3() { + return { + getObject(params, cb) { + counter++ + try { + expect(params).to.deep.equal({ + Key: 'a', + Bucket: 's3storageclass-bucket-customstage' + }) + cb() + } catch (e) { + cb(e) + } + } + } + } + } + + container.registerType(S3Api, S3A) + + const invoker = Home.createInvoker() + invoker({}, { + send: (result) => { + expect(counter).to.equal(2) + expect(result).to.deep.equal({ ok: 1 }) + done() + } + }, (e) => { e && done(e) }) + .catch((e) => { e && done(e) }) + }) + + it("empty table name no env", (done) => { + let counter = 0 + + @injectable() + @s3Storage() + class S3StorageClass extends S3Storage { } + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + process.env.FUNCTIONAL_STAGE = 'customstage' + class Home extends FunctionalService { + public static async handle( @inject(S3StorageClass) a: S3StorageClass) { + counter++ + + await a.getObject({ + Key: 'a' + }) + + return { ok: 1 } + } + } + + @injectable() + class S3A extends S3Api { + public async init() { } + public getS3() { + return { + getObject(params, cb) { + counter++ + try { + expect(params).to.deep.equal({ + Key: 'a', + Bucket: 's3storageclass-bucket-customstage' + }) + cb() + } catch (e) { + cb(e) + } + } + } + } + } + + container.registerType(S3Api, S3A) + + const invoker = Home.createInvoker() + invoker({}, { + send: (result) => { + expect(counter).to.equal(2) + expect(result).to.deep.equal({ ok: 1 }) + done() + } + }, (e) => { e && done(e) }) + .catch((e) => { e && done(e) }) + }) + + it("empty table name different env", (done) => { + let counter = 0 + + @injectable() + @s3Storage() + class S3StorageClass extends S3Storage { } + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + process.env.FUNCTIONAL_STAGE = 'customstage' + process.env.S3StorageClass_S3_BUCKET = 's3storageclass-bucket-custom' + class Home extends FunctionalService { + public static async handle( @inject(S3StorageClass) a: S3StorageClass) { + counter++ + + await a.getObject({ + Key: 'a' + }) + + return { ok: 1 } + } + } + + @injectable() + class S3A extends S3Api { + public async init() { } + public getS3() { + return { + getObject(params, cb) { + counter++ + try { + expect(params).to.deep.equal({ + Key: 'a', + Bucket: 's3storageclass-bucket-custom-customstage' + }) + cb() + } catch (e) { + cb(e) + } + } + } + } + } + + container.registerType(S3Api, S3A) + + const invoker = Home.createInvoker() + invoker({}, { + send: (result) => { + expect(counter).to.equal(2) + expect(result).to.deep.equal({ ok: 1 }) + done() + } + }, (e) => { e && done(e) }) + .catch((e) => { e && done(e) }) + }) + + it("custom table name", (done) => { + let counter = 0 + + @injectable() + @s3Storage({ bucketName: 'custom-name' }) + class S3StorageClass extends S3Storage { } + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + process.env.FUNCTIONAL_STAGE = 'customstage' + process.env.S3StorageClass_S3_BUCKET = 'custom-name' + class Home extends FunctionalService { + public static async handle( @inject(S3StorageClass) a: S3StorageClass) { + counter++ + + await a.getObject({ + Key: 'a' + }) + + return { ok: 1 } + } + } + + @injectable() + class S3A extends S3Api { + public async init() { } + public getS3() { + return { + getObject(params, cb) { + counter++ + try { + expect(params).to.deep.equal({ + Key: 'a', + Bucket: 'custom-name-customstage' + }) + cb() + } catch (e) { + cb(e) + } + } + } + } + } + + container.registerType(S3Api, S3A) + + const invoker = Home.createInvoker() + invoker({}, { + send: (result) => { + expect(counter).to.equal(2) + expect(result).to.deep.equal({ ok: 1 }) + done() + } + }, (e) => { e && done(e) }) + .catch((e) => { e && done(e) }) + }) + + it("custom environmentKey", (done) => { + let counter = 0 + + @injectable() + @s3Storage({ environmentKey: 'S3StorageClass_ENV_NAME' }) + class S3StorageClass extends S3Storage { } + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + process.env.FUNCTIONAL_STAGE = 'customstage' + process.env.S3StorageClass_ENV_NAME = 's3storageclass-other' + class Home extends FunctionalService { + public static async handle( @inject(S3StorageClass) a: S3StorageClass) { + counter++ + + await a.getObject({ + Key: 'a' + }) + + return { ok: 1 } + } + } + + @injectable() + class S3A extends S3Api { + public async init() { } + public getS3() { + return { + getObject(params, cb) { + counter++ + try { + expect(params).to.deep.equal({ + Key: 'a', + Bucket: 's3storageclass-other-customstage' + }) + cb() + } catch (e) { + cb(e) + } + } + } + } + } + + container.registerType(S3Api, S3A) + + const invoker = Home.createInvoker() + invoker({}, { + send: (result) => { + expect(counter).to.equal(2) + expect(result).to.deep.equal({ ok: 1 }) + done() + } + }, (e) => { e && done(e) }) + .catch((e) => { e && done(e) }) + }) + + it("existing bucket", (done) => { + let counter = 0 + + @injectable() + @s3Storage({ bucketName: 'custom-bucket-name', exists: true }) + class S3StorageClass extends S3Storage { } + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + process.env.FUNCTIONAL_STAGE = 'customstage' + process.env.S3StorageClass_S3_BUCKET = 'custom-bucket-name' + class Home extends FunctionalService { + public static async handle( @inject(S3StorageClass) a: S3StorageClass) { + counter++ + + await a.getObject({ + Key: 'a' + }) + + return { ok: 1 } + } + } + + @injectable() + class S3A extends S3Api { + public async init() { } + public getS3() { + return { + getObject(params, cb) { + counter++ + try { + expect(params).to.deep.equal({ + Key: 'a', + Bucket: 'custom-bucket-name' + }) + cb() + } catch (e) { + cb(e) + } + } + } + } + } + + container.registerType(S3Api, S3A) + + const invoker = Home.createInvoker() + invoker({}, { + send: (result) => { + expect(counter).to.equal(2) + expect(result).to.deep.equal({ ok: 1 }) + done() + } + }, (e) => { e && done(e) }) + .catch((e) => { e && done(e) }) + }) + }) + + describe('SimpleNotificationService', () => { + afterEach(() => { + delete process.env.FUNCTIONAL_ENVIRONMENT + delete process.env.FUNCTIONAL_STAGE + delete process.env.SNSTopicClass_SNS_TOPICNAME + delete process.env.SNSTopicClass_SNS_TOPICNAME_ARN + delete process.env.SNSTopicClass_ENV_NAME + delete process.env.SNSTopicClass_ENV_NAME_ARN + container.clearType(SNSApi) + }) + + it("empty topic name", (done) => { + let counter = 0 + + @injectable() + @sns() + class SNSTopicClass extends SimpleNotificationService { } + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + process.env.FUNCTIONAL_STAGE = 'customstage' + process.env.SNSTopicClass_SNS_TOPICNAME = 'SNSTopicClass-topic123456789-customstage' + process.env.SNSTopicClass_SNS_TOPICNAME_ARN = 'arn:aws:sns:Z:1:SNSTopicClass-topic123456789-customstage' + class Home extends FunctionalService { + public static async handle( @inject(SNSTopicClass) a: SNSTopicClass) { + counter++ + + await a.publish({ + Message: 'message', + Subject: 'subject' + }) + + return { ok: 1 } + } + } + + @injectable() + class SNSA extends SNSApi { + public async init() { } + public getSNS() { + return { + publish(params, cb) { + counter++ + try { + expect(params).to.deep.equal({ + Message: 'message', + Subject: 'subject', + TopicArn: 'arn:aws:sns:Z:1:SNSTopicClass-topic123456789-customstage' + }) + cb() + } catch (e) { + cb(e) + } + } + } + } + } + + container.registerType(SNSApi, SNSA) + + const invoker = Home.createInvoker() + invoker({}, { + send: (result) => { + expect(counter).to.equal(2) + expect(result).to.deep.equal({ ok: 1 }) + done() + } + }, (e) => { e && done(e) }) + .catch((e) => { e && done(e) }) + }) + + it("empty topic name no env", (done) => { + let counter = 0 + + @injectable() + @sns() + class SNSTopicClass extends SimpleNotificationService { } + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + process.env.FUNCTIONAL_STAGE = 'customstage' + class Home extends FunctionalService { + public static async handle( @inject(SNSTopicClass) a: SNSTopicClass) { + counter++ + + await a.publish({ + Message: 'message', + Subject: 'subject' + }) + + return { ok: 1 } + } + } + + @injectable() + class SNSA extends SNSApi { + public async init() { } + public getSNS() { + return { + publish(params, cb) { + counter++ + try { + expect(params).to.deep.equal({ + Message: 'message', + Subject: 'subject', + TopicArn: '' + }) + cb() + } catch (e) { + cb(e) + } + } + } + } + } + + container.registerType(SNSApi, SNSA) + + const invoker = Home.createInvoker() + invoker({}, { + send: (result) => { + expect(counter).to.equal(2) + expect(result).to.deep.equal({ ok: 1 }) + done() + } + }, (e) => { e && done(e) }) + .catch((e) => { e && done(e) }) + }) + + it("empty topic name different env", (done) => { + let counter = 0 + + @injectable() + @sns() + class SNSTopicClass extends SimpleNotificationService { } + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + process.env.FUNCTIONAL_STAGE = 'customstage' + process.env.SNSTopicClass_SNS_TOPICNAME = 'SNSTopicClass-topic' + process.env.SNSTopicClass_SNS_TOPICNAME_ARN = 'arn:aws:sns:Z:1:SNSTopicClass-topic' + class Home extends FunctionalService { + public static async handle( @inject(SNSTopicClass) a: SNSTopicClass) { + counter++ + + await a.publish({ + Message: 'message', + Subject: 'subject' + }) + + return { ok: 1 } + } + } + + @injectable() + class SNSA extends SNSApi { + public async init() { } + public getSNS() { + return { + publish(params, cb) { + counter++ + try { + expect(params).to.deep.equal({ + Message: 'message', + Subject: 'subject', + TopicArn: 'arn:aws:sns:Z:1:SNSTopicClass-topic' + }) + cb() + } catch (e) { + cb(e) + } + } + } + } + } + + container.registerType(SNSApi, SNSA) + + const invoker = Home.createInvoker() + invoker({}, { + send: (result) => { + expect(counter).to.equal(2) + expect(result).to.deep.equal({ ok: 1 }) + done() + } + }, (e) => { e && done(e) }) + .catch((e) => { e && done(e) }) + }) + + it("custom topic name", (done) => { + let counter = 0 + + @injectable() + @sns({ topicName: 'custom-name' }) + class SNSTopicClass extends SimpleNotificationService { } + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + process.env.FUNCTIONAL_STAGE = 'customstage' + process.env.SNSTopicClass_SNS_TOPICNAME = 'custom-name123456789-customstage' + process.env.SNSTopicClass_SNS_TOPICNAME_ARN = 'arn:aws:sns:Z:1:custom-name123456789-customstage' + class Home extends FunctionalService { + public static async handle( @inject(SNSTopicClass) a: SNSTopicClass) { + counter++ + + await a.publish({ + Message: 'message', + Subject: 'subject' + }) + + return { ok: 1 } + } + } + + @injectable() + class SNSA extends SNSApi { + public async init() { } + public getSNS() { + return { + publish(params, cb) { + counter++ + try { + expect(params).to.deep.equal({ + Message: 'message', + Subject: 'subject', + TopicArn: 'arn:aws:sns:Z:1:custom-name123456789-customstage' + }) + cb() + } catch (e) { + cb(e) + } + } + } + } + } + + container.registerType(SNSApi, SNSA) + + const invoker = Home.createInvoker() + invoker({}, { + send: (result) => { + expect(counter).to.equal(2) + expect(result).to.deep.equal({ ok: 1 }) + done() + } + }, (e) => { e && done(e) }) + .catch((e) => { e && done(e) }) + }) + + it("custom environmentKey", (done) => { + let counter = 0 + + @injectable() + @sns({ environmentKey: 'SNSTopicClass_ENV_NAME' }) + class SNSTopicClass extends SimpleNotificationService { } + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + process.env.FUNCTIONAL_STAGE = 'customstage' + process.env.SNSTopicClass_ENV_NAME = 'SNSTopicClass-topic123456789-customstage' + process.env.SNSTopicClass_ENV_NAME_ARN = 'arn:aws:sns:Z:1:SNSTopicClass-topic123456789-customstage' + class Home extends FunctionalService { + public static async handle( @inject(SNSTopicClass) a: SNSTopicClass) { + counter++ + + await a.publish({ + Message: 'message', + Subject: 'subject' + }) + + return { ok: 1 } + } + } + + @injectable() + class SNSA extends SNSApi { + public async init() { } + public getSNS() { + return { + publish(params, cb) { + counter++ + try { + expect(params).to.deep.equal({ + Message: 'message', + Subject: 'subject', + TopicArn: 'arn:aws:sns:Z:1:SNSTopicClass-topic123456789-customstage' + }) + cb() + } catch (e) { + cb(e) + } + } + } + } + } + + container.registerType(SNSApi, SNSA) + + const invoker = Home.createInvoker() + invoker({}, { + send: (result) => { + expect(counter).to.equal(2) + expect(result).to.deep.equal({ ok: 1 }) + done() + } + }, (e) => { e && done(e) }) + .catch((e) => { e && done(e) }) + }) + + it("existing topic", (done) => { + let counter = 0 + + @injectable() + @sns({ topicName: 'SNSTopicClass-topic', exists: true }) + class SNSTopicClass extends SimpleNotificationService { } + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + process.env.FUNCTIONAL_STAGE = 'customstage' + process.env.SNSTopicClass_SNS_TOPICNAME = 'SNSTopicClass-topic' + process.env.SNSTopicClass_SNS_TOPICNAME_ARN = 'arn:aws:sns:Z:1:SNSTopicClass-topic' + class Home extends FunctionalService { + public static async handle( @inject(SNSTopicClass) a: SNSTopicClass) { + counter++ + + await a.publish({ + Message: 'message', + Subject: 'subject' + }) + + return { ok: 1 } + } + } + + @injectable() + class SNSA extends SNSApi { + public async init() { } + public getSNS() { + return { + publish(params, cb) { + counter++ + try { + expect(params).to.deep.equal({ + Message: 'message', + Subject: 'subject', + TopicArn: 'arn:aws:sns:Z:1:SNSTopicClass-topic' + }) + cb() + } catch (e) { + cb(e) + } + } + } + } + } + + container.registerType(SNSApi, SNSA) + + const invoker = Home.createInvoker() + invoker({}, { + send: (result) => { + expect(counter).to.equal(2) + expect(result).to.deep.equal({ ok: 1 }) + done() + } + }, (e) => { e && done(e) }) + .catch((e) => { e && done(e) }) + }) + }) +}) \ No newline at end of file diff --git a/test/hook.tests.ts b/test/hook.tests.ts new file mode 100644 index 0000000..0fd7901 --- /dev/null +++ b/test/hook.tests.ts @@ -0,0 +1,1761 @@ +import { expect } from 'chai' +import { FunctionalService, Service, Api, PreHook, PostHook, Resource, DynamoTable, SimpleNotificationService, S3Storage } from '../src/classes' +import { getOverridableMetadata, constants, getMetadata, getFunctionName } from '../src/annotations' +const { PARAMETER_PARAMKEY, CLASS_ENVIRONMENTKEY, CLASS_DYNAMOTABLECONFIGURATIONKEY, CLASS_SNSCONFIGURATIONKEY, CLASS_S3CONFIGURATIONKEY, + CLASS_MIDDLEWAREKEY } = constants +import { use, error, result, param, inject, injectable, environment, dynamoTable, sns, s3Storage, functionalServiceName, functionName } from '../src/annotations' + + +describe('hooks', () => { + beforeEach(() => { + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + }) + afterEach(() => { + delete process.env.FUNCTIONAL_ENVIRONMENT + }) + + describe('use annotation', () => { + describe('general', () => { + it("use", () => { + class TestHook extends PreHook { + public static async handle() { } + } + + @use(TestHook) + class BTestClass extends FunctionalService { + public static async handle() { } + } + + const value = getMetadata(CLASS_MIDDLEWAREKEY, BTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.equal(TestHook) + }) + it("use multiple param", () => { + class TestHook1 extends PreHook { + public static async handle() { } + } + class TestHook2 extends PreHook { + public static async handle() { } + } + class TestHook3 extends PreHook { + public static async handle() { } + } + + @use(TestHook1, TestHook2, TestHook3) + class BTestClass extends FunctionalService { + public static async handle() { } + } + + const value = getMetadata(CLASS_MIDDLEWAREKEY, BTestClass) + + expect(value).to.have.lengthOf(3); + + expect(value[0]).to.equal(TestHook1) + expect(value[1]).to.equal(TestHook2) + expect(value[2]).to.equal(TestHook3) + }) + it("multiple use", () => { + class TestHook1 extends PreHook { + public static async handle() { } + } + class TestHook2 extends PreHook { + public static async handle() { } + } + class TestHook3 extends PreHook { + public static async handle() { } + } + + @use(TestHook1) + @use(TestHook2) + @use(TestHook3) + class BTestClass extends FunctionalService { + public static async handle() { } + } + + const value = getMetadata(CLASS_MIDDLEWAREKEY, BTestClass) + + expect(value).to.have.lengthOf(3); + + expect(value[0]).to.equal(TestHook1) + expect(value[1]).to.equal(TestHook2) + expect(value[2]).to.equal(TestHook3) + }) + it("multiple use multiple param", () => { + class TestHook11 extends PreHook { + public static async handle() { } + } + class TestHook12 extends PreHook { + public static async handle() { } + } + class TestHook21 extends PreHook { + public static async handle() { } + } + class TestHook22 extends PreHook { + public static async handle() { } + } + class TestHook31 extends PreHook { + public static async handle() { } + } + class TestHook32 extends PreHook { + public static async handle() { } + } + + @use(TestHook11, TestHook12) + @use(TestHook21, TestHook22) + @use(TestHook31, TestHook32) + class BTestClass extends FunctionalService { + public static async handle() { } + } + + const value = getMetadata(CLASS_MIDDLEWAREKEY, BTestClass) + + expect(value).to.have.lengthOf(6); + + expect(value[0]).to.equal(TestHook11) + expect(value[1]).to.equal(TestHook12) + expect(value[2]).to.equal(TestHook21) + expect(value[3]).to.equal(TestHook22) + expect(value[4]).to.equal(TestHook31) + expect(value[5]).to.equal(TestHook32) + }) + }) + + describe('inject on hooks', () => { + + it("functional service inject", () => { + @injectable() + class ATestClass extends FunctionalService { } + + class TestHook extends PreHook { + public static async handle( @inject(ATestClass) a) { } + } + + @use(TestHook) + class BTestClass extends FunctionalService { + public static async handle() { } + } + + const environmentMetadata = getMetadata(CLASS_ENVIRONMENTKEY, BTestClass) + expect(environmentMetadata).to.have + .property(`FUNCTIONAL_SERVICE_${ATestClass.name.toUpperCase()}`, getFunctionName(ATestClass)) + }) + + it("service inject", () => { + @injectable() + @environment('%ClassName%_defined_environment', 'value') + class ATestClass extends Resource { } + + class TestHook extends PreHook { + public static async handle( @inject(ATestClass) a) { } + } + + @use(TestHook) + class BTestClass extends FunctionalService { + public static async handle() { } + } + + const environmentMetadata = getMetadata(CLASS_ENVIRONMENTKEY, BTestClass) + expect(environmentMetadata).to.have + .property(`ATestClass_defined_environment`, 'value') + }); + + it("injected DynamoTable", () => { + @injectable() + @dynamoTable({ tableName: 'ATable' }) + class ATestClass extends DynamoTable { } + + class TestHook extends PreHook { + public static async handle( @inject(ATestClass) a) { } + } + + @use(TestHook) + class BTestClass extends FunctionalService { + public static async handle() { } + } + + const value = getMetadata(CLASS_DYNAMOTABLECONFIGURATIONKEY, BTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('tableName', 'ATable') + }) + + it("injected SimpleNotificationService", () => { + @injectable() + @sns({ topicName: 'ATopic' }) + class ATestClass extends SimpleNotificationService { } + + class TestHook extends PreHook { + public static async handle( @inject(ATestClass) a) { } + } + + @use(TestHook) + class BTestClass extends FunctionalService { + public static async handle() { } + } + + const value = getMetadata(CLASS_SNSCONFIGURATIONKEY, BTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('topicName', 'ATopic') + }) + + it("injected S3Storage", () => { + @injectable() + @s3Storage({ bucketName: 'ABucket' }) + class ATestClass extends S3Storage { } + + class TestHook extends PreHook { + public static async handle( @inject(ATestClass) a) { } + } + + @use(TestHook) + class BTestClass extends FunctionalService { + public static async handle() { } + } + + const value = getMetadata(CLASS_S3CONFIGURATIONKEY, BTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('bucketName', 'abucket') + }) + }) + + describe('inject on level2 hooks', () => { + + it("functional service inject", () => { + @injectable() + class ATestClass extends FunctionalService { } + + class TestHookLevel2 extends PreHook { + public static async handle( @inject(ATestClass) a) { } + } + + @use(TestHookLevel2) + class TestHook extends PreHook { + public static async handle() { } + } + + @use(TestHook) + class BTestClass extends FunctionalService { + public static async handle() { } + } + + const environmentMetadata = getMetadata(CLASS_ENVIRONMENTKEY, BTestClass) + expect(environmentMetadata).to.have + .property(`FUNCTIONAL_SERVICE_${ATestClass.name.toUpperCase()}`, getFunctionName(ATestClass)) + }) + + it("service inject", () => { + @injectable() + @environment('%ClassName%_defined_environment', 'value') + class ATestClass extends Resource { } + + class TestHookLevel2 extends PreHook { + public static async handle( @inject(ATestClass) a) { } + } + + @use(TestHookLevel2) + class TestHook extends PreHook { + public static async handle() { } + } + + @use(TestHook) + class BTestClass extends FunctionalService { + public static async handle() { } + } + + const environmentMetadata = getMetadata(CLASS_ENVIRONMENTKEY, BTestClass) + expect(environmentMetadata).to.have + .property(`ATestClass_defined_environment`, 'value') + }); + + it("injected DynamoTable", () => { + @injectable() + @dynamoTable({ tableName: 'ATable' }) + class ATestClass extends DynamoTable { } + + class TestHookLevel2 extends PreHook { + public static async handle( @inject(ATestClass) a) { } + } + + @use(TestHookLevel2) + class TestHook extends PreHook { + public static async handle() { } + } + + @use(TestHook) + class BTestClass extends FunctionalService { + public static async handle() { } + } + + const value = getMetadata(CLASS_DYNAMOTABLECONFIGURATIONKEY, BTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('tableName', 'ATable') + }) + + it("injected SimpleNotificationService", () => { + @injectable() + @sns({ topicName: 'ATopic' }) + class ATestClass extends SimpleNotificationService { } + + class TestHookLevel2 extends PreHook { + public static async handle( @inject(ATestClass) a) { } + } + + @use(TestHookLevel2) + class TestHook extends PreHook { + public static async handle() { } + } + + @use(TestHook) + class BTestClass extends FunctionalService { + public static async handle() { } + } + + const value = getMetadata(CLASS_SNSCONFIGURATIONKEY, BTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('topicName', 'ATopic') + }) + + it("injected S3Storage", () => { + @injectable() + @s3Storage({ bucketName: 'ABucket' }) + class ATestClass extends S3Storage { } + + class TestHookLevel2 extends PreHook { + public static async handle( @inject(ATestClass) a) { } + } + + @use(TestHookLevel2) + class TestHook extends PreHook { + public static async handle() { } + } + + @use(TestHook) + class BTestClass extends FunctionalService { + public static async handle() { } + } + + const value = getMetadata(CLASS_S3CONFIGURATIONKEY, BTestClass) + + expect(value).to.have.lengthOf(1); + + const metadata = value[0] + + expect(metadata).to.have.property('bucketName', 'abucket') + }) + }) + }) + + describe('usage', () => { + + it('prehook', async () => { + let counter = 0 + class TestHook extends PreHook { + public static async handle() { + counter++ + expect(counter).to.equal(1) + } + } + + @use(TestHook) + class TestFunctionalService extends FunctionalService { + public static async handle() { + counter++ + expect(counter).to.equal(2) + } + } + + const invoker = TestFunctionalService.createInvoker() + await invoker({}, { + send: (result) => { + counter++ + expect(counter).to.equal(3) + } + }, (e) => { expect(true).to.equal(false, e.message) }) + + expect(counter).to.equal(3) + }) + + it('posthook', async () => { + let counter = 0 + class TestHook extends PostHook { + public static async handle() { + counter++ + expect(counter).to.equal(2) + } + } + + @use(TestHook) + class TestFunctionalService extends FunctionalService { + public static async handle() { + counter++ + expect(counter).to.equal(1) + } + } + + const invoker = TestFunctionalService.createInvoker() + await invoker({}, { + send: (result) => { + counter++ + expect(counter).to.equal(3) + } + }, (e) => { expect(true).to.equal(false, e.message) }) + + expect(counter).to.equal(3) + }) + + it('posthook result chain', async () => { + let counter = 0 + class TestPostHook1 extends PostHook { + public static async handle( @result result) { + counter++ + + return result + 1 + } + } + class TestPostHook2 extends PostHook { + public static async handle( @result result) { + counter++ + + return result + 1 + } + } + class TestPostHook3 extends PostHook { + public static async handle( @result result) { + counter++ + + return result + 1 + } + } + + @use(TestPostHook1) + @use(TestPostHook2) + @use(TestPostHook3) + class TestFunctionalService extends FunctionalService { + public static async handle() { + counter++ + expect(counter).to.equal(1) + + return 10 + } + } + + const invoker = TestFunctionalService.createInvoker() + await invoker({}, { + send: (result) => { + counter++ + expect(counter).to.equal(5) + + expect(result).to.equal(13) + } + }, (e) => { expect(true).to.equal(false, e.message) }) + + expect(counter).to.equal(5) + }) + + it('posthook catch error in service', async () => { + let counter = 0 + class TestHook extends PostHook { + public static async catch() { + counter++ + expect(counter).to.equal(2) + } + } + + @use(TestHook) + class TestFunctionalService extends FunctionalService { + public static async handle() { + counter++ + expect(counter).to.equal(1) + throw new Error('error') + } + } + + const invoker = TestFunctionalService.createInvoker() + await invoker({}, { + send: (result) => { + counter++ + expect(counter).to.equal(3) + } + }, (e) => { expect(true).to.equal(false, e.message) }) + + expect(counter).to.equal(3) + }) + + it('posthook catch error in service with prehook', async () => { + let counter = 0 + class TestPreHook extends PostHook { + public static async catch() { + counter++ + expect(counter).to.equal(1) + } + } + + class TestCatchHook extends PostHook { + public static async catch() { + counter++ + expect(counter).to.equal(3) + } + } + + @use(TestPreHook) + @use(TestCatchHook) + class TestFunctionalService extends FunctionalService { + public static async handle() { + counter++ + expect(counter).to.equal(2) + throw new Error('error') + } + } + + const invoker = TestFunctionalService.createInvoker() + await invoker({}, { + send: (result) => { + counter++ + expect(counter).to.equal(4) + } + }, (e) => { expect(true).to.equal(false, e.message) }) + + expect(counter).to.equal(4) + }) + + it('posthook catch error in prehook', async () => { + let counter = 0 + class TestHook extends PreHook { + public static async handle() { + counter++ + expect(counter).to.equal(1) + throw new Error('error') + } + } + class TestCatchHook extends PostHook { + public static async catch() { + counter++ + expect(counter).to.equal(2) + } + } + + @use(TestHook) + @use(TestCatchHook) + class TestFunctionalService extends FunctionalService { + public static async handle() { + expect(true).to.equal(false, 'have to skip this') + } + } + + const invoker = TestFunctionalService.createInvoker() + await invoker({}, { + send: (result) => { + counter++ + expect(counter).to.equal(3) + } + }, (e) => { expect(true).to.equal(false, e.message) }) + + expect(counter).to.equal(3) + }) + + it('posthook catch error in posthook', async () => { + let counter = 0 + class TestHook extends PreHook { + public static async handle() { + counter++ + expect(counter).to.equal(1) + } + } + class TestCatchHook extends PostHook { + public static async handle() { + counter++ + expect(counter).to.equal(3) + + throw new Error('error') + } + public static async catch() { + expect(true).to.equal(false, 'have to skip this') + } + } + + @use(TestHook) + @use(TestCatchHook) + class TestFunctionalService extends FunctionalService { + public static async handle() { + counter++ + expect(counter).to.equal(2) + } + } + + const invoker = TestFunctionalService.createInvoker() + await invoker({}, { + send: (result) => { + expect(true).to.equal(false, 'have to skip this') + } + }, (e) => { + counter++ + expect(counter).to.equal(4) + expect(e.message).to.equal('error') + }) + + expect(counter).to.equal(4) + }) + + it('posthook catch error in posthook handled', async () => { + let counter = 0 + class TestHook extends PreHook { + public static async handle() { + counter++ + expect(counter).to.equal(1) + } + } + class TestCatchHook extends PostHook { + public static async handle() { + counter++ + expect(counter).to.equal(3) + + throw new Error('error') + } + public static async catch() { + expect(true).to.equal(false, 'have to skip this') + } + } + class TestCatchHook2 extends PostHook { + public static async handle() { + expect(true).to.equal(false, 'have to skip this') + } + public static async catch() { + counter++ + expect(counter).to.equal(4) + + return { catch: 1 } + } + } + + @use(TestHook) + @use(TestCatchHook) + @use(TestCatchHook2) + class TestFunctionalService extends FunctionalService { + public static async handle() { + counter++ + expect(counter).to.equal(2) + + return { handle: 1 } + } + } + + const invoker = TestFunctionalService.createInvoker() + await invoker({}, { + send: (result) => { + counter++ + expect(counter).to.equal(5) + expect(result).to.deep.equal({ catch: 1 }) + } + }, (e) => { expect(true).to.equal(false, e.message) }) + + expect(counter).to.equal(5) + }) + + it('decorator resolution in hooks', async () => { + let counter = 0 + + @injectable() + class AuthHook extends PreHook { + public static async handle( @param authorization, @param Authorization) { + counter++ + const auth = authorization || Authorization + if (!auth) throw new Error('auth') + return 'me' + } + } + class PermissionHook extends PreHook { + public static async handle( @inject(AuthHook) identity) { + counter++ + expect(identity).to.equal('me') + } + } + class ResultHook extends PostHook { + public static async handle( @result result) { + counter++ + expect(result).to.deep.equal([{ a: 1 }, { a: 2 }, { a: 3 }]) + return result + } + async catch( @error e) { + expect(true).to.equal(false, e.message) + } + } + + @use(AuthHook) + @use(PermissionHook) + @use(ResultHook) + class TestFunctionalService extends FunctionalService { + public static async handle() { + counter++ + + return [{ a: 1 }, { a: 2 }, { a: 3 }] + } + } + + const invoker = TestFunctionalService.createInvoker() + const req = { + headers: { authorization: 'somevalue' } + } + await invoker(req, { + send: (result) => { + counter++ + } + }, (e) => { expect(true).to.equal(false, e.message) }) + + expect(counter).to.equal(5) + }) + + describe('hook on hooks', () => { + it('prehook', async () => { + let counter = 0 + + class TestHookLevel2 extends PreHook { + public static async handle() { + counter++ + expect(counter).to.equal(1) + } + } + + @use(TestHookLevel2) + class TestHook extends PreHook { + public static async handle() { + counter++ + expect(counter).to.equal(2) + } + } + + @use(TestHook) + class TestFunctionalService extends FunctionalService { + public static async handle() { + counter++ + expect(counter).to.equal(3) + } + } + + const invoker = TestFunctionalService.createInvoker() + await invoker({}, { + send: (result) => { + counter++ + expect(counter).to.equal(4) + } + }, (e) => { expect(true).to.equal(false, e.message) }) + + expect(counter).to.equal(4) + }) + + it('posthook', async () => { + let counter = 0 + + class TestHookLevel2 extends PostHook { + public static async handle() { + counter++ + expect(counter).to.equal(2) + } + } + + @use(TestHookLevel2) + class TestHook extends PreHook { + public static async handle() { + counter++ + expect(counter).to.equal(1) + } + } + + @use(TestHook) + class TestFunctionalService extends FunctionalService { + public static async handle() { + counter++ + expect(counter).to.equal(3) + } + } + + const invoker = TestFunctionalService.createInvoker() + await invoker({}, { + send: (result) => { + counter++ + expect(counter).to.equal(4) + } + }, (e) => { expect(true).to.equal(false, e.message) }) + + expect(counter).to.equal(4) + }) + + it('pre-posthook', async () => { + let counter = 0 + + class TestPreHookLevel2 extends PreHook { + public static async handle() { + counter++ + expect(counter).to.equal(1) + } + } + + class TestPostHookLevel2 extends PostHook { + public static async handle() { + counter++ + expect(counter).to.equal(3) + } + } + + @use(TestPreHookLevel2) + @use(TestPostHookLevel2) + class TestHook extends PreHook { + public static async handle() { + counter++ + expect(counter).to.equal(2) + } + } + + @use(TestHook) + class TestFunctionalService extends FunctionalService { + public static async handle() { + counter++ + expect(counter).to.equal(4) + } + } + + const invoker = TestFunctionalService.createInvoker() + await invoker({}, { + send: (result) => { + counter++ + expect(counter).to.equal(5) + } + }, (e) => { expect(true).to.equal(false, e.message) }) + + expect(counter).to.equal(5) + }) + + it('pre-posthook pre exception', async () => { + let counter = 0 + + class TestPreHookLevel2 extends PreHook { + public static async handle() { + counter++ + expect(counter).to.equal(1) + + throw new Error('error') + } + } + + class TestPostHookLevel2 extends PostHook { + public static async handle() { + expect(true).to.equal(false, 'have to skip this') + } + } + + @use(TestPreHookLevel2) + @use(TestPostHookLevel2) + class TestHook extends PreHook { + public static async handle() { + expect(true).to.equal(false, 'have to skip this') + } + } + + class TestCatchHook extends PostHook { + public static async catch( @error e) { + counter++ + expect(counter).to.equal(2) + expect(e.message).to.equal('error') + } + } + + @use(TestHook) + @use(TestCatchHook) + class TestFunctionalService extends FunctionalService { + public static async handle() { + expect(true).to.equal(false, 'have to skip this') + } + } + + const invoker = TestFunctionalService.createInvoker() + await invoker({}, { + send: (result) => { + counter++ + expect(counter).to.equal(3) + } + }, (e) => { expect(true).to.equal(false, e.message) }) + + expect(counter).to.equal(3) + }) + + it('pre-posthook exception', async () => { + let counter = 0 + + class TestPreHookLevel2 extends PreHook { + public static async handle() { + counter++ + expect(counter).to.equal(1) + } + } + + class TestPostHookLevel2 extends PostHook { + public static async handle() { + expect(true).to.equal(false, 'have to skip this') + } + } + + @use(TestPreHookLevel2) + @use(TestPostHookLevel2) + class TestHook extends PreHook { + public static async handle() { + counter++ + expect(counter).to.equal(2) + + throw new Error('error') + } + } + + class TestCatchHook extends PostHook { + public static async catch( @error e) { + counter++ + expect(counter).to.equal(3) + expect(e.message).to.equal('error') + } + } + + @use(TestHook) + @use(TestCatchHook) + class TestFunctionalService extends FunctionalService { + public static async handle() { + expect(true).to.equal(false, 'have to skip this') + } + } + + const invoker = TestFunctionalService.createInvoker() + await invoker({}, { + send: (result) => { + counter++ + expect(counter).to.equal(4) + } + }, (e) => { expect(true).to.equal(false, e.message) }) + + expect(counter).to.equal(4) + }) + + it('pre-posthook post exception', async () => { + let counter = 0 + + class TestPreHookLevel2 extends PreHook { + public static async handle() { + counter++ + expect(counter).to.equal(1) + } + } + + class TestPostHookLevel2 extends PostHook { + public static async handle() { + counter++ + expect(counter).to.equal(3) + + throw new Error('error') + } + } + + @use(TestPreHookLevel2) + @use(TestPostHookLevel2) + class TestHook extends PreHook { + public static async handle() { + counter++ + expect(counter).to.equal(2) + } + } + + class TestCatchHook extends PostHook { + public static async catch( @error e) { + counter++ + expect(counter).to.equal(4) + expect(e.message).to.equal('error') + } + } + + @use(TestHook) + @use(TestCatchHook) + class TestFunctionalService extends FunctionalService { + public static async handle() { + expect(true).to.equal(false, 'have to skip this') + } + } + + const invoker = TestFunctionalService.createInvoker() + await invoker({}, { + send: (result) => { + counter++ + expect(counter).to.equal(5) + } + }, (e) => { expect(true).to.equal(false, e.message) }) + + expect(counter).to.equal(5) + }) + }) + }) + + describe('hook decorators', () => { + it('prehook return => get value', async () => { + let counter = 0 + + @injectable() + class TestHook extends PreHook { + public static async handle() { + counter++ + expect(counter).to.equal(1) + return 'v1' + } + } + + @use(TestHook) + class TestFunctionalService extends FunctionalService { + public static async handle( @inject(TestHook) p1) { + counter++ + expect(counter).to.equal(2) + + expect(p1).to.equal('v1') + + return { ok: 1 } + } + } + + const invoker = TestFunctionalService.createInvoker() + await invoker({}, { + send: (result) => { + expect(result).to.deep.equal({ ok: 1 }) + counter++ + expect(counter).to.equal(3) + } + }, (e) => { expect(true).to.equal(false, e.message) }) + + expect(counter).to.equal(3) + }) + + it('prehook no return => get value', async () => { + let counter = 0 + + @injectable() + class TestHook extends PreHook { + public static async handle() { + counter++ + expect(counter).to.equal(1) + } + } + + @use(TestHook) + class TestFunctionalService extends FunctionalService { + public static async handle( @inject(TestHook) p1) { + counter++ + expect(counter).to.equal(2) + + expect(p1).to.null + + return { ok: 1 } + } + } + + const invoker = TestFunctionalService.createInvoker() + await invoker({}, { + send: (result) => { + expect(result).to.deep.equal({ ok: 1 }) + counter++ + expect(counter).to.equal(3) + } + }, (e) => { expect(true).to.equal(false, e.message) }) + + expect(counter).to.equal(3) + }) + + it('posthook result decorator', async () => { + let counter = 0 + + class TestHook extends PostHook { + public static async handle( @result res) { + counter++ + expect(counter).to.equal(2) + return { ...res, p1: 'p1' } + } + } + + @use(TestHook) + class TestFunctionalService extends FunctionalService { + public static async handle() { + counter++ + expect(counter).to.equal(1) + + return { ok: 1 } + } + } + + const invoker = TestFunctionalService.createInvoker() + await invoker({}, { + send: (result) => { + expect(result).to.deep.equal({ ok: 1, p1: 'p1' }) + counter++ + expect(counter).to.equal(3) + } + }, (e) => { expect(true).to.equal(false, e.message) }) + + expect(counter).to.equal(3) + }) + + it('posthook result decorator return result', async () => { + let counter = 0 + + class TestHook extends PostHook { + public static async handle( @result res) { + counter++ + expect(counter).to.equal(2) + return res + } + } + + class TestHook2 extends PostHook { + public static async catch( @error e) { + throw e + } + } + + @use(TestHook) + @use(TestHook2) + class TestFunctionalService extends FunctionalService { + public static async handle() { + counter++ + expect(counter).to.equal(1) + + return { ok: 1 } + } + } + + const invoker = TestFunctionalService.createInvoker() + await invoker({}, { + send: (result) => { + expect(result).to.deep.equal({ ok: 1 }) + counter++ + expect(counter).to.equal(3) + } + }, (e) => { expect(true).to.equal(false, e.message) }) + + expect(counter).to.equal(3) + }) + + it('posthook result decorator no handle', async () => { + let counter = 0 + + class TestHook extends PostHook { + } + + class TestHook2 extends PostHook { + public static async catch( @error e) { + throw e + } + } + + @use(TestHook) + @use(TestHook2) + class TestFunctionalService extends FunctionalService { + public static async handle() { + counter++ + expect(counter).to.equal(1) + + return { ok: 1 } + } + } + + const invoker = TestFunctionalService.createInvoker() + await invoker({}, { + send: (result) => { + expect(result).to.deep.equal({ ok: 1 }) + counter++ + expect(counter).to.equal(2) + } + }, (e) => { expect(true).to.equal(false, e.message) }) + + expect(counter).to.equal(2) + }) + + it('prehook inject in posthook handle', async () => { + let counter = 0 + + @injectable() + class TestPreHook extends PreHook { + public static async handle() { + counter++ + expect(counter).to.equal(1) + return 'p1' + } + } + + class TestPostHook extends PostHook { + public static async handle( @result res, @inject(TestPreHook) p1) { + counter++ + expect(counter).to.equal(3) + return { ...res, p1 } + } + } + + @use(TestPreHook) + @use(TestPostHook) + class TestFunctionalService extends FunctionalService { + public static async handle() { + counter++ + expect(counter).to.equal(2) + + return { ok: 1 } + } + } + + const invoker = TestFunctionalService.createInvoker() + await invoker({}, { + send: (result) => { + expect(result).to.deep.equal({ ok: 1, p1: 'p1' }) + counter++ + expect(counter).to.equal(4) + } + }, (e) => { expect(true).to.equal(false, e.message) }) + + expect(counter).to.equal(4) + }) + + it('prehook inject in posthook catch', async () => { + let counter = 0 + + @injectable() + class TestPreHook extends PreHook { + public static async handle() { + counter++ + expect(counter).to.equal(1) + return 'p1' + } + } + + class TestPostHook extends PostHook { + public static async catch( @result res, @inject(TestPreHook) p1) { + counter++ + expect(counter).to.equal(3) + return { ok: 1, p1 } + } + } + + @use(TestPreHook) + @use(TestPostHook) + class TestFunctionalService extends FunctionalService { + public static async handle() { + counter++ + expect(counter).to.equal(2) + + throw new Error('error') + } + } + + const invoker = TestFunctionalService.createInvoker() + await invoker({}, { + send: (result) => { + expect(result).to.deep.equal({ ok: 1, p1: 'p1' }) + counter++ + expect(counter).to.equal(4) + } + }, (e) => { expect(true).to.equal(false, e.message) }) + + expect(counter).to.equal(4) + }) + + it('multiple prehook set property => get param', async () => { + let counter = 0 + + @injectable() + class TestHook1 extends PreHook { + public static async handle() { + return 'v1' + } + } + + @injectable() + class TestHook2 extends PreHook { + public static async handle( @inject(TestHook1) p1) { + expect(p1).to.equal('v1') + + return 'v2' + } + } + + @injectable() + class TestHook3 extends PreHook { + public static async handle( @inject(TestHook1) p1, @inject(TestHook2) p2) { + expect(p1).to.equal('v1') + expect(p2).to.equal('v2') + + return 'v3' + } + } + + @use(TestHook1) + @use(TestHook2) + @use(TestHook3) + class TestFunctionalService extends FunctionalService { + public static async handle( @inject(TestHook1) p1, @inject(TestHook2) p2, @inject(TestHook3) p3) { + counter++ + + expect(p1).to.equal('v1') + expect(p2).to.equal('v2') + expect(p3).to.equal('v3') + + return { ok: 1 } + } + } + + const invoker = TestFunctionalService.createInvoker() + await invoker({}, { + send: (result) => { + expect(result).to.deep.equal({ ok: 1 }) + counter++ + } + }, (e) => { expect(true).to.equal(false, e.message) }) + + expect(counter).to.equal(2) + }) + + it('@error', async () => { + let counter = 0 + class TestHook extends PostHook { + public static async catch( @error e) { + counter++ + expect(e).instanceOf(Error) + + expect(e.message).to.deep.equal('custom error message') + } + } + + @use(TestHook) + class TestFunctionalService extends FunctionalService { + public static async handle() { + counter++ + throw new Error('custom error message') + } + } + + const invoker = TestFunctionalService.createInvoker() + await invoker({}, { + send: (result) => { + counter++ + expect(counter).to.equal(3) + } + }, (e) => { expect(true).to.equal(false, e.message) }) + + expect(counter).to.equal(3) + }) + + it("@functionalServiceName", async () => { + let counter = 0 + class TestHook extends PreHook { + public static async handle( @functionalServiceName serviceName) { + counter++ + expect(serviceName).to.equal('TestFunctionalService') + } + } + + @use(TestHook) + class TestFunctionalService extends FunctionalService { + public static async handle() { + counter++ + return { ok: 1 } + } + } + + const invoker = TestFunctionalService.createInvoker() + await invoker({}, { + send: (result) => { + expect(result).to.deep.equal({ ok: 1 }) + counter++ + expect(counter).to.equal(3) + } + }, (e) => { expect(true).to.equal(false, e.message) }) + + expect(counter).to.equal(3) + }) + + it("@functionalServiceName with functionName decorator", async () => { + let counter = 0 + class TestHook extends PreHook { + public static async handle( @functionalServiceName serviceName) { + counter++ + expect(serviceName).to.equal('MyTestFunctionalService') + } + } + + @use(TestHook) + @functionName('MyTestFunctionalService') + class TestFunctionalService extends FunctionalService { + public static async handle() { + counter++ + return { ok: 1 } + } + } + + const invoker = TestFunctionalService.createInvoker() + await invoker({}, { + send: (result) => { + expect(result).to.deep.equal({ ok: 1 }) + counter++ + expect(counter).to.equal(3) + } + }, (e) => { expect(true).to.equal(false, e.message) }) + + expect(counter).to.equal(3) + }) + }) + + describe('hook value deep', () => { + it('prehook value in service', async () => { + let counter = 0 + + @injectable() + class TestHook extends PreHook { + public static async handle() { + counter++ + expect(counter).to.equal(1) + return 'v1' + } + } + + @injectable() + class TestService extends Service { + public static async handle( @inject(TestHook) p1) { + counter++ + expect(counter).to.equal(3) + + expect(p1).to.equal('v1') + + return p1 + } + } + + @use(TestHook) + class TestFunctionalService extends FunctionalService { + public static async handle( @inject(TestService) testService) { + counter++ + expect(counter).to.equal(2) + + const ts = await testService() + + expect(ts).to.equal('v1') + + return { ok: 1 } + } + } + + const invoker = TestFunctionalService.createInvoker() + await invoker({}, { + send: (result) => { + expect(result).to.deep.equal({ ok: 1 }) + counter++ + expect(counter).to.equal(4) + } + }, (e) => { expect(true).to.equal(false, e.message) }) + + expect(counter).to.equal(4) + }) + + it('prehook value in service of service', async () => { + let counter = 0 + + @injectable() + class TestHook extends PreHook { + public static async handle() { + counter++ + expect(counter).to.equal(1) + return 'v1' + } + } + + @injectable() + class TestService2 extends Service { + public static async handle( @inject(TestHook) p1) { + counter++ + expect(counter).to.equal(4) + + expect(p1).to.equal('v1') + + return p1 + } + } + + @injectable() + class TestService extends Service { + public static async handle( @inject(TestService2) testService2) { + counter++ + expect(counter).to.equal(3) + + const p1 = await testService2() + + return p1 + } + } + + @use(TestHook) + class TestFunctionalService extends FunctionalService { + public static async handle( @inject(TestService) testService) { + counter++ + expect(counter).to.equal(2) + + const ts = await testService() + + expect(ts).to.equal('v1') + + return { ok: 1 } + } + } + + const invoker = TestFunctionalService.createInvoker() + await invoker({}, { + send: (result) => { + expect(result).to.deep.equal({ ok: 1 }) + counter++ + expect(counter).to.equal(5) + } + }, (e) => { expect(true).to.equal(false, e.message) }) + + expect(counter).to.equal(5) + }) + + it('prehook value in api of service', async () => { + let counter = 0 + + @injectable() + class TestHook extends PreHook { + public static async handle() { + counter++ + expect(counter).to.equal(1) + return 'v1' + } + } + + @injectable() + class TestApi2 extends Api { + public constructor( @inject(TestHook) p1) { + super() + + counter++ + expect(counter).to.equal(3) + + expect(p1).to.equal('v1') + } + } + + @injectable() + class TestService extends Service { + public static async handle( @inject(TestApi2) testApi) { + counter++ + expect(counter).to.equal(4) + } + } + + @use(TestHook) + class TestFunctionalService extends FunctionalService { + public static async handle( @inject(TestService) testService) { + counter++ + expect(counter).to.equal(2) + + await testService() + + return { ok: 1 } + } + } + + const invoker = TestFunctionalService.createInvoker() + await invoker({}, { + send: (result) => { + expect(result).to.deep.equal({ ok: 1 }) + counter++ + expect(counter).to.equal(5) + } + }, (e) => { expect(true).to.equal(false, e.message) }) + + expect(counter).to.equal(5) + }) + + it('prehook value in api', async () => { + let counter = 0 + + @injectable() + class TestHook extends PreHook { + public static async handle() { + counter++ + expect(counter).to.equal(1) + return 'v1' + } + } + + @injectable() + class TestApi extends Api { + public constructor( @inject(TestHook) p1) { + super() + + counter++ + expect(counter).to.equal(2) + + expect(p1).to.equal('v1') + } + } + + @use(TestHook) + class TestFunctionalService extends FunctionalService { + public static async handle( @inject(TestApi) testApi) { + counter++ + expect(counter).to.equal(3) + + return { ok: 1 } + } + } + + const invoker = TestFunctionalService.createInvoker() + await invoker({}, { + send: (result) => { + expect(result).to.deep.equal({ ok: 1 }) + counter++ + expect(counter).to.equal(4) + } + }, (e) => { expect(true).to.equal(false, e.message) }) + + expect(counter).to.equal(4) + }) + + it('prehook value in api of api', async () => { + let counter = 0 + + @injectable() + class TestHook extends PreHook { + public static async handle() { + counter++ + expect(counter).to.equal(1) + return 'v1' + } + } + + @injectable() + class TestApi2 extends Api { + public constructor( @inject(TestHook) p1) { + super() + + counter++ + expect(counter).to.equal(2) + + expect(p1).to.equal('v1') + } + } + + @injectable() + class TestApi extends Api { + public constructor( @inject(TestApi2) testApi2) { + super() + + counter++ + expect(counter).to.equal(3) + } + } + + @use(TestHook) + class TestFunctionalService extends FunctionalService { + public static async handle( @inject(TestApi) testApi) { + counter++ + expect(counter).to.equal(4) + + return { ok: 1 } + } + } + + const invoker = TestFunctionalService.createInvoker() + await invoker({}, { + send: (result) => { + expect(result).to.deep.equal({ ok: 1 }) + counter++ + expect(counter).to.equal(5) + } + }, (e) => { expect(true).to.equal(false, e.message) }) + + expect(counter).to.equal(5) + }) + + it('prehook value in service of api', async () => { + let counter = 0 + + @injectable() + class TestHook extends PreHook { + public static async handle() { + counter++ + expect(counter).to.equal(1) + return 'v1' + } + } + + @injectable() + class TestService extends Service { + public static async handle( @inject(TestHook) p1) { + counter++ + expect(counter).to.equal(4) + + expect(p1).to.equal('v1') + + return p1 + } + } + + @injectable() + class TestApi extends Api { + public constructor( @inject(TestService) private testService) { + super() + + counter++ + expect(counter).to.equal(2) + } + + public async init (){ + counter++ + expect(counter).to.equal(3) + + const p1 = await this.testService() + + expect(p1).to.equal('v1') + } + } + + @use(TestHook) + class TestFunctionalService extends FunctionalService { + public static async handle( @inject(TestApi) testApi) { + counter++ + expect(counter).to.equal(5) + + return { ok: 1 } + } + } + + const invoker = TestFunctionalService.createInvoker() + await invoker({}, { + send: (result) => { + expect(result).to.deep.equal({ ok: 1 }) + counter++ + expect(counter).to.equal(6) + } + }, (e) => { expect(true).to.equal(false, e.message) }) + + expect(counter).to.equal(6) + }) + }) +}) \ No newline at end of file diff --git a/test/invoke.tests.ts b/test/invoke.tests.ts new file mode 100644 index 0000000..51d9bd1 --- /dev/null +++ b/test/invoke.tests.ts @@ -0,0 +1,925 @@ +import { expect } from 'chai' + +import { AzureProvider } from '../src/providers/azure' +import { LocalProvider } from '../src/providers/local' +import { AWSProvider } from '../src/providers/aws' +import { addProvider, removeProvider } from '../src/providers' +import { FunctionalService } from '../src/classes/functionalService' +import { httpTrigger, inject, injectable, param, rest, createParameterDecorator } from '../src/annotations' + +describe('invoke', () => { + const FUNCTIONAL_ENVIRONMENT = 'custom' + const FUNCTIONAL_PROJECTNAME = 'my-test-project' + + describe('general', () => { + beforeEach(() => { + process.env.FUNCTIONAL_ENVIRONMENT = FUNCTIONAL_ENVIRONMENT + }) + afterEach(() => { + delete process.env.FUNCTIONAL_ENVIRONMENT + removeProvider(FUNCTIONAL_ENVIRONMENT) + }) + + it("no params", async () => { + let counter = 0 + + class TestProvider extends LocalProvider { + public async invoke(serviceType, params, invokeConfig?) { + counter++ + + expect(serviceType).to.equal(A) + expect(params).to.have.deep.equal({}) + expect(invokeConfig).is.deep.equal({ context: {} }) + } + } + addProvider(FUNCTIONAL_ENVIRONMENT, new TestProvider()) + + @injectable() + class A extends FunctionalService { + public static async handle() { } + } + + class B extends FunctionalService { + public static async handle( @inject(A) a) { + counter++ + const aResult = await a({}) + } + } + + const invoker = B.createInvoker() + await invoker({}, { + send: () => { counter++ } + }, (e) => { expect(false).to.equal(true, e.message) }) + + expect(counter).to.equal(3) + }) + + it("with params", async () => { + let counter = 0 + + class TestProvider extends LocalProvider { + public async invoke(serviceType, params, invokeConfig?) { + counter++ + + expect(serviceType).to.equal(A) + expect(params).to.have.deep.equal({ p1: 'p1' }) + expect(invokeConfig).is.deep.equal({ context: {} }) + } + } + addProvider(FUNCTIONAL_ENVIRONMENT, new TestProvider()) + + @injectable() + class A extends FunctionalService { + public static async handle( @param p1) { } + } + + class B extends FunctionalService { + public static async handle( @inject(A) a) { + counter++ + const aResult = await a({ p1: 'p1' }) + } + } + + const invoker = B.createInvoker() + await invoker({}, { + send: () => { counter++ } + }, (e) => { expect(false).to.equal(true, e.message) }) + + expect(counter).to.equal(3) + }) + + it("additional property that not listed as param", async () => { + let counter = 0 + + class TestProvider extends LocalProvider { + public async invoke(serviceType, params, invokeConfig?) { + counter++ + + expect(serviceType).to.equal(A) + expect(params).to.have.deep.equal({ p1: 'p1' }) + expect(invokeConfig).is.deep.equal({ context: {} }) + } + } + addProvider(FUNCTIONAL_ENVIRONMENT, new TestProvider()) + + @injectable() + class A extends FunctionalService { + public static async handle( @param p1) { } + } + + class B extends FunctionalService { + public static async handle( @inject(A) a) { + counter++ + const aResult = await a({ p1: 'p1', p2: 'p2' }) + } + } + + const invoker = B.createInvoker() + await invoker({}, { + send: () => { counter++ } + }, (e) => { expect(false).to.equal(true, e.message) }) + + expect(counter).to.equal(3) + }) + + it("invokeConfig", async () => { + let counter = 0 + + class TestProvider extends LocalProvider { + public async invoke(serviceType, params, invokeConfig?) { + counter++ + + expect(serviceType).to.equal(A) + expect(params).to.deep.equal({}) + expect(invokeConfig).to.deep.equal({ a: 1, b: 2, c: 3, context: {} }) + } + } + addProvider(FUNCTIONAL_ENVIRONMENT, new TestProvider()) + + @injectable() + class A extends FunctionalService { + public static async handle() { } + } + + class B extends FunctionalService { + public static async handle( @inject(A) a) { + counter++ + const aResult = await a({}, { a: 1, b: 2, c: 3 }) + } + } + + const invoker = B.createInvoker() + await invoker({}, { + send: () => { counter++ } + }, (e) => { expect(false).to.equal(true, e.message) }) + + expect(counter).to.equal(3) + }) + + it("unknown environment", async () => { + let counter = 0 + + class TestProvider extends LocalProvider { + public async invoke(serviceType, params, invokeConfig?) { + counter++ + + expect(false).to.equal(true, 'unknown provider have to select') + } + } + addProvider(FUNCTIONAL_ENVIRONMENT, new TestProvider()) + + @injectable() + class A extends FunctionalService { + public static async handle() { } + } + + class B extends FunctionalService { + public static async handle( @inject(A) a) { + counter++ + const aResult = await a({}) + } + } + + const invoker = B.createInvoker() + process.env.FUNCTIONAL_ENVIRONMENT = 'unknown' + await invoker({}, { + send: () => { expect(false).to.equal(true, 'error required') } + }, (e) => { + counter++ + expect(e.message).to.equal("missing environment: 'unknown' for invoke") + }) + + expect(counter).to.equal(2) + }) + + it("unknown environment mode", async () => { + let counter = 0 + + class TestProvider extends LocalProvider { + public async invoke(serviceType, params, invokeConfig?) { + counter++ + + expect(false).to.equal(true, 'unknown provider have to select') + } + } + addProvider(FUNCTIONAL_ENVIRONMENT, new TestProvider()) + + @injectable() + class A extends FunctionalService { + public static async handle() { } + } + + class B extends FunctionalService { + public static async handle( @inject(A) a) { + counter++ + const aResult = await a({}, { mode: 'unknown' }) + } + } + + const invoker = B.createInvoker() + await invoker({}, { + send: () => { expect(false).to.equal(true, 'error required') } + }, (e) => { + counter++ + expect(e.message).to.equal("missing environment: 'unknown' for invoke") + }) + + expect(counter).to.equal(2) + }) + + describe('addParameterDecoratorImplementation', () => { + describe('inheritance', () => { + it("get not defined", () => { + class A extends LocalProvider { } + class B extends A { } + + expect(LocalProvider.getParameterDecoratorImplementation('aa')).is.undefined + expect(A.getParameterDecoratorImplementation('aa')).is.undefined + expect(B.getParameterDecoratorImplementation('aa')).is.undefined + }) + it("get defined", () => { + class A extends LocalProvider { } + class B extends A { } + + const f1 = () => { } + A.addParameterDecoratorImplementation('aa', f1) + + expect(LocalProvider.getParameterDecoratorImplementation('aa')).is.undefined + expect(A.getParameterDecoratorImplementation('aa')).to.equal(f1) + expect(B.getParameterDecoratorImplementation('aa')).to.equal(f1) + }) + }) + + it("add to provider", async () => { + let counter = 0 + + class TestProvider extends LocalProvider { } + const testProviderInstance = new TestProvider() + addProvider(FUNCTIONAL_ENVIRONMENT, testProviderInstance) + TestProvider.addParameterDecoratorImplementation('myDecorator', (parameter, context, provider) => { + counter++ + + expect(parameter).to.have.property("from", 'p1') + expect(parameter).to.have.property("type", 'myDecorator') + expect(parameter).to.have.property("parameterIndex", 0) + + expect(context).to.have.property("event") + expect(context).to.have.nested.property("event.req") + expect(context).to.have.nested.property("event.res") + expect(context).to.have.nested.property("event.next") + + expect(provider).to.equal(testProviderInstance) + + return { instance: 1 } + }) + + const myDecorator = createParameterDecorator('myDecorator') + + class A extends FunctionalService { + public static async handle( @myDecorator p1) { + counter++ + + expect(p1).to.deep.equal({ instance: 1 }) + } + } + + const invoker = A.createInvoker() + await invoker({}, { + send: () => { counter++ } + }, (e) => { expect(false).to.equal(true, e.message) }) + + expect(counter).to.equal(3) + }) + it("add to provider base", async () => { + let counter = 0 + + class TestProviderBase extends LocalProvider { } + class TestProvider extends TestProviderBase { } + const testProviderInstance = new TestProvider() + addProvider(FUNCTIONAL_ENVIRONMENT, testProviderInstance) + TestProviderBase.addParameterDecoratorImplementation('myDecorator', (parameter, context, provider) => { + counter++ + + expect(parameter).to.have.property("from", 'p1') + expect(parameter).to.have.property("type", 'myDecorator') + expect(parameter).to.have.property("parameterIndex", 0) + + expect(context).to.have.property("event") + expect(context).to.have.nested.property("event.req") + expect(context).to.have.nested.property("event.res") + expect(context).to.have.nested.property("event.next") + + expect(provider).to.equal(testProviderInstance) + + return { instance: 1 } + }) + + const myDecorator = createParameterDecorator('myDecorator') + + class A extends FunctionalService { + public static async handle( @myDecorator p1) { + counter++ + + expect(p1).to.deep.equal({ instance: 1 }) + } + } + + const invoker = A.createInvoker() + await invoker({}, { + send: () => { counter++ } + }, (e) => { + expect(false).to.equal(true, e.message) + }) + + expect(counter).to.equal(3) + }) + + it("exception in decorator", async () => { + let counter = 0 + + class TestProvider extends LocalProvider { } + const testProviderInstance = new TestProvider() + addProvider(FUNCTIONAL_ENVIRONMENT, testProviderInstance) + TestProvider.addParameterDecoratorImplementation('myDecorator', (parameter, context, provider) => { + counter++ + + throw new Error('e1') + }) + + const myDecorator = createParameterDecorator('myDecorator') + + class A extends FunctionalService { + public static async handle( @myDecorator p1) { + expect(false).to.equal(true, 'error not catched') + } + } + + const invoker = A.createInvoker() + await invoker({}, { + send: () => { expect(false).to.equal(true, 'error not catched') } + }, (e) => { + counter++ + expect(e.message).to.equal('e1') + }) + + expect(counter).to.equal(2) + }) + }) + }) + + describe('azure', () => { + const FUNCTIONLY_FUNCTION_KEY = 'xxxxx' + const FUNCION_APP_BASEURL = 'http://example/api' + + beforeEach(() => { + process.env.FUNCTIONAL_ENVIRONMENT = FUNCTIONAL_ENVIRONMENT + process.env.FUNCTIONLY_FUNCTION_KEY = FUNCTIONLY_FUNCTION_KEY + process.env.FUNCION_APP_BASEURL = FUNCION_APP_BASEURL + }) + afterEach(() => { + delete process.env.FUNCTIONAL_ENVIRONMENT + delete process.env.FUNCTIONLY_FUNCTION_KEY + delete process.env.FUNCION_APP_BASEURL + removeProvider(FUNCTIONAL_ENVIRONMENT) + }) + + it("route get /v1/a1", async () => { + let counter = 0 + + class TestProvider extends AzureProvider { + public async invokeExec(config: any): Promise { + counter++ + + expect(config).to.have.property('method', 'get') + expect(config).to.have.property('url', `${FUNCION_APP_BASEURL}/v1/a1`) + expect(config).to.have.property('qs').that.deep.equal({ code: FUNCTIONLY_FUNCTION_KEY, p1: 'p1' }) + } + } + addProvider(FUNCTIONAL_ENVIRONMENT, new TestProvider()) + + @httpTrigger({ route: '/v1/a1', authLevel: 'function' }) + @injectable() + class A extends FunctionalService { + public static async handle( @param p1) { } + } + + class B extends FunctionalService { + public static async handle( @inject(A) a) { + counter++ + const aResult = await a({ p1: 'p1' }) + } + } + + const invoker = B.createInvoker() + const context: any = {} + await invoker(context, {}) + + expect(context).to.have.nested.property('res.status', 200, context.res.body) + expect(counter).to.equal(2) + }) + + it("route post /v1/a1", async () => { + let counter = 0 + + class TestProvider extends AzureProvider { + public async invokeExec(config: any): Promise { + counter++ + + expect(config).to.have.property('method', 'post') + expect(config).to.have.property('url', `${FUNCION_APP_BASEURL}/v1/a1`) + expect(config).to.have.property('qs').that.deep.equal({ code: FUNCTIONLY_FUNCTION_KEY }) + expect(config).to.have.property('body').that.deep.equal({ p1: 'p1' }) + expect(config).to.have.property('json', true) + + } + } + addProvider(FUNCTIONAL_ENVIRONMENT, new TestProvider()) + + @httpTrigger({ route: '/v1/a1', methods: ['post'], authLevel: 'function' }) + @injectable() + class A extends FunctionalService { + public static async handle( @param p1) { } + } + + class B extends FunctionalService { + public static async handle( @inject(A) a) { + counter++ + const aResult = await a({ p1: 'p1' }) + } + } + + const invoker = B.createInvoker() + const context: any = {} + await invoker(context, {}) + + expect(context).to.have.nested.property('res.status', 200, context.res.body) + expect(counter).to.equal(2) + }) + + it("multiple method get/post", async () => { + let counter = 0 + + class TestProvider extends AzureProvider { + public async invokeExec(config: any): Promise { + counter++ + + expect(config).to.have.property('method', 'get') + expect(config).to.have.property('url', `${FUNCION_APP_BASEURL}/v1/a1`) + expect(config).to.have.property('qs').that.deep.equal({ code: FUNCTIONLY_FUNCTION_KEY, p1: 'p1' }) + } + } + addProvider(FUNCTIONAL_ENVIRONMENT, new TestProvider()) + + @httpTrigger({ route: '/v1/a1', methods: ['get', 'post'], authLevel: 'function' }) + @injectable() + class A extends FunctionalService { + public static async handle( @param p1) { } + } + + class B extends FunctionalService { + public static async handle( @inject(A) a) { + counter++ + const aResult = await a({ p1: 'p1' }) + } + } + + const invoker = B.createInvoker() + const context: any = {} + await invoker(context, {}) + + expect(context).to.have.nested.property('res.status', 200, context.res.body) + expect(counter).to.equal(2) + }) + + it("multiple method post/get", async () => { + let counter = 0 + + class TestProvider extends AzureProvider { + public async invokeExec(config: any): Promise { + counter++ + + expect(config).to.have.property('method', 'post') + expect(config).to.have.property('url', `${FUNCION_APP_BASEURL}/v1/a1`) + expect(config).to.have.property('qs').that.deep.equal({ code: FUNCTIONLY_FUNCTION_KEY }) + expect(config).to.have.property('body').that.deep.equal({ p1: 'p1' }) + expect(config).to.have.property('json', true) + + } + } + addProvider(FUNCTIONAL_ENVIRONMENT, new TestProvider()) + + @httpTrigger({ route: '/v1/a1', methods: ['post', 'get'], authLevel: 'function' }) + @injectable() + class A extends FunctionalService { + public static async handle( @param p1) { } + } + + class B extends FunctionalService { + public static async handle( @inject(A) a) { + counter++ + const aResult = await a({ p1: 'p1' }) + } + } + + const invoker = B.createInvoker() + const context: any = {} + await invoker(context, {}) + + expect(context).to.have.nested.property('res.status', 200, context.res.body) + expect(counter).to.equal(2) + }) + + it("get anonymous", async () => { + let counter = 0 + + class TestProvider extends AzureProvider { + public async invokeExec(config: any): Promise { + counter++ + + expect(config).to.have.property('method', 'get') + expect(config).to.have.property('url', `${FUNCION_APP_BASEURL}/v1/a1`) + expect(config).to.have.property('qs').that.deep.equal({ p1: 'p1' }) + } + } + addProvider(FUNCTIONAL_ENVIRONMENT, new TestProvider()) + + @httpTrigger({ route: '/v1/a1', authLevel: 'anonymous' }) + @injectable() + class A extends FunctionalService { + public static async handle( @param p1) { } + } + + class B extends FunctionalService { + public static async handle( @inject(A) a) { + counter++ + const aResult = await a({ p1: 'p1' }) + } + } + + const invoker = B.createInvoker() + const context: any = {} + await invoker(context, {}) + + expect(context).to.have.nested.property('res.status', 200, context.res.body) + expect(counter).to.equal(2) + }) + + it("post anonymous", async () => { + let counter = 0 + + class TestProvider extends AzureProvider { + public async invokeExec(config: any): Promise { + counter++ + + expect(config).to.have.property('method', 'post') + expect(config).to.have.property('url', `${FUNCION_APP_BASEURL}/v1/a1`) + expect(config).to.not.have.property('qs') + expect(config).to.have.property('body').that.deep.equal({ p1: 'p1' }) + expect(config).to.have.property('json', true) + + } + } + addProvider(FUNCTIONAL_ENVIRONMENT, new TestProvider()) + + @httpTrigger({ route: '/v1/a1', methods: ['post'], authLevel: 'anonymous' }) + @injectable() + class A extends FunctionalService { + public static async handle( @param p1) { } + } + + class B extends FunctionalService { + public static async handle( @inject(A) a) { + counter++ + const aResult = await a({ p1: 'p1' }) + } + } + + const invoker = B.createInvoker() + const context: any = {} + await invoker(context, {}) + + expect(context).to.have.nested.property('res.status', 200, context.res.body) + expect(counter).to.equal(2) + }) + + it("authLevel function no function key", async () => { + let counter = 0 + delete process.env.FUNCTIONLY_FUNCTION_KEY + + class TestProvider extends AzureProvider { + public async invokeExec(config: any): Promise { + counter++ + + expect(config).to.have.property('method', 'get') + expect(config).to.have.property('url', `${FUNCION_APP_BASEURL}/v1/a1`) + expect(config).to.have.property('qs').that.deep.equal({ p1: 'p1' }) + } + } + addProvider(FUNCTIONAL_ENVIRONMENT, new TestProvider()) + + @httpTrigger({ route: '/v1/a1', authLevel: 'function' }) + @injectable() + class A extends FunctionalService { + public static async handle( @param p1) { } + } + + class B extends FunctionalService { + public static async handle( @inject(A) a) { + counter++ + const aResult = await a({ p1: 'p1' }) + } + } + + const invoker = B.createInvoker() + const context: any = {} + await invoker(context, {}) + + expect(context).to.have.nested.property('res.status', 500, context.res.body) + expect(context).to.have.nested.property('res.body').that.contains('process.env.FUNCTIONLY_FUNCTION_KEY is not set') + expect(counter).to.equal(1) + }) + + it("authLevel anonymous no function key", async () => { + let counter = 0 + delete process.env.FUNCTIONLY_FUNCTION_KEY + + class TestProvider extends AzureProvider { + public async invokeExec(config: any): Promise { + counter++ + + expect(config).to.have.property('method', 'get') + expect(config).to.have.property('url', `${FUNCION_APP_BASEURL}/v1/a1`) + expect(config).to.have.property('qs').that.deep.equal({ p1: 'p1' }) + } + } + addProvider(FUNCTIONAL_ENVIRONMENT, new TestProvider()) + + @httpTrigger({ route: '/v1/a1', authLevel: 'anonymous' }) + @injectable() + class A extends FunctionalService { + public static async handle( @param p1) { } + } + + class B extends FunctionalService { + public static async handle( @inject(A) a) { + counter++ + const aResult = await a({ p1: 'p1' }) + } + } + + const invoker = B.createInvoker() + const context: any = {} + await invoker(context, {}) + + expect(context).to.have.nested.property('res.status', 200, context.res.body) + expect(counter).to.equal(2) + }) + + }) + + describe('local', () => { + const APP_BASE_URL = 'http://localhost:3000' + const FUNCTIONAL_LOCAL_PORT = 3000 + + beforeEach(() => { + process.env.FUNCTIONAL_ENVIRONMENT = FUNCTIONAL_ENVIRONMENT + process.env.FUNCTIONAL_LOCAL_PORT = FUNCTIONAL_LOCAL_PORT + }) + afterEach(() => { + delete process.env.FUNCTIONAL_ENVIRONMENT + delete process.env.FUNCTIONAL_LOCAL_PORT + removeProvider(FUNCTIONAL_ENVIRONMENT) + }) + + it("route get /v1/a1", async () => { + let counter = 0 + + class TestProvider extends LocalProvider { + public async invokeExec(config: any): Promise { + counter++ + + expect(config).to.have.property('method', 'get') + expect(config).to.have.property('url', `${APP_BASE_URL}/v1/a1`) + expect(config).to.have.property('qs').that.deep.equal({ p1: 'p1' }) + } + } + addProvider(FUNCTIONAL_ENVIRONMENT, new TestProvider()) + + @rest({ path: '/v1/a1' }) + @injectable() + class A extends FunctionalService { + public static async handle( @param p1) { } + } + + class B extends FunctionalService { + public static async handle( @inject(A) a) { + counter++ + const aResult = await a({ p1: 'p1' }) + } + } + + const invoker = B.createInvoker() + await invoker({}, { + send: () => { counter++ } + }, (e) => { expect(false).to.equal(true, e.message) }) + + expect(counter).to.equal(3) + }) + + it("route post /v1/a1", async () => { + let counter = 0 + + class TestProvider extends LocalProvider { + public async invokeExec(config: any): Promise { + counter++ + + expect(config).to.have.property('method', 'post') + expect(config).to.have.property('url', `${APP_BASE_URL}/v1/a1`) + expect(config).to.have.not.property('qs') + expect(config).to.have.property('body').that.deep.equal({ p1: 'p1' }) + expect(config).to.have.property('json', true) + + } + } + addProvider(FUNCTIONAL_ENVIRONMENT, new TestProvider()) + + @rest({ path: '/v1/a1', methods: ['post'] }) + @injectable() + class A extends FunctionalService { + public static async handle( @param p1) { } + } + + class B extends FunctionalService { + public static async handle( @inject(A) a) { + counter++ + const aResult = await a({ p1: 'p1' }) + } + } + + const invoker = B.createInvoker() + await invoker({}, { + send: () => { counter++ } + }, (e) => { expect(false).to.equal(true, e.message) }) + + expect(counter).to.equal(3) + }) + + it("multiple method get/post", async () => { + let counter = 0 + + class TestProvider extends LocalProvider { + public async invokeExec(config: any): Promise { + counter++ + + expect(config).to.have.property('method', 'get') + expect(config).to.have.property('url', `${APP_BASE_URL}/v1/a1`) + expect(config).to.have.property('qs').that.deep.equal({ p1: 'p1' }) + } + } + addProvider(FUNCTIONAL_ENVIRONMENT, new TestProvider()) + + @rest({ path: '/v1/a1', methods: ['get', 'post'] }) + @injectable() + class A extends FunctionalService { + public static async handle( @param p1) { } + } + + class B extends FunctionalService { + public static async handle( @inject(A) a) { + counter++ + const aResult = await a({ p1: 'p1' }) + } + } + + const invoker = B.createInvoker() + await invoker({}, { + send: () => { counter++ } + }, (e) => { expect(false).to.equal(true, e.message) }) + + expect(counter).to.equal(3) + }) + + it("multiple method post/get", async () => { + let counter = 0 + + class TestProvider extends LocalProvider { + public async invokeExec(config: any): Promise { + counter++ + + expect(config).to.have.property('method', 'post') + expect(config).to.have.property('url', `${APP_BASE_URL}/v1/a1`) + expect(config).to.have.not.property('qs') + expect(config).to.have.property('body').that.deep.equal({ p1: 'p1' }) + expect(config).to.have.property('json', true) + + } + } + addProvider(FUNCTIONAL_ENVIRONMENT, new TestProvider()) + + @rest({ path: '/v1/a1', methods: ['post', 'get'] }) + @injectable() + class A extends FunctionalService { + public static async handle( @param p1) { } + } + + class B extends FunctionalService { + public static async handle( @inject(A) a) { + counter++ + const aResult = await a({ p1: 'p1' }) + } + } + + const invoker = B.createInvoker() + await invoker({}, { + send: () => { counter++ } + }, (e) => { expect(false).to.equal(true, e.message) }) + + expect(counter).to.equal(3) + }) + + it("method any", async () => { + let counter = 0 + + class TestProvider extends LocalProvider { + public async invokeExec(config: any): Promise { + counter++ + + expect(config).to.have.property('method', 'post') + expect(config).to.have.property('url', `${APP_BASE_URL}/v1/a1`) + expect(config).to.have.not.property('qs') + expect(config).to.have.property('body').that.deep.equal({ p1: 'p1' }) + expect(config).to.have.property('json', true) + + } + } + addProvider(FUNCTIONAL_ENVIRONMENT, new TestProvider()) + + @rest({ path: '/v1/a1', methods: ['any'] }) + @injectable() + class A extends FunctionalService { + public static async handle( @param p1) { } + } + + class B extends FunctionalService { + public static async handle( @inject(A) a) { + counter++ + const aResult = await a({ p1: 'p1' }) + } + } + + const invoker = B.createInvoker() + await invoker({}, { + send: () => { counter++ } + }, (e) => { expect(false).to.equal(true, e.message) }) + + expect(counter).to.equal(3) + }) + }) + + describe('aws', () => { + beforeEach(() => { + process.env.FUNCTIONAL_ENVIRONMENT = FUNCTIONAL_ENVIRONMENT + process.env.FUNCTIONAL_PROJECTNAME = FUNCTIONAL_PROJECTNAME + }) + afterEach(() => { + delete process.env.FUNCTIONAL_ENVIRONMENT + delete process.env.FUNCTIONAL_PROJECTNAME + removeProvider(FUNCTIONAL_ENVIRONMENT) + }) + + it("lambda call", async () => { + let counter = 0 + + class TestProvider extends AWSProvider { + public async invokeExec(config: any): Promise { + counter++ + + expect(config).to.have.property('FunctionName', `my-test-project-A`) + expect(config).to.have.property('Payload', JSON.stringify({ p1: 'p1' })) + } + } + addProvider(FUNCTIONAL_ENVIRONMENT, new TestProvider()) + + @injectable() + class A extends FunctionalService { + public static async handle( @param p1) { } + } + + class B extends FunctionalService { + public static async handle( @inject(A) a) { + counter++ + const aResult = await a({ p1: 'p1' }) + } + } + + const invoker = B.createInvoker() + + await invoker({}, {}, (e, result) => { + counter++ + expect(!e).to.equal(true, e && e.message) + }) + + expect(counter).to.equal(3) + }) + }) +}) \ No newline at end of file diff --git a/test/invoker.tests.ts b/test/invoker.tests.ts new file mode 100644 index 0000000..be87730 --- /dev/null +++ b/test/invoker.tests.ts @@ -0,0 +1,2690 @@ +import { expect } from 'chai' + +import { FunctionalService, Resource, Service, Api } from '../src/classes' +import { param, inject, injectable, serviceParams, request, functionalServiceName, functionName, provider, stage, InjectionScope } from '../src/annotations' +import { parse } from 'url' + + +describe('invoker', () => { + + describe("general", () => { + class MockService extends FunctionalService { } + + afterEach(() => { + delete process.env.FUNCTIONAL_ENVIRONMENT + }) + + it("Functional Service createInvoker", () => { + expect(MockService).to.has.property('createInvoker').that.to.be.a('function') + }) + + it("Service createInvoker", () => { + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + + const invoker = MockService.createInvoker() + expect(invoker).to.be.a('function') + }) + + it("inject service", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + + @injectable() + class CustomService extends Service { + public static async handle( @param p1, @param p2) { + counter++ + expect(p1).to.equal('p1', 'CustomService') + expect(p2).to.equal('p2', 'CustomService') + } + } + + class MockService extends FunctionalService { + public static async handle( @param noparam, @inject(CustomService) myService) { + counter++ + expect(noparam).to.undefined + expect(myService).to.instanceof(Function) + + await myService({ p1: 'p1', p2: 'p2' }) + } + } + + const invoker = MockService.createInvoker() + await invoker({}, { + send: () => { + expect(counter).to.equal(2) + } + }, (e) => { + expect(null).to.equal(e) + throw e + }) + + expect(counter).to.equal(2) + }) + + it("inject api with service", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + + @injectable() + class CustomService extends Service { + public static async handle( @param p1, @param p2) { + counter++ + expect(p1).to.equal('p1', 'CustomService') + expect(p2).to.equal('p2', 'CustomService') + } + } + + @injectable() + class CustomApi extends Api { + private _myService + public constructor( @inject(CustomService) myService) { + super() + this._myService = myService + } + public async myMethod(p1, p2) { + counter++ + expect(p1).to.equal('p1', 'CustomApi') + expect(p2).to.equal('p2', 'CustomApi') + + await this._myService({ p1, p2 }) + } + } + + class MockService extends FunctionalService { + public static async handle( @param p1, @inject(CustomApi) api) { + counter++ + expect(p1).to.undefined + expect(api).to.instanceof(CustomApi) + + await api.myMethod('p1', 'p2') + } + } + + const invoker = MockService.createInvoker() + await invoker({}, { + send: () => { + expect(counter).to.equal(3) + } + }, (e) => { + expect(null).to.equal(e) + throw e + }) + + expect(counter).to.equal(3) + }) + + it("inject async api with service", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + + @injectable() + class CustomService extends Service { + public static async handle( @param p1, @param p2) { + counter++ + expect(p1).to.equal('p1', 'CustomService') + expect(p2).to.equal('p2', 'CustomService') + } + } + + @injectable() + class CustomApi extends Api { + private _myService + public constructor( @inject(CustomService) myService) { + super() + this._myService = myService + } + + public init() { + counter++ + expect(counter).to.equal(1) + return new Promise(resolve => { + setTimeout(_ => { + counter++ + expect(counter).to.equal(2) + + resolve() + }, 10); + }) + } + + public async myMethod(p1, p2) { + counter++ + expect(p1).to.equal('p1', 'CustomApi') + expect(p2).to.equal('p2', 'CustomApi') + + await this._myService({ p1, p2 }) + } + } + + class MockService extends FunctionalService { + public static async handle( @param p1, @inject(CustomApi) api) { + counter++ + expect(counter).to.equal(3) + expect(p1).to.undefined + expect(api).to.instanceof(CustomApi) + + await api.myMethod('p1', 'p2') + } + } + + const invoker = MockService.createInvoker() + await invoker({}, { + send: () => { + expect(counter).to.equal(5) + } + }, (e) => { + expect(null).to.equal(e) + throw e + }) + + expect(counter).to.equal(5) + }) + + describe("injection modes", () => { + it("multiple inject transient api with a service", async () => { + let counter = 0 + let instanceCreation = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + + @injectable() + class CustomService extends Service { + public constructor() { + super() + instanceCreation++ + } + public static async handle( @param p1, @param p2) { + counter++ + expect(p1).to.equal('p1', 'CustomService') + expect(p2).to.equal('p2', 'CustomService') + } + } + + @injectable(InjectionScope.Transient) + class CustomApi extends Api { + private _myService + public constructor( @inject(CustomService) myService, @inject(CustomService) myService2) { + super() + instanceCreation++ + this._myService = myService + } + public async myMethod(p1, p2) { + counter++ + expect(p1).to.equal('p1', 'CustomApi') + expect(p2).to.equal('p2', 'CustomApi') + + await this._myService({ p1, p2 }) + } + } + + class MockService extends FunctionalService { + public static async handle( @param p1, @inject(CustomApi) api, @inject(CustomApi) api2) { + counter++ + expect(p1).to.undefined + expect(api).to.instanceof(CustomApi) + + await api.myMethod('p1', 'p2') + } + } + + const invoker = MockService.createInvoker() + await invoker({}, { + send: () => { + expect(counter).to.equal(3) + expect(instanceCreation).to.equal(2) + } + }, (e) => { + expect(null).to.equal(e) + throw e + }) + + expect(counter).to.equal(3) + expect(instanceCreation).to.equal(2) + }) + + it("multiple inject singleton api with a service", async () => { + let counter = 0 + let instanceCreation = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + + @injectable(InjectionScope.Transient) + class CustomService extends Service { + public constructor() { + super() + instanceCreation++ + } + public static async handle( @param p1, @param p2) { + counter++ + expect(p1).to.equal('p1', 'CustomService') + expect(p2).to.equal('p2', 'CustomService') + } + } + + @injectable(InjectionScope.Singleton) + class CustomApi extends Api { + private _myService + public constructor( @inject(CustomService) myService, @inject(CustomService) myService2) { + super() + instanceCreation++ + this._myService = myService + } + public async myMethod(p1, p2) { + counter++ + expect(p1).to.equal('p1', 'CustomApi') + expect(p2).to.equal('p2', 'CustomApi') + + await this._myService({ p1, p2 }) + } + } + + class MockService extends FunctionalService { + public static async handle( @param p1, @inject(CustomApi) api, @inject(CustomApi) api2) { + counter++ + expect(p1).to.undefined + expect(api).to.instanceof(CustomApi) + + await api.myMethod('p1', 'p2') + } + } + + const invoker = MockService.createInvoker() + await invoker({}, { + send: () => { + expect(counter).to.equal(3) + expect(instanceCreation).to.equal(1) + } + }, (e) => { + expect(null).to.equal(e) + throw e + }) + + expect(counter).to.equal(3) + expect(instanceCreation).to.equal(1) + }) + }) + }) + + describe("local", () => { + + afterEach(() => { + delete process.env.FUNCTIONAL_ENVIRONMENT + delete process.env.FUNCTIONAL_STAGE + }) + + it("invoke", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + class MockService extends FunctionalService { + static handle() { + counter++ + } + } + + const invoker = MockService.createInvoker() + invoker({}, { + send: () => { + expect(counter).to.equal(1) + done() + } + }, (e) => { e && done(e) }) + }) + + it("return value", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + class MockService extends FunctionalService { + static handle() { + counter++ + return { ok: 1 } + } + } + + const invoker = MockService.createInvoker() + invoker({}, { + send: (result) => { + expect(counter).to.equal(1) + expect(result).to.deep.equal({ ok: 1 }) + done() + } + }, (e) => { e && done(e) }) + }) + + it("handler throw error", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + class MockService extends FunctionalService { + static handle() { + counter++ + throw new Error('error in handle') + } + } + + const invoker = MockService.createInvoker() + invoker({}, { + send: (result) => { + expect(false).to.equal(true, 'skippable code') + done() + } + }, (e) => { + expect(counter).to.equal(1) + expect(e.message).to.equal('error in handle') + done() + }) + }) + + it("query param", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + class MockService extends FunctionalService { + static handle( @param p1, @param p2) { + counter++ + expect(p1).to.equal('v1') + expect(p2).to.equal('v2') + } + } + + const invoker = MockService.createInvoker() + const req = { + query: { + p1: 'v1', + p2: 'v2' + } + } + invoker(req, { + send: () => { + expect(counter).to.equal(1) + done() + } + }, (e) => { e && done(e) }) + }) + + it("body param", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + class MockService extends FunctionalService { + static handle( @param p1, @param p2) { + counter++ + expect(p1).to.equal('v1') + expect(p2).to.equal('v2') + } + } + + const invoker = MockService.createInvoker() + const req = { + body: { + p1: 'v1', + p2: 'v2' + } + } + invoker(req, { + send: () => { + expect(counter).to.equal(1) + done() + } + }, (e) => { e && done(e) }) + }) + + it("params param", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + class MockService extends FunctionalService { + static handle( @param p1, @param p2) { + counter++ + expect(p1).to.equal('v1') + expect(p2).to.equal('v2') + } + } + + const invoker = MockService.createInvoker() + const req = { + params: { + p1: 'v1', + p2: 'v2' + } + } + invoker(req, { + send: () => { + expect(counter).to.equal(1) + done() + } + }, (e) => { e && done(e) }) + }) + + it("headers param", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + class MockService extends FunctionalService { + static handle( @param p1, @param p2) { + counter++ + expect(p1).to.equal('v1') + expect(p2).to.equal('v2') + } + } + + const invoker = MockService.createInvoker() + const req = { + headers: { + p1: 'v1', + p2: 'v2' + } + } + invoker(req, { + send: () => { + expect(counter).to.equal(1) + done() + } + }, (e) => { e && done(e) }) + }) + + it("missing param", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + class MockService extends FunctionalService { + static handle( @param p1, @param p2) { + counter++ + expect(p1).to.equal('v1') + expect(p2).to.undefined + } + } + + const invoker = MockService.createInvoker() + const req = { + params: { + p1: 'v1' + } + } + invoker(req, { + send: () => { + expect(counter).to.equal(1) + done() + } + }, (e) => { e && done(e) }) + }) + + it("params resolve order", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + class MockService extends FunctionalService { + static handle( @param p1, @param p2, @param p3, @param p4) { + counter++ + expect(p1).to.equal('body') + expect(p2).to.equal('query') + expect(p3).to.equal('params') + expect(p4).to.equal('headers') + } + } + + const invoker = MockService.createInvoker() + const req = { + body: { + p1: 'body' + }, + query: { + p1: 'query', + p2: 'query' + }, + params: { + p1: 'params', + p2: 'params', + p3: 'params' + }, + headers: { + p1: 'headers', + p2: 'headers', + p3: 'headers', + p4: 'headers' + } + + } + invoker(req, { + send: () => { + expect(counter).to.equal(1) + done() + } + }, (e) => { e && done(e) }) + }) + + it("params resolve hint", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + class MockService extends FunctionalService { + static handle( @param({ source: 'event.req.query' }) p1, @param({ source: 'event.req.params' }) p2, @param({ source: 'event.req.headers' }) p3, @param p4) { + counter++ + expect(p1).to.equal('query') + expect(p2).to.equal('params') + expect(p3).to.equal('headers') + expect(p4).to.equal('headers') + } + } + + const invoker = MockService.createInvoker() + const req = { + body: { + p1: 'body' + }, + query: { + p1: 'query', + p2: 'query' + }, + params: { + p1: 'params', + p2: 'params', + p3: 'params' + }, + headers: { + p1: 'headers', + p2: 'headers', + p3: 'headers', + p4: 'headers' + } + + } + invoker(req, { + send: () => { + expect(counter).to.equal(1) + done() + } + }, (e) => { e && done(e) }) + }) + + it("inject param", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + + @injectable() + class MockInjectable extends Resource { } + + class MockService extends FunctionalService { + static handle( @param p1, @inject(MockInjectable) p2) { + counter++ + expect(p1).to.undefined + expect(p2).to.instanceof(Resource) + expect(p2).to.instanceof(MockInjectable) + } + } + + const invoker = MockService.createInvoker() + invoker({}, { + send: () => { + expect(counter).to.equal(1) + done() + } + }, (e) => { e && done(e) }) + }) + + it("inject service", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + + @injectable() + class CustomService extends Service { + static handle( @param p1, @param p2) { + counter++ + expect(p1).to.equal('p1') + expect(p2).to.equal('p2') + } + } + + class MockService extends FunctionalService { + static handle( @param p1, @inject(CustomService) p2) { + counter++ + expect(p1).to.undefined + expect(p2).to.instanceof(Function) + + p2({ p1: 'p1', p2: 'p2' }) + } + } + + const invoker = MockService.createInvoker() + invoker({}, { + send: () => { + expect(counter).to.equal(2) + done() + } + }, (e) => { e && done(e) }) + }) + + + it("serviceParams param", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + + @injectable() + class MockInjectable extends Resource { } + + const req = {} + const res = { + send: () => { + expect(counter).to.equal(1) + done() + } + } + const next = (e) => { e && done(e) } + + class MockService extends FunctionalService { + static handle( @param p1, @serviceParams p2) { + counter++ + expect(p1).to.undefined + expect(p2).to.have.property('req').that.to.equal(req) + expect(p2).to.have.property('res').that.to.equal(res) + expect(p2).to.have.property('next').that.to.equal(next) + } + } + + const invoker = MockService.createInvoker() + invoker(req, res, next) + }) + + it("request originalUrl param", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + + @injectable() + class MockInjectable extends Resource { } + + const req = { + originalUrl: '/a/b', + method: 'GET', + body: { + p1: 'body' + }, + query: { + p2: 'query' + }, + params: { + p3: 'params' + }, + headers: { + p4: 'headers' + }, + anyprop: {} + } + const res = { + send: () => { + expect(counter).to.equal(1) + done() + } + } + const next = (e) => { e && done(e) } + + class MockService extends FunctionalService { + static handle( @param p1, @request r) { + counter++ + expect(r).to.have.property('url').that.deep.equal(parse(req.originalUrl)) + expect(r).to.have.property('method', req.method) + expect(r).to.have.property('body').that.deep.equal(req.body) + expect(r).to.have.property('query').that.deep.equal(req.query) + expect(r).to.have.property('params').that.deep.equal(req.params) + expect(r).to.have.property('headers').that.deep.equal(req.headers) + expect(r).to.not.have.property('anyprop') + } + } + + const invoker = MockService.createInvoker() + invoker(req, res, next) + }) + + it("request _parsedUrl param", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + + @injectable() + class MockInjectable extends Resource { } + + const req = { + originalUrl: '/a/b?a=b', + _parsedUrl: parse('/b/c?a=b2'), + method: 'GET', + body: { + p1: 'body' + }, + query: { + p2: 'query' + }, + params: { + p3: 'params' + }, + headers: { + p4: 'headers' + }, + anyprop: {} + } + const res = { + send: () => { + expect(counter).to.equal(1) + done() + } + } + const next = (e) => { e && done(e) } + + class MockService extends FunctionalService { + static handle( @param p1, @request r) { + counter++ + expect(r).to.have.property('url').that.deep.equal(req._parsedUrl) + expect(r).to.have.property('method', req.method) + expect(r).to.have.property('body').that.deep.equal(req.body) + expect(r).to.have.property('query').that.deep.equal(req.query) + expect(r).to.have.property('params').that.deep.equal(req.params) + expect(r).to.have.property('headers').that.deep.equal(req.headers) + expect(r).to.not.have.property('anyprop') + } + } + + const invoker = MockService.createInvoker() + invoker(req, res, next) + }) + + it("generic result format", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + + @injectable() + class MockInjectable extends Resource { } + + const req = { + } + const res = { + send: (data) => { + counter++ + expect(data).to.deep.equal({ + ok: 1 + }) + }, + status: (code) => { + counter++ + expect(code).to.equal(201) + }, + set: (headers) => { + counter++ + expect(headers).to.deep.equal({ + 'content-type': 'application/json' + }) + } + } + const next = (e) => { expect(true).to.equal(false, e.message) } + + class MockService extends FunctionalService { + static handle() { + counter++ + + return { + status: 201, + headers: { + 'content-type': 'application/json' + }, + data: { + ok: 1 + } + } + } + } + + const invoker = MockService.createInvoker() + await invoker(req, res, next) + + expect(counter).to.equal(4) + }) + + it("functionalServiceName", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + + const req = { + } + const res = { + send: (data) => { + counter++ + } + } + const next = (e) => { expect(true).to.equal(false, e.message) } + + class MockService extends FunctionalService { + static handle( @functionalServiceName serviceName) { + counter++ + + expect(serviceName).to.equal('MockService') + + return { ok: 1 } + } + } + + const invoker = MockService.createInvoker() + await invoker(req, res, next) + + expect(counter).to.equal(2) + }) + + it("functionalServiceName with functionName decorator", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + + const req = { + } + const res = { + send: (data) => { + counter++ + } + } + const next = (e) => { expect(true).to.equal(false, e.message) } + + @functionName('MyMockService') + class MockService extends FunctionalService { + static handle( @functionalServiceName serviceName) { + counter++ + + expect(serviceName).to.equal('MyMockService') + + return { ok: 1 } + } + } + + const invoker = MockService.createInvoker() + await invoker(req, res, next) + + expect(counter).to.equal(2) + }) + + it("provider", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + + const req = { + } + const res = { + send: (data) => { + counter++ + } + } + const next = (e) => { expect(true).to.equal(false, e.message) } + + class MockService extends FunctionalService { + static handle( @provider provider) { + counter++ + + expect(provider).to.equal('local') + + return { ok: 1 } + } + } + + const invoker = MockService.createInvoker() + await invoker(req, res, next) + + expect(counter).to.equal(2) + }) + + it("stage", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + process.env.FUNCTIONAL_STAGE = 'dev' + + const req = { + } + const res = { + send: (data) => { + counter++ + } + } + const next = (e) => { expect(true).to.equal(false, e.message) } + + class MockService extends FunctionalService { + static handle( @stage stage) { + counter++ + + expect(stage).to.equal('dev') + + return { ok: 1 } + } + } + + const invoker = MockService.createInvoker() + await invoker(req, res, next) + + expect(counter).to.equal(2) + }) + }) + + describe("aws", () => { + + afterEach(() => { + delete process.env.FUNCTIONAL_ENVIRONMENT + delete process.env.FUNCTIONAL_STAGE + }) + + it("invoke", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + class MockService extends FunctionalService { + static handle() { + counter++ + } + } + + const invoker = MockService.createInvoker() + invoker({}, {}, (e) => { + expect(counter).to.equal(1) + done(e) + }) + }) + + it("return value", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + class MockService extends FunctionalService { + static handle() { + counter++ + return { ok: 1 } + } + } + + const invoker = MockService.createInvoker() + invoker({}, {}, (e, result) => { + expect(counter).to.equal(1) + expect(result).to.deep.equal({ ok: 1 }) + done(e) + }) + }) + + it("handler throw error", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + class MockService extends FunctionalService { + static handle() { + counter++ + throw new Error('error in handle') + } + } + + const invoker = MockService.createInvoker() + invoker({}, {}, (e, result) => { + counter++ + expect(counter).to.equal(2) + expect(e.message).to.equal('error in handle') + done() + }) + }) + + describe("eventSources", () => { + + it("lambda param error", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + class MockService extends FunctionalService { + static handle( @param p1, @param p2) { + counter++ + expect(p1).to.equal('v1') + expect(p2).to.equal('v2') + } + } + + const invoker = MockService.createInvoker() + const awsEvent = { + p1: undefined, + p2: 'v2' + } + invoker(awsEvent, {}, (e) => { + counter++ + expect(counter).to.equal(2) + expect(e).is.instanceof(Error) + done() + }) + }) + + it("lambda param", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + class MockService extends FunctionalService { + static handle( @param p1, @param p2) { + counter++ + expect(p1).to.equal('v1') + expect(p2).to.equal('v2') + } + } + + const invoker = MockService.createInvoker() + const awsEvent = { + p1: 'v1', + p2: 'v2' + } + invoker(awsEvent, {}, (e) => { + expect(counter).to.equal(1) + done(e) + }) + }) + + it("api gateway error", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + class MockService extends FunctionalService { + static async handle( @param p1, @param p2) { + counter++ + expect(p1).to.equal('v1') + expect(p2).to.equal('v2') + } + } + + const invoker = MockService.createInvoker() + const awsEvent = { + requestContext: { apiId: 'apiId' }, + body: { + p2: 'v2' + } + } + const r = await invoker(awsEvent, {}, (e) => { + counter++ + expect(counter).to.equal(2) + }) + + expect(counter).to.equal(2) + expect(r.statusCode).to.equal(500) + }) + + it("api gateway body param", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + class MockService extends FunctionalService { + static async handle( @param p1, @param p2) { + counter++ + expect(p1).to.equal('v1') + expect(p2).to.equal('v2') + } + } + + const invoker = MockService.createInvoker() + const awsEvent = { + requestContext: { apiId: 'apiId' }, + body: { + p1: 'v1', + p2: 'v2' + } + } + const r = await invoker(awsEvent, {}, (e) => { + counter++ + expect(counter).to.equal(2) + }) + + expect(counter).to.equal(2) + expect(r.statusCode).to.equal(200, r.body) + }) + + it("api gateway string body param", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + class MockService extends FunctionalService { + static async handle( @param p1, @param p2) { + counter++ + expect(p1).to.equal('v1') + expect(p2).to.equal('v2') + } + } + + const invoker = MockService.createInvoker() + const awsEvent = { + requestContext: { apiId: 'apiId' }, + body: JSON.stringify({ + p1: 'v1', + p2: 'v2' + }) + } + const r = await invoker(awsEvent, {}, (e) => { + counter++ + expect(counter).to.equal(2) + }) + + expect(counter).to.equal(2) + expect(r.statusCode).to.equal(200, r.body) + }) + + it("api gateway query param", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + class MockService extends FunctionalService { + static async handle( @param p1, @param p2) { + counter++ + expect(p1).to.equal('v1') + expect(p2).to.equal('v2') + } + } + + const invoker = MockService.createInvoker() + const awsEvent = { + requestContext: { apiId: 'apiId' }, + queryStringParameters: { + p1: 'v1', + p2: 'v2' + } + } + const r = await invoker(awsEvent, {}, (e) => { + counter++ + expect(counter).to.equal(2) + }) + + expect(counter).to.equal(2) + expect(r.statusCode).to.equal(200, r.body) + }) + + it("api gateway pathParameters param", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + class MockService extends FunctionalService { + static async handle( @param p1, @param p2) { + counter++ + expect(p1).to.equal('v1') + expect(p2).to.equal('v2') + } + } + + const invoker = MockService.createInvoker() + const awsEvent = { + requestContext: { apiId: 'apiId' }, + pathParameters: { + p1: 'v1', + p2: 'v2' + } + } + const r = await invoker(awsEvent, {}, (e) => { + counter++ + expect(counter).to.equal(2) + }) + + expect(counter).to.equal(2) + expect(r.statusCode).to.equal(200, r.body) + }) + + it("api gateway header param", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + class MockService extends FunctionalService { + static async handle( @param p1, @param p2) { + counter++ + expect(p1).to.equal('v1') + expect(p2).to.equal('v2') + } + } + + const invoker = MockService.createInvoker() + const awsEvent = { + requestContext: { apiId: 'apiId' }, + headers: { + p1: 'v1', + p2: 'v2' + } + } + const r = await invoker(awsEvent, {}, (e) => { + counter++ + expect(counter).to.equal(2) + }) + + expect(counter).to.equal(2) + expect(r.statusCode).to.equal(200, r.body) + }) + + it("api gateway params resolve order", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + class MockService extends FunctionalService { + static async handle( @param p1, @param p2, @param p3, @param p4) { + counter++ + expect(p1).to.equal('body') + expect(p2).to.equal('queryStringParameters') + expect(p3).to.equal('pathParameters') + expect(p4).to.equal('headers') + } + } + + const invoker = MockService.createInvoker() + const awsEvent = { + requestContext: { apiId: 'apiId' }, + body: { + p1: 'body' + }, + queryStringParameters: { + p1: 'queryStringParameters', + p2: 'queryStringParameters' + }, + pathParameters: { + p1: 'pathParameters', + p2: 'pathParameters', + p3: 'pathParameters' + }, + headers: { + p1: 'headers', + p2: 'headers', + p3: 'headers', + p4: 'headers' + } + } + const r = await invoker(awsEvent, {}, (e) => { + counter++ + expect(counter).to.equal(2) + }) + + expect(counter).to.equal(2) + expect(r.statusCode).to.equal(200, r.body) + }) + + it("api gateway params resolve hint", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + class MockService extends FunctionalService { + static async handle( @param({ source: 'event.event.queryStringParameters' }) p1, @param({ source: 'event.event.pathParameters' }) p2, @param({ source: 'event.event.headers' }) p3, @param p4) { + counter++ + expect(p1).to.equal('queryStringParameters') + expect(p2).to.equal('pathParameters') + expect(p3).to.equal('headers') + expect(p4).to.equal('headers') + } + } + + const invoker = MockService.createInvoker() + const awsEvent = { + requestContext: { apiId: 'apiId' }, + body: { + p1: 'body' + }, + queryStringParameters: { + p1: 'queryStringParameters', + p2: 'queryStringParameters' + }, + pathParameters: { + p1: 'pathParameters', + p2: 'pathParameters', + p3: 'pathParameters' + }, + headers: { + p1: 'headers', + p2: 'headers', + p3: 'headers', + p4: 'headers' + } + } + const r = await invoker(awsEvent, {}, (e) => { + counter++ + expect(counter).to.equal(2) + }) + + expect(counter).to.equal(2) + expect(r.statusCode).to.equal(200, r.body) + }) + + it("api gateway return value", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + class MockService extends FunctionalService { + static async handle() { + counter++ + return { ok: 1 } + } + } + + const invoker = MockService.createInvoker() + const awsEvent = { + requestContext: { apiId: 'apiId' }, + } + const r = await invoker(awsEvent, {}, (e) => { + counter++ + expect(counter).to.equal(2) + }) + + expect(counter).to.equal(2) + expect(r.statusCode).to.equal(200, r.body) + expect(r).to.deep.equal({ + statusCode: 200, + headers: {}, + body: '{"ok":1}' + }) + }) + + it("api gateway return value advanced", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + class MockService extends FunctionalService { + static async handle() { + counter++ + return { + statusCode: 200, + body: 'myresult' + } + } + } + + const invoker = MockService.createInvoker() + const awsEvent = { + requestContext: { apiId: 'apiId' }, + } + const r = await invoker(awsEvent, {}, (e) => { + counter++ + expect(counter).to.equal(2) + }) + + expect(counter).to.equal(2) + expect(r.statusCode).to.equal(200, r.body) + expect(r).to.deep.equal({ + statusCode: 200, + body: 'myresult' + }) + }) + + it("api gateway return value advanced - error", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + class MockService extends FunctionalService { + static async handle() { + counter++ + return { + statusCode: 500, + body: 'myerror' + } + } + } + + const invoker = MockService.createInvoker() + const awsEvent = { + requestContext: { apiId: 'apiId' }, + } + const r = await invoker(awsEvent, {}, (e) => { + counter++ + expect(counter).to.equal(2) + }) + + expect(counter).to.equal(2) + expect(r.statusCode).to.equal(500, r.body) + expect(r).to.deep.equal({ + statusCode: 500, + body: 'myerror' + }) + }) + + it("api gateway return value advanced - throw error", async () => { + let counter = 0 + + class MyError extends Error { + constructor(public name, ...params) { + super(...params); + } + } + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + class MockService extends FunctionalService { + static async handle() { + counter++ + throw new MyError('error in handle') + } + } + + const invoker = MockService.createInvoker() + const awsEvent = { + requestContext: { apiId: 'apiId' }, + } + const r = await invoker(awsEvent, {}, (e) => { + counter++ + expect(counter).to.equal(2) + }) + + expect(counter).to.equal(2) + expect(r.statusCode).to.equal(500, r.body) + expect(r).to.deep.equal({ + statusCode: 500, + headers: {}, + body: JSON.stringify(new MyError('error in handle')) + }) + }) + + it("s3 param", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + + const awsEvent = { + Records: [{ + eventSource: 'aws:s3', + s3: { + "object": { + "key": "filename", + "size": 1234, + "eTag": "rnd", + "sequencer": "sssss" + } + } + }] + } + + class MockService extends FunctionalService { + static handle( @param s3, @param('s3.object.key') p2) { + counter++ + expect(s3).to.deep.equal(awsEvent.Records[0].s3) + expect(p2).to.equal('filename') + } + } + + const invoker = MockService.createInvoker() + + invoker(awsEvent, {}, (e) => { + expect(counter).to.equal(1) + done(e) + }) + }) + + it("s3 param source", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + + const awsEvent = { + Records: [{ + eventSource: 'aws:s3', + s3: { + "object": { + "key": "filename", + "size": 1234, + "eTag": "rnd", + "sequencer": "sssss" + } + } + }] + } + + class MockService extends FunctionalService { + static handle( @param s3, @param({ name: 'event.event.Records', source: null }) p2) { + counter++ + expect(s3).to.deep.equal(awsEvent.Records[0].s3) + expect(p2).to.have.lengthOf(1); + expect(p2).to.deep.equal(awsEvent.Records) + } + } + + const invoker = MockService.createInvoker() + + invoker(awsEvent, {}, (e) => { + expect(counter).to.equal(1) + done(e) + }) + }) + + it("sns param", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + + const awsEvent = { + Records: [{ + EventSource: 'aws:sns', + Sns: { + "Type": "Notification", + "Subject": "subject", + "Message": "message" + } + }] + } + + class MockService extends FunctionalService { + static handle( @param Sns, @param('Sns.Message') p2) { + counter++ + expect(Sns).to.deep.equal(awsEvent.Records[0].Sns) + expect(p2).to.equal('message') + } + } + + const invoker = MockService.createInvoker() + + invoker(awsEvent, {}, (e) => { + expect(counter).to.equal(1) + done(e) + }) + }) + + it("sns param source", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + + const awsEvent = { + Records: [{ + EventSource: 'aws:sns', + Sns: { + "Type": "Notification", + "Subject": "subject", + "Message": "message" + } + }] + } + + class MockService extends FunctionalService { + static handle( @param Sns, @param({ name: 'event.event.Records', source: null }) p2) { + counter++ + expect(Sns).to.deep.equal(awsEvent.Records[0].Sns) + expect(p2).to.have.lengthOf(1); + expect(p2).to.deep.equal(awsEvent.Records) + } + } + + const invoker = MockService.createInvoker() + + invoker(awsEvent, {}, (e) => { + expect(counter).to.equal(1) + done(e) + }) + }) + + it("api gateway generic result format", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + + @injectable() + class MockInjectable extends Resource { } + + const awsEvent = { + requestContext: { apiId: 'apiId' }, + } + const awsContext = {} + const cb = (e, result) => { + counter++ + + expect(e).is.null + + expect(result).to.deep.equal({ + statusCode: 201, + headers: { + 'content-type': 'application/json' + }, + body: JSON.stringify({ + ok: 1 + }) + }) + } + + class MockService extends FunctionalService { + static handle() { + counter++ + + return { + status: 201, + headers: { + 'content-type': 'application/json' + }, + data: { + ok: 1 + } + } + } + } + + const invoker = MockService.createInvoker() + await invoker(awsEvent, awsContext, cb) + + expect(counter).to.equal(2) + }) + }) + + it("inject param", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + + @injectable() + class MockInjectable extends Resource { } + + class MockService extends FunctionalService { + static handle( @param p1, @inject(MockInjectable) p2) { + counter++ + expect(p1).to.undefined + expect(p2).to.instanceof(Resource) + expect(p2).to.instanceof(MockInjectable) + } + } + + const invoker = MockService.createInvoker() + + invoker({}, {}, (e) => { + expect(counter).to.equal(1) + done(e) + }) + }) + + it("service param", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + + @injectable() + class CustomService extends Service { + static handle( @param p1, @param p2) { + counter++ + expect(p1).to.equal('p1') + expect(p2).to.equal('p2') + } + } + + class MockService extends FunctionalService { + static handle( @param p1, @inject(CustomService) p2) { + counter++ + expect(p1).to.undefined + expect(p2).to.instanceof(Function) + + p2({ p1: 'p1', p2: 'p2' }) + } + } + + const invoker = MockService.createInvoker() + + invoker({}, {}, (e) => { + expect(counter).to.equal(2) + done(e) + }) + }) + + it("serviceParams param", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + + @injectable() + class MockInjectable extends Resource { } + + const awsEvent = {} + const awsContext = {} + const cb = (e) => { + expect(counter).to.equal(1) + done(e) + } + + class MockService extends FunctionalService { + static handle( @param p1, @serviceParams p2) { + counter++ + expect(p1).to.undefined + expect(p2).to.have.property('event').that.to.equal(awsEvent) + expect(p2).to.have.property('context').that.to.equal(awsContext) + expect(p2).to.have.property('cb').that.to.equal(cb) + } + } + + const invoker = MockService.createInvoker() + invoker(awsEvent, awsContext, cb) + }) + + it("request param", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + + @injectable() + class MockInjectable extends Resource { } + + const awsEvent = { + path: '/a/b', + httpMethod: 'GET', + requestContext: { apiId: 'apiId' }, + body: { + p1: 'body' + }, + queryStringParameters: { + p2: 'queryStringParameters' + }, + pathParameters: { + p3: 'pathParameters' + }, + headers: { + p4: 'headers' + }, + anyprop: {} + } + const awsContext = {} + const cb = (e) => { + expect(counter).to.equal(1) + done(e) + } + + class MockService extends FunctionalService { + static handle( @param p1, @request r) { + counter++ + expect(r).to.have.property('url').that.deep.equal(parse(awsEvent.path)) + expect(r).to.have.property('method', awsEvent.httpMethod) + expect(r).to.have.property('body').that.deep.equal(awsEvent.body) + expect(r).to.have.property('query').that.deep.equal(awsEvent.queryStringParameters) + expect(r).to.have.property('params').that.deep.equal(awsEvent.pathParameters) + expect(r).to.have.property('headers').that.deep.equal(awsEvent.headers) + expect(r).to.not.have.property('anyprop') + } + } + + const invoker = MockService.createInvoker() + invoker(awsEvent, awsContext, cb) + }) + + it("request param - string body", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + + @injectable() + class MockInjectable extends Resource { } + + const awsEvent = { + path: '/a/b', + httpMethod: 'GET', + requestContext: { apiId: 'apiId' }, + body: '{"p1":"body"}', + queryStringParameters: { + p2: 'queryStringParameters' + }, + pathParameters: { + p3: 'pathParameters' + }, + headers: { + p4: 'headers' + }, + anyprop: {} + } + const awsContext = {} + const cb = (e) => { + expect(counter).to.equal(1) + done(e) + } + + class MockService extends FunctionalService { + static handle( @param p1, @request r) { + counter++ + expect(r).to.have.property('url').that.deep.equal(parse(awsEvent.path)) + expect(r).to.have.property('method', awsEvent.httpMethod) + expect(r).to.have.property('body').that.deep.equal({ "p1": "body" }) + expect(r).to.have.property('query').that.deep.equal(awsEvent.queryStringParameters) + expect(r).to.have.property('params').that.deep.equal(awsEvent.pathParameters) + expect(r).to.have.property('headers').that.deep.equal(awsEvent.headers) + expect(r).to.not.have.property('anyprop') + } + } + + const invoker = MockService.createInvoker() + invoker(awsEvent, awsContext, cb) + }) + + it("request param - string body not json", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + + @injectable() + class MockInjectable extends Resource { } + + const awsEvent = { + path: '/a/b', + httpMethod: 'GET', + requestContext: { apiId: 'apiId' }, + body: 'request body', + queryStringParameters: { + p2: 'queryStringParameters' + }, + pathParameters: { + p3: 'pathParameters' + }, + headers: { + p4: 'headers' + }, + anyprop: {} + } + const awsContext = {} + const cb = (e) => { + expect(counter).to.equal(1) + done(e) + } + + class MockService extends FunctionalService { + static handle( @param p1, @request r) { + counter++ + expect(r).to.have.property('url').that.deep.equal(parse(awsEvent.path)) + expect(r).to.have.property('method', awsEvent.httpMethod) + expect(r).to.have.property('body').that.equal(awsEvent.body) + expect(r).to.have.property('query').that.deep.equal(awsEvent.queryStringParameters) + expect(r).to.have.property('params').that.deep.equal(awsEvent.pathParameters) + expect(r).to.have.property('headers').that.deep.equal(awsEvent.headers) + expect(r).to.not.have.property('anyprop') + } + } + + const invoker = MockService.createInvoker() + invoker(awsEvent, awsContext, cb) + }) + + it("generic result format", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + + @injectable() + class MockInjectable extends Resource { } + + const awsEvent = { + } + const awsContext = {} + const cb = (e, result) => { + counter++ + + expect(e).is.null + + expect(result).to.deep.equal({ + ok: 1 + }) + } + + class MockService extends FunctionalService { + static handle() { + counter++ + + return { + status: 201, + headers: { + 'content-type': 'application/json' + }, + data: { + ok: 1 + } + } + } + } + + const invoker = MockService.createInvoker() + await invoker(awsEvent, awsContext, cb) + + expect(counter).to.equal(2) + }) + + it("functionalServiceName", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + + const awsEvent = {} + const awsContext = {} + const cb = (e, result) => { + counter++ + + expect(result).to.deep.equal({ ok: 1 }) + } + + class MockService extends FunctionalService { + static handle( @functionalServiceName serviceName) { + counter++ + + expect(serviceName).to.equal('MockService') + + return { ok: 1 } + } + } + + const invoker = MockService.createInvoker() + await invoker(awsEvent, awsContext, cb) + + expect(counter).to.equal(2) + }) + + it("functionalServiceName with functionName decorator", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + + const awsEvent = {} + const awsContext = {} + const cb = (e, result) => { + counter++ + + expect(result).to.deep.equal({ ok: 1 }) + } + + @functionName('MyMockService') + class MockService extends FunctionalService { + static handle( @functionalServiceName serviceName) { + counter++ + + expect(serviceName).to.equal('MyMockService') + + return { ok: 1 } + } + } + + const invoker = MockService.createInvoker() + await invoker(awsEvent, awsContext, cb) + + expect(counter).to.equal(2) + }) + + it("provider", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + + const awsEvent = {} + const awsContext = {} + const cb = (e, result) => { + counter++ + + expect(result).to.deep.equal({ ok: 1 }) + } + + class MockService extends FunctionalService { + static handle( @provider provider) { + counter++ + + expect(provider).to.equal('aws') + + return { ok: 1 } + } + } + + const invoker = MockService.createInvoker() + await invoker(awsEvent, awsContext, cb) + + expect(counter).to.equal(2) + }) + + it("stage", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + process.env.FUNCTIONAL_STAGE = 'dev' + + const awsEvent = {} + const awsContext = {} + const cb = (e, result) => { + counter++ + + expect(result).to.deep.equal({ ok: 1 }) + } + + class MockService extends FunctionalService { + static handle( @stage stage) { + counter++ + + expect(stage).to.equal('dev') + + return { ok: 1 } + } + } + + const invoker = MockService.createInvoker() + await invoker(awsEvent, awsContext, cb) + + expect(counter).to.equal(2) + }) + }) + + describe("azure", () => { + + afterEach(() => { + delete process.env.FUNCTIONAL_ENVIRONMENT + delete process.env.FUNCTIONAL_STAGE + }) + + it("invoke", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'azure' + class MockService extends FunctionalService { + static handle() { + counter++ + } + } + + const invoker = MockService.createInvoker() + + const context = {} + const req = {} + + await invoker(context, req) + + expect(counter).to.equal(1) + }) + + it("return value", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'azure' + class MockService extends FunctionalService { + static handle() { + counter++ + return { ok: 1 } + } + } + + const invoker = MockService.createInvoker() + + const context = {} + const req = {} + + await invoker(context, req) + + expect(counter).to.equal(1) + expect(context).to.have.nested.property('res.status', 200) + expect(context).to.have.nested.property('res.body').that.deep.equal({ ok: 1 }) + }) + + it("handler throw error", async () => { + let counter = 0 + let ex + + process.env.FUNCTIONAL_ENVIRONMENT = 'azure' + class MockService extends FunctionalService { + static handle() { + counter++ + ex = new Error('error in handle') + throw ex + } + } + + const invoker = MockService.createInvoker() + + const context = {} + const req = {} + + await invoker(context, req) + + expect(counter).to.equal(1) + expect(context).to.have.nested.property('res.status', 500) + expect(context).to.have.nested.property('res.body', `${ex.message} - ${ex.stack}`) + }) + + describe("eventSources", () => { + it("httpTrigger body param", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'azure' + class MockService extends FunctionalService { + static handle( @param p1, @param p2) { + counter++ + expect(p1).to.equal('v1') + expect(p2).to.equal('v2') + } + } + + const invoker = MockService.createInvoker() + + const context = {} + const req = { + body: { + p1: 'v1', + p2: 'v2' + } + } + + await invoker(context, req) + + expect(context).to.have.nested.property('res.status', 200) + expect(counter).to.equal(1) + }) + + it("httpTrigger query param", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'azure' + class MockService extends FunctionalService { + static handle( @param p1, @param p2) { + counter++ + expect(p1).to.equal('v1') + expect(p2).to.equal('v2') + } + } + + const invoker = MockService.createInvoker() + + const context = {} + const req = { + query: { + p1: 'v1', + p2: 'v2' + } + } + + await invoker(context, req) + + expect(context).to.have.nested.property('res.status', 200) + expect(counter).to.equal(1) + }) + + it("httpTrigger pathParameters param", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'azure' + class MockService extends FunctionalService { + static handle( @param p1, @param p2) { + counter++ + expect(p1).to.equal('v1') + expect(p2).to.equal('v2') + } + } + + const invoker = MockService.createInvoker() + + const context = {} + const req = { + params: { + p1: 'v1', + p2: 'v2' + } + } + + await invoker(context, req) + + expect(context).to.have.nested.property('res.status', 200) + expect(counter).to.equal(1) + }) + + it("httpTrigger header param", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'azure' + class MockService extends FunctionalService { + static handle( @param p1, @param p2) { + counter++ + expect(p1).to.equal('v1') + expect(p2).to.equal('v2') + } + } + + const invoker = MockService.createInvoker() + + const context = {} + const req = { + headers: { + p1: 'v1', + p2: 'v2' + } + } + + await invoker(context, req) + + expect(context).to.have.nested.property('res.status', 200) + expect(counter).to.equal(1) + }) + + it("httpTrigger params resolve order", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'azure' + class MockService extends FunctionalService { + static handle( @param p1, @param p2, @param p3, @param p4) { + counter++ + expect(p1).to.equal('body') + expect(p2).to.equal('queryStringParameters') + expect(p3).to.equal('pathParameters') + expect(p4).to.equal('headers') + } + } + + const invoker = MockService.createInvoker() + + const context = {} + const req = { + body: { + p1: 'body' + }, + query: { + p1: 'queryStringParameters', + p2: 'queryStringParameters' + }, + params: { + p1: 'pathParameters', + p2: 'pathParameters', + p3: 'pathParameters' + }, + headers: { + p1: 'headers', + p2: 'headers', + p3: 'headers', + p4: 'headers' + } + } + + await invoker(context, req) + + expect(context).to.have.nested.property('res.status', 200) + expect(counter).to.equal(1) + }) + + it("httpTrigger params resolve hint", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'azure' + class MockService extends FunctionalService { + static handle( @param({ source: 'event.req.query' }) p1, @param({ source: 'event.req.params' }) p2, @param({ source: 'event.req.headers' }) p3, @param p4) { + counter++ + expect(p1).to.equal('queryStringParameters') + expect(p2).to.equal('pathParameters') + expect(p3).to.equal('headers') + expect(p4).to.equal('headers') + } + } + + const invoker = MockService.createInvoker() + + const context = {} + const req = { + body: { + p1: 'body' + }, + query: { + p1: 'queryStringParameters', + p2: 'queryStringParameters' + }, + params: { + p1: 'pathParameters', + p2: 'pathParameters', + p3: 'pathParameters' + }, + headers: { + p1: 'headers', + p2: 'headers', + p3: 'headers', + p4: 'headers' + } + } + + await invoker(context, req) + + expect(context).to.have.nested.property('res.status', 200) + expect(counter).to.equal(1) + }) + + it("httpTrigger return value", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'azure' + class MockService extends FunctionalService { + static handle() { + counter++ + return { ok: 1 } + } + } + + const invoker = MockService.createInvoker() + + const context = {} + const req = {} + + await invoker(context, req) + + expect(counter).to.equal(1) + expect(context).to.have.nested.property('res.status', 200) + expect(context).to.have.nested.property('res.body').that.deep.equal({ ok: 1 }) + }) + + it("httpTrigger return value advanced", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'azure' + class MockService extends FunctionalService { + static handle() { + counter++ + return { + status: 200, + body: 'myresult' + } + } + } + + const invoker = MockService.createInvoker() + + const context = {} + const req = {} + + await invoker(context, req) + + expect(counter).to.equal(1) + expect(context).to.have.nested.property('res.status', 200) + expect(context).to.have.nested.property('res.body', 'myresult') + }) + + it("httpTrigger return value advanced - error", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'azure' + class MockService extends FunctionalService { + static handle() { + counter++ + return { + status: 500, + body: 'myerror' + } + } + } + + const invoker = MockService.createInvoker() + + const context = {} + const req = {} + + await invoker(context, req) + + expect(counter).to.equal(1) + expect(context).to.have.nested.property('res.status', 500) + expect(context).to.have.nested.property('res.body', 'myerror') + }) + + it("httpTrigger return value advanced - throw error", async () => { + let counter = 0 + let ex + + process.env.FUNCTIONAL_ENVIRONMENT = 'azure' + class MockService extends FunctionalService { + static handle() { + counter++ + ex = new Error('error in handle') + throw ex + } + } + + const invoker = MockService.createInvoker() + + const context = {} + const req = {} + + await invoker(context, req) + + expect(counter).to.equal(1) + expect(context).to.have.nested.property('res.status', 500) + expect(context).to.have.nested.property('res.body', `${ex.message} - ${ex.stack}`) + }) + }) + + it("inject api with service", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'azure' + + @injectable() + class CustomService extends Service { + static handle( @param p1, @param p2) { + counter++ + expect(p1).to.equal('p1') + expect(p2).to.equal('p2') + } + } + + class MockService extends FunctionalService { + static handle( @param p1, @inject(CustomService) p2) { + counter++ + expect(p1).to.undefined + expect(p2).to.instanceof(Function) + + p2({ p1: 'p1', p2: 'p2' }) + } + } + + const invoker = MockService.createInvoker() + + const context = {} + const req = {} + + await invoker(context, req) + + expect(context).to.have.nested.property('res.status', 200) + expect(counter).to.equal(2) + }) + + it("serviceParams param", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'azure' + + @injectable() + class MockInjectable extends Resource { } + + const context = {} + const req = {} + + class MockService extends FunctionalService { + static handle( @param p1, @serviceParams p2) { + counter++ + expect(p1).to.undefined + expect(p2).to.have.property('context').that.to.equal(context) + expect(p2).to.have.property('req').that.to.equal(req) + } + } + + const invoker = MockService.createInvoker() + + await invoker(context, req) + + expect(context).to.have.nested.property('res.status', 200) + expect(counter).to.equal(1) + }) + + it("request param", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'azure' + + @injectable() + class MockInjectable extends Resource { } + + const context = {} + const req = { + originalUrl: 'https://asd.azure.com/api/a/b?code=a&a=b', + method: 'GET', + body: { + p1: 'body' + }, + query: { + p2: 'queryStringParameters' + }, + params: { + p3: 'pathParameters' + }, + headers: { + p4: 'headers' + } + } + + class MockService extends FunctionalService { + static handle( @param p1, @request r) { + counter++ + expect(r).to.have.property('url').that.deep.equal(parse(req.originalUrl)) + expect(r).to.have.property('method', req.method) + expect(r).to.have.property('body').that.deep.equal(req.body) + expect(r).to.have.property('query').that.deep.equal(req.query) + expect(r).to.have.property('params').that.deep.equal(req.params) + expect(r).to.have.property('headers').that.deep.equal(req.headers) + expect(r).to.not.have.property('anyprop') + } + } + + const invoker = MockService.createInvoker() + + await invoker(context, req) + + expect(context).to.have.nested.property('res.status', 200) + expect(counter).to.equal(1) + }) + + it("generic result format", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'azure' + + @injectable() + class MockInjectable extends Resource { } + + const context = {} + const req = { + } + + class MockService extends FunctionalService { + static handle() { + counter++ + + return { + status: 201, + headers: { + 'content-type': 'application/json' + }, + data: { + ok: 1 + } + } + } + } + + const invoker = MockService.createInvoker() + + await invoker(context, req) + + expect(context).to.have.nested.property('res.status', 201) + expect(context).to.have.nested.property('res.headers').that.deep.equal({ + 'content-type': 'application/json' + }) + expect(context).to.have.nested.property('res.body').that.deep.equal({ + ok: 1 + }) + expect(counter).to.equal(1) + }) + + it("functionalServiceName", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'azure' + + const context = {} + const req = {} + + class MockService extends FunctionalService { + static handle( @functionalServiceName serviceName) { + counter++ + + expect(serviceName).to.equal('MockService') + + return { ok: 1 } + } + } + + const invoker = MockService.createInvoker() + + await invoker(context, req) + + expect(context).to.have.nested.property('res.status', 200) + expect(context).to.have.nested.property('res.body').that.deep.equal({ ok: 1 }) + expect(counter).to.equal(1) + }) + + it("functionalServiceName with functionName decorator", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'azure' + + const context = {} + const req = {} + + @functionName('MyMockService') + class MockService extends FunctionalService { + static handle( @functionalServiceName serviceName) { + counter++ + + expect(serviceName).to.equal('MyMockService') + + return { ok: 1 } + } + } + + const invoker = MockService.createInvoker() + + await invoker(context, req) + + expect(context).to.have.nested.property('res.status', 200) + expect(context).to.have.nested.property('res.body').that.deep.equal({ ok: 1 }) + expect(counter).to.equal(1) + }) + + it("provider", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'azure' + + const context = {} + const req = {} + + class MockService extends FunctionalService { + static handle( @provider provider) { + counter++ + + expect(provider).to.equal('azure') + + return { ok: 1 } + } + } + + const invoker = MockService.createInvoker() + + await invoker(context, req) + + expect(context).to.have.nested.property('res.status', 200) + expect(context).to.have.nested.property('res.body').that.deep.equal({ ok: 1 }) + expect(counter).to.equal(1) + }) + + it("stage", async () => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'azure' + process.env.FUNCTIONAL_STAGE = 'dev' + + const context = {} + const req = {} + + class MockService extends FunctionalService { + static handle( @stage stage) { + counter++ + + expect(stage).to.equal('dev') + + return { ok: 1 } + } + } + + const invoker = MockService.createInvoker() + + await invoker(context, req) + + expect(context).to.have.nested.property('res.status', 200) + expect(context).to.have.nested.property('res.body').that.deep.equal({ ok: 1 }) + expect(counter).to.equal(1) + }) + }) +}) \ No newline at end of file diff --git a/test/ioc.tests.ts b/test/ioc.tests.ts new file mode 100644 index 0000000..1493d66 --- /dev/null +++ b/test/ioc.tests.ts @@ -0,0 +1,324 @@ +import { injectable, InjectionScope, inject } from '../src/annotations' +import { IOC, container } from '../src/helpers/ioc' + +import { LocalProvider } from '../src/providers/local' +import { addProvider, removeProvider } from '../src/providers' +import { FunctionalService } from '../src/classes/functionalService' +import { Api } from '../src/classes/api' +import { Service } from '../src/classes/service' + +import { expect } from 'chai' + +describe('IOC', () => { + const FUNCTIONAL_ENVIRONMENT = 'custom' + let mycontainer: IOC = null; + + beforeEach(() => { + mycontainer = new IOC(); + process.env.FUNCTIONAL_ENVIRONMENT = FUNCTIONAL_ENVIRONMENT + }) + + afterEach(() => { + delete process.env.FUNCTIONAL_ENVIRONMENT + removeProvider(FUNCTIONAL_ENVIRONMENT) + }) + + it("interface", () => { + expect(mycontainer).has.property('registerType').that.to.be.a('function') + expect(mycontainer).has.property('clearType').that.to.be.a('function') + expect(mycontainer).has.property('resolveType').that.to.be.a('function') + expect(mycontainer).has.property('registerInstance').that.to.be.a('function') + expect(mycontainer).has.property('resolve').that.to.be.a('function') + expect(mycontainer).has.property('containsInstance').that.to.be.a('function') + }) + + it("resolveType", () => { + class MyType { } + const a = mycontainer.resolveType(MyType) + expect(a).to.equal(MyType) + }) + + it("registerType", () => { + class MyType1 { } + class MyType2 { } + mycontainer.registerType(MyType1, MyType2) + const a = mycontainer.resolveType(MyType1) + expect(a).to.equal(MyType2) + }) + + it("clearType", () => { + class MyType1 { } + class MyType2 { } + mycontainer.registerType(MyType1, MyType2) + const a = mycontainer.resolveType(MyType1) + expect(a).to.equal(MyType2) + + mycontainer.clearType(MyType1) + + const b = mycontainer.resolveType(MyType1) + expect(b).to.equal(MyType1) + }) + + it("InjectionScope: not marked as injectable", () => { + class MyType { } + const a = mycontainer.resolve(MyType) + expect(a).instanceof(MyType) + + const b = mycontainer.resolve(MyType) + expect(b).instanceof(MyType) + + expect(a).is.not.equal(b) + }) + + it("InjectionScope: default", () => { + @injectable() + class MyType { } + const a = mycontainer.resolve(MyType) + expect(a).instanceof(MyType) + + const b = mycontainer.resolve(MyType) + expect(b).instanceof(MyType) + + expect(a).is.not.equal(b) + }) + + it("InjectionScope: Transient", () => { + @injectable(InjectionScope.Transient) + class MyType { } + const a = mycontainer.resolve(MyType) + expect(a).instanceof(MyType) + + const b = mycontainer.resolve(MyType) + expect(b).instanceof(MyType) + + expect(a).is.not.equal(b) + }) + + it("InjectionScope: Singleton", () => { + @injectable(InjectionScope.Singleton) + class MyType { } + const a = mycontainer.resolve(MyType) + expect(a).instanceof(MyType) + + const b = mycontainer.resolve(MyType) + expect(b).instanceof(MyType) + + expect(a).is.equal(b) + }) + + it("InjectionScope: Transient, conainsInstance", () => { + @injectable(InjectionScope.Transient) + class MyType { } + const a = mycontainer.resolve(MyType) + expect(a).instanceof(MyType) + + const contains = mycontainer.containsInstance(MyType) + expect(contains).is.equal(false) + }) + + it("InjectionScope: Singleton, conainsInstance", () => { + @injectable(InjectionScope.Singleton) + class MyType { } + const a = mycontainer.resolve(MyType) + expect(a).instanceof(MyType) + + const contains = mycontainer.containsInstance(MyType) + expect(contains).is.equal(true) + }) + + it("InjectionScope: Transient, registerInstance", () => { + @injectable(InjectionScope.Transient) + class MyType { } + + const instance = new MyType() + mycontainer.registerInstance(MyType, instance) + + const a = mycontainer.resolve(MyType) + expect(a).instanceof(MyType) + + const contains = mycontainer.containsInstance(MyType) + expect(contains).is.equal(false) + + const b = mycontainer.resolve(MyType) + expect(b).instanceof(MyType) + + expect(a).is.not.equal(b) + }) + + it("InjectionScope: Singleton, registerInstance", () => { + @injectable(InjectionScope.Singleton) + class MyType { } + + const instance = new MyType() + mycontainer.registerInstance(MyType, instance) + + const a = mycontainer.resolve(MyType) + expect(a).instanceof(MyType) + + const contains = mycontainer.containsInstance(MyType) + expect(contains).is.equal(true) + + const b = mycontainer.resolve(MyType) + expect(b).instanceof(MyType) + + expect(a).is.equal(b) + }) + + it("InjectionScope: Transient, registerType Transient", () => { + @injectable(InjectionScope.Transient) + class MyTypeA { } + + @injectable(InjectionScope.Transient) + class MyTypeB { } + + mycontainer.registerType(MyTypeA, MyTypeB) + + const a = mycontainer.resolve(MyTypeA) + expect(a).instanceof(MyTypeB) + + const b = mycontainer.resolve(MyTypeA) + expect(b).instanceof(MyTypeB) + + expect(a).is.not.equal(b) + }) + + it("InjectionScope: Singleton, registerType Singleton", () => { + @injectable(InjectionScope.Singleton) + class MyTypeA { } + + @injectable(InjectionScope.Singleton) + class MyTypeB { } + + mycontainer.registerType(MyTypeA, MyTypeB) + + const a = mycontainer.resolve(MyTypeA) + expect(a).instanceof(MyTypeB) + + const b = mycontainer.resolve(MyTypeA) + expect(b).instanceof(MyTypeB) + + expect(a).is.equal(b) + }) + + it("InjectionScope: Transient, registerType Singleton", () => { + @injectable(InjectionScope.Transient) + class MyTypeA { } + + @injectable(InjectionScope.Singleton) + class MyTypeB { } + + mycontainer.registerType(MyTypeA, MyTypeB) + + const a = mycontainer.resolve(MyTypeA) + expect(a).instanceof(MyTypeB) + + const b = mycontainer.resolve(MyTypeA) + expect(b).instanceof(MyTypeB) + + expect(a).is.equal(b) + }) + + it("InjectionScope: Singleton, registerType Transient", () => { + @injectable(InjectionScope.Singleton) + class MyTypeA { } + + @injectable(InjectionScope.Transient) + class MyTypeB { } + + mycontainer.registerType(MyTypeA, MyTypeB) + + const a = mycontainer.resolve(MyTypeA) + expect(a).instanceof(MyTypeB) + + const b = mycontainer.resolve(MyTypeA) + expect(b).instanceof(MyTypeB) + + expect(a).is.not.equal(b) + }) + + it("registerType depth 3", () => { + @injectable() + class MyTypeA { } + + @injectable() + class MyTypeB { } + + @injectable() + class MyTypeC { } + + mycontainer.registerType(MyTypeA, MyTypeB) + mycontainer.registerType(MyTypeB, MyTypeC) + + const a = mycontainer.resolve(MyTypeA) + expect(a).instanceof(MyTypeC) + }) + + + + it("inject remapped api class in ioc", async () => { + let counter = 0 + + addProvider(FUNCTIONAL_ENVIRONMENT, new LocalProvider()) + + @injectable() + class A extends Api { } + + @injectable() + class AOtherApi extends Api { } + + container.registerType(A, AOtherApi) + + class B extends FunctionalService { + public static async handle( @inject(A) a: A) { + counter++ + + expect(a).is.instanceof(AOtherApi) + } + } + + const invoker = B.createInvoker() + await invoker({}, { + send: () => { counter++ } + }, (e) => { expect(false).to.equal(true, e.message) }) + + expect(counter).to.equal(2) + }) + + it("inject remapped service class in ioc", async () => { + let counter = 0 + + addProvider(FUNCTIONAL_ENVIRONMENT, new LocalProvider()) + + @injectable() + class A extends Service { + public static async handle() { + counter++ + expect(false).to.equal(true, 'remap required') + } + } + + @injectable() + class AOtherService extends Service { + public static async handle() { + counter++ + } + } + + container.registerType(A, AOtherService) + + class B extends FunctionalService { + public static async handle( @inject(A) a) { + counter++ + + await a() + } + } + + const invoker = B.createInvoker() + await invoker({}, { + send: () => { counter++ } + }, (e) => { expect(false).to.equal(true, e.message) }) + + expect(counter).to.equal(3) + }) +}) \ No newline at end of file diff --git a/test/serviceOnEvent.tests.ts b/test/serviceOnEvent.tests.ts new file mode 100644 index 0000000..4cb25f3 --- /dev/null +++ b/test/serviceOnEvent.tests.ts @@ -0,0 +1,632 @@ +import { expect } from 'chai' + +import { FunctionalService, Resource } from '../src/classes' +import { param, inject, injectable } from '../src/annotations' +import { addProvider, removeProvider } from '../src/providers' +import { LocalProvider } from '../src/providers/local' + +import { PARAMETER_PARAMKEY } from '../src/annotations/constants' +import { getOverridableMetadata } from '../src/annotations/metadata' + +describe('service events', () => { + afterEach(() => { + delete process.env.FUNCTIONAL_ENVIRONMENT + }) + + describe("local", () => { + describe("onInject", () => { + it("static onInject", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + + @injectable() + class MockInjectable extends Resource { + public static async onInject({ parameter }) { + counter++ + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, MockService, 'handle') + expect(value).to.have.lengthOf(1); + const metadata = value[0] + + expect(metadata).to.deep.equal(parameter) + } + } + + class MockService extends FunctionalService { + public static handle( @inject(MockInjectable) p1) { + counter++ + expect(p1).to.instanceof(Resource) + expect(p1).to.instanceof(MockInjectable) + } + } + + const invoker = MockService.createInvoker() + invoker({}, { + send: () => { + expect(counter).to.equal(2) + done() + } + }, (e) => { e && done(e) }) + }) + + it("static onInject return", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + + @injectable() + class MockInjectable extends Resource { + public static async onInject({ parameter }) { + counter++ + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, MockService, 'handle') + expect(value).to.have.lengthOf(1); + const metadata = value[0] + + expect(metadata).to.deep.equal(parameter) + + return { mockRetrurn: 1 } + } + } + + class MockService extends FunctionalService { + public static handle( @inject(MockInjectable) p1) { + counter++ + expect(p1).to.not.instanceof(Resource) + expect(p1).to.not.instanceof(MockInjectable) + expect(p1).to.deep.equal({ mockRetrurn: 1 }) + } + } + + const invoker = MockService.createInvoker() + invoker({}, { + send: () => { + expect(counter).to.equal(2) + done() + } + }, (e) => { e && done(e) }) + }) + + it("onInject", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + + @injectable() + class MockInjectable extends Resource { + public async onInject({ parameter }) { + counter++ + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, MockService, 'handle') + expect(value).to.have.lengthOf(1); + const metadata = value[0] + + expect(metadata).to.deep.equal(parameter) + } + } + + class MockService extends FunctionalService { + public static handle( @inject(MockInjectable) p1) { + counter++ + expect(p1).to.instanceof(Resource) + expect(p1).to.instanceof(MockInjectable) + } + } + + const invoker = MockService.createInvoker() + invoker({}, { + send: () => { + expect(counter).to.equal(2) + done() + } + }, (e) => { e && done(e) }) + }) + + it("static onInject - onInject both called", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + + @injectable() + class MockInjectable extends Resource { + public static async onInject({ parameter }) { + counter++ + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, MockService, 'handle') + expect(value).to.have.lengthOf(1); + const metadata = value[0] + + expect(metadata).to.deep.equal(parameter) + } + public async onInject({ parameter }) { + counter++ + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, MockService, 'handle') + expect(value).to.have.lengthOf(1); + const metadata = value[0] + + expect(metadata).to.deep.equal(parameter) + } + } + + class MockService extends FunctionalService { + public static handle( @inject(MockInjectable) p1) { + counter++ + expect(p1).to.instanceof(Resource) + expect(p1).to.instanceof(MockInjectable) + } + } + + const invoker = MockService.createInvoker() + invoker({}, { + send: () => { + expect(counter).to.equal(3) + done() + } + }, (e) => { e && done(e) }) + }) + + it("static onInject return - onInject skipped", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + + @injectable() + class MockInjectable extends Resource { + public static async onInject({ parameter }) { + counter++ + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, MockService, 'handle') + expect(value).to.have.lengthOf(1); + const metadata = value[0] + + expect(metadata).to.deep.equal(parameter) + + return { mockRetrurn: 1 } + } + public async onInject({ parameter }) { + counter++ + expect(false).to.equal(true, 'skippable code') + } + } + + class MockService extends FunctionalService { + public static handle( @inject(MockInjectable) p1) { + counter++ + expect(p1).to.not.instanceof(Resource) + expect(p1).to.not.instanceof(MockInjectable) + expect(p1).to.deep.equal({ mockRetrurn: 1 }) + } + } + + const invoker = MockService.createInvoker() + invoker({}, { + send: () => { + expect(counter).to.equal(2) + done() + } + }, (e) => { e && done(e) }) + }) + }) + + describe("onHandle", () => { + it("onHandle call", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + + class MockService extends FunctionalService { + public static handle( @param p1, @param p2) { + counter++ + + return { ok: 1 } + } + + public static async onHandle_local(req, res, next) { + counter++ + + expect(req).to.deep.equal({ body: { p1: 1, p2: 2 } }) + } + } + + const invoker = MockService.createInvoker() + + const req = { + body: { p1: 1, p2: 2 } + } + invoker(req, { + send: (result) => { + expect(counter).to.equal(2) + expect(result).to.deep.equal({ ok: 1 }) + done() + } + }, (e) => { e && done(e) }) + }) + + it("onHandle call return", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'local' + + class MockService extends FunctionalService { + public static handle( @param p1, @param p2) { + counter++ + expect(false).to.equal(true, 'skippable code') + + return { ok: 1 } + } + + public static async onHandle_local(req, res, next) { + counter++ + + expect(req).to.deep.equal({ body: { p1: 1, p2: 2 } }) + + res.send({ ok: 2 }) + return true + } + } + + const invoker = MockService.createInvoker() + + const req = { + body: { p1: 1, p2: 2 } + } + invoker(req, { + send: (result) => { + expect(counter).to.equal(1) + expect(result).to.deep.equal({ ok: 2 }) + done() + } + }, (e) => { e && done(e) }) + }) + }) + }) + + describe("aws", () => { + describe("onInject", () => { + it("static onInject", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + + @injectable() + class MockInjectable extends Resource { + public static async onInject({ parameter }) { + counter++ + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, MockService, 'handle') + expect(value).to.have.lengthOf(1); + const metadata = value[0] + + expect(metadata).to.deep.equal(parameter) + } + } + + class MockService extends FunctionalService { + public static async handle( @inject(MockInjectable) p1) { + counter++ + expect(p1).to.instanceof(Resource) + expect(p1).to.instanceof(MockInjectable) + } + } + + const invoker = MockService.createInvoker() + invoker({}, {}, (e) => { + expect(counter).to.equal(2) + done(e) + }) + }) + + it("static onInject return", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + + @injectable() + class MockInjectable extends Resource { + public static async onInject({ parameter }) { + counter++ + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, MockService, 'handle') + expect(value).to.have.lengthOf(1); + const metadata = value[0] + + expect(metadata).to.deep.equal(parameter) + + return { mockRetrurn: 1 } + } + } + + class MockService extends FunctionalService { + public static async handle( @inject(MockInjectable) p1) { + counter++ + expect(p1).to.not.instanceof(Resource) + expect(p1).to.not.instanceof(MockInjectable) + expect(p1).to.deep.equal({ mockRetrurn: 1 }) + } + } + + const invoker = MockService.createInvoker() + invoker({}, {}, (e) => { + expect(counter).to.equal(2) + done(e) + }) + }) + + it("onInject", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + + @injectable() + class MockInjectable extends Resource { + public async onInject({ parameter }) { + counter++ + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, MockService, 'handle') + expect(value).to.have.lengthOf(1); + const metadata = value[0] + + expect(metadata).to.deep.equal(parameter) + } + } + + class MockService extends FunctionalService { + public static async handle( @inject(MockInjectable) p1) { + counter++ + expect(p1).to.instanceof(Resource) + expect(p1).to.instanceof(MockInjectable) + } + } + + const invoker = MockService.createInvoker() + invoker({}, {}, (e) => { + expect(counter).to.equal(2) + done(e) + }) + }) + + it("static onInject - onInject both called", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + + @injectable() + class MockInjectable extends Resource { + public static async onInject({ parameter }) { + counter++ + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, MockService, 'handle') + expect(value).to.have.lengthOf(1); + const metadata = value[0] + + expect(metadata).to.deep.equal(parameter) + } + public async onInject({ parameter }) { + counter++ + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, MockService, 'handle') + expect(value).to.have.lengthOf(1); + const metadata = value[0] + + expect(metadata).to.deep.equal(parameter) + } + } + + class MockService extends FunctionalService { + public static async handle( @inject(MockInjectable) p1) { + counter++ + expect(p1).to.instanceof(Resource) + expect(p1).to.instanceof(MockInjectable) + } + } + + const invoker = MockService.createInvoker() + invoker({}, {}, (e) => { + expect(counter).to.equal(3) + done(e) + }) + }) + + it("static onInject return - onInject skipped", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + + @injectable() + class MockInjectable extends Resource { + public static async onInject({ parameter }) { + counter++ + + const value = getOverridableMetadata(PARAMETER_PARAMKEY, MockService, 'handle') + expect(value).to.have.lengthOf(1); + const metadata = value[0] + + expect(metadata).to.deep.equal(parameter) + + return { mockRetrurn: 1 } + } + public async onInject({ parameter }) { + counter++ + expect(false).to.equal(true, 'skippable code') + } + } + + class MockService extends FunctionalService { + public static async handle( @inject(MockInjectable) p1) { + counter++ + expect(p1).to.not.instanceof(Resource) + expect(p1).to.not.instanceof(MockInjectable) + expect(p1).to.deep.equal({ mockRetrurn: 1 }) + } + } + + const invoker = MockService.createInvoker() + invoker({}, {}, (e) => { + expect(counter).to.equal(2) + done(e) + }) + }) + }) + + describe("onHandle", () => { + it("onHandle call", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + + class MockService extends FunctionalService { + public static async handle( @param p1, @param p2) { + counter++ + + return { ok: 1 } + } + + public static async onHandle_aws(event, context, cb) { + counter++ + + expect(event).to.deep.equal({ p1: 1, p2: 2 }) + } + } + + const invoker = MockService.createInvoker() + + const awsEvent = { + p1: 1, + p2: 2 + } + invoker(awsEvent, {}, (e, result) => { + expect(counter).to.equal(2) + expect(result).to.deep.equal({ ok: 1 }) + done(e) + }) + }) + + it("onHandle call return", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'aws' + + class MockService extends FunctionalService { + public static async handle( @param p1, @param p2) { + counter++ + expect(false).to.equal(true, 'skippable code') + + return { ok: 1 } + } + + public static async onHandle_aws(event, context, cb) { + counter++ + + expect(event).to.deep.equal({ p1: 1, p2: 2 }) + + cb(null, { ok: 2 }) + return true + } + } + + const invoker = MockService.createInvoker() + + const awsEvent = { + p1: 1, + p2: 2 + } + invoker(awsEvent, {}, (e, result) => { + expect(counter).to.equal(1) + expect(result).to.deep.equal({ ok: 2 }) + done(e) + }) + }) + }) + }) + + + describe("mock", () => { + class MockProvider extends LocalProvider { + public async invoke(serviceType, params, invokeConfig?): Promise { } + } + before(() => { + addProvider('mock', new MockProvider()) + }) + after(() => { + removeProvider('mock') + }) + + + describe("onInvoke", () => { + it("onInvoke", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'mock' + + @injectable() + class MockInjectable extends FunctionalService { + public static async onInvoke({ params, invokeConfig }) { + counter++ + + expect(params).to.deep.equal({ p1: 1, p2: 2 }) + expect(invokeConfig).to.deep.equal({ config: 1, context: {} }) + } + } + + class MockService extends FunctionalService { + public static async handle( @inject(MockInjectable) p1) { + counter++ + + await p1({ p1: 1, p2: 2 }, { config: 1 }) + } + } + + const invoker = MockService.createInvoker() + invoker({}, { + send: () => { + expect(counter).to.equal(2) + done() + } + }, (e) => { e && done(e) }) + }) + it("onInvoke mock", (done) => { + let counter = 0 + + process.env.FUNCTIONAL_ENVIRONMENT = 'mock' + + @injectable() + class MockInjectable extends FunctionalService { + public static async handle( @param p1) { } + static onInvoke_mock({ invokeParams, params, invokeConfig, parameterMapping, currentEnvironment, environmentMode }) { + counter++ + + expect(invokeParams).to.deep.equal({ p1: 1, p2: 2 }) + expect(params).to.deep.equal({ p1: 1 }) + expect(parameterMapping).to.have.lengthOf(1); + expect(parameterMapping[0]).to.deep.equal({ + from: 'p1', + parameterIndex: 0, + targetKey: "handle", + type: 'param' + }) + expect(currentEnvironment).to.instanceof(MockProvider) + expect(environmentMode).to.equal('mock') + } + } + + class MockService extends FunctionalService { + public static async handle( @inject(MockInjectable) p1) { + counter++ + + await p1({ p1: 1, p2: 2 }, { config: 1 }) + } + } + + const invoker = MockService.createInvoker() + invoker({}, { + send: () => { + expect(counter).to.equal(2) + done() + } + }, (e) => { e && done(e) }) + }) + }) + }) +}) \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 09e6902..82927bb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,8 @@ "declaration": true, "sourceMap": true, "allowSyntheticDefaultImports": true, - "experimentalDecorators": true + "experimentalDecorators": true, + "newLine": "LF" }, "exclude": [ "node_modules",