├── .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 | [](https://travis-ci.org/restify/conductor)
5 | [](https://coveralls.io/r/restify/conductor?branch=master)
6 | [](https://david-dm.org/restify/conductor)
7 | [](https://david-dm.org/restify/conductor#info=devDependencies)
8 | [](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 |
--------------------------------------------------------------------------------