├── .eslintrc ├── .eslintrc.js ├── .gitignore ├── .jscsrc ├── .prettierignore ├── .prettierrc.js ├── .travis.yml ├── .yo-rc.json ├── CHANGELOG.md ├── Makefile ├── README.md ├── api.md ├── example ├── conductors │ ├── handlerConductor.js │ ├── inheritance │ │ ├── inheritanceConductor.js │ │ ├── inheritanceConductor2.js │ │ ├── inheritanceConductor3.js │ │ ├── inheritanceConductor4.js │ │ └── inheritanceConductor5.js │ ├── models │ │ ├── modelConductor.js │ │ ├── modelConductor2.js │ │ ├── modelConductor3.js │ │ ├── modelConductor4.js │ │ └── modelConductor5.js │ ├── props │ │ └── propsConductor.js │ ├── sharding │ │ └── shardConductor.js │ └── simple │ │ ├── simpleConductor.js │ │ ├── simpleConductor2.js │ │ ├── simpleConductor3.js │ │ └── simpleConductor4.js ├── demo.js ├── index.js └── models │ ├── ip.js │ ├── posts.js │ ├── serverEnv.js │ └── userAgent.js ├── lib ├── Conductor.js ├── clients │ └── index.js ├── errors │ └── index.js ├── handlers │ ├── buildModels.js │ ├── index.js │ ├── init.js │ └── run.js ├── helpers.js ├── index.js ├── logHelpers.js ├── models │ ├── Model.js │ └── RestModel.js └── reqHelpers.js ├── package.json ├── test ├── .eslintrc ├── ConductorSpec.js ├── HelperSpec.js ├── IntegrationSpec.js └── runSpec.js └── tools └── githooks └── pre-push /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": false, 4 | "node": true, 5 | "es6": false 6 | }, 7 | "rules": { 8 | // possible errors 9 | "no-cond-assign": [ 2 ], 10 | "no-console": [ 2 ], 11 | "no-constant-condition": [ 2 ], 12 | "no-control-regex": [ 2 ], 13 | "no-debugger": [ 2 ], 14 | "no-dupe-args": [ 2 ], 15 | "no-dupe-keys": [ 2 ], 16 | "no-duplicate-case": [ 2 ], 17 | "no-empty": [ 2 ], 18 | "no-empty-character-class": [ 2 ], 19 | "no-ex-assign": [ 2 ], 20 | "no-extra-boolean-cast": [ 2 ], 21 | "no-extra-semi": [ 2 ], 22 | "no-func-assign": [ 2 ], 23 | // this is for variable hoisting, not necessary if we use block scoped declarations 24 | // "no-inner-declarations": [ 2, "both" ], 25 | "no-invalid-regexp": [ 2 ], 26 | "no-irregular-whitespace": [ 2 ], 27 | "no-negated-in-lhs": [ 2 ], 28 | // when IE8 dies 29 | "no-reserved-keys": [ 0 ], 30 | "no-regex-spaces": [ 2 ], 31 | "no-sparse-arrays": [ 2 ], 32 | "no-unreachable": [ 2 ], 33 | "use-isnan": [ 2 ], 34 | // should we enforce valid documentation comments? 35 | // i.e., if you do documentation, do it right 36 | "valid-jsdoc": [ 2, { 37 | "requireReturnDescription": false, 38 | "prefer": { 39 | "return": "returns" 40 | } 41 | }], 42 | "valid-typeof": [ 2 ], 43 | 44 | // best practices 45 | "block-scoped-var": [ 2 ], 46 | // warning for now until we get them fixed 47 | "consistent-return": [ 2 ], 48 | "curly": [ 2 ], 49 | "default-case": [ 2 ], 50 | "dot-notation": [ 2, { "allowKeywords": true } ], 51 | "eqeqeq": [ 2 ], 52 | "guard-for-in": [ 2 ], 53 | "no-alert": [ 2 ], 54 | "no-caller": [ 2 ], 55 | "no-div-regex": [ 2 ], 56 | "no-eq-null": [ 2 ], 57 | "no-eval": [ 2 ], 58 | "no-extend-native": [ 2 ], 59 | "no-extra-bind": [ 2 ], 60 | "no-fallthrough": [ 2 ], 61 | "no-floating-decimal": [ 2 ], 62 | "no-implied-eval": [ 2 ], 63 | "no-iterator": [ 2 ], 64 | "no-labels": [ 2 ], 65 | "no-lone-blocks": [ 2 ], 66 | "no-loop-func": [ 2 ], 67 | "no-multi-spaces": [ 0 ], 68 | "no-native-reassign": [ 2 ], 69 | "no-new": [ 2 ], 70 | "no-new-func": [ 2 ], 71 | "no-new-wrappers": [ 2 ], 72 | "no-octal": [ 2 ], 73 | "no-octal-escape": [ 2 ], 74 | // "no-param-reassign": [ 2 ], 75 | "no-proto": [ 2 ], 76 | "no-redeclare": [ 2 ], 77 | "no-return-assign": [ 2 ], 78 | "no-script-url": [ 2 ], 79 | "no-self-compare": [ 2 ], 80 | "no-sequences": [ 2 ], 81 | "no-throw-literal": [ 2 ], 82 | "no-unused-expressions": [ 2 ], 83 | "no-void": [ 2 ], 84 | "no-with": [ 2 ], 85 | "wrap-iife": [ 2 ], 86 | "yoda": [ 2, "never" ], 87 | 88 | // strict mode 89 | "strict": [ 2, "global" ], 90 | 91 | // variables 92 | "no-catch-shadow": [ 2 ], 93 | "no-delete-var": [ 2 ], 94 | "no-shadow": [ 2 ], 95 | "no-shadow-restricted-names": [ 2 ], 96 | "no-undef": [ 2 ], 97 | "no-undef-init": [ 2 ], 98 | "no-undefined": [ 2 ], 99 | "no-unused-vars": [ 2, { "vars": "all", "args": "none" } ], 100 | "no-use-before-define": [ 2, "nofunc" ], 101 | 102 | // node.js 103 | "handle-callback-err": [ 2, "^.*(e|E)rr" ], 104 | "no-mixed-requires": [ 2 ], 105 | "no-new-require": [ 2 ], 106 | "no-path-concat": [ 2 ], 107 | "no-process-exit": [ 0 ], 108 | 109 | // ES6 110 | "generator-star-spacing": [ 2, "after" ], 111 | 112 | // stylistic 113 | // we use JSCS, set most to off because they're on by default. 114 | // turn the few on that aren't handled by JSCS today. 115 | "camelcase": [ 0 ], 116 | "comma-dangle": [ 0 ], 117 | "key-spacing": [ 0 ], 118 | "no-lonely-if": [ 0 ], 119 | "no-multi-str": [ 0 ], 120 | "no-underscore-dangle": [ 0 ], 121 | "quotes": [ 0 ], 122 | "semi": [ 0 ], 123 | "space-infix-ops": [ 0 ], 124 | "space-return-throw-case": [ 0 ], 125 | "space-unary-ops": [ 0 ], 126 | 127 | "no-array-constructor": [ 2 ], 128 | "no-nested-ternary": [ 2 ], 129 | "no-new-object": [ 2 ], 130 | 131 | // for warning on TODO comments 132 | "no-warning-comments": [ 1, { 133 | "terms": ["todo"], 134 | "location": "anywhere" 135 | }] 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | // eslint: recommended automatically enables most/all rules from the 5 | // possible errors section and more: 6 | // http://eslint.org/docs/rules/#possible-errors 7 | extends: ['plugin:prettier/recommended'], 8 | env: { 9 | browser: false, 10 | node: true, 11 | es6: true, 12 | mocha: true 13 | }, 14 | plugins: ['prettier'], 15 | rules: { 16 | 'prettier/prettier': 'error', 17 | 18 | // possible errors 19 | 'no-cond-assign': ['error'], 20 | 'no-constant-condition': ['error'], 21 | 'no-control-regex': ['error'], 22 | 'no-debugger': ['error'], 23 | 'no-dupe-args': ['error'], 24 | 'no-dupe-keys': ['error'], 25 | 'no-duplicate-case': ['error'], 26 | 'no-empty': ['error'], 27 | 'no-empty-character-class': ['error'], 28 | 'no-ex-assign': ['error'], 29 | 'no-extra-boolean-cast': ['error'], 30 | 'no-extra-semi': ['error'], 31 | 'no-func-assign': ['error'], 32 | // this is for variable hoisting, not necessary if we use block scoped declarations 33 | // "no-inner-declarations": ["error", "both" ], 34 | 'no-invalid-regexp': ['error'], 35 | 'no-irregular-whitespace': ['error'], 36 | 'no-reserved-keys': ['off'], 37 | 'no-regex-spaces': ['error'], 38 | 'no-sparse-arrays': ['error'], 39 | 'no-unreachable': ['error'], 40 | 'no-unsafe-negation': ['error'], 41 | 'use-isnan': ['error'], 42 | 'valid-jsdoc': [ 43 | 'error', 44 | { 45 | requireReturnDescription: false 46 | } 47 | ], 48 | 'valid-typeof': ['error'], 49 | 50 | // best practices 51 | 'array-callback-return': ['error'], 52 | 'block-scoped-var': ['error'], 53 | 'class-methods-use-this': ['error'], 54 | complexity: ['warn'], 55 | 'consistent-return': ['error'], 56 | curly: ['error'], 57 | 'default-case': ['error'], 58 | 'dot-notation': ['error', { allowKeywords: true }], 59 | eqeqeq: ['error'], 60 | 'guard-for-in': ['error'], 61 | 'no-alert': ['error'], 62 | 'no-caller': ['error'], 63 | 'no-case-declarations': ['error'], 64 | 'no-div-regex': ['error'], 65 | 'no-empty-function': ['error'], 66 | 'no-empty-pattern': ['error'], 67 | 'no-eq-null': ['error'], 68 | 'no-eval': ['error'], 69 | 'no-extend-native': ['error'], 70 | 'no-extra-bind': ['error'], 71 | 'no-extra-label': ['error'], 72 | 'no-fallthrough': ['error'], 73 | 'no-floating-decimal': ['error'], 74 | 'no-global-assign': ['error'], 75 | 'no-implicit-coercion': ['error'], 76 | 'no-implied-eval': ['error'], 77 | 'no-iterator': ['error'], 78 | 'no-labels': ['error'], 79 | 'no-lone-blocks': ['error'], 80 | 'no-loop-func': ['error'], 81 | 'no-magic-numbers': ['off'], 82 | 'no-multi-spaces': ['off'], 83 | 'no-new': ['error'], 84 | 'no-new-func': ['error'], 85 | 'no-new-wrappers': ['error'], 86 | 'no-octal': ['error'], 87 | 'no-octal-escape': ['error'], 88 | 'no-proto': ['error'], 89 | 'no-redeclare': ['error'], 90 | 'no-return-assign': ['error'], 91 | 'no-script-url': ['error'], 92 | 'no-self-assign': ['error'], 93 | 'no-self-compare': ['error'], 94 | 'no-sequences': ['error'], 95 | 'no-throw-literal': ['error'], 96 | 'no-unmodified-loop-condition': ['error'], 97 | 'no-unused-expressions': ['error'], 98 | 'no-unused-labels': ['error'], 99 | 'no-useless-call': ['error'], 100 | 'no-useless-concat': ['error'], 101 | 'no-void': ['error'], 102 | 'no-warning-comments': ['warn'], 103 | 'no-with': ['error'], 104 | 'wrap-iife': ['error'], 105 | yoda: ['error', 'never'], 106 | 107 | // strict mode 108 | strict: ['error', 'global'], 109 | 110 | // variables 111 | 'no-catch-shadow': ['error'], 112 | 'no-delete-var': ['error'], 113 | 'no-shadow': ['error'], 114 | 'no-shadow-restricted-names': ['error'], 115 | 'no-undef': ['error'], 116 | 'no-undef-init': ['error'], 117 | 'no-unused-vars': ['error', { vars: 'all', args: 'none' }], 118 | 'no-use-before-define': ['error', 'nofunc'], 119 | 120 | // node.js 121 | 'callback-return': [ 122 | 'error', 123 | ['callback', 'cb', 'cb1', 'cb2', 'cb3', 'next', 'innerCb', 'done'] 124 | ], 125 | 'global-require': ['error'], 126 | 'handle-callback-err': ['error', '^.*(e|E)rr'], 127 | 'no-mixed-requires': ['error'], 128 | 'no-new-require': ['error'], 129 | 'no-path-concat': ['error'], 130 | 'no-process-exit': ['error'] 131 | } 132 | }; 133 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS generated files # 2 | ###################### 3 | .DS_Store 4 | .DS_Store? 5 | ._* 6 | .Spotlight-V100 7 | .Trashes 8 | ehthumbs.db 9 | Thumbs.db 10 | 11 | /node_modules/ 12 | npm-debug.log 13 | yarn.lock 14 | package-lock.json 15 | 16 | # VIM viles # 17 | ############# 18 | [._]*.s[a-w][a-z] 19 | [._]s[a-w][a-z] 20 | *.un~ 21 | Session.vim 22 | .netrwhist 23 | *~ 24 | 25 | # Unit Test Coverage # 26 | ###################### 27 | coverage/ 28 | .nyc_output/ 29 | 30 | *.log 31 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | // setup 3 | "fileExtensions": [ ".js" ], 4 | "excludeFiles": [ "./node_modules/**" ], 5 | 6 | // general rules 7 | "safeContextKeyword": [ "self" ], 8 | "validateParameterSeparator": ", ", 9 | "validateQuoteMarks": { 10 | "escape": true, 11 | "mark": "'" 12 | }, 13 | "requireLineFeedAtFileEnd": true, 14 | 15 | // alignment rules 16 | "maximumLineLength": { 17 | "value": 80, 18 | "allowComments": true, 19 | "allowUrlComments": true, 20 | "allowRegex": true 21 | }, 22 | "validateIndentation": 4, 23 | 24 | // disallow rules 25 | "disallowImplicitTypeConversion": [ 26 | "numeric", 27 | "boolean", 28 | "binary", 29 | "string" 30 | ], 31 | "disallowMixedSpacesAndTabs": true, 32 | "disallowMultipleVarDecl": "exceptUndefined", 33 | "disallowNewlineBeforeBlockStatements": true, 34 | "disallowOperatorBeforeLineBreak": [ "." ], 35 | "disallowQuotedKeysInObjects": true, 36 | "disallowSpaceAfterPrefixUnaryOperators": true, 37 | "disallowSpaceBeforePostfixUnaryOperators": true, 38 | "disallowSpacesInFunction": { 39 | "beforeOpeningRoundBrace": true 40 | }, 41 | "disallowSpacesInCallExpression": true, 42 | "disallowTrailingComma": true, 43 | "disallowTrailingWhitespace": true, 44 | "disallowYodaConditions": true, 45 | 46 | // require rules 47 | "requireBlocksOnNewline": true, 48 | "requireCamelCaseOrUpperCaseIdentifiers": "ignoreProperties", 49 | "requireCapitalizedConstructors": true, 50 | "requireCurlyBraces": [ 51 | "if", 52 | "else", 53 | "for", 54 | "while", 55 | "do", 56 | "try", 57 | "catch" 58 | ], 59 | "requireDotNotation": true, 60 | "requireLineBreakAfterVariableAssignment": true, 61 | "requirePaddingNewLinesAfterUseStrict": true, 62 | "requirePaddingNewLinesBeforeExport": true, 63 | "requirePaddingNewlinesBeforeKeywords": [ 64 | "do", 65 | "for", 66 | "if", 67 | "switch", 68 | "try", 69 | "while" 70 | ], 71 | "requireSemicolons": true, 72 | "requireSpaceAfterBinaryOperators": true, 73 | "requireSpaceAfterKeywords": [ 74 | "do", 75 | "for", 76 | "if", 77 | "else", 78 | "switch", 79 | "case", 80 | "try", 81 | "catch", 82 | "while", 83 | "return", 84 | "typeof", 85 | "delete", 86 | "new", 87 | "void" 88 | ], 89 | "requireSpaceBeforeBinaryOperators": true, 90 | "requireSpaceBeforeBlockStatements": true, 91 | "requireSpaceBeforeKeywords": [ 92 | "else", 93 | "while", 94 | "catch" 95 | ], 96 | "requireSpaceBetweenArguments": true, 97 | "requireSpacesInConditionalExpression": true, 98 | "requireSpacesInForStatement": true, 99 | "requireSpacesInFunction": { 100 | "beforeOpeningCurlyBrace": true 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .githooks 2 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | singleQuote: true, 5 | printWidth: 80, 6 | tabWidth: 4, 7 | semi: true, 8 | arrowParens: 'always' 9 | }; 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" # Maintenance LTS release 4 | - "lts/*" # Active LTS release 5 | - "node" # Latest stable release 6 | after_success: 'make report-coverage' 7 | -------------------------------------------------------------------------------- /.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "generator-nf-npm": {} 3 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ### 2.0.2 (2018-12-06) 3 | 4 | 5 | #### Bug Fixes 6 | 7 | * remove model failure logging (#129) ([fbfa9cb5](https://github.com/restify/conductor/commit/fbfa9cb5)) 8 | 9 | 10 | 11 | ### 2.0.1 (2018-10-30) 12 | 13 | 14 | #### Bug Fixes 15 | 16 | * support loggers that don't implement `addSerializers` (such as pino) (#128) ([f9061e4d](https://github.com/restify/conductor/commit/f9061e4d)) 17 | 18 | 19 | 20 | ## 2.0.0 (2018-10-12) 21 | 22 | 23 | #### Breaking Changes 24 | 25 | * updating restify changes route registration interface 26 | due to new find-my-way router 27 | 28 | ([165ee16a](https://github.com/restify/conductor/commit/165ee16a)) 29 | 30 | 31 | 32 | ## 1.3.0 (2018-09-06) 33 | 34 | 35 | #### Bug Fixes 36 | 37 | * update broken tests (#122) ([0b0f39d4](https://github.com/restify/conductor/commit/0b0f39d4)) 38 | 39 | 40 | #### Features 41 | 42 | * add createConductorHandlers function (#121) ([a25ed5c0](https://github.com/restify/conductor/commit/a25ed5c0)) 43 | 44 | 45 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Directories 3 | # 4 | ROOT_SLASH := $(dir $(realpath $(firstword $(MAKEFILE_LIST)))) 5 | ROOT := $(patsubst %/,%,$(ROOT_SLASH)) 6 | BENCHMARK := $(ROOT)/benchmark 7 | LIB := $(ROOT)/lib 8 | TEST := $(ROOT)/test 9 | TOOLS := $(ROOT)/tools 10 | GITHOOKS_SRC := $(TOOLS)/githooks 11 | GITHOOKS_DEST := $(ROOT)/.git/hooks 12 | 13 | 14 | # 15 | # Generated Files & Directories 16 | # 17 | NODE_MODULES := $(ROOT)/node_modules 18 | NODE_BIN := $(NODE_MODULES)/.bin 19 | COVERAGE := $(ROOT)/.nyc_output 20 | COVERAGE_RES := $(ROOT)/coverage 21 | YARN_LOCK := $(ROOT)/yarn.lock 22 | PACKAGE_LOCK := $(ROOT)/package-lock.json 23 | 24 | 25 | # 26 | # Tools and binaries 27 | # 28 | DOCUMENT := $(NODE_BIN)/documentation 29 | NPM := npm 30 | NODE := node 31 | YARN := yarn 32 | ESLINT := $(NODE_BIN)/eslint 33 | MOCHA := $(NODE_BIN)/mocha 34 | NYC := $(NODE_BIN)/nyc 35 | PRETTIER := $(NODE_BIN)/prettier 36 | UNLEASH := $(NODE_BIN)/unleash 37 | CONVENTIONAL_RECOMMENDED_BUMP := $(NODE_BIN)/conventional-recommended-bump 38 | 39 | 40 | # 41 | # Files and globs 42 | # 43 | PACKAGE_JSON := $(ROOT)/package.json 44 | API_MD := $(ROOT)/api.md 45 | GITHOOKS := $(wildcard $(GITHOOKS_SRC)/*) 46 | ALL_FILES := $(shell find $(ROOT) \ 47 | -not \( -path $(NODE_MODULES) -prune \) \ 48 | -not \( -path $(COVERAGE) -prune \) \ 49 | -not \( -path $(COVERAGE_RES) -prune \) \ 50 | -name '*.js' -type f) 51 | TEST_FILES := $(shell find $(TEST) -name '*.js' -type f) 52 | 53 | # 54 | # Targets 55 | # 56 | 57 | $(NODE_MODULES): $(PACKAGE_JSON) ## Install node_modules 58 | @$(YARN) 59 | @touch $(NODE_MODULES) 60 | 61 | 62 | .PHONY: docs 63 | docs: $(DOCUMENT) $(ALL_FILES) 64 | @$(DOCUMENT) build $(LIB) -f md -o $(API_MD) 65 | 66 | 67 | .PHONY: help 68 | help: 69 | @perl -nle'print $& if m{^[a-zA-Z_-]+:.*?## .*$$}' $(MAKEFILE_LIST) \ 70 | | sort | awk 'BEGIN {FS = ":.*?## "}; \ 71 | {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 72 | 73 | 74 | .PHONY: githooks 75 | githooks: $(GITHOOKS) ## Symlink githooks 76 | @$(foreach hook,\ 77 | $(GITHOOKS),\ 78 | ln -sf $(hook) $(GITHOOKS_DEST)/$(hook##*/);\ 79 | ) 80 | 81 | 82 | .PHONY: benchmark 83 | benchmark: $(BENCHMARK) 84 | @$(NODE) $(BENCHMARK) 85 | 86 | 87 | .PHONY: release-dry 88 | release-dry: $(NODE_MODULES) 89 | $(UNLEASH) -d --type=$(shell $(CONVENTIONAL_RECOMMENDED_BUMP) -p angular) 90 | 91 | 92 | .PHONY: release 93 | release: $(NODE_MODULES) ## Versions, tags, and updates changelog based on commit messages 94 | $(UNLEASH) --type=$(shell $(CONVENTIONAL_RECOMMENDED_BUMP) -p angular) --no-publish 95 | $(NPM) publish 96 | 97 | 98 | .PHONY: lint 99 | lint: $(NODE_MODULES) $(ESLINT) $(ALL_FILES) ## Run lint checker (eslint). 100 | @$(ESLINT) $(ALL_FILES) 101 | 102 | 103 | .PHONY: lint-fix 104 | lint-fix: $(NODE_MODULES) $(PRETTIER) $(ALL_FILES) ## Reprint code (prettier, eslint). 105 | @$(PRETTIER) --write $(ALL_FILES) 106 | @$(ESLINT) --fix $(ALL_FILES) 107 | 108 | 109 | .PHONY: security 110 | security: $(NODE_MODULES) ## Check for dependency vulnerabilities. 111 | @$(NPM) install --package-lock-only 112 | @$(NPM) audit 113 | @rm $(PACKAGE_LOCK) 114 | 115 | 116 | .PHONY: prepush 117 | prepush: $(NODE_MODULES) lint docs coverage ## Git pre-push hook task. Run before committing and pushing. 118 | 119 | 120 | .PHONY: test 121 | test: $(NODE_MODULES) $(MOCHA) ## Run unit tests. 122 | @$(MOCHA) -R spec --full-trace --no-exit --no-timeouts $(TEST_FILES) 123 | 124 | 125 | .PHONY: coverage 126 | coverage: $(NODE_MODULES) $(NYC) ## Run unit tests with coverage reporting. Generates reports into /coverage. 127 | @$(NYC) --reporter=html --reporter=text make test 128 | 129 | 130 | .PHONY: report-coverage 131 | report-coverage: $(NODE_MODULES) $(NYC) ## Report unit test coverage to coveralls 132 | @$(NYC) report --reporter=text-lcov make test | $(COVERALLS) 133 | 134 | 135 | .PHONY: clean 136 | clean: ## Cleans unit test coverage files and node_modules. 137 | @rm -rf $(NODE_MODULES) $(COVERAGE) $(COVERAGE_RES) $(YARN_LOCK) $(PACKAGE_LOCK) 138 | 139 | 140 | # 141 | ## Debug -- print out a a variable via `make print-FOO` 142 | # 143 | print-% : ; @echo $* = $($*) 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # restify-conductor 2 | 3 | 4 | [![Build Status](https://travis-ci.org/restify/conductor.svg?branch=master)](https://travis-ci.org/restify/conductor) 5 | [![Coverage Status](https://coveralls.io/repos/restify/conductor/badge.svg?branch=master)](https://coveralls.io/r/restify/conductor?branch=master) 6 | [![Dependency Status](https://david-dm.org/restify/conductor.svg)](https://david-dm.org/restify/conductor) 7 | [![devDependency Status](https://david-dm.org/restify/conductor/dev-status.svg)](https://david-dm.org/restify/conductor#info=devDependencies) 8 | [![bitHound Score](https://www.bithound.io/github/restify/conductor/badges/score.svg)](https://www.bithound.io/github/restify/conductor/master) 9 | 10 | > an abstraction framework for building composable endpoints in restify 11 | 12 | 13 | ## Getting Started 14 | 15 | Install the module with: `npm install restify-conductor` 16 | 17 | 18 | ## Why? 19 | 20 | Restify, like other Node.js frameworks, provides built in support for 21 | [Connect](https://github.com/senchalabs/connect) style handlers. This simple 22 | yet elegant solution works well for many scenarios. But sometimes, as 23 | complexity in your application grows, it can become increasingly difficult to 24 | share and manage. This module alleviates those pain points by providing a 25 | `Conductor` construct, which serves as an orchestration layer on top of your 26 | handler stack, as well as providing some nice built-in support for fetching 27 | remote resources. This construct also allows easing "moving" of an entire page 28 | from one URL to another, which can be otherwise non-trivial. 29 | 30 | The top 5 reasons for using restify-conductor: 31 | 32 | * You want to decouple an endpoint's logic from the URL it is installed to. 33 | * You want to easily move a page from one URL to another. 34 | * Your site is large and complex with many pages sharing similar context and 35 | handler stacks. 36 | * You have long (15, 20+) handler stacks, and when the handler stacks change, 37 | you want to be able to change it at only one place. 38 | * You want to be able to reuse existing stacks, but customize their behavior as 39 | needed on a per URL basis instead of having to rewrite the entire handler but 40 | with one line slightly different. 41 | * You want to be able to serve two completely different responses to the same 42 | URL, based on user state (e.g., home page for logged in vs logged out), 43 | __without__ redirecting. 44 | 45 | 46 | ## Basic Usage 47 | 48 | ### Handlers 49 | 50 | Assuming you have a Restify server, you can install `Conductor` objects at a 51 | given endpoint: 52 | 53 | ```js 54 | var restify = require('restify'); 55 | var rc = require('restify-conductor'); 56 | 57 | var server = restify.createServer(); 58 | 59 | var simpleConductor = rc.createConductor({ 60 | name: 'simpleConductor', 61 | handlers: [ 62 | function render(req, res, next) { 63 | res.send(200, 'hello world!'); 64 | return next(); 65 | } 66 | ] 67 | }); 68 | 69 | rc.get('/foo', simpleConductor, server); 70 | ``` 71 | 72 | This conductor has only one handler, a render function that renders 'hello 73 | world!' to the client. Like other frameworks, you can pass in multiple handlers 74 | which will run in serial. 75 | 76 | 77 | ### Props 78 | 79 | We can extend this conductor object with the concept of `props`, or immuatable 80 | properties. Simply pass in a function to the `createConductor` method, and it 81 | will be invoked at creation time. The object returned by the props function is 82 | immutable over the lifetime of the server. You can access these props from your 83 | handlers: 84 | 85 | 86 | ```js 87 | 88 | var propsConductor = rc.createConductor({ 89 | name: 'propsConductor', 90 | props: function() { 91 | return { 92 | blacklistQueries: ['foo'] 93 | }; 94 | }, 95 | handlers: [ 96 | function validateQuery(req, res, next) { 97 | // retrieve props via helper. 98 | var blacklist = rc.getProps(req, 'blacklistQueries'); 99 | 100 | // check if the search query is in the allowed list 101 | var query = req.query.search; 102 | if (blacklist.indexOf(query) === -1) { 103 | // if it is, return a 500 104 | return next(new restify.errors.InternalServerError('blacklisted query!')); 105 | } 106 | 107 | // otherwise, we're good! 108 | return next(); 109 | }, 110 | function render(req, res, next) { 111 | // respond with the valid query 112 | res.render(req.query); 113 | } 114 | ] 115 | }); 116 | 117 | rc.get('/props', propsConductor, server); 118 | ``` 119 | 120 | Looking at this example, we _could_ just hard code the value of blacklistQueries 121 | into the handler. But using props allows us to easily share this handler across 122 | other conductors that may have different values for the blacklist. 123 | 124 | 125 | ### Models 126 | 127 | restify-conductor also comes with first class support for the concept of 128 | models. Models are sources of data needed by your conductor. The source of the 129 | model data can be anything. It can be the request (e.g., user agent parsing), 130 | or a data store of some kind (Redis, mySQL), or even a remote data source. 131 | 132 | The Model construct provides a lifecycle of methods available to you to act on the data. 133 | 134 | * `before` {Function} - a function invoked before the request for your data 135 | source is made 136 | * `isValid` {Function} - a function invoked to ensure validity of your payload 137 | * `after` {Function} - a function invoked after the isValid check to do 138 | additional manipulation or storage of your data 139 | * `fallback` {Function} - a function that allows you to set model data in the 140 | event the request fails 141 | 142 | 143 | Creating models is easy: 144 | 145 | ```js 146 | // a model whose data source is the request 147 | var userAgent = rc.createModel({ 148 | name: 'userAgent', 149 | data: req.headers['user-agent'] 150 | }); 151 | 152 | 153 | // a model whose data source is coming from a remote location 154 | var ipModel = rc.createModel({ 155 | name: 'ip', 156 | host: 'jsonip.com', 157 | isValid: function(data) { 158 | // validate the payload coming back, it should have two fields. 159 | return (data.hasOwnProperty('ip') && data.hasOwnProperty('about')); 160 | } 161 | }); 162 | ``` 163 | 164 | You can then consume them in your conductor. The default behavior is to fetch all 165 | models specified in the models config in parallel: 166 | 167 | 168 | ```js 169 | var modelConductor = new Conductor({ 170 | name: 'modelConductor', 171 | models: [ userAgent, ip ], 172 | handlers: [ 173 | rc.handlers.buildModels(), // fetch models in parallel 174 | function render(req, res, next) { 175 | // now we can access the models 176 | var uaModel = rc.getModel(req, 'userAgent'), 177 | ipModel = rc.getModel(req, 'ip'); 178 | 179 | // put together a payload. 180 | var out = { 181 | userAgentModel: uaModel.data, 182 | ipModel: ipModel.data 183 | }; 184 | 185 | res.render(out, next); 186 | } 187 | ] 188 | }); 189 | ``` 190 | 191 | It is also possible to fetch multiple models in serial, if you have models 192 | dependent on the output of another async model. To do so, you can pass an object 193 | into models instead, with each key of the object specifying an array of models. 194 | This allows you to address each 'bucket' of models using the key: 195 | 196 | 197 | ```js 198 | var seriesModelConductor = rc.createConductor({ 199 | name: 'seriesModelConductor', 200 | models: { 201 | bucketA: [ ip, userAgent ], 202 | bucketB: [ date ] 203 | }, 204 | handlers: [ 205 | rc.handlers.buildModels('bucketA'), // fetch bucketA models in parallel 206 | function check(req, res, next) { 207 | var ipModel = rc.getModels(req, 'ip'); 208 | var uaModel = rc.getModels(req, 'userAgent'); 209 | 210 | // the ip and user agent models are done! 211 | assert.ok(ipModel); 212 | assert.ok(uaModel); 213 | 214 | return next(); 215 | }, 216 | rc.handlers.buildModels('bucketB'), // then, fetch bucketB in parallel 217 | function render(req, res, next) { 218 | var allModels = rc.getModels(req); 219 | 220 | // make sure we got three models 221 | assert.equal(_.size(allModels), 3); 222 | } 223 | ] 224 | }); 225 | 226 | ``` 227 | 228 | 229 | ### Inheritance/Composition 230 | 231 | Conductors can also be inherited from. Inheriting from another conductor 232 | automatically gives you the same props and handlers as the parent conductor. 233 | Props can be mutated by the inheriting conductor, but handlers cannot. However, 234 | handlers can be appended and prepended to. Let's look at props first. 235 | 236 | ```js 237 | // here is our parent conductor 238 | var parentConductor = rc.createConductor({ 239 | name: 'parent', 240 | props: function() { 241 | return { 242 | count: 0, 243 | candies: [ 'twix', 'snickers', 'kit kat' ] 244 | }; 245 | } 246 | }); 247 | 248 | // now we inherit by specifying a deps config 249 | var childConductor = rc.createConductor({ 250 | name: 'child', 251 | deps: [ parentConductor ], 252 | props: function(inheritedProps) { 253 | // children conductor are provided with the parent props. 254 | // you can choose to mutate this object for the child conductor. 255 | 256 | // note that mutating inheritedProps does NOT affect the parent 257 | // conductor's props! 258 | inheritedProps.count += 1; 259 | inheritedProps.candies = inheritedProps.candies.concat('butterfinger'); 260 | 261 | // like the parent conductor, this returned value will become immutable 262 | return inheritedProps; 263 | }, 264 | handlers: [ 265 | function render(req, res, next) { 266 | var props = rc.getProps(req); 267 | 268 | res.render(props, next); 269 | // => will render: 270 | // { 271 | // count: 1, 272 | // candies: [ 'twix', 'snickers', 'kit kat', 'butterfinger' ] 273 | // } 274 | } 275 | ] 276 | }); 277 | ``` 278 | 279 | Handlers can also be inherited, and appended to: 280 | 281 | ```js 282 | var parentConductor = rc.createConductor({ 283 | name: 'parent', 284 | handlers: [ addName ] 285 | }); 286 | 287 | var childConductor = rc.createConductor({ 288 | name: 'child', 289 | deps: [ parentConductor ], 290 | handlers: [ render ] 291 | }); 292 | 293 | // => resulting handler stack: 294 | // [ addName, render ] 295 | ``` 296 | 297 | It is possible to prepend and insert handlers arbitrarily into the handler 298 | stack, by using the concept of handler 'blocks'. By changing handlers to an 299 | array of arrays, we can implicitly specify ordering of different handler stacks 300 | when doing inheritance: 301 | 302 | 303 | ```js 304 | var parentConductor = rc.createConductor({ 305 | name: 'parent', 306 | handlers: [ 307 | [], 308 | addName 309 | ] 310 | }); 311 | 312 | var childConductor = rc.createConductor({ 313 | name: 'child', 314 | deps: [ parentConductor ], 315 | handlers: [ 316 | addRequestId, 317 | [], 318 | render 319 | ] 320 | }); 321 | 322 | // => resulting handler stack: 323 | // [ addRequestId, addName, render ] 324 | 325 | ``` 326 | 327 | However, using the array index as an implicit ordering mechanism can be a bit 328 | confusing, so it is recommended to use an object with numerical keys. Using 329 | numerical keys also makes it easy to insert handlers inbetween existing 330 | 'blocks'. Note that duplicated keys are appended to: 331 | 332 | 333 | ```js 334 | var parentConductor = rc.createConductor({ 335 | name: 'parent', 336 | handlers: { 337 | 10: [ addName ] 338 | } 339 | }); 340 | 341 | var childConductor = rc.createConductor({ 342 | name: 'child', 343 | deps: [ parentConductor ], 344 | handlers: { 345 | 5: [ addRequestId ], 346 | 10: [ addTimestamp ], 347 | 15: [ render ] 348 | } 349 | }); 350 | 351 | 352 | // => the merged handlers: 353 | // { 354 | // 5: [ addRequestId ], 355 | // 10: [ addName, addTimestamp ], 356 | // 15: [ render ] 357 | // 358 | // } 359 | 360 | // => and the resulting execution order of the handler stack: 361 | // [ addRequestId, addName, addTimestamp, render ] 362 | 363 | ``` 364 | 365 | 366 | ## Composition 367 | 368 | Because `deps` is an array, you can also opt for flatter trees using more 369 | compositional conductors: 370 | 371 | ```js 372 | var compositionConductor = new Conductor({ 373 | name: 'compositionConductor', 374 | deps: [ baseCondcutor, anotherConductor, yetAnotherConductor ] 375 | }); 376 | ``` 377 | 378 | Using a compositional pattern may make easier to see at a glance what the 379 | handler stacks look like, with the trade off of being slightly less DRY. It will 380 | be up to you to determine what works best for your application. 381 | 382 | In any case, both these inheritance and composition pattern allow for some very 383 | powerful constructs. It also allows you to easily move conductors from one URL 384 | path to another completely transparently. 385 | 386 | 387 | ## Conductor Sharding 388 | 389 | You may sometimes want to render a different page under the same URL. A great 390 | example is the root URL, '/'. If the user is logged in, you want to be able to 391 | serve a logged in experience. If the user is logged out, you want to serve them 392 | a login page. However, the URL needs to stay the same in both cases. 393 | 394 | restify-constructor provides this capability through sharding. Consider the two 395 | pages above, let's mount the logged in experience at /home, and the logged out 396 | experience at /login: 397 | 398 | ```js 399 | rc.get('/home', homeConductor); 400 | rc.get('/login', loginConductor); 401 | 402 | // how do we handle this scenario? 403 | // rc.get('/', ?) 404 | ``` 405 | 406 | With shards, you can reuse existing conductors by simply "sharding" to them: 407 | 408 | ```js 409 | var shardConductor = rc.createConductor({ 410 | name: 'shardConductor', 411 | models: [ userInfo ], 412 | handlers: { 413 | 10: [ 414 | rc.buildModels() 415 | ], 416 | 20: [ 417 | function shard(req, res, next) { 418 | // fetch the userInfo model we just built. 419 | var userModel = rc.getModels(req, 'userInfo'); 420 | 421 | if (userModel.data.isLoggedIn === true) { 422 | rc.shardConductor(req, homeConductor); 423 | } else { 424 | rc.shardConductor(req, loginConductor); 425 | } 426 | 427 | return next(); 428 | } 429 | ] 430 | } 431 | }) 432 | 433 | rc.get('/', shardConductor); 434 | ``` 435 | 436 | Note that in this example we sharded the conductor at index 20. That means the 437 | request will continue to flow through the handler stacks defined at 438 | homeConductor and loginConductor starting from the next index _higher_ than 20. 439 | In other words, __if homeConductor or loginConductor have handlers defined at 440 | any indicies 20 and lower, they will NOT be run__. They will only be run in 441 | the non sharded scenario, where the user directly hits /home or /login. 442 | 443 | One of the main advantages of sharding is that there is no redirect. You can 444 | serve the desired experience directly within the same request, by simply 445 | reusing existing conductors. 446 | 447 | 448 | ## API 449 | 450 | _(Coming soon)_ 451 | 452 | 453 | ## Contributing 454 | 455 | Add unit tests for any new or changed functionality. Ensure that lint and style 456 | checks pass. 457 | 458 | To start contributing, install the git preush hooks: 459 | 460 | ```sh 461 | make githooks 462 | ``` 463 | 464 | Before committing, run the prepush hook: 465 | 466 | ```sh 467 | make prepush 468 | ``` 469 | 470 | If you have style errors, you can auto fix whitespace issues by running: 471 | 472 | ```sh 473 | make codestyle-fix 474 | ``` 475 | 476 | 477 | ## License 478 | 479 | Copyright (c) 2015 Netflix, Inc. 480 | 481 | Licensed under the MIT license. 482 | -------------------------------------------------------------------------------- /api.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Table of Contents 4 | 5 | - [create][1] 6 | - [Parameters][2] 7 | - [Conductor][3] 8 | - [Parameters][4] 9 | - [name][5] 10 | - [getHandlers][6] 11 | - [Parameters][7] 12 | - [getHandlerKeys][8] 13 | - [getProps][9] 14 | - [Parameters][10] 15 | - [getProps][11] 16 | - [Parameters][12] 17 | - [createModels][13] 18 | - [Parameters][14] 19 | - [getDebugHandlerStack][15] 20 | - [buildModelsWrapper][16] 21 | - [Parameters][17] 22 | - [initWrapper][18] 23 | - [Parameters][19] 24 | - [runHandlersWrapper][20] 25 | - [createConductor][21] 26 | - [Parameters][22] 27 | - [createConductor][23] 28 | - [Parameters][24] 29 | - [createConductorHandlers][25] 30 | - [Parameters][26] 31 | - [methodInstaller][27] 32 | - [Parameters][28] 33 | - [getDefault][29] 34 | - [child][30] 35 | - [Parameters][31] 36 | - [addSerializers][32] 37 | - [Parameters][33] 38 | - [Model][34] 39 | - [Parameters][35] 40 | - [props][36] 41 | - [data][37] 42 | - [errors][38] 43 | - [log][39] 44 | - [client][40] 45 | - [type][41] 46 | - [name][42] 47 | - [async][43] 48 | - [before][44] 49 | - [Parameters][45] 50 | - [after][46] 51 | - [Parameters][47] 52 | - [isValid][48] 53 | - [Parameters][49] 54 | - [fallback][50] 55 | - [get][51] 56 | - [Parameters][52] 57 | - [get][53] 58 | - [Parameters][54] 59 | - [preConfigure][55] 60 | - [Parameters][56] 61 | - [postConfigure][57] 62 | - [Parameters][58] 63 | - [RestModel][59] 64 | - [Parameters][60] 65 | - [type][61] 66 | - [async][62] 67 | - [method][63] 68 | - [secure][64] 69 | - [host][65] 70 | - [port][66] 71 | - [baseUrl][67] 72 | - [url][68] 73 | - [qs][69] 74 | - [postBody][70] 75 | - [postType][71] 76 | - [headers][72] 77 | - [resourceType][73] 78 | - [rawResponseData][74] 79 | - [fallbackMode][75] 80 | - [getConductor][76] 81 | - [Parameters][77] 82 | - [getClient][78] 83 | - [Parameters][79] 84 | - [getModels][80] 85 | - [Parameters][81] 86 | - [getReqTimerPrefix][82] 87 | - [Parameters][83] 88 | - [setReqTimerPrefix][84] 89 | - [Parameters][85] 90 | - [setModel][86] 91 | - [Parameters][87] 92 | - [shardConductor][88] 93 | - [Parameters][89] 94 | 95 | ## create 96 | 97 | create a new restify json client. a new one is 98 | created for each outbound API request (i.e., one per model). 99 | TODO: this is potentially expensive, need to investigate 100 | creating a single client per incoming request (i.e., per user, per remote host) 101 | crux of the problem is allowing customization on a per request basis (headers), 102 | which currently requires creating a new client per model. 103 | 104 | ### Parameters 105 | 106 | - `model` **[Object][90]** a restify model 107 | 108 | Returns **[Object][90]** a restify JsonClient 109 | 110 | ## Conductor 111 | 112 | Class definition 113 | 114 | ### Parameters 115 | 116 | - `config` **[Object][90]** user configuration object. 117 | - `config.name` **[String][91]** a name for your conductor 118 | - `config.deps` **[Array][92]?** an array of dependencies to be mixed in 119 | - `config.props` **[Object][90]?** props to create for this conductor. 120 | - `config.handlers` **[Object][90]?** an object or array of handlers 121 | 122 | ### name 123 | 124 | name of the conductor 125 | 126 | Type: [String][91] 127 | 128 | ## getHandlers 129 | 130 | retrieves a handler block for a given key. 131 | 132 | ### Parameters 133 | 134 | - `key` **[String][91]** the key of the handler block 135 | 136 | Returns **[Array][92]** an array of function handlers 137 | 138 | ## getHandlerKeys 139 | 140 | retrieves the sorted handler keys for the conductor. 141 | 142 | Returns **[Array][92]** an array of strings 143 | 144 | ## getProps 145 | 146 | retrieves an immutable property. 147 | if no name passed in, return all props. 148 | 149 | ### Parameters 150 | 151 | - `name` **[String][91]?** optional name of the prop to retrieve. 152 | 153 | Returns **[Object][90]** the copy of the prop 154 | 155 | ## getProps 156 | 157 | retrieve an immutable prop off the conductor object 158 | 159 | ### Parameters 160 | 161 | - `req` **[Object][90]** the request object 162 | - `propName` **[String][91]** the name of the prop to retrieve 163 | 164 | Returns **[Object][90]** a prop value 165 | 166 | ## createModels 167 | 168 | iterates through a specific model bucket, invoking each function in the array 169 | to create a new instance of a model. does not change any state. 170 | 171 | ### Parameters 172 | 173 | - `req` **[Object][90]** the request object 174 | - `res` **[Object][90]** the response object 175 | - `bucketName` **[String][91]** the name of the model bucket to create. 176 | 177 | Returns **[Array][92]** an array of models 178 | 179 | ## getDebugHandlerStack 180 | 181 | returns a flattened list of handler stacks. 182 | for debug use only. 183 | 184 | Returns **[Array][92]** an array of function names and the index of their blocks 185 | 186 | ## buildModelsWrapper 187 | 188 | a handler to build any models defined on the conductor. 189 | 190 | ### Parameters 191 | 192 | - `modelBucket` **[Array][92]** a key we can use to look up a bucket 193 | of models defined on the conductor 194 | - `modelFetcher` **[Function][93]** function to run for fetching / creating 195 | models. The function should accept req 196 | as the first parameter, req as the 197 | second, a models array as the third, and 198 | a callback as the fourth. 199 | 200 | Returns **[undefined][94]** 201 | 202 | ## initWrapper 203 | 204 | a handler to initialize the restify conductor namespaces on the request 205 | and response objects. 206 | 207 | ### Parameters 208 | 209 | - `conductor` **[Object][90]** an instance of restify conductor 210 | 211 | Returns **void** 212 | 213 | ## runHandlersWrapper 214 | 215 | a handler to run the wrapped conductor handlers. 216 | no options at the moment. 217 | 218 | Returns **[undefined][94]** 219 | 220 | ## createConductor 221 | 222 | wrapper function for creating conductors 223 | 224 | ### Parameters 225 | 226 | - `options` **[Object][90]** an options object 227 | 228 | Returns **[Conductor][95]** a Conductor instance 229 | 230 | ## createConductor 231 | 232 | wrapper function for creating models. 233 | we MUST return a closure, this is necessary to provide 234 | req res to the lifecycle methods, and allow us to return a new model for 235 | each new incoming request. 236 | 237 | ### Parameters 238 | 239 | - `options` **[Object][90]** an options object 240 | 241 | Returns **[Function][93]** 242 | 243 | ## createConductorHandlers 244 | 245 | Create a middleware chain that executes a specific conductor 246 | 247 | ### Parameters 248 | 249 | - `conductor` **[Object][90]** a Conductor instance 250 | 251 | Returns **[Array][92]<[Function][93]>** 252 | 253 | ## methodInstaller 254 | 255 | programatically create wrapperis for Restify's server[method] 256 | 257 | ### Parameters 258 | 259 | - `opts` **([String][91] \| [Object][90])** the url of REST resource or 260 | opts to pass to Restify 261 | - `conductor` **[Conductor][95]** a conductor instance 262 | - `server` **[Object][90]** a restify server 263 | 264 | Returns **[undefined][94]** 265 | 266 | ## getDefault 267 | 268 | retrieves default restify-conductor logger 269 | 270 | Returns **[Object][90]** bunyan logger 271 | 272 | ## child 273 | 274 | creates a child logger from default restify-conductor logger 275 | 276 | ### Parameters 277 | 278 | - `name` **[String][91]** name of child logger 279 | 280 | Returns **[Object][90]** bunyan logger 281 | 282 | ## addSerializers 283 | 284 | add the restify-conductor specific serializers 285 | 286 | ### Parameters 287 | 288 | - `log` **[Object][90]** bunyan instance 289 | 290 | Returns **void** 291 | 292 | ## Model 293 | 294 | Model class. 295 | abstraction for restify-conductor models. 296 | 297 | ### Parameters 298 | 299 | - `config` **[Object][90]** model configuration object 300 | 301 | ### props 302 | 303 | arbitrary model props 304 | 305 | Type: [Object][90] 306 | 307 | ### data 308 | 309 | the model data 310 | 311 | Type: [Object][90] 312 | 313 | ### errors 314 | 315 | collected errors that may have occurred 316 | through the lifecycle methods. 317 | 318 | Type: [Array][92] 319 | 320 | ### log 321 | 322 | a bunyan instance for loggin 323 | 324 | Type: [Object][90] 325 | 326 | ### client 327 | 328 | a remote client that implements a get() method 329 | for fetching remote data 330 | 331 | Type: [Object][90] 332 | 333 | ### type 334 | 335 | model type for debugging purposes 336 | 337 | Type: [String][91] 338 | 339 | ### name 340 | 341 | model name 342 | 343 | Type: [String][91] 344 | 345 | ### async 346 | 347 | flag used to help debug. 348 | true if the model is async. 349 | 350 | Type: [Boolean][96] 351 | 352 | ## before 353 | 354 | default noop for all models. 355 | gives users a hook to modify the model 356 | before requesting it. 357 | 358 | ### Parameters 359 | 360 | - `req` **[Object][90]** the request object 361 | - `res` **[Object][90]** the response object 362 | 363 | Returns **[undefined][94]** 364 | 365 | ## after 366 | 367 | default noop for all models. 368 | gives users a hook to modify the model 369 | after getting a return value. 370 | 371 | ### Parameters 372 | 373 | - `req` **[Object][90]** the request object 374 | - `res` **[Object][90]** the response object 375 | 376 | Returns **[undefined][94]** 377 | 378 | ## isValid 379 | 380 | lifecycle method for validating returned data. 381 | 382 | ### Parameters 383 | 384 | - `data` **[Object][90]** the data to validate 385 | 386 | Returns **[Boolean][96]** 387 | 388 | ## fallback 389 | 390 | default noop for all models. 391 | gives users a hook to handle validation errors. 392 | 393 | Returns **[Object][90]** 394 | 395 | ## get 396 | 397 | public method to invoke the get of the model data. 398 | 399 | ### Parameters 400 | 401 | - `cb` **[Function][93]** callback function 402 | 403 | Returns **[undefined][94]** 404 | 405 | ## get 406 | 407 | retrieves the remote resource. 408 | 409 | ### Parameters 410 | 411 | - `callback` **[Function][93]** a callback function to invoke when complete 412 | 413 | Returns **[Object][90]** the parsed JSON response 414 | 415 | ## preConfigure 416 | 417 | public method to invoke the before chain of lifecycle events. 418 | 419 | ### Parameters 420 | 421 | - `req` **[Object][90]** the request object 422 | - `res` **[Object][90]** the response object 423 | - `options` **[Object][90]** an options object 424 | 425 | Returns **[undefined][94]** 426 | 427 | ## postConfigure 428 | 429 | public method to invoke the after chain of lifecycle events. 430 | 431 | ### Parameters 432 | 433 | - `req` **[Object][90]** the request object 434 | - `res` **[Object][90]** the response object 435 | 436 | Returns **[undefined][94]** 437 | 438 | ## RestModel 439 | 440 | RestModel class. 441 | abstraction for restify-conductor models. 442 | 443 | ### Parameters 444 | 445 | - `config` **[Object][90]** model configuration object 446 | 447 | ### type 448 | 449 | model type for debugging purposes 450 | 451 | Type: [String][91] 452 | 453 | ### async 454 | 455 | flag used to help debug. 456 | true if the model is async. 457 | 458 | Type: [Boolean][96] 459 | 460 | ### method 461 | 462 | the type of http request. defaults to GET. 463 | 464 | Type: [String][91] 465 | 466 | ### secure 467 | 468 | whether or not the request should be made over https. 469 | 470 | Type: [Boolean][96] 471 | 472 | ### host 473 | 474 | the hostname for the request 475 | 476 | Type: [String][91] 477 | 478 | ### port 479 | 480 | port number for remote host 481 | 482 | Type: [Number][97] 483 | 484 | ### baseUrl 485 | 486 | the base url of the request: 487 | [http://{hostname}/{baseurl}][98] 488 | 489 | Type: [String][91] 490 | 491 | ### url 492 | 493 | the specific url of the request: 494 | [http://{hostname}/{baseurl}/{url}][99] 495 | 496 | Type: [String][91] 497 | 498 | ### qs 499 | 500 | a query string object 501 | 502 | Type: [Object][90] 503 | 504 | ### postBody 505 | 506 | a post body object 507 | 508 | Type: [Object][90] 509 | 510 | ### postType 511 | 512 | if a post request, the post type. 513 | defafult is json, can also be 'form'. 514 | 515 | Type: [String][91] 516 | 517 | ### headers 518 | 519 | specific headers set for this model 520 | 521 | Type: [Object][90] 522 | 523 | ### resourceType 524 | 525 | the format of the returned payload. 526 | defaults to JSON, but can be XML or other. 527 | 528 | Type: [String][91] 529 | 530 | ### rawResponseData 531 | 532 | some cherry picked debug about the external 533 | resource call. 534 | 535 | Type: [Object][90] 536 | 537 | ### fallbackMode 538 | 539 | whether or not model is operating in fallback mode. 540 | 541 | Type: [Boolean][96] 542 | 543 | ## getConductor 544 | 545 | returns the conductor for a given request. 546 | 547 | ### Parameters 548 | 549 | - `req` **[Object][90]** the request object 550 | 551 | Returns **[undefined][94]** 552 | 553 | ## getClient 554 | 555 | returns a restify JSON client if one exists for this host for this incoming request. 556 | otherwise, creates one. 557 | 558 | ### Parameters 559 | 560 | - `req` **[Object][90]** the request object 561 | - `model` **[Object][90]** a restify model 562 | 563 | Returns **[Object][90]** a restify JSON client 564 | 565 | ## getModels 566 | 567 | gets all the saved models off the request 568 | 569 | ### Parameters 570 | 571 | - `req` **[Object][90]** the request object 572 | - `modelName` **[String][91]?** name of the model to retrieve. returns all models if not specified. 573 | 574 | Returns **([Object][90] \| [Array][92])** returns an array of models, or just one model. 575 | 576 | ## getReqTimerPrefix 577 | 578 | gets the current request timer prefix name. 579 | useful for using it to prefix other request timers. 580 | 581 | ### Parameters 582 | 583 | - `req` **[Object][90]** the request object 584 | 585 | Returns **[String][91]** 586 | 587 | ## setReqTimerPrefix 588 | 589 | sets the current timer name prefix. 590 | 591 | ### Parameters 592 | 593 | - `req` **[Object][90]** the request object 594 | - `prefix` **[String][91]** the timer name prefix 595 | 596 | Returns **[undefined][94]** 597 | 598 | ## setModel 599 | 600 | saves a model onto the request 601 | 602 | ### Parameters 603 | 604 | - `req` **[Object][90]** the request object 605 | - `model` **[Object][90]** an instance of a Model or RestModel. 606 | 607 | Returns **[undefined][94]** 608 | 609 | ## shardConductor 610 | 611 | replace an conductor midstream with a .createAction 612 | 613 | ### Parameters 614 | 615 | - `req` **[Object][90]** the request object 616 | - `newConductor` **[Object][90]** a Conductor 617 | 618 | Returns **[undefined][94]** 619 | 620 | [1]: #create 621 | 622 | [2]: #parameters 623 | 624 | [3]: #conductor 625 | 626 | [4]: #parameters-1 627 | 628 | [5]: #name 629 | 630 | [6]: #gethandlers 631 | 632 | [7]: #parameters-2 633 | 634 | [8]: #gethandlerkeys 635 | 636 | [9]: #getprops 637 | 638 | [10]: #parameters-3 639 | 640 | [11]: #getprops-1 641 | 642 | [12]: #parameters-4 643 | 644 | [13]: #createmodels 645 | 646 | [14]: #parameters-5 647 | 648 | [15]: #getdebughandlerstack 649 | 650 | [16]: #buildmodelswrapper 651 | 652 | [17]: #parameters-6 653 | 654 | [18]: #initwrapper 655 | 656 | [19]: #parameters-7 657 | 658 | [20]: #runhandlerswrapper 659 | 660 | [21]: #createconductor 661 | 662 | [22]: #parameters-8 663 | 664 | [23]: #createconductor-1 665 | 666 | [24]: #parameters-9 667 | 668 | [25]: #createconductorhandlers 669 | 670 | [26]: #parameters-10 671 | 672 | [27]: #methodinstaller 673 | 674 | [28]: #parameters-11 675 | 676 | [29]: #getdefault 677 | 678 | [30]: #child 679 | 680 | [31]: #parameters-12 681 | 682 | [32]: #addserializers 683 | 684 | [33]: #parameters-13 685 | 686 | [34]: #model 687 | 688 | [35]: #parameters-14 689 | 690 | [36]: #props 691 | 692 | [37]: #data 693 | 694 | [38]: #errors 695 | 696 | [39]: #log 697 | 698 | [40]: #client 699 | 700 | [41]: #type 701 | 702 | [42]: #name-1 703 | 704 | [43]: #async 705 | 706 | [44]: #before 707 | 708 | [45]: #parameters-15 709 | 710 | [46]: #after 711 | 712 | [47]: #parameters-16 713 | 714 | [48]: #isvalid 715 | 716 | [49]: #parameters-17 717 | 718 | [50]: #fallback 719 | 720 | [51]: #get 721 | 722 | [52]: #parameters-18 723 | 724 | [53]: #get-1 725 | 726 | [54]: #parameters-19 727 | 728 | [55]: #preconfigure 729 | 730 | [56]: #parameters-20 731 | 732 | [57]: #postconfigure 733 | 734 | [58]: #parameters-21 735 | 736 | [59]: #restmodel 737 | 738 | [60]: #parameters-22 739 | 740 | [61]: #type-1 741 | 742 | [62]: #async-1 743 | 744 | [63]: #method 745 | 746 | [64]: #secure 747 | 748 | [65]: #host 749 | 750 | [66]: #port 751 | 752 | [67]: #baseurl 753 | 754 | [68]: #url 755 | 756 | [69]: #qs 757 | 758 | [70]: #postbody 759 | 760 | [71]: #posttype 761 | 762 | [72]: #headers 763 | 764 | [73]: #resourcetype 765 | 766 | [74]: #rawresponsedata 767 | 768 | [75]: #fallbackmode 769 | 770 | [76]: #getconductor 771 | 772 | [77]: #parameters-23 773 | 774 | [78]: #getclient 775 | 776 | [79]: #parameters-24 777 | 778 | [80]: #getmodels 779 | 780 | [81]: #parameters-25 781 | 782 | [82]: #getreqtimerprefix 783 | 784 | [83]: #parameters-26 785 | 786 | [84]: #setreqtimerprefix 787 | 788 | [85]: #parameters-27 789 | 790 | [86]: #setmodel 791 | 792 | [87]: #parameters-28 793 | 794 | [88]: #shardconductor 795 | 796 | [89]: #parameters-29 797 | 798 | [90]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object 799 | 800 | [91]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String 801 | 802 | [92]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array 803 | 804 | [93]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function 805 | 806 | [94]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/undefined 807 | 808 | [95]: #conductor 809 | 810 | [96]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean 811 | 812 | [97]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number 813 | 814 | [98]: http://{hostname}/{baseurl} 815 | 816 | [99]: http://{hostname}/{baseurl}/{url} 817 | -------------------------------------------------------------------------------- /example/conductors/handlerConductor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var rc = require('../../lib'); 4 | 5 | // a super simple conductor that has only one handler on it: 6 | // render a string 'hello world' to the client. 7 | 8 | module.exports = rc.createConductorHandlers( 9 | rc.createConductor({ 10 | name: 'simpleConductor', 11 | handlers: [ 12 | function render(req, res, next) { 13 | res.send(200, 'hello world!'); 14 | return next(); 15 | } 16 | ] 17 | }) 18 | ); 19 | -------------------------------------------------------------------------------- /example/conductors/inheritance/inheritanceConductor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var rc = require('../../../lib'); 4 | var propsConductor = require('../props/propsConductor').propsConductor; 5 | 6 | // this conductor inherits from propsConductor. 7 | // that means we'll get all the props and handlers for free, 8 | // and the output is exactly the same. 9 | 10 | module.exports = rc.createConductor({ 11 | name: 'inheritanceConductor', 12 | deps: [propsConductor] 13 | }); 14 | -------------------------------------------------------------------------------- /example/conductors/inheritance/inheritanceConductor2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var rc = require('../../../lib'); 4 | var propsConductor = require('../props/propsConductor').propsConductor; 5 | 6 | // while inheriting, we can also modify the inherited 7 | // props if we want to change them. 8 | 9 | module.exports = rc.createConductor({ 10 | name: 'inheritanceConductor2', 11 | deps: [propsConductor], 12 | props: function(inheritedProps) { 13 | // in this case, we want to change the list of blacklisted 14 | // queries from our original props conductor. 15 | inheritedProps.blacklistedQueries = ['override']; 16 | return inheritedProps; 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /example/conductors/inheritance/inheritanceConductor3.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var rc = require('../../../lib'); 4 | var propsConductor = require('../props/propsConductor').propsConductor; 5 | 6 | // while inheriting, we can also append to the existing handler stack. 7 | 8 | module.exports = rc.createConductor({ 9 | name: 'inheritanceConductor3', 10 | deps: [propsConductor], 11 | handlers: [ 12 | [ 13 | function postRender(req, res, next) { 14 | req.log.info( 15 | 'Not much we can do here since we already rendered \ 16 | the response.' 17 | ); 18 | return next(); 19 | } 20 | ] 21 | ] 22 | }); 23 | -------------------------------------------------------------------------------- /example/conductors/inheritance/inheritanceConductor4.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var rc = require('../../../lib'); 4 | 5 | // what if we want to prepend to the existing stack? 6 | // we'll have to build our original conductor slightly differently. 7 | 8 | var parentConductor = rc.createConductor({ 9 | name: 'parentInheritanceConductor', 10 | handlers: [ 11 | [], // empty array, on purpose 12 | [ 13 | function render(req, res, next) { 14 | // render 15 | res.send(200, 'Name: ' + req.name, next); 16 | return next(); 17 | } 18 | ] 19 | ] 20 | }); 21 | 22 | // so our conductor is now inheriting from parent conductor. 23 | // it will line up the array of arrays in the handlers and concat the arrays. 24 | // that means the concatted handlers look like this: 25 | // [ 26 | // [ timestamp ], 27 | // [ render ] 28 | // ] 29 | // 30 | // and when it's flattened, the handler stack looks like: 31 | // [ timestamp, render ] 32 | 33 | module.exports = rc.createConductor({ 34 | name: 'inheritanceConductor4', 35 | deps: [parentConductor], 36 | handlers: [ 37 | [ 38 | function addName(req, res, next) { 39 | req.name = 'inheritanceConductor4'; 40 | return next(); 41 | } 42 | ], 43 | [] 44 | ] 45 | }); 46 | -------------------------------------------------------------------------------- /example/conductors/inheritance/inheritanceConductor5.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var rc = require('../../../lib'); 4 | 5 | // leaving empty arrays in your handlers is a bit silly though, 6 | // and really hard to plan ahead. 7 | // why not use numerical keys? 8 | 9 | var parentConductor = rc.createConductor({ 10 | name: 'parentInheritanceConductor', 11 | handlers: { 12 | 10: [ 13 | function timestamp(req, res, next) { 14 | req.data = { 15 | timestamp: new Date() 16 | }; 17 | return next(); 18 | } 19 | ], 20 | 30: [ 21 | function render(req, res, next) { 22 | // render 23 | res.send(200, req.data); 24 | return next(); 25 | } 26 | ] 27 | } 28 | }); 29 | 30 | // we can use keys to random insert/append/prepend handlers in our stack. 31 | // numerical keys that are duplicated get appended to by child conductors. 32 | // that means the concatted handlers look like this: 33 | // [ 34 | // 10: [ timestamp, dataA ], 35 | // 20: [ dataB ], 36 | // 30: [ render ] 37 | // ] 38 | // 39 | // and when it's flattened, the handler stack looks like: 40 | // [ timestamp, dataA, dataB, render ] 41 | 42 | module.exports = rc.createConductor({ 43 | name: 'inheritanceConductor4', 44 | deps: [parentConductor], 45 | handlers: { 46 | 10: [ 47 | function dataA(req, res, next) { 48 | req.data.a = 1; 49 | return next(); 50 | } 51 | ], 52 | 20: [ 53 | function dataB(req, res, next) { 54 | req.data.b = 2; 55 | return next(); 56 | } 57 | ] 58 | } 59 | }); 60 | -------------------------------------------------------------------------------- /example/conductors/models/modelConductor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var assert = require('assert-plus'); 5 | 6 | var rc = require('../../../lib'); 7 | var userAgent = require('../../models/userAgent'); 8 | var serverEnv = require('../../models/serverEnv'); 9 | 10 | // models are also a first class concept. each conductor can specify a set of 11 | // 'models', which are addressable buckets of data. 12 | 13 | module.exports = rc.createConductor({ 14 | name: 'modelConductor', 15 | models: [userAgent, serverEnv], 16 | handlers: [ 17 | [ 18 | // this is built in handler available to you. 19 | // it is a handler that builds all the models. 20 | rc.handlers.buildModels(), 21 | function render(req, res, next) { 22 | // once the models are built, let's get 23 | // their contents. we can get models by directly 24 | // using their names: 25 | 26 | var userAgentModel = rc.getModels(req, 'userAgent'); 27 | var serverEnvModel = rc.getModels(req, 'serverEnv'); 28 | 29 | // or we can pass no model names, and get back an array of all 30 | // models 31 | var allModels = rc.getModels(req); 32 | 33 | // these assertion statements are just to show the getters 34 | // working as expected. 35 | assert.equal(userAgentModel, allModels.userAgent); 36 | assert.equal(serverEnvModel, allModels.serverEnv); 37 | 38 | // put together a payload by looping through all the models, 39 | // and creating a key/val pair of model names to their 40 | // contents. 41 | var out = _.reduce( 42 | allModels, 43 | function(acc, model) { 44 | acc[model.name] = model.data; 45 | return acc; 46 | }, 47 | {} 48 | ); 49 | 50 | // render the model data 51 | res.send(200, out); 52 | return next(); 53 | } 54 | ] 55 | ] 56 | }); 57 | -------------------------------------------------------------------------------- /example/conductors/models/modelConductor2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var assert = require('assert-plus'); 5 | var rc = require('../../../lib'); 6 | var userAgent = require('../../models/userAgent'); 7 | var serverEnv = require('../../models/serverEnv'); 8 | 9 | // models are also a first class concept. 10 | // each conductor can specify a set of 'models', 11 | // which are addressable buckets of data. 12 | 13 | module.exports = rc.createConductor({ 14 | name: 'modelConductor2', 15 | models: { 16 | basic: [userAgent, serverEnv] 17 | }, 18 | handlers: [ 19 | [ 20 | // this is built in handler available to you. 21 | // it is a handler that builds all the models 22 | // defined on the 'basic' key of the models config. 23 | // models have their own lifecycle methods. 24 | rc.handlers.buildModels('basic'), 25 | function render(req, res, next) { 26 | // once the models are built, let's get 27 | // their contents. we can get models by directly 28 | // using their names: 29 | 30 | var userAgentModel = rc.getModels(req, 'userAgent'); 31 | var serverEnvModel = rc.getModels(req, 'serverEnv'); 32 | 33 | // or we can pass no model names, and get back an array of all models 34 | var allModels = rc.getModels(req); 35 | 36 | // these assertion statements are just to show the getters 37 | // working as expected. 38 | assert.equal(userAgentModel, allModels.userAgent); 39 | assert.equal(serverEnvModel, allModels.serverEnv); 40 | 41 | // put together a payload by looping through all the models, 42 | // and creating a key/val pair of model names to their 43 | // contents. 44 | var out = _.reduce( 45 | allModels, 46 | function(acc, model) { 47 | acc[model.name] = model.data; 48 | return acc; 49 | }, 50 | {} 51 | ); 52 | 53 | // render the model data 54 | res.send(200, out); 55 | return next(); 56 | } 57 | ] 58 | ] 59 | }); 60 | -------------------------------------------------------------------------------- /example/conductors/models/modelConductor3.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var rc = require('../../../lib'); 5 | var ip = require('../../models/ip'); 6 | var posts = require('../../models/posts'); 7 | 8 | // now let's build an conductor that fetches 9 | // models from a remote data source. 10 | // the ip model data 11 | 12 | module.exports = rc.createConductor({ 13 | name: 'modelConductor3', 14 | models: { 15 | // this time, we're fetching a 'remote' model, which 16 | // means it is async. all models specified in the 17 | // key here must complete before we render. 18 | // these models are built in parallel! 19 | basic: [ip, posts] 20 | }, 21 | handlers: [ 22 | [ 23 | rc.handlers.buildModels('basic'), 24 | function render(req, res, next) { 25 | var allModels = rc.getModels(req); 26 | 27 | // put together a payload by looping through all the models, 28 | // and creating a key/val pair of model names to their 29 | // contents. 30 | var out = _.reduce( 31 | allModels, 32 | function(acc, model) { 33 | acc[model.name] = model.data; 34 | return acc; 35 | }, 36 | {} 37 | ); 38 | 39 | // render the model data 40 | res.send(200, out); 41 | return next(); 42 | } 43 | ] 44 | ] 45 | }); 46 | -------------------------------------------------------------------------------- /example/conductors/models/modelConductor4.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var rc = require('../../../lib'); 4 | 5 | // you can also create custom models that can do anything. 6 | // just implement a get() method, which gives you a callback on completion. 7 | 8 | var asyncModel = rc.createModel({ 9 | name: 'asyncModel', 10 | get: function(cb) { 11 | setTimeout(function fakeAsync() { 12 | cb(null, { 13 | hello: 'world', 14 | async: true 15 | }); 16 | }, 1000); 17 | } 18 | }); 19 | 20 | module.exports = rc.createConductor({ 21 | name: 'modelConductor4', 22 | models: [asyncModel], 23 | handlers: [ 24 | [ 25 | rc.handlers.buildModels(), 26 | function render(req, res, next) { 27 | var model = rc.getModels(req, 'asyncModel'); 28 | 29 | // render the model data 30 | res.send(200, model.data); 31 | return next(); 32 | } 33 | ] 34 | ] 35 | }); 36 | -------------------------------------------------------------------------------- /example/conductors/models/modelConductor5.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var assert = require('assert-plus'); 5 | var rc = require('../../../lib'); 6 | var md5 = require('../../models/ip'); 7 | var posts = require('../../models/posts'); 8 | var userAgent = require('../../models/userAgent'); 9 | 10 | // it is possible to fetch multiple async models in series. 11 | // while this not desirable from a perf persepctive, it is sometimes necessary. 12 | // by using an object instead of array for the models configuration, 13 | // you can specify different 'buckets' of models to fetch. 14 | 15 | module.exports = rc.createConductor({ 16 | name: 'modelConductor4', 17 | models: { 18 | bucketA: [md5, userAgent], 19 | bucketB: [posts] 20 | }, 21 | handlers: [ 22 | rc.handlers.buildModels('bucketA'), 23 | function check(req, res, next) { 24 | var ipModel = rc.getModels(req, 'ip'); 25 | var uaModel = rc.getModels(req, 'userAgent'); 26 | 27 | // the md5 and user agent models are done! 28 | assert.ok(ipModel); 29 | assert.ok(uaModel); 30 | 31 | return next(); 32 | }, 33 | rc.handlers.buildModels('bucketB'), 34 | function render(req, res, next) { 35 | var allModels = rc.getModels(req); 36 | 37 | // make sure we got three models 38 | assert.equal(_.size(allModels), 3); 39 | 40 | // put together a payload by looping through all the models, 41 | // and creating a key/val pair of model names to their 42 | // contents. 43 | var out = _.reduce( 44 | allModels, 45 | function(acc, model) { 46 | acc[model.name] = model.data; 47 | return acc; 48 | }, 49 | {} 50 | ); 51 | 52 | // render the model data 53 | res.send(200, out); 54 | return next(); 55 | } 56 | ] 57 | }); 58 | -------------------------------------------------------------------------------- /example/conductors/props/propsConductor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var rc = require('../../../lib'); 4 | var assert = require('assert-plus'); 5 | var restifyErrors = require('restify-errors'); 6 | 7 | // immutable properties are useful as a way to expose 8 | // a set of data that differs from conductor to conductor. 9 | 10 | // props are initialized via a function, which returns 11 | // an object. the object returned will be 'frozen' and 12 | // become immutable. 13 | 14 | // props can then be accessed in your handlers via 15 | // methods provided to you. 16 | 17 | // in this way, we can share handlers between conductors, 18 | // but work against a known set of props. 19 | 20 | function validateQuery(req, res, next) { 21 | // in this example, we take a query param and validate it 22 | // against the conductor props. 23 | var query = req.query.search; 24 | 25 | // props can be retrieved via getters 26 | var allProps = rc.getProps(req); 27 | // => { foo: 'bar', baz: 'qux', blacklistedQueries: [ 'foo', 'bar' ] } 28 | 29 | // some assertion statements to show the getters 30 | // are working as expected. 31 | assert.equal(allProps.foo, rc.getProps(req, 'foo')); 32 | assert.equal(allProps.baz, rc.getProps(req, 'baz')); 33 | 34 | // if the passed in query was blacklisted, return a 500 35 | var blacklistedQueries = rc.getProps(req, 'blacklistedQueries'); 36 | 37 | if (blacklistedQueries.indexOf(query) !== -1) { 38 | var err = new restifyErrors.BadRequestError('query not allowed!'); 39 | return next(err); 40 | } 41 | return next(); 42 | } 43 | 44 | function render(req, res, next) { 45 | var searchQuery = req.query.search || 'no query specified'; 46 | 47 | // render 48 | res.send(200, 'searchQuery: ' + searchQuery); 49 | return next(); 50 | } 51 | 52 | module.exports.propsConductor = rc.createConductor({ 53 | name: 'propsConductor', 54 | props: function() { 55 | return { 56 | foo: 'bar', 57 | baz: 'qux', 58 | blacklistedQueries: ['foo', 'bar'] 59 | }; 60 | }, 61 | handlers: [[validateQuery, render]] 62 | }); 63 | 64 | module.exports.propsConductor2 = rc.createConductor({ 65 | name: 'propsConductor2', 66 | props: function() { 67 | return { 68 | foo: 'bar', 69 | baz: 'qux', 70 | blacklistedQueries: ['baz', 'qux'] 71 | }; 72 | }, 73 | handlers: [[validateQuery, render]] 74 | }); 75 | -------------------------------------------------------------------------------- /example/conductors/sharding/shardConductor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var rc = require('../../../lib'); 4 | 5 | // sharding enables us to serve URLs that "shard" 6 | // from one conductor into another dynamically. 7 | // 8 | // That means conductorA might be servicing a request at first, 9 | // but that can shard into conductorB halfway through the 10 | // request chain. 11 | 12 | var shardTextConductor = rc.createConductor({ 13 | name: 'shardTextConductor', 14 | handlers: { 15 | 10: [ 16 | function addName(req, res, next) { 17 | req.name = 'text'; 18 | return next(); 19 | } 20 | ], 21 | 20: [ 22 | function render(req, res, next) { 23 | res.send(200, 'name: ' + req.name); 24 | return next(); 25 | } 26 | ] 27 | } 28 | }); 29 | 30 | var shardJsonConductor = rc.createConductor({ 31 | name: 'shardJsonConductor', 32 | handlers: { 33 | 10: [ 34 | function addName(req, res, next) { 35 | req.name = 'json'; 36 | return next(); 37 | } 38 | ], 39 | 20: [ 40 | function render(req, res, next) { 41 | res.send(200, { 42 | name: req.name 43 | }); 44 | return next(); 45 | } 46 | ] 47 | } 48 | }); 49 | 50 | var nextLevelShardJsonConductor = rc.createConductor({ 51 | name: 'nextLevelShardJsonConductor', 52 | handlers: { 53 | 15: [ 54 | function addName(req, res, next) { 55 | req.name = 'json'; 56 | return next(); 57 | } 58 | ], 59 | 20: [ 60 | function render(req, res, next) { 61 | res.send(200, { 62 | name: req.name 63 | }); 64 | return next(); 65 | } 66 | ] 67 | } 68 | }); 69 | 70 | var nextLevelNotSameLevel = rc.createConductor({ 71 | name: 'nextLevelShardJsonConductor', 72 | handlers: { 73 | 15: [ 74 | function addName(req, res, next) { 75 | req.name = 'json'; 76 | return next(); 77 | } 78 | ], 79 | 21: [ 80 | function render(req, res, next) { 81 | res.send(200, { 82 | name: req.name 83 | }); 84 | return next(); 85 | } 86 | ] 87 | } 88 | }); 89 | 90 | var noHandlers = rc.createConductor({ 91 | name: 'noHandlers', 92 | handlers: {} 93 | }); 94 | 95 | var shardMap = { 96 | text: shardTextConductor, 97 | json: shardJsonConductor, 98 | nextLevelJson: nextLevelShardJsonConductor, 99 | nextLevelNotSameLevel: nextLevelNotSameLevel, 100 | noHandlers: noHandlers 101 | }; 102 | 103 | module.exports.shardText = shardTextConductor; 104 | module.exports.shardJson = shardJsonConductor; 105 | module.exports.shard = rc.createConductor({ 106 | name: 'shardConductor', 107 | handlers: { 108 | 9: [ 109 | function name(req, res, next) { 110 | req.name = 'preshard'; 111 | return next(); 112 | } 113 | ], 114 | 10: [ 115 | function shard(req, res, next) { 116 | var type = req.query.type; 117 | 118 | var finalConductor = shardMap[type]; 119 | 120 | // here, based on some conditional, we can 121 | // choose to "shard" into another conductor. 122 | // the handler chain will pick up where 123 | // it left off numerically. in this case, 10. 124 | rc.shardConductor(req, finalConductor); 125 | return next(); 126 | } 127 | ], 128 | 20: [ 129 | function nothingHere(req, res, next) { 130 | // Not calling next! 131 | } 132 | ] 133 | } 134 | }); 135 | -------------------------------------------------------------------------------- /example/conductors/simple/simpleConductor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var rc = require('../../../lib'); 4 | 5 | // a super simple conductor that has only one handler on it: 6 | // render a string 'hello world' to the client. 7 | 8 | module.exports = rc.createConductor({ 9 | name: 'simpleConductor', 10 | handlers: [ 11 | function render(req, res, next) { 12 | res.send(200, 'hello world!'); 13 | return next(); 14 | } 15 | ] 16 | }); 17 | -------------------------------------------------------------------------------- /example/conductors/simple/simpleConductor2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var rc = require('../../../lib'); 4 | 5 | // like any other framework, you can pass in an array 6 | // of functions. 7 | 8 | module.exports = rc.createConductor({ 9 | name: 'simpleConductor2', 10 | handlers: [ 11 | function addName(req, res, next) { 12 | // put a random attribute on the request 13 | req.name = 'simpleConductor2'; 14 | return next(); 15 | }, 16 | function render(req, res, next) { 17 | res.send(200, 'hello world: ' + req.name + '!'); 18 | return next(); 19 | } 20 | ] 21 | }); 22 | -------------------------------------------------------------------------------- /example/conductors/simple/simpleConductor3.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var rc = require('../../../lib'); 4 | 5 | // you can also pass in an array of arrays. 6 | // this becomes extremely useful when working 7 | // with inherited conductors. 8 | // the array index determines order of execution. 9 | 10 | module.exports = rc.createConductor({ 11 | name: 'simpleConductor3', 12 | handlers: [ 13 | [ 14 | function addName(req, res, next) { 15 | // put a random attribute on the request 16 | req.name = 'simpleConductor3'; 17 | return next(); 18 | }, 19 | function addMessage(req, res, next) { 20 | // put a random attribute on the request 21 | req.message = 'success'; 22 | return next(); 23 | } 24 | ], 25 | [ 26 | function render(req, res, next) { 27 | res.send( 28 | 200, 29 | 'hello world: ' + req.name + ' ' + req.message + '!' 30 | ); 31 | return next(); 32 | } 33 | ] 34 | ] 35 | }); 36 | -------------------------------------------------------------------------------- /example/conductors/simple/simpleConductor4.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var rc = require('../../../lib'); 4 | 5 | // an conductor that redirect. handler chain stops executing. 6 | 7 | module.exports = rc.createConductor({ 8 | name: 'simpleConductor4', 9 | handlers: [ 10 | function redirect(req, res, next) { 11 | // redirect the page! 12 | res.redirect('/simple1', next); 13 | }, 14 | function render(req, res, next) { 15 | // this should never get executed! 16 | res.send(200, 'hello world!'); 17 | return next(); 18 | } 19 | ] 20 | }); 21 | -------------------------------------------------------------------------------- /example/demo.js: -------------------------------------------------------------------------------- 1 | /* 2 | * restify-conductor 3 | * an abstraction framework for building composable endpoints in restify 4 | * 5 | * This is a demo! Please run this with bunyan: 6 | * `node demo.js | bunyan` 7 | * 8 | * Licensed under the MIT license. 9 | */ 10 | 11 | 'use strict'; 12 | 13 | var rc = require('../lib'); 14 | var bunyan = require('bunyan'); 15 | var restify = require('restify'); 16 | 17 | // conductors 18 | // jscs:disable maximumLineLength 19 | var simpleConductor = require('./conductors/simple/simpleConductor'); 20 | var simpleConductor2 = require('./conductors/simple/simpleConductor2'); 21 | var simpleConductor3 = require('./conductors/simple/simpleConductor3'); 22 | var simpleConductor4 = require('./conductors/simple/simpleConductor4'); 23 | var propsConductor = require('./conductors/props/propsConductor'); 24 | var modelConductor = require('./conductors/models/modelConductor'); 25 | var modelConductor2 = require('./conductors/models/modelConductor2'); 26 | var modelConductor3 = require('./conductors/models/modelConductor3'); 27 | var modelConductor4 = require('./conductors/models/modelConductor4'); 28 | var modelConductor5 = require('./conductors/models/modelConductor5'); 29 | var inheritanceConductor = require('./conductors/inheritance/inheritanceConductor'); 30 | var inheritanceConductor2 = require('./conductors/inheritance/inheritanceConductor2'); 31 | var inheritanceConductor3 = require('./conductors/inheritance/inheritanceConductor3'); 32 | var inheritanceConductor4 = require('./conductors/inheritance/inheritanceConductor4'); 33 | var inheritanceConductor5 = require('./conductors/inheritance/inheritanceConductor5'); 34 | var shardConductor = require('./conductors/sharding/shardConductor'); 35 | var simpleConductorHandlers = require('./conductors/handlerConductor'); 36 | // jscs:enable maximumLineLength 37 | 38 | // create a server 39 | var logger = bunyan.createLogger({ 40 | name: 'demo-server', 41 | level: process.env.LOG_LEVEL 42 | }); 43 | 44 | var demoServer = restify.createServer({ 45 | name: 'test-server', 46 | log: logger 47 | }); 48 | 49 | // set up auditing, error handling 50 | demoServer.on( 51 | 'after', 52 | restify.plugins.auditLogger({ 53 | log: logger.child({ component: 'restify-audit' }), 54 | event: 'after' 55 | }) 56 | ); 57 | 58 | // handle uncaught exceptions 59 | demoServer.on('uncaughtException', function(req, res, route, err) { 60 | err.domain = null; 61 | req.log.error( 62 | { 63 | err: err, 64 | stack: err.stack 65 | }, 66 | 'Uncaught exception!' 67 | ); 68 | }); 69 | 70 | // set up server 71 | demoServer.pre(restify.plugins.pre.sanitizePath()); 72 | demoServer.use(restify.plugins.queryParser()); 73 | demoServer.use(restify.plugins.requestLogger()); 74 | 75 | // simple examples 76 | rc.get('/simple', simpleConductor, demoServer); 77 | rc.get('/simple2', simpleConductor2, demoServer); 78 | rc.get('/simple3', simpleConductor3, demoServer); 79 | rc.get('/simple4', simpleConductor4, demoServer); 80 | 81 | // props examples 82 | rc.get('/props', propsConductor.propsConductor, demoServer); 83 | rc.get('/props2', propsConductor.propsConductor2, demoServer); 84 | 85 | // model examples 86 | rc.get('/model', modelConductor, demoServer); 87 | rc.get('/model2', modelConductor2, demoServer); 88 | rc.get('/model3', modelConductor3, demoServer); 89 | rc.get('/model4', modelConductor4, demoServer); 90 | rc.get('/model5', modelConductor5, demoServer); 91 | 92 | // inheritance examples 93 | rc.get('/inherit', inheritanceConductor, demoServer); 94 | rc.get('/inherit2', inheritanceConductor2, demoServer); 95 | rc.get('/inherit3', inheritanceConductor3, demoServer); 96 | rc.get('/inherit4', inheritanceConductor4, demoServer); 97 | rc.get('/inherit5', inheritanceConductor5, demoServer); 98 | 99 | // sharding examples 100 | rc.get('/shard', shardConductor.shard, demoServer); 101 | rc.get('/shardText', shardConductor.shardText, demoServer); 102 | rc.get('/shardJson', shardConductor.shardJson, demoServer); 103 | 104 | // Object examples 105 | rc.get({ path: '/object' }, simpleConductor, demoServer); 106 | rc.get({ path: '/Object1' }, simpleConductor2, demoServer); 107 | rc.get({ path: '/Object2' }, simpleConductor2, demoServer); 108 | rc.get({ path: '/OBJECT3' }, simpleConductor2, demoServer); 109 | 110 | demoServer.get('/simpleConductorHandlers', simpleConductorHandlers); 111 | 112 | module.exports = demoServer; 113 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var demoServer = require('./demo'); 4 | 5 | // now take traffic! 6 | demoServer.listen(3003, function() { 7 | console.info('listening at 3003'); // eslint-disable-line no-console 8 | }); 9 | -------------------------------------------------------------------------------- /example/models/ip.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var rc = require('../../lib/'); 4 | 5 | // a RestModel is like a model, except it gets a data 6 | // from a remote resource. like the regular model, 7 | // life cycle methods are available to you. 8 | // before() is called the outbound request is made 9 | // isValid() is called right after, 10 | // after() is called validation is successful 11 | 12 | module.exports = rc.createModel({ 13 | name: 'ip', 14 | host: 'jsonip.com', 15 | secure: true, 16 | isValid: function(data) { 17 | // validate the payload coming back. 18 | return data.hasOwnProperty('ip') && data.hasOwnProperty('about'); 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /example/models/posts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var rc = require('../../lib/'); 4 | 5 | // a RestModel is like a model, except it gets a data 6 | // from a remote resource. like the regular model, 7 | // life cycle methods are available to you. 8 | // before() is called the outbound request is made 9 | // isValid() is called right after, 10 | // after() is called validation is successful 11 | 12 | module.exports = rc.createModel({ 13 | name: 'posts', 14 | host: 'jsonplaceholder.typicode.com', 15 | url: '/posts', 16 | before: function(req, res) { 17 | // if the user passed in something as a query param 18 | // to be hashed, used that instead! 19 | this.qs.userId = req.query.userId || 1; 20 | }, 21 | isValid: function(data) { 22 | // validate the payload coming back. 23 | return Array.isArray(data) && data.length > 0; 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /example/models/serverEnv.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var rc = require('../../lib/'); 4 | 5 | // models are actually functions that return a new model. 6 | // this allows us to create new models easily for every 7 | // new incoming request. 8 | 9 | module.exports = rc.createModel({ 10 | name: 'serverEnv', 11 | before: function(req, res) { 12 | // you can also use a function to set 13 | // the data fields of the model. 14 | this.data = { 15 | env: process.env.NODE_ENV || 'development', 16 | osUser: process.env.user, 17 | shellPath: process.env.PATH || '', 18 | pwd: process.env.PWD 19 | }; 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /example/models/userAgent.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var rc = require('../../lib/'); 4 | 5 | // models are actually functions that return a new model. 6 | // this allows us to create new models easily for every 7 | // new incoming request. 8 | 9 | module.exports = rc.createModel({ 10 | name: 'userAgent', 11 | before: function(req, res) { 12 | this.data = req.headers['user-agent']; 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /lib/Conductor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // external modules 4 | var _ = require('lodash'); 5 | var immutable = require('immutable'); 6 | var assert = require('assert-plus'); 7 | var Toposort = require('toposort-class'); 8 | 9 | // internal files 10 | var helpers = require('./helpers'); 11 | 12 | //------------------------------------------------------------------------------ 13 | // class definition 14 | //------------------------------------------------------------------------------ 15 | 16 | /** 17 | * Class definition 18 | * @public 19 | * @class 20 | * @param {Object} config user configuration object. 21 | * @param {String} config.name a name for your conductor 22 | * @param {Array} [config.deps] an array of dependencies to be mixed in 23 | * @param {Object} [config.props] props to create for this conductor. 24 | * @param {Object} [config.handlers] an object or array of handlers 25 | */ 26 | function Conductor(config) { 27 | var self = this; 28 | 29 | // assert required options 30 | assert.object(config, 'config'); 31 | assert.string(config.name, 'config.name'); 32 | 33 | // assert optional 34 | assert.optionalArrayOfObject(config.deps, 'config.deps'); 35 | 36 | // since models and handlers can be either objects or arrays, how do we 37 | // assert them? optionalArrayOrObject? 38 | 39 | //---------------------------------------------------------- 40 | // initialize public properties 41 | //---------------------------------------------------------- 42 | 43 | /** 44 | * name of the conductor 45 | * @public 46 | * @type {String} 47 | */ 48 | this.name = config.name; 49 | 50 | //---------------------------------------------------------- 51 | // initialize private properties 52 | //---------------------------------------------------------- 53 | 54 | /** 55 | * map of conductor names to conductor objects for all dependent conductors. 56 | * @private 57 | * @type {Object} 58 | */ 59 | this._registry = {}; 60 | 61 | /** 62 | * an array of conductor names, in order, for the flattened dependency 63 | * conductors. 64 | * @private 65 | * @type {Array} 66 | */ 67 | this._sortedDeps = []; 68 | 69 | /** 70 | * the finalized version of props used by the conductor. 71 | * serves as an accumulator as we traverse the dependency tree, and is then 72 | * frozen and becomes immutable as the constructor finishes up. 73 | * @private 74 | * @type {Object} 75 | */ 76 | this._props = {}; 77 | 78 | /** 79 | * An object where keys are numbers and vals are an array of functions. 80 | * each array is a mini handler array, or handler block. 81 | * created by mergeHandlers(). 82 | * @private 83 | * @type {Object} 84 | */ 85 | this._handlers = {}; 86 | 87 | /** 88 | * the finalized version of models used by the conductor. 89 | * includes all models passed in, plus any potentially inherited models. 90 | * created by mergeModels(). 91 | * @private 92 | * @type {Object} 93 | */ 94 | this._models = {}; 95 | 96 | /** 97 | * an array of sorted handler keys. 98 | * the final result of sorting all flattened. 99 | * @private 100 | * @type {Array} 101 | */ 102 | this._sortedHandlerKeys = []; 103 | 104 | /** 105 | * the configuration object passsed in for this conductor. 106 | * store it here so that when we merge dependencies later we can use 107 | * these instead of the flattened values. 108 | * @private 109 | * @type {Object} 110 | */ 111 | this._config = { 112 | /** 113 | * an array of dependency conductors 114 | * @private 115 | * @type {Array} 116 | */ 117 | deps: config.deps || [], 118 | 119 | /** 120 | * an object map, all values are arrays of functions (handlers). 121 | * keys can be any string, but most likely numbers. 122 | * @private 123 | * @type {Array | Object} 124 | */ 125 | handlers: config.handlers || {}, 126 | 127 | /** 128 | * object of props. generated by a props function that's passed in. 129 | * default to empty object first, we'll start appending to this 130 | * value as we merged down the dependency tree. 131 | * @private 132 | * @type {Function} 133 | * @param {Object} inheritedProps the initial props given to the conductor 134 | * @returns {Object} 135 | */ 136 | propsFn: null, 137 | /** 138 | * key/val pair where val is an array of functions returning instances 139 | * of Models. keys allow you to specify multiple buckets of models. e.g., 140 | * { 141 | * bucketA: [ model1, model2 ], 142 | * bucketB: [ model3, model4 ] 143 | * } 144 | * @private 145 | * @type {Object} 146 | */ 147 | models: config.models || {} 148 | }; 149 | 150 | // This will be the normal case, devs should pass a function to handle 151 | // props 152 | if (_.isFunction(config.props)) { 153 | this._config.propsFn = config.props; 154 | } else if (_.isPlainObject(config.props)) { 155 | // If props isn't a function and is instead an object, wrap it 156 | this._config.propsFn = function setWrapperProps() { 157 | return config.props; 158 | }; 159 | } else { 160 | // Wrapper for generating default props for a conductor 161 | this._config.propsFn = function setDefaultProps(inheritedProps) { 162 | return inheritedProps || {}; 163 | }; 164 | } 165 | 166 | // Determine if the propsFn passed is now a proper function after 167 | // being wrapped or not. If it wasn't a plain object it won't now be 168 | // wrapped by a function so this assert will fail 169 | assert.func(this._config.propsFn, 'config.props'); 170 | 171 | // first, sort the dependencies. 172 | // this call is recursive, will walk through all deps and sort 173 | // and flatten them. 174 | this._sortedDeps = this._sortDeps(this); 175 | 176 | // now that dependencies are sorted, merge all the props, handlers, models. 177 | this._mergeDeps(); 178 | 179 | // this._handlers is an array of handler arrays, so loop in to the 180 | // lowest level to check for each one being a function 181 | _.map(this._handlers, function(handlers) { 182 | _.map(handlers, function(handler) { 183 | if (!_.isFunction(handler)) { 184 | assert.func( 185 | handler, 186 | 'handler is not a function in conductor: ' + self.name 187 | ); 188 | } 189 | }); 190 | }); 191 | 192 | // lastly, sort the finalized handler keys, after we've created all the oban 193 | // props. do this now so we don't have to do it at run time to determine 194 | // the correct run time order. 195 | this._sortedHandlerKeys = helpers.sortNumericalKeys(this._handlers); 196 | 197 | // now freeze the props after merging them all down! 198 | this._props = immutable.Map(this._props); // eslint-disable-line new-cap 199 | } 200 | 201 | //------------------------------------------------------------------------------ 202 | // private prototype methods 203 | //------------------------------------------------------------------------------ 204 | 205 | /** 206 | * crawl the dependency tree, and tsort the deps 207 | * @private 208 | * @function _sortDeps 209 | * @param {Object} conductor an instance of this conductor class 210 | * @param {Object} [tsort] an instance of tsort utility, only exists when recursive. 211 | * @returns {Array} a sorted array of Conductor names 212 | */ 213 | Conductor.prototype._sortDeps = function(conductor, tsort) { 214 | var localTsort = tsort; 215 | var len = conductor._config.deps && conductor._config.deps.length; 216 | var i; 217 | 218 | // must use for loop here to guarantee order! 219 | // go down the rabbit hole if we have deps. 220 | for (i = 0; i < len; i++) { 221 | var dep = conductor._config.deps[i]; 222 | 223 | // if tsort doesn't exist yet (because it's not recursive), 224 | // create a new tsort bucket. 225 | if (!localTsort) { 226 | localTsort = new Toposort(); 227 | } 228 | 229 | // go down the deps tree and keep adding 230 | this._sortDeps(dep, localTsort); 231 | 232 | // add to tsort after we've added all children 233 | localTsort.add(conductor.name, dep.name); 234 | } 235 | 236 | // add the configuration to internal registry. 237 | this._registry[conductor.name] = conductor; 238 | 239 | // this next section is executed on every recursive call. 240 | // the tsort object is the accumulator for building deps tree. 241 | // if we had deps, we'll return the sorted tree, otherwise 242 | // we will return ourselves. 243 | 244 | // if there were deps, return the sorted deps 245 | if (localTsort) { 246 | return localTsort.sort(); 247 | } else { 248 | // if no tsort bucket exists, we had no deps. 249 | // just return the current conductor 250 | return [conductor.name]; 251 | } 252 | }; 253 | 254 | /** 255 | * once we have the flattened dependency tree, loop through them one by one 256 | * and merge props, models, and handlers. 257 | * @private 258 | * @function _mergeDeps 259 | * @returns {undefined} 260 | */ 261 | Conductor.prototype._mergeDeps = function() { 262 | var i = this._sortedDeps.length; 263 | var depName; 264 | var dep; 265 | 266 | // loop through deps in reverse order, start merging the configs down. 267 | // reverse order is necessary to build the dependency tree in the right 268 | // order. 269 | while (i--) { 270 | depName = this._sortedDeps[i]; 271 | dep = this._registry[depName]; 272 | 273 | // merge special constructs. 274 | // pass in existing values to mutate/accumulate as we go up the tree. 275 | // merge props 276 | this._props = dep._config.propsFn(this._props); 277 | // merge models 278 | this._models = helpers.mergeObjArrays(this._models, dep._config.models); 279 | // merge handlers 280 | this._handlers = helpers.mergeObjArrays( 281 | this._handlers, 282 | dep._config.handlers 283 | ); 284 | } 285 | }; 286 | 287 | //------------------------------------------------------------------------------ 288 | // public prototype methods 289 | //------------------------------------------------------------------------------ 290 | 291 | /** 292 | * retrieves a handler block for a given key. 293 | * @public 294 | * @function getHandlers 295 | * @param {String} key the key of the handler block 296 | * @returns {Array} an array of function handlers 297 | */ 298 | Conductor.prototype.getHandlers = function(key) { 299 | assert.notEqual(key, undefined, 'key'); // eslint-disable-line no-undefined 300 | assert.notEqual(key, null, 'key'); 301 | 302 | if (!this._handlers[key]) { 303 | throw new Error('Attempted to retrieve an invalid handler block!'); 304 | } 305 | 306 | return this._handlers[key]; 307 | }; 308 | 309 | /** 310 | * retrieves the sorted handler keys for the conductor. 311 | * @public 312 | * @function getHandlerKeys 313 | * @returns {Array} an array of strings 314 | */ 315 | Conductor.prototype.getHandlerKeys = function() { 316 | return this._sortedHandlerKeys; 317 | }; 318 | 319 | /** 320 | * retrieves an immutable property. 321 | * if no name passed in, return all props. 322 | * @public 323 | * @function getProps 324 | * @param {String} [name] optional name of the prop to retrieve. 325 | * @returns {Object} the copy of the prop 326 | */ 327 | Conductor.prototype.getProps = function(name) { 328 | assert.optionalString(name, 'name'); 329 | 330 | var props = this._props.toObject(); 331 | 332 | // if no name passed in, return all props 333 | if (!name) { 334 | return props; 335 | } 336 | 337 | // otherwise, return the name specified 338 | return props[name]; 339 | }; 340 | 341 | /** 342 | * iterates through a specific model bucket, invoking each function in the array 343 | * to create a new instance of a model. does not change any state. 344 | * @public 345 | * @static 346 | * @function createModels 347 | * @param {Object} req the request object 348 | * @param {Object} res the response object 349 | * @param {String} bucketName the name of the model bucket to create. 350 | * @returns {Array} an array of models 351 | */ 352 | Conductor.prototype.createModels = function(req, res, bucketName) { 353 | assert.object(req, 'req'); 354 | assert.object(res, 'res'); 355 | 356 | // if modelBucket is not passed in, models might be using a single 357 | // flattened array, instead of a complex bucketed scenario. 358 | // in this scenario, bucketName is just index 0 359 | var normalizedBucketName = bucketName || '0'; 360 | 361 | // retrieve the oban container for this bucket of models 362 | var bucket = this._models[normalizedBucketName]; 363 | 364 | return _.reduce( 365 | bucket, 366 | function(acc, modelFn) { 367 | var model = modelFn(req, res); 368 | acc.push(model); 369 | return acc; 370 | }, 371 | [] 372 | ); 373 | }; 374 | 375 | /** 376 | * returns a flattened list of handler stacks. 377 | * for debug use only. 378 | * @public 379 | * @function getDebugHandlerStack 380 | * @returns {Array} an array of function names and the index of their blocks 381 | */ 382 | Conductor.prototype.getDebugHandlerStack = function() { 383 | return _.reduce( 384 | this._handlers, 385 | function(acc, block, key) { 386 | // loop through each handler block, get each handler name for 387 | // debug console logging. 388 | _.forEach(block, function(handler) { 389 | acc.push(key + '-' + (handler.name || '?')); 390 | }); 391 | return acc; 392 | }, 393 | [] 394 | ); 395 | }; 396 | 397 | module.exports = Conductor; 398 | -------------------------------------------------------------------------------- /lib/clients/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // external modules 4 | var assert = require('assert-plus'); 5 | var restify = require('restify-clients'); 6 | var Urijs = require('urijs'); 7 | 8 | // internal files 9 | var logHelpers = require('../logHelpers'); 10 | 11 | /** 12 | * create a new restify json client. a new one is 13 | * created for each outbound API request (i.e., one per model). 14 | * TODO: this is potentially expensive, need to investigate 15 | * creating a single client per incoming request (i.e., per user, per remote host) 16 | * crux of the problem is allowing customization on a per request basis (headers), 17 | * which currently requires creating a new client per model. 18 | * @public 19 | * @function create 20 | * @param {Object} model a restify model 21 | * @returns {Object} a restify JsonClient 22 | */ 23 | function create(model) { 24 | assert.string(model.host, 'model.host'); 25 | assert.optionalNumber(model.port, 'model.port'); 26 | assert.optionalObject(model.headers, 'model.headers'); 27 | 28 | var remoteHost = new Urijs(model.host) 29 | .port(model.port || 80) 30 | .protocol(model.secure ? 'https' : 'http') 31 | .toString(); 32 | 33 | return restify.createJsonClient({ 34 | url: remoteHost, 35 | // TODO: this doesn't seem to log anything?! 36 | log: logHelpers.child({ component: 'jsonClient' }) 37 | }); 38 | } 39 | 40 | module.exports.create = create; 41 | -------------------------------------------------------------------------------- /lib/errors/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var errors = require('restify-errors'); 4 | 5 | module.exports = { 6 | ModelValidationError: errors.makeConstructor('ModelValidationError'), 7 | ModelRequestError: errors.makeConstructor('ModelRequestError'), 8 | EmptyHandlersError: errors.makeConstructor('EmptyHandlersError'), 9 | ShardError: errors.makeConstructor('ShardError') 10 | }; 11 | -------------------------------------------------------------------------------- /lib/handlers/buildModels.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // external modules 4 | var _ = require('lodash'); 5 | var vasync = require('vasync'); 6 | var assert = require('assert-plus'); 7 | 8 | // internal files 9 | var reqHelpers = require('../reqHelpers'); 10 | var logHelpers = require('../logHelpers'); 11 | var errorTypes = require('../errors'); 12 | 13 | /** 14 | * Internal method for fetching any models that are resolved from 15 | * the model bucket. 16 | * 17 | * @function getModel 18 | * @private 19 | * @param {Object} req request object 20 | * @param {Object} res response object 21 | * @param {Object} model model object 22 | * @param {Function} callback callback when fetch is done 23 | * @returns {undefined} 24 | */ 25 | function getModel(req, res, model, callback) { 26 | var log; 27 | 28 | // get a req.log if available, or use the default. 29 | // create a child logger off of it. 30 | if (req.log) { 31 | log = req.log.child({ component: 'buildModels' }); 32 | logHelpers.addSerializers(log); 33 | } else { 34 | // default logger already has serializers, no need to add. 35 | log = logHelpers.getDefault(); 36 | } 37 | 38 | // set the model on the request always, regardless of 39 | // success or failure. 40 | reqHelpers.setModel(req, model); 41 | 42 | // preconfigure the model with other things we need. 43 | // only populate the client if it's a model that requires fetching something 44 | // from a remote host. 45 | model.preConfigure(req, res, { 46 | client: reqHelpers.getClient(req, model), 47 | log: log 48 | }); 49 | 50 | // log a debug, then go fetch it! 51 | // also start req timers. 52 | req.startHandlerTimer( 53 | req._restifyConductor.timerNamePrefix + '-' + model.name 54 | ); 55 | log.debug({ model: model }, 'Building model...'); 56 | 57 | model.get(function getModelComplete(reqErr, rawData) { 58 | // log debug on completion 59 | log.debug({ modelName: model.name }, 'Build complete!'); 60 | // stop the request timers 61 | req.endHandlerTimer( 62 | req._restifyConductor.timerNamePrefix + '-' + model.name 63 | ); 64 | 65 | var finalErr; 66 | 67 | // handle lower level errors first 68 | if (reqErr) { 69 | // handle 401s uniquely, check for status codes 70 | if (rawData.statusCode === 401) { 71 | finalErr = new errorTypes.UnauthorizedError(reqErr); 72 | // create a new error, log it, add it to model error state, return. 73 | // push error in the errors array, and return 74 | model.errors.push(finalErr); 75 | return callback(finalErr, model); 76 | } else if (_.isFunction(model.fallback)) { 77 | // if we have a fallback function, attempt to use it. 78 | log.info( 79 | { 80 | name: model.name 81 | }, 82 | 'attempting fallback mode!' 83 | ); 84 | 85 | // run the function, expect consumers to set this.data 86 | model.fallback(); 87 | 88 | // set the fallback mode flag to true 89 | model.fallbackMode = true; 90 | 91 | // in fallback mode, don't return on callback. 92 | // attempt to go through regular flow 93 | } 94 | } 95 | 96 | // if no lower level err, check validity. 97 | if (model.isValid(rawData)) { 98 | // if valid, save it! 99 | model.data = rawData; 100 | 101 | // do any munging if needed 102 | model.postConfigure(req, res); 103 | 104 | // success! return with no err. 105 | return callback(null); 106 | } else { 107 | // if we failed validation, create a validation error, log it, return 108 | finalErr = new errorTypes.ModelValidationError( 109 | 'model validation error for ' + model.name 110 | ); 111 | model.errors.push(finalErr); 112 | return callback(finalErr, model); 113 | } 114 | }); 115 | } 116 | 117 | /** 118 | * a handler to build any models defined on the conductor. 119 | * @public 120 | * @function buildModelsWrapper 121 | * @param {Array} modelBucket a key we can use to look up a bucket 122 | * of models defined on the conductor 123 | * @param {Function} modelFetcher function to run for fetching / creating 124 | * models. The function should accept req 125 | * as the first parameter, req as the 126 | * second, a models array as the third, and 127 | * a callback as the fourth. 128 | * @returns {undefined} 129 | */ 130 | function buildModelsWrapper(modelBucket, modelFetcher) { 131 | assert.optionalFunc(modelFetcher, 'modelFetcher'); 132 | 133 | if (!modelFetcher) { 134 | modelFetcher = getModel; 135 | } 136 | 137 | return function buildModels(req, res, next) { 138 | var conductor = reqHelpers.getConductor(req); 139 | var models = conductor.createModels(req, res, modelBucket); 140 | var log; 141 | var partialModel; 142 | 143 | // get a req.log if available, or use the default. 144 | // create a child logger off of it. 145 | if (req.log) { 146 | log = req.log.child({ component: 'buildModels' }); 147 | logHelpers.addSerializers(log); 148 | } else { 149 | // default logger already has serializers, no need to add. 150 | log = logHelpers.getDefault(); 151 | } 152 | 153 | partialModel = _.partial(modelFetcher, req, res); 154 | 155 | // there's a restify bug here in req.timers. 156 | // if we call start/endHandlerTimer with the same name a second time, 157 | // it won't register, since req.timers is a map. 158 | vasync.forEachParallel( 159 | { 160 | func: partialModel, 161 | inputs: models 162 | }, 163 | function vasyncComplete(err, results) { 164 | // vasync returns a multierror, but that multierror isn't output 165 | // correctly by bunyan. let's create our own logging context 166 | // by looping through all results. 167 | if (err) { 168 | var modelErrs = []; 169 | var failedModels = []; 170 | var failedModelNames = []; 171 | 172 | // loop through all results 173 | _.forEach(results.operations, function(op) { 174 | // construct debug context. 175 | // op.result is the failed model. 176 | // create separate arrays just so when we output 177 | // we don't have to loop again... yeah, yeah, if we did it 178 | // functionally it would be prettier, but slower. 179 | 180 | // if this operation had an err, append the info. 181 | if (!_.isEmpty(op.err)) { 182 | modelErrs.push(op.err); 183 | failedModelNames.push(op.result.name); 184 | failedModels.push(op.result); 185 | } 186 | }); 187 | } 188 | 189 | // we actually don't want a failed model to force an error page. 190 | // users should handle it themselves. don't return with err. 191 | return next(); 192 | } 193 | ); 194 | }; 195 | } 196 | 197 | module.exports = buildModelsWrapper; 198 | -------------------------------------------------------------------------------- /lib/handlers/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.buildModels = require('./buildModels'); 4 | module.exports.init = require('./init'); 5 | module.exports.run = require('./run'); 6 | -------------------------------------------------------------------------------- /lib/handlers/init.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * a handler to initialize the restify conductor namespaces on the request 5 | * and response objects. 6 | * @public 7 | * @method initWrapper 8 | * @param {Object} conductor an instance of restify conductor 9 | * @returns {void} 10 | */ 11 | function initWrapper(conductor) { 12 | return function restifyConductorInit(req, res, next) { 13 | // setup the context 14 | var context = { 15 | conductor: conductor, 16 | models: {}, 17 | clients: {}, 18 | reqTimerPrefix: '' 19 | }; 20 | 21 | // attach it to req 22 | req._restifyConductor = context; 23 | 24 | return next(); 25 | }; 26 | } 27 | 28 | module.exports = initWrapper; 29 | -------------------------------------------------------------------------------- /lib/handlers/run.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // external modules 4 | var _ = require('lodash'); 5 | var vasync = require('vasync'); 6 | 7 | // internal files 8 | var reqHelpers = require('../reqHelpers'); 9 | var logHelper = require('../logHelpers'); 10 | var errors = require('../errors'); 11 | 12 | /** 13 | * run through the handlers of an conductor, starting with the provided index. 14 | * starts at the lowest number if no startIndex is provided. 15 | * this is the internal implementation of the publicly exposed runHandlers. 16 | * potentially async recursive, as we run through each of the handler blocks. 17 | * @private 18 | * @function run 19 | * @param {Object} req the request object 20 | * @param {Object} res the response object 21 | * @param {Function} next the next function in handler stack. 22 | * @param {Object} options an options object 23 | * @returns {undefined} 24 | */ 25 | function run(req, res, next, options) { 26 | var startConductor = reqHelpers.getConductor(req); 27 | var sortedKeys = startConductor.getHandlerKeys(); 28 | var blockKeys; 29 | var log = req.log || logHelper.getDefault(); 30 | 31 | // in recursive scenarios, an options object may be passed in. 32 | // determine the handler keys/blocks we'll be executing, and whether or not 33 | // we start in the middle of a block. this happens when we do sharding. 34 | if (options) { 35 | if (options.lastKey) { 36 | blockKeys = _.takeRightWhile(sortedKeys, function(key) { 37 | return key > options.lastKey; 38 | }); 39 | } else if (options.startKey) { 40 | blockKeys = _.takeRightWhile(sortedKeys, function(key) { 41 | return key === options.startKey; 42 | }); 43 | 44 | if (_.isEmpty(blockKeys)) { 45 | log.info( 46 | 'No handlers at next level in New Shard ' + 47 | 'continuing at the next available level', 48 | { 49 | startKey: options.startKey, 50 | startConductor: startConductor.name 51 | } 52 | ); 53 | return run(req, res, next, { lastKey: options.startKey }); 54 | } 55 | } 56 | } else { 57 | blockKeys = sortedKeys; 58 | } 59 | 60 | if (_.isEmpty(blockKeys)) { 61 | return next( 62 | new errors.EmptyHandlersError( 63 | 'No Handlers to run for ' + startConductor.name 64 | ) 65 | ); 66 | } 67 | 68 | // now that we've got the set of keys/blocks we want to run, use vasync 69 | // to coordinate running each block. 70 | return vasync.forEachPipeline( 71 | { 72 | func: function runHandlerBlock(blockKey, blockCb) { 73 | // use a logger if present, otherwise fall back on default logger. 74 | var currentConductor = reqHelpers.getConductor(req); 75 | 76 | // if we didn't shard, just get the next handler block and continue 77 | // executing. 78 | var handlerBlock = currentConductor.getHandlers(blockKey); 79 | 80 | // pass it a timerNamePrefix for nested handler timing. 81 | // now, execute the handler block. it's async. 82 | vasync.forEachPipeline( 83 | { 84 | func: function runHandlers(fn, handlerCb) { 85 | var fnName = fn.name || '?'; 86 | // for req timers, use the current block name to prefix 87 | // the function name. 88 | var timerName = blockKey + '-' + fnName; 89 | 90 | // save this prefix to the request before we start running 91 | // the handlers. that way handler can use it to add 92 | // arbitrary req timers for using current prefix. 93 | reqHelpers.setReqTimerPrefix(req, timerName); 94 | // start the timer! 95 | req.startHandlerTimer(timerName); 96 | 97 | // run through the current handler. 98 | fn(req, res, function postRun(errOrStop) { 99 | // end req timer if available 100 | req.endHandlerTimer(timerName); 101 | 102 | // the errOrStop flag is designed to mimic restify's 103 | // next behavior. It can be one of two values: 104 | // 1) false 105 | // Tells us to bail out of the handler stack. this is 106 | // necessary in the case of redirects, where we want to 107 | // stop processing the handler stack. 108 | // 2) an error 109 | // An error should get propagated back to restify, 110 | // which will handle the error accordingly. 111 | 112 | // to be super clear, finalCb here is actually a 113 | // handlerCb that manages the conductor handler stacks. 114 | // so something there will need to check for the 115 | // aborted flag we pass back here. 116 | 117 | // check for explicit case #1 here, can't do truthy and 118 | // falsy checks. 119 | if (errOrStop === false) { 120 | return next(false); 121 | } 122 | 123 | // If this is set to something other than undefined 124 | // we should pass the real error object on and ignore 125 | // the sharding 126 | if (errOrStop) { 127 | return handlerCb(errOrStop); 128 | } 129 | 130 | // if the conductor we started with is not equal to the current 131 | // conductor, we sharded! in this scenario, we want to bail out 132 | // of the current vasync process, and restart it again with the 133 | // new conductor, at the next numerical key. 134 | var _currentConductor = reqHelpers.getConductor( 135 | req 136 | ); 137 | 138 | if (startConductor !== _currentConductor) { 139 | log.info('Conductor sharded!', { 140 | oldConductor: startConductor.name, 141 | newConductor: _currentConductor.name 142 | }); 143 | 144 | // this is dirty, but the only way to bail out of vasync 145 | // to next with an "error", then have the vasync completion 146 | // handler avoid calling next because the error isn't REALLY an 147 | // error. 148 | errOrStop = new errors.ShardError( 149 | 'Sharding occurred! Early exiting vasync ' + 150 | 'operation.' 151 | ); 152 | // append some meta data onto the err 153 | errOrStop.startKey = blockKey; 154 | } 155 | 156 | return handlerCb(errOrStop); 157 | }); 158 | }, 159 | inputs: handlerBlock 160 | }, 161 | blockCb 162 | ); 163 | }, 164 | inputs: blockKeys 165 | }, 166 | function runHandlersComplete(err, results) { 167 | // to reach the vasync completion handlers, 168 | // there are three possible scenarios. 169 | 170 | // 1) we sharded, and a shard error was returned. we want to call run 171 | // all over again except on newly sharded conductor, starting at the 172 | // same index we left off of. we don't want to call next here, as it 173 | // will be passed down to the next handler execution chain. 174 | if (err instanceof errors.ShardError) { 175 | return run(req, res, next, { 176 | startKey: err.startKey 177 | }); 178 | } 179 | 180 | // 2) in second scenario, we check for sharding on completion. 181 | // sometimes the original conductor may have a shorter index than the 182 | // child conductor, in which case we need to continue execution in the 183 | // child conductor. 184 | // i.e., the original conductor only has handlers on 185 | // index 10, but the newly sharded conductor has handlers on 20. 186 | var currentConductor = reqHelpers.getConductor(req); 187 | 188 | if (startConductor !== currentConductor) { 189 | return run(req, res, next, { 190 | lastKey: _.last(startConductor.getHandlerKeys()) 191 | }); 192 | } 193 | 194 | // 3) we completed successfully, return next 195 | return next(err); 196 | } 197 | ); 198 | } 199 | 200 | /** 201 | * a handler to run the wrapped conductor handlers. 202 | * no options at the moment. 203 | * @public 204 | * @function runHandlersWrapper 205 | * @returns {undefined} 206 | */ 207 | function runHandlersWrapper() { 208 | // conductors cannot execute the handlers themselves, 209 | // due to sharding. we must handle orchestration 210 | // within restify-conductor. 211 | return function runHandlers(req, res, next) { 212 | // start looping through the handler stacks, by getting 213 | // the sorted handler stack keys, then using them to 214 | // get each handler block and execute it. 215 | run(req, res, next); 216 | }; 217 | } 218 | 219 | module.exports = runHandlersWrapper; 220 | -------------------------------------------------------------------------------- /lib/helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | 5 | /** 6 | * sorts the numerical keys of an object 7 | * @private 8 | * @function sortNumericalKeys 9 | * @param {Object} obj an object whose keys are to be sorted 10 | * @returns {Array} an array of sorted numbers in ascending order 11 | */ 12 | function sortNumericalKeys(obj) { 13 | return _.chain(obj) 14 | .keys() 15 | .map(function(handlerKey) { 16 | return parseInt(handlerKey, 10); 17 | }) 18 | .value() 19 | .sort(function(a, b) { 20 | return a - b; 21 | }); 22 | } 23 | 24 | /** 25 | * this function handles merging of objects and arrays. handles two use cases: 26 | * 1) merges two dimension arrays 27 | * 2) merged objects containing arrays 28 | * when merging, use obj1 as the 'base' or accumulator, and obj2 as the new 29 | * object that's being merged in. 30 | * @private 31 | * @function mergeObjArrays 32 | * @param {Object} obj1 the base or accmulator object 33 | * @param {Object} obj2 the object to be merged in. 34 | * @returns {Object} the merged object of arrays 35 | */ 36 | function mergeObjArrays(obj1, obj2) { 37 | var acc = _.cloneDeep(obj1); 38 | 39 | return _.reduce( 40 | obj2, 41 | function(innerAcc, arr, key) { 42 | // if obj2 is a not a nested array, just concat and return. 43 | if (!_.isArray(arr)) { 44 | if (!innerAcc[0]) { 45 | innerAcc[0] = []; 46 | } 47 | innerAcc[0] = innerAcc[0].concat(arr); 48 | return innerAcc; 49 | } 50 | 51 | // if the innerAcc has this key, let's concat it. 52 | if (innerAcc.hasOwnProperty(key)) { 53 | innerAcc[key] = innerAcc[key].concat(arr); 54 | } else { 55 | // otherwise, create it. 56 | innerAcc[key] = [].concat(arr); 57 | } 58 | 59 | return innerAcc; 60 | }, 61 | acc 62 | ); 63 | } 64 | 65 | module.exports.sortNumericalKeys = sortNumericalKeys; 66 | module.exports.mergeObjArrays = mergeObjArrays; 67 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // external modules 4 | var assert = require('assert-plus'); 5 | var _ = require('lodash'); 6 | 7 | // internal files 8 | var Conductor = require('./Conductor'); 9 | var Model = require('./models/Model'); 10 | var RestModel = require('./models/RestModel'); 11 | var internalHandlers = require('./handlers'); 12 | var reqHelpers = require('./reqHelpers'); 13 | 14 | // local globals 15 | var METHODS = ['del', 'get', 'head', 'opts', 'post', 'put', 'patch']; 16 | 17 | //------------------------------------------------------------------------------ 18 | // private methods 19 | //------------------------------------------------------------------------------ 20 | 21 | /** 22 | * install a Conductor against a given URL 23 | * @private 24 | * @function installConductor 25 | * @param {String} method get | post | head | put | del 26 | * @param {String|Object} opts the url of REST resource or 27 | * opts to pass to Restify 28 | * @param {Object} server a restify server object 29 | * @param {Object} conductor a Conductor instance 30 | * @returns {undefined} 31 | */ 32 | function installConductor(method, opts, server, conductor) { 33 | // assert params 34 | assert.string(method, 'method'); 35 | assert.object(server, 'server'); 36 | assert.equal(conductor instanceof Conductor, true); 37 | 38 | if (!opts || (typeof opts !== 'string' && typeof opts !== 'object')) { 39 | return assert.fail('opts needs to be a string or object'); 40 | } 41 | 42 | var handlers = []; 43 | var applyArgs; 44 | 45 | // add a 'universal' conductor to beginning of handler stack. 46 | // save the restify conductor on the request. 47 | handlers.push(createConductorHandlers(conductor)); 48 | 49 | // add the method to the top of the args 50 | applyArgs = [opts].concat(handlers); 51 | 52 | // now install the route 53 | return server[method].apply(server, applyArgs); 54 | } 55 | 56 | //------------------------------------------------------------------------------ 57 | // public methods 58 | //------------------------------------------------------------------------------ 59 | 60 | /** 61 | * wrapper function for creating conductors 62 | * @public 63 | * @function createConductor 64 | * @param {Object} options an options object 65 | * @returns {Conductor} a Conductor instance 66 | */ 67 | function createConductor(options) { 68 | return new Conductor(options); 69 | } 70 | 71 | /** 72 | * wrapper function for creating models. 73 | * we MUST return a closure, this is necessary to provide 74 | * req res to the lifecycle methods, and allow us to return a new model for 75 | * each new incoming request. 76 | * @public 77 | * @function createConductor 78 | * @param {Object} options an options object 79 | * @returns {Function} 80 | */ 81 | function createModel(options) { 82 | if (options.host || options.qs) { 83 | return function(req, res) { 84 | return new RestModel(options); 85 | }; 86 | } else { 87 | return function(req, res) { 88 | return new Model(options); 89 | }; 90 | } 91 | } 92 | 93 | /** 94 | * Create a middleware chain that executes a specific conductor 95 | * @public 96 | * @function createConductorHandlers 97 | * @param {Object} conductor a Conductor instance 98 | * @returns {Function[]} 99 | */ 100 | function createConductorHandlers(conductor) { 101 | assert.equal(conductor instanceof Conductor, true); 102 | return [internalHandlers.init(conductor), internalHandlers.run()]; 103 | } 104 | 105 | _.forEach(METHODS, function(method) { 106 | /** 107 | * programatically create wrapperis for Restify's server[method] 108 | * @param {String|Object} opts the url of REST resource or 109 | * opts to pass to Restify 110 | * @param {Conductor} conductor a conductor instance 111 | * @param {Object} server a restify server 112 | * @returns {undefined} 113 | */ 114 | var methodInstaller = function(opts, conductor, server) { 115 | installConductor(method, opts, server, conductor); 116 | }; 117 | methodInstaller.displayName = method; 118 | 119 | module.exports[method] = methodInstaller; 120 | }); 121 | 122 | // specific classes and wrappers 123 | module.exports.createConductor = createConductor; 124 | module.exports.createModel = createModel; 125 | module.exports.createConductorHandlers = createConductorHandlers; 126 | 127 | // exposed handlers, only expose a subset of all internal handlers. 128 | module.exports.handlers = { 129 | buildModels: internalHandlers.buildModels 130 | }; 131 | 132 | // request helper pass through APIs. 133 | // this is only so the user doesn't have to 134 | // require in a different file, 135 | // i.e., require('restify-conductor/req'); 136 | module.exports.getConductor = reqHelpers.getConductor; 137 | module.exports.getProps = reqHelpers.getProps; 138 | module.exports.getModels = reqHelpers.getModels; 139 | module.exports.setModel = reqHelpers.setModel; 140 | module.exports.shardConductor = reqHelpers.shardConductor; 141 | -------------------------------------------------------------------------------- /lib/logHelpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // external modules 4 | var _ = require('lodash'); 5 | var bunyan = require('bunyan'); 6 | 7 | // local globals 8 | var LOG = bunyan.createLogger({ 9 | name: 'restify-conductor', 10 | level: process.env.LOG_LEVEL || bunyan.info, 11 | src: process.env.LOG_LEVEL === 'TRACE' || process.env.LOG_LEVEL === 'DEBUG', 12 | serializers: bunyan.stdSerializers 13 | }); 14 | 15 | // add the default serializers to the default logger 16 | addSerializers(LOG); 17 | 18 | //------------------------------------------------------------------------------ 19 | // private methods 20 | //------------------------------------------------------------------------------ 21 | 22 | /** 23 | * strips off stuff from the model before logging 24 | * @private 25 | * @function stripModelForLogging 26 | * @param {Object} model a restify model 27 | * @returns {Object} 28 | */ 29 | function stripModelForLogging(model) { 30 | var out = { 31 | name: model.name, 32 | data: model.data 33 | }; 34 | 35 | // if it's a RestModel, add more fields. 36 | if (model.type === 'RestModel') { 37 | _.assign( 38 | out, 39 | { 40 | host: model.host 41 | }, 42 | model.rawResponseData 43 | ); 44 | } 45 | 46 | return out; 47 | } 48 | 49 | //------------------------------------------------------------------------------ 50 | // public methods 51 | //------------------------------------------------------------------------------ 52 | 53 | /** 54 | * retrieves default restify-conductor logger 55 | * @public 56 | * @function getDefault 57 | * @returns {Object} bunyan logger 58 | */ 59 | function getDefault() { 60 | return LOG; 61 | } 62 | 63 | /** 64 | * creates a child logger from default restify-conductor logger 65 | * @public 66 | * @function child 67 | * @param {String} name name of child logger 68 | * @returns {Object} bunyan logger 69 | */ 70 | function child(name) { 71 | return LOG.child({ 72 | component: name 73 | }); 74 | } 75 | 76 | /** 77 | * add the restify-conductor specific serializers 78 | * @public 79 | * @function addSerializers 80 | * @param {Object} log bunyan instance 81 | * @returns {void} 82 | */ 83 | function addSerializers(log) { 84 | if (!log.addSerializers) { 85 | return; 86 | } 87 | log.addSerializers({ 88 | conductorModel: function(model) { 89 | if (!model) { 90 | return null; 91 | } 92 | 93 | if (_.isArray(model)) { 94 | return _.reduce( 95 | model, 96 | function(acc, m) { 97 | acc.push(stripModelForLogging(m)); 98 | return acc; 99 | }, 100 | [] 101 | ); 102 | } else { 103 | return stripModelForLogging(model); 104 | } 105 | } 106 | }); 107 | } 108 | 109 | module.exports.getDefault = getDefault; 110 | module.exports.child = child; 111 | module.exports.addSerializers = addSerializers; 112 | -------------------------------------------------------------------------------- /lib/models/Model.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // external modules 4 | var assert = require('assert-plus'); 5 | var _ = require('lodash'); 6 | 7 | // internal files 8 | var logHelpers = require('../logHelpers'); 9 | 10 | // local global 11 | 12 | // fallback logger to use if the model wasn't configured with one 13 | var LOG = logHelpers.child('models'); 14 | 15 | /** 16 | * Model class. 17 | * abstraction for restify-conductor models. 18 | * @public 19 | * @class 20 | * @param {Object} config model configuration object 21 | */ 22 | function Model(config) { 23 | assert.object(config, 'config'); 24 | assert.string(config.name, 'config.name'); 25 | assert.optionalFunc(config.before, 'config.before'); 26 | assert.optionalFunc(config.after, 'config.after'); 27 | assert.optionalFunc(config.isValid, 'config.isValid'); 28 | 29 | // initialize instance level attributes 30 | this.props = {}; 31 | this.data = {}; 32 | this.errors = []; 33 | 34 | // merge down the config 35 | this._mergeConfig(config); 36 | 37 | // assign a logger if it wasn't passed in. 38 | if (!this.log) { 39 | this.log = LOG; 40 | } 41 | } 42 | 43 | /** 44 | * arbitrary model props 45 | * @type {Object} 46 | */ 47 | Model.prototype.props = null; 48 | 49 | /** 50 | * the model data 51 | * @type {Object} 52 | */ 53 | Model.prototype.data = null; 54 | 55 | /** 56 | * collected errors that may have occurred 57 | * through the lifecycle methods. 58 | * @type {Array} 59 | */ 60 | Model.prototype.errors = null; 61 | 62 | /** 63 | * a bunyan instance for loggin 64 | * @type {Object} 65 | */ 66 | Model.prototype.log = null; 67 | 68 | /** 69 | * a remote client that implements a get() method 70 | * for fetching remote data 71 | * @type {Object} 72 | */ 73 | Model.prototype.client = null; 74 | 75 | /** 76 | * model type for debugging purposes 77 | * @type {String} 78 | */ 79 | Model.prototype.type = 'Model'; 80 | 81 | /** 82 | * model name 83 | * @type {String} 84 | */ 85 | Model.prototype.name = ''; 86 | 87 | /** 88 | * flag used to help debug. 89 | * true if the model is async. 90 | * @type {Boolean} 91 | */ 92 | Model.prototype.async = false; 93 | 94 | //------------------------------------------------------------------------------ 95 | // private methods 96 | //------------------------------------------------------------------------------ 97 | 98 | /** 99 | * merges configuration. 100 | * only merges whitelisted values (values found on the prototype). 101 | * this prevents arbitrary values from potentially breaking lifecycle methods. 102 | * @private 103 | * @function mergeConfig 104 | * @param {Object} config The configuration object. 105 | * @returns {undefined} 106 | */ 107 | Model.prototype._mergeConfig = function(config) { 108 | var self = this; 109 | 110 | _.forEach(config, function(val, key) { 111 | // if prototype property exists, 112 | // override it instance 113 | // TODO: make this self.prototype 114 | if (key in self) { 115 | self[key] = val; 116 | } 117 | }); 118 | }; 119 | 120 | //------------------------------------------------------------------------------ 121 | // public instance lifecycle method 122 | //------------------------------------------------------------------------------ 123 | 124 | // The lifecycle methods work as follows: 125 | 126 | // before -> get -> isValid -> after 127 | 128 | // In this Model, because there is no async get, we don't really have 129 | // to worry about errors. However, in subclasses, like AsyncModel 130 | // or RestModel, errors can occur during the get lifecycle method. 131 | // Request timeouts, database timeouts etc. 132 | 133 | // In the case an error occurs fallback is 134 | // invoked in an attempt to get a fallback data payload: 135 | 136 | // before -> get (error) -> fallback -> isValid -> after 137 | 138 | /** 139 | * default noop for all models. 140 | * gives users a hook to modify the model 141 | * before requesting it. 142 | * @public 143 | * @function before 144 | * @param {Object} req the request object 145 | * @param {Object} res the response object 146 | * @returns {undefined} 147 | */ 148 | Model.prototype.before = function(req, res) { 149 | // noop 150 | }; 151 | 152 | /** 153 | * default noop for all models. 154 | * gives users a hook to modify the model 155 | * after getting a return value. 156 | * @public 157 | * @function after 158 | * @param {Object} req the request object 159 | * @param {Object} res the response object 160 | * @returns {undefined} 161 | */ 162 | Model.prototype.after = function(req, res) { 163 | // noop 164 | }; 165 | 166 | /** 167 | * lifecycle method for validating returned data. 168 | * @public 169 | * @function isValid 170 | * @param {Object} data the data to validate 171 | * @returns {Boolean} 172 | */ 173 | Model.prototype.isValid = function(data) { 174 | // overridable instance method. 175 | // noop on prototype, but must take 176 | // argument so V8 can optimize 177 | return true; 178 | }; 179 | 180 | /** 181 | * default noop for all models. 182 | * gives users a hook to handle validation errors. 183 | * @public 184 | * @function fallback 185 | * @returns {Object} 186 | */ 187 | Model.prototype.fallback = null; 188 | 189 | /** 190 | * public method to invoke the get of the model data. 191 | * @public 192 | * @function get 193 | * @param {Function} cb callback function 194 | * @returns {undefined} 195 | */ 196 | Model.prototype.get = function(cb) { 197 | cb(null, this.data); 198 | }; 199 | 200 | //------------------------------------------------------------------------------ 201 | // private subclass overridable lifecycle methods 202 | //------------------------------------------------------------------------------ 203 | 204 | /** 205 | * subclass wrapper for before lifecycle method. 206 | * @private 207 | * @function _before 208 | * @param {Object} req the request object 209 | * @param {Object} res the response object 210 | * @returns {undefined} 211 | */ 212 | Model.prototype._before = function(req, res) { 213 | this.before(req, res); 214 | }; 215 | 216 | /** 217 | * subclass wrapper for after lifecycle method. 218 | * @private 219 | * @function _after 220 | * @param {Object} req the request object 221 | * @param {Object} res the response object 222 | * @returns {undefined} 223 | */ 224 | Model.prototype._after = function(req, res) { 225 | this.after(req, res); 226 | }; 227 | 228 | /** 229 | * subclass wrapper for onError lifecycle method. 230 | * @private 231 | * @function _fallback 232 | * @param {Error} err error object 233 | * @returns {undefined} 234 | */ 235 | Model.prototype._fallback = function(err) { 236 | // invoke error handler 237 | var fallbackData = this.fallback(err); 238 | 239 | // expect error handler to return a boolean 240 | // indicating the error was handled. 241 | // if so, swallow the error. 242 | if (!_.isUndefined(fallbackData)) { 243 | this.data = fallbackData; 244 | } 245 | 246 | // now save the error for debugging purposes 247 | this.errors.push(err); 248 | }; 249 | 250 | /** 251 | * sublcass wrapper for validating model data 252 | * @private 253 | * @function _isValid 254 | * @param {Object} data the data to validate 255 | * @returns {Boolean} 256 | */ 257 | Model.prototype._isValid = function(data) { 258 | // checks for undefined, then calls instance level validator 259 | return !_.isUndefined(data) && this.isValid(data); 260 | }; 261 | 262 | //------------------------------------------------------------------------------ 263 | // public methods inherited by all Model classes 264 | //------------------------------------------------------------------------------ 265 | 266 | /** 267 | * public method to invoke the before chain of lifecycle events. 268 | * @public 269 | * @function preConfigure 270 | * @param {Object} req the request object 271 | * @param {Object} res the response object 272 | * @param {Object} options an options object 273 | * @returns {undefined} 274 | */ 275 | Model.prototype.preConfigure = function(req, res, options) { 276 | // set configuration that's injected at the last second 277 | if (options) { 278 | if (options.client) { 279 | this.client = options.client; 280 | } 281 | 282 | if (options.log) { 283 | this.log = options.log; 284 | } 285 | } 286 | 287 | // call lifecycle methods 288 | this._before(req, res); 289 | }; 290 | 291 | /** 292 | * public method to invoke the after chain of lifecycle events. 293 | * @public 294 | * @function postConfigure 295 | * @param {Object} req the request object 296 | * @param {Object} res the response object 297 | * @returns {undefined} 298 | */ 299 | Model.prototype.postConfigure = function(req, res) { 300 | this._after(req, res); 301 | }; 302 | 303 | module.exports = Model; 304 | -------------------------------------------------------------------------------- /lib/models/RestModel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // external modules 4 | var assert = require('assert-plus'); 5 | var Urijs = require('urijs'); 6 | var util = require('util'); 7 | 8 | // internal files 9 | var Model = require('./Model'); 10 | var errors = require('../errors'); 11 | 12 | /** 13 | * RestModel class. 14 | * abstraction for restify-conductor models. 15 | * @public 16 | * @class 17 | * @param {Object} config model configuration object 18 | */ 19 | function RestModel(config) { 20 | assert.string(config.host, 'config.host'); 21 | assert.optionalString(config.url, 'config.url'); 22 | 23 | // give default instance values so when we do merging 24 | // we don't merge into the prototype value 25 | this.qs = {}; 26 | this.postBody = {}; 27 | this.headers = {}; 28 | this.cookies = {}; 29 | this.rawResponseData = {}; 30 | 31 | // call super ctor 32 | RestModel.super_.call(this, config); 33 | } 34 | util.inherits(RestModel, Model); 35 | 36 | /** 37 | * model type for debugging purposes 38 | * @type {String} 39 | */ 40 | RestModel.prototype.type = 'RestModel'; 41 | 42 | /** 43 | * flag used to help debug. 44 | * true if the model is async. 45 | * @type {Boolean} 46 | */ 47 | RestModel.prototype.async = true; 48 | 49 | /** 50 | * the type of http request. defaults to GET. 51 | * @type {String} 52 | */ 53 | RestModel.prototype.method = 'get'; 54 | 55 | /** 56 | * whether or not the request should be made over https. 57 | * @type {Boolean} 58 | */ 59 | RestModel.prototype.secure = false; 60 | 61 | /** 62 | * the hostname for the request 63 | * @type {String} 64 | */ 65 | RestModel.prototype.host = ''; 66 | 67 | /** 68 | * port number for remote host 69 | * @type {Number} 70 | */ 71 | RestModel.prototype.port = 80; 72 | 73 | /** 74 | * the base url of the request: 75 | * http://{hostname}/{baseurl} 76 | * @type {String} 77 | */ 78 | RestModel.prototype.baseUrl = ''; 79 | 80 | /** 81 | * the specific url of the request: 82 | * http://{hostname}/{baseurl}/{url} 83 | * @type {String} 84 | */ 85 | RestModel.prototype.url = ''; 86 | 87 | /** 88 | * a query string object 89 | * @type {Object} 90 | */ 91 | RestModel.prototype.qs = null; 92 | 93 | /** 94 | * a post body object 95 | * @type {Object} 96 | */ 97 | RestModel.prototype.postBody = null; 98 | 99 | /** 100 | * if a post request, the post type. 101 | * defafult is json, can also be 'form'. 102 | * @type {String} 103 | */ 104 | RestModel.prototype.postType = 'json'; 105 | 106 | /** 107 | * specific headers set for this model 108 | * @type {Object} 109 | */ 110 | RestModel.prototype.headers = null; 111 | 112 | /** 113 | * the format of the returned payload. 114 | * defaults to JSON, but can be XML or other. 115 | * @type {String} 116 | */ 117 | RestModel.prototype.resourceType = 'json'; 118 | 119 | /** 120 | * some cherry picked debug about the external 121 | * resource call. 122 | * @type {Object} 123 | */ 124 | RestModel.prototype.rawResponseData = null; 125 | 126 | /** 127 | * whether or not model is operating in fallback mode. 128 | * @type {Boolean} 129 | */ 130 | RestModel.prototype.fallbackMode = false; 131 | 132 | /** 133 | * retrieves the remote resource. 134 | * @public 135 | * @function get 136 | * @param {Function} callback a callback function to invoke when complete 137 | * @returns {Object} the parsed JSON response 138 | */ 139 | RestModel.prototype.get = function(callback) { 140 | var self = this; 141 | var resourcePath = new Urijs() 142 | .pathname(self.url) 143 | .search(self.qs) 144 | .toString(); 145 | 146 | // fetch a json client, then use to request the resource 147 | self.client[self.method](resourcePath, function getRestModelComplete( 148 | reqErr, 149 | req, 150 | res, 151 | rawData 152 | ) { 153 | var err; 154 | 155 | // no need to log error or do anything here, 156 | // just wrap it and bubble it up for the consumer to handle. 157 | if (reqErr) { 158 | err = new errors.RequestError(reqErr, 'http request error'); 159 | } 160 | 161 | // cherry pick some data from req and res so we have some debug data 162 | // if needed. 163 | // TODO: should we save req/res too? seems verbose, might be helpful 164 | // though. 165 | // self._req = req; 166 | // self._res = res; 167 | self.rawResponseData = { 168 | resourcePath: resourcePath, 169 | statusCode: (res && res.statusCode) || -1, 170 | headers: (res && res.headers) || null, 171 | body: (res && res.body) || null 172 | }; 173 | 174 | return callback(err, rawData); 175 | }); 176 | }; 177 | 178 | module.exports = RestModel; 179 | -------------------------------------------------------------------------------- /lib/reqHelpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // external modules 4 | var assert = require('assert-plus'); 5 | 6 | // internal files 7 | var clients = require('./clients'); 8 | 9 | // helpers for getting context off the request object that are specific to 10 | // restify-conductor. the request construct looks like this: 11 | // 12 | // req._restifyConductor = { 13 | // conductor: conductor 14 | // } 15 | 16 | //------------------------------------------------------------------------------ 17 | // getters 18 | //------------------------------------------------------------------------------ 19 | 20 | /** 21 | * returns the conductor for a given request. 22 | * @public 23 | * @function getConductor 24 | * @param {Object} req the request object 25 | * @returns {undefined} 26 | */ 27 | function getConductor(req) { 28 | var conductor = 29 | req && req._restifyConductor && req._restifyConductor.conductor; 30 | 31 | return conductor || null; 32 | } 33 | 34 | /** 35 | * retrieve an immutable prop off the conductor object 36 | * @public 37 | * @function getProps 38 | * @param {Object} req the request object 39 | * @param {String} propName the name of the prop to retrieve 40 | * @returns {Object} a prop value 41 | */ 42 | function getProps(req, propName) { 43 | var conductor = getConductor(req); 44 | 45 | return conductor ? conductor.getProps(propName) : null; 46 | } 47 | 48 | /** 49 | * returns a restify JSON client if one exists for this host for this incoming request. 50 | * otherwise, creates one. 51 | * @public 52 | * @function getClient 53 | * @param {Object} req the request object 54 | * @param {Object} model a restify model 55 | * @returns {Object} a restify JSON client 56 | */ 57 | function getClient(req, model) { 58 | var host = model.host; 59 | 60 | // if no host, return null. we don't need a client. 61 | if (!host) { 62 | return null; 63 | } 64 | 65 | if (!req._restifyConductor.clients[host]) { 66 | req._restifyConductor.clients[host] = clients.create(model); 67 | } 68 | 69 | return req._restifyConductor.clients[host]; 70 | } 71 | 72 | /** 73 | * gets all the saved models off the request 74 | * @public 75 | * @function getModels 76 | * @param {Object} req the request object 77 | * @param {String} [modelName] name of the model to retrieve. returns all models if not specified. 78 | * @returns {Object | Array} returns an array of models, or just one model. 79 | */ 80 | function getModels(req, modelName) { 81 | return modelName 82 | ? req._restifyConductor.models[modelName] 83 | : req._restifyConductor.models; 84 | } 85 | 86 | /** 87 | * gets the current request timer prefix name. 88 | * useful for using it to prefix other request timers. 89 | * @public 90 | * @function getReqTimerPrefix 91 | * @param {Object} req the request object 92 | * @returns {String} 93 | */ 94 | function getReqTimerPrefix(req) { 95 | return req._restifyConductor.reqTimerPrefix || 'no-prefix-found'; 96 | } 97 | 98 | //------------------------------------------------------------------------------ 99 | // setters 100 | //------------------------------------------------------------------------------ 101 | 102 | /** 103 | * sets the current timer name prefix. 104 | * @public 105 | * @function setReqTimerPrefix 106 | * @param {Object} req the request object 107 | * @param {String} prefix the timer name prefix 108 | * @returns {undefined} 109 | */ 110 | function setReqTimerPrefix(req, prefix) { 111 | req._restifyConductor.reqTimerPrefix = prefix; 112 | } 113 | 114 | /** 115 | * saves a model onto the request 116 | * @public 117 | * @function setModel 118 | * @param {Object} req the request object 119 | * @param {Object} model an instance of a Model or RestModel. 120 | * @returns {undefined} 121 | */ 122 | function setModel(req, model) { 123 | req._restifyConductor.models[model.name] = model; 124 | } 125 | 126 | /** 127 | * replace an conductor midstream with a .createAction 128 | * @public 129 | * @function shardConductor 130 | * @param {Object} req the request object 131 | * @param {Object} newConductor a Conductor 132 | * @returns {undefined} 133 | */ 134 | function shardConductor(req, newConductor) { 135 | assert.ok(req, 'req'); 136 | assert.object(newConductor, 'newConductor'); 137 | 138 | req._restifyConductor.conductor = newConductor; 139 | } 140 | 141 | // getters 142 | module.exports.getConductor = getConductor; 143 | module.exports.getProps = getProps; 144 | module.exports.getClient = getClient; 145 | module.exports.getModels = getModels; 146 | module.exports.getReqTimerPrefix = getReqTimerPrefix; 147 | 148 | // setters 149 | module.exports.setModel = setModel; 150 | module.exports.setReqTimerPrefix = setReqTimerPrefix; 151 | module.exports.shardConductor = shardConductor; 152 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "restify-conductor", 3 | "version": "2.0.2", 4 | "main": "lib/index.js", 5 | "description": "an abstraction framework for building composable endpoints", 6 | "homepage": "https://github.com/restify/conductor", 7 | "author": { 8 | "email": "aliu@netflix.com", 9 | "name": "Alex Liu" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/restify/conductor" 14 | }, 15 | "license": "MIT", 16 | "files": [ 17 | "lib" 18 | ], 19 | "keywords": [ 20 | "branching", 21 | "conductor", 22 | "express", 23 | "react", 24 | "render", 25 | "restify", 26 | "restify-conductor" 27 | ], 28 | "scripts": { 29 | "test": "make test" 30 | }, 31 | "devDependencies": { 32 | "chai": "^4.1.2", 33 | "conventional-changelog-angular": "^5.0.1", 34 | "conventional-recommended-bump": "^4.0.1", 35 | "coveralls": "^3.0.2", 36 | "documentation": "^8.1.2", 37 | "eslint": "^5.5.0", 38 | "eslint-config-prettier": "^3.0.1", 39 | "eslint-plugin-prettier": "^2.6.2", 40 | "mocha": "^5.2.0", 41 | "nyc": "^13.0.1", 42 | "prettier": "^1.14.2", 43 | "sinon": "^6.2.0", 44 | "unleash": "^2.0.1" 45 | }, 46 | "dependencies": { 47 | "assert-plus": "^0.1.5", 48 | "bunyan": "^1.4.0", 49 | "immutable": "^3.7.4", 50 | "lodash": "^4.17.5", 51 | "restify": "^7.2.1", 52 | "restify-clients": "^2.5.2", 53 | "restify-errors": "^6.1.1", 54 | "toposort-class": "^0.3.1", 55 | "urijs": "^1.17.0", 56 | "vasync": "^1.6.3" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "rules": { 6 | "no-unused-expressions": [ 0 ], 7 | "no-undefined": [ 0 ] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/ConductorSpec.js: -------------------------------------------------------------------------------- 1 | // eslint-disable no-new 2 | // jscs:disable maximumLineLength 3 | 4 | 'use strict'; 5 | 6 | var _ = require('lodash'); 7 | var assert = require('assert'); 8 | var rc = require('../lib'); 9 | var Model = require('../lib/models/Model'); 10 | var restify = require('restify'); 11 | 12 | describe('Restify Conductor', function() { 13 | describe('route installation', function() { 14 | var server; 15 | var conductor; 16 | 17 | beforeEach(function() { 18 | server = restify.createServer({ 19 | name: 'testing' 20 | }); 21 | conductor = rc.createConductor({ 22 | name: 'test' 23 | }); 24 | }); 25 | 26 | describe('with rc verbs', function() { 27 | it('should add get route', function() { 28 | rc.get('/foo', conductor, server); 29 | assert.equal(_.keys(server.router.getRoutes()).length, 1); 30 | }); 31 | 32 | it('should add all route types', function() { 33 | var routeVerbs = [ 34 | 'del', 35 | 'get', 36 | 'head', 37 | 'opts', 38 | 'post', 39 | 'put', 40 | 'patch' 41 | ]; 42 | _.map(routeVerbs, function(verb) { 43 | rc[verb]('/bar', conductor, server); 44 | }); 45 | 46 | assert.equal( 47 | _.keys(server.router.getRoutes()).length, 48 | routeVerbs.length 49 | ); 50 | }); 51 | 52 | it('should accept an object for the route', function() { 53 | rc.get({ path: '/path' }, conductor, server); 54 | assert.equal(_.keys(server.router.getRoutes()).length, 1); 55 | }); 56 | 57 | it('should throw when no object or string present', function() { 58 | assert.throws(function() { 59 | rc.get(null, conductor, server); 60 | }); 61 | assert.throws(function() { 62 | rc.get(false, conductor, server); 63 | }); 64 | assert.throws(function() { 65 | rc.get(1, conductor, server); 66 | }); 67 | assert.equal(_.keys(server.router.getRoutes()).length, 0); 68 | }); 69 | }); 70 | 71 | describe('with createConductorHandler', function() { 72 | it('should add get route', function() { 73 | server.get('/foo', rc.createConductorHandlers(conductor)); 74 | assert.equal(_.keys(server.router.getRoutes()).length, 1); 75 | }); 76 | 77 | it('should add all route types', function() { 78 | var routeVerbs = [ 79 | 'del', 80 | 'get', 81 | 'head', 82 | 'opts', 83 | 'post', 84 | 'put', 85 | 'patch' 86 | ]; 87 | _.map(routeVerbs, function(verb) { 88 | server[verb]('/bar', rc.createConductorHandlers(conductor)); 89 | }); 90 | 91 | assert.equal( 92 | _.keys(server.router.getRoutes()).length, 93 | routeVerbs.length 94 | ); 95 | }); 96 | 97 | it('should accept an object for the route', function() { 98 | server.get( 99 | { path: '/path' }, 100 | rc.createConductorHandlers(conductor) 101 | ); 102 | assert.equal(_.keys(server.router.getRoutes()).length, 1); 103 | }); 104 | }); 105 | }); 106 | 107 | describe('constructor tests', function() { 108 | it('should throw if no options passed to constructor', function() { 109 | assert.throws(function() { 110 | rc.createConductor(); 111 | }); 112 | }); 113 | 114 | it('should throw if no name specified in constructor', function() { 115 | assert.throws(function() { 116 | rc.createConductor({ 117 | title: 'A', 118 | props: _.noop 119 | }); 120 | }); 121 | }); 122 | 123 | it("should throw if a handler isn't a function in constructor", function() { 124 | assert.throws(function() { 125 | rc.createConductor({ 126 | name: 'A', 127 | handlers: { 5: [[_.noop]] } 128 | }); 129 | }, 'throw an error with nested handlers'); 130 | }); 131 | 132 | it('should create internal props using passed in config', function() { 133 | var conductorA = rc.createConductor({ 134 | name: 'A', 135 | props: function() { 136 | return { 137 | title: 'A', 138 | numbers: [1, 2, 3], 139 | fastProperties: { 140 | a: 'A' 141 | } 142 | }; 143 | } 144 | }); 145 | 146 | assert.deepEqual(conductorA.getProps('title'), 'A'); 147 | assert.deepEqual(conductorA.getProps('numbers'), [1, 2, 3]); 148 | assert.deepEqual(conductorA.getProps('fastProperties'), { a: 'A' }); 149 | }); 150 | }); 151 | 152 | describe('handler tests', function() { 153 | function A(innerReq, innerRes, next) { 154 | next(); 155 | } 156 | 157 | function B(innerReq, innerRes, next) { 158 | next(); 159 | } 160 | 161 | function C(innerReq, innerRes, next) { 162 | next(); 163 | } 164 | 165 | it('should take an array of handlers', function() { 166 | var conductorA = rc.createConductor({ 167 | name: 'A', 168 | handlers: [A] 169 | }); 170 | var conductorB = rc.createConductor({ 171 | name: 'B', 172 | deps: [conductorA], 173 | handlers: [B] 174 | }); 175 | 176 | var handlerStackA = conductorA.getDebugHandlerStack(); 177 | assert.equal(handlerStackA[0], '0-A'); 178 | 179 | var handlerStackB = conductorB.getDebugHandlerStack(); 180 | assert.equal(handlerStackB[0], '0-A'); 181 | assert.equal(handlerStackB[1], '0-B'); 182 | }); 183 | 184 | it('should take an array of array of handlers', function() { 185 | var conductorA = rc.createConductor({ 186 | name: 'A', 187 | handlers: [[A], [B]] 188 | }); 189 | var conductorB = rc.createConductor({ 190 | name: 'B', 191 | deps: [conductorA], 192 | handlers: [[C]] 193 | }); 194 | 195 | var handlerStackA = conductorA.getDebugHandlerStack(); 196 | assert.equal(handlerStackA[0], '0-A'); 197 | assert.equal(handlerStackA[1], '1-B'); 198 | 199 | var handlerStackB = conductorB.getDebugHandlerStack(); 200 | assert.equal(handlerStackB[0], '0-A'); 201 | assert.equal(handlerStackB[1], '0-C'); 202 | assert.equal(handlerStackB[2], '1-B'); 203 | }); 204 | 205 | it('should take a numerically keyed object of arrays', function() { 206 | var conductorA = rc.createConductor({ 207 | name: 'A', 208 | handlers: { 209 | 10: [A] 210 | } 211 | }); 212 | var conductorB = rc.createConductor({ 213 | name: 'B', 214 | deps: [conductorA], 215 | handlers: { 216 | 30: [C] 217 | } 218 | }); 219 | var conductorC = rc.createConductor({ 220 | name: 'C', 221 | deps: [conductorB], 222 | handlers: { 223 | 20: [B] 224 | } 225 | }); 226 | 227 | var handlerStack = conductorC.getDebugHandlerStack(); 228 | assert.equal(handlerStack[0], '10-A'); 229 | assert.equal(handlerStack[1], '20-B'); 230 | assert.equal(handlerStack[2], '30-C'); 231 | }); 232 | 233 | it('should retrieve handler blocks', function() { 234 | var conductorA = rc.createConductor({ 235 | name: 'A', 236 | handlers: { 237 | 10: [A] 238 | } 239 | }); 240 | var conductorB = rc.createConductor({ 241 | name: 'B', 242 | deps: [conductorA], 243 | handlers: { 244 | 30: [C] 245 | } 246 | }); 247 | var conductorC = rc.createConductor({ 248 | name: 'C', 249 | deps: [conductorB], 250 | handlers: { 251 | 20: [B] 252 | } 253 | }); 254 | 255 | assert.deepEqual(conductorA.getHandlers(10), [A]); 256 | assert.deepEqual(conductorB.getHandlers(10), [A]); 257 | assert.deepEqual(conductorB.getHandlers(30), [C]); 258 | assert.deepEqual(conductorC.getHandlers(10), [A]); 259 | assert.deepEqual(conductorC.getHandlers(30), [C]); 260 | assert.deepEqual(conductorC.getHandlers(20), [B]); 261 | }); 262 | 263 | it('should throw when retrieving handler blocks', function() { 264 | var conductorA = rc.createConductor({ 265 | name: 'A', 266 | handlers: { 267 | 10: [A] 268 | } 269 | }); 270 | 271 | assert.throws(function() { 272 | conductorA.getHandlers(5); 273 | }); 274 | 275 | assert.throws(function() { 276 | conductorA.getHandlers(); 277 | }); 278 | }); 279 | 280 | it('should return sorted handler block keys', function() { 281 | var conductorA = rc.createConductor({ 282 | name: 'A', 283 | handlers: { 284 | 5: [A] 285 | } 286 | }); 287 | var conductorB = rc.createConductor({ 288 | name: 'B', 289 | deps: [conductorA], 290 | handlers: { 291 | 0: [C] 292 | } 293 | }); 294 | var conductorC = rc.createConductor({ 295 | name: 'C', 296 | deps: [conductorB], 297 | handlers: { 298 | 100: [B] 299 | } 300 | }); 301 | 302 | assert.deepEqual(conductorC.getHandlerKeys(), [0, 5, 100]); 303 | }); 304 | }); 305 | 306 | describe('props and first class property tests', function() { 307 | it('should resolve properties on an conductor', function() { 308 | var configA = { 309 | name: 'A', 310 | props: function(inheritedProps) { 311 | assert.deepEqual(inheritedProps, {}); 312 | return { 313 | title: 'A', 314 | models: [{ modelA: 'A' }], 315 | fastProperties: { 316 | a: 'A' 317 | } 318 | }; 319 | } 320 | }; 321 | var conductorA = rc.createConductor(configA); 322 | 323 | assert.equal(conductorA.name, configA.name); 324 | assert.equal(conductorA.getProps('title'), 'A'); 325 | assert.deepEqual(conductorA.getProps('models'), [{ modelA: 'A' }]); 326 | }); 327 | 328 | it('should override props from parent', function() { 329 | var conductorA = rc.createConductor({ 330 | name: 'A', 331 | props: function() { 332 | return { 333 | letters: { 334 | a: 1 335 | }, 336 | numbers: [1, 2, 3] 337 | }; 338 | } 339 | }); 340 | var conductorB = rc.createConductor({ 341 | name: 'B', 342 | deps: [conductorA], 343 | props: function() { 344 | return { 345 | letters: { 346 | b: 2 347 | }, 348 | numbers: [4, 5, 6] 349 | }; 350 | } 351 | }); 352 | 353 | assert.deepEqual(conductorB.getProps('letters'), { b: 2 }); 354 | assert.deepEqual(conductorB.getProps('numbers'), [4, 5, 6]); 355 | }); 356 | 357 | it('should resolve parent props from child conductor', function() { 358 | var conductorA = rc.createConductor({ 359 | name: 'A', 360 | props: function() { 361 | return { 362 | letters: { 363 | a: 1 364 | }, 365 | numbers: [1, 2, 3] 366 | }; 367 | } 368 | }); 369 | var conductorB = rc.createConductor({ 370 | name: 'B', 371 | deps: [conductorA] 372 | }); 373 | 374 | assert.deepEqual(conductorB.getProps('letters'), { a: 1 }); 375 | assert.deepEqual(conductorB.getProps('numbers'), [1, 2, 3]); 376 | }); 377 | 378 | it('should extend parent props from child conductor', function() { 379 | var conductorA = rc.createConductor({ 380 | name: 'A', 381 | props: function() { 382 | return { 383 | letters: { 384 | a: 1 385 | }, 386 | numbers: [1, 2, 3] 387 | }; 388 | } 389 | }); 390 | var conductorB = rc.createConductor({ 391 | name: 'B', 392 | deps: [conductorA], 393 | extendProps: ['letters', 'numbers'], 394 | props: function(inheritedProps) { 395 | assert.deepEqual(inheritedProps, conductorA.getProps()); 396 | return _.mergeWith( 397 | {}, 398 | inheritedProps, 399 | { 400 | letters: { 401 | b: 2 402 | }, 403 | numbers: [4, 5, 6] 404 | }, 405 | /* eslint-disable consistent-return */ 406 | function mergeCustomizer(a, b) { 407 | if (_.isArray(a)) { 408 | return a.concat(b); 409 | } 410 | } 411 | ); 412 | /* eslint-enable consistent-return */ 413 | } 414 | }); 415 | 416 | assert.deepEqual(conductorB.getProps('letters'), { a: 1, b: 2 }); 417 | assert.deepEqual(conductorB.getProps('numbers'), [ 418 | 1, 419 | 2, 420 | 3, 421 | 4, 422 | 5, 423 | 6 424 | ]); 425 | }); 426 | 427 | it('should not mutate parent props', function() { 428 | var conductorA = rc.createConductor({ 429 | name: 'A', 430 | props: function() { 431 | return { 432 | letters: { 433 | a: 1 434 | } 435 | }; 436 | } 437 | }); 438 | var conductorB = rc.createConductor({ 439 | name: 'B', 440 | deps: [conductorA], 441 | extendProps: ['letters', 'numbers'], 442 | props: function(inheritedProps) { 443 | assert.deepEqual(inheritedProps, conductorA.getProps()); 444 | return _.merge(inheritedProps, { 445 | letters: { 446 | b: 2 447 | } 448 | }); 449 | } 450 | }); 451 | 452 | assert.deepEqual(conductorA.getProps('letters'), { a: 1 }); 453 | assert.deepEqual(conductorB.getProps('letters'), { a: 1, b: 2 }); 454 | }); 455 | 456 | it('gh-8 should allow props to be an object', function() { 457 | var conductorA = rc.createConductor({ 458 | name: 'A', 459 | props: { 460 | foo: 'bar' 461 | } 462 | }); 463 | assert.equal(conductorA.getProps('foo'), 'bar'); 464 | }); 465 | }); 466 | 467 | describe('model tests', function() { 468 | it('should build models', function() { 469 | var conductorA = rc.createConductor({ 470 | name: 'A', 471 | models: { 472 | a: [ 473 | function browserInfoModel(innerReq, res) { 474 | return new Model({ 475 | name: 'browserInfo', 476 | get: function(cb) { 477 | cb(null, { 478 | ua: innerReq.headers['user-agent'] 479 | }); 480 | }, 481 | isValid: function(data) { 482 | return typeof data === 'string'; 483 | } 484 | }); 485 | } 486 | ] 487 | } 488 | }); 489 | var conductorB = rc.createConductor({ 490 | name: 'B', 491 | deps: [conductorA], 492 | models: { 493 | a: [ 494 | function fooModel(innerReq, res) { 495 | return new Model({ 496 | name: 'foo', 497 | get: function(cb) { 498 | cb(null, { 499 | ua: innerReq.query.foo 500 | }); 501 | } 502 | }); 503 | } 504 | ], 505 | b: [ 506 | function timestampModel(innerReq, res) { 507 | return new Model({ 508 | name: 'timestamp', 509 | get: function(cb) { 510 | cb(null, Date.now()); 511 | } 512 | }); 513 | } 514 | ] 515 | }, 516 | handlers: { 517 | 10: [rc.handlers.buildModels('a')] 518 | } 519 | }); 520 | var req = { 521 | query: { 522 | foo: 'bar' 523 | }, 524 | headers: { 525 | 'user-agent': 'linx' 526 | }, 527 | _restifyConductor: { 528 | conductor: conductorB, 529 | models: {}, 530 | clients: {}, 531 | reqTimerPrefix: '' 532 | }, 533 | startHandlerTimer: _.noop, 534 | endHandlerTimer: _.noop 535 | }; 536 | 537 | _.forEach(conductorB.getHandlers(10), function(handler) { 538 | handler(req, {}, _.noop); 539 | }); 540 | 541 | var models = rc.getModels(req); 542 | assert(models.browserInfo.isValid); 543 | assert(models.foo.data.ua, 'bar'); 544 | assert.equal(false, models.hasOwnProperty('timestamp')); 545 | }); 546 | 547 | it('should return models', function() { 548 | var conductorA = rc.createConductor({ 549 | name: 'A', 550 | models: { 551 | a: [ 552 | function browserInfoModel(innerReq, res) { 553 | return new Model({ 554 | name: 'browserInfo', 555 | get: function(cb) { 556 | cb(null, { 557 | ua: innerReq.headers['user-agent'] 558 | }); 559 | }, 560 | isValid: function(data) { 561 | return typeof data === 'string'; 562 | } 563 | }); 564 | } 565 | ] 566 | } 567 | }); 568 | var conductorB = rc.createConductor({ 569 | name: 'B', 570 | deps: [conductorA], 571 | models: { 572 | a: [ 573 | function fooModel(innerReq, res) { 574 | return new Model({ 575 | name: 'foo', 576 | get: function(cb) { 577 | cb(null, { 578 | ua: innerReq.query.foo 579 | }); 580 | } 581 | }); 582 | } 583 | ], 584 | b: [ 585 | function timestampModel(innerReq, res) { 586 | return new Model({ 587 | name: 'timestamp', 588 | get: function(cb) { 589 | cb(null, Date.now()); 590 | } 591 | }); 592 | } 593 | ] 594 | } 595 | }); 596 | var req = { 597 | query: { 598 | foo: 'bar' 599 | } 600 | }; 601 | 602 | var modelsA = conductorB.createModels(req, {}, 'a'); 603 | var modelsB = conductorB.createModels(req, {}, 'b'); 604 | assert.equal(modelsA.length, 2); 605 | assert.equal(modelsA[0].name, 'browserInfo'); 606 | assert.equal(modelsA[1].name, 'foo'); 607 | assert.equal(modelsB.length, 1); 608 | assert.equal(modelsB[0].name, 'timestamp'); 609 | }); 610 | }); 611 | }); 612 | -------------------------------------------------------------------------------- /test/HelperSpec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('chai').assert; 4 | var h = require('../lib/helpers'); 5 | 6 | describe('Helpers', function() { 7 | it('should merge two objects of arrays', function() { 8 | var objOne = { 9 | 0: [1, 2, 3], 10 | 1: [4, 5] 11 | }; 12 | var objTwo = { 13 | 1: [6], 14 | 2: [7, 8, 9] 15 | }; 16 | var merged = h.mergeObjArrays(objOne, objTwo); 17 | 18 | assert.deepEqual(merged, { 19 | 0: [1, 2, 3], 20 | 1: [4, 5, 6], 21 | 2: [7, 8, 9] 22 | }); 23 | // ensure no mutation has occurred 24 | assert.deepEqual(objOne, objOne); 25 | assert.deepEqual(objTwo, objTwo); 26 | }); 27 | 28 | it('should merge an object with an array', function() { 29 | var objOne = { 30 | 0: [1, 2, 3] 31 | }; 32 | var arrOne = [4, 5, 6]; 33 | var merged = h.mergeObjArrays(objOne, arrOne); 34 | 35 | assert.deepEqual(merged, { 36 | 0: [1, 2, 3, 4, 5, 6] 37 | }); 38 | // ensure no mutation has occurred 39 | assert.deepEqual(objOne, objOne); 40 | assert.deepEqual(arrOne, arrOne); 41 | }); 42 | 43 | it('should merge array of arrays', function() { 44 | var arrOne = [[1], [2]]; 45 | var arrTwo = [[3], [4]]; 46 | var merged = h.mergeObjArrays(arrOne, arrTwo); 47 | 48 | assert.deepEqual(merged, [[1, 3], [2, 4]]); 49 | // ensure no mutation has occurred 50 | assert.deepEqual(arrOne, arrOne); 51 | assert.deepEqual(arrTwo, arrTwo); 52 | }); 53 | 54 | it('should sort keys of object', function() { 55 | var obj = { 56 | 5: {}, 57 | 20: {}, 58 | 15: {}, 59 | 1: {} 60 | }; 61 | var sorted = h.sortNumericalKeys(obj); 62 | assert.deepEqual(sorted, [1, 5, 15, 20]); 63 | }); 64 | 65 | it('should sort keys of arrays of object', function() { 66 | var obj = { 67 | 10: [], 68 | 20: [], 69 | 30: [], 70 | 40: [], 71 | 50: [], 72 | 51: [], 73 | 53: [], 74 | 60: [], 75 | 65: [], 76 | 70: [], 77 | 100: [] 78 | }; 79 | var sorted = h.sortNumericalKeys(obj); 80 | assert.deepEqual(sorted, [10, 20, 30, 40, 50, 51, 53, 60, 65, 70, 100]); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /test/IntegrationSpec.js: -------------------------------------------------------------------------------- 1 | // jscs:disable maximumLineLength 2 | 3 | 'use strict'; 4 | 5 | var chai = require('chai'); 6 | var _ = require('lodash'); 7 | var restify = require('restify-clients'); 8 | var demoServer = require('../example/demo'); 9 | 10 | var assert = chai.assert; 11 | var client = restify.createJsonClient({ 12 | url: 'http://localhost:3003' 13 | }); 14 | var stringClient = restify.createStringClient({ 15 | url: 'http://localhost:3003' 16 | }); 17 | 18 | // star tests 19 | describe('Integration tests using the demo app', function() { 20 | before(function(done) { 21 | demoServer.listen(3003, done); 22 | }); 23 | 24 | after(function(done) { 25 | client.close(); 26 | stringClient.close(); 27 | demoServer.close(done); 28 | }); 29 | 30 | describe('Simple handler chains', function() { 31 | it('should return hello world', function(done) { 32 | stringClient.get('/simple', function(err, req, res, data) { 33 | assert.ifError(err); 34 | assert.equal(data, 'hello world!'); 35 | done(); 36 | }); 37 | }); 38 | 39 | it('should return hello world and conductor name', function(done) { 40 | stringClient.get('/simple2', function(err, req, res, data) { 41 | assert.ifError(err); 42 | assert.equal(data, 'hello world: simpleConductor2!'); 43 | done(); 44 | }); 45 | }); 46 | 47 | it('should return hello world, conductor name, and message', function(done) { 48 | stringClient.get('/simple3', function(err, req, res, data) { 49 | assert.ifError(err); 50 | assert.equal(data, 'hello world: simpleConductor3 success!'); 51 | done(); 52 | }); 53 | }); 54 | 55 | it('should stop execution of handler chain when calling redirect', function(done) { 56 | stringClient.get('/simple4', function(err, req, res, data) { 57 | assert.ifError(err); 58 | assert.equal(res.statusCode, 302); 59 | assert.notEqual(res.body, 'hello world!'); 60 | done(); 61 | }); 62 | }); 63 | }); 64 | 65 | describe('Props', function() { 66 | it('should return success due to ok query (props1)', function(done) { 67 | stringClient.get('/props?search=hello', function( 68 | err, 69 | req, 70 | res, 71 | data 72 | ) { 73 | assert.ifError(err); 74 | assert.equal(data, 'searchQuery: hello'); 75 | done(); 76 | }); 77 | }); 78 | 79 | it('should return 400 due to invalid query (props1)', function(done) { 80 | client.get('/props?search=foo', function(err, req, res, data) { 81 | assert.ok(err); 82 | assert.equal(res.statusCode, 400); 83 | assert.equal(data.code, 'BadRequest'); 84 | assert.equal(data.message, 'query not allowed!'); 85 | done(); 86 | }); 87 | }); 88 | 89 | it('should return success due to ok query (props2)', function(done) { 90 | client.get('/props2?search=hello', function(err, req, res, data) { 91 | assert.ifError(err); 92 | assert.equal(data, 'searchQuery: hello'); 93 | done(); 94 | }); 95 | }); 96 | 97 | it('should return 400 due to invalid query (props2)', function(done) { 98 | client.get('/props2?search=baz', function(err, req, res, data) { 99 | assert.ok(err); 100 | assert.equal(res.statusCode, 400); 101 | assert.equal(data.code, 'BadRequest'); 102 | assert.equal(data.message, 'query not allowed!'); 103 | done(); 104 | }); 105 | }); 106 | }); 107 | 108 | describe('models', function() { 109 | it('should return local models defined in array', function(done) { 110 | client.get('/model?search=baz', function(err, req, res, data) { 111 | assert.ifError(err); 112 | assert.isString(data.userAgent); 113 | assert.isObject(data.serverEnv); 114 | done(); 115 | }); 116 | }); 117 | 118 | it('should return local models defined in object', function(done) { 119 | client.get('/model2?search=baz', function(err, req, res, data) { 120 | assert.ifError(err); 121 | assert.isString(data.userAgent); 122 | assert.isObject(data.serverEnv); 123 | done(); 124 | }); 125 | }); 126 | 127 | it('should return remote models', function(done) { 128 | this.timeout(10000); 129 | 130 | client.get('/model3?userId=2', function(err, req, res, data) { 131 | assert.ifError(err); 132 | 133 | // assert ip model 134 | assert.isObject(data.ip); 135 | assert.isString(data.ip.ip); 136 | 137 | // assert posts model 138 | assert.isArray(data.posts); 139 | _.forEach(data.posts, function(post) { 140 | assert.equal(post.userId, 2); 141 | assert.isNumber(post.id); 142 | assert.isString(post.title); 143 | assert.isString(post.body); 144 | }); 145 | done(); 146 | }); 147 | }); 148 | 149 | it('should return custom models', function(done) { 150 | client.get('/model4', function(err, req, res, data) { 151 | assert.ifError(err); 152 | assert.equal(data.hello, 'world'); 153 | assert.equal(data.async, true); 154 | done(); 155 | }); 156 | }); 157 | 158 | it('should fetch two async models in series', function(done) { 159 | this.timeout(10000); 160 | 161 | client.get('/model5?text=helloworld', function( 162 | err, 163 | req, 164 | res, 165 | data 166 | ) { 167 | assert.ifError(err); 168 | 169 | // assert ip model 170 | assert.isObject(data.ip); 171 | assert.isString(data.ip.ip); 172 | 173 | // assert posts model 174 | assert.isArray(data.posts); 175 | _.forEach(data.posts, function(post) { 176 | assert.equal(post.userId, 1); 177 | assert.isNumber(post.id); 178 | assert.isString(post.title); 179 | assert.isString(post.body); 180 | }); 181 | 182 | done(); 183 | }); 184 | }); 185 | }); 186 | 187 | describe('inheritance', function() { 188 | it('should inherit from propsConductor, return success due to ok query (inherit1)', function(done) { 189 | client.get('/inherit?search=hello', function(err, req, res, data) { 190 | assert.ifError(err); 191 | assert.equal(data, 'searchQuery: hello'); 192 | done(); 193 | }); 194 | }); 195 | 196 | it('should inherit from propsConductor, return 400 due to invalid query (inherit1)', function(done) { 197 | client.get('/inherit?search=foo', function(err, req, res, data) { 198 | assert.ok(err); 199 | assert.equal(res.statusCode, 400); 200 | assert.equal(data.code, 'BadRequest'); 201 | assert.equal(data.message, 'query not allowed!'); 202 | done(); 203 | }); 204 | }); 205 | 206 | it('should return success due to ok query (inherit2)', function(done) { 207 | client.get('/inherit2?search=hello', function(err, req, res, data) { 208 | assert.ifError(err); 209 | assert.equal(data, 'searchQuery: hello'); 210 | done(); 211 | }); 212 | }); 213 | 214 | it('should return 400 due to invalid query (inherit2)', function(done) { 215 | client.get('/inherit2?search=override', function( 216 | err, 217 | req, 218 | res, 219 | data 220 | ) { 221 | assert.ok(err); 222 | assert.equal(res.statusCode, 400); 223 | assert.equal(data.code, 'BadRequest'); 224 | assert.equal(data.message, 'query not allowed!'); 225 | done(); 226 | }); 227 | }); 228 | 229 | it('should work like inherit2 (inherit3)', function(done) { 230 | client.get('/inherit2?search=override', function( 231 | err, 232 | req, 233 | res, 234 | data 235 | ) { 236 | assert.ok(err); 237 | assert.equal(res.statusCode, 400); 238 | assert.equal(data.code, 'BadRequest'); 239 | assert.equal(data.message, 'query not allowed!'); 240 | done(); 241 | }); 242 | }); 243 | 244 | it('should return conductor name (inherit4)', function(done) { 245 | client.get('/inherit4', function(err, req, res, data) { 246 | assert.ifError(err); 247 | assert.equal(data, 'Name: inheritanceConductor4'); 248 | done(); 249 | }); 250 | }); 251 | 252 | it('should return a proper data structure (inherit5)', function(done) { 253 | client.get('/inherit5', function(err, req, res, data) { 254 | assert.ifError(err); 255 | assert.equal(data.a, 1); 256 | assert.equal(data.b, 2); 257 | assert.isString(data.timestamp); 258 | done(); 259 | }); 260 | }); 261 | }); 262 | 263 | describe('sharding', function() { 264 | it('should return text response', function(done) { 265 | client.get('/shardText', function(err, req, res, data) { 266 | assert.ifError(err); 267 | assert.equal(data, 'name: text'); 268 | done(); 269 | }); 270 | }); 271 | 272 | it('should return json response', function(done) { 273 | client.get('/shardJson', function(err, req, res, data) { 274 | assert.ifError(err); 275 | assert.isObject(data); 276 | assert.equal(data.name, 'json'); 277 | done(); 278 | }); 279 | }); 280 | 281 | it('should shard into text response', function(done) { 282 | client.get('/shard?type=text', function(err, req, res, data) { 283 | assert.ifError(err); 284 | assert.equal(data, 'name: preshard'); 285 | done(); 286 | }); 287 | }); 288 | 289 | it('should shard into json response', function(done) { 290 | client.get('/shard?type=json', function(err, req, res, data) { 291 | assert.ifError(err); 292 | assert.isObject(data); 293 | assert.equal(data.name, 'preshard'); 294 | done(); 295 | }); 296 | }); 297 | 298 | it('should shard to next level into json response', function(done) { 299 | client.get('/shard?type=nextLevelJson', function( 300 | err, 301 | req, 302 | res, 303 | data 304 | ) { 305 | assert.ifError(err); 306 | assert.isObject(data); 307 | assert.equal(data.name, 'json'); 308 | done(); 309 | }); 310 | }); 311 | 312 | it('should shard to next level into json response', function(done) { 313 | client.get('/shard?type=nextLevelNotSameLevel', function( 314 | err, 315 | req, 316 | res, 317 | data 318 | ) { 319 | assert.ifError(err); 320 | assert.isObject(data); 321 | assert.equal(data.name, 'json'); 322 | done(); 323 | }); 324 | }); 325 | 326 | it('should error as there are no handlers', function(done) { 327 | client.get('/shard?type=noHandlers', function(err, req, res, data) { 328 | assert.ok(err); 329 | assert.isObject(data); 330 | assert.equal(res.statusCode, 500); 331 | done(); 332 | }); 333 | }); 334 | }); 335 | 336 | describe('object', function() { 337 | it('should return hello world', function(done) { 338 | stringClient.get('/object', function(err, req, res, data) { 339 | assert.ifError(err); 340 | assert.equal(data, 'hello world!'); 341 | done(); 342 | }); 343 | }); 344 | 345 | it('should return hello world and conductor name', function(done) { 346 | stringClient.get('/Object1', function(err, req, res, data) { 347 | assert.ifError(err); 348 | assert.equal(data, 'hello world: simpleConductor2!'); 349 | done(); 350 | }); 351 | }); 352 | 353 | it('should return hello world, conductor name', function(done) { 354 | stringClient.get('/Object2', function(err, req, res, data) { 355 | assert.ifError(err); 356 | assert.equal(data, 'hello world: simpleConductor2!'); 357 | done(); 358 | }); 359 | }); 360 | 361 | it('should return a 404', function(done) { 362 | stringClient.get('/object3', function(err, req, res, data) { 363 | assert.ok(err); 364 | assert.equal(res.statusCode, 404); 365 | assert.equal( 366 | data, 367 | '{"code":"ResourceNotFound","message":"/object3 does not exist"}' 368 | ); 369 | done(); 370 | }); 371 | }); 372 | }); 373 | 374 | describe('conductor handler chains', function() { 375 | it('should support conductor handler chain', function(done) { 376 | stringClient.get('/simpleConductorHandlers', function( 377 | err, 378 | req, 379 | res, 380 | data 381 | ) { 382 | assert.ifError(err); 383 | assert.equal(data, 'hello world!'); 384 | done(); 385 | }); 386 | }); 387 | }); 388 | }); 389 | -------------------------------------------------------------------------------- /test/runSpec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert-plus'); 4 | var _ = require('lodash'); 5 | var rc = require('../lib'); 6 | var runHandlers = require('../lib/handlers/run')(); 7 | 8 | function mockReq(conductor) { 9 | return { 10 | _restifyConductor: { 11 | conductor: conductor 12 | }, 13 | startHandlerTimer: _.noop, 14 | endHandlerTimer: _.noop 15 | }; 16 | } 17 | 18 | function createConductor(handlers) { 19 | return rc.createConductor({ 20 | name: 'A', 21 | handlers: { 22 | 0: handlers 23 | } 24 | }); 25 | } 26 | 27 | describe('Restify Conductor run', function() { 28 | var conductorA; 29 | var calledOnce = false; 30 | var calledOnceHandler = function(req, res, next) { 31 | calledOnce = true; 32 | next(); 33 | }; 34 | var handlers = [calledOnceHandler]; 35 | 36 | beforeEach(function() { 37 | conductorA = createConductor(handlers); 38 | }); 39 | 40 | it('should run the first handler block', function(done) { 41 | runHandlers(mockReq(conductorA), {}, function() { 42 | assert.ok(calledOnce); 43 | done(); 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /tools/githooks/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # commit hook for JShint 4 | make prepush 5 | prepushProcess=$? 6 | 7 | # now output the stuff 8 | exit $prepushProcess 9 | --------------------------------------------------------------------------------