diff --git a/.babelrc b/.babelrc index de4463d..c52d4a5 100644 --- a/.babelrc +++ b/.babelrc @@ -1,3 +1,16 @@ { - "plugins": ["transform-flow-comments", ["transform-object-rest-spread", { "useBuiltIns": true }]] + "presets": [ + [ + "@babel/preset-env", + { + "targets": { + "node": "current" + } + } + ], + "@babel/preset-flow" + ], + "plugins": [ + "@babel/plugin-transform-flow-comments" + ] } diff --git a/.eslintrc b/.eslintrc index d28c149..d646379 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,34 +1,53 @@ { "parser": "babel-eslint", - "globals": {}, - "env": { - "es6": true, - "node": true + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module", + "ecmaFeatures": { + "jsx": true + } }, - "rules": { - "comma-dangle": [0], - "max-len": [0], - "camelcase": [0], - "quotes": [2, "double"], - "prefer-arrow-callback": [0], - "new-cap": [0], - "no-underscore-dangle": [0], - "arrow-body-style" : [0], - "no-param-reassign" : [0], - "no-console": [0], - "eqeqeq": [1], - "prefer-destructuring": [0], - "class-methods-use-this": [0], - "flowtype-errors/show-errors": 2, - "spaced-comment": ["error", "always", { "markers": [":", "::"] }], - "no-use-before-define": ["error", { "classes": false }] + "settings": { + "ecmascript": 6, + "jsx": true, + "import/extensions": [".js", ".jsx"], + "flowtype": { + "onlyFilesWithFlowAnnotation": true + } }, "extends": [ - "airbnb-base", + "eslint:recommended", + "prettier", + "prettier/flowtype", + "prettier/standard", "plugin:flowtype/recommended" ], "plugins": [ + "prettier", "flowtype", "flowtype-errors" ], + "env": { + "node": true, + "browser": true, + "commonjs": true, + "worker": true, + "mongo": true, + "es6": true + }, + "rules": { + "strict": 0, + "camelcase": [0], + "class-methods-use-this": [0], + "no-underscore-dangle": [0], + "no-console": [1], + "eqeqeq": [1], + "new-cap": [1], + "no-param-reassign": [1], + "no-unused-vars": [1], + "prefer-arrow-callback": [1], + "prefer-destructuring": [1], + "flowtype-errors/show-errors": [2], + "no-use-before-define": ["error", { "classes": false }] + } } diff --git a/.flowconfig b/.flowconfig index 87c9e0f..81fb70c 100644 --- a/.flowconfig +++ b/.flowconfig @@ -12,4 +12,4 @@ flow-typed all=warn [options] -include_warnings=true +include_warnings=false diff --git a/.gitignore b/.gitignore index b940d44..6b916a7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .DS_Store node_modules +.env .env.sh /lib *tgz diff --git a/API.md b/API.md index 9eccc4b..1bba7eb 100644 --- a/API.md +++ b/API.md @@ -4,7 +4,7 @@ HullClient instance constructor - creates new instance to perform API calls, issue traits/track calls and log information -**Parameters** +### Parameters - `config` **[Object][1]** configuration object - `config.id` **[string][2]** Connector ID - required @@ -20,7 +20,7 @@ HullClient instance constructor - creates new instance to perform API calls, iss - `config.logs` **[Array][4]?** an optional array to capture all logs entries, you can provide your own array or use `captureLogs` to initiate empty one - `config.firehoseEvents` **[Array][4]?** an optional array to capture all firehose events, you can provide your own array or use `captureFirehoseEvents` to initiate empty one -**Examples** +### Examples ```javascript const HullClient = require("hull-client"); @@ -35,7 +35,7 @@ const hullClient = new HullClient({ Returns the global configuration object. -**Examples** +#### Examples ```javascript const hullClient = new HullClient({}); @@ -57,7 +57,7 @@ Returns **[Object][1]** current `HullClient` configuration parameters Takes User Claims (link to User Identity docs) and returnes `HullClient` instance scoped to this User. This makes [#traits][5] and [#track][6] methods available. -**Parameters** +#### Parameters - `userClaim` **[Object][1]** - `additionalClaims` **[Object][1]** (optional, default `{}`) @@ -72,10 +72,9 @@ Returns **[UserScopedHullClient][8]** ### asAccount -Takes Account Claims (link to User Identity docs) and returnes `HullClient` instance scoped to this Account. This makes [#traits][5] method available. -**Parameters** +#### Parameters - `accountClaim` **[Object][1]** - `additionalClaims` **[Object][1]** (optional, default `Object.freeze({})`) @@ -93,7 +92,7 @@ The following methods are available when `HullClient` instance is scoped to an u How to get scoped client? Use `asUser` or `asAccount` methods. The `EntityScopedHullClient` is never directly returned by the HullClient but is a base class for UserScopedHullClient and AccountScopedHullClient classes. -**Examples** +### Examples ```javascript const hullClient = new HullClient({ id, secret, organization }); @@ -107,11 +106,11 @@ Used for [Bring your own users][10]. Creates a signed string for the user passed in hash. `userHash` needs an `email` field. [You can then pass this client-side to Hull.js][11] to authenticate users client-side and cross-domain -**Parameters** +#### Parameters - `claims` **[Object][1]** additionalClaims -**Examples** +#### Examples ```javascript hullClient.asUser({ email: "xxx@example.com", external_id: "1234" }).token(optionalClaims); @@ -124,7 +123,7 @@ Returns **[string][2]** token Saves attributes on the user or account. Only available on User or Account scoped `HullClient` instance (see [#asuser][12] and [#asaccount][13]). -**Parameters** +#### Parameters - `traits` **[Object][1]** object with new attributes, it's always flat object, without nested subobjects @@ -137,7 +136,7 @@ Returns **[Promise][14]** The following methods are available when `HullClient` instance is scoped to an user only How to get scoped client? Use `asUser` method -**Examples** +### Examples ```javascript const hullClient = new HullClient({ id, secret, organization }); @@ -150,7 +149,7 @@ scopedHullClient.traits({ new_attribute: "new_value" }); Available only for User scoped `HullClient` instance (see [#asuser][12]). Returns `HullClient` instance scoped to both User and Account, but all traits/track call would be performed on the User, who will be also linked to the Account. -**Parameters** +#### Parameters - `accountClaim` **[Object][1]** [description] (optional, default `Object.freeze({})`) @@ -160,7 +159,7 @@ Returns **[HullClient][15]** HullClient scoped to a User and linked to an Accoun Issues an `alias` event on user? -**Parameters** +#### Parameters - `body` **[Object][1]** @@ -170,7 +169,7 @@ Returns **[Promise][14]** Stores events on user. Only available on User scoped `HullClient` instance (see [#asuser][12]). -**Parameters** +#### Parameters - `event` **[string][2]** event name - `properties` **[Object][1]** additional information about event, this is a one-level JSON object (optional, default `{}`) @@ -190,7 +189,7 @@ Returns **[Promise][14]** This is a class returned when we scope client to account. It provides `token` and `traits` methods. -**Examples** +### Examples ```javascript const hullClient = new HullClient({ id, secret, organization }); @@ -207,7 +206,7 @@ Their are available on base `HullClient` and all scoped classes. Performs a HTTP request on selected url of Hull REST API (prefixed with `prefix` param of the constructor) -**Parameters** +#### Parameters - `url` **[string][2]** - `method` **[string][2]** @@ -220,7 +219,7 @@ Performs a HTTP request on selected url of Hull REST API (prefixed with `prefix` Performs a GET HTTP request on selected url of Hull REST API (prefixed with `prefix` param of the constructor) -**Parameters** +#### Parameters - `url` **[string][2]** - `params` **[Object][1]?** @@ -232,7 +231,7 @@ Performs a GET HTTP request on selected url of Hull REST API (prefixed with `pre Performs a POST HTTP request on selected url of Hull REST API (prefixed with `prefix` param of the constructor -**Parameters** +#### Parameters - `url` **[string][2]** - `params` **[Object][1]?** @@ -244,7 +243,7 @@ Performs a POST HTTP request on selected url of Hull REST API (prefixed with `pr Performs a DELETE HTTP request on selected url of Hull REST API (prefixed with `prefix` param of the constructor) -**Parameters** +#### Parameters - `url` **[string][2]** - `params` **[Object][1]?** @@ -256,7 +255,7 @@ Performs a DELETE HTTP request on selected url of Hull REST API (prefixed with ` Performs a PUT HTTP request on selected url of Hull REST API (prefixed with `prefix` param of the constructor) -**Parameters** +#### Parameters - `url` **[string][2]** - `params` **[Object][1]?** @@ -280,7 +279,7 @@ Updates `private_settings` merging them with existing ones before. Note: this method will trigger `hullClient.put` and will result in `ship:update` notify event coming from Hull platform - possible loop condition. -**Parameters** +#### Parameters - `newSettings` **[Object][1]** settings to update @@ -291,11 +290,11 @@ Returns **[Promise][14]** The Hull API returns traits in a "flat" format, with '/' delimiters in the key. This method can be used to group those traits into subobjects: -**Parameters** +#### Parameters - `user` **[Object][1]** flat object -**Examples** +#### Examples ```javascript hullClient.utils.traits.group({ diff --git a/CHANGELOG.md b/CHANGELOG.md index 07471d6..b2fc613 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,23 @@ # CHANGELOG +## 2.0.0-rc1 +* Removed and deprecated hull.asUser("id") syntax. Use: hull.asUser({ id: id }) +* winston 3 -> https://github.com/winstonjs/winston/blob/master/UPGRADE-3.0.md +* babel 7 +* update dependencies +* add `yarn watch` rask to build continuously. Use with `yarn link hull-client` +* add `debug` - Logs all calls to the REST api when active. +* tweak Flow types +* be more strict with code style, enforce flow, prettier + lint at each commit. +* fix CHANGELOG to be consistent with actual code behaviour for `firehoseEventsArray` + ## 2.0.0-beta.3 * fix missing flow types * documentation adjustments ## 2.0.0-beta.2 -* adds experimental `logsArray` and `firehoseEventsArray` to additionally capture log lines and firehose events to separate arrays. - CAUTION: this does not disable normal behaviour of the libary +* adds experimental `logsArray` and `firehoseEventsArray` to capture log lines and firehose events to separate arrays. + CAUTION: this DOES disable normal behaviour of the libary * fix retry callback errors ## 2.0.0-beta.1 diff --git a/mocha.opts b/mocha.opts new file mode 100644 index 0000000..53cb3d6 --- /dev/null +++ b/mocha.opts @@ -0,0 +1,4 @@ +--compilers js:@babel/register +--exit +--require @babel/register +-R spec diff --git a/package.json b/package.json index 7c37afc..ae2efbf 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,11 @@ "version": "2.0.0", "description": "A barebones Node.js API client for hull.io", "main": "lib", + "author": { + "name": "Hull", + "email": "contact@hull.io", + "url": "https://github.com/hull" + }, "repository": { "type": "git", "url": "https://github.com/hull/hull-client-node.git" @@ -11,54 +16,68 @@ "bugs": { "url": "https://github.com/hull/hull-client-node/issues" }, - "author": "Romain Dardour ", + "engines": { + "node": "^8.11.x", + "yarn": "^1.6.x" + }, "license": "MIT", "scripts": { - "test": "npm run test:lint && npm run test:flow && npm run test:unit && npm run test:integration", - "test:lint": "eslint src && documentation lint src", - "test:dependencies": "npm outdated --depth=0", - "test:unit": "NODE_ENV=test mocha --require babel-register --exit -R spec ./test/unit/** ./test/unit/*", - "test:integration": "NODE_ENV=test mocha --require babel-register --exit -R spec ./test/integration/* ./test/integration/**", - "test:flow": "flow check", + "build": "yarn clean && babel src -d lib && flow-copy-source src lib", "clean": "rimraf lib", - "build": "npm run clean && babel src -d lib && npm run documentation", - "prepublish": "npm run build", + "documentation:lint": "documentation lint src", "documentation": "documentation build src -f md -o API.md --access public --markdownToc=false --config documentation.yml", - "precommit": "npm run documentation && git add API.md" + "flow:stop": "flow stop", + "flow": "flow", + "lint": "eslint src", + "prepublish": "yarn build && yarn documentation", + "test:dependencies": "npm outdated --depth=0", + "test:flow": "flow check", + "test:integration": "NODE_ENV=test mocha --opts mocha.opts ./test/integration/* ./test/integration/**", + "test:unit": "NODE_ENV=test mocha --require @babel/register --exit -R spec ./test/unit/** ./test/unit/*", + "test": "yarn lint && yarn test:flow && yarn test:unit && yarn test:integration", + "watch": "./node_modules/.bin/watch 'yarn build' src", + "precommit": "yarn lint && yarn flow && yarn documentation && git add API.md" }, "dependencies": { + "debug": "^3.1.0", "jwt-simple": "^0.5.0", "lodash": "^4.17.5", "superagent": "^3.8.3", "urijs": "^1.18.7", "uuid": "^3.0.1", - "winston": "^2.2.0" + "winston": "^3.1.0", + "winston-transport": "^4.2.0" }, "devDependencies": { - "babel": "^6.23.0", - "babel-cli": "^6.24.1", - "babel-eslint": "^8.2.2", - "babel-plugin-transform-flow-comments": "^6.22.0", - "babel-plugin-transform-object-rest-spread": "^6.26.0", - "babel-register": "^6.26.0", + "@babel/cli": "^7.0.0", + "@babel/core": "^7.0.0", + "@babel/plugin-transform-flow-comments": "^7.0.0", + "@babel/preset-env": "^7.0.0", + "@babel/preset-flow": "^7.0.0", + "@babel/register": "^7.0.0", + "babel-eslint": "^9.0.0", + "babel-loader": "^8.0.0", "bluebird": "^3.5.1", "chai": "^4.1.2", - "documentation": "^6.1.0", - "eslint": "^4.18.2", - "eslint-config-airbnb-base": "^12.1.0", - "eslint-plugin-flowtype": "^2.46.1", - "eslint-plugin-flowtype-errors": "^3.5.1", - "eslint-plugin-import": "^2.9.0", - "flow-bin": "^0.71.0", + "documentation": "https://github.com/vicapow/documentation#update-babel-dist", + "eslint": "^5.3.0", + "eslint-config-prettier": "^3.0.1", + "eslint-plugin-flowtype": "^2.50.0", + "eslint-plugin-flowtype-errors": "^3.6.0", + "eslint-plugin-import": "^2.13.0", + "eslint-plugin-prettier": "^2.6.2", + "flow-bin": "^0.80.0", + "flow-copy-source": "^2.0.2", "flow-typed": "^2.4.0", - "husky": "^0.14.3", "isparta": "^4.0.0", - "minihull": "0.0.7", + "minihull": "3.0.2", "mkdirp": "^0.5.1", "mocha": "^5.0.4", + "prettier": "^1.14.2", "rimraf": "^2.6.2", - "sinon": "^4.4.5", - "sinon-chai": "^3.0.0" + "sinon": "^6.2.0", + "sinon-chai": "^3.0.0", + "watch": "^1.0.2" }, "nodeBoilerplateOptions": { "mochaGlobals": [ diff --git a/src/client.js b/src/client.js index fffcc4c..fef5098 100644 --- a/src/client.js +++ b/src/client.js @@ -1,17 +1,30 @@ // @flow import type { - HullClientConfiguration, HullEntityAttributes, - HullEventName, HullEventProperties, HullEventContext, - HullAccountClaims, HullUserClaims, HullAuxiliaryClaims, HullEntityClaims, - HullClientLogger, HullClientUtils, HullClientStaticLogger + HullClientConfiguration, + HullEntityAttributes, + HullEventName, + HullEventProperties, + HullEventContext, + HullAccount, + HullUser, + HullAccountClaims, + HullUserClaims, + HullAdditionalClaims, + HullEntityClaims, + HullClientLogger, + HullClientUtils, + HullClientStaticLogger } from "./types"; const _ = require("lodash"); -const winston = require("winston"); + +const { createLogger, format, transports } = require("winston"); + const uuidV4 = require("uuid/v4"); const Configuration = require("./lib/configuration"); +const LogsArrayTransport = require("./lib/logs-array-transport"); const restAPI = require("./lib/rest-api"); const crypto = require("./lib/crypto"); const Firehose = require("./lib/firehose"); @@ -20,14 +33,12 @@ const traitsUtils = require("./utils/traits"); const settingsUtils = require("./utils/settings"); const propertiesUtils = require("./utils/properties"); -const logger = new (winston.Logger)({ - transports: [ - new (winston.transports.Console)({ - level: "info", - json: true, - stringify: true - }) - ] +const consoleTransport = new transports.Console({ + level: process.env.LOG_LEVEL || "info" +}); +const logger = createLogger({ + format: format.json(), + transports: [consoleTransport] }); /** @@ -83,10 +94,16 @@ class HullClient { this.clientConfig = new Configuration(config); const conf = this.configuration() || {}; - const ctxKeys = _.pick(conf, ["organization", "id", "connectorName", "subjectType", "requestId"]); + const ctxKeys = _.pick(conf, [ + "organization", + "id", + "connectorName", + "subjectType", + "requestId" + ]); const ctxe = _.mapKeys(ctxKeys, (value, key) => _.snakeCase(key)); - ["user", "account"].forEach((k) => { + ["user", "account"].forEach(k => { const claim = conf[`${k}Claim`]; if (_.isString(claim)) { ctxe[`${k}_id`] = claim; @@ -103,20 +120,25 @@ class HullClient { if (!Array.isArray(firehoseEventsArray)) { throw new Error("Configuration `firehoseEvents` must be an Array"); } - this.batch = (data) => { + this.batch = data => { firehoseEventsArray.push({ context: ctxe, data }); return Promise.resolve(); }; } else { - this.batch = Firehose.getInstance(this.clientConfig.get(), (params, batcher) => { - const protocol = this.clientConfig._state.protocol || ""; - const domain = this.clientConfig._state.domain || ""; - const firehoseUrl = this.clientConfig._state.firehoseUrl || `${protocol}://firehose.${domain}`; - return restAPI(this, batcher.config, firehoseUrl, "post", params, { - timeout: process.env.BATCH_TIMEOUT || 10000, - retry: process.env.BATCH_RETRY || 5000 - }); - }); + this.batch = Firehose.getInstance( + this.clientConfig.get(), + (params, batcher) => { + const { + protocol = "", + domain = "", + firehoseUrl = `${protocol}://firehose.${domain}` + } = this.clientConfig._state; + return restAPI(this, batcher.config, firehoseUrl, "post", params, { + timeout: process.env.BATCH_TIMEOUT || 10000, + retry: process.env.BATCH_RETRY || 5000 + }); + } + ); } /** @@ -128,14 +150,15 @@ class HullClient { this.utils = { traits: traitsUtils, properties: { - get: propertiesUtils.get.bind(this), + get: propertiesUtils.get.bind(this) }, settings: { - update: settingsUtils.update.bind(this), + update: settingsUtils.update.bind(this) } }; - const logFactory = level => (message: string, data: Object) => logger[level](message, { context: ctxe, data }); + const logFactory = level => (message: string, data: Object) => + logger[level](message, { context: ctxe, data }); this.logger = { log: logFactory("info"), silly: logFactory("silly"), @@ -143,9 +166,11 @@ class HullClient { verbose: logFactory("verbose"), info: logFactory("info"), warn: logFactory("warn"), - error: logFactory("error"), + error: logFactory("error") }; + consoleTransport.level = config.logLevel || "info"; + this.requestId = conf.requestId; if (this.clientConfig.get("logs")) { @@ -153,43 +178,43 @@ class HullClient { if (!Array.isArray(logsArray)) { throw new Error("Configuration `logs` must be an Array"); } - if (logger.transports.console) { - logger.remove("console"); - logger.add(winston.transports.Memory, { - level: "debug", - json: true, - stringify: input => input - }); - logger.on("logged", (level, message, payload) => { - logsArray.push({ - message, - level, - data: payload.data, - context: payload.context, - timestamp: new Date().toISOString() - }); + const foundConsoleTransport = logger.transports.find( + t => t.name === "console" + ); + if (foundConsoleTransport) { + logger.remove(foundConsoleTransport); + } + + const foundLogsArrayTransport = logger.transports.find( + t => t.name === "logsArray" + ); + if (!foundLogsArrayTransport) { + const logsArrayTransport = new LogsArrayTransport({ + logsArray, + level: "debug" }); + logger.add(logsArrayTransport); } } } /** - * Returns the global configuration object. - * - * @public - * @return {Object} current `HullClient` configuration parameters - * @example - * const hullClient = new HullClient({}); - * hullClient.configuration() == { - * prefix: "/api/v1", - * domain: "hullapp.io", - * protocol: "https", - * id: "58765f7de3aa14001999", - * secret: "12347asc855041674dc961af50fc1", - * organization: "fa4321.hullapp.io", - * version: "0.13.10" - * }; - */ + * Returns the global configuration object. + * + * @public + * @return {Object} current `HullClient` configuration parameters + * @example + * const hullClient = new HullClient({}); + * hullClient.configuration() == { + * prefix: "/api/v1", + * domain: "hullapp.io", + * protocol: "https", + * id: "58765f7de3aa14001999", + * secret: "12347asc855041674dc961af50fc1", + * organization: "fa4321.hullapp.io", + * version: "0.13.10" + * }; + */ configuration(): HullClientConfiguration { return this.clientConfig.getAll(); } @@ -279,17 +304,23 @@ class HullClient { * @throws {Error} if no valid claims are passed * @return {UserScopedHullClient} */ - asUser(userClaim: string | HullUserClaims, additionalClaims: HullAuxiliaryClaims = Object.freeze({})) { + asUser( + userClaim: HullUserClaims | HullUser, + additionalClaims: HullAdditionalClaims = Object.freeze({}) + //eslint-disable-next-line no-use-before-define + ): UserScopedHullClient { if (!userClaim) { throw new Error("User Claims was not defined when calling hull.asUser()"); } return new UserScopedHullClient({ - ...this.config, subjectType: "user", userClaim, additionalClaims + ...this.config, + subjectType: "user", + userClaim: { ...userClaim }, //Fixes flow Error https://flow.org/try/#0C4TwDgpgBAglC8UDeUCWATAXFAzsATqgHYDmUAvgNwBQokUAQgsmugPzZ6GkU0DGAeyJ4oAMwEDscRCgzYARAEYATAGYALPIrVBw4GOYAKAEaYGASgQA+KLpwCANhAB0DgSRPOM5mqMPiBHyggA + additionalClaims }); } /** - * Takes Account Claims (link to User Identity docs) and returnes `HullClient` instance scoped to this Account. * This makes {@link #traits} method available. * * @public @@ -298,12 +329,22 @@ class HullClient { * @throws {Error} If no valid claims are passed * @return {AccountScopedHullClient} instance scoped to account claims */ - asAccount(accountClaim: string | HullAccountClaims, additionalClaims: HullAuxiliaryClaims = Object.freeze({})) { + asAccount( + accountClaim: HullAccountClaims | HullAccount, + additionalClaims: HullAdditionalClaims = Object.freeze({}) + //eslint-disable-next-line no-use-before-define + ): AccountScopedHullClient { if (!accountClaim) { - throw new Error("Account Claims was not defined when calling hull.asAccount()"); + throw new Error( + "Account Claims was not defined when calling hull.asAccount()" + ); } + const claim = _.isString(accountClaim) ? accountClaim : { ...accountClaim }; return new AccountScopedHullClient({ - ...this.config, subjectType: "account", accountClaim, additionalClaims + ...this.config, + subjectType: "account", + accountClaim: { ...claim }, //Fixes flow Error https://flow.org/try/#0C4TwDgpgBAglC8UDeUCWATAXFAzsATqgHYDmUAvgNwBQokUAQgsmugPzZ6GkU0DGAeyJ4oAMwEDscRCgzYARAEYATAGYALPIrVBw4GOYAKAEaYGASgQA+KLpwCANhAB0DgSRPOM5mqMPiBHyggA + additionalClaims }); } } @@ -334,11 +375,17 @@ class EntityScopedHullClient extends HullClient { */ token(claims: HullEntityClaims) { const subjectType = this.clientConfig._state.subjectType || ""; - const claim = subjectType === "account" - ? this.clientConfig._state.accountClaim - : this.clientConfig._state.userClaim; + const claim = + subjectType === "account" + ? this.clientConfig._state.accountClaim + : this.clientConfig._state.userClaim; - return crypto.lookupToken(this.clientConfig.get(), subjectType, { [subjectType]: claim }, claims); + return crypto.lookupToken( + this.clientConfig.get(), + subjectType, + { [subjectType]: claim }, + claims + ); } /** @@ -373,8 +420,15 @@ class UserScopedHullClient extends EntityScopedHullClient { * @param {Object} accountClaim [description] * @return {HullClient} HullClient scoped to a User and linked to an Account */ - account(accountClaim: HullAccountClaims = Object.freeze({})) { - return new AccountScopedHullClient({ ...this.config, subjectType: "account", accountClaim }); + account( + accountClaim: HullAccountClaims = Object.freeze({}) + //eslint-disable-next-line no-use-before-define + ): AccountScopedHullClient { + return new AccountScopedHullClient({ + ...this.config, + subjectType: "account", + accountClaim + }); } /** @@ -407,7 +461,11 @@ class UserScopedHullClient extends EntityScopedHullClient { * @param {string} [context.referer] Define the Referer. `null` for server calls. * @return {Promise} */ - track(event: HullEventName, properties: HullEventProperties = {}, context: HullEventContext = {}): Promise<*> { + track( + event: HullEventName, + properties: HullEventProperties = {}, + context: HullEventContext = {} + ): Promise<*> { _.defaults(context, { event_id: uuidV4() }); diff --git a/src/index.js b/src/index.js index e12d6a8..d55497a 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,6 @@ // @flow -/*:: export type * from "./types"; */ -module.exports = require("./client"); +export type * from "./types"; +const Client = require("./client"); +export type HullClient = Client; +module.exports = Client; diff --git a/src/lib/configuration.js b/src/lib/configuration.js index efd4889..5be0081 100644 --- a/src/lib/configuration.js +++ b/src/lib/configuration.js @@ -1,5 +1,10 @@ // @flow -import type { HullClientConfiguration, HullEntityClaims, HullEntityType, HullAuxiliaryClaims } from "../types"; +import type { + HullClientConfiguration, + HullEntityClaims, + HullEntityType, + HullAdditionalClaims, +} from "../types"; const _ = require("lodash"); const pkg = require("../../package.json"); @@ -7,13 +12,13 @@ const crypto = require("./crypto"); const GLOBALS = { prefix: "/api/v1", - protocol: "https" + protocol: "https", }; const VALID_OBJECT_ID = new RegExp("^[0-9a-fA-F]{24}$"); const VALID = { boolean(val) { - return (val === true || val === false); + return val === true || val === false; }, object(val) { return _.isObject(val); @@ -29,13 +34,13 @@ const VALID = { }, array(arr) { return _.isArray(arr); - } + }, }; const REQUIRED_PROPS = { id: VALID.objectId, secret: VALID.string, - organization: VALID.string + organization: VALID.string, }; const VALID_PROPS = { @@ -55,20 +60,30 @@ const VALID_PROPS = { connectorName: VALID.string, requestId: VALID.string, logs: VALID.array, - firehoseEvents: VALID.array + firehoseEvents: VALID.array, }; /** * All valid user claims, used for validation and filterind .asUser calls * @type {Array} */ -const USER_CLAIMS: Array = ["id", "email", "external_id", "anonymous_id"]; +const USER_CLAIMS: Array = [ + "id", + "email", + "external_id", + "anonymous_id", +]; /** * All valid accounts claims, used for validation and filtering .asAccount calls * @type {Array} */ -const ACCOUNT_CLAIMS: Array = ["id", "external_id", "domain", "anonymous_id"]; +const ACCOUNT_CLAIMS: Array = [ + "id", + "external_id", + "domain", + "anonymous_id", +]; /** * Class containing configuration @@ -77,7 +92,9 @@ class Configuration { _state: HullClientConfiguration; constructor(config: HullClientConfiguration) { if (!_.isObject(config) || !_.size(config)) { - throw new Error("Configuration is invalid, it should be a non-empty object"); + throw new Error( + "Configuration is invalid, it should be a non-empty object" + ); } if (config.userClaim !== undefined || config.accountClaim !== undefined) { @@ -89,13 +106,22 @@ class Configuration { } if (config.accountClaim) { - config.accountClaim = this.filterEntityClaims("account", config.accountClaim); + config.accountClaim = this.filterEntityClaims( + "account", + config.accountClaim + ); } - const accessToken = crypto.lookupToken(config, config.subjectType, { - user: config.userClaim, - account: config.accountClaim - }, config.additionalClaims); + const accessToken = crypto.lookupToken( + config, + config.subjectType, + { + user: config.userClaim, + account: config.accountClaim, + }, + config.additionalClaims + ); + //eslint-disable-next-line no-param-reassign config = { ...config, accessToken }; } @@ -106,7 +132,9 @@ class Configuration { throw new Error(`Configuration is missing required property: ${prop}`); } if (!test(config[prop])) { - throw new Error(`${prop} property in Configuration is invalid: ${config[prop]}`); + throw new Error( + `${prop} property in Configuration is invalid: ${config[prop]}` + ); } }); @@ -132,35 +160,56 @@ class Configuration { * claim is an object * @throws Error */ - assertEntityClaimsValidity(type: HullEntityType, object: void | string | HullEntityClaims): void { - const claimsToCheck = type === "user" - ? USER_CLAIMS - : ACCOUNT_CLAIMS; + assertEntityClaimsValidity( + type: HullEntityType, + object: void | string | HullEntityClaims + ): void { + const claimsToCheck = type === "user" ? USER_CLAIMS : ACCOUNT_CLAIMS; if (!_.isEmpty(object)) { if (typeof object === "string") { if (!object) { throw new Error(`Missing ${type} ID`); } - } else if (typeof object !== "object" || _.intersection(_.keys(object), claimsToCheck).length === 0) { - throw new Error(`You need to pass an ${type} hash with an ${claimsToCheck.join(", ")} field`); + } else if ( + typeof object !== "object" || + _.intersection(_.keys(object), claimsToCheck).length === 0 + ) { + throw new Error( + `You need to pass a ${type} hash with an ${claimsToCheck.join( + ", " + )} field` + ); } } } - filterEntityClaims(type: HullEntityType, object: void | string | HullEntityClaims): * { - const claimsToFilter = type === "user" - ? USER_CLAIMS - : ACCOUNT_CLAIMS; + filterEntityClaims( + type: HullEntityType, + object: void | string | HullEntityClaims + ): * { + const claimsToFilter = type === "user" ? USER_CLAIMS : ACCOUNT_CLAIMS; return typeof object === "string" ? object - : _.pick(object, claimsToFilter); + : _.mapValues( //Ensure we don't return Arrays in the various claims, just primitives. + _.pick(object, claimsToFilter), + v => (_.isArray(v) ? _.first(v) : v) + ); } set(key: string, value: $Values): void { this._state[key] = value; } - get(key?: string): string | number | Array | HullEntityType | HullEntityClaims | HullAuxiliaryClaims | HullClientConfiguration | void { + get( + key?: string + ): | string + | number + | Array + | HullEntityType + | HullEntityClaims + | HullAdditionalClaims + | HullClientConfiguration + | void { if (key) { return this._state[key]; } diff --git a/src/lib/crypto.js b/src/lib/crypto.js index d60bfb1..3f12076 100644 --- a/src/lib/crypto.js +++ b/src/lib/crypto.js @@ -2,7 +2,6 @@ const _ = require("lodash"); const crypto = require("crypto"); const jwt = require("jwt-simple"); - function getSecret(config = {}, secret) { return secret || config.accessToken || config.secret; } @@ -11,10 +10,7 @@ function sign(config, data) { throw new Error("Signatures can only be generated for Strings"); } const sha1 = getSecret(config); - return crypto - .createHmac("sha1", sha1) - .update(data) - .digest("hex"); + return crypto.createHmac("sha1", sha1).update(data).digest("hex"); } function checkConfig(config) { if (!config || !_.isObject(config) || !config.id || !config.secret) { @@ -23,8 +19,12 @@ function checkConfig(config) { } function buildToken(config, claims = {}) { - if (claims.nbf) { claims.nbf = Number(claims.nbf); } - if (claims.exp) { claims.exp = Number(claims.exp); } + if (claims.nbf) { + claims.nbf = Number(claims.nbf); + } + if (claims.exp) { + claims.exp = Number(claims.exp); + } const iat = Math.floor(new Date().getTime() / 1000); const claim = { iss: config.id, @@ -57,15 +57,15 @@ module.exports = { * @returns {string} The jwt token to identity the user. */ lookupToken(config, subjectType, objectClaims = {}, additionalClaims = {}) { - subjectType = _.toLower(subjectType); - if (!_.includes(["user", "account"], subjectType)) { + const subject = _.toLower(subjectType); + if (!_.includes(["user", "account"], subject)) { throw new Error("Lookup token supports only `user` and `account` types"); } checkConfig(config); const claims = {}; - const subjectClaim = objectClaims[subjectType]; + const subjectClaim = objectClaims[subject]; if (_.isString(subjectClaim)) { claims.sub = subjectClaim; @@ -73,14 +73,22 @@ module.exports = { claims.sub = subjectClaim.id; } - _.reduce(objectClaims, (c, oClaims, objectType) => { - if (_.isObject(oClaims) && !_.isEmpty(oClaims)) { - c[`io.hull.as${_.upperFirst(objectType)}`] = oClaims; - } else if (_.isString(oClaims) && !_.isEmpty(oClaims) && objectType !== subjectType) { - c[`io.hull.as${_.upperFirst(objectType)}`] = { id: oClaims }; - } - return c; - }, claims); + _.reduce( + objectClaims, + (c, oClaims, objectType) => { + if (_.isObject(oClaims) && !_.isEmpty(oClaims)) { + c[`io.hull.as${_.upperFirst(objectType)}`] = oClaims; + } else if ( + _.isString(oClaims) && + !_.isEmpty(oClaims) && + objectType !== subject + ) { + c[`io.hull.as${_.upperFirst(objectType)}`] = { id: oClaims }; + } + return c; + }, + claims + ); if (_.has(additionalClaims, "scopes")) { claims.scopes = additionalClaims.scopes; @@ -94,7 +102,7 @@ module.exports = { claims["io.hull.active"] = additionalClaims.active; } - claims["io.hull.subjectType"] = subjectType; + claims["io.hull.subjectType"] = subject; return buildToken(config, claims); }, @@ -107,7 +115,9 @@ module.exports = { */ currentUserId(config, userId, userSig) { checkConfig(config); - if (!userId || !userSig) { return false; } + if (!userId || !userSig) { + return false; + } const [time, signature] = userSig.split("."); const data = [time, userId].join("-"); return sign(config, data) === signature; diff --git a/src/lib/firehose.js b/src/lib/firehose.js index 5d029d7..de63fe5 100644 --- a/src/lib/firehose.js +++ b/src/lib/firehose.js @@ -5,7 +5,6 @@ const BATCHERS = {}; global.setImmediate = global.setImmediate || process.nextTick.bind(process); - class FirehoseBatcher { static getInstance(config, handler) { const { @@ -44,13 +43,12 @@ class FirehoseBatcher { if (this.queue.length >= this.flushAt) this.flush(); if (this.timer) clearTimeout(this.timer); - if (this.flushAfter) this.timer = setTimeout(this.flush.bind(this), this.flushAfter); + if (this.flushAfter) { this.timer = setTimeout(this.flush.bind(this), this.flushAfter); } return true; }); } - flush(fn) { - fn = fn || (() => {}); + flush(fn = (() => {})) { if (!this.queue.length) return setImmediate(fn); const items = this.queue.splice(0, this.flushAt); @@ -77,5 +75,4 @@ function handleBeforeExit() { process.on("beforeExit", handleBeforeExit); - module.exports = FirehoseBatcher; diff --git a/src/lib/logs-array-transport.js b/src/lib/logs-array-transport.js new file mode 100644 index 0000000..669830d --- /dev/null +++ b/src/lib/logs-array-transport.js @@ -0,0 +1,38 @@ +// @flow +const Transport = require("winston-transport"); + +// +// Inherit from `winston-transport` so you can take advantage +// of the base functionality and `.exceptions.handle()`. +// + +type LogsArrayOpts = { + logsArray: Array +}; +type LogMessage = { + context: {}, + data: {}, + level: string, + message: string +}; + +class LogsArrayTransport extends Transport { + constructor(opts: LogsArrayOpts) { + super(opts); + this.name = "logsArray" + this.logsArray = opts.logsArray; + } + + log({ context, data, level, message }: LogMessage, callback: () => void) { + this.logsArray.push({ + message, + level, + data, + context, + timestamp: new Date().toISOString() + }); + callback(); + } +} + +module.exports = LogsArrayTransport; diff --git a/src/lib/rest-api.js b/src/lib/rest-api.js index ddf5f38..01feffe 100644 --- a/src/lib/rest-api.js +++ b/src/lib/rest-api.js @@ -1,14 +1,18 @@ // const rest = require("restler"); const superagent = require("superagent"); +const jwt = require("jwt-simple"); +const debug = require("debug")("hull-client-node:rest-api"); const pkg = require("../../package.json"); const DEFAULT_HEADERS = { "Content-Type": "application/json", - "User-Agent": `Hull Node Client version: ${pkg.version}` + "User-Agent": `Hull Node Client version: ${pkg.version}`, }; function strip(url = "") { - if (url.indexOf("/") === 0) { return url.slice(1); } + if (url.indexOf("/") === 0) { + return url.slice(1); + } return url; } @@ -16,7 +20,14 @@ function isAbsolute(url = "") { return /http[s]?:\/\//.test(url); } -function perform(client, config = {}, method = "get", path, params = {}, options = {}) { +function perform( + client, + config = {}, + method = "get", + path, + params = {}, + options = {} +) { const methodCall = superagent[method]; if (!methodCall) { throw new Error(`Unsupported method ${method}`); @@ -28,25 +39,31 @@ function perform(client, config = {}, method = "get", path, params = {}, options "Hull-App-Id": config.id, "Hull-Access-Token": config.token, "Hull-Organization": config.organization, - ...(params.headers || {}) + ...(params.headers || {}), }) .retry(2, function retryCallback(err, res) { const retryCount = this._retries; if (err && err.timeout) { client.logger.debug("client.timeout", { - timeout: err.timeout, retryCount, path, method + timeout: err.timeout, + retryCount, + path, + method, }); return true; } if (res && res.statusCode >= 500 && retryCount <= 2) { client.logger.debug("client.fail", { - statusCode: res.statusCode, retryCount, path, method + statusCode: res.statusCode, + retryCount, + path, + method, }); return true; } if (err) { client.logger.debug("client.fail.unknown", { - err: err.toString() + err: err.toString(), }); } return false; @@ -64,6 +81,23 @@ function perform(client, config = {}, method = "get", path, params = {}, options agent.timeout(options.timeout || 10000); } + if ( + params.batch && + process.env.NODE_ENV === "development" && + process.env.DEBUG + ) { + debug("perform:"); + params.batch.forEach(b => { + const { type, body } = b; + const { + iss, // eslint-disable-line no-unused-vars + iat, // eslint-disable-line no-unused-vars + ...claims + } = jwt.decode(b.headers["Hull-Access-Token"], config.secret); + debug("%j", { type, body, claims }); + }); + } + if (method === "get") { return agent.query(params).then(res => res.body); } @@ -71,18 +105,31 @@ function perform(client, config = {}, method = "get", path, params = {}, options } function format(config, url) { - if (isAbsolute(url)) { return url; } - return `${config.get("protocol")}://${config.get("organization")}${config.get("prefix")}/${strip(url)}`; + if (isAbsolute(url)) { + return url; + } + return `${config.get("protocol")}://${config.get("organization")}${config.get( + "prefix" + )}/${strip(url)}`; } -module.exports = function restAPI(client, config, url, method, params, options = {}) { - const token = config.get("sudo") ? config.get("secret") : (config.get("accessToken") || config.get("secret")); +module.exports = function restAPI( + client, + config, + url, + method, + params, + options = {} +) { + const token = config.get("sudo") + ? config.get("secret") + : config.get("accessToken") || config.get("secret"); const conf = { token, id: config.get("id"), secret: config.get("secret"), userId: config.get("userId"), - organization: config.get("organization") + organization: config.get("organization"), }; const path = format(config, url); diff --git a/src/types.js b/src/types.js index d9b2e2a..c2d2b5a 100644 --- a/src/types.js +++ b/src/types.js @@ -36,26 +36,124 @@ export type HullConnectorSettings = { [HullConnectorSettingName: string]: any }; +type HullManifestSetting = { + [string]: any +}; + +export type HullNotificationHandlerOptions = { + disableErrorHandling?: boolean, + maxTime?: number, + maxSize?: number, +} +type HullNotificationHandlerChannel = { + handler: string, + options: HullNotificationHandlerOptions +}; + +/** + * A Manifest Notification block. Defines a route for Hull to send Notifications to. + */ +type HullManifestNotification = { + url: string, + channels: { + [channelName: string]: HullNotificationHandlerChannel + } +}; + +/** + * A Manifest Batch block. Defines a route for Hull to send Notifications to. + */ +type HullManifestBatch = { + url: string, + channels: { + [channelName: string]: HullNotificationHandlerChannel + } +}; + +/** + * A Manifest Schedule block. Defines a schedule for Hull to ping the connector + */ +export type HullSchedulerHandlerOptions = { + disableErrorHandling?: boolean + }; +type HullManifestSchedule = { + url: string, + handler: string, + interval: string, + options?: HullSchedulerHandlerOptions +}; + +/** + * A Manifest Endpoint block. Defines a publicly-available route + * for the Connector to receive Service data + */ +export type HullExternalHandlerOptions = { + respondWithError?: boolean, + disableErrorHandling?: boolean, + [string]: any +} +type HullManifestEndpoint = { + url: string, + handler: string, + method: "get" | "post" | "put" | "patch" | "delete", + options?: HullExternalHandlerOptions +}; + +/** + * A Manifest Tab config. Defines a route to display a screen in the Dashboard + */ +type HullManifestTabConfig = { + title: string, + url: string, + handler: string, + options?: HullExternalHandlerOptions, + size: "small" | "large", + editable: boolean +}; + +/** + * Manifest. Defines a Connector's exposed endpoints and features + */ +export type HullManifest = { + name: string, + description: string, + tags: Array<"batch" | "batch-accounts" | "kraken">, + source: string, + logo: string, + picture: string, + readme: string, + version: string, + tabs: Array, + schedules?: Array, + status?: Array, + subscriptions?: Array, + batch?: Array, + endpoints?: Array, + deployment_settings: Array, + settings?: Array, + private_settings?: Array +}; + /** * Connector (also called ship) object with settings, private settings and manifest.json * Used for both read and write operations */ export type HullConnector = { - id: string; - updated_at: string; - created_at: string; - name: string; - description: string; - tags: Array; - source_url: string; - index: string; - picture: string; - homepage_url: string; - manifest_url: string; - manifest: Object; - settings: HullConnectorSettings; - private_settings: HullConnectorSettings; - status: Object; + id: string, + updated_at: string, + created_at: string, + name: string, + description: string, + tags: Array, + source_url: string, + index: string, + picture: string, + homepage_url: string, + manifest_url: string, + manifest: HullManifest, + settings: HullConnectorSettings, + private_settings: HullConnectorSettings, + status: Object }; export type HullSegmentType = "users_segment" | "accounts_segment"; @@ -65,15 +163,15 @@ export type HullSegmentType = "users_segment" | "accounts_segment"; * Used in read operations */ export type HullSegment = { - id: string; - name: string; - type: HullSegmentType; + id: string, + name: string, + type: HullSegmentType, stats: { users?: number, accounts?: number // is it really there? - }; - created_at: string; - updated_at: string; + }, + created_at: string, + updated_at: string }; /* @@ -83,21 +181,22 @@ export type HullSegment = { /** * This are claims we can use to identify account */ -export type HullAccountClaims = {| - id?: string; - domain?: string; - external_id?: string; -|}; +export type HullAccountClaims = { + id?: ?string, + domain?: ?string, + external_id?: ?string, + anonymous_id?: ?string +}; /** * This are claims we can use to identify user */ -export type HullUserClaims = {| - id?: string; - email?: string; - external_id?: string; - anonymous_id?: string; -|}; +export type HullUserClaims = { + id?: ?string, + email?: ?string, + external_id?: ?string, + anonymous_id?: ?string +}; /** * This is a combined entity claims type. It's either account or user claims @@ -105,10 +204,10 @@ export type HullUserClaims = {| export type HullEntityClaims = HullUserClaims | HullAccountClaims; /** - * Auxiliary claims which can be added to the main identity claims, + * Additional claims which can be added to the main identity claims, * both for users and account which change resolution behavior */ -export type HullAuxiliaryClaims = {| +export type HullAdditionalClaims = {| create?: boolean, scopes?: Array, active?: boolean @@ -119,7 +218,7 @@ export type HullAuxiliaryClaims = {| * This are direct attribute values or operation definitions */ export type HullAccountAttributes = { - [HullAttributeName]: HullAttributeValue | HullAttributeOperation; + [HullAttributeName]: HullAttributeValue | HullAttributeOperation }; /** @@ -127,7 +226,7 @@ export type HullAccountAttributes = { * This are direct attribute values or operation definitions */ export type HullUserAttributes = { - [HullAttributeName]: HullAttributeValue | HullAttributeOperation; + [HullAttributeName]: HullAttributeValue | HullAttributeOperation }; /** @@ -141,10 +240,10 @@ export type HullEntityAttributes = HullAccountAttributes | HullUserAttributes; export type HullEventName = string; /** - * This is are event's properties which we use when tracking an event + * These are event's properties which we use when tracking an event */ export type HullEventProperties = { - [HullEventProperty: string]: string + [HullEventProperty: string]: HullAttributeValue }; /** @@ -171,9 +270,12 @@ export type HullEventContext = { * Combined ident and attributes object for account coming from platform */ export type HullAccount = { - id: string, - domain: string, - external_id: string, + id?: string, + domain?: ?string, + external_id?: ?string, + anonymous_ids?: ?Array, + anonymous_id?: ?string, // TODO: Flow Workaround -> force anonymous_id to be recognized as a ?string, Should be forced on Platform for safety + name?: ?string, [HullAttributeName]: HullAttributeValue }; @@ -182,10 +284,13 @@ export type HullAccount = { */ export type HullUser = { id: string, - email: string, - external_id: string, - anonymous_ids: Array, - [HullAttributeName]: HullAttributeValue; + email?: ?string, + external_id: ?string, + anonymous_ids: ?Array, + anonymous_id?: ?string, // TODO: Flow Workaround -> force anonymous_id to be recognized as a ?string, Should be forced on Platform for safety + segment_ids: Array | null, + domain?: ?string, + [HullAttributeName]: HullAttributeValue }; /** @@ -197,20 +302,20 @@ export type HullEntity = HullAccount | HullUser; * Event coming from platform */ export type HullEvent = { - event_id: string; - event: HullEventName; - created_at: string; - event_source?: string; - event_type?: string; - track_id?: string; - user_id?: string; - anonymous_id?: string; // not sure whether it's string or an array of strings - session_id?: string; - ship_id?: string; - app_id?: string; - app_name?: string; - context: HullEventContext; - properties: HullEventProperties; + event_id: string, + event: HullEventName, + created_at: string, + event_source?: string, + event_type?: string, + track_id?: string, + user_id?: string, + anonymous_id?: ?string, + session_id?: string, + ship_id?: string, + app_id?: string, + app_name?: string, + context: HullEventContext, + properties: HullEventProperties }; /** @@ -228,46 +333,64 @@ export type HullAttributesChanges = {| * It may contain none, one or multiple HullSegment in both params. */ export type HullSegmentsChanges = {| - entered?: Array; - left?: Array; + entered?: Array, + left?: Array |}; /** * Object containing all changes related to User in HullUserUpdateMessage */ export type HullUserChanges = {| - user: HullAttributesChanges; - account: HullAttributesChanges; - segments: HullSegmentsChanges; // should be segments or user_segments? + is_new: boolean, + user: HullAttributesChanges, + account: HullAttributesChanges, + segment_ids: Array, + segments: HullSegmentsChanges, // should be segments or user_segments? + account_segments: HullSegmentsChanges // should be segments or user_segments? |}; /** * Object containing all changes related to Account in HullUserUpdateMessage */ export type HullAccountChanges = {| - account: HullAttributesChanges; - segments: HullSegmentsChanges; // should be segments or account_segments? + is_new: boolean, + account: HullAttributesChanges, + account_segments: HullSegmentsChanges // should be segments or user_segments? |}; /** * A message sent by the platform when any event, attribute (trait) or segment change happens on the user. */ export type HullUserUpdateMessage = {| - user: HullUser; - changes: HullUserChanges; - segments: Array; // should be segments or user_segments? - events: Array; - account: HullAccount; + user: HullUser, + changes: HullUserChanges, + segments: Array, + account_segments: Array, + events: Array, + account: HullAccount, + message_id: string |}; +// TODO: What does a segment update message look like +export type HullSegmentUpdateMessage = {} +// TODO: What does a segment delete message look like +export type HullSegmentDeleteMessage = {} +// TODO: What does a user delete message look like +export type HullUserDeleteMessage = {} +// TODO: What does a account delete message look like +export type HullAccountDeleteMessage = {} +// TODO: What does a connector update message look like +export type HullConnectorUpdateMessage = {} + /** * A message sent by the platform when any attribute (trait) or segment change happens on the account. */ export type HullAccountUpdateMessage = {| - changes: HullUserChanges; - segments: Array; // should be segments or account_segments? - events: Array; - account: HullAccount; + changes: HullAccountChanges, + account_segments: Array, // should be segments or account_segments? + events: Array, + account: HullAccount, + message_id: string |}; /** @@ -276,9 +399,9 @@ export type HullAccountUpdateMessage = {| export type HullNotification = { notification_id: string, configuration: { - id?: string, - secret?: string, - organization?: string, + id: string, + secret: string, + organization: string }, channel: string, connector: HullConnector, @@ -303,13 +426,14 @@ export type HullClientConfiguration = { namespace?: string, requestId?: string, connectorName?: string, - firehoseUrl?: string, + firehoseUrl?: ?string, protocol?: string, prefix?: string, + logLevel?: "info" | "error" | "warn" | "debug", userClaim?: string | HullUserClaims, accountClaim?: string | HullAccountClaims, subjectType?: HullEntityType, - additionalClaims?: HullAuxiliaryClaims, + additionalClaims?: HullAdditionalClaims, accessToken?: string, hostSecret?: string, flushAt?: number, @@ -334,21 +458,19 @@ export type HullClientLogger = {| error: (string, Object) => void |}; - // Definition of static logger param available on HullClient class export type HullClientStaticLogger = {| ...HullClientLogger, transports: Object |}; - /** * Definition of utilities object */ export type HullClientUtils = {| traits: typeof traitsUtils, settings: typeof settingsUtils, - properties: typeof propertiesUtils, + properties: typeof propertiesUtils |}; export type HullProperties = { diff --git a/src/utils/properties.js b/src/utils/properties.js index 0caacce..201934e 100644 --- a/src/utils/properties.js +++ b/src/utils/properties.js @@ -6,7 +6,7 @@ const _ = require("lodash"); type HullPropertiesRawResponseTreeItemChild = { id: string, text: string, - type: string, + type: string }; type HullPropertiesRawResponseTreeItem = { @@ -19,25 +19,37 @@ type HullPropertiesRawResponse = { tree: Array }; - -function getProperties(raw: HullPropertiesRawResponse | HullPropertiesRawResponseTreeItem, path?: Array, id_path?: Array): { +function getProperties( + raw: HullPropertiesRawResponse | HullPropertiesRawResponseTreeItem, + path?: Array, + id_path?: Array +): { properties: HullProperties, tree: Array<*> } { const properties = {}; const tree = []; - _.each(raw, (props) => { + _.each(raw, props => { const title = props.text || props.name; const key = props.id || props.key; const node = { - ...props, id_path, path, title, key + ...props, + id_path, + path, + title, + key }; if (key) { properties[key] = node; } else if (node.children) { - const path_id = node.ship_id || node.app_id || node.platform_id || node.resource_id || title; + const path_id = + node.ship_id || + node.app_id || + node.platform_id || + node.resource_id || + title; const lpath = (path || []).concat([title]); const ipath = (id_path || []).concat([path_id]); @@ -60,9 +72,7 @@ function getProperties(raw: HullPropertiesRawResponse | HullPropertiesRawRespons * @return {Promise} */ function get(): Promise { - return this - .get("search/user_reports/bootstrap") - .then(({ tree }) => getProperties(tree).properties); + return this.get("search/user_reports/bootstrap").then(({ tree }) => getProperties(tree).properties); } module.exports = { diff --git a/src/utils/settings.js b/src/utils/settings.js index 00ac25a..c3f7195 100644 --- a/src/utils/settings.js +++ b/src/utils/settings.js @@ -13,12 +13,14 @@ import type { HullConnector, HullConnectorSettings } from "../types"; * @return {Promise} */ function update(newSettings: HullConnectorSettings): Promise { - return this.get("app") - .then((connector: HullConnector) => { - const private_settings: HullConnectorSettings = { ...connector.private_settings, ...newSettings }; - connector.private_settings = private_settings; - return this.put(connector.id, { private_settings }); - }); + return this.get("app").then((connector: HullConnector) => { + const private_settings: HullConnectorSettings = { + ...connector.private_settings, + ...newSettings + }; + connector.private_settings = private_settings; + return this.put(connector.id, { private_settings }); + }); } module.exports = { diff --git a/src/utils/traits.js b/src/utils/traits.js index 7a07ede..e2abf96 100644 --- a/src/utils/traits.js +++ b/src/utils/traits.js @@ -1,5 +1,10 @@ // @flow -import type { HullAttributeName, HullAttributeValue, HullEntity, HullEntityAttributes } from "../types"; +import type { + HullAttributeName, + HullAttributeValue, + HullEntity, + HullEntityAttributes +} from "../types"; const _ = require("lodash"); @@ -52,28 +57,36 @@ type HullEntityNested = { * }; */ function group(user: HullEntity): HullEntityNested { - return _.reduce(user, (grouped, value, key) => { - let dest = key; - if (key.match(/^traits_/)) { - if (key.match(/\//)) { - dest = key.replace(/^traits_/, ""); - } else { - dest = key.replace(/^traits_/, "traits/"); + return _.reduce( + user, + (grouped, value, key) => { + let dest = key; + if (key.match(/^traits_/)) { + if (key.match(/\//)) { + dest = key.replace(/^traits_/, ""); + } else { + dest = key.replace(/^traits_/, "traits/"); + } } - } - return _.setWith(grouped, dest.split("/"), value, Object); - }, {}); + return _.setWith(grouped, dest.split("/"), value, Object); + }, + {} + ); } function normalize(traits: HullEntityAttributes): HullEntityAttributes { - return _.reduce(traits, (memo, value, key) => { - if (!_.isObject(value)) { - value = { operation: "set", value }; - } - if (!value.operation) { value.operation = "set"; } - memo[key] = value; - return memo; - }, {}); + return _.reduce( + traits, + (memo, value, key) => { + const v = _.isObject(value) ? value : { operation: "set", value }; + if (!v.operation) { + v.operation = "set"; + } + memo[key] = v; + return memo; + }, + {} + ); } module.exports = { diff --git a/test/integration/array-capture-test.js b/test/integration/array-capture-test.js index 5840672..48d6fb0 100644 --- a/test/integration/array-capture-test.js +++ b/test/integration/array-capture-test.js @@ -57,7 +57,8 @@ describe("HullClient array capture feature", () => { expect(hullClient.configuration().logs).to.eql([ { context: { - organization: "hull-demos", id: "550964db687ee7866d000057" + organization: "hull-demos", + id: "550964db687ee7866d000057" }, data: { foo: "bar" }, level: "info", @@ -67,11 +68,15 @@ describe("HullClient array capture feature", () => { ]); // then log as user - hullClient.asUser({ email: "foo@bar.com" }).logger.info("outgoing.user.success", { baz: "bay" }); + hullClient + .asUser({ email: "foo@bar.com" }) + .logger.info("outgoing.user.success", { baz: "bay" }); + /* TODO: Clarify expectations for logs array behaviour. Should it be scoped by client instance? */ expect(hullClient.configuration().logs).to.eql([ { context: { - organization: "hull-demos", id: "550964db687ee7866d000057" + organization: "hull-demos", + id: "550964db687ee7866d000057" }, data: { foo: "bar" }, level: "info", @@ -94,4 +99,3 @@ describe("HullClient array capture feature", () => { clock.restore(); }); }); - diff --git a/test/integration/retry-test.js b/test/integration/retry-test.js index 03799f2..745791a 100644 --- a/test/integration/retry-test.js +++ b/test/integration/retry-test.js @@ -21,7 +21,7 @@ describe("client retrying", function test() { }); it("should retry 2 times if get 503 response, then reject", (done) => { - const stub = minihull.stubGet("/api/v1/testing") + const stub = minihull.stubApp("/api/v1/testing") .callsFake((req, res) => { res.status(503).end("error 503"); }); @@ -35,7 +35,7 @@ describe("client retrying", function test() { }); it("should retry 2 times if get 502 response, then reject", (done) => { - const stub = minihull.stubGet("/api/v1/testing") + const stub = minihull.stubApp("/api/v1/testing") .callsFake((req, res) => { res.status(502).end("error 502"); }); @@ -49,7 +49,7 @@ describe("client retrying", function test() { }); it("should retry first 503 response, then resolve", (done) => { - const stub = minihull.stubGet("/api/v1/testing") + const stub = minihull.stubApp("/api/v1/testing") .onFirstCall() .callsFake((req, res) => { res.status(503).end("error 503"); @@ -68,7 +68,7 @@ describe("client retrying", function test() { }); it("shoud retry 2 times on timeout, then reject", (done) => { - const stub = minihull.stubGet("/api/v1/testing") + const stub = minihull.stubApp("/api/v1/testing") .callsFake((req, res) => {}); client.get("/testing", {}, { timeout: 20, retry: 10 }) @@ -80,7 +80,7 @@ describe("client retrying", function test() { }); it("shoud retry first timeout, then resolve", (done) => { - const stub = minihull.stubGet("/api/v1/testing") + const stub = minihull.stubApp("/api/v1/testing") .onFirstCall() .callsFake((req, res) => { }) diff --git a/test/integration/track-test.js b/test/integration/track-test.js index ac10514..c97c0f4 100644 --- a/test/integration/track-test.js +++ b/test/integration/track-test.js @@ -29,11 +29,11 @@ describe("client.track()", function test() { }); it("should set default event_id", (done) => { - const stub = minihull.stubPost("/boom/firehose") + const stub = minihull.stubApp("POST", "/boom/firehose") .callsFake((req, res) => { res.end("ok"); }); - client.asUser("123").track("Foo") + client.asUser({ id: "123" }).track("Foo") .then(() => { const firstReq = minihull.requests.get("incoming").get(0).value(); expect(firstReq.body.batch[0].body.event_id).to.not.be.empty; @@ -42,11 +42,11 @@ describe("client.track()", function test() { }); it("should not overwrite event_id if provided", (done) => { - const stub = minihull.stubPost("/boom/firehose") + const stub = minihull.stubApp("POST", "/boom/firehose") .callsFake((req, res) => { res.end("ok"); }); - client.asUser("123").track("Foo", {}, { event_id: "someCustomValue" }) + client.asUser({ id: "123" }).track("Foo", {}, { event_id: "someCustomValue" }) .then(() => { const firstReq = minihull.requests.get("incoming").get(0).value(); expect(firstReq.body.batch[0].body.event_id).to.equal("someCustomValue"); @@ -55,7 +55,7 @@ describe("client.track()", function test() { }); it("shoud retry with the same event_id", (done) => { - const stub = minihull.stubPost("/boom/firehose") + const stub = minihull.stubApp("POST", "/boom/firehose") .onFirstCall() .callsFake((req, res) => {}) .onSecondCall() @@ -63,7 +63,7 @@ describe("client.track()", function test() { res.end("ok"); }); - client.asUser("123").track("Foo") + client.asUser({ id: "123" }).track("Foo") .then(() => { expect(stub.callCount).to.be.eql(2); const firstReq = minihull.requests.get("incoming").get(0).value(); diff --git a/test/unit/client-test.js b/test/unit/client-test.js index 081cdb4..771f3e9 100644 --- a/test/unit/client-test.js +++ b/test/unit/client-test.js @@ -1,6 +1,6 @@ /* global describe, it */ const { expect } = require("chai"); -const sinon = require("sinon"); +// const sinon = require("sinon"); const jwt = require("jwt-simple"); const Hull = require("../../src"); @@ -9,9 +9,8 @@ describe("Hull", () => { describe("as", () => { it("should return scoped client with traits, track and alias methods", () => { const hull = new Hull({ id: "562123b470df84b740000042", secret: "1234", organization: "test" }); - const scopedAccount = hull.asAccount({ domain: "hull.io" }); - const scopedUser = hull.asUser("1234"); + const scopedUser = hull.asUser({ id: "1234"}); expect(scopedAccount).to.has.property("token") .that.is.an("function"); @@ -55,15 +54,9 @@ describe("Hull", () => { .that.eql(["admin"]); }); - it("should allow to pass user id as a string", () => { + it("should disallow to pass user id as a string", () => { const hull = new Hull({ id: "562123b470df84b740000042", secret: "1234", organization: "test" }); - - const scoped = hull.asUser("123456"); - const scopedConfig = scoped.configuration(); - const scopedJwtClaims = jwt.decode(scopedConfig.accessToken, scopedConfig.secret); - expect(scopedJwtClaims) - .to.have.property("sub") - .that.eql("123456"); + expect(hull.asUser.bind(hull.asUser, "1234")).to.throw(); }); it("should allow to pass account domain as an object property", () => { @@ -100,7 +93,7 @@ describe("Hull", () => { it("should allow to link a user using its id to an account", () => { const hull = new Hull({ id: "562123b470df84b740000042", secret: "1234", organization: "test" }); - const scoped = hull.asUser("1234").account({ domain: "hull.io" }); + const scoped = hull.asUser({ id: "1234" }).account({ domain: "hull.io" }); const scopedJwtClaims = jwt.decode(scoped.configuration().accessToken, scoped.configuration().secret); expect(scopedJwtClaims)