From 1af0aa0d0e3318a491153a65408af0a0b6d3a89b Mon Sep 17 00:00:00 2001 From: Eduardo Rodrigues Date: Wed, 11 Mar 2020 01:20:06 +0000 Subject: [PATCH 01/15] add rpdk for typescript --- jest.config.js | 10 + package-lock.json | 4674 ++++++++++++++++++++++++++++++++++ package.json | 45 + src/callback.ts | 48 + src/exceptions.ts | 58 + src/interface.ts | 90 + src/proxy.ts | 151 ++ tests/lib/callback.test.ts | 93 + tests/lib/exceptions.test.ts | 25 + tests/lib/proxy.test.ts | 30 + tsconfig.json | 15 + 11 files changed, 5239 insertions(+) create mode 100644 jest.config.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/callback.ts create mode 100644 src/exceptions.ts create mode 100644 src/interface.ts create mode 100644 src/proxy.ts create mode 100644 tests/lib/callback.test.ts create mode 100644 tests/lib/exceptions.test.ts create mode 100644 tests/lib/proxy.test.ts create mode 100644 tsconfig.json diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..b23ae09 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,10 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + globals: { + 'ts-jest': { + diagnostics: false, // Necessary to avoid typeschecking error in decorators + } + }, + testRegex: '\\.test.ts$', +}; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..2de3e0d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4674 @@ +{ + "name": "cfn-rpdk", + "version": "0.0.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/core": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.8.6.tgz", + "integrity": "sha512-Sheg7yEJD51YHAvLEV/7Uvw95AeWqYPL3Vk3zGujJKIhJ+8oLw2ALaf3hbucILhKsgSoADOvtKRJuNVdcJkOrg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.8.6", + "@babel/helpers": "^7.8.4", + "@babel/parser": "^7.8.6", + "@babel/template": "^7.8.6", + "@babel/traverse": "^7.8.6", + "@babel/types": "^7.8.6", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.0", + "lodash": "^4.17.13", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.6.tgz", + "integrity": "sha512-4bpOR5ZBz+wWcMeVtcf7FbjcFzCp+817z2/gHNncIRcM9MmKzUhtWCYAq27RAfUrAFwb+OCG1s9WEaVxfi6cjg==", + "dev": true, + "requires": { + "@babel/types": "^7.8.6", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/helper-function-name": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", + "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", + "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", + "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", + "dev": true + }, + "@babel/helper-split-export-declaration": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", + "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helpers": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.8.4.tgz", + "integrity": "sha512-VPbe7wcQ4chu4TDQjimHv/5tj73qz88o12EPkO2ValS2QiQS/1F2SsjyIGNnAD0vF/nZS6Cf9i+vW6HIlnaR8w==", + "dev": true, + "requires": { + "@babel/template": "^7.8.3", + "@babel/traverse": "^7.8.4", + "@babel/types": "^7.8.3" + } + }, + "@babel/highlight": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", + "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/parser": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.6.tgz", + "integrity": "sha512-trGNYSfwq5s0SgM1BMEB8hX3NDmO7EP2wsDGDexiaKMB92BaRpS+qZfpkMqUBhcsOTBwNy9B/jieo4ad/t/z2g==", + "dev": true + }, + "@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/template": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", + "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6" + } + }, + "@babel/traverse": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.6.tgz", + "integrity": "sha512-2B8l0db/DPi8iinITKuo7cbPznLCEk0kCxDoB9/N6gGNg/gxOXiR/IcymAFPiBwk5w6TtQ27w4wpElgp9btR9A==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.8.6", + "@babel/helper-function-name": "^7.8.3", + "@babel/helper-split-export-declaration": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + } + }, + "@babel/types": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.6.tgz", + "integrity": "sha512-wqz7pgWMIrht3gquyEFPVXeXCti72Rm8ep9b5tQKz9Yg9LzJA3HxosF1SB3Kc81KD1A3XBkkVYtJvCKS2Z/QrA==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "@cnakazawa/watch": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz", + "integrity": "sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==", + "dev": true, + "requires": { + "exec-sh": "^0.3.2", + "minimist": "^1.2.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "@istanbuljs/load-nyc-config": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz", + "integrity": "sha512-ZR0rq/f/E4f4XcgnDvtMWXCUJpi8eO0rssVhmztsZqLIEFA9UUP9zmpE0VxlM+kv/E1ul2I876Fwil2ayptDVg==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + } + }, + "@istanbuljs/schema": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", + "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", + "dev": true + }, + "@jest/console": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-25.1.0.tgz", + "integrity": "sha512-3P1DpqAMK/L07ag/Y9/Jup5iDEG9P4pRAuZiMQnU0JB3UOvCyYCjCoxr7sIA80SeyUCUKrr24fKAxVpmBgQonA==", + "dev": true, + "requires": { + "@jest/source-map": "^25.1.0", + "chalk": "^3.0.0", + "jest-util": "^25.1.0", + "slash": "^3.0.0" + } + }, + "@jest/core": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-25.1.0.tgz", + "integrity": "sha512-iz05+NmwCmZRzMXvMo6KFipW7nzhbpEawrKrkkdJzgytavPse0biEnCNr2wRlyCsp3SmKaEY+SGv7YWYQnIdig==", + "dev": true, + "requires": { + "@jest/console": "^25.1.0", + "@jest/reporters": "^25.1.0", + "@jest/test-result": "^25.1.0", + "@jest/transform": "^25.1.0", + "@jest/types": "^25.1.0", + "ansi-escapes": "^4.2.1", + "chalk": "^3.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.3", + "jest-changed-files": "^25.1.0", + "jest-config": "^25.1.0", + "jest-haste-map": "^25.1.0", + "jest-message-util": "^25.1.0", + "jest-regex-util": "^25.1.0", + "jest-resolve": "^25.1.0", + "jest-resolve-dependencies": "^25.1.0", + "jest-runner": "^25.1.0", + "jest-runtime": "^25.1.0", + "jest-snapshot": "^25.1.0", + "jest-util": "^25.1.0", + "jest-validate": "^25.1.0", + "jest-watcher": "^25.1.0", + "micromatch": "^4.0.2", + "p-each-series": "^2.1.0", + "realpath-native": "^1.1.0", + "rimraf": "^3.0.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "@jest/environment": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-25.1.0.tgz", + "integrity": "sha512-cTpUtsjU4cum53VqBDlcW0E4KbQF03Cn0jckGPW/5rrE9tb+porD3+hhLtHAwhthsqfyF+bizyodTlsRA++sHg==", + "dev": true, + "requires": { + "@jest/fake-timers": "^25.1.0", + "@jest/types": "^25.1.0", + "jest-mock": "^25.1.0" + } + }, + "@jest/fake-timers": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-25.1.0.tgz", + "integrity": "sha512-Eu3dysBzSAO1lD7cylZd/CVKdZZ1/43SF35iYBNV1Lvvn2Undp3Grwsv8PrzvbLhqwRzDd4zxrY4gsiHc+wygQ==", + "dev": true, + "requires": { + "@jest/types": "^25.1.0", + "jest-message-util": "^25.1.0", + "jest-mock": "^25.1.0", + "jest-util": "^25.1.0", + "lolex": "^5.0.0" + } + }, + "@jest/reporters": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-25.1.0.tgz", + "integrity": "sha512-ORLT7hq2acJQa8N+NKfs68ZtHFnJPxsGqmofxW7v7urVhzJvpKZG9M7FAcgh9Ee1ZbCteMrirHA3m5JfBtAaDg==", + "dev": true, + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^25.1.0", + "@jest/environment": "^25.1.0", + "@jest/test-result": "^25.1.0", + "@jest/transform": "^25.1.0", + "@jest/types": "^25.1.0", + "chalk": "^3.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.2", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^4.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.0", + "jest-haste-map": "^25.1.0", + "jest-resolve": "^25.1.0", + "jest-runtime": "^25.1.0", + "jest-util": "^25.1.0", + "jest-worker": "^25.1.0", + "node-notifier": "^6.0.0", + "slash": "^3.0.0", + "source-map": "^0.6.0", + "string-length": "^3.1.0", + "terminal-link": "^2.0.0", + "v8-to-istanbul": "^4.0.1" + } + }, + "@jest/source-map": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-25.1.0.tgz", + "integrity": "sha512-ohf2iKT0xnLWcIUhL6U6QN+CwFWf9XnrM2a6ybL9NXxJjgYijjLSitkYHIdzkd8wFliH73qj/+epIpTiWjRtAA==", + "dev": true, + "requires": { + "callsites": "^3.0.0", + "graceful-fs": "^4.2.3", + "source-map": "^0.6.0" + } + }, + "@jest/test-result": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-25.1.0.tgz", + "integrity": "sha512-FZzSo36h++U93vNWZ0KgvlNuZ9pnDnztvaM7P/UcTx87aPDotG18bXifkf1Ji44B7k/eIatmMzkBapnAzjkJkg==", + "dev": true, + "requires": { + "@jest/console": "^25.1.0", + "@jest/transform": "^25.1.0", + "@jest/types": "^25.1.0", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + } + }, + "@jest/test-sequencer": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-25.1.0.tgz", + "integrity": "sha512-WgZLRgVr2b4l/7ED1J1RJQBOharxS11EFhmwDqknpknE0Pm87HLZVS2Asuuw+HQdfQvm2aXL2FvvBLxOD1D0iw==", + "dev": true, + "requires": { + "@jest/test-result": "^25.1.0", + "jest-haste-map": "^25.1.0", + "jest-runner": "^25.1.0", + "jest-runtime": "^25.1.0" + } + }, + "@jest/transform": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-25.1.0.tgz", + "integrity": "sha512-4ktrQ2TPREVeM+KxB4zskAT84SnmG1vaz4S+51aTefyqn3zocZUnliLLm5Fsl85I3p/kFPN4CRp1RElIfXGegQ==", + "dev": true, + "requires": { + "@babel/core": "^7.1.0", + "@jest/types": "^25.1.0", + "babel-plugin-istanbul": "^6.0.0", + "chalk": "^3.0.0", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.2.3", + "jest-haste-map": "^25.1.0", + "jest-regex-util": "^25.1.0", + "jest-util": "^25.1.0", + "micromatch": "^4.0.2", + "pirates": "^4.0.1", + "realpath-native": "^1.1.0", + "slash": "^3.0.0", + "source-map": "^0.6.1", + "write-file-atomic": "^3.0.0" + } + }, + "@jest/types": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.1.0.tgz", + "integrity": "sha512-VpOtt7tCrgvamWZh1reVsGADujKigBUFTi19mlRjqEGsE8qH4r3s+skY33dNdXOwyZIvuftZ5tqdF1IgsMejMA==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@sinonjs/commons": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.7.1.tgz", + "integrity": "sha512-Debi3Baff1Qu1Unc3mjJ96MgpbwTn43S1+9yJ0llWygPwDNu2aaWBD6yc9y/Z8XDRNhx7U+u2UDg2OGQXkclUQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@types/babel__core": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.6.tgz", + "integrity": "sha512-tTnhWszAqvXnhW7m5jQU9PomXSiKXk2sFxpahXvI20SZKu9ylPi8WtIxueZ6ehDWikPT0jeFujMj3X4ZHuf3Tg==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.1.tgz", + "integrity": "sha512-bBKm+2VPJcMRVwNhxKu8W+5/zT7pwNEqeokFOmbvVSqGzFneNxYcEBro9Ac7/N9tlsaPYnZLK8J1LWKkMsLAew==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.0.2.tgz", + "integrity": "sha512-/K6zCpeW7Imzgab2bLkLEbz0+1JlFSrUMdw7KoIIu+IUdu51GWaBZpd3y1VXGVXzynvGa4DaIaxNZHiON3GXUg==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.9.tgz", + "integrity": "sha512-jEFQ8L1tuvPjOI8lnpaf73oCJe+aoxL6ygqSy6c8LcW98zaC+4mzWuQIRCEvKeCOu+lbqdXcg4Uqmm1S8AP1tw==", + "dev": true, + "requires": { + "@babel/types": "^7.3.0" + } + }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz", + "integrity": "sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg==", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.1.tgz", + "integrity": "sha512-UpYjBi8xefVChsCoBpKShdxTllC9pwISirfoZsUa2AAdQg/Jd2KQGtSbw+ya7GPo7x/wAPlH6JBhKhAsXUEZNA==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "@types/jest": { + "version": "25.1.3", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-25.1.3.tgz", + "integrity": "sha512-jqargqzyJWgWAJCXX96LBGR/Ei7wQcZBvRv0PLEu9ZByMfcs23keUJrKv9FMR6YZf9YCbfqDqgmY+JUBsnqhrg==", + "dev": true, + "requires": { + "jest-diff": "^25.1.0", + "pretty-format": "^25.1.0" + } + }, + "@types/node": { + "version": "12.12.29", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.29.tgz", + "integrity": "sha512-yo8Qz0ygADGFptISDj3pOC9wXfln/5pQaN/ysDIzOaAWXt73cNHmtEC8zSO2Y+kse/txmwIAJzkYZ5fooaS5DQ==", + "dev": true + }, + "@types/stack-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", + "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==", + "dev": true + }, + "@types/uuid": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-7.0.0.tgz", + "integrity": "sha512-RiX1I0lK9WFLFqy2xOxke396f0wKIzk5sAll0tL4J4XDYJXURI7JOs96XQb3nP+2gEpQ/LutBb66jgiT5oQshQ==", + "dev": true + }, + "@types/yargs": { + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.4.tgz", + "integrity": "sha512-9T1auFmbPZoxHz0enUFlUuKRy3it01R+hlggyVUMtnCTQRunsQYifnSGb8hET4Xo8yiC0o0r1paW3ud5+rbURg==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz", + "integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==", + "dev": true + }, + "abab": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz", + "integrity": "sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg==", + "dev": true + }, + "acorn": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", + "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", + "dev": true + }, + "acorn-globals": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz", + "integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==", + "dev": true, + "requires": { + "acorn": "^6.0.1", + "acorn-walk": "^6.0.1" + }, + "dependencies": { + "acorn": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.0.tgz", + "integrity": "sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw==", + "dev": true + } + } + }, + "acorn-walk": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", + "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==", + "dev": true + }, + "ajv": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", + "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-escapes": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "dev": true, + "requires": { + "type-fest": "^0.11.0" + } + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "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==", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "array-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", + "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dev": true, + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true + }, + "aws-sdk": { + "version": "2.635.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.635.0.tgz", + "integrity": "sha512-NlKqMB4HqMqSutY6YmPzQVa+mMhqo0655hYYl8G2zkUvrYy+YxDitvwDEUkSsNKVFkEvmHtZggFCgVYIUu/sXg==", + "requires": { + "buffer": "4.9.1", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.15.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "uuid": "3.3.2", + "xml2js": "0.4.19" + }, + "dependencies": { + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + } + } + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true + }, + "aws4": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz", + "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==", + "dev": true + }, + "babel-jest": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-25.1.0.tgz", + "integrity": "sha512-tz0VxUhhOE2y+g8R2oFrO/2VtVjA1lkJeavlhExuRBg3LdNJY9gwQ+Vcvqt9+cqy71MCTJhewvTB7Qtnnr9SWg==", + "dev": true, + "requires": { + "@jest/transform": "^25.1.0", + "@jest/types": "^25.1.0", + "@types/babel__core": "^7.1.0", + "babel-plugin-istanbul": "^6.0.0", + "babel-preset-jest": "^25.1.0", + "chalk": "^3.0.0", + "slash": "^3.0.0" + } + }, + "babel-plugin-istanbul": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz", + "integrity": "sha512-AF55rZXpe7trmEylbaE1Gv54wn6rwU03aptvRoVIGP8YykoSxqdVLV1TfwflBCE/QtHmqtP8SWlTENqbK8GCSQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^4.0.0", + "test-exclude": "^6.0.0" + } + }, + "babel-plugin-jest-hoist": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-25.1.0.tgz", + "integrity": "sha512-oIsopO41vW4YFZ9yNYoLQATnnN46lp+MZ6H4VvPKFkcc2/fkl3CfE/NZZSmnEIEsJRmJAgkVEK0R7Zbl50CpTw==", + "dev": true, + "requires": { + "@types/babel__traverse": "^7.0.6" + } + }, + "babel-preset-jest": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-25.1.0.tgz", + "integrity": "sha512-eCGn64olaqwUMaugXsTtGAM2I0QTahjEtnRu0ql8Ie+gDWAc1N6wqN0k2NilnyTunM69Pad7gJY7LOtwLimoFQ==", + "dev": true, + "requires": { + "@babel/plugin-syntax-bigint": "^7.0.0", + "@babel/plugin-syntax-object-rest-spread": "^7.0.0", + "babel-plugin-jest-hoist": "^25.1.0" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dev": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browser-process-hrtime": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz", + "integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==", + "dev": true + }, + "browser-resolve": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", + "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==", + "dev": true, + "requires": { + "resolve": "1.1.7" + }, + "dependencies": { + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + } + } + }, + "bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "requires": { + "fast-json-stable-stringify": "2.x" + } + }, + "bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "requires": { + "node-int64": "^0.4.0" + } + }, + "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-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "capture-exit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", + "integrity": "sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==", + "dev": true, + "requires": { + "rsvp": "^4.8.4" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "collect-v8-coverage": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.0.tgz", + "integrity": "sha512-VKIhJgvk8E1W28m5avZ2Gv2Ruv5YiF56ug2oclvaG9md69BuZImMG2sk9g7QNKLUbtYAKQjXjYxbYZVUlMMKmQ==", + "dev": true + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "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=", + "dev": true + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "cssom": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", + "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", + "dev": true + }, + "cssstyle": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.2.0.tgz", + "integrity": "sha512-sEb3XFPx3jNnCAMtqrXPDeSgQr+jojtCeNf8cvMNMh1cG970+lljssvQDzPq6lmmJu2Vhqood/gtEomBiHOGnA==", + "dev": true, + "requires": { + "cssom": "~0.3.6" + }, + "dependencies": { + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + } + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "data-urls": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", + "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", + "dev": true, + "requires": { + "abab": "^2.0.0", + "whatwg-mimetype": "^2.2.0", + "whatwg-url": "^7.0.0" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "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 + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true + }, + "diff-sequences": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.1.0.tgz", + "integrity": "sha512-nFIfVk5B/NStCsJ+zaPO4vYuLjlzQ6uFvPxzYyHlejNZ/UGa7G/n7peOXVrVNvRuyfstt+mZQYGpjxg9Z6N8Kw==", + "dev": true + }, + "domexception": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", + "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", + "dev": true, + "requires": { + "webidl-conversions": "^4.0.2" + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dev": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "es-abstract": { + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", + "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "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.14.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.1.tgz", + "integrity": "sha512-Bmt7NcRySdIfNPfU2ZoXDrrXsG9ZjvDxcAlMfDUgRBjLOWTuIACXPBFJH7Z+cLb40JeQco5toikyc9t9P8E9SQ==", + "dev": true, + "requires": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" + }, + "exec-sh": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz", + "integrity": "sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A==", + "dev": true + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "expect": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-25.1.0.tgz", + "integrity": "sha512-wqHzuoapQkhc3OKPlrpetsfueuEiMf3iWh0R8+duCu9PIjXoP7HgD5aeypwTnXUAjC8aMsiVDaWwlbJ1RlQ38g==", + "dev": true, + "requires": { + "@jest/types": "^25.1.0", + "ansi-styles": "^4.0.0", + "jest-get-type": "^25.1.0", + "jest-matcher-utils": "^25.1.0", + "jest-message-util": "^25.1.0", + "jest-regex-util": "^25.1.0" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "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 + }, + "fb-watchman": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", + "integrity": "sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==", + "dev": true, + "requires": { + "bser": "2.1.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz", + "integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "gensync": { + "version": "1.0.0-beta.1", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", + "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": 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" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "graceful-fs": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", + "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", + "dev": true + }, + "growly": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", + "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", + "dev": true, + "optional": true + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "dev": true, + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "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=", + "dev": true, + "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=", + "dev": true, + "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=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "html-encoding-sniffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", + "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", + "dev": true, + "requires": { + "whatwg-encoding": "^1.0.1" + } + }, + "html-escaper": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.0.tgz", + "integrity": "sha512-a4u9BeERWGu/S8JiWEAQcdrg9v4QArtP9keViQjGMdff20fBdd8waotXaNmODqBe6uZ3Nafi7K/ho4gCQHV3Ig==", + "dev": true + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, + "import-local": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", + "integrity": "sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==", + "dev": true, + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", + "dev": true + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "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=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-callable": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", + "dev": true + }, + "is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "requires": { + "ci-info": "^2.0.0" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "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=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-date-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", + "dev": true + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "is-regex": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", + "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "is-wsl": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.1.1.tgz", + "integrity": "sha512-umZHcSrwlDHo2TGMXv0DZ8dIUGunZ2Iv68YZnrmCiBPkZ4aaOhtv7pXJKeki9k3qJ3RJr0cDyitcl5wEH3AYog==", + "dev": true, + "optional": true + }, + "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": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", + "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.1.tgz", + "integrity": "sha512-imIchxnodll7pvQBYOqUu88EufLCU56LMeFPZZM/fJZ1irYcYdqroaV+ACK1Ila8ls09iEYArp+nqyC6lW1Vfg==", + "dev": true, + "requires": { + "@babel/core": "^7.7.5", + "@babel/parser": "^7.7.5", + "@babel/template": "^7.7.4", + "@babel/traverse": "^7.7.4", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", + "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + } + }, + "istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-2osTcC8zcOSUkImzN2EWQta3Vdi4WjjKw99P2yWx5mLnigAM0Rd5uYFn1cf2i/Ois45GkNjaoTqc5CxgMSX80A==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jest": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-25.1.0.tgz", + "integrity": "sha512-FV6jEruneBhokkt9MQk0WUFoNTwnF76CLXtwNMfsc0um0TlB/LG2yxUd0KqaFjEJ9laQmVWQWS0sG/t2GsuI0w==", + "dev": true, + "requires": { + "@jest/core": "^25.1.0", + "import-local": "^3.0.2", + "jest-cli": "^25.1.0" + }, + "dependencies": { + "jest-cli": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-25.1.0.tgz", + "integrity": "sha512-p+aOfczzzKdo3AsLJlhs8J5EW6ffVidfSZZxXedJ0mHPBOln1DccqFmGCoO8JWd4xRycfmwy1eoQkMsF8oekPg==", + "dev": true, + "requires": { + "@jest/core": "^25.1.0", + "@jest/test-result": "^25.1.0", + "@jest/types": "^25.1.0", + "chalk": "^3.0.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "is-ci": "^2.0.0", + "jest-config": "^25.1.0", + "jest-util": "^25.1.0", + "jest-validate": "^25.1.0", + "prompts": "^2.0.1", + "realpath-native": "^1.1.0", + "yargs": "^15.0.0" + } + } + } + }, + "jest-changed-files": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-25.1.0.tgz", + "integrity": "sha512-bdL1aHjIVy3HaBO3eEQeemGttsq1BDlHgWcOjEOIAcga7OOEGWHD2WSu8HhL7I1F0mFFyci8VKU4tRNk+qtwDA==", + "dev": true, + "requires": { + "@jest/types": "^25.1.0", + "execa": "^3.2.0", + "throat": "^5.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", + "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "execa": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-3.4.0.tgz", + "integrity": "sha512-r9vdGQk4bmCuK1yKQu1KTwcT2zwfWdbdaXfCtAh+5nU/4fSX+JAb7vZGvI5naJrQlvONrEB20jeruESI69530g==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "p-finally": "^2.0.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + } + }, + "get-stream": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", + "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "dev": true + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, + "p-finally": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz", + "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "jest-config": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-25.1.0.tgz", + "integrity": "sha512-tLmsg4SZ5H7tuhBC5bOja0HEblM0coS3Wy5LTCb2C8ZV6eWLewHyK+3qSq9Bi29zmWQ7ojdCd3pxpx4l4d2uGw==", + "dev": true, + "requires": { + "@babel/core": "^7.1.0", + "@jest/test-sequencer": "^25.1.0", + "@jest/types": "^25.1.0", + "babel-jest": "^25.1.0", + "chalk": "^3.0.0", + "glob": "^7.1.1", + "jest-environment-jsdom": "^25.1.0", + "jest-environment-node": "^25.1.0", + "jest-get-type": "^25.1.0", + "jest-jasmine2": "^25.1.0", + "jest-regex-util": "^25.1.0", + "jest-resolve": "^25.1.0", + "jest-util": "^25.1.0", + "jest-validate": "^25.1.0", + "micromatch": "^4.0.2", + "pretty-format": "^25.1.0", + "realpath-native": "^1.1.0" + } + }, + "jest-diff": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.1.0.tgz", + "integrity": "sha512-nepXgajT+h017APJTreSieh4zCqnSHEJ1iT8HDlewu630lSJ4Kjjr9KNzm+kzGwwcpsDE6Snx1GJGzzsefaEHw==", + "dev": true, + "requires": { + "chalk": "^3.0.0", + "diff-sequences": "^25.1.0", + "jest-get-type": "^25.1.0", + "pretty-format": "^25.1.0" + } + }, + "jest-docblock": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-25.1.0.tgz", + "integrity": "sha512-370P/mh1wzoef6hUKiaMcsPtIapY25suP6JqM70V9RJvdKLrV4GaGbfUseUVk4FZJw4oTZ1qSCJNdrClKt5JQA==", + "dev": true, + "requires": { + "detect-newline": "^3.0.0" + } + }, + "jest-each": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-25.1.0.tgz", + "integrity": "sha512-R9EL8xWzoPySJ5wa0DXFTj7NrzKpRD40Jy+zQDp3Qr/2QmevJgkN9GqioCGtAJ2bW9P/MQRznQHQQhoeAyra7A==", + "dev": true, + "requires": { + "@jest/types": "^25.1.0", + "chalk": "^3.0.0", + "jest-get-type": "^25.1.0", + "jest-util": "^25.1.0", + "pretty-format": "^25.1.0" + } + }, + "jest-environment-jsdom": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-25.1.0.tgz", + "integrity": "sha512-ILb4wdrwPAOHX6W82GGDUiaXSSOE274ciuov0lztOIymTChKFtC02ddyicRRCdZlB5YSrv3vzr1Z5xjpEe1OHQ==", + "dev": true, + "requires": { + "@jest/environment": "^25.1.0", + "@jest/fake-timers": "^25.1.0", + "@jest/types": "^25.1.0", + "jest-mock": "^25.1.0", + "jest-util": "^25.1.0", + "jsdom": "^15.1.1" + } + }, + "jest-environment-node": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-25.1.0.tgz", + "integrity": "sha512-U9kFWTtAPvhgYY5upnH9rq8qZkj6mYLup5l1caAjjx9uNnkLHN2xgZy5mo4SyLdmrh/EtB9UPpKFShvfQHD0Iw==", + "dev": true, + "requires": { + "@jest/environment": "^25.1.0", + "@jest/fake-timers": "^25.1.0", + "@jest/types": "^25.1.0", + "jest-mock": "^25.1.0", + "jest-util": "^25.1.0" + } + }, + "jest-get-type": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.1.0.tgz", + "integrity": "sha512-yWkBnT+5tMr8ANB6V+OjmrIJufHtCAqI5ic2H40v+tRqxDmE0PGnIiTyvRWFOMtmVHYpwRqyazDbTnhpjsGvLw==", + "dev": true + }, + "jest-haste-map": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-25.1.0.tgz", + "integrity": "sha512-/2oYINIdnQZAqyWSn1GTku571aAfs8NxzSErGek65Iu5o8JYb+113bZysRMcC/pjE5v9w0Yz+ldbj9NxrFyPyw==", + "dev": true, + "requires": { + "@jest/types": "^25.1.0", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.1.2", + "graceful-fs": "^4.2.3", + "jest-serializer": "^25.1.0", + "jest-util": "^25.1.0", + "jest-worker": "^25.1.0", + "micromatch": "^4.0.2", + "sane": "^4.0.3", + "walker": "^1.0.7" + } + }, + "jest-jasmine2": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-25.1.0.tgz", + "integrity": "sha512-GdncRq7jJ7sNIQ+dnXvpKO2MyP6j3naNK41DTTjEAhLEdpImaDA9zSAZwDhijjSF/D7cf4O5fdyUApGBZleaEg==", + "dev": true, + "requires": { + "@babel/traverse": "^7.1.0", + "@jest/environment": "^25.1.0", + "@jest/source-map": "^25.1.0", + "@jest/test-result": "^25.1.0", + "@jest/types": "^25.1.0", + "chalk": "^3.0.0", + "co": "^4.6.0", + "expect": "^25.1.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^25.1.0", + "jest-matcher-utils": "^25.1.0", + "jest-message-util": "^25.1.0", + "jest-runtime": "^25.1.0", + "jest-snapshot": "^25.1.0", + "jest-util": "^25.1.0", + "pretty-format": "^25.1.0", + "throat": "^5.0.0" + } + }, + "jest-leak-detector": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-25.1.0.tgz", + "integrity": "sha512-3xRI264dnhGaMHRvkFyEKpDeaRzcEBhyNrOG5oT8xPxOyUAblIAQnpiR3QXu4wDor47MDTiHbiFcbypdLcLW5w==", + "dev": true, + "requires": { + "jest-get-type": "^25.1.0", + "pretty-format": "^25.1.0" + } + }, + "jest-matcher-utils": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-25.1.0.tgz", + "integrity": "sha512-KGOAFcSFbclXIFE7bS4C53iYobKI20ZWleAdAFun4W1Wz1Kkej8Ng6RRbhL8leaEvIOjGXhGf/a1JjO8bkxIWQ==", + "dev": true, + "requires": { + "chalk": "^3.0.0", + "jest-diff": "^25.1.0", + "jest-get-type": "^25.1.0", + "pretty-format": "^25.1.0" + } + }, + "jest-message-util": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-25.1.0.tgz", + "integrity": "sha512-Nr/Iwar2COfN22aCqX0kCVbXgn8IBm9nWf4xwGr5Olv/KZh0CZ32RKgZWMVDXGdOahicM10/fgjdimGNX/ttCQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@jest/test-result": "^25.1.0", + "@jest/types": "^25.1.0", + "@types/stack-utils": "^1.0.1", + "chalk": "^3.0.0", + "micromatch": "^4.0.2", + "slash": "^3.0.0", + "stack-utils": "^1.0.1" + } + }, + "jest-mock": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-25.1.0.tgz", + "integrity": "sha512-28/u0sqS+42vIfcd1mlcg4ZVDmSUYuNvImP4X2lX5hRMLW+CN0BeiKVD4p+ujKKbSPKd3rg/zuhCF+QBLJ4vag==", + "dev": true, + "requires": { + "@jest/types": "^25.1.0" + } + }, + "jest-pnp-resolver": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.1.tgz", + "integrity": "sha512-pgFw2tm54fzgYvc/OHrnysABEObZCUNFnhjoRjaVOCN8NYc032/gVjPaHD4Aq6ApkSieWtfKAFQtmDKAmhupnQ==", + "dev": true + }, + "jest-regex-util": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-25.1.0.tgz", + "integrity": "sha512-9lShaDmDpqwg+xAd73zHydKrBbbrIi08Kk9YryBEBybQFg/lBWR/2BDjjiSE7KIppM9C5+c03XiDaZ+m4Pgs1w==", + "dev": true + }, + "jest-resolve": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-25.1.0.tgz", + "integrity": "sha512-XkBQaU1SRCHj2Evz2Lu4Czs+uIgJXWypfO57L7JYccmAXv4slXA6hzNblmcRmf7P3cQ1mE7fL3ABV6jAwk4foQ==", + "dev": true, + "requires": { + "@jest/types": "^25.1.0", + "browser-resolve": "^1.11.3", + "chalk": "^3.0.0", + "jest-pnp-resolver": "^1.2.1", + "realpath-native": "^1.1.0" + } + }, + "jest-resolve-dependencies": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-25.1.0.tgz", + "integrity": "sha512-Cu/Je38GSsccNy4I2vL12ZnBlD170x2Oh1devzuM9TLH5rrnLW1x51lN8kpZLYTvzx9j+77Y5pqBaTqfdzVzrw==", + "dev": true, + "requires": { + "@jest/types": "^25.1.0", + "jest-regex-util": "^25.1.0", + "jest-snapshot": "^25.1.0" + } + }, + "jest-runner": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-25.1.0.tgz", + "integrity": "sha512-su3O5fy0ehwgt+e8Wy7A8CaxxAOCMzL4gUBftSs0Ip32S0epxyZPDov9Znvkl1nhVOJNf4UwAsnqfc3plfQH9w==", + "dev": true, + "requires": { + "@jest/console": "^25.1.0", + "@jest/environment": "^25.1.0", + "@jest/test-result": "^25.1.0", + "@jest/types": "^25.1.0", + "chalk": "^3.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.3", + "jest-config": "^25.1.0", + "jest-docblock": "^25.1.0", + "jest-haste-map": "^25.1.0", + "jest-jasmine2": "^25.1.0", + "jest-leak-detector": "^25.1.0", + "jest-message-util": "^25.1.0", + "jest-resolve": "^25.1.0", + "jest-runtime": "^25.1.0", + "jest-util": "^25.1.0", + "jest-worker": "^25.1.0", + "source-map-support": "^0.5.6", + "throat": "^5.0.0" + } + }, + "jest-runtime": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-25.1.0.tgz", + "integrity": "sha512-mpPYYEdbExKBIBB16ryF6FLZTc1Rbk9Nx0ryIpIMiDDkOeGa0jQOKVI/QeGvVGlunKKm62ywcioeFVzIbK03bA==", + "dev": true, + "requires": { + "@jest/console": "^25.1.0", + "@jest/environment": "^25.1.0", + "@jest/source-map": "^25.1.0", + "@jest/test-result": "^25.1.0", + "@jest/transform": "^25.1.0", + "@jest/types": "^25.1.0", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.3", + "jest-config": "^25.1.0", + "jest-haste-map": "^25.1.0", + "jest-message-util": "^25.1.0", + "jest-mock": "^25.1.0", + "jest-regex-util": "^25.1.0", + "jest-resolve": "^25.1.0", + "jest-snapshot": "^25.1.0", + "jest-util": "^25.1.0", + "jest-validate": "^25.1.0", + "realpath-native": "^1.1.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0", + "yargs": "^15.0.0" + } + }, + "jest-serializer": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-25.1.0.tgz", + "integrity": "sha512-20Wkq5j7o84kssBwvyuJ7Xhn7hdPeTXndnwIblKDR2/sy1SUm6rWWiG9kSCgJPIfkDScJCIsTtOKdlzfIHOfKA==", + "dev": true + }, + "jest-snapshot": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-25.1.0.tgz", + "integrity": "sha512-xZ73dFYN8b/+X2hKLXz4VpBZGIAn7muD/DAg+pXtDzDGw3iIV10jM7WiHqhCcpDZfGiKEj7/2HXAEPtHTj0P2A==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0", + "@jest/types": "^25.1.0", + "chalk": "^3.0.0", + "expect": "^25.1.0", + "jest-diff": "^25.1.0", + "jest-get-type": "^25.1.0", + "jest-matcher-utils": "^25.1.0", + "jest-message-util": "^25.1.0", + "jest-resolve": "^25.1.0", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "pretty-format": "^25.1.0", + "semver": "^7.1.1" + }, + "dependencies": { + "semver": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.1.3.tgz", + "integrity": "sha512-ekM0zfiA9SCBlsKa2X1hxyxiI4L3B6EbVJkkdgQXnSEEaHlGdvyodMruTiulSRWMMB4NeIuYNMC9rTKTz97GxA==", + "dev": true + } + } + }, + "jest-util": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-25.1.0.tgz", + "integrity": "sha512-7did6pLQ++87Qsj26Fs/TIwZMUFBXQ+4XXSodRNy3luch2DnRXsSnmpVtxxQ0Yd6WTipGpbhh2IFP1mq6/fQGw==", + "dev": true, + "requires": { + "@jest/types": "^25.1.0", + "chalk": "^3.0.0", + "is-ci": "^2.0.0", + "mkdirp": "^0.5.1" + } + }, + "jest-validate": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-25.1.0.tgz", + "integrity": "sha512-kGbZq1f02/zVO2+t1KQGSVoCTERc5XeObLwITqC6BTRH3Adv7NZdYqCpKIZLUgpLXf2yISzQ465qOZpul8abXA==", + "dev": true, + "requires": { + "@jest/types": "^25.1.0", + "camelcase": "^5.3.1", + "chalk": "^3.0.0", + "jest-get-type": "^25.1.0", + "leven": "^3.1.0", + "pretty-format": "^25.1.0" + } + }, + "jest-watcher": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-25.1.0.tgz", + "integrity": "sha512-Q9eZ7pyaIr6xfU24OeTg4z1fUqBF/4MP6J801lyQfg7CsnZ/TCzAPvCfckKdL5dlBBEKBeHV0AdyjFZ5eWj4ig==", + "dev": true, + "requires": { + "@jest/test-result": "^25.1.0", + "@jest/types": "^25.1.0", + "ansi-escapes": "^4.2.1", + "chalk": "^3.0.0", + "jest-util": "^25.1.0", + "string-length": "^3.1.0" + } + }, + "jest-worker": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-25.1.0.tgz", + "integrity": "sha512-ZHhHtlxOWSxCoNOKHGbiLzXnl42ga9CxDr27H36Qn+15pQZd3R/F24jrmjDelw9j/iHUIWMWs08/u2QN50HHOg==", + "dev": true, + "requires": { + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + } + }, + "jmespath": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", + "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true + }, + "jsdom": { + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-15.2.1.tgz", + "integrity": "sha512-fAl1W0/7T2G5vURSyxBzrJ1LSdQn6Tr5UX/xD4PXDx/PDgwygedfW6El/KIj3xJ7FU61TTYnc/l/B7P49Eqt6g==", + "dev": true, + "requires": { + "abab": "^2.0.0", + "acorn": "^7.1.0", + "acorn-globals": "^4.3.2", + "array-equal": "^1.0.0", + "cssom": "^0.4.1", + "cssstyle": "^2.0.0", + "data-urls": "^1.1.0", + "domexception": "^1.0.1", + "escodegen": "^1.11.1", + "html-encoding-sniffer": "^1.0.2", + "nwsapi": "^2.2.0", + "parse5": "5.1.0", + "pn": "^1.1.0", + "request": "^2.88.0", + "request-promise-native": "^1.0.7", + "saxes": "^3.1.9", + "symbol-tree": "^3.2.2", + "tough-cookie": "^3.0.1", + "w3c-hr-time": "^1.0.1", + "w3c-xmlserializer": "^1.1.2", + "webidl-conversions": "^4.0.2", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^7.0.0", + "ws": "^7.0.0", + "xml-name-validator": "^3.0.0" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "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=", + "dev": true + }, + "json5": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.1.tgz", + "integrity": "sha512-l+3HXD0GEI3huGq1njuqtzYK8OYJyXMkOLtQ53pjWh89tvWS2h6l+1zMkYWqlb57+SiQodKZyvMEFb2X+KrFhQ==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true + }, + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true + }, + "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" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", + "dev": true + }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", + "dev": true + }, + "lolex": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz", + "integrity": "sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "make-dir": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.2.tgz", + "integrity": "sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w==", + "dev": true, + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "makeerror": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", + "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", + "dev": true, + "requires": { + "tmpl": "1.0.x" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "mime-db": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", + "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==", + "dev": true + }, + "mime-types": { + "version": "2.1.26", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", + "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", + "dev": true, + "requires": { + "mime-db": "1.43.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "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=", + "dev": true + }, + "mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", + "dev": true + }, + "node-modules-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", + "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=", + "dev": true + }, + "node-notifier": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-6.0.0.tgz", + "integrity": "sha512-SVfQ/wMw+DesunOm5cKqr6yDcvUTDl/yc97ybGHMrteNEY6oekXpNpS3lZwgLlwz0FLgHoiW28ZpmBHUDg37cw==", + "dev": true, + "optional": true, + "requires": { + "growly": "^1.3.0", + "is-wsl": "^2.1.1", + "semver": "^6.3.0", + "shellwords": "^0.1.1", + "which": "^1.3.1" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "optional": true + } + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "nwsapi": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", + "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", + "dev": true + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-inspect": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "^3.0.0" + } + }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, + "object.getownpropertydescriptors": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz", + "integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", + "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "p-each-series": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.1.0.tgz", + "integrity": "sha512-ZuRs1miPT4HrjFa+9fRfOFXxGJfORgelKV9f9nNOWw2gl6gVsRaVDOQP0+MI0G0wGKns1Yacsu0GjOFbTK0JFQ==", + "dev": true + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-limit": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", + "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "parse5": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", + "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==", + "dev": true + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "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=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true + }, + "picomatch": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.1.tgz", + "integrity": "sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==", + "dev": true + }, + "pirates": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", + "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==", + "dev": true, + "requires": { + "node-modules-regexp": "^1.0.0" + } + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "pn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", + "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", + "dev": true + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, + "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 + }, + "pretty-format": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.1.0.tgz", + "integrity": "sha512-46zLRSGLd02Rp+Lhad9zzuNZ+swunitn8zIpfD2B4OPCRLXbM87RJT2aBLBWYOznNUML/2l/ReMyWNC80PJBUQ==", + "dev": true, + "requires": { + "@jest/types": "^25.1.0", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^16.12.0" + } + }, + "prompts": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.3.1.tgz", + "integrity": "sha512-qIP2lQyCwYbdzcqHIUi2HAxiWixhoM9OdLCWf8txXsapC/X9YdsCoeyRIXE/GP+Q0J37Q7+XN/MFqbUa7IzXNA==", + "dev": true, + "requires": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.4" + } + }, + "psl": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.7.0.tgz", + "integrity": "sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==", + "dev": true + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, + "react-is": { + "version": "16.13.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.0.tgz", + "integrity": "sha512-GFMtL0vHkiBv9HluwNZTggSn/sCyEt9n02aM0dSAjGGyqyNlAyftYm4phPxdvCigG15JreC5biwxCgTAJZ7yAA==", + "dev": true + }, + "realpath-native": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-1.1.0.tgz", + "integrity": "sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA==", + "dev": true, + "requires": { + "util.promisify": "^1.0.0" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "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=", + "dev": true + }, + "repeat-element": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } + } + }, + "request-promise-core": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz", + "integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==", + "dev": true, + "requires": { + "lodash": "^4.17.15" + } + }, + "request-promise-native": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.8.tgz", + "integrity": "sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ==", + "dev": true, + "requires": { + "request-promise-core": "1.1.3", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + } + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "resolve": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz", + "integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "rsvp": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", + "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", + "dev": true + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "sane": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/sane/-/sane-4.1.0.tgz", + "integrity": "sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==", + "dev": true, + "requires": { + "@cnakazawa/watch": "^1.0.3", + "anymatch": "^2.0.0", + "capture-exit": "^2.0.0", + "exec-sh": "^0.3.2", + "execa": "^1.0.0", + "fb-watchman": "^2.0.0", + "micromatch": "^3.1.4", + "minimist": "^1.1.1", + "walker": "~1.0.5" + }, + "dependencies": { + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "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=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" + }, + "saxes": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.11.tgz", + "integrity": "sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g==", + "dev": true, + "requires": { + "xmlchars": "^2.1.1" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "shellwords": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", + "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "sisteransi": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.4.tgz", + "integrity": "sha512-/ekMoM4NJ59ivGSfKapeG+FWtrmWvA1p6FBZwXrqojw90vJu8lBmrTxCMuBCydKtkaUe2zt4PlxeTKpjwMbyig==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "dev": true, + "requires": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-support": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz", + "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "dev": 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", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "stack-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.2.tgz", + "integrity": "sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==", + "dev": true + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", + "dev": true + }, + "string-length": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-3.1.0.tgz", + "integrity": "sha512-Ttp5YvkGm5v9Ijagtaz1BnN+k9ObpvS0eIBblPMp2YWL8FBmi9qblQ9fexc2k/CXFgrTIteU3jAw3payCnwSTA==", + "dev": true, + "requires": { + "astral-regex": "^1.0.0", + "strip-ansi": "^5.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "string.prototype.trimleft": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", + "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + }, + "string.prototype.trimright": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", + "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-hyperlinks": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.1.0.tgz", + "integrity": "sha512-zoE5/e+dnEijk6ASB6/qrK+oYdm2do1hjoLWrqUC/8WEIW1gbxFcKuBof7sW8ArN6e+AYvsE8HBGiVRWL/F5CA==", + "dev": true, + "requires": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + } + }, + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, + "terminal-link": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", + "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + } + }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, + "throat": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", + "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", + "dev": true + }, + "tmpl": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", + "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "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=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "tombok": { + "version": "git+https://github.com/eduardomourar/tombok.git#a6f84ddcdb2d145f5bcd42f3882e0f00532abee7", + "from": "git+https://github.com/eduardomourar/tombok.git#feature/basic-implementation", + "dev": true + }, + "tough-cookie": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", + "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", + "dev": true, + "requires": { + "ip-regex": "^2.1.0", + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + } + } + }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", + "dev": true, + "requires": { + "punycode": "^2.1.0" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + } + } + }, + "ts-jest": { + "version": "25.2.1", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-25.2.1.tgz", + "integrity": "sha512-TnntkEEjuXq/Gxpw7xToarmHbAafgCaAzOpnajnFC6jI7oo1trMzAHA04eWpc3MhV6+yvhE8uUBAmN+teRJh0A==", + "dev": true, + "requires": { + "bs-logger": "0.x", + "buffer-from": "1.x", + "fast-json-stable-stringify": "2.x", + "json5": "2.x", + "lodash.memoize": "4.x", + "make-error": "1.x", + "mkdirp": "0.x", + "resolve": "1.x", + "semver": "^5.5", + "yargs-parser": "^16.1.0" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "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=", + "dev": 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.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true + }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "requires": { + "is-typedarray": "^1.0.0" + } + }, + "typescript": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", + "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", + "dev": true + }, + "union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + } + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + } + } + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + } + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "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" + } + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true + }, + "util.promisify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz", + "integrity": "sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.2", + "has-symbols": "^1.0.1", + "object.getownpropertydescriptors": "^2.1.0" + } + }, + "uuid": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.2.tgz", + "integrity": "sha512-vy9V/+pKG+5ZTYKf+VcphF5Oc6EFiu3W8Nv3P3zIh0EqVI80ZxOzuPfe9EHjkFNvf8+xuTHVeei4Drydlx4zjw==" + }, + "v8-to-istanbul": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-4.1.2.tgz", + "integrity": "sha512-G9R+Hpw0ITAmPSr47lSlc5A1uekSYzXxTMlFxso2xoffwo4jQnzbv1p9yXIinO8UMZKfAFewaCHwWvnH4Jb4Ug==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0", + "source-map": "^0.7.3" + }, + "dependencies": { + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + } + } + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "w3c-hr-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz", + "integrity": "sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=", + "dev": true, + "requires": { + "browser-process-hrtime": "^0.1.2" + } + }, + "w3c-xmlserializer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-1.1.2.tgz", + "integrity": "sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg==", + "dev": true, + "requires": { + "domexception": "^1.0.1", + "webidl-conversions": "^4.0.2", + "xml-name-validator": "^3.0.0" + } + }, + "walker": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", + "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", + "dev": true, + "requires": { + "makeerror": "1.0.x" + } + }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "dev": true, + "requires": { + "iconv-lite": "0.4.24" + } + }, + "whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", + "dev": true + }, + "whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "ws": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.1.tgz", + "integrity": "sha512-sucePNSafamSKoOqoNfBd8V0StlkzJKL2ZAhGQinCfNQ+oacw+Pk7lcdAElecBF2VkLNZRiIb5Oi1Q5lVUVt2A==", + "dev": true + }, + "xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", + "dev": true + }, + "xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~9.0.1" + } + }, + "xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" + }, + "xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yargs": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.1.0.tgz", + "integrity": "sha512-T39FNN1b6hCW4SOIk1XyTOWxtXdcen0t+XYrysQmChzSipvhBO8Bj0nK1ozAasdk24dNWuMZvr4k24nz+8HHLg==", + "dev": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^16.1.0" + } + }, + "yargs-parser": { + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-16.1.0.tgz", + "integrity": "sha512-H/V41UNZQPkUMIT5h5hiwg4QKIY1RPvoBV4XcjUbRM8Bk2oKqqyZ0DIEbTFZB0XjbtSPG8SAa/0DxCQmiRgzKg==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ae1af81 --- /dev/null +++ b/package.json @@ -0,0 +1,45 @@ +{ + "name": "cfn-rpdk", + "version": "0.0.1", + "description": "The CloudFormation Resource Provider Development Kit (RPDK) allows you to author your own resource providers that can be used by CloudFormation. This plugin library helps to provide runtime bindings for the execution of your providers by CloudFormation.", + "main": "dist/index.js", + "directories": { + "test": "tests" + }, + "files": [ + "dist", + "global.d.ts" + ], + "scripts": { + "build": "npx tsc", + "test": "npx jest", + "test:debug": "npx --node-arg=--inspect jest --runInBand" + }, + "engines": { + "node": ">=10.0.0", + "npm": ">=5.6.0" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/eduardomourar/cloudformation-cli-typescript-plugin.git" + }, + "author": "eduardomourar", + "license": "MIT", + "bugs": { + "url": "https://github.com/eduardomourar/cloudformation-cli-typescript-plugin/issues" + }, + "homepage": "https://github.com/eduardomourar/cloudformation-cli-typescript-plugin#readme", + "dependencies": { + "aws-sdk": "^2.635.0", + "uuid": "^7.0.2" + }, + "devDependencies": { + "@types/jest": "^25.1.0", + "@types/node": "^12.0.0", + "@types/uuid": "^7.0.0", + "jest": "^25.1.0", + "tombok": "git+https://github.com/eduardomourar/tombok.git#feature/basic-implementation", + "ts-jest": "^25.2.1", + "typescript": "^3.8.3" + } +} diff --git a/src/callback.ts b/src/callback.ts new file mode 100644 index 0000000..b23f0d8 --- /dev/null +++ b/src/callback.ts @@ -0,0 +1,48 @@ +import { v4 as uuidv4 } from 'uuid'; + +import { + SessionProxy, +} from './proxy'; +import { BaseResourceModel, OperationStatus, Response } from './interface'; +import { KitchenSinkEncoder } from './utils'; + + +const LOG = console; + +interface ProgressOptions extends Response { + session: SessionProxy, + currentOperationStatus?: OperationStatus, +} + +export async function reportProgress(options: ProgressOptions): Promise { + + const { + session, + bearerToken, + errorCode, + operationStatus, + currentOperationStatus, + resourceModel, + message, + } = options; + const client = session.client('CloudFormation'); + + const request: { [key: string]: any; } = { + BearerToken: bearerToken, + OperationStatus: operationStatus, + StatusMessage: message, + ClientRequestToken: uuidv4(), + }; + if (resourceModel) { + request.ResourceModel = JSON.stringify(resourceModel); + } + if (errorCode) { + request.ErrorCode = errorCode; + } + if (currentOperationStatus) { + request['CurrentOperationStatus'] = currentOperationStatus; + const response: { [key: string]: any; } = await client.makeRequest('recordHandlerProgress', request).promise() + const requestId = response['ResponseMetadata']['RequestId']; + LOG.info(`Record Handler Progress with Request Id ${requestId} and Request: ${request}`); + } +} diff --git a/src/exceptions.ts b/src/exceptions.ts new file mode 100644 index 0000000..2a6e53a --- /dev/null +++ b/src/exceptions.ts @@ -0,0 +1,58 @@ +import { HandlerErrorCode } from './interface'; +import { ProgressEvent } from './proxy'; + +export abstract class BaseHandlerException extends Error { + static serialVersionUID: number = -1646136434112354328; + + private errorCode: HandlerErrorCode; + + public constructor(message? : any, errorCode? : HandlerErrorCode) { + super(message); + this.errorCode = errorCode || HandlerErrorCode[this.constructor.name as HandlerErrorCode]; + Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain + } + + public toProgressEvent(): ProgressEvent { + return ProgressEvent.failed(this.errorCode, this.toString()); + } +} + +export class NotUpdatable extends BaseHandlerException {} + +export class InvalidRequest extends BaseHandlerException {} + +export class AccessDenied extends BaseHandlerException {} + +export class InvalidCredentials extends BaseHandlerException {} + +export class AlreadyExists extends BaseHandlerException { + constructor(typeName: string, identifier: string) { + super( + `Resource of type '${typeName}' with identifier '${identifier}' already exists.` + ) + } +} + +export class NotFound extends BaseHandlerException { + constructor(typeName: string, identifier: string) { + super( + `Resource of type '${typeName}' with identifier '${identifier}' was not found.` + ) + } +} + +export class ResourceConflict extends BaseHandlerException {} + +export class Throttling extends BaseHandlerException {} + +export class ServiceLimitExceeded extends BaseHandlerException {} + +export class NotStabilized extends BaseHandlerException {} + +export class GeneralServiceException extends BaseHandlerException {} + +export class ServiceInternalError extends BaseHandlerException {} + +export class NetworkFailure extends BaseHandlerException {} + +export class InternalFailure extends BaseHandlerException {} diff --git a/src/interface.ts b/src/interface.ts new file mode 100644 index 0000000..3a9b63e --- /dev/null +++ b/src/interface.ts @@ -0,0 +1,90 @@ +import { + ClientRequestToken, + LogicalResourceId, + NextToken, +} from 'aws-sdk/clients/cloudformation'; + +export type Optional = T | undefined | null; + +export interface Callable, T> { + (...args: R): T; +} + +export enum Action { + Create = "CREATE", + Read = "READ", + Update = "UPDATE", + Delete = "DELETE", + List = "LIST", +} + +export enum StandardUnit { + Count = "Count", + Milliseconds = "Milliseconds", +} + +export enum MetricTypes { + HandlerException = "HandlerException", + HandlerInvocationCount = "HandlerInvocationCount", + HandlerInvocationDuration = "HandlerInvocationDuration", +} + +export enum OperationStatus { + Pending = "PENDING", + InProgress = "IN_PROGRESS", + Success = "SUCCESS", + Failed = "FAILED", +} + +export enum HandlerErrorCode { + NotUpdatable = "NotUpdatable", + InvalidRequest = "InvalidRequest", + AccessDenied = "AccessDenied", + InvalidCredentials = "InvalidCredentials", + AlreadyExists = "AlreadyExists", + NotFound = "NotFound", + ResourceConflict = "ResourceConflict", + Throttling = "Throttling", + ServiceLimitExceeded = "ServiceLimitExceeded", + NotStabilized = "NotStabilized", + GeneralServiceException = "GeneralServiceException", + ServiceInternalError = "ServiceInternalError", + NetworkFailure = "NetworkFailure", + InternalFailure = "InternalFailure", +} + +export interface Credentials { + accessKeyId: string; + secretAccessKey: string; + sessionToken: string; +} + +export interface RequestContext { + invocation: number; + callbackContext: CallbackT; + cloudWatchEventsRuleName: string; + cloudWatchEventsTargetId: string; +} + +export interface BaseResourceModel { + serialize(): Map; + deserialize(): BaseResourceModel; +} + +export interface BaseResourceHandlerRequest { + clientRequestToken: ClientRequestToken; + desiredResourceState?: T; + previousResourceState?: T; + logicalResourceIdentifier?: LogicalResourceId; + nextToken?: NextToken; +} + +export interface Response { + bearerToken: string; + errorCode?: HandlerErrorCode; + operationStatus: OperationStatus; + message: string; + resourceModel?: T; + resourceModels?: T[]; + nextToken?: NextToken; +} diff --git a/src/proxy.ts b/src/proxy.ts new file mode 100644 index 0000000..1b24786 --- /dev/null +++ b/src/proxy.ts @@ -0,0 +1,151 @@ +import { ConfigurationOptions } from 'aws-sdk/lib/config'; +import { CredentialsOptions } from 'aws-sdk/lib/credentials'; +import * as Aws from 'aws-sdk/clients/all'; +import { NextToken } from 'aws-sdk/clients/cloudformation'; +import { allArgsConstructor, builder } from 'tombok'; + +import { + BaseResourceModel, + HandlerErrorCode, + OperationStatus, +} from './interface'; + + +type ClientMap = typeof Aws; +type Client = InstanceType; + +export class SessionProxy { + + constructor(private options: ConfigurationOptions) { } + + public resource(): void { } + + public client(name: keyof ClientMap, options?: ConfigurationOptions): Client { + const clients: { [K in keyof ClientMap]: ClientMap[K] } = Aws; + const service: Client = new clients[name]({ + ...this.options, + ...options, + }); + return service; + } + + public static getSession(credentials?: CredentialsOptions, region?: string): SessionProxy | null { + if (!credentials) { + return null; + } + return new SessionProxy({ + credentials, + region, + }); + } +} + +@allArgsConstructor +@builder +export class ProgressEvent> { + /** + * The status indicates whether the handler has reached a terminal state or is + * still computing and requires more time to complete + */ + private status: OperationStatus; + + /** + * If OperationStatus is FAILED or IN_PROGRESS, an error code should be provided + */ + private errorCode?: HandlerErrorCode; + + /** + * The handler can (and should) specify a contextual information message which + * can be shown to callers to indicate the nature of a progress transition or + * callback delay; for example a message indicating "propagating to edge" + */ + private message: string = ''; + + /** + * The callback context is an arbitrary datum which the handler can return in an + * IN_PROGRESS event to allow the passing through of additional state or + * metadata between subsequent retries; for example to pass through a Resource + * identifier which can be used to continue polling for stabilization + */ + private callbackContext?: T; + + /** + * A callback will be scheduled with an initial delay of no less than the number + * of seconds specified in the progress event. + */ + private callbackDelaySeconds: number; + + /** + * The output resource instance populated by a READ for synchronous results and + * by CREATE/UPDATE/DELETE for final response validation/confirmation + */ + private resourceModel?: R; + + /** + * The output resource instances populated by a LIST for synchronous results + */ + private resourceModels?: Array; + + /** + * The token used to request additional pages of resources for a LIST operation + */ + private nextToken?: NextToken; + + public serialize( + toTesponse: boolean = false, bearerToken?: string + ): Map { + // to match Java serialization, which drops `null` values, and the + // contract tests currently expect this also + let ser: Map = JSON.parse(JSON.stringify(this)); + + return ser; + } + + /** + * Convenience method for constructing a FAILED response + */ + public static failed(errorCode: HandlerErrorCode, message: string): ProgressEvent { + // @ts-ignore + const event = ProgressEvent.builder() + .status(OperationStatus.Failed) + .errorCode(errorCode) + .message(message) + .build(); + return event; + } + + /** + * Convenience method for constructing a IN_PROGRESS response + */ + public static progress(model: any, cxt: any): ProgressEvent { + // @ts-ignore + const event = ProgressEvent.builder() + .callbackContext(cxt) + .resourceModel(model) + .status(OperationStatus.InProgress) + .build(); + return event; + } +} + +/** + * This interface describes the request object for the provisioning request + * passed to the implementor. It is transformed from an instance of + * HandlerRequest by the LambdaWrapper to only items of concern + * + * @param Type of resource model being provisioned + */ +@allArgsConstructor +@builder +export class ResourceHandlerRequest { + private clientRequestToken: string; + private desiredResourceState: T; + private previousResourceState: T; + private desiredResourceTags: Map; + private systemTags: Map; + private awsAccountId: string; + private awsPartition: string; + private logicalResourceIdentifier: string; + private nextToken: string; + private region: string; +} diff --git a/tests/lib/callback.test.ts b/tests/lib/callback.test.ts new file mode 100644 index 0000000..7695523 --- /dev/null +++ b/tests/lib/callback.test.ts @@ -0,0 +1,93 @@ +import CloudFormation from 'aws-sdk/clients/cloudformation'; +import { reportProgress } from '../../src/callback'; +import { SessionProxy } from '../../src/proxy'; +import { + BaseResourceModel, + HandlerErrorCode, + OperationStatus, +} from '../../src/interface'; + + +const mockResult = (output: any): jest.Mock => { + return jest.fn().mockReturnValue({ + promise: jest.fn().mockResolvedValue(output) + }); +}; + +const IDENTIFIER: string = 'f3390613-b2b5-4c31-a4c6-66813dff96a6'; + +jest.mock('aws-sdk/clients/cloudformation'); +jest.mock('uuid', () => { + return { + v4: () => IDENTIFIER + }; +}); + +describe('when getting callback', () => { + + let session: SessionProxy; + let recordHandlerProgress: jest.Mock; + + beforeEach(() => { + recordHandlerProgress = mockResult({ + ResponseMetadata: {RequestId: 'mock_request'} + }); + const cfn = (CloudFormation as unknown) as jest.Mock; + cfn.mockImplementation(() => { + return { + makeRequest: (operation: string, params?: {[key: string]: any}) => { + const returnValue = { + recordHandlerProgress + }; + return returnValue[operation](params); + } + }; + }); + session = new SessionProxy({}); + + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + test('report progress minimal', () => { + reportProgress({ + session: session, + bearerToken: '123', + operationStatus: OperationStatus.InProgress, + currentOperationStatus: OperationStatus.InProgress, + message: '', + }); + expect(recordHandlerProgress).toHaveBeenCalledTimes(1); + expect(recordHandlerProgress).toHaveBeenCalledWith({ + BearerToken: '123', + OperationStatus: 'IN_PROGRESS', + CurrentOperationStatus: 'IN_PROGRESS', + StatusMessage: '', + ClientRequestToken: IDENTIFIER, + }); + }); + + test('report progress full', () => { + reportProgress({ + session: session, + bearerToken: '123', + errorCode: HandlerErrorCode.InternalFailure, + operationStatus: OperationStatus.Failed, + currentOperationStatus: OperationStatus.InProgress, + resourceModel: {} as BaseResourceModel, + message: 'test message', + }); + expect(recordHandlerProgress).toHaveBeenCalledTimes(1); + expect(recordHandlerProgress).toHaveBeenCalledWith({ + BearerToken: '123', + OperationStatus: 'FAILED', + CurrentOperationStatus: 'IN_PROGRESS', + StatusMessage: 'test message', + ResourceModel: '{}', + ErrorCode: 'InternalFailure', + ClientRequestToken: IDENTIFIER, + }); + }); +}); diff --git a/tests/lib/exceptions.test.ts b/tests/lib/exceptions.test.ts new file mode 100644 index 0000000..b7043a1 --- /dev/null +++ b/tests/lib/exceptions.test.ts @@ -0,0 +1,25 @@ +import * as exceptions from '../../src/exceptions'; +import { HandlerErrorCode, OperationStatus } from '../../src/interface'; + +describe('when getting exceptions', () => { + test('all error codes have exceptions', () => { + expect(exceptions.BaseHandlerException).toBeDefined(); + for (let errorCode in HandlerErrorCode) { + expect(exceptions[errorCode].prototype).toBeInstanceOf(exceptions.BaseHandlerException); + } + }); + + test('exception to progress event', () => { + for (let errorCode in HandlerErrorCode) { + let e: exceptions.BaseHandlerException; + try { + e = new exceptions[errorCode](); + } catch(err) { + e = new exceptions[errorCode]('Foo::Bar::Baz', 'ident'); + } + const progressEvent = e.toProgressEvent(); + expect(progressEvent.status).toBe(OperationStatus.Failed); + expect(progressEvent.errorCode).toBe(HandlerErrorCode[errorCode]); + } + }); +}); diff --git a/tests/lib/proxy.test.ts b/tests/lib/proxy.test.ts new file mode 100644 index 0000000..e069286 --- /dev/null +++ b/tests/lib/proxy.test.ts @@ -0,0 +1,30 @@ +import { ProgressEvent, SessionProxy } from '../../src/proxy'; +import { Credentials, HandlerErrorCode, OperationStatus } from '../../src/interface'; + +describe('when getting session proxy', () => { + test('get session returns proxy', () => { + const proxy = SessionProxy.getSession({ + accessKeyId: '', + secretAccessKey: '', + sessionToken: '', + } as Credentials); + expect(proxy).toBeInstanceOf(SessionProxy); + }); + + test('get session returns null', () => { + const proxy = SessionProxy.getSession(null); + expect(proxy).toBeNull(); + }); + + test('progress event failed is json serializable', () => { + const errorCode = HandlerErrorCode.AlreadyExists; + const message = 'message of failed event'; + const event = ProgressEvent.failed(errorCode, message); + expect(JSON.parse(JSON.stringify(event.serialize()))).toEqual({ + status: OperationStatus.Failed, + errorCode: errorCode, + message: message, + // callbackDelaySeconds: 0, + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ce0efcf --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES6", + "module": "commonjs", + "noImplicitAny": true, + "alwaysStrict": true, + "declaration": true, + "esModuleInterop": true, + "sourceMap": true, + "experimentalDecorators": true, + "outDir": "dist" + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules"] +} From f444170f8caca5fb407d8eb3309394ba798dab55 Mon Sep 17 00:00:00 2001 From: Eduardo Rodrigues Date: Tue, 31 Mar 2020 23:34:52 +0200 Subject: [PATCH 02/15] update callback file --- src/callback.ts | 17 ++++++++++------- src/index.ts | 9 +++++++++ tests/lib/callback.test.ts | 11 ++++++----- 3 files changed, 25 insertions(+), 12 deletions(-) create mode 100644 src/index.ts diff --git a/src/callback.ts b/src/callback.ts index b23f0d8..22d9dc8 100644 --- a/src/callback.ts +++ b/src/callback.ts @@ -1,10 +1,10 @@ import { v4 as uuidv4 } from 'uuid'; +import CloudFormation from 'aws-sdk/clients/cloudformation'; import { SessionProxy, } from './proxy'; import { BaseResourceModel, OperationStatus, Response } from './interface'; -import { KitchenSinkEncoder } from './utils'; const LOG = console; @@ -25,14 +25,14 @@ export async function reportProgress(options: ProgressOptions): Promise { resourceModel, message, } = options; - const client = session.client('CloudFormation'); + const client: CloudFormation = session.client('CloudFormation') as CloudFormation; - const request: { [key: string]: any; } = { + const request: CloudFormation.RecordHandlerProgressInput = { BearerToken: bearerToken, OperationStatus: operationStatus, StatusMessage: message, ClientRequestToken: uuidv4(), - }; + } as CloudFormation.RecordHandlerProgressInput; if (resourceModel) { request.ResourceModel = JSON.stringify(resourceModel); } @@ -40,9 +40,12 @@ export async function reportProgress(options: ProgressOptions): Promise { request.ErrorCode = errorCode; } if (currentOperationStatus) { - request['CurrentOperationStatus'] = currentOperationStatus; - const response: { [key: string]: any; } = await client.makeRequest('recordHandlerProgress', request).promise() - const requestId = response['ResponseMetadata']['RequestId']; + request.CurrentOperationStatus = currentOperationStatus; + const response: { [key: string]: any; } = await client.recordHandlerProgress(request).promise(); + let requestId: string = ''; + if (response['ResponseMetadata']) { + requestId = response.ResponseMetadata.RequestId; + } LOG.info(`Record Handler Progress with Request Id ${requestId} and Request: ${request}`); } } diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..584e1c9 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,9 @@ +export * from './callback'; +export * as exceptions from './exceptions'; +export * from './interface'; +export * from './log-delivery'; +export * from './metrics'; +export * from './proxy'; +export * from './resource'; +export * from './scheduler'; +export * from './utils'; diff --git a/tests/lib/callback.test.ts b/tests/lib/callback.test.ts index 7695523..0601355 100644 --- a/tests/lib/callback.test.ts +++ b/tests/lib/callback.test.ts @@ -30,24 +30,25 @@ describe('when getting callback', () => { beforeEach(() => { recordHandlerProgress = mockResult({ - ResponseMetadata: {RequestId: 'mock_request'} + ResponseMetadata: {RequestId: 'mock-request'} }); const cfn = (CloudFormation as unknown) as jest.Mock; cfn.mockImplementation(() => { + const returnValue = { + recordHandlerProgress + }; return { + ...returnValue, makeRequest: (operation: string, params?: {[key: string]: any}) => { - const returnValue = { - recordHandlerProgress - }; return returnValue[operation](params); } }; }); session = new SessionProxy({}); - }); afterEach(() => { + jest.clearAllMocks(); jest.restoreAllMocks(); }); From 823ec6e3f356f1ba871e0b13801b22d9df560993 Mon Sep 17 00:00:00 2001 From: Eduardo Rodrigues Date: Thu, 2 Apr 2020 23:35:55 +0200 Subject: [PATCH 03/15] add progress event and session proxy --- src/proxy.ts | 177 ++++++++++++++++++++++++++++++++-------- tests/lib/proxy.test.ts | 138 +++++++++++++++++++++++++++++-- 2 files changed, 274 insertions(+), 41 deletions(-) diff --git a/src/proxy.ts b/src/proxy.ts index 1b24786..8631175 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -1,18 +1,67 @@ +import { AWSError, CredentialProviderChain, Request, Service } from 'aws-sdk'; import { ConfigurationOptions } from 'aws-sdk/lib/config'; -import { CredentialsOptions } from 'aws-sdk/lib/credentials'; +import { Credentials, CredentialsOptions } from 'aws-sdk/lib/credentials'; +import { ServiceConfigurationOptions } from 'aws-sdk/lib/service'; import * as Aws from 'aws-sdk/clients/all'; import { NextToken } from 'aws-sdk/clients/cloudformation'; -import { allArgsConstructor, builder } from 'tombok'; +import { allArgsConstructor, builder, IBuilder} from 'tombok'; import { + BaseResourceHandlerRequest, BaseResourceModel, + Callable, HandlerErrorCode, + Newable, OperationStatus, } from './interface'; type ClientMap = typeof Aws; type Client = InstanceType; +type ClientType = T[keyof T] extends Service ? never : T[keyof T]; + +// type Async = T extends AsyncGenerator ? AsyncGenerator : T extends Generator ? AsyncGenerator : T extends Promise ? Promise : Promise; + +// type ProxyModule = { +// [K in keyof M]: M[K] extends (...args: infer A) => infer R ? (...args: A) => Async : never; +// }; + +// type Callback = (err: AWSError | undefined, data: D) => void; + +// interface AWSRequestMethod { +// (params: P, callback?: Callback): Request; +// (callback?: Callback): Request; +// } + +// export type CapturedAWSClient = { +// [K in keyof C]: C[K] extends AWSRequestMethod +// ? AWSRequestMethod +// : C[K]; +// }; + +// export type CapturedAWS = { +// [K in keyof T]: T[K] extends AWSClient ? CapturedAWSClient : T[K]; +// }; + +// export function captureAWSClient( +// client: C +// ): CapturedAWSClient; +// export function captureAWS(awssdk: ClientMap): CapturedAWS; + +// type Clients = { [K in keyof AwsClientMap]?: AwsClientMap[K] extends Service ? never : AwsClientMap[K] }; + +class SessionCredentialsProvider { + + private awsSessionCredentials: Credentials; + + public get(): Credentials { + return this.awsSessionCredentials; + } + + public setCredentials(credentials: CredentialsOptions): void { + this.awsSessionCredentials = new Credentials(credentials); + } +} export class SessionProxy { @@ -26,7 +75,37 @@ export class SessionProxy { ...this.options, ...options, }); - return service; + return service; //this.promisifyReturn(service); + } + + // private createService(client: Newable, options: ServiceConfigurationOptions): InstanceType { + // // const clients: { [K in keyof ClientMap]?: ClientMap[K] } = Aws; + // // const name: T; + + // return new client(); + // } + + // Wraps an Aws endpoint instance so that you don’t always have to chain `.promise()` onto every function + public promisifyReturn(obj: any): ProxyConstructor { + return new Proxy(obj, { + get(target, propertyKey) { + const property = target[propertyKey]; + + if (typeof property === "function") { + return function (...args: any[]) { + const result = property.apply(this, args); + + if (result instanceof Request) { + return result.promise(); + } else { + return result; + } + } + } else { + return property; + } + }, + }); } public static getSession(credentials?: CredentialsOptions, region?: string): SessionProxy | null { @@ -42,24 +121,24 @@ export class SessionProxy { @allArgsConstructor @builder -export class ProgressEvent> { +export class ProgressEvent> { /** * The status indicates whether the handler has reached a terminal state or is * still computing and requires more time to complete */ - private status: OperationStatus; + public status: OperationStatus; /** * If OperationStatus is FAILED or IN_PROGRESS, an error code should be provided */ - private errorCode?: HandlerErrorCode; + public errorCode?: HandlerErrorCode; /** * The handler can (and should) specify a contextual information message which * can be shown to callers to indicate the nature of a progress transition or * callback delay; for example a message indicating "propagating to edge" */ - private message: string = ''; + public message: string = ''; /** * The callback context is an arbitrary datum which the handler can return in an @@ -67,45 +146,75 @@ export class ProgressEvent> { * metadata between subsequent retries; for example to pass through a Resource * identifier which can be used to continue polling for stabilization */ - private callbackContext?: T; + public callbackContext?: T; /** * A callback will be scheduled with an initial delay of no less than the number * of seconds specified in the progress event. */ - private callbackDelaySeconds: number; + public callbackDelaySeconds: number; /** * The output resource instance populated by a READ for synchronous results and * by CREATE/UPDATE/DELETE for final response validation/confirmation */ - private resourceModel?: R; + public resourceModel?: R; /** * The output resource instances populated by a LIST for synchronous results */ - private resourceModels?: Array; + public resourceModels?: Array; /** * The token used to request additional pages of resources for a LIST operation */ - private nextToken?: NextToken; + public nextToken?: NextToken; + + // TODO: remove workaround when decorator mutation implemented: https://github.com/microsoft/TypeScript/issues/4881 + constructor(...args: any[]) {} + public static builder(template?: Partial): IBuilder {return null} public serialize( toTesponse: boolean = false, bearerToken?: string + // ): Record { ): Map { - // to match Java serialization, which drops `null` values, and the - // contract tests currently expect this also - let ser: Map = JSON.parse(JSON.stringify(this)); - - return ser; + // To match Java serialization, which drops `null` values, and the + // contract tests currently expect this also. + const json: Map = new Map(Object.entries(this));//JSON.parse(JSON.stringify(this))); + json.forEach((value: any, key: string) => { + if (value == null) { + json.delete(key); + } + }); + // Object.keys(json).forEach((key) => (json[key] == null) && delete json[key]); + // Mutate to what's expected in the response. + if (toTesponse) { + json.set('bearerToken', bearerToken); + json.set('operationStatus', json.get('status')); + json.delete('status'); + if (this.resourceModel) { + json.set('resourceModel', this.resourceModel.toObject()); + } + if (this.resourceModels) { + const models = this.resourceModels.map((resource: R) => resource.toObject()); + json.set('resourceModels', models); + } + json.delete('callbackDelaySeconds'); + if (json.has('callbackContext')) { + json.delete('callbackContext'); + } + if (this.errorCode) { + json.set('errorCode', this.errorCode); + } + } + return json; + // return new Map(Object.entries(jsonData)); } /** - * Convenience method for constructing a FAILED response + * Convenience method for constructing FAILED response */ public static failed(errorCode: HandlerErrorCode, message: string): ProgressEvent { - // @ts-ignore const event = ProgressEvent.builder() .status(OperationStatus.Failed) .errorCode(errorCode) @@ -115,10 +224,9 @@ export class ProgressEvent> { } /** - * Convenience method for constructing a IN_PROGRESS response + * Convenience method for constructing IN_PROGRESS response */ public static progress(model: any, cxt: any): ProgressEvent { - // @ts-ignore const event = ProgressEvent.builder() .callbackContext(cxt) .resourceModel(model) @@ -137,15 +245,20 @@ export class ProgressEvent> { */ @allArgsConstructor @builder -export class ResourceHandlerRequest { - private clientRequestToken: string; - private desiredResourceState: T; - private previousResourceState: T; - private desiredResourceTags: Map; - private systemTags: Map; - private awsAccountId: string; - private awsPartition: string; - private logicalResourceIdentifier: string; - private nextToken: string; - private region: string; +export class ResourceHandlerRequest extends BaseResourceHandlerRequest { + public clientRequestToken: string; + public desiredResourceState: T; + public previousResourceState: T; + public desiredResourceTags: Map; + public systemTags: Map; + public awsAccountId: string; + public awsPartition: string; + public logicalResourceIdentifier: string; + public nextToken: string; + public region: string; + + constructor(...args: any[]) {super()} + public static builder(template?: Partial>): IBuilder> { + return null + } } diff --git a/tests/lib/proxy.test.ts b/tests/lib/proxy.test.ts index e069286..6a7a47c 100644 --- a/tests/lib/proxy.test.ts +++ b/tests/lib/proxy.test.ts @@ -1,9 +1,50 @@ +import { allArgsConstructor, builder, IBuilder } from 'tombok'; + import { ProgressEvent, SessionProxy } from '../../src/proxy'; -import { Credentials, HandlerErrorCode, OperationStatus } from '../../src/interface'; +import { + BaseResourceModel, + Credentials, + HandlerErrorCode, + OperationStatus, + Optional, +} from '../../src/interface'; + describe('when getting session proxy', () => { + + const BEARER_TOKEN: string = 'f3390613-b2b5-4c31-a4c6-66813dff96a6'; + + @builder + @allArgsConstructor + class ResourceModel extends Map implements BaseResourceModel { + + public static typeName: string = 'Test::Resource::Model'; + + public somekey: Optional; + public someotherkey: Optional; + + constructor(...args: any[]) {super()} + public static builder(template?: Partial): IBuilder {return null} + + serialize(): Map { + const data: Map = new Map(Object.entries(JSON.parse(JSON.stringify(this)))); + data.forEach((value: any, key: string) => { + if (value == null) { + data.delete(key); + } + }); + return data; + } + deserialize(): ResourceModel {return undefined} + toObject(): Object { + // @ts-ignore + const obj = Object.fromEntries(this.serialize().entries()); + return obj; + } + } + test('get session returns proxy', () => { - const proxy = SessionProxy.getSession({ + const proxy: SessionProxy = SessionProxy.getSession({ accessKeyId: '', secretAccessKey: '', sessionToken: '', @@ -12,19 +53,98 @@ describe('when getting session proxy', () => { }); test('get session returns null', () => { - const proxy = SessionProxy.getSession(null); + const proxy: SessionProxy = SessionProxy.getSession(null); expect(proxy).toBeNull(); }); test('progress event failed is json serializable', () => { - const errorCode = HandlerErrorCode.AlreadyExists; - const message = 'message of failed event'; - const event = ProgressEvent.failed(errorCode, message); - expect(JSON.parse(JSON.stringify(event.serialize()))).toEqual({ + const errorCode: HandlerErrorCode = HandlerErrorCode.AlreadyExists; + const message: string = 'message of failed event'; + const event: ProgressEvent = ProgressEvent.failed(errorCode, message); + expect(event.status).toBe(OperationStatus.Failed); + expect(event.errorCode).toBe(errorCode); + expect(event.message).toBe(message); + const serialized = event.serialize(); + expect(serialized).toEqual(new Map(Object.entries({ status: OperationStatus.Failed, errorCode: errorCode, - message: message, + message, // callbackDelaySeconds: 0, - }); + }))); + }); + + test('progress event serialize to response with context', () => { + const message: string = 'message of event with context'; + const event = ProgressEvent.builder() + .callbackContext({ "a": "b" }) + .message(message) + .status(OperationStatus.Success) + .build(); + const serialized = event.serialize(true, BEARER_TOKEN); + expect(serialized).toEqual(new Map(Object.entries({ + operationStatus: OperationStatus.Success, + message, + bearerToken: BEARER_TOKEN, + }))); + }); + + test('progress event serialize to response with model', () => { + const message = 'message of event with model'; + const model = new ResourceModel(new Map(Object.entries({ + "somekey": "a", "someotherkey": "b" + }))); + const event = new ProgressEvent(new Map(Object.entries({ + status: OperationStatus.Success, + message, + resourceModel: model, + }))); + const serialized = event.serialize(true, BEARER_TOKEN); + expect(serialized).toEqual(new Map(Object.entries({ + operationStatus: OperationStatus.Success, + message, + bearerToken: BEARER_TOKEN, + resourceModel: {"somekey": "a", "someotherkey": "b"}, + }))); + }); + + test('progress event serialize to response with models', () => { + const message = 'message of event with models'; + const models = [new ResourceModel(new Map(Object.entries({ + "somekey": "a", "someotherkey": "b" + }))), + new ResourceModel(new Map(Object.entries({ + "somekey": "c", "someotherkey": "d" + })))]; + const event = new ProgressEvent(new Map(Object.entries({ + status: OperationStatus.Success, + message, + resourceModels: models, + }))); + const serialized = event.serialize(true, BEARER_TOKEN); + expect(serialized).toEqual(new Map(Object.entries({ + operationStatus: OperationStatus.Success, + message, + bearerToken: BEARER_TOKEN, + resourceModels: [ + {"somekey": "a", "someotherkey": "b"}, + {"somekey": "c", "someotherkey": "d"}, + ], + }))); + }); + + test('progress event serialize to response with error code', () => { + const message = 'message of event with error code'; + const event = new ProgressEvent(new Map(Object.entries({ + status: OperationStatus.Success, + message, + errorCode: HandlerErrorCode.InvalidRequest, + }))); + const serialized = event.serialize(true, BEARER_TOKEN); + expect(serialized).toEqual(new Map(Object.entries({ + operationStatus: OperationStatus.Success, + message, + bearerToken: BEARER_TOKEN, + errorCode: HandlerErrorCode.InvalidRequest, + }))); }); }); From 726cd87b3b16afd7d3751bf3fb53e7b4418aa9e2 Mon Sep 17 00:00:00 2001 From: Eduardo Rodrigues Date: Fri, 3 Apr 2020 21:56:36 +0200 Subject: [PATCH 04/15] add log delivery mechanism --- src/log-delivery.ts | 168 +++++++++++++++++++++ tests/lib/log-delivery.test.ts | 261 +++++++++++++++++++++++++++++++++ 2 files changed, 429 insertions(+) create mode 100644 src/log-delivery.ts create mode 100644 tests/lib/log-delivery.test.ts diff --git a/src/log-delivery.ts b/src/log-delivery.ts new file mode 100644 index 0000000..aa7a81f --- /dev/null +++ b/src/log-delivery.ts @@ -0,0 +1,168 @@ +import { boundMethod } from 'autobind-decorator' +import { EventEmitter } from 'events'; +import CloudWatchLogs, { + InputLogEvent, + PutLogEventsRequest, + PutLogEventsResponse, +} from 'aws-sdk/clients/cloudwatchlogs'; + +import { + SessionProxy, +} from './proxy'; +import { HandlerRequest } from './utils'; + + +type Console = globalThis.Console; + +interface ILogOptions { + groupName: string, + stream: string, + session: SessionProxy, + logger?: Console, +} + +class LogEmitter extends EventEmitter {} + +export class ProviderLogHandler { + private static instance: ProviderLogHandler; + private emitter: LogEmitter; + public client: CloudWatchLogs; + public sequenceToken: string; + public groupName: string; + public stream: string; + private logger: Console; + + /** + * The ProviderLogHandler's constructor should always be private to prevent direct + * construction calls with the `new` operator. + */ + private constructor(options: ILogOptions) { + this.stream = options.stream.replace(':', '__'); + this.client = options.session.client('CloudWatchLogs') as CloudWatchLogs; + this.sequenceToken = ''; + this.logger = options.logger || global.console; + // Attach the logger methods to localized event emitter. + const emitter = new LogEmitter(); + this.emitter = emitter; + emitter.on('log', this.logListener); + // Create maps of each logger Function and then alias that. + Object.entries(this.logger).forEach(([key, val]) => { + if (typeof val === 'function') { + if (['log', 'error', 'warn', 'info'].includes(key)) { + this.logger[key as 'log' | 'error' | 'warn' | 'info'] = function() { + // Calls the logger method. + val.apply(this, arguments); + // For adding other event watchers later. + emitter.emit('log', arguments); + }; + } + } + }); + } + + /** + * The static method that controls the access to the singleton instance. + * + * This implementation let you subclass the ProviderLogHandler class while keeping + * just one instance of each subclass around. + */ + public static getInstance(): ProviderLogHandler { + if (!ProviderLogHandler.instance) { + return null; + } + return ProviderLogHandler.instance; + } + + public static setup( + request: HandlerRequest, providerSession?: SessionProxy + ): void { + const logGroup: string = request.requestData?.providerLogGroupName; + let streamName: string = `${request.awsAccountId}-${request.region}`; + if (request.stackId && request.requestData?.logicalResourceId) { + streamName = `${request.stackId}/${request.requestData.logicalResourceId}`; + } + let logHandler = ProviderLogHandler.getInstance(); + if (providerSession && logGroup) { + if (logHandler) { + // This is a re-used lambda container, log handler is already setup, so + // we just refresh the client with new creds. + logHandler.client = providerSession.client('CloudWatchLogs') as CloudWatchLogs; + } else { + // Filter provider messages from platform. + const provider: string = request.resourceType.replace('::', '_').toLowerCase(); + logHandler = ProviderLogHandler.instance = new ProviderLogHandler({ + groupName: logGroup, + stream: streamName, + session: providerSession, + }); + } + } + } + + private async createLogGroup(): Promise { + try { + await this.client.createLogGroup({ + logGroupName: this.groupName, + }).promise(); + } catch(err) { + if (err.code !== 'ResourceAlreadyExistsException') { + throw err; + } + } + } + + private async createLogStream(): Promise { + try { + await this.client.createLogStream({ + logGroupName: this.groupName, + logStreamName: this.stream, + }).promise(); + } catch(err) { + if (err.code !== 'ResourceAlreadyExistsException') { + throw err; + } + } + } + + private async putLogEvent(record: InputLogEvent): Promise { + if (!record.timestamp) { + const currentTime = new Date(Date.now()); + record.timestamp = Math.round(currentTime.getTime()); + } + const logEventsParams: PutLogEventsRequest = { + logGroupName: this.groupName, + logStreamName: this.stream, + logEvents: [ record ], + }; + if (this.sequenceToken) { + logEventsParams.sequenceToken = this.sequenceToken; + } + try { + const response: PutLogEventsResponse = await this.client.putLogEvents(logEventsParams).promise(); + this.sequenceToken = response.nextSequenceToken; + } catch(err) { + if (err.code === 'DataAlreadyAcceptedException' || err.code === 'InvalidSequenceTokenException') { + this.sequenceToken = (err.message || '').split(' ')[0]; + this.putLogEvent(record); + } + } + } + + @boundMethod + logListener(...args: any[]): void { + const currentTime = new Date(Date.now()); + const record: InputLogEvent = { + message: JSON.stringify(args[0]), + timestamp: Math.round(currentTime.getTime()), + } + try { + this.putLogEvent(record); + } catch(err) { + if (err.message.includes('log group does not exist')) { + this.createLogGroup(); + } + this.createLogStream(); + this.putLogEvent(record); + } + } +} diff --git a/tests/lib/log-delivery.test.ts b/tests/lib/log-delivery.test.ts new file mode 100644 index 0000000..df27341 --- /dev/null +++ b/tests/lib/log-delivery.test.ts @@ -0,0 +1,261 @@ +import { v4 as uuidv4 } from 'uuid'; +import CloudWatchLogs from 'aws-sdk/clients/cloudwatchlogs'; +import awsUtil = require('aws-sdk/lib/util'); + +import { Action } from '../../src/interface'; +import { SessionProxy } from '../../src/proxy'; +import { ProviderLogHandler } from '../../src/log-delivery'; +import { HandlerRequest, RequestData } from '../../src/utils'; + + +const mockResult = (output: any): jest.Mock => { + return jest.fn().mockReturnValue({ + promise: jest.fn().mockResolvedValue(output) + }); +}; + +const IDENTIFIER: string = 'f3390613-b2b5-4c31-a4c6-66813dff96a6'; + +jest.mock('aws-sdk/clients/cloudwatchlogs'); +jest.mock('uuid', () => { + return { + v4: () => IDENTIFIER + }; +}); + +describe('when delivering log', () => { + + let payload: HandlerRequest; + let session: SessionProxy; + let providerLogHandler: ProviderLogHandler; + let cwLogs: jest.Mock; + let createLogGroup: jest.Mock; + let createLogStream: jest.Mock; + let putLogEvents: jest.Mock; + + beforeAll(() => { + session = new SessionProxy({}); + createLogGroup = mockResult({ ResponseMetadata: { RequestId: 'mock-request' }}); + createLogStream = mockResult({ ResponseMetadata: { RequestId: 'mock-request' }}); + putLogEvents = mockResult({ ResponseMetadata: { RequestId: 'mock-request' }}); + cwLogs = (CloudWatchLogs as unknown) as jest.Mock; + cwLogs.mockImplementation(() => { + const returnValue = { + createLogGroup, + createLogStream, + putLogEvents, + }; + return { + ...returnValue, + makeRequest: (operation: string, params?: {[key: string]: any}) => { + return returnValue[operation](params); + } + }; + }); + session['client'] = cwLogs; + const Mock = jest.fn(() => { + const request = new HandlerRequest(new Map(Object.entries({ + resourceType: 'Foo::Bar::Baz', + requestData: new RequestData(new Map(Object.entries({ + providerLogGroupName: 'test-group', + logicalResourceId: 'MyResourceId', + resourceProperties: {}, + systemTags: {}, + }))), + stackId: 'an-arn', + }))); + ProviderLogHandler.setup(request, session); + // Get a copy of the instance to avoid changing the singleton + const instance = ProviderLogHandler.getInstance(); + ProviderLogHandler['instance'] = null; + cwLogs.mockClear(); + return instance; + }); + providerLogHandler = new Mock(); + }); + + beforeEach(() => { + payload = new HandlerRequest(new Map(Object.entries({ + action: Action.Create, + awsAccountId: '123412341234', + bearerToken: uuidv4(), + region: 'us-east-1', + responseEndpoint: '', + resourceType: 'Foo::Bar::Baz', + resourceTypeVersion: '4', + requestData: new RequestData(new Map(Object.entries({ + providerLogGroupName: 'test_group', + logicalResourceId: 'MyResourceId', + resourceProperties: {}, + systemTags: {}, + }))), + stackId: 'an-arn', + }))); + }); + + afterEach(() => { + ProviderLogHandler['instance'] = null; + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + test('class singleton check instance is null', () => { + const instance = ProviderLogHandler.getInstance(); + expect(instance).toBeNull(); + }); + + test('setup with provider creds and stack id and logical resource id', () => { + ProviderLogHandler.setup(payload, session); + expect(cwLogs).toHaveBeenCalledTimes(1); + expect(cwLogs).toHaveBeenCalledWith('CloudWatchLogs'); + const logHandler = ProviderLogHandler.getInstance(); + expect(logHandler.stream).toContain(payload.stackId); + expect(logHandler.stream).toContain(payload.requestData.logicalResourceId); + }); + + test('setup with provider creds without stack id', () => { + payload.stackId = null; + ProviderLogHandler.setup(payload, session); + expect(cwLogs).toHaveBeenCalledTimes(1); + expect(cwLogs).toHaveBeenCalledWith('CloudWatchLogs'); + const logHandler = ProviderLogHandler.getInstance(); + expect(logHandler.stream).toContain(payload.awsAccountId); + expect(logHandler.stream).toContain(payload.region); + }); + + test('setup with provider creds without logical resource id', () => { + payload.requestData.logicalResourceId = null; + ProviderLogHandler.setup(payload, session); + expect(cwLogs).toHaveBeenCalledTimes(1); + expect(cwLogs).toHaveBeenCalledWith('CloudWatchLogs'); + const logHandler = ProviderLogHandler.getInstance(); + expect(logHandler.stream).toContain(payload.awsAccountId); + expect(logHandler.stream).toContain(payload.region); + }); + + test('setup existing logger', () => { + ProviderLogHandler.setup(payload, session); + const oldInstance = ProviderLogHandler.getInstance(); + expect(cwLogs).toHaveBeenCalledTimes(1); + expect(cwLogs).toHaveBeenCalledWith('CloudWatchLogs'); + ProviderLogHandler.setup(payload, session); + const newInstance = ProviderLogHandler.getInstance(); + expect(newInstance).toBe(oldInstance); + expect(newInstance.stream).toContain(payload.stackId); + expect(newInstance.stream).toContain(payload.requestData.logicalResourceId); + }); + + test('setup without log group should not set up', () => { + payload.requestData.providerLogGroupName = ''; + ProviderLogHandler.setup(payload, session); + const logHandler = ProviderLogHandler.getInstance(); + expect(logHandler).toBeNull(); + }); + + test('setup without session should not set up', () => { + ProviderLogHandler.setup(payload, null); + const logHandler = ProviderLogHandler.getInstance(); + expect(logHandler).toBeNull(); + }); + + test('log group create success', () => { + providerLogHandler.client.createLogGroup(); + expect(createLogGroup).toHaveBeenCalledTimes(1); + }); + + test('log stream create success', () => { + providerLogHandler.client.createLogStream(); + expect(createLogStream).toHaveBeenCalledTimes(1); + }); + + test('create already exists', () => { + ['createLogGroup', 'createLogStream'].forEach((methodName: string) => { + const mockLogsMethod: jest.Mock = jest.fn().mockImplementationOnce(() => { + throw awsUtil.error(new Error(), { code: 'ResourceAlreadyExistsException' }); + }); + providerLogHandler.client[methodName] = mockLogsMethod; + // Should not raise an exception if the log group already exists. + providerLogHandler[methodName](); + expect(mockLogsMethod).toHaveBeenCalledTimes(1); + }); + }); + + test('put log event success', () => { + [null, 'some-seq'].forEach((sequenceToken: string) => { + providerLogHandler.sequenceToken = sequenceToken; + const mockPut: jest.Mock = jest.fn().mockImplementationOnce(() => { + return { nextSequenceToken: 'some-other-seq' }; + }); + providerLogHandler.client.putLogEvents = mockPut; + providerLogHandler['putLogEvent']({ + message: 'log-msg', + timestamp: 123, + }); + expect(mockPut).toHaveBeenCalledTimes(1); + }); + }); + + test('put log event invalid token', () => { + const mockPut: jest.Mock = jest.fn().mockImplementationOnce(() => { + throw awsUtil.error(new Error(), { code: 'InvalidSequenceTokenException' }); + }) + .mockImplementationOnce(() => { + throw awsUtil.error(new Error(), { code: 'DataAlreadyAcceptedException' }); + }) + .mockImplementation(() => { + return { nextSequenceToken: 'some-other-seq' }; + }); + providerLogHandler.client.putLogEvents = mockPut; + for(let i = 1; i < 4; i++) { + providerLogHandler['putLogEvent']({ + message: 'log-msg', + timestamp: i, + }); + } + expect(mockPut).toHaveBeenCalledTimes(5); + }); + + test('emit existing cwl group stream', () => { + const mock: jest.Mock = jest.fn(); + providerLogHandler['putLogEvent'] = mock; + providerLogHandler['emitter'].emit('log', 'log-msg'); + expect(mock).toHaveBeenCalledTimes(1); + }); + + test('emit no group stream', () => { + const putLogEvent: jest.Mock = jest.fn().mockImplementationOnce(() => { + throw awsUtil.error(new Error(), { message: 'log group does not exist' }); + }); + const createLogGroup: jest.Mock = jest.fn(); + const createLogStream: jest.Mock = jest.fn(); + providerLogHandler['putLogEvent'] = putLogEvent; + providerLogHandler['createLogGroup'] = createLogGroup; + providerLogHandler['createLogStream'] = createLogStream; + providerLogHandler['emitter'].emit('log', 'log-msg'); + expect(putLogEvent).toHaveBeenCalledTimes(2); + expect(createLogGroup).toHaveBeenCalledTimes(1); + expect(createLogStream).toHaveBeenCalledTimes(1); + + // Function createGroup should not be called again if the group already exists. + putLogEvent.mockImplementationOnce(() => { + throw awsUtil.error(new Error(), { message: 'log stream does not exist' }); + }); + providerLogHandler['emitter'].emit('log', 'log-msg'); + expect(putLogEvent).toHaveBeenCalledTimes(4); + expect(createLogGroup).toHaveBeenCalledTimes(1); + expect(createLogStream).toHaveBeenCalledTimes(2); + }); + + test('get instance no logger present', () => { + ProviderLogHandler['instance'] = undefined; + const actual = ProviderLogHandler.getInstance(); + expect(actual).toBeNull(); + }); + + test('get instance logger present', () => { + const expected = providerLogHandler; + ProviderLogHandler['instance'] = providerLogHandler; + const actual = ProviderLogHandler.getInstance(); + expect(actual).toBe(expected); + }); +}); From d8516efacb7602ebc250ee13463e79beaae235a6 Mon Sep 17 00:00:00 2001 From: Eduardo Rodrigues Date: Sun, 5 Apr 2020 22:31:53 +0200 Subject: [PATCH 05/15] add cloudwatch events scheduler --- src/scheduler.ts | 88 ++++++++++++++++++++ tests/lib/scheduler.test.ts | 158 ++++++++++++++++++++++++++++++++++++ 2 files changed, 246 insertions(+) create mode 100644 src/scheduler.ts create mode 100644 tests/lib/scheduler.test.ts diff --git a/src/scheduler.ts b/src/scheduler.ts new file mode 100644 index 0000000..fd9289a --- /dev/null +++ b/src/scheduler.ts @@ -0,0 +1,88 @@ +import { v4 as uuidv4 } from 'uuid'; +import CloudWatchEvents from 'aws-sdk/clients/cloudwatchevents'; + +import { SessionProxy } from './proxy'; +import { HandlerRequest, minToCron } from './utils'; + + +const LOGGER = console; + +/** + * Schedule a re-invocation of the executing handler no less than 1 minute from + * now + * + * @param session AWS session where to retrieve CloudWatchEvents client + * @param functionArn the ARN of the Lambda function to be invoked + * @param minutesFromNow the minimum minutes from now that the re-invocation + * will occur. CWE provides only minute-granularity + * @param handlerRequest additional context which the handler can provide itself + * for re-invocation + */ +export function rescheduleAfterMinutes( + session: SessionProxy, + functionArn: string, + minutesFromNow: number, + handlerRequest: HandlerRequest, +): void { + const client: CloudWatchEvents = session.client('CloudWatchEvents') as CloudWatchEvents; + const cron = minToCron(Math.max(minutesFromNow, 1)); + const identifier = uuidv4(); + const ruleName = `reinvoke-handler-${identifier}`; + const targetId = `reinvoke-target-${identifier}`; + handlerRequest.requestContext.cloudWatchEventsRuleName = ruleName; + handlerRequest.requestContext.cloudWatchEventsTargetId = targetId; + const jsonRequest = JSON.stringify(handlerRequest); + LOGGER.debug(`Scheduling re-invoke at ${cron} (${identifier})`); + client.putRule({ + Name: ruleName, + ScheduleExpression: cron, + State: 'ENABLED', + }); + client.putTargets({ + Rule: ruleName, + Targets: [{ + Id: targetId, + Arn: functionArn, + Input: jsonRequest, + }], + }); +} + +/** + * After a re-invocation, the CWE rule which generated the reinvocation should + * be scrubbed + * + * @param session AWS session where to retrieve CloudWatchEvents client + * @param ruleName the name of the CWE rule which triggered a re-invocation + * @param targetId the target of the CWE rule which triggered a re-invocation + */ +export function cleanupCloudwatchEvents( + session: SessionProxy, ruleName: string, targetId: string +): void { + const client: CloudWatchEvents = session.client('CloudWatchEvents') as CloudWatchEvents; + try { + if (targetId && ruleName) { + client.removeTargets({ + Rule: ruleName, + Ids: [targetId], + }); + } + } catch(err) { + LOGGER.error( + `Error cleaning CloudWatchEvents Target (targetId=${targetId}): ${err.message}` + ); + } + + try { + if (ruleName) { + client.deleteRule({ + Name: ruleName, + Force: true, + }); + } + } catch(err) { + LOGGER.error( + `Error cleaning CloudWatchEvents Rule (ruleName=${ruleName}): ${err.message}` + ); + } +} diff --git a/tests/lib/scheduler.test.ts b/tests/lib/scheduler.test.ts new file mode 100644 index 0000000..11260b9 --- /dev/null +++ b/tests/lib/scheduler.test.ts @@ -0,0 +1,158 @@ +import CloudWatchEvents from 'aws-sdk/clients/cloudwatchevents'; +import awsUtil = require('aws-sdk/lib/util'); + +import { cleanupCloudwatchEvents, rescheduleAfterMinutes } from '../../src/scheduler'; +import { SessionProxy } from '../../src/proxy'; +import { RequestContext } from '../../src/interface'; +import * as utils from '../../src/utils'; + + +const mockResult = (output: any): jest.Mock => { + return jest.fn().mockReturnValue({ + promise: jest.fn().mockResolvedValue(output) + }); +}; + +const IDENTIFIER: string = 'f3390613-b2b5-4c31-a4c6-66813dff96a6'; + +jest.mock('aws-sdk/clients/cloudwatchevents'); +jest.mock('uuid', () => { + return { + v4: () => IDENTIFIER + }; +}); + +describe('when getting scheduler', () => { + + let session: SessionProxy; + let handlerRequest: utils.HandlerRequest; + let cwEvents: jest.Mock; + let spyConsoleError: jest.SpyInstance; + let spyMinToCron: jest.SpyInstance; + let mockPutRule: jest.Mock; + let mockPutTargets: jest.Mock; + let mockRemoveTargets: jest.Mock; + let mockDeleteRule: jest.Mock; + + beforeEach(() => { + spyConsoleError = jest.spyOn(global.console, 'error').mockImplementation(() => {}); + spyMinToCron = jest.spyOn(utils, 'minToCron') + .mockReturnValue('cron(30 16 21 11 ? 2019)'); + mockPutRule = mockResult({ ResponseMetadata: { RequestId: 'mock-request' }}); + mockPutTargets = mockResult({ ResponseMetadata: { RequestId: 'mock-request' }}); + mockRemoveTargets = mockResult({ ResponseMetadata: { RequestId: 'mock-request' }}); + mockDeleteRule = mockResult({ ResponseMetadata: { RequestId: 'mock-request' }}); + + cwEvents = (CloudWatchEvents as unknown) as jest.Mock; + cwEvents.mockImplementation(() => { + const returnValue = { + deleteRule: mockDeleteRule, + putRule: mockPutRule, + putTargets: mockPutTargets, + removeTargets: mockRemoveTargets, + }; + return { + ...returnValue, + makeRequest: (operation: string, params?: {[key: string]: any}) => { + return returnValue[operation](params); + } + }; + }); + session = new SessionProxy({}); + session['client'] = cwEvents; + + handlerRequest = new utils.HandlerRequest() + handlerRequest.requestContext = {} as RequestContext>; + handlerRequest.toJSON = jest.fn(() => new Object()); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + test('reschedule after minutes zero', () => { + // if called with zero, should call cron with a 1 + rescheduleAfterMinutes(session, 'arn:goes:here', 0, handlerRequest); + + expect(cwEvents).toHaveBeenCalledTimes(1); + expect(cwEvents).toHaveBeenCalledWith('CloudWatchEvents'); + expect(spyMinToCron).toHaveBeenCalledTimes(1); + expect(spyMinToCron).toHaveBeenCalledWith(1); + }); + + test('reschedule after minutes not zero', () => { + // if called with another number, should use that + rescheduleAfterMinutes(session, 'arn:goes:here', 2, handlerRequest); + + expect(cwEvents).toHaveBeenCalledTimes(1); + expect(cwEvents).toHaveBeenCalledWith('CloudWatchEvents'); + expect(spyMinToCron).toHaveBeenCalledTimes(1); + expect(spyMinToCron).toHaveBeenCalledWith(2); + }); + + test('reschedule after minutes success', () => { + rescheduleAfterMinutes(session, 'arn:goes:here', 2, handlerRequest); + + expect(cwEvents).toHaveBeenCalledTimes(1); + expect(cwEvents).toHaveBeenCalledWith('CloudWatchEvents'); + expect(mockPutRule).toHaveBeenCalledTimes(1); + expect(mockPutRule).toHaveBeenCalledWith({ + Name: `reinvoke-handler-${IDENTIFIER}`, + ScheduleExpression: 'cron(30 16 21 11 ? 2019)', + State: 'ENABLED', + }); + expect(mockPutTargets).toHaveBeenCalledTimes(1); + expect(mockPutTargets).toHaveBeenCalledWith({ + Rule: `reinvoke-handler-${IDENTIFIER}`, + Targets: [ + { + Id: `reinvoke-target-${IDENTIFIER}`, + Arn: 'arn:goes:here', + Input: '{}', + } + ], + }); + }); + + test('cleanup cloudwatch events empty', () => { + // cleanup should silently pass if rule/target are empty + cleanupCloudwatchEvents(session, '', ''); + + expect(cwEvents).toHaveBeenCalledTimes(1); + expect(cwEvents).toHaveBeenCalledWith('CloudWatchEvents'); + expect(mockRemoveTargets).toHaveBeenCalledTimes(0); + expect(mockDeleteRule).toHaveBeenCalledTimes(0); + expect(spyConsoleError).toHaveBeenCalledTimes(0); + }); + + test('cleanup cloudwatch events success', () => { + // when rule_name and target_id are provided we should call events client and not + // log errors if the deletion succeeds + cleanupCloudwatchEvents(session, 'rulename', 'targetid'); + + expect(spyConsoleError).toHaveBeenCalledTimes(0); + expect(cwEvents).toHaveBeenCalledTimes(1); + expect(cwEvents).toHaveBeenCalledWith('CloudWatchEvents'); + expect(mockRemoveTargets).toHaveBeenCalledTimes(1); + expect(mockDeleteRule).toHaveBeenCalledTimes(1); + expect(mockPutRule).toHaveBeenCalledTimes(0); + expect(spyConsoleError).toHaveBeenCalledTimes(0); + }); + + test('cleanup cloudwatch events client error', () => { + // cleanup should catch and log client failures + const error = awsUtil.error(new Error(), { code: '1' }); + mockRemoveTargets.mockImplementation(() => {throw error}); + mockDeleteRule.mockImplementation(() => {throw error}); + + cleanupCloudwatchEvents(session, 'rulename', 'targetid'); + + expect(cwEvents).toHaveBeenCalledTimes(1); + expect(cwEvents).toHaveBeenCalledWith('CloudWatchEvents'); + expect(spyConsoleError).toHaveBeenCalledTimes(2); + expect(mockRemoveTargets).toHaveBeenCalledTimes(1); + expect(mockDeleteRule).toHaveBeenCalledTimes(1); + expect(mockPutTargets).toHaveBeenCalledTimes(0); + }); +}); From 3f85ca0a6e0bfaefc4493a1230b93eb3d3d4a5aa Mon Sep 17 00:00:00 2001 From: Eduardo Rodrigues Date: Mon, 6 Apr 2020 20:49:25 +0200 Subject: [PATCH 06/15] add cloudwatch metrics --- src/metrics.ts | 134 ++++++++++++++++++++++ tests/lib/metrics.test.ts | 233 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 367 insertions(+) create mode 100644 src/metrics.ts create mode 100644 tests/lib/metrics.test.ts diff --git a/src/metrics.ts b/src/metrics.ts new file mode 100644 index 0000000..4dc851f --- /dev/null +++ b/src/metrics.ts @@ -0,0 +1,134 @@ +import CloudWatch, { Dimension } from 'aws-sdk/clients/cloudwatch'; + +import { SessionProxy } from './proxy'; +import { Action, MetricTypes, StandardUnit } from './interface'; + + +const LOGGER = console; +const METRIC_NAMESPACE_ROOT = 'AWS/CloudFormation'; + +export function formatDimensions(dimensions: Map): Array { + const formatted: Array = []; + dimensions.forEach((value: string, key: string) => { + formatted.push({ + Name: key, + Value: value, + }) + }); + return formatted; +} + +export class MetricPublisher { + public client: CloudWatch; + + constructor (session: SessionProxy, public namespace: string) { + this.client = session.client('CloudWatch') as CloudWatch; + } + + publishMetric( + metricName: MetricTypes, + dimensions: Map, + unit: StandardUnit, + value: number, + timestamp: Date, + ): void { + try { + this.client.putMetricData({ + Namespace: this.namespace, + MetricData: [{ + MetricName: metricName, + Dimensions: formatDimensions(dimensions), + Unit: unit, + Timestamp: timestamp, + Value: value, + }], + }); + } catch(err) { + LOGGER.error(`An error occurred while publishing metrics: ${err.message}`); + } + } +} + +export class MetricsPublisherProxy { + public namespace: string; + private publishers: Array; + + constructor(public accountId: string, public resourceType: string) { + this.namespace = MetricsPublisherProxy.makeNamespace(accountId, resourceType); + this.resourceType = resourceType; + this.publishers = []; + } + + static makeNamespace(accountId: string, resourceType: string): string { + const suffix = resourceType.replace(/::/g, '/'); + return `${METRIC_NAMESPACE_ROOT}/${accountId}/${suffix}`; + } + + addMetricsPublisher(session?: SessionProxy): void { + if (session) { + this.publishers.push(new MetricPublisher(session, this.namespace)); + } + } + + publishExceptionMetric(timestamp: Date, action: Action, error: Error): void { + const dimensions = new Map(); + dimensions.set('DimensionKeyActionType', action); + dimensions.set('DimensionKeyExceptionType', error.constructor.name); + dimensions.set('DimensionKeyResourceType', this.resourceType); + this.publishers.forEach((publisher: MetricPublisher) => { + publisher.publishMetric( + MetricTypes.HandlerException, + dimensions, + StandardUnit.Count, + 1.0, + timestamp, + ); + }); + } + + publishInvocationMetric(timestamp: Date, action: Action): void { + const dimensions = new Map(); + dimensions.set('DimensionKeyActionType', action); + dimensions.set('DimensionKeyResourceType', this.resourceType); + this.publishers.forEach((publisher: MetricPublisher) => { + publisher.publishMetric( + MetricTypes.HandlerInvocationCount, + dimensions, + StandardUnit.Count, + 1.0, + timestamp, + ); + }); + } + + publishDurationMetric(timestamp: Date, action: Action, milliseconds: number): void { + const dimensions = new Map(); + dimensions.set('DimensionKeyActionType', action); + dimensions.set('DimensionKeyResourceType', this.resourceType); + this.publishers.forEach((publisher: MetricPublisher) => { + publisher.publishMetric( + MetricTypes.HandlerInvocationDuration, + dimensions, + StandardUnit.Milliseconds, + milliseconds, + timestamp, + ); + }); + } + + publishLogDeliveryExceptionMetric(timestamp: Date, error: any): void { + const dimensions = new Map(); + dimensions.set('DimensionKeyActionType', 'ProviderLogDelivery'); + dimensions.set('DimensionKeyExceptionType', error.constructor.name); + dimensions.set('DimensionKeyResourceType', this.resourceType); + this.publishers.forEach((publisher: MetricPublisher) => { + publisher.publishMetric( + MetricTypes.HandlerException, + dimensions, + StandardUnit.Count, + 1.0, + timestamp, + ); + }); + } +} diff --git a/tests/lib/metrics.test.ts b/tests/lib/metrics.test.ts new file mode 100644 index 0000000..946689c --- /dev/null +++ b/tests/lib/metrics.test.ts @@ -0,0 +1,233 @@ +import CloudWatch from 'aws-sdk/clients/cloudwatch'; +import awsUtil = require('aws-sdk/lib/util'); + +import { Action, MetricTypes, StandardUnit } from '../../src/interface'; +import { SessionProxy } from '../../src/proxy'; +import { + MetricPublisher, + MetricsPublisherProxy, + formatDimensions, +} from '../../src/metrics'; + +const mockResult = (output: any): jest.Mock => { + return jest.fn().mockReturnValue({ + promise: jest.fn().mockResolvedValue(output) + }); +}; + +const MOCK_DATE = new Date('2020-01-01T23:05:38.964Z'); +const ACCOUNT_ID = '123412341234'; +const RESOURCE_TYPE = 'Aa::Bb::Cc'; +const NAMESPACE = MetricsPublisherProxy.makeNamespace( + ACCOUNT_ID, RESOURCE_TYPE +); + +jest.mock('aws-sdk/clients/cloudwatch'); + +describe('when getting metrics', () => { + + let session: SessionProxy; + let cloudwatch: jest.Mock; + let putMetricData: jest.Mock; + + beforeAll(() => { + session = new SessionProxy({}); + putMetricData = mockResult({ ResponseMetadata: { RequestId: 'mock-request' }}); + cloudwatch = (CloudWatch as unknown) as jest.Mock; + cloudwatch.mockImplementation(() => { + const returnValue = { + putMetricData, + }; + return { + ...returnValue, + makeRequest: (operation: string, params?: {[key: string]: any}) => { + return returnValue[operation](params); + } + }; + }); + session['client'] = cloudwatch; + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + test('format dimensions', () => { + const dimensions = new Map(); + dimensions.set('MyDimensionKeyOne', 'valOne'); + dimensions.set('MyDimensionKeyTwo', 'valTwo'); + const result = formatDimensions(dimensions); + expect(result).toMatchObject([ + {Name: 'MyDimensionKeyOne', Value: 'valOne'}, + {Name: 'MyDimensionKeyTwo', Value: 'valTwo'}, + ]); + }); + + test('put metric catches error', () => { + const spyConsoleError: jest.SpyInstance = jest + .spyOn(global.console, 'error').mockImplementation(() => {}); + putMetricData.mockImplementationOnce(() => { + throw awsUtil.error(new Error(), { + code: 'InternalServiceError', + message: 'An error occurred (InternalServiceError) when ' + + 'calling the PutMetricData operation: ', + }); + }); + const publisher = new MetricPublisher(session, NAMESPACE); + const dimensions = new Map(); + dimensions.set('DimensionKeyActionType', Action.Create); + dimensions.set('DimensionKeyResourceType', RESOURCE_TYPE); + publisher.publishMetric( + MetricTypes.HandlerInvocationCount, + dimensions, + StandardUnit.Count, + 1.0, + MOCK_DATE, + ); + expect(putMetricData).toHaveBeenCalledTimes(1); + expect(putMetricData).toHaveBeenCalledWith({ + MetricData: [{ + Dimensions: [ + { + Name: 'DimensionKeyActionType', + Value: 'CREATE', + }, + { + Name: 'DimensionKeyResourceType', + Value: 'Aa::Bb::Cc', + }, + ], + MetricName: MetricTypes.HandlerInvocationCount, + Timestamp: MOCK_DATE, + Unit: StandardUnit.Count, + Value: 1.0, + }], + Namespace: 'AWS/CloudFormation/123412341234/Aa/Bb/Cc', + }); + expect(spyConsoleError).toHaveBeenCalledTimes(1); + expect(spyConsoleError).toHaveBeenCalledWith('An error occurred while ' + + 'publishing metrics: An error occurred (InternalServiceError) ' + + 'when calling the PutMetricData operation: ' + ); + }); + + test('publish exception metric', () => { + const proxy = new MetricsPublisherProxy(ACCOUNT_ID, RESOURCE_TYPE); + proxy.addMetricsPublisher(session); + proxy.publishExceptionMetric( MOCK_DATE, Action.Create, new Error('fake-err')); + expect(putMetricData).toHaveBeenCalledTimes(1); + expect(putMetricData).toHaveBeenCalledWith({ + MetricData: [{ + Dimensions: [ + { + Name: 'DimensionKeyActionType', + Value: 'CREATE', + }, + { + Name: 'DimensionKeyExceptionType', + Value: 'Error', + }, + { + Name: 'DimensionKeyResourceType', + Value: 'Aa::Bb::Cc', + }, + ], + MetricName: MetricTypes.HandlerException, + Timestamp: MOCK_DATE, + Unit: StandardUnit.Count, + Value: 1.0, + }], + Namespace: 'AWS/CloudFormation/123412341234/Aa/Bb/Cc', + }); + }); + + test('publish invocation metric', () => { + const proxy = new MetricsPublisherProxy(ACCOUNT_ID, RESOURCE_TYPE); + proxy.addMetricsPublisher(session); + proxy.publishInvocationMetric( MOCK_DATE, Action.Create); + expect(putMetricData).toHaveBeenCalledTimes(1); + expect(putMetricData).toHaveBeenCalledWith({ + MetricData: [{ + Dimensions: [ + { + Name: 'DimensionKeyActionType', + Value: 'CREATE', + }, + { + Name: 'DimensionKeyResourceType', + Value: 'Aa::Bb::Cc', + }, + ], + MetricName: MetricTypes.HandlerInvocationCount, + Timestamp: MOCK_DATE, + Unit: StandardUnit.Count, + Value: 1.0, + }], + Namespace: 'AWS/CloudFormation/123412341234/Aa/Bb/Cc', + }); + }); + + test('publish duration metric', () => { + const proxy = new MetricsPublisherProxy(ACCOUNT_ID, RESOURCE_TYPE); + proxy.addMetricsPublisher(session); + proxy.publishDurationMetric( MOCK_DATE, Action.Create, 100); + expect(putMetricData).toHaveBeenCalledTimes(1); + expect(putMetricData).toHaveBeenCalledWith({ + MetricData: [{ + Dimensions: [ + { + Name: 'DimensionKeyActionType', + Value: 'CREATE', + }, + { + Name: 'DimensionKeyResourceType', + Value: 'Aa::Bb::Cc', + }, + ], + MetricName: MetricTypes.HandlerInvocationDuration, + Timestamp: MOCK_DATE, + Unit: StandardUnit.Milliseconds, + Value: 100, + }], + Namespace: 'AWS/CloudFormation/123412341234/Aa/Bb/Cc', + }); + }); + + + test('publish log delivery exception metric', () => { + const proxy = new MetricsPublisherProxy(ACCOUNT_ID, RESOURCE_TYPE); + proxy.addMetricsPublisher(session); + proxy.publishLogDeliveryExceptionMetric( MOCK_DATE, new TypeError('test')); + expect(putMetricData).toHaveBeenCalledTimes(1); + expect(putMetricData).toHaveBeenCalledWith({ + MetricData: [{ + Dimensions: [ + { + Name: 'DimensionKeyActionType', + Value: 'ProviderLogDelivery', + }, + { + Name: 'DimensionKeyExceptionType', + Value: 'TypeError', + }, + { + Name: 'DimensionKeyResourceType', + Value: 'Aa::Bb::Cc', + }, + ], + MetricName: MetricTypes.HandlerException, + Timestamp: MOCK_DATE, + Unit: StandardUnit.Count, + Value: 1.0, + }], + Namespace: 'AWS/CloudFormation/123412341234/Aa/Bb/Cc', + }); + }); + + test('metrics publisher proxy add metrics publisher null safe', () => { + const proxy = new MetricsPublisherProxy(ACCOUNT_ID, RESOURCE_TYPE); + proxy.addMetricsPublisher(null); + expect(proxy['publishers']).toMatchObject([]); + }); +}); From 5ec6fdc4c7cd73f5efc28532d8585fa8f18948d1 Mon Sep 17 00:00:00 2001 From: Eduardo Rodrigues Date: Thu, 9 Apr 2020 22:07:39 +0200 Subject: [PATCH 07/15] add utils to typescript rpdk --- src/utils.ts | 139 ++++++++++++++++++++++++++++++++++++++++ tests/lib/utils.test.ts | 16 +++++ 2 files changed, 155 insertions(+) create mode 100644 src/utils.ts create mode 100644 tests/lib/utils.test.ts diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..6a4bcbc --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,139 @@ +import { + LogGroupName, + LogicalResourceId, + NextToken, +} from 'aws-sdk/clients/cloudformation'; +import { allArgsConstructor } from 'tombok'; +import { + Action, + BaseResourceHandlerRequest, + BaseResourceModel, + Credentials, + RequestContext, +} from './interface'; + + +export type Constructor = new (...args: any[]) => T; + +/** + * Convert minutes to a valid scheduling expression to be used in the AWS Events + * + * @param {number} minutes Minutes to be converted + */ +export function minToCron(minutes: number): string { + const date = new Date(Date.now()); + // add another minute, as per java implementation + date.setMinutes(date.getMinutes() + minutes + 1); + return `cron(${date.getMinutes()} ${date.getHours()} ${date.getDate()} ${date.getMonth()} ? ${date.getFullYear()})`; +} + +@allArgsConstructor +export class TestEvent { + credentials: Credentials; + action: Action; + request: Map; + callbackContext: Map; + region?: string; + + constructor(...args: any[]) {} +} + +@allArgsConstructor +export class RequestData> { + callerCredentials?: Credentials; + platformCredentials?: Credentials; + providerCredentials?: Credentials; + providerLogGroupName: LogGroupName; + logicalResourceId: LogicalResourceId; + resourceProperties: T; + previousResourceProperties?: T; + systemTags: { [index: string]: string }; + stackTags?: { [index: string]: string }; + previousStackTags?: { [index: string]: string }; + + constructor(...args: any[]) {} + + public static deserialize(jsonData: Map): RequestData { + if (!jsonData) { + jsonData = new Map(); + } + const reqData: RequestData = new RequestData(jsonData); + jsonData.forEach((value: any, key: string) => { + if (key.endsWith('Credentials')) { + type credentialsType = 'callerCredentials' | 'platformCredentials' | 'providerCredentials'; + const prop: credentialsType = key as credentialsType; + const creds = value; + if (creds) { + reqData[prop] = creds as Credentials; + } + } + }); + return reqData; + } + + serialize(): Map { + return null; + } +} + +@allArgsConstructor +export class HandlerRequest, CallbackT = Map> { + action: Action; + awsAccountId: string; + bearerToken: string; + nextToken?: NextToken; + region: string; + responseEndpoint: string; + resourceType: string; + resourceTypeVersion: string; + requestData: RequestData; + stackId: string; + requestContext: RequestContext; + + constructor(...args: any[]) {} + + public static deserialize(jsonData: Map): HandlerRequest { + if (!jsonData) { + jsonData = new Map(); + } + const event: HandlerRequest = new HandlerRequest(jsonData); + const requestData = new Map(Object.entries(jsonData.get('requestData') || {})); + event.requestData = RequestData.deserialize(requestData); + return event; + }; + + public fromJSON(jsonData: Map): HandlerRequest { + return null; + }; + + public toJSON(): any { + return null; + }; +} + +@allArgsConstructor +export class UnmodeledRequest extends BaseResourceHandlerRequest { + + constructor(...args: any[]) {super()} + + public static fromUnmodeled(obj: Object): UnmodeledRequest { + const mapped = new Map(Object.entries(obj)); + const request: UnmodeledRequest = new UnmodeledRequest(mapped); + return request; + } + + public toModeled(modelCls: Constructor & { deserialize?: Function }): BaseResourceHandlerRequest { + return new BaseResourceHandlerRequest(new Map(Object.entries({ + clientRequestToken: this.clientRequestToken, + desiredResourceState: modelCls.deserialize(this.desiredResourceState || {}), + previousResourceState: modelCls.deserialize(this.previousResourceState || {}), + logicalResourceIdentifier: this.logicalResourceIdentifier, + nextToken: this.nextToken, + }))); + } +} + +export interface LambdaContext { + invokedFunctionArn: string; + getRemainingTimeInMillis(): number; +} diff --git a/tests/lib/utils.test.ts b/tests/lib/utils.test.ts new file mode 100644 index 0000000..a0ed382 --- /dev/null +++ b/tests/lib/utils.test.ts @@ -0,0 +1,16 @@ +import { + minToCron, +} from '../../src/utils'; + + +describe('when getting utils', () => { + + test('minutes to cron', () => { + const spy: jest.SpyInstance = jest.spyOn(global.Date, 'now').mockImplementationOnce(() => { + return new Date(2020, 1, 1, 1, 1).valueOf(); + }); + const cron = minToCron(1); + expect(spy).toHaveBeenCalledTimes(1); + expect(cron).toBe('cron(3 1 1 1 ? 2020)'); + }); +}); From 56819238f145a71a9cf54146007e85de16c23b36 Mon Sep 17 00:00:00 2001 From: Eduardo Rodrigues Date: Sat, 11 Apr 2020 20:56:01 +0200 Subject: [PATCH 08/15] add resource for event handlers --- src/resource.ts | 389 +++++++++++++++++++++++++++ tests/lib/resource.test.ts | 528 +++++++++++++++++++++++++++++++++++++ 2 files changed, 917 insertions(+) create mode 100644 src/resource.ts create mode 100644 tests/lib/resource.test.ts diff --git a/src/resource.ts b/src/resource.ts new file mode 100644 index 0000000..b49226c --- /dev/null +++ b/src/resource.ts @@ -0,0 +1,389 @@ +import 'reflect-metadata'; +import { boundMethod } from 'autobind-decorator'; + +import { ProgressEvent, SessionProxy } from './proxy'; +import { reportProgress } from './callback'; +import { BaseHandlerException, InternalFailure, InvalidRequest } from './exceptions'; +import { + Action, + BaseResourceModel, + BaseResourceHandlerRequest, + Callable, + Credentials, + HandlerErrorCode, + OperationStatus, + Optional, + Response, +} from './interface'; +import { ProviderLogHandler } from './log-delivery'; +import { MetricsPublisherProxy } from './metrics'; +import { cleanupCloudwatchEvents, rescheduleAfterMinutes } from './scheduler'; +import { + Constructor, + HandlerRequest, + LambdaContext, + TestEvent, + UnmodeledRequest, +} from './utils'; + +const LOGGER = console; +const MUTATING_ACTIONS: [Action, Action, Action] = [Action.Create, Action.Update, Action.Delete]; +const INVOCATION_TIMEOUT_MS = 60000; + +export type HandlerSignature = Callable<[Optional, any, Map], Promise>; +export class HandlerSignatures extends Map {}; +class HandlerEvents extends Map {}; + +/** + * Decorates a method to ensure that the JSON input and output are serialized properly. + * + * @returns {PropertyDescriptor} + */ +function ensureSerialize(target: any, propertyKey: string, descriptor: PropertyDescriptor): PropertyDescriptor { + + // Save a reference to the original method this way we keep the values currently in the + // descriptor and don't overwrite what another decorator might have done to the descriptor. + if(descriptor === undefined) { + descriptor = Object.getOwnPropertyDescriptor(target, propertyKey); + } + const originalMethod = descriptor.value; + // Wrapping the original method with new signature. + descriptor.value = async function(event: Object | Map, context: any): Promise { + let mappedEvent: Map; + if (event instanceof Map) { + mappedEvent = new Map(event); + } else { + mappedEvent = new Map(Object.entries(event)); + } + const progress = await originalMethod.apply(this, [mappedEvent, context]); + // Use the raw event data as a last-ditch attempt to call back if the + // request is invalid. + const serialized = progress.serialize(true, mappedEvent.get('bearerToken')); + return serialized.toObject(); + } + return descriptor; +} + +/** + * Decorates a method to point to the proper action + * + * @returns {MethodDecorator} + */ +export function handlerEvent(action: Action): MethodDecorator { + return function(target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor): PropertyDescriptor { + if (target instanceof BaseResource) { + const actions: HandlerEvents = Reflect.getMetadata('handlerEvents', target) || new HandlerEvents(); + if (!actions.has(action)) { + actions.set(action, propertyKey); + } + Reflect.defineMetadata('handlerEvents', actions, target); + } + if (descriptor) { + return descriptor; + } + } +} + +export abstract class BaseResource { + constructor( + public typeName: string, + private modelCls: Constructor, + private handlers?: HandlerSignatures, + ) { + this.typeName = typeName || ''; + this.handlers = handlers || new HandlerSignatures(); + const actions: HandlerEvents = Reflect.getMetadata('handlerEvents', this) || new HandlerEvents(); + actions.forEach((value: string | symbol, key: Action) => { + this.addHandler(key, (this as any)[value]); + }); + } + + public addHandler = (action: Action, f: HandlerSignature): HandlerSignature => { + this.handlers.set(action, f); + return f; + } + + public static scheduleReinvocation = async ( + handlerRequest: HandlerRequest, + handlerResponse: ProgressEvent, + context: LambdaContext, + session: SessionProxy, + ): Promise => { + if (handlerResponse.status !== OperationStatus.InProgress) { + return false; + } + // Modify requestContext dict in-place, so that invoke count is bumped on local + // reinvoke too + const reinvokeContext = handlerRequest.requestContext; + reinvokeContext.invocation = (reinvokeContext.invocation || 0) + 1; + const callbackDelaySeconds = handlerResponse.callbackDelaySeconds; + const remainingMs = context.getRemainingTimeInMillis(); + + // When a handler requests a sub-minute callback delay, and if the lambda + // invocation has enough runtime (with 20% buffer), we can re-run the handler + // locally otherwise we re-invoke through CloudWatchEvents + const neededMsRemaining = callbackDelaySeconds * 1200 + INVOCATION_TIMEOUT_MS; + if (callbackDelaySeconds < 60 && remainingMs > neededMsRemaining) { + const delay = async (ms: number) => { + await new Promise(r => setTimeout(() => r(), ms)); + }; + delay(callbackDelaySeconds * 1000); + return true; + } + const callbackDelayMin = Number(callbackDelaySeconds / 60); + rescheduleAfterMinutes( + session, + context.invokedFunctionArn, + callbackDelayMin, + handlerRequest, + ); + return false; + } + + private invokeHandler = async ( + session: Optional, + request: BaseResourceHandlerRequest, + action: Action, + callbackContext: Map, + ): Promise => { + const handle: HandlerSignature = this.handlers.get(action); + if (!handle) { + return ProgressEvent.failed( + HandlerErrorCode.InternalFailure, `No handler for ${action}` + ); + } + const progress = await handle(session, request, callbackContext); + const isInProgress = progress.status === OperationStatus.InProgress; + const isMutable = MUTATING_ACTIONS.some(x => x === action); + if (isInProgress && !isMutable) { + throw new InternalFailure('READ and LIST handlers must return synchronously.'); + } + return progress; + } + + private parseTestRequest = ( + eventData: Map + ): [ + Optional, + BaseResourceHandlerRequest, + Action, + Map, + ] => { + let session: SessionProxy; + let request: BaseResourceHandlerRequest; + let action: Action; + let event: TestEvent; + try { + event = new TestEvent(eventData); + const creds = event.credentials as Credentials; + if (!creds) { + throw new Error('Event data is missing required property "credentials".') + } + if (!this.modelCls) { + throw new Error('Missing Model class to be used to deserialize JSON data.') + } + if (event.request instanceof Map) { + event.request = new Map(event.request); + } else { + event.request = new Map(Object.entries(event.request)); + } + request = new UnmodeledRequest(event.request).toModeled(this.modelCls); + + session = SessionProxy.getSession(creds, event.region); + action = event.action; + } catch(err) { + LOGGER.error('Invalid request'); + throw new InternalFailure(`${err} (${err.name})`); + } + + return [session, request, action, event.callbackContext || new Map()]; + } + + // @ts-ignore + public async testEntrypoint ( + eventData: Object | Map, context: any + ): Promise>; + @boundMethod + @ensureSerialize + public async testEntrypoint( + eventData: Map, context: any + ): Promise { + let msg = 'Uninitialized'; + let progress: ProgressEvent; + try { + const [ session, request, action, callbackContext ] = this.parseTestRequest(eventData); + progress = await this.invokeHandler(session, request, action, callbackContext); + } catch(err) { + if (err instanceof BaseHandlerException) { + LOGGER.error('Handler error') + progress = err.toProgressEvent(); + } + LOGGER.error('Exception caught'); + msg = err.message || msg; + progress = ProgressEvent.failed(HandlerErrorCode.InternalFailure, msg); + } + return Promise.resolve(progress); + } + + private static parseRequest = ( + eventData: Map + ): [ + [Optional, Optional, SessionProxy], + Action, + Map, + HandlerRequest, + ] => { + let callerSession: Optional; + let platformSession: Optional; + let providerSession: SessionProxy; + let action: Action; + let callbackContext: Map; + let event: HandlerRequest; + try { + event = HandlerRequest.deserialize(eventData); + if (!event.awsAccountId) { + throw new Error('Event data is missing required property "awsAccountId".') + } + const platformCredentials = event.requestData.platformCredentials; + platformSession = SessionProxy.getSession(platformCredentials); + callerSession = SessionProxy.getSession(event.requestData.callerCredentials); + providerSession = SessionProxy.getSession(event.requestData.providerCredentials); + // Credentials are used when rescheduling, so can't zero them out (for now). + if (!platformSession || !platformCredentials || Object.keys(platformCredentials).length === 0) { + throw new Error('No platform credentials'); + } + action = event.action; + callbackContext = event.requestContext.callbackContext || {} as Map; + } catch(err) { + LOGGER.error('Invalid request'); + throw new InvalidRequest(`${err} (${err.name})`); + } + return [ + [callerSession, platformSession, providerSession], + action, + callbackContext, + event, + ] + } + + private castResourceRequest = ( + request: HandlerRequest + ): BaseResourceHandlerRequest => { + try { + const unmodeled: UnmodeledRequest = UnmodeledRequest.fromUnmodeled({ + clientRequestToken: request.bearerToken, + desiredResourceState: request.requestData.resourceProperties, + previousResourceState: request.requestData.previousResourceProperties, + logicalResourceIdentifier: request.requestData.logicalResourceId, + }); + return unmodeled.toModeled(this.modelCls); + } catch(err) { + LOGGER.error('Invalid request'); + throw new InvalidRequest(`${err} (${err.name})`); + } + } + + // @ts-ignore + public async entrypoint ( + eventData: Object | Map, context: LambdaContext + ): Promise>; + @boundMethod + @ensureSerialize + public async entrypoint ( + eventData: Map, context: LambdaContext + ): Promise { + + let isLogSetup: boolean = false; + let progress: ProgressEvent; + + const printOrLog = (message: string): void => { + if (isLogSetup) { + LOGGER.error(message); + } else { + console.log(message); + console.trace(); + } + } + + try { + const [sessions, action, callback, event] = BaseResource.parseRequest(eventData); + const [callerSession, platformSession, providerSession] = sessions; + ProviderLogHandler.setup(event, providerSession); + isLogSetup = true; + + const request = this.castResourceRequest(event); + + const metrics = new MetricsPublisherProxy(event.awsAccountId, event.resourceType); + metrics.addMetricsPublisher(platformSession); + metrics.addMetricsPublisher(callerSession); + // Acknowledge the task for first time invocation. + if (!event.requestContext || Object.keys(event.requestContext).length === 0) { + await reportProgress({ + session: platformSession, + bearerToken: event.bearerToken, + errorCode: null, + operationStatus: OperationStatus.InProgress, + currentOperationStatus: OperationStatus.Pending, + resourceModel: null, + message: '', + }); + } else { + // If this invocation was triggered by a 're-invoke' CloudWatch Event, + // clean it up. + cleanupCloudwatchEvents( + platformSession, + event.requestContext.cloudWatchEventsRuleName || '', + event.requestContext.cloudWatchEventsTargetId || '', + ); + } + let invoke: boolean = true; + while (invoke) { + const startTime = new Date(Date.now()); + metrics.publishInvocationMetric(startTime, action); + let error: Error; + try { + progress = await this.invokeHandler( + callerSession, request, action, callback + ); + } catch(err) { + error = err; + } + const endTime = new Date(Date.now()); + const milliseconds: number = endTime.getTime() - startTime.getTime(); + metrics.publishDurationMetric(endTime, action, milliseconds); + if (error) { + metrics.publishExceptionMetric(new Date(Date.now()), action, error); + throw error; + } + if (progress.callbackContext) { + const callback = progress.callbackContext; + event.requestContext.callbackContext = callback; + } + if (MUTATING_ACTIONS.includes(event.action)) { + await reportProgress({ + session: platformSession, + bearerToken: event.bearerToken, + errorCode: progress.errorCode, + operationStatus: progress.status, + currentOperationStatus: OperationStatus.InProgress, + resourceModel: progress.resourceModel, + message: progress.message, + }); + } + invoke = await BaseResource.scheduleReinvocation( + event, progress, context, platformSession + ); + } + } catch(err) { + if (err instanceof BaseHandlerException) { + printOrLog('Handler error'); + progress = err.toProgressEvent(); + } else { + printOrLog('Exception caught'); + progress = ProgressEvent.failed(HandlerErrorCode.InternalFailure, err.message); + } + } + + return progress; + } +} diff --git a/tests/lib/resource.test.ts b/tests/lib/resource.test.ts new file mode 100644 index 0000000..3e9a58b --- /dev/null +++ b/tests/lib/resource.test.ts @@ -0,0 +1,528 @@ +import CloudWatchEvents from 'aws-sdk/clients/cloudwatchevents'; +import CloudFormation from 'aws-sdk/clients/cloudformation'; + +import * as exceptions from '../../src/exceptions'; +import { ProgressEvent, SessionProxy } from '../../src/proxy'; +import { reportProgress } from '../../src/callback'; +import { + Action, + BaseResourceHandlerRequest, + HandlerErrorCode, + OperationStatus, + RequestContext, + Response, + BaseResourceModel, +} from '../../src/interface'; +import { ProviderLogHandler } from '../../src/log-delivery'; +import { MetricsPublisherProxy } from '../../src/metrics'; +import { handlerEvent, HandlerSignatures, BaseResource } from '../../src/resource'; +import { cleanupCloudwatchEvents, rescheduleAfterMinutes } from '../../src/scheduler'; +import { HandlerRequest, LambdaContext } from '../../src/utils'; + + +const mockResult = (output: any): jest.Mock => { + return jest.fn().mockReturnValue({ + promise: jest.fn().mockResolvedValue(output) + }); +}; + +jest.useFakeTimers(); +jest.mock('aws-sdk/clients/cloudformation'); +jest.mock('aws-sdk/clients/cloudwatchevents'); +jest.mock('../../src/callback'); +jest.mock('../../src/log-delivery'); +jest.mock('../../src/metrics'); +jest.mock('../../src/scheduler'); + +describe('when getting resource', () => { + + let entrypointPayload: Object; + let mockSession: jest.SpyInstance; + const TYPE_NAME = 'Test::Foo::Bar'; + class Resource extends BaseResource {}; + class MockModel extends BaseResourceModel { + ['constructor']: typeof MockModel; + public static deserialize(jsonData: any): MockModel { + return new MockModel(); + } + } + + beforeEach(() => { + const mockEvents = (CloudWatchEvents as unknown) as jest.Mock; + mockEvents.mockImplementation(() => { + const returnValue = { + deleteRule: mockResult({}), + putRule: mockResult({}), + putTargets: mockResult({}), + removeTargets: mockResult({}), + }; + return { + ...returnValue, + makeRequest: (operation: string, params?: {[key: string]: any}) => { + return returnValue[operation](params); + } + }; + }); + const mockCloudformation = (CloudFormation as unknown) as jest.Mock; + mockCloudformation.mockImplementation(() => { + const returnValue = { + recordHandlerProgress: mockResult({}), + }; + return { + ...returnValue, + makeRequest: (operation: string, params?: {[key: string]: any}) => { + return returnValue[operation](params); + } + }; + }); + entrypointPayload = { + awsAccountId: '123456789012', + bearerToken: '123456', + region: 'us-east-1', + action: 'CREATE', + responseEndpoint: 'cloudformation.us-west-2.amazonaws.com', + resourceType: 'AWS::Test::TestModel', + resourceTypeVersion: '1.0', + requestContext: {}, + requestData: { + callerCredentials: { + accessKeyId: 'IASAYK835GAIFHAHEI23', + secretAccessKey: '66iOGPN5LnpZorcLr8Kh25u8AbjHVllv5/poh2O0', + sessionToken: 'lameHS2vQOknSHWhdFYTxm2eJc1JMn9YBNI4nV4mXue945KPL6DHfW8EsUQT5zwssYEC1NvYP9yD6Y5s5lKR3chflOHPFsIe6eqg', + }, + platformCredentials: { + accessKeyId: '32IEHAHFIAG538KYASAI', + secretAccessKey: '0O2hop/5vllVHjbA8u52hK8rLcroZpnL5NPGOi66', + sessionToken: 'gqe6eIsFPHOlfhc3RKl5s5Y6Dy9PYvN1CEYsswz5TQUsE8WfHD6LPK549euXm4Vn4INBY9nMJ1cJe2mxTYFdhWHSnkOQv2SHemal', + }, + providerCredentials: { + accessKeyId: 'HDI0745692Y45IUTYR78', + secretAccessKey: '4976TUYVI234/5GW87ERYG823RF87GY9EIUH452I3', + sessionToken: '842HYOFIQAEUDF78R8T7IU43HSADYGIFHBJSDHFA87SDF9PYvN1CEYASDUYFT5TQ97YASIHUDFAIUEYRISDKJHFAYSUDTFSDFADS', + }, + providerLogGroupName: 'providerLoggingGroupName', + logicalResourceId: 'myBucket', + resourceProperties: 'state1', + previousResourceProperties: 'state2', + systemTags: { 'aws:cloudformation:stack-id': 'SampleStack' }, + stackTags: { tag1: 'abc' }, + previousStackTags: { tag1: 'def' }, + }, + stackId: 'arn:aws:cloudformation:us-east-1:123456789012:stack/SampleStack/e722ae60-fe62-11e8-9a0e-0ae8cc519968', + }; + mockSession = jest.spyOn(SessionProxy, 'getSession').mockImplementation(() => { + return new SessionProxy({}); + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + const getResource = (handlers?: HandlerSignatures) => { + const instance = new Resource(TYPE_NAME, null, handlers); + return instance; + } + + test('entrypoint handler error', async () => { + const resource = getResource(); + const event: Response = await resource.entrypoint({}, null); + expect(event.operationStatus).toBe(OperationStatus.Failed); + expect(event.errorCode).toBe(HandlerErrorCode.InvalidRequest); + }); + + test('entrypoint success', async () => { + const mockLogDelivery: jest.Mock = (ProviderLogHandler.setup as unknown) as jest.Mock; + const mockReportProgress: jest.Mock = (reportProgress as unknown) as jest.Mock; + const mockHandler: jest.Mock = jest.fn(() => ProgressEvent.success()); + const resource = new Resource(TYPE_NAME, MockModel); + resource.addHandler(Action.Create, mockHandler); + const event: Response = await resource.entrypoint(entrypointPayload, null); + expect(mockLogDelivery).toBeCalledTimes(1); + expect(mockReportProgress).toBeCalledTimes(2); + expect(event).toMatchObject({ + message: '', + bearerToken: '123456', + operationStatus: OperationStatus.Success, + }); + expect(mockHandler).toBeCalledTimes(1); + }); + + test('entrypoint handler raises', async () => { + class Model extends BaseResourceModel { + ['constructor']: typeof Model; + aString: string; + public static deserialize(jsonData: any): Model { + return new Model('test'); + } + } + const resource = new Resource(TYPE_NAME, Model); + const mockPublishException = (MetricsPublisherProxy.prototype.publishExceptionMetric as unknown) as jest.Mock; + const mockInvokeHandler = jest.spyOn(resource, 'invokeHandler'); + mockInvokeHandler.mockImplementation(() => { + throw new exceptions.InvalidRequest('handler failed'); + }); + const event: Response = await resource.entrypoint(entrypointPayload, null); + expect(mockPublishException).toBeCalledTimes(1); + expect(mockInvokeHandler).toBeCalledTimes(1); + expect(event).toMatchObject({ + errorCode: 'InvalidRequest', + message: 'Error: handler failed', + bearerToken: '123456', + operationStatus: OperationStatus.Failed, + }); + }); + + test('entrypoint non mutating action', async () => { + const resource = new Resource(TYPE_NAME, MockModel); + entrypointPayload['action'] = 'READ'; + const mockReportProgress: jest.Mock = (reportProgress as unknown) as jest.Mock; + const mockHandler: jest.Mock = jest.fn(() => ProgressEvent.success()); + resource.addHandler(Action.Create, mockHandler); + await resource.entrypoint(entrypointPayload, null); + expect(mockReportProgress).toBeCalledTimes(1); + }); + + test('entrypoint with context', async () => { + entrypointPayload['requestContext'] = { 'a': 'b' }; + const mockCleanupEvents: jest.Mock = (cleanupCloudwatchEvents as unknown) as jest.Mock; + const event: ProgressEvent = ProgressEvent.success(null, { 'c': 'd' }); + const mockHandler: jest.Mock = jest.fn(() => event); + const resource = new Resource(TYPE_NAME, MockModel); + resource.addHandler(Action.Create, mockHandler); + await resource.entrypoint(entrypointPayload, null); + expect(mockCleanupEvents).toBeCalledTimes(1); + expect(mockCleanupEvents).toBeCalledWith(expect.anything(), '', ''); + expect(mockHandler).toBeCalledTimes(1); + }); + + test('entrypoint success without caller provider creds', async () => { + const mockHandler: jest.Mock = jest.fn(() => ProgressEvent.success()); + const resource = new Resource(TYPE_NAME, MockModel); + resource.addHandler(Action.Create, mockHandler); + const expected = { + message: '', + bearerToken: '123456', + operationStatus: OperationStatus.Success, + }; + // Credentials are defined in payload, but null. + entrypointPayload['requestData']['providerCredentials'] = null; + entrypointPayload['requestData']['callerCredentials'] = null; + let response: Response = await resource.entrypoint(entrypointPayload, null); + expect(response).toMatchObject(expected); + + // Credentials are undefined in payload. + delete entrypointPayload['requestData']['providerCredentials']; + delete entrypointPayload['requestData']['callerCredentials']; + response = await resource.entrypoint(entrypointPayload, null); + expect(response).toMatchObject(expected); + }); + + test('parse request fail without platform creds', () => { + const resource = new Resource(TYPE_NAME, MockModel); + entrypointPayload['requestData']['platformCredentials'] = null; + const payload = new Map(Object.entries(entrypointPayload)); + const parseRequest = () => { + resource.constructor['parseRequest'](payload); + }; + expect(parseRequest).toThrow(exceptions.InvalidRequest); + expect(parseRequest).toThrow('Error: No platform credentials (Error)'); + }); + + test('parse request invalid request', () => { + const parseRequest = () => { + Resource['parseRequest'](new Map()); + }; + expect(parseRequest).toThrow(exceptions.InvalidRequest); + expect(parseRequest).toThrow(/missing.+awsAccountId/i); + }); + + test('cast resource request invalid request', () => { + const payload = new Map(Object.entries(entrypointPayload)); + const request = HandlerRequest.deserialize(payload); + request.requestData = null; + const resource = getResource(); + const castResourceRequest = () => { + resource['castResourceRequest'](request); + }; + expect(castResourceRequest).toThrow(exceptions.InvalidRequest); + expect(castResourceRequest).toThrow('TypeError: Cannot read property'); + }); + + test('parse request valid request and cast resource request', () => { + const mockDeserialize: jest.Mock = jest.fn() + .mockImplementationOnce(() => { + return { state: 'state1' }; + }).mockImplementationOnce(() => { + return { state: 'state2' }; + }); + + class Model extends BaseResourceModel { + ['constructor']: typeof Model; + public static deserialize = mockDeserialize; + } + + const resource = new Resource(TYPE_NAME, Model); + + const payload = new Map(Object.entries(entrypointPayload)); + const [sessions, action, callback, request] = resource.constructor['parseRequest'](payload); + + expect(mockSession).toBeCalledTimes(3); + expect(mockSession).nthCalledWith(1, entrypointPayload['requestData']['platformCredentials']); + expect(mockSession).nthCalledWith(2, entrypointPayload['requestData']['callerCredentials']); + expect(mockSession).nthCalledWith(3, entrypointPayload['requestData']['providerCredentials']); + // Credentials are used when rescheduling, so can't zero them out (for now). + expect(request.requestData.callerCredentials).not.toBeNull(); + expect(request.requestData.providerCredentials).not.toBeNull(); + expect(request.requestData.platformCredentials).not.toBeNull(); + + const [callerSession, platformSession, providerSession] = sessions; + expect(mockSession).nthReturnedWith(1, platformSession) + expect(mockSession).nthReturnedWith(2, callerSession) + expect(mockSession).nthReturnedWith(3, providerSession) + + expect(action).toBe(Action.Create); + expect(callback).toMatchObject({}); + + const modeledRequest = resource['castResourceRequest'](request); + expect(mockDeserialize).nthCalledWith(1, 'state1'); + expect(mockDeserialize).nthCalledWith(2, 'state2'); + expect(modeledRequest).toMatchObject({ + clientRequestToken: request.bearerToken, + desiredResourceState: {state: 'state1'}, + previousResourceState: {state: 'state2'}, + logicalResourceIdentifier: 'myBucket', + }); + }); + + test('entrypoint uncaught exception', async () => { + const mockParseRequest = jest.spyOn(BaseResource, 'parseRequest'); + mockParseRequest.mockImplementationOnce(() => { + throw new Error('exception'); + }); + const resource = getResource(); + const event: Response = await resource.entrypoint({}, null); + expect(mockParseRequest).toBeCalledTimes(1); + expect(event.operationStatus).toBe(OperationStatus.Failed); + expect(event.errorCode).toBe(HandlerErrorCode.InternalFailure); + expect(event.message).toBe('exception'); + }); + + test('add handler', () => { + class ResourceEventHandler extends BaseResource { + @handlerEvent(Action.Create) + public create() {} + @handlerEvent(Action.Read) + public read() {} + @handlerEvent(Action.Update) + public update() {} + @handlerEvent(Action.Delete) + public delete() {} + @handlerEvent(Action.List) + public list() {} + }; + const handlers: HandlerSignatures = new HandlerSignatures(); + const resource = new ResourceEventHandler(null, null, handlers); + expect(resource['handlers'].get(Action.Create)).toBe(resource.create); + expect(resource['handlers'].get(Action.Read)).toBe(resource.read); + expect(resource['handlers'].get(Action.Update)).toBe(resource.update); + expect(resource['handlers'].get(Action.Delete)).toBe(resource.delete); + expect(resource['handlers'].get(Action.List)).toBe(resource.list); + + }); + + test('invoke handler not found', async () => { + const resource = getResource(); + const callbackContext = new Map(); + const actual = await resource['invokeHandler'](null, null, Action.Create, callbackContext); + const expected = ProgressEvent.failed(HandlerErrorCode.InternalFailure, 'No handler for CREATE'); + expect(actual).toStrictEqual(expected); + }); + + test('invoke handler was found', async () => { + const event: ProgressEvent = ProgressEvent.progress(); + const mockHandler: jest.Mock = jest.fn(() => event); + const handlers: HandlerSignatures = new HandlerSignatures(); + handlers.set(Action.Create, mockHandler); + const resource = getResource(handlers); + const session = new SessionProxy({}); + const request = new BaseResourceHandlerRequest(); + const callbackContext = new Map(); + const response = await resource['invokeHandler']( + session, request, Action.Create, callbackContext); + expect(response).toBe(event); + expect(mockHandler).toBeCalledTimes(1); + expect(mockHandler).toBeCalledWith(session, request, callbackContext); + }); + + test('invoke handler non mutating must be synchronous', () => { + [Action.List, Action.Read].forEach((action: Action) => { + const mockHandler: jest.Mock = jest.fn(() => ProgressEvent.progress()); + const handlers: HandlerSignatures = new HandlerSignatures(); + handlers.set(action, mockHandler); + const resource = getResource(handlers); + const callbackContext = new Map(); + expect(resource['invokeHandler'](null, null, action, callbackContext)).rejects.toEqual( + new exceptions.InternalFailure('READ and LIST handlers must return synchronously.')); + }); + }); + + test('parse test request invalid request', () => { + const resource = getResource(); + const parseTestRequest = () => { + resource['parseTestRequest'](new Map()); + }; + expect(parseTestRequest).toThrow(exceptions.InternalFailure); + expect(parseTestRequest).toThrow(/missing.+credentials/i); + }); + + test('parse test request valid request', () => { + const mockDeserialize: jest.Mock = jest.fn() + .mockImplementationOnce(() => { + return { state: 'state1' }; + }).mockImplementationOnce(() => { + return { state: 'state2' }; + }); + + class Model extends BaseResourceModel { + ['constructor']: typeof Model; + public static deserialize = mockDeserialize; + } + + const resource = new Resource(TYPE_NAME, Model); + resource.addHandler(Action.Create, jest.fn()); + const payload = new Map(Object.entries({ + credentials: { + accessKeyId: '', secretAccessKey: '', sessionToken: '' + }, + action: 'CREATE', + request: { + clientRequestToken: 'ecba020e-b2e6-4742-a7d0-8a06ae7c4b2b', + desiredResourceState: 'state1', + previousResourceState: 'state2', + logicalResourceIdentifier: null, + }, + callbackContext: null, + })); + const [session, request, action, callback] = resource['parseTestRequest'](payload); + + expect(mockSession).toBeCalledTimes(1); + expect(mockSession).toHaveReturnedWith(session); + + expect(mockDeserialize).nthCalledWith(1, 'state1'); + expect(mockDeserialize).nthCalledWith(2, 'state2'); + expect(request).toMatchObject({ + clientRequestToken: 'ecba020e-b2e6-4742-a7d0-8a06ae7c4b2b', + desiredResourceState: {state: 'state1'}, + previousResourceState: {state: 'state2'}, + logicalResourceIdentifier: null, + }); + + expect(action).toBe(Action.Create); + expect(callback).toMatchObject({}); + }); + + test('test entrypoint handler error', async () => { + const resource = getResource(); + const event: Response = await resource.testEntrypoint({}, null); + expect(event.operationStatus).toBe(OperationStatus.Failed); + expect(event.errorCode).toBe(HandlerErrorCode.InternalFailure); + }); + + test('test entrypoint uncaught exception', async () => { + const resource = getResource(); + const mockParseRequest = jest.spyOn(resource, 'parseTestRequest'); + mockParseRequest.mockImplementationOnce(() => { + throw new Error('exception'); + }); + const event: Response = await resource.testEntrypoint({}, null); + expect(event.operationStatus).toBe(OperationStatus.Failed); + expect(event.errorCode).toBe(HandlerErrorCode.InternalFailure); + expect(event.message).toBe('exception'); + }); + + test('test entrypoint success', async () => { + class Model extends BaseResourceModel { + ['constructor']: typeof Model; + } + const spyDeserialize: jest.SpyInstance = jest.spyOn(Model, 'deserialize'); + + const resource = new Resource(TYPE_NAME, Model); + + const progressEvent: ProgressEvent = ProgressEvent.progress(); + const mockHandler: jest.Mock = jest.fn(() => progressEvent); + resource.addHandler(Action.Create, mockHandler); + const payload = { + credentials: { + accessKeyId: '', secretAccessKey: '', sessionToken: '' + }, + action: 'CREATE', + request: { + clientRequestToken: 'ecba020e-b2e6-4742-a7d0-8a06ae7c4b2b', + desiredResourceState: {state: 'state1'}, + previousResourceState: {state: 'state2'}, + logicalResourceIdentifier: null, + }, + }; + const event: Response = await resource.testEntrypoint(payload, null); + expect(event).toMatchObject({ + message: '', + operationStatus: OperationStatus.InProgress, + }); + + expect(spyDeserialize).nthCalledWith(1, {state: 'state1'}); + expect(spyDeserialize).nthCalledWith(2, {state: 'state2'}); + expect(mockHandler).toBeCalledTimes(1); + }); + + test('schedule reinvocation not in progress', async () => { + const mockReschedule: jest.Mock = (rescheduleAfterMinutes as unknown) as jest.Mock; + const session = new SessionProxy({}); + const request = new HandlerRequest(); + const context: LambdaContext = {} as LambdaContext; + const reinvoke = await Resource['scheduleReinvocation']( + request, ProgressEvent.success(), context, session); + expect(reinvoke).toBe(false); + expect(mockSession).not.toHaveBeenCalled(); + expect(mockReschedule).not.toHaveBeenCalled(); + }); + + test('schedule reinvocation local callback', async () => { + const event = ProgressEvent.progress(); + event.callbackDelaySeconds = 5; + const session = new SessionProxy({}); + const request = new HandlerRequest(); + request.requestContext = {} as RequestContext>; + const context: LambdaContext = { + invokedFunctionArn: 'arn:aaa:bbb:ccc', + getRemainingTimeInMillis: jest.fn().mockReturnValue(600000), + } as LambdaContext; + const reinvoke = await Resource['scheduleReinvocation']( + request, event, context, session); + expect(reinvoke).toBe(true); + expect(setTimeout).toHaveBeenCalledTimes(1); + expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 5000); + expect(request.requestContext.invocation).toBe(1); + }); + + test('schedule reinvocation cloudwatch callback', async () => { + const event = ProgressEvent.progress(); + event.callbackDelaySeconds = 60; + const mockReschedule: jest.Mock = (rescheduleAfterMinutes as unknown) as jest.Mock; + const session = new SessionProxy({}); + const request = new HandlerRequest(); + request.requestContext = {} as RequestContext>; + const context: LambdaContext = { + invokedFunctionArn: 'arn:aaa:bbb:ccc', + getRemainingTimeInMillis: jest.fn().mockReturnValue(6000), + } as LambdaContext; + const reinvoke = await Resource['scheduleReinvocation']( + request, event, context, session); + expect(reinvoke).toBe(false); + expect(mockReschedule).toBeCalledTimes(1); + expect(mockReschedule).toHaveBeenCalledWith(expect.anything(), 'arn:aaa:bbb:ccc', 1, request); + expect(setTimeout).not.toHaveBeenCalled(); + expect(request.requestContext.invocation).toBe(1); + }); +}); From dedc050bdbb0bd93054be74ab574c03e18061555 Mon Sep 17 00:00:00 2001 From: Eduardo Rodrigues Date: Wed, 15 Apr 2020 00:41:04 +0200 Subject: [PATCH 09/15] update to the cli plugin --- README.md | 6 +- package-lock.json | 72 ++++++++--- package.json | 18 ++- python/rpdk/typescript/codegen.py | 113 ++++++------------ python/rpdk/typescript/data/.npmrc | 1 + python/rpdk/typescript/data/tsconfig.json | 2 +- .../rpdk/typescript/data/typescript.gitignore | 5 + python/rpdk/typescript/templates/README.md | 28 +++-- python/rpdk/typescript/templates/handlers.ts | 98 +++++++-------- python/rpdk/typescript/templates/models.ts | 8 +- python/rpdk/typescript/templates/package.json | 16 ++- tsconfig.json | 3 +- 12 files changed, 197 insertions(+), 173 deletions(-) create mode 100644 python/rpdk/typescript/data/.npmrc diff --git a/README.md b/README.md index 8e46fa4..bbcf216 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # DEVELOPER PREVIEW (COMMUNITY DRIVEN) -We're excited to share our progress with adding new languages to the CloudFormation CLI! This plugin is an early preview prepared by the community, and not ready for production use. +We're excited to share our progress with adding new languages to the CloudFormation CLI! +> This plugin is an early preview prepared by the community, and not ready for production use. ## AWS CloudFormation Resource Provider TypeScript Plugin @@ -16,6 +17,7 @@ If you are using this package to build resource providers for CloudFormation, in **Prerequisites** - Python version 3.6 or above + - [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) - Your choice of TypeScript IDE **Installation** @@ -28,7 +30,7 @@ pip3 install git+https://github.com/eduardomourar/cloudformation-cli-typescript- Refer to the [CloudFormation CLI User Guide](https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-types.html) for the [CloudFormation CLI](https://github.com/aws-cloudformation/cloudformation-cli) for usage instructions. -** Howto** +**Howto** Example run: diff --git a/package-lock.json b/package-lock.json index 2de3e0d..5ceaea5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -465,6 +465,15 @@ "type-detect": "4.0.8" } }, + "@types/aws-sdk": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@types/aws-sdk/-/aws-sdk-2.7.0.tgz", + "integrity": "sha1-g1iLPRTr3KHUzl4CM4dXdWjOgvM=", + "dev": true, + "requires": { + "aws-sdk": "*" + } + }, "@types/babel__core": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.6.tgz", @@ -548,9 +557,9 @@ } }, "@types/node": { - "version": "12.12.29", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.29.tgz", - "integrity": "sha512-yo8Qz0ygADGFptISDj3pOC9wXfln/5pQaN/ysDIzOaAWXt73cNHmtEC8zSO2Y+kse/txmwIAJzkYZ5fooaS5DQ==", + "version": "12.12.34", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.34.tgz", + "integrity": "sha512-BneGN0J9ke24lBRn44hVHNeDlrXRYF+VRp0HbSUNnEZahXGAysHZIqnf/hER6aabdBgzM4YOV4jrR8gj4Zfi0g==", "dev": true }, "@types/stack-utils": { @@ -741,10 +750,15 @@ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", "dev": true }, + "autobind-decorator": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/autobind-decorator/-/autobind-decorator-2.4.0.tgz", + "integrity": "sha512-OGYhWUO72V6DafbF8PM8rm3EPbfuyMZcJhtm5/n26IDwO18pohE4eNazLoCGhPiXOCD0gEGmrbU3849QvM8bbw==" + }, "aws-sdk": { - "version": "2.635.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.635.0.tgz", - "integrity": "sha512-NlKqMB4HqMqSutY6YmPzQVa+mMhqo0655hYYl8G2zkUvrYy+YxDitvwDEUkSsNKVFkEvmHtZggFCgVYIUu/sXg==", + "version": "2.656.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.656.0.tgz", + "integrity": "sha512-UzqDvvt6i7gpuzEdK0GT/JOfBJcsCPranzZWdQ9HR4+5E0m5kf5gybZ6OX+UseIAE2/WND6Dv0aHgiI21AKenw==", "requires": { "buffer": "4.9.1", "events": "1.1.1", @@ -3389,6 +3403,11 @@ "util.promisify": "^1.0.0" } }, + "reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" + }, "regex-not": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", @@ -4246,9 +4265,8 @@ } }, "tombok": { - "version": "git+https://github.com/eduardomourar/tombok.git#a6f84ddcdb2d145f5bcd42f3882e0f00532abee7", - "from": "git+https://github.com/eduardomourar/tombok.git#feature/basic-implementation", - "dev": true + "version": "https://github.com/eduardomourar/tombok/releases/download/v0.0.1/tombok-0.0.1.tar.gz", + "integrity": "sha512-VQimIr0UaGvuZ4Pgr2RCOUAgAKQKRjhEi+onESlZ15kQZiw1gPygykEDH0Xu9TkjP9CtZ+c5UByRG+uXKnJ0bQ==" }, "tough-cookie": { "version": "3.0.1", @@ -4287,9 +4305,9 @@ } }, "ts-jest": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-25.2.1.tgz", - "integrity": "sha512-TnntkEEjuXq/Gxpw7xToarmHbAafgCaAzOpnajnFC6jI7oo1trMzAHA04eWpc3MhV6+yvhE8uUBAmN+teRJh0A==", + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-25.3.0.tgz", + "integrity": "sha512-qH/uhaC+AFDU9JfAueSr0epIFJkGMvUPog4FxSEVAtPOur1Oni5WBJMiQIkfHvc7PviVRsnlVLLY2I6221CQew==", "dev": true, "requires": { "bs-logger": "0.x", @@ -4298,10 +4316,34 @@ "json5": "2.x", "lodash.memoize": "4.x", "make-error": "1.x", - "mkdirp": "0.x", + "mkdirp": "1.x", "resolve": "1.x", - "semver": "^5.5", - "yargs-parser": "^16.1.0" + "semver": "6.x", + "yargs-parser": "^18.1.1" + }, + "dependencies": { + "mkdirp": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.3.tgz", + "integrity": "sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "yargs-parser": { + "version": "18.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.2.tgz", + "integrity": "sha512-hlIPNR3IzC1YuL1c2UwwDKpXlNFBqD1Fswwh1khz5+d8Cq/8yc/Mn0i+rQXduu8hcrFKvO7Eryk+09NecTQAAQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } } }, "tunnel-agent": { diff --git a/package.json b/package.json index ae1af81..b9f60fe 100644 --- a/package.json +++ b/package.json @@ -7,13 +7,14 @@ "test": "tests" }, "files": [ - "dist", - "global.d.ts" + "dist" ], "scripts": { "build": "npx tsc", + "prepack": "npm run build", "test": "npx jest", - "test:debug": "npx --node-arg=--inspect jest --runInBand" + "test:debug": "npx --node-arg=--inspect jest --runInBand", + "test:ci": "npx jest --ci --collect-coverage" }, "engines": { "node": ">=10.0.0", @@ -30,16 +31,21 @@ }, "homepage": "https://github.com/eduardomourar/cloudformation-cli-typescript-plugin#readme", "dependencies": { - "aws-sdk": "^2.635.0", + "autobind-decorator": "^2.4.0", + "reflect-metadata": "^0.1.13", + "tombok": "https://github.com/eduardomourar/tombok/releases/download/v0.0.1/tombok-0.0.1.tar.gz", "uuid": "^7.0.2" }, "devDependencies": { + "@types/aws-sdk": "^2.7.0", "@types/jest": "^25.1.0", "@types/node": "^12.0.0", "@types/uuid": "^7.0.0", "jest": "^25.1.0", - "tombok": "git+https://github.com/eduardomourar/tombok.git#feature/basic-implementation", - "ts-jest": "^25.2.1", + "ts-jest": "^25.3.0", "typescript": "^3.8.3" + }, + "optionalDependencies": { + "aws-sdk": "^2.656.0" } } diff --git a/python/rpdk/typescript/codegen.py b/python/rpdk/typescript/codegen.py index d97f2c2..29a7844 100644 --- a/python/rpdk/typescript/codegen.py +++ b/python/rpdk/typescript/codegen.py @@ -5,8 +5,6 @@ from subprocess import PIPE, CalledProcessError, run as subprocess_run # nosec from tempfile import TemporaryFile -import docker -from docker.errors import APIError, ContainerError, ImageLoadError from requests.exceptions import ConnectionError as RequestsConnectionError from rpdk.core.data_loaders import resource_stream from rpdk.core.exceptions import DownstreamError, SysExitRecommendedError @@ -21,6 +19,7 @@ EXECUTABLE = "cfn" SUPPORT_LIB_NAME = "cfn-rpdk" +MAIN_HANDLER_FUNCTION = "TypeFunction" class StandardDistNotFoundError(SysExitRecommendedError): @@ -35,9 +34,9 @@ class TypescriptLanguagePlugin(LanguagePlugin): MODULE_NAME = __name__ NAME = "typescript" RUNTIME = "nodejs12.x" - ENTRY_POINT = "handlers.resource" - TEST_ENTRY_POINT = "handlers.testEntrypoint" - CODE_URI = "build/" + ENTRY_POINT = "dist/handlers.entrypoint" + TEST_ENTRY_POINT = "dist/handlers.testEntrypoint" + CODE_URI = "./" def __init__(self): self.env = self._setup_jinja_env( @@ -50,12 +49,14 @@ def __init__(self): self.package_name = None self.package_root = None self._use_docker = True + self._build_command = None def _init_from_project(self, project): self.namespace = tuple(s.lower() for s in project.type_info) self.package_name = "-".join(self.namespace) - self._use_docker = project.settings.get("use_docker", True) + self._use_docker = project.settings.get("useDocker", True) self.package_root = project.root / "src" + self._build_command = project.settings.get("buildCommand", None) def _prompt_for_use_docker(self, project): self._use_docker = input_with_validation( @@ -64,7 +65,7 @@ def _prompt_for_use_docker(self, project): "This is highly recommended unless you are experienced \n" "with cross-platform Typescript packaging.", ) - project.settings["use_docker"] = self._use_docker + project.settings["useDocker"] = self._use_docker def init(self, project): LOG.debug("Init started") @@ -102,6 +103,7 @@ def _copy_resource(path, resource_name=None): # project support files _copy_resource(project.root / ".gitignore", "typescript.gitignore") + _copy_resource(project.root / ".npmrc", ".npmrc") _copy_resource(project.root / "tsconfig.json", "tsconfig.json") _render_template( project.root / "package.json", @@ -124,16 +126,17 @@ def _copy_resource(path, resource_name=None): "Runtime": project.runtime, "CodeUri": self.CODE_URI, } + handler_function = { + "TestEntrypoint": { + **handler_params, + "Handler": project.test_entrypoint, + }, + } + handler_function[MAIN_HANDLER_FUNCTION] = handler_params _render_template( project.root / "template.yml", resource_type=project.type_name, - functions={ - "TypeFunction": handler_params, - "TestEntrypoint": { - **handler_params, - "Handler": project.test_entrypoint, - }, - }, + functions=handler_function, ) LOG.debug("Init complete") @@ -183,9 +186,8 @@ def package(self, project, zip_file): self._remove_build_artifacts(build_path) self._build(project.root) - shutil.copytree(str(handler_package_path), str(build_path / self.package_name)) - inner_zip = self._pre_package(build_path) + inner_zip = self._pre_package(build_path / MAIN_HANDLER_FUNCTION) zip_file.writestr("ResourceProvider.zip", inner_zip.read()) self._recursive_relative_write(handler_package_path, project.root, zip_file) @@ -198,74 +200,33 @@ def _remove_build_artifacts(deps_path): except FileNotFoundError: LOG.debug("'%s' not found, skipping removal", deps_path, exc_info=True) + @staticmethod + def _make_build_command(base_path, build_command=None): + command = f"sam build --build-dir {base_path}/build" + if build_command: + command = build_command + return command + def _build(self, base_path): LOG.debug("Dependencies build started from '%s'", base_path) - if self._use_docker: - self._docker_build(base_path) - else: - self._npm_build(base_path) - LOG.debug("Dependencies build finished") - @staticmethod - def _make_npm_command(base_path): - return [ - "npm", - "install", - "--no-optional", - str(base_path), - ] - - @classmethod - def _docker_build(cls, external_path): - - internal_path = PurePosixPath("/project") - command = " ".join(cls._make_npm_command(internal_path)) - LOG.debug("command is '%s'", command) + # TODO: We should use the build logic from SAM CLI library, instead: + # https://github.com/awslabs/aws-sam-cli/blob/master/samcli/lib/build/app_builder.py + command = self._make_build_command(base_path, self._build_command) + if self._use_docker: + command = command + " --use-container" + command = command + " " + MAIN_HANDLER_FUNCTION - volumes = {str(external_path): {"bind": str(internal_path), "mode": "rw"}} - image = f"lambci/lambda:build-{cls.RUNTIME}" - LOG.warning( - "Starting Docker build. This may take several minutes if the " - "image '%s' needs to be pulled first.", - image, - ) - docker_client = docker.from_env() - try: - logs = docker_client.containers.run( - image=image, - command=command, - auto_remove=True, - volumes=volumes, - stream=True, - ) - except RequestsConnectionError as e: - # it seems quite hard to reliably extract the cause from - # ConnectionError. we replace it with a friendlier error message - # and preserve the cause for debug traceback - cause = RequestsConnectionError( - "Could not connect to docker - is it running?" - ) - cause.__cause__ = e - raise DownstreamError("Error running docker build") from cause - except (ContainerError, ImageLoadError, APIError) as e: - raise DownstreamError("Error running docker build") from e - LOG.debug("Build running. Output:") - for line in logs: - LOG.debug(line.rstrip(b"\n").decode("utf-8")) - - @classmethod - def _npm_build(cls, base_path): - - command = cls._make_npm_command(base_path) LOG.debug("command is '%s'", command) - LOG.warning("Starting npm build.") + LOG.warning("Starting build.") try: completed_proc = subprocess_run( # nosec - command, stdout=PIPE, stderr=PIPE, cwd=base_path, check=True + ["/bin/bash", "-c", command], stdout=PIPE, stderr=PIPE, cwd=base_path, check=True ) except (FileNotFoundError, CalledProcessError) as e: - raise DownstreamError("npm build failed") from e + raise DownstreamError("local build failed") from e - LOG.debug("--- npm stdout:\n%s", completed_proc.stdout) - LOG.debug("--- npm stderr:\n%s", completed_proc.stderr) + LOG.debug("--- build stdout:\n%s", completed_proc.stdout) + LOG.debug("--- build stderr:\n%s", completed_proc.stderr) + LOG.debug("Dependencies build finished") diff --git a/python/rpdk/typescript/data/.npmrc b/python/rpdk/typescript/data/.npmrc new file mode 100644 index 0000000..6ad0abd --- /dev/null +++ b/python/rpdk/typescript/data/.npmrc @@ -0,0 +1 @@ +optional = false diff --git a/python/rpdk/typescript/data/tsconfig.json b/python/rpdk/typescript/data/tsconfig.json index 487225a..f3b0f7c 100644 --- a/python/rpdk/typescript/data/tsconfig.json +++ b/python/rpdk/typescript/data/tsconfig.json @@ -8,7 +8,7 @@ "moduleResolution": "node", "allowJs": true, "experimentalDecorators": true, - "outDir": "build" + "outDir": "dist" }, "include": ["src/**/*.ts"], "exclude": ["node_modules"] diff --git a/python/rpdk/typescript/data/typescript.gitignore b/python/rpdk/typescript/data/typescript.gitignore index 36243a3..171d74d 100644 --- a/python/rpdk/typescript/data/typescript.gitignore +++ b/python/rpdk/typescript/data/typescript.gitignore @@ -2,6 +2,11 @@ build/ dist/ +# Unit test / coverage reports +.cache +.hypothesis/ +.pytest_cache/ + # RPDK logs rpdk.log diff --git a/python/rpdk/typescript/templates/README.md b/python/rpdk/typescript/templates/README.md index 895906b..0914e86 100644 --- a/python/rpdk/typescript/templates/README.md +++ b/python/rpdk/typescript/templates/README.md @@ -2,34 +2,36 @@ Congratulations on starting development! Next steps: -1. Write the JSON schema describing your resource, `{{ schema_path.name }}` -2. Implement your resource handlers in `{{ project_path }}/handlers.ts` +1. Write the JSON schema describing your resource, [{{ schema_path.name }}](./{{ schema_path.name }}) +2. Implement your resource handlers in [handlers.ts](./{{ project_path }}/handlers.ts) -> Don't modify `models.ts` by hand, any modifications will be overwritten when the `generate` or `package` commands are run. +> Don't modify [models.ts](./{{ project_path }}/models.ts) by hand, any modifications will be overwritten when the `generate` or `package` commands are run. Implement CloudFormation resource here. Each function must always return a ProgressEvent. ```typescript -const built = ProgressEvent.builder({ +const progress: ProgressEvent = ProgressEvent.builder() + // Required // Must be one of OperationStatus.InProgress, OperationStatus.Failed, OperationStatus.Success - status: OperationStatus.InProgress, + .status(OperationStatus.InProgress) // Required on SUCCESS (except for LIST where resourceModels is required) // The current resource model after the operation; instance of ResourceModel class - resourceModel: model, - resourceModels: null, + .resourceModel(model) + .resourceModels(null) // Required on FAILED // Customer-facing message, displayed in e.g. CloudFormation stack events - message: '', + .message('') // Required on FAILED a HandlerErrorCode - errorCode: HandlerErrorCode.InternalFailure, + .errorCode(HandlerErrorCode.InternalFailure) // Optional // Use to store any state between re-invocation via IN_PROGRESS - callbackContext: {}, + .callbackContext({}) // Required on IN_PROGRESS // The number of seconds to delay before re-invocation - callbackDelaySeconds: 0, -}).build() + .callbackDelaySeconds(0) + + .build() ``` -Failures can be passed back to CloudFormation by either raising an exception from `{{ lib_name }}.exceptions`, or setting the ProgressEvent's `status` to `OperationStatus.Failed` and `errorCode` to one of `{{ lib_name }}.HandlerErrorCode`. There is a static helper function, `ProgressEvent.failed`, for this common case. +While importing the [{{ lib_name }}](https://github.com/eduardomourar/cloudformation-cli-typescript-plugin) library, failures can be passed back to CloudFormation by either raising an exception from `exceptions`, or setting the ProgressEvent's `status` to `OperationStatus.Failed` and `errorCode` to one of `HandlerErrorCode`. There is a static helper function, `ProgressEvent.failed`, for this common case. diff --git a/python/rpdk/typescript/templates/handlers.ts b/python/rpdk/typescript/templates/handlers.ts index e97ee54..2001b14 100644 --- a/python/rpdk/typescript/templates/handlers.ts +++ b/python/rpdk/typescript/templates/handlers.ts @@ -1,7 +1,8 @@ import { Action, BaseResource, - handlerAction, + exceptions, + handlerEvent, HandlerErrorCode, OperationStatus, Optional, @@ -9,107 +10,108 @@ import { ResourceHandlerRequest, SessionProxy, } from '{{lib_name}}'; -import * as exceptions from '{{lib_name}}/dist/exceptions'; import { ResourceModel } from './models'; + // Use this logger to forward log messages to CloudWatch Logs. -const LOG = console; +const LOGGER = console; -class Resource extends BaseResource { +class Resource extends BaseResource { - @handlerAction(Action.Create) - public create( + @handlerEvent(Action.Create) + public async create( session: Optional, request: ResourceHandlerRequest, callbackContext: Map, - ): ProgressEvent { + ): Promise { const model: ResourceModel = request.desiredResourceState; - // @ts-ignore - const progress: ProgressEvent = ProgressEvent.builder({ - status: OperationStatus.InProgress, - resourceModel: model, - }).build(); + const progress: ProgressEvent = ProgressEvent.builder() + .status(OperationStatus.InProgress) + .resourceModel(model) + .build() as ProgressEvent; // TODO: put code here // Example: try { if (session instanceof SessionProxy) { - const client = session.client('s3'); + const client = session.client('S3'); } // Setting Status to success will signal to cfn that the operation is complete progress.status = OperationStatus.Success; } catch(err) { - LOG.log(err); + LOGGER.log(err); + // exceptions module lets CloudFormation know the type of failure that occurred throw new exceptions.InternalFailure(err.message); + // this can also be done by returning a failed progress event + // return ProgressEvent.failed(HandlerErrorCode.InternalFailure, err.message); } return progress; } - @handlerAction(Action.Update) - public update( + @handlerEvent(Action.Update) + public async update( session: Optional, request: ResourceHandlerRequest, callbackContext: Map, - ): ProgressEvent { + ): Promise { const model: ResourceModel = request.desiredResourceState; - // @ts-ignore - const progress: ProgressEvent = ProgressEvent.builder({ - status: OperationStatus.InProgress, - resourceModel: model, - }).build(); + const progress: ProgressEvent = ProgressEvent.builder() + .status(OperationStatus.InProgress) + .resourceModel(model) + .build() as ProgressEvent; // TODO: put code here + progress.status = OperationStatus.Success; return progress; } - @handlerAction(Action.Delete) - public delete( + @handlerEvent(Action.Delete) + public async delete( session: Optional, request: ResourceHandlerRequest, callbackContext: Map, - ): ProgressEvent { + ): Promise { const model: ResourceModel = request.desiredResourceState; - // @ts-ignore - const progress: ProgressEvent = ProgressEvent.builder({ - status: OperationStatus.InProgress, - resourceModel: model, - }).build(); + const progress: ProgressEvent = ProgressEvent.builder() + .status(OperationStatus.InProgress) + .resourceModel(model) + .build() as ProgressEvent; // TODO: put code here + progress.status = OperationStatus.Success; return progress; } - @handlerAction(Action.Read) - public read( + @handlerEvent(Action.Read) + public async read( session: Optional, request: ResourceHandlerRequest, callbackContext: Map, - ): ProgressEvent { + ): Promise { const model: ResourceModel = request.desiredResourceState; // TODO: put code here - // @ts-ignore - ProgressEvent.progress() - const progress: ProgressEvent = ProgressEvent.builder({ - status: OperationStatus.Success, - resourceModel: model, - }).build(); + const progress: ProgressEvent = ProgressEvent.builder() + .status(OperationStatus.Success) + .resourceModel(model) + .build() as ProgressEvent; return progress; } - @handlerAction(Action.List) - public list( + @handlerEvent(Action.List) + public async list( session: Optional, request: ResourceHandlerRequest, callbackContext: Map, - ): ProgressEvent { + ): Promise { // TODO: put code here - // @ts-ignore - const progress: ProgressEvent = ProgressEvent.builder({ - status: OperationStatus.Success, - resourceModels: [], - }).build(); + const progress: ProgressEvent = ProgressEvent.builder() + .status(OperationStatus.Success) + .resourceModels([]) + .build() as ProgressEvent; return progress; } } -export const resource = new Resource(); +const resource = new Resource(ResourceModel.TYPE_NAME, ResourceModel); + +export const entrypoint = resource.entrypoint; export const testEntrypoint = resource.testEntrypoint; diff --git a/python/rpdk/typescript/templates/models.ts b/python/rpdk/typescript/templates/models.ts index d927f28..6814a8e 100644 --- a/python/rpdk/typescript/templates/models.ts +++ b/python/rpdk/typescript/templates/models.ts @@ -1,15 +1,13 @@ // This is a generated file. Modifications will be overwritten. import { BaseResourceModel, Optional } from '{{lib_name}}'; -import { allArgsConstructor, builder } from 'tombok'; {% for model, properties in models.items() %} -@builder -@allArgsConstructor export class {{ model|uppercase_first_letter }}{% if model == "ResourceModel" %} extends BaseResourceModel{% endif %} { - public static typeName: string = '{{ type_name }}'; + ['constructor']: typeof {{ model|uppercase_first_letter }}; + public static readonly TYPE_NAME: string = '{{ type_name }}'; {% for name, type in properties.items() %} - {{ name|lowercase_first_letter|safe_reserved }}: Optional<{{ type|translate_type }}>; + {{ name|safe_reserved }}: Optional<{{ type|translate_type }}>; {% endfor %} } diff --git a/python/rpdk/typescript/templates/package.json b/python/rpdk/typescript/templates/package.json index bafa24b..95d7b75 100644 --- a/python/rpdk/typescript/templates/package.json +++ b/python/rpdk/typescript/templates/package.json @@ -2,19 +2,23 @@ "name": "{{ name }}", "version": "1.0.0", "description": "{{ description }}", - "main": "index.js", + "main": "dist/handlers.js", + "files": [ + "dist" + ], "scripts": { "build": "npx tsc", - "postinstall": "npm run build", + "prepack": "npm run build", "test": "echo \"Error: no test specified\" && exit 1" }, - "author": "", - "license": "ISC", "dependencies": { - "{{lib_name}}": "git+https://github.com/eduardomourar/cloudformation-cli-typescript-plugin.git" + "{{lib_name}}": "https://github.com/eduardomourar/cloudformation-cli-typescript-plugin/releases/download/v0.0.1/cfn-rpdk-0.0.1.tgz" }, "devDependencies": { - "tombok": "git+https://github.com/eduardomourar/tombok.git#feature/basic-implementation", + "@types/node": "^12.0.0", "typescript": "^3.8.3" + }, + "optionalDependencies": { + "aws-sdk": "^2.656.0" } } diff --git a/tsconfig.json b/tsconfig.json index ce0efcf..3ccc131 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,8 @@ "alwaysStrict": true, "declaration": true, "esModuleInterop": true, - "sourceMap": true, + "removeComments": true, + "sourceMap": false, "experimentalDecorators": true, "outDir": "dist" }, From ffc49dba322305292c4d8ef142684758ad81c724 Mon Sep 17 00:00:00 2001 From: Eduardo Rodrigues Date: Wed, 15 Apr 2020 00:45:07 +0200 Subject: [PATCH 10/15] update log delivery with async calls --- src/log-delivery.ts | 41 ++++++++------ tests/lib/log-delivery.test.ts | 98 +++++++++++++++++++--------------- 2 files changed, 79 insertions(+), 60 deletions(-) diff --git a/src/log-delivery.ts b/src/log-delivery.ts index aa7a81f..cebea9b 100644 --- a/src/log-delivery.ts +++ b/src/log-delivery.ts @@ -1,4 +1,4 @@ -import { boundMethod } from 'autobind-decorator' +import { boundMethod } from 'autobind-decorator'; import { EventEmitter } from 'events'; import CloudWatchLogs, { InputLogEvent, @@ -6,9 +6,7 @@ import CloudWatchLogs, { PutLogEventsResponse, } from 'aws-sdk/clients/cloudwatchlogs'; -import { - SessionProxy, -} from './proxy'; +import { SessionProxy } from './proxy'; import { HandlerRequest } from './utils'; @@ -37,6 +35,7 @@ export class ProviderLogHandler { * construction calls with the `new` operator. */ private constructor(options: ILogOptions) { + this.groupName = options.groupName; this.stream = options.stream.replace(':', '__'); this.client = options.session.client('CloudWatchLogs') as CloudWatchLogs; this.sequenceToken = ''; @@ -44,7 +43,7 @@ export class ProviderLogHandler { // Attach the logger methods to localized event emitter. const emitter = new LogEmitter(); this.emitter = emitter; - emitter.on('log', this.logListener); + this.emitter.on('log', this.logListener); // Create maps of each logger Function and then alias that. Object.entries(this.logger).forEach(([key, val]) => { if (typeof val === 'function') { @@ -105,7 +104,8 @@ export class ProviderLogHandler { logGroupName: this.groupName, }).promise(); } catch(err) { - if (err.code !== 'ResourceAlreadyExistsException') { + const errorCode = err.code || err.name; + if (errorCode !== 'ResourceAlreadyExistsException') { throw err; } } @@ -118,13 +118,14 @@ export class ProviderLogHandler { logStreamName: this.stream, }).promise(); } catch(err) { - if (err.code !== 'ResourceAlreadyExistsException') { + const errorCode = err.code || err.name; + if (errorCode !== 'ResourceAlreadyExistsException') { throw err; } } } - private async putLogEvent(record: InputLogEvent): Promise { + private async putLogEvents(record: InputLogEvent): Promise { if (!record.timestamp) { const currentTime = new Date(Date.now()); record.timestamp = Math.round(currentTime.getTime()); @@ -141,28 +142,34 @@ export class ProviderLogHandler { const response: PutLogEventsResponse = await this.client.putLogEvents(logEventsParams).promise(); this.sequenceToken = response.nextSequenceToken; } catch(err) { - if (err.code === 'DataAlreadyAcceptedException' || err.code === 'InvalidSequenceTokenException') { - this.sequenceToken = (err.message || '').split(' ')[0]; - this.putLogEvent(record); + const errorCode = err.code || err.name; + if (errorCode === 'DataAlreadyAcceptedException' || errorCode === 'InvalidSequenceTokenException') { + this.sequenceToken = (err.message || '').split(' ').pop(); + this.putLogEvents(record); + } else { + throw err; } } } @boundMethod - logListener(...args: any[]): void { + async logListener(...args: any[]): Promise { const currentTime = new Date(Date.now()); const record: InputLogEvent = { message: JSON.stringify(args[0]), timestamp: Math.round(currentTime.getTime()), } try { - this.putLogEvent(record); + await this.putLogEvents(record); } catch(err) { - if (err.message.includes('log group does not exist')) { - this.createLogGroup(); + const errorCode = err.code || err.name; + if (errorCode === 'ResourceNotFoundException') { + if (err.message.includes('log group does not exist')) { + await this.createLogGroup(); + } + await this.createLogStream(); + await this.putLogEvents(record); } - this.createLogStream(); - this.putLogEvent(record); } } } diff --git a/tests/lib/log-delivery.test.ts b/tests/lib/log-delivery.test.ts index df27341..24b5f2e 100644 --- a/tests/lib/log-delivery.test.ts +++ b/tests/lib/log-delivery.test.ts @@ -35,6 +35,9 @@ describe('when delivering log', () => { beforeAll(() => { session = new SessionProxy({}); + }); + + beforeEach(() => { createLogGroup = mockResult({ ResponseMetadata: { RequestId: 'mock-request' }}); createLogStream = mockResult({ ResponseMetadata: { RequestId: 'mock-request' }}); putLogEvents = mockResult({ ResponseMetadata: { RequestId: 'mock-request' }}); @@ -65,16 +68,14 @@ describe('when delivering log', () => { stackId: 'an-arn', }))); ProviderLogHandler.setup(request, session); - // Get a copy of the instance to avoid changing the singleton + // Get a copy of the instance and remove it from class + // to avoid changing the singleton. const instance = ProviderLogHandler.getInstance(); ProviderLogHandler['instance'] = null; cwLogs.mockClear(); return instance; }); providerLogHandler = new Mock(); - }); - - beforeEach(() => { payload = new HandlerRequest(new Map(Object.entries({ action: Action.Create, awsAccountId: '123412341234', @@ -158,36 +159,40 @@ describe('when delivering log', () => { expect(logHandler).toBeNull(); }); - test('log group create success', () => { - providerLogHandler.client.createLogGroup(); + test('log group create success', async () => { + await providerLogHandler['createLogGroup'](); expect(createLogGroup).toHaveBeenCalledTimes(1); }); - test('log stream create success', () => { - providerLogHandler.client.createLogStream(); + test('log stream create success', async () => { + await providerLogHandler['createLogStream'](); expect(createLogStream).toHaveBeenCalledTimes(1); }); test('create already exists', () => { - ['createLogGroup', 'createLogStream'].forEach((methodName: string) => { - const mockLogsMethod: jest.Mock = jest.fn().mockImplementationOnce(() => { - throw awsUtil.error(new Error(), { code: 'ResourceAlreadyExistsException' }); + ['createLogGroup', 'createLogStream'].forEach(async (methodName: string) => { + const mockLogsMethod: jest.Mock = jest.fn().mockReturnValue({ + promise: jest.fn().mockRejectedValueOnce( + awsUtil.error(new Error(), { code: 'ResourceAlreadyExistsException' }) + ) }); providerLogHandler.client[methodName] = mockLogsMethod; // Should not raise an exception if the log group already exists. - providerLogHandler[methodName](); + await providerLogHandler[methodName](); expect(mockLogsMethod).toHaveBeenCalledTimes(1); }); }); test('put log event success', () => { - [null, 'some-seq'].forEach((sequenceToken: string) => { + [null, 'some-seq'].forEach(async (sequenceToken: string) => { providerLogHandler.sequenceToken = sequenceToken; - const mockPut: jest.Mock = jest.fn().mockImplementationOnce(() => { - return { nextSequenceToken: 'some-other-seq' }; + const mockPut: jest.Mock = jest.fn().mockReturnValue({ + promise: jest.fn().mockResolvedValueOnce( + { nextSequenceToken: 'some-other-seq' } + ) }); providerLogHandler.client.putLogEvents = mockPut; - providerLogHandler['putLogEvent']({ + await providerLogHandler['putLogEvents']({ message: 'log-msg', timestamp: 123, }); @@ -195,53 +200,60 @@ describe('when delivering log', () => { }); }); - test('put log event invalid token', () => { - const mockPut: jest.Mock = jest.fn().mockImplementationOnce(() => { - throw awsUtil.error(new Error(), { code: 'InvalidSequenceTokenException' }); - }) - .mockImplementationOnce(() => { - throw awsUtil.error(new Error(), { code: 'DataAlreadyAcceptedException' }); - }) - .mockImplementation(() => { - return { nextSequenceToken: 'some-other-seq' }; + test('put log event invalid token', async () => { + putLogEvents.mockReturnValue({ + promise: jest.fn().mockRejectedValueOnce( + awsUtil.error(new Error(), { code: 'InvalidSequenceTokenException' }) + ).mockRejectedValueOnce( + awsUtil.error(new Error(), { code: 'DataAlreadyAcceptedException' }) + ).mockResolvedValue( + { nextSequenceToken: 'some-other-seq' } + ) }); - providerLogHandler.client.putLogEvents = mockPut; for(let i = 1; i < 4; i++) { - providerLogHandler['putLogEvent']({ + await providerLogHandler['putLogEvents']({ message: 'log-msg', timestamp: i, }); } - expect(mockPut).toHaveBeenCalledTimes(5); + expect(putLogEvents).toHaveBeenCalledTimes(5); }); - test('emit existing cwl group stream', () => { - const mock: jest.Mock = jest.fn(); - providerLogHandler['putLogEvent'] = mock; + test('emit existing cwl group stream', async () => { + const mock: jest.Mock = jest.fn().mockResolvedValue({}); + providerLogHandler['putLogEvents'] = mock; providerLogHandler['emitter'].emit('log', 'log-msg'); + await new Promise(resolve => setTimeout(resolve, 300)); expect(mock).toHaveBeenCalledTimes(1); }); - test('emit no group stream', () => { - const putLogEvent: jest.Mock = jest.fn().mockImplementationOnce(() => { - throw awsUtil.error(new Error(), { message: 'log group does not exist' }); - }); + test('emit no group stream', async () => { + const putLogEvents: jest.Mock = jest.fn().mockResolvedValue({}).mockRejectedValueOnce( + awsUtil.error(new Error(), { + code: 'ResourceNotFoundException', + message: 'log group does not exist', + }) + ); const createLogGroup: jest.Mock = jest.fn(); const createLogStream: jest.Mock = jest.fn(); - providerLogHandler['putLogEvent'] = putLogEvent; + providerLogHandler['putLogEvents'] = putLogEvents; providerLogHandler['createLogGroup'] = createLogGroup; providerLogHandler['createLogStream'] = createLogStream; - providerLogHandler['emitter'].emit('log', 'log-msg'); - expect(putLogEvent).toHaveBeenCalledTimes(2); + await providerLogHandler['logListener']('log-msg'); + expect(putLogEvents).toHaveBeenCalledTimes(2); expect(createLogGroup).toHaveBeenCalledTimes(1); expect(createLogStream).toHaveBeenCalledTimes(1); // Function createGroup should not be called again if the group already exists. - putLogEvent.mockImplementationOnce(() => { - throw awsUtil.error(new Error(), { message: 'log stream does not exist' }); - }); - providerLogHandler['emitter'].emit('log', 'log-msg'); - expect(putLogEvent).toHaveBeenCalledTimes(4); + putLogEvents.mockRejectedValueOnce( + awsUtil.error(new Error(), { + code: 'ResourceNotFoundException', + message: 'log stream does not exist', + }) + ); + console.log('log-msg'); + await new Promise(resolve => setTimeout(resolve, 300)); + expect(putLogEvents).toHaveBeenCalledTimes(4); expect(createLogGroup).toHaveBeenCalledTimes(1); expect(createLogStream).toHaveBeenCalledTimes(2); }); From c1e21cb3624e11b0a415cb3776edf967b76daf98 Mon Sep 17 00:00:00 2001 From: Eduardo Rodrigues Date: Wed, 15 Apr 2020 00:48:10 +0200 Subject: [PATCH 11/15] update metrics with async calls --- src/metrics.ts | 10 +++++----- tests/lib/metrics.test.ts | 23 ++++++++++++----------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/metrics.ts b/src/metrics.ts index 4dc851f..fbce62d 100644 --- a/src/metrics.ts +++ b/src/metrics.ts @@ -25,15 +25,15 @@ export class MetricPublisher { this.client = session.client('CloudWatch') as CloudWatch; } - publishMetric( + async publishMetric( metricName: MetricTypes, dimensions: Map, unit: StandardUnit, value: number, timestamp: Date, - ): void { + ): Promise { try { - this.client.putMetricData({ + const metric = await this.client.putMetricData({ Namespace: this.namespace, MetricData: [{ MetricName: metricName, @@ -42,7 +42,7 @@ export class MetricPublisher { Timestamp: timestamp, Value: value, }], - }); + }).promise(); } catch(err) { LOGGER.error(`An error occurred while publishing metrics: ${err.message}`); } @@ -56,7 +56,7 @@ export class MetricsPublisherProxy { constructor(public accountId: string, public resourceType: string) { this.namespace = MetricsPublisherProxy.makeNamespace(accountId, resourceType); this.resourceType = resourceType; - this.publishers = []; + this.publishers = []; } static makeNamespace(accountId: string, resourceType: string): string { diff --git a/tests/lib/metrics.test.ts b/tests/lib/metrics.test.ts index 946689c..7750751 100644 --- a/tests/lib/metrics.test.ts +++ b/tests/lib/metrics.test.ts @@ -64,21 +64,23 @@ describe('when getting metrics', () => { ]); }); - test('put metric catches error', () => { + test('put metric catches error', async () => { const spyConsoleError: jest.SpyInstance = jest .spyOn(global.console, 'error').mockImplementation(() => {}); - putMetricData.mockImplementationOnce(() => { - throw awsUtil.error(new Error(), { - code: 'InternalServiceError', - message: 'An error occurred (InternalServiceError) when ' - + 'calling the PutMetricData operation: ', - }); + putMetricData.mockReturnValueOnce({ + promise: jest.fn().mockRejectedValueOnce( + awsUtil.error(new Error(), { + code: 'InternalServiceError', + message: 'An error occurred (InternalServiceError) when ' + + 'calling the PutMetricData operation: ', + }) + ) }); const publisher = new MetricPublisher(session, NAMESPACE); const dimensions = new Map(); dimensions.set('DimensionKeyActionType', Action.Create); dimensions.set('DimensionKeyResourceType', RESOURCE_TYPE); - publisher.publishMetric( + await publisher.publishMetric( MetricTypes.HandlerInvocationCount, dimensions, StandardUnit.Count, @@ -115,7 +117,7 @@ describe('when getting metrics', () => { test('publish exception metric', () => { const proxy = new MetricsPublisherProxy(ACCOUNT_ID, RESOURCE_TYPE); proxy.addMetricsPublisher(session); - proxy.publishExceptionMetric( MOCK_DATE, Action.Create, new Error('fake-err')); + proxy.publishExceptionMetric(MOCK_DATE, Action.Create, new Error('fake-err')); expect(putMetricData).toHaveBeenCalledTimes(1); expect(putMetricData).toHaveBeenCalledWith({ MetricData: [{ @@ -145,7 +147,7 @@ describe('when getting metrics', () => { test('publish invocation metric', () => { const proxy = new MetricsPublisherProxy(ACCOUNT_ID, RESOURCE_TYPE); proxy.addMetricsPublisher(session); - proxy.publishInvocationMetric( MOCK_DATE, Action.Create); + proxy.publishInvocationMetric(MOCK_DATE, Action.Create); expect(putMetricData).toHaveBeenCalledTimes(1); expect(putMetricData).toHaveBeenCalledWith({ MetricData: [{ @@ -194,7 +196,6 @@ describe('when getting metrics', () => { }); }); - test('publish log delivery exception metric', () => { const proxy = new MetricsPublisherProxy(ACCOUNT_ID, RESOURCE_TYPE); proxy.addMetricsPublisher(session); From 8bda729aabff29f3b58df1440a2b1299003e83a8 Mon Sep 17 00:00:00 2001 From: Eduardo Rodrigues Date: Wed, 15 Apr 2020 00:49:59 +0200 Subject: [PATCH 12/15] update scheduler with async calls --- src/scheduler.ts | 24 ++++++++++++------------ tests/lib/scheduler.test.ts | 24 ++++++++++++------------ 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/scheduler.ts b/src/scheduler.ts index fd9289a..a1c4cc7 100644 --- a/src/scheduler.ts +++ b/src/scheduler.ts @@ -18,12 +18,12 @@ const LOGGER = console; * @param handlerRequest additional context which the handler can provide itself * for re-invocation */ -export function rescheduleAfterMinutes( +export const rescheduleAfterMinutes = async ( session: SessionProxy, functionArn: string, minutesFromNow: number, handlerRequest: HandlerRequest, -): void { +): Promise => { const client: CloudWatchEvents = session.client('CloudWatchEvents') as CloudWatchEvents; const cron = minToCron(Math.max(minutesFromNow, 1)); const identifier = uuidv4(); @@ -33,19 +33,19 @@ export function rescheduleAfterMinutes( handlerRequest.requestContext.cloudWatchEventsTargetId = targetId; const jsonRequest = JSON.stringify(handlerRequest); LOGGER.debug(`Scheduling re-invoke at ${cron} (${identifier})`); - client.putRule({ + await client.putRule({ Name: ruleName, ScheduleExpression: cron, State: 'ENABLED', - }); - client.putTargets({ + }).promise(); + await client.putTargets({ Rule: ruleName, Targets: [{ Id: targetId, Arn: functionArn, Input: jsonRequest, }], - }); + }).promise(); } /** @@ -56,16 +56,16 @@ export function rescheduleAfterMinutes( * @param ruleName the name of the CWE rule which triggered a re-invocation * @param targetId the target of the CWE rule which triggered a re-invocation */ -export function cleanupCloudwatchEvents( +export const cleanupCloudwatchEvents = async ( session: SessionProxy, ruleName: string, targetId: string -): void { +): Promise => { const client: CloudWatchEvents = session.client('CloudWatchEvents') as CloudWatchEvents; try { if (targetId && ruleName) { - client.removeTargets({ + await client.removeTargets({ Rule: ruleName, Ids: [targetId], - }); + }).promise(); } } catch(err) { LOGGER.error( @@ -75,10 +75,10 @@ export function cleanupCloudwatchEvents( try { if (ruleName) { - client.deleteRule({ + await client.deleteRule({ Name: ruleName, Force: true, - }); + }).promise(); } } catch(err) { LOGGER.error( diff --git a/tests/lib/scheduler.test.ts b/tests/lib/scheduler.test.ts index 11260b9..4bfe0e3 100644 --- a/tests/lib/scheduler.test.ts +++ b/tests/lib/scheduler.test.ts @@ -71,9 +71,9 @@ describe('when getting scheduler', () => { jest.restoreAllMocks(); }); - test('reschedule after minutes zero', () => { + test('reschedule after minutes zero', async () => { // if called with zero, should call cron with a 1 - rescheduleAfterMinutes(session, 'arn:goes:here', 0, handlerRequest); + await rescheduleAfterMinutes(session, 'arn:goes:here', 0, handlerRequest); expect(cwEvents).toHaveBeenCalledTimes(1); expect(cwEvents).toHaveBeenCalledWith('CloudWatchEvents'); @@ -81,9 +81,9 @@ describe('when getting scheduler', () => { expect(spyMinToCron).toHaveBeenCalledWith(1); }); - test('reschedule after minutes not zero', () => { + test('reschedule after minutes not zero', async () => { // if called with another number, should use that - rescheduleAfterMinutes(session, 'arn:goes:here', 2, handlerRequest); + await rescheduleAfterMinutes(session, 'arn:goes:here', 2, handlerRequest); expect(cwEvents).toHaveBeenCalledTimes(1); expect(cwEvents).toHaveBeenCalledWith('CloudWatchEvents'); @@ -91,8 +91,8 @@ describe('when getting scheduler', () => { expect(spyMinToCron).toHaveBeenCalledWith(2); }); - test('reschedule after minutes success', () => { - rescheduleAfterMinutes(session, 'arn:goes:here', 2, handlerRequest); + test('reschedule after minutes success', async () => { + await rescheduleAfterMinutes(session, 'arn:goes:here', 2, handlerRequest); expect(cwEvents).toHaveBeenCalledTimes(1); expect(cwEvents).toHaveBeenCalledWith('CloudWatchEvents'); @@ -115,9 +115,9 @@ describe('when getting scheduler', () => { }); }); - test('cleanup cloudwatch events empty', () => { + test('cleanup cloudwatch events empty', async () => { // cleanup should silently pass if rule/target are empty - cleanupCloudwatchEvents(session, '', ''); + await cleanupCloudwatchEvents(session, '', ''); expect(cwEvents).toHaveBeenCalledTimes(1); expect(cwEvents).toHaveBeenCalledWith('CloudWatchEvents'); @@ -126,10 +126,10 @@ describe('when getting scheduler', () => { expect(spyConsoleError).toHaveBeenCalledTimes(0); }); - test('cleanup cloudwatch events success', () => { + test('cleanup cloudwatch events success', async () => { // when rule_name and target_id are provided we should call events client and not // log errors if the deletion succeeds - cleanupCloudwatchEvents(session, 'rulename', 'targetid'); + await cleanupCloudwatchEvents(session, 'rulename', 'targetid'); expect(spyConsoleError).toHaveBeenCalledTimes(0); expect(cwEvents).toHaveBeenCalledTimes(1); @@ -140,13 +140,13 @@ describe('when getting scheduler', () => { expect(spyConsoleError).toHaveBeenCalledTimes(0); }); - test('cleanup cloudwatch events client error', () => { + test('cleanup cloudwatch events client error', async () => { // cleanup should catch and log client failures const error = awsUtil.error(new Error(), { code: '1' }); mockRemoveTargets.mockImplementation(() => {throw error}); mockDeleteRule.mockImplementation(() => {throw error}); - cleanupCloudwatchEvents(session, 'rulename', 'targetid'); + await cleanupCloudwatchEvents(session, 'rulename', 'targetid'); expect(cwEvents).toHaveBeenCalledTimes(1); expect(cwEvents).toHaveBeenCalledWith('CloudWatchEvents'); From ad4ab12c2ea5ae95e396796d6a8f924c090af930 Mon Sep 17 00:00:00 2001 From: Eduardo Rodrigues Date: Wed, 15 Apr 2020 00:52:57 +0200 Subject: [PATCH 13/15] add success method to progress class --- src/proxy.ts | 116 ++++++++++------------------------------ tests/lib/proxy.test.ts | 57 +++++++++----------- 2 files changed, 52 insertions(+), 121 deletions(-) diff --git a/src/proxy.ts b/src/proxy.ts index 8631175..1c72078 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -1,7 +1,5 @@ -import { AWSError, CredentialProviderChain, Request, Service } from 'aws-sdk'; import { ConfigurationOptions } from 'aws-sdk/lib/config'; -import { Credentials, CredentialsOptions } from 'aws-sdk/lib/credentials'; -import { ServiceConfigurationOptions } from 'aws-sdk/lib/service'; +import { CredentialsOptions } from 'aws-sdk/lib/credentials'; import * as Aws from 'aws-sdk/clients/all'; import { NextToken } from 'aws-sdk/clients/cloudformation'; import { allArgsConstructor, builder, IBuilder} from 'tombok'; @@ -9,59 +7,13 @@ import { allArgsConstructor, builder, IBuilder} from 'tombok'; import { BaseResourceHandlerRequest, BaseResourceModel, - Callable, HandlerErrorCode, - Newable, OperationStatus, } from './interface'; type ClientMap = typeof Aws; type Client = InstanceType; -type ClientType = T[keyof T] extends Service ? never : T[keyof T]; - -// type Async = T extends AsyncGenerator ? AsyncGenerator : T extends Generator ? AsyncGenerator : T extends Promise ? Promise : Promise; - -// type ProxyModule = { -// [K in keyof M]: M[K] extends (...args: infer A) => infer R ? (...args: A) => Async : never; -// }; - -// type Callback = (err: AWSError | undefined, data: D) => void; - -// interface AWSRequestMethod { -// (params: P, callback?: Callback): Request; -// (callback?: Callback): Request; -// } - -// export type CapturedAWSClient = { -// [K in keyof C]: C[K] extends AWSRequestMethod -// ? AWSRequestMethod -// : C[K]; -// }; - -// export type CapturedAWS = { -// [K in keyof T]: T[K] extends AWSClient ? CapturedAWSClient : T[K]; -// }; - -// export function captureAWSClient( -// client: C -// ): CapturedAWSClient; -// export function captureAWS(awssdk: ClientMap): CapturedAWS; - -// type Clients = { [K in keyof AwsClientMap]?: AwsClientMap[K] extends Service ? never : AwsClientMap[K] }; - -class SessionCredentialsProvider { - - private awsSessionCredentials: Credentials; - - public get(): Credentials { - return this.awsSessionCredentials; - } - - public setCredentials(credentials: CredentialsOptions): void { - this.awsSessionCredentials = new Credentials(credentials); - } -} export class SessionProxy { @@ -75,37 +27,7 @@ export class SessionProxy { ...this.options, ...options, }); - return service; //this.promisifyReturn(service); - } - - // private createService(client: Newable, options: ServiceConfigurationOptions): InstanceType { - // // const clients: { [K in keyof ClientMap]?: ClientMap[K] } = Aws; - // // const name: T; - - // return new client(); - // } - - // Wraps an Aws endpoint instance so that you don’t always have to chain `.promise()` onto every function - public promisifyReturn(obj: any): ProxyConstructor { - return new Proxy(obj, { - get(target, propertyKey) { - const property = target[propertyKey]; - - if (typeof property === "function") { - return function (...args: any[]) { - const result = property.apply(this, args); - - if (result instanceof Request) { - return result.promise(); - } else { - return result; - } - } - } else { - return property; - } - }, - }); + return service; } public static getSession(credentials?: CredentialsOptions, region?: string): SessionProxy | null { @@ -152,7 +74,7 @@ export class ProgressEvent { ): Map { // To match Java serialization, which drops `null` values, and the // contract tests currently expect this also. @@ -226,14 +147,33 @@ export class ProgressEvent { const BEARER_TOKEN: string = 'f3390613-b2b5-4c31-a4c6-66813dff96a6'; - @builder - @allArgsConstructor - class ResourceModel extends Map implements BaseResourceModel { - - public static typeName: string = 'Test::Resource::Model'; - + class ResourceModel extends BaseResourceModel { + + public static readonly TYPE_NAME: string = 'Test::Resource::Model'; + public somekey: Optional; public someotherkey: Optional; - - constructor(...args: any[]) {super()} - public static builder(template?: Partial): IBuilder {return null} - - serialize(): Map { - const data: Map = new Map(Object.entries(JSON.parse(JSON.stringify(this)))); - data.forEach((value: any, key: string) => { - if (value == null) { - data.delete(key); - } - }); - return data; - } - deserialize(): ResourceModel {return undefined} - toObject(): Object { - // @ts-ignore - const obj = Object.fromEntries(this.serialize().entries()); - return obj; - } } test('get session returns proxy', () => { @@ -69,14 +48,14 @@ describe('when getting session proxy', () => { status: OperationStatus.Failed, errorCode: errorCode, message, - // callbackDelaySeconds: 0, + callbackDelaySeconds: 0, }))); }); test('progress event serialize to response with context', () => { const message: string = 'message of event with context'; const event = ProgressEvent.builder() - .callbackContext({ "a": "b" }) + .callbackContext({ a: 'b' }) .message(message) .status(OperationStatus.Success) .build(); @@ -91,7 +70,8 @@ describe('when getting session proxy', () => { test('progress event serialize to response with model', () => { const message = 'message of event with model'; const model = new ResourceModel(new Map(Object.entries({ - "somekey": "a", "someotherkey": "b" + somekey: 'a', + someotherkey: 'b', }))); const event = new ProgressEvent(new Map(Object.entries({ status: OperationStatus.Success, @@ -103,17 +83,22 @@ describe('when getting session proxy', () => { operationStatus: OperationStatus.Success, message, bearerToken: BEARER_TOKEN, - resourceModel: {"somekey": "a", "someotherkey": "b"}, + resourceModel: { + somekey: 'a', + someotherkey: 'b', + }, }))); }); test('progress event serialize to response with models', () => { const message = 'message of event with models'; const models = [new ResourceModel(new Map(Object.entries({ - "somekey": "a", "someotherkey": "b" + somekey: 'a', + someotherkey: 'b', }))), new ResourceModel(new Map(Object.entries({ - "somekey": "c", "someotherkey": "d" + somekey: 'c', + someotherkey: 'd', })))]; const event = new ProgressEvent(new Map(Object.entries({ status: OperationStatus.Success, @@ -126,8 +111,14 @@ describe('when getting session proxy', () => { message, bearerToken: BEARER_TOKEN, resourceModels: [ - {"somekey": "a", "someotherkey": "b"}, - {"somekey": "c", "someotherkey": "d"}, + { + somekey: 'a', + someotherkey: 'b', + }, + { + somekey: 'c', + someotherkey: 'd', + }, ], }))); }); From 11659ead977582bdade5bb0fd877e4e031cb0d3c Mon Sep 17 00:00:00 2001 From: Eduardo Rodrigues Date: Wed, 15 Apr 2020 00:56:16 +0200 Subject: [PATCH 14/15] ensure resource serialization on both entrypoints --- src/resource.ts | 82 +++++++++++++++++++++----------------- tests/lib/resource.test.ts | 48 ++++++++++++++-------- 2 files changed, 78 insertions(+), 52 deletions(-) diff --git a/src/resource.ts b/src/resource.ts index b49226c..fa0df56 100644 --- a/src/resource.ts +++ b/src/resource.ts @@ -9,11 +9,12 @@ import { BaseResourceModel, BaseResourceHandlerRequest, Callable, + CfnResponse, Credentials, HandlerErrorCode, OperationStatus, Optional, - Response, + RequestContext, } from './interface'; import { ProviderLogHandler } from './log-delivery'; import { MetricsPublisherProxy } from './metrics'; @@ -37,31 +38,36 @@ class HandlerEvents extends Map {}; /** * Decorates a method to ensure that the JSON input and output are serialized properly. * - * @returns {PropertyDescriptor} + * @returns {MethodDecorator} */ -function ensureSerialize(target: any, propertyKey: string, descriptor: PropertyDescriptor): PropertyDescriptor { - - // Save a reference to the original method this way we keep the values currently in the - // descriptor and don't overwrite what another decorator might have done to the descriptor. - if(descriptor === undefined) { - descriptor = Object.getOwnPropertyDescriptor(target, propertyKey); - } - const originalMethod = descriptor.value; - // Wrapping the original method with new signature. - descriptor.value = async function(event: Object | Map, context: any): Promise { - let mappedEvent: Map; - if (event instanceof Map) { - mappedEvent = new Map(event); - } else { - mappedEvent = new Map(Object.entries(event)); +function ensureSerialize(toResponse: boolean = false): MethodDecorator { + return function(target: any, propertyKey: string, descriptor: PropertyDescriptor): PropertyDescriptor { + type Resource = typeof target; + // Save a reference to the original method this way we keep the values currently in the + // descriptor and don't overwrite what another decorator might have done to the descriptor. + if(descriptor === undefined) { + descriptor = Object.getOwnPropertyDescriptor(target, propertyKey); + } + const originalMethod = descriptor.value; + // Wrapping the original method with new signature. + descriptor.value = async function(event: Object | Map, context: any): Promise> { + let mappedEvent: Map; + if (event instanceof Map) { + mappedEvent = new Map(event); + } else { + mappedEvent = new Map(Object.entries(event)); + } + const progress: ProgressEvent = await originalMethod.apply(this, [mappedEvent, context]); + if (toResponse) { + // Use the raw event data as a last-ditch attempt to call back if the + // request is invalid. + const serialized = progress.serialize(true, mappedEvent.get('bearerToken')); + return serialized.toObject() as CfnResponse; + } + return progress; } - const progress = await originalMethod.apply(this, [mappedEvent, context]); - // Use the raw event data as a last-ditch attempt to call back if the - // request is invalid. - const serialized = progress.serialize(true, mappedEvent.get('bearerToken')); - return serialized.toObject(); + return descriptor; } - return descriptor; } /** @@ -112,16 +118,16 @@ export abstract class BaseResource> = handlerRequest.requestContext; reinvokeContext.invocation = (reinvokeContext.invocation || 0) + 1; const callbackDelaySeconds = handlerResponse.callbackDelaySeconds; const remainingMs = context.getRemainingTimeInMillis(); // When a handler requests a sub-minute callback delay, and if the lambda // invocation has enough runtime (with 20% buffer), we can re-run the handler - // locally otherwise we re-invoke through CloudWatchEvents + // locally otherwise we re-invoke through CloudWatchEvents. const neededMsRemaining = callbackDelaySeconds * 1200 + INVOCATION_TIMEOUT_MS; if (callbackDelaySeconds < 60 && remainingMs > neededMsRemaining) { const delay = async (ms: number) => { @@ -196,15 +202,15 @@ export abstract class BaseResource()]; } // @ts-ignore public async testEntrypoint ( eventData: Object | Map, context: any - ): Promise>; + ): Promise; @boundMethod - @ensureSerialize + @ensureSerialize() public async testEntrypoint( eventData: Map, context: any ): Promise { @@ -217,10 +223,11 @@ export abstract class BaseResource; + callbackContext = event.requestContext?.callbackContext || new Map(); } catch(err) { LOGGER.error('Invalid request'); throw new InvalidRequest(`${err} (${err.name})`); @@ -286,9 +293,9 @@ export abstract class BaseResource, context: LambdaContext - ): Promise>; + ): Promise>; @boundMethod - @ensureSerialize + @ensureSerialize(true) public async entrypoint ( eventData: Map, context: LambdaContext ): Promise { @@ -357,6 +364,9 @@ export abstract class BaseResource>; + } event.requestContext.callbackContext = callback; } if (MUTATING_ACTIONS.includes(event.action)) { diff --git a/tests/lib/resource.test.ts b/tests/lib/resource.test.ts index 3e9a58b..70797b6 100644 --- a/tests/lib/resource.test.ts +++ b/tests/lib/resource.test.ts @@ -7,11 +7,11 @@ import { reportProgress } from '../../src/callback'; import { Action, BaseResourceHandlerRequest, + BaseResourceModel, + CfnResponse, HandlerErrorCode, OperationStatus, RequestContext, - Response, - BaseResourceModel, } from '../../src/interface'; import { ProviderLogHandler } from '../../src/log-delivery'; import { MetricsPublisherProxy } from '../../src/metrics'; @@ -127,7 +127,7 @@ describe('when getting resource', () => { test('entrypoint handler error', async () => { const resource = getResource(); - const event: Response = await resource.entrypoint({}, null); + const event: CfnResponse = await resource.entrypoint({}, null); expect(event.operationStatus).toBe(OperationStatus.Failed); expect(event.errorCode).toBe(HandlerErrorCode.InvalidRequest); }); @@ -138,7 +138,7 @@ describe('when getting resource', () => { const mockHandler: jest.Mock = jest.fn(() => ProgressEvent.success()); const resource = new Resource(TYPE_NAME, MockModel); resource.addHandler(Action.Create, mockHandler); - const event: Response = await resource.entrypoint(entrypointPayload, null); + const event: CfnResponse = await resource.entrypoint(entrypointPayload, null); expect(mockLogDelivery).toBeCalledTimes(1); expect(mockReportProgress).toBeCalledTimes(2); expect(event).toMatchObject({ @@ -163,7 +163,7 @@ describe('when getting resource', () => { mockInvokeHandler.mockImplementation(() => { throw new exceptions.InvalidRequest('handler failed'); }); - const event: Response = await resource.entrypoint(entrypointPayload, null); + const event: CfnResponse = await resource.entrypoint(entrypointPayload, null); expect(mockPublishException).toBeCalledTimes(1); expect(mockInvokeHandler).toBeCalledTimes(1); expect(event).toMatchObject({ @@ -197,6 +197,25 @@ describe('when getting resource', () => { expect(mockHandler).toBeCalledTimes(1); }); + test('entrypoint without context', async () => { + entrypointPayload['requestContext'] = null; + const mockLogDelivery: jest.Mock = (ProviderLogHandler.setup as unknown) as jest.Mock; + const mockReportProgress: jest.Mock = (reportProgress as unknown) as jest.Mock; + const event: ProgressEvent = ProgressEvent.success(null, { 'c': 'd' }); + const mockHandler: jest.Mock = jest.fn(() => event); + const resource = new Resource(TYPE_NAME, MockModel); + resource.addHandler(Action.Create, mockHandler); + const response: CfnResponse = await resource.entrypoint(entrypointPayload, null); + expect(mockLogDelivery).toBeCalledTimes(1); + expect(mockReportProgress).toBeCalledTimes(2); + expect(response).toMatchObject({ + message: '', + bearerToken: '123456', + operationStatus: OperationStatus.Success, + }); + expect(mockHandler).toBeCalledTimes(1); + }); + test('entrypoint success without caller provider creds', async () => { const mockHandler: jest.Mock = jest.fn(() => ProgressEvent.success()); const resource = new Resource(TYPE_NAME, MockModel); @@ -209,7 +228,7 @@ describe('when getting resource', () => { // Credentials are defined in payload, but null. entrypointPayload['requestData']['providerCredentials'] = null; entrypointPayload['requestData']['callerCredentials'] = null; - let response: Response = await resource.entrypoint(entrypointPayload, null); + let response: CfnResponse = await resource.entrypoint(entrypointPayload, null); expect(response).toMatchObject(expected); // Credentials are undefined in payload. @@ -302,7 +321,7 @@ describe('when getting resource', () => { throw new Error('exception'); }); const resource = getResource(); - const event: Response = await resource.entrypoint({}, null); + const event: CfnResponse = await resource.entrypoint({}, null); expect(mockParseRequest).toBeCalledTimes(1); expect(event.operationStatus).toBe(OperationStatus.Failed); expect(event.errorCode).toBe(HandlerErrorCode.InternalFailure); @@ -425,8 +444,8 @@ describe('when getting resource', () => { test('test entrypoint handler error', async () => { const resource = getResource(); - const event: Response = await resource.testEntrypoint({}, null); - expect(event.operationStatus).toBe(OperationStatus.Failed); + const event: ProgressEvent = await resource.testEntrypoint({}, null); + expect(event.status).toBe(OperationStatus.Failed); expect(event.errorCode).toBe(HandlerErrorCode.InternalFailure); }); @@ -436,8 +455,8 @@ describe('when getting resource', () => { mockParseRequest.mockImplementationOnce(() => { throw new Error('exception'); }); - const event: Response = await resource.testEntrypoint({}, null); - expect(event.operationStatus).toBe(OperationStatus.Failed); + const event: ProgressEvent = await resource.testEntrypoint({}, null); + expect(event.status).toBe(OperationStatus.Failed); expect(event.errorCode).toBe(HandlerErrorCode.InternalFailure); expect(event.message).toBe('exception'); }); @@ -465,11 +484,8 @@ describe('when getting resource', () => { logicalResourceIdentifier: null, }, }; - const event: Response = await resource.testEntrypoint(payload, null); - expect(event).toMatchObject({ - message: '', - operationStatus: OperationStatus.InProgress, - }); + const event: ProgressEvent = await resource.testEntrypoint(payload, null); + expect(event).toBe(progressEvent); expect(spyDeserialize).nthCalledWith(1, {state: 'state1'}); expect(spyDeserialize).nthCalledWith(2, {state: 'state2'}); From 9600fe0a17f0b294a327f76fcdfd59752c03270c Mon Sep 17 00:00:00 2001 From: Eduardo Rodrigues Date: Wed, 15 Apr 2020 01:02:18 +0200 Subject: [PATCH 15/15] add builder and serializer to base resource model --- src/callback.ts | 12 ++-- src/interface.ts | 114 +++++++++++++++++++++++------------- src/utils.ts | 27 +++++++++ tests/lib/interface.test.ts | 45 ++++++++++++++ 4 files changed, 151 insertions(+), 47 deletions(-) create mode 100644 tests/lib/interface.test.ts diff --git a/src/callback.ts b/src/callback.ts index 22d9dc8..0d4a0bc 100644 --- a/src/callback.ts +++ b/src/callback.ts @@ -1,15 +1,13 @@ import { v4 as uuidv4 } from 'uuid'; import CloudFormation from 'aws-sdk/clients/cloudformation'; -import { - SessionProxy, -} from './proxy'; -import { BaseResourceModel, OperationStatus, Response } from './interface'; +import { SessionProxy } from './proxy'; +import { BaseResourceModel, CfnResponse, OperationStatus } from './interface'; -const LOG = console; +const LOGGER = console; -interface ProgressOptions extends Response { +interface ProgressOptions extends CfnResponse { session: SessionProxy, currentOperationStatus?: OperationStatus, } @@ -46,6 +44,6 @@ export async function reportProgress(options: ProgressOptions): Promise { if (response['ResponseMetadata']) { requestId = response.ResponseMetadata.RequestId; } - LOG.info(`Record Handler Progress with Request Id ${requestId} and Request: ${request}`); + LOGGER.debug(`Record Handler Progress with Request Id ${requestId} and Request: ${JSON.stringify(request)}`); } } diff --git a/src/interface.ts b/src/interface.ts index 3a9b63e..59edbfd 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -3,6 +3,8 @@ import { LogicalResourceId, NextToken, } from 'aws-sdk/clients/cloudformation'; +import { allArgsConstructor, builder } from 'tombok'; + export type Optional = T | undefined | null; @@ -11,46 +13,46 @@ export interface Callable, T> { } export enum Action { - Create = "CREATE", - Read = "READ", - Update = "UPDATE", - Delete = "DELETE", - List = "LIST", + Create = 'CREATE', + Read = 'READ', + Update = 'UPDATE', + Delete = 'DELETE', + List = 'LIST', } export enum StandardUnit { - Count = "Count", - Milliseconds = "Milliseconds", + Count = 'Count', + Milliseconds = 'Milliseconds', } export enum MetricTypes { - HandlerException = "HandlerException", - HandlerInvocationCount = "HandlerInvocationCount", - HandlerInvocationDuration = "HandlerInvocationDuration", + HandlerException = 'HandlerException', + HandlerInvocationCount = 'HandlerInvocationCount', + HandlerInvocationDuration = 'HandlerInvocationDuration', } export enum OperationStatus { - Pending = "PENDING", - InProgress = "IN_PROGRESS", - Success = "SUCCESS", - Failed = "FAILED", + Pending = 'PENDING', + InProgress = 'IN_PROGRESS', + Success = 'SUCCESS', + Failed = 'FAILED', } export enum HandlerErrorCode { - NotUpdatable = "NotUpdatable", - InvalidRequest = "InvalidRequest", - AccessDenied = "AccessDenied", - InvalidCredentials = "InvalidCredentials", - AlreadyExists = "AlreadyExists", - NotFound = "NotFound", - ResourceConflict = "ResourceConflict", - Throttling = "Throttling", - ServiceLimitExceeded = "ServiceLimitExceeded", - NotStabilized = "NotStabilized", - GeneralServiceException = "GeneralServiceException", - ServiceInternalError = "ServiceInternalError", - NetworkFailure = "NetworkFailure", - InternalFailure = "InternalFailure", + NotUpdatable = 'NotUpdatable', + InvalidRequest = 'InvalidRequest', + AccessDenied = 'AccessDenied', + InvalidCredentials = 'InvalidCredentials', + AlreadyExists = 'AlreadyExists', + NotFound = 'NotFound', + ResourceConflict = 'ResourceConflict', + Throttling = 'Throttling', + ServiceLimitExceeded = 'ServiceLimitExceeded', + NotStabilized = 'NotStabilized', + GeneralServiceException = 'GeneralServiceException', + ServiceInternalError = 'ServiceInternalError', + NetworkFailure = 'NetworkFailure', + InternalFailure = 'InternalFailure', } export interface Credentials { @@ -59,27 +61,59 @@ export interface Credentials { sessionToken: string; } -export interface RequestContext { +export interface RequestContext { invocation: number; - callbackContext: CallbackT; + callbackContext: T; cloudWatchEventsRuleName: string; cloudWatchEventsTargetId: string; } -export interface BaseResourceModel { - serialize(): Map; - deserialize(): BaseResourceModel; +@builder +@allArgsConstructor +export class BaseResourceModel { + ['constructor']: typeof BaseResourceModel; + protected static readonly TYPE_NAME?: string; + + constructor(...args: any[]) {} + public static builder() {} + + public getTypeName(): string { + return Object.getPrototypeOf(this).constructor.TYPE_NAME; + } + + public serialize(): Map { + const data: Map = new Map(Object.entries(this)); + data.forEach((value: any, key: string) => { + if (value == null) { + data.delete(key); + } + }); + return data; + } + + public static deserialize(jsonData: Object): ThisType { + return new this(new Map(Object.entries(jsonData))); + } + + public toObject(): Object { + // @ts-ignore + const obj = Object.fromEntries(this.serialize().entries()); + return obj; + } } -export interface BaseResourceHandlerRequest { - clientRequestToken: ClientRequestToken; - desiredResourceState?: T; - previousResourceState?: T; - logicalResourceIdentifier?: LogicalResourceId; - nextToken?: NextToken; +@allArgsConstructor +export class BaseResourceHandlerRequest { + public clientRequestToken: ClientRequestToken; + public desiredResourceState?: T; + public previousResourceState?: T; + public logicalResourceIdentifier?: LogicalResourceId; + public nextToken?: NextToken; + + constructor(...args: any[]) {} } -export interface Response { +export interface CfnResponse { bearerToken: string; errorCode?: HandlerErrorCode; operationStatus: OperationStatus; diff --git a/src/utils.ts b/src/utils.ts index 6a4bcbc..a5c13a4 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -137,3 +137,30 @@ export interface LambdaContext { invokedFunctionArn: string; getRemainingTimeInMillis(): number; } + +/** + * Returns an ordinary object using the Map's keys as the object's keys and its values as the object's values. + * + * @throws {Error} Since object keys are evaluated as strings (in particular, `{ [myObj]: value }` will have a key named + * `[Object object]`), it's possible that two keys within the Map may evaluate to the same object key. + * In this case, if the associated values are not the same, throws an Error. + */ +Map.prototype.toObject = function(): Object { + let o: any = {}; + for (let [key, value] of this.entries()) { + if (o.hasOwnProperty(key) && o[key] !== value) { + throw new Error(`Duplicate key ${key} found in Map. First value: ${o[key]}, next value: ${value}`); + } + + o[key] = value; + } + + return o; +}; + +/** + * Defines the default JSON representation of a Map to be an array of key-value pairs. + */ +Map.prototype.toJSON = function(this: Map): Array<[K, V]> { + return Array.from(this.entries()); +}; diff --git a/tests/lib/interface.test.ts b/tests/lib/interface.test.ts new file mode 100644 index 0000000..ee2b729 --- /dev/null +++ b/tests/lib/interface.test.ts @@ -0,0 +1,45 @@ +import { + BaseResourceModel, + Optional, +} from '../../src/interface'; + + +describe('when getting interface', () => { + + class ResourceModel extends BaseResourceModel { + ['constructor']: typeof ResourceModel; + public static readonly TYPE_NAME: string = 'Test::Resource::Model'; + + public somekey: Optional; + public someotherkey: Optional; + } + + test('base resource model get type name', () => { + const model = new ResourceModel(); + expect(model.getTypeName()).toBe(model.constructor.TYPE_NAME); + }); + + test('base resource model deserialize', () => { + expect(() => ResourceModel.deserialize(null)).toThrow('Cannot convert undefined or null to object'); + }); + + test('base resource model serialize', () => { + const model = new ResourceModel(new Map(Object.entries({ + somekey: 'a', someotherkey: null + }))); + const serialized = model.serialize(); + expect(serialized.size).toBe(1); + expect(serialized.get('someotherkey')).not.toBeDefined(); + }); + + test('base resource model to object', () => { + const model = new ResourceModel(new Map(Object.entries({ + somekey: 'a', someotherkey: 'b' + }))); + const obj = model.toObject(); + expect(obj).toMatchObject({ + somekey: 'a', + someotherkey: 'b', + }); + }); +});