{"id":18435385,"url":"https://github.com/restify/conductor","last_synced_at":"2025-04-07T19:32:20.214Z","repository":{"id":34355476,"uuid":"38277541","full_name":"restify/conductor","owner":"restify","description":"an abstraction framework for building composable endpoints","archived":false,"fork":false,"pushed_at":"2023-11-30T14:42:15.000Z","size":117,"stargazers_count":26,"open_issues_count":25,"forks_count":5,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-03-23T00:06:50.872Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/restify.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2015-06-29T23:56:16.000Z","updated_at":"2024-03-10T08:59:06.000Z","dependencies_parsed_at":"2023-11-30T15:48:25.590Z","dependency_job_id":null,"html_url":"https://github.com/restify/conductor","commit_stats":null,"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/restify%2Fconductor","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/restify%2Fconductor/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/restify%2Fconductor/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/restify%2Fconductor/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/restify","download_url":"https://codeload.github.com/restify/conductor/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247716437,"owners_count":20984241,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-06T06:08:04.262Z","updated_at":"2025-04-07T19:32:19.938Z","avatar_url":"https://github.com/restify.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# restify-conductor\n\n\u003c!-- [![NPM Version](https://img.shields.io/npm/v/errors.svg)](https://npmjs.org/package/errors) --\u003e\n[![Build Status](https://travis-ci.org/restify/conductor.svg?branch=master)](https://travis-ci.org/restify/conductor)\n[![Coverage Status](https://coveralls.io/repos/restify/conductor/badge.svg?branch=master)](https://coveralls.io/r/restify/conductor?branch=master)\n[![Dependency Status](https://david-dm.org/restify/conductor.svg)](https://david-dm.org/restify/conductor)\n[![devDependency Status](https://david-dm.org/restify/conductor/dev-status.svg)](https://david-dm.org/restify/conductor#info=devDependencies)\n[![bitHound Score](https://www.bithound.io/github/restify/conductor/badges/score.svg)](https://www.bithound.io/github/restify/conductor/master)\n\n\u003e an abstraction framework for building composable endpoints in restify\n\n\n## Getting Started\n\nInstall the module with: `npm install restify-conductor`\n\n\n## Why?\n\nRestify, like other Node.js frameworks, provides built in support for\n[Connect](https://github.com/senchalabs/connect) style handlers. This simple\nyet elegant solution works well for many scenarios. But sometimes, as\ncomplexity in your application grows, it can become increasingly difficult to\nshare and manage. This module alleviates those pain points by providing a\n`Conductor` construct, which serves as an orchestration layer on top of your\nhandler stack, as well as providing some nice built-in support for fetching\nremote resources. This construct also allows easing \"moving\" of an entire page\nfrom one URL to another, which can be otherwise non-trivial.\n\nThe top 5 reasons for using restify-conductor:\n\n* You want to decouple an endpoint's logic from the URL it is installed to.\n* You want to easily move a page from one URL to another.\n* Your site is large and complex with many pages sharing similar context and\n  handler stacks.\n* You have long (15, 20+) handler stacks, and when the handler stacks change,\n  you want to be able to change it at only one place.\n* You want to be able to reuse existing stacks, but customize their behavior as\n  needed on a per URL basis instead of having to rewrite the entire handler but\n  with one line slightly different.\n* You want to be able to serve two completely different responses to the same\n  URL, based on user state (e.g., home page for logged in vs logged out),\n  __without__ redirecting.\n\n\n## Basic Usage\n\n### Handlers\n\nAssuming you have a Restify server, you can install `Conductor` objects at a\ngiven endpoint:\n\n```js\nvar restify = require('restify');\nvar rc = require('restify-conductor');\n\nvar server = restify.createServer();\n\nvar simpleConductor = rc.createConductor({\n    name: 'simpleConductor',\n    handlers: [\n        function render(req, res, next) {\n            res.send(200, 'hello world!');\n            return next();\n        }\n    ]\n});\n\nrc.get('/foo', simpleConductor, server);\n```\n\nThis conductor has only one handler, a render function that renders 'hello\nworld!' to the client. Like other frameworks, you can pass in multiple handlers\nwhich will run in serial.\n\n\n### Props\n\nWe can extend this conductor object with the concept of `props`, or immuatable\nproperties. Simply pass in a function to the `createConductor` method, and it\nwill be invoked at creation time. The object returned by the props function is\nimmutable over the lifetime of the server. You can access these props from your\nhandlers:\n\n\n```js\n\nvar propsConductor = rc.createConductor({\n    name: 'propsConductor',\n    props: function() {\n        return {\n            blacklistQueries: ['foo']\n        };\n    },\n    handlers: [\n        function validateQuery(req, res, next) {\n            // retrieve props via helper.\n            var blacklist = rc.getProps(req, 'blacklistQueries');\n\n            // check if the search query is in the allowed list\n            var query = req.query.search;\n            if (blacklist.indexOf(query) === -1) {\n                // if it is, return a 500\n                return next(new restify.errors.InternalServerError('blacklisted query!'));\n            }\n\n            // otherwise, we're good!\n            return next();\n        },\n        function render(req, res, next) {\n            // respond with the valid query\n            res.render(req.query);\n        }\n    ]\n});\n\nrc.get('/props', propsConductor, server);\n```\n\nLooking at this example, we _could_ just hard code the value of blacklistQueries\ninto the handler. But using props allows us to easily share this handler across\nother conductors that may have different values for the blacklist.\n\n\n### Models\n\nrestify-conductor also comes with first class support for the concept of\nmodels. Models are sources of data needed by your conductor. The source of the\nmodel data can be anything. It can be the request (e.g., user agent parsing),\nor a data store of some kind (Redis, mySQL), or even a remote data source.\n\nThe Model construct provides a lifecycle of methods available to you to act on the data.\n\n* `before` {Function} - a function invoked before the request for your data\n  source is made\n* `isValid` {Function} - a function invoked to ensure validity of your payload\n* `after` {Function} - a function invoked after the isValid check to do\n  additional manipulation or storage of your data\n* `fallback` {Function} - a function that allows you to set model data in the\n  event the request fails\n\n\nCreating models is easy:\n\n```js\n// a model whose data source is the request\nvar userAgent = rc.createModel({\n    name: 'userAgent',\n    data: req.headers['user-agent']\n});\n\n\n// a model whose data source is coming from a remote location\nvar ipModel = rc.createModel({\n    name: 'ip',\n    host: 'jsonip.com',\n    isValid: function(data) {\n        // validate the payload coming back, it should have two fields.\n        return (data.hasOwnProperty('ip') \u0026\u0026 data.hasOwnProperty('about'));\n    }\n});\n```\n\nYou can then consume them in your conductor. The default behavior is to fetch all\nmodels specified in the models config in parallel:\n\n\n```js\nvar modelConductor = new Conductor({\n    name: 'modelConductor',\n    models: [ userAgent, ip ],\n    handlers: [\n        rc.handlers.buildModels(), // fetch models in parallel\n        function render(req, res, next) {\n            // now we can access the models\n            var uaModel = rc.getModel(req, 'userAgent'),\n                ipModel = rc.getModel(req, 'ip');\n\n            // put together a payload.\n            var out = {\n                userAgentModel: uaModel.data,\n                ipModel: ipModel.data\n            };\n\n            res.render(out, next);\n        }\n    ]\n});\n```\n\nIt is also possible to fetch multiple models in serial, if you have models\ndependent on the output of another async model. To do so, you can pass an object\ninto models instead, with each key of the object specifying an array of models.\nThis allows you to address each 'bucket' of models using the key:\n\n\n```js\nvar seriesModelConductor = rc.createConductor({\n    name: 'seriesModelConductor',\n    models: {\n        bucketA: [ ip, userAgent ],\n        bucketB: [ date ]\n    },\n    handlers: [\n        rc.handlers.buildModels('bucketA'), // fetch bucketA models in parallel\n        function check(req, res, next) {\n            var ipModel = rc.getModels(req, 'ip');\n            var uaModel = rc.getModels(req, 'userAgent');\n\n            // the ip and user agent models are done!\n            assert.ok(ipModel);\n            assert.ok(uaModel);\n\n            return next();\n        },\n        rc.handlers.buildModels('bucketB'),  // then, fetch bucketB in parallel\n        function render(req, res, next) {\n            var allModels = rc.getModels(req);\n\n            // make sure we got three models\n            assert.equal(_.size(allModels), 3);\n        }\n    ]\n});\n\n```\n\n\n### Inheritance/Composition\n\nConductors can also be inherited from. Inheriting from another conductor\nautomatically gives you the same props and handlers as the parent conductor.\nProps can be mutated by the inheriting conductor, but handlers cannot. However,\nhandlers can be appended and prepended to. Let's look at props first.\n\n```js\n// here is our parent conductor\nvar parentConductor = rc.createConductor({\n    name: 'parent',\n    props: function() {\n        return {\n            count: 0,\n            candies: [ 'twix', 'snickers', 'kit kat' ]\n        };\n    }\n});\n\n// now we inherit by specifying a deps config\nvar childConductor = rc.createConductor({\n    name: 'child',\n    deps: [ parentConductor ],\n    props: function(inheritedProps) {\n        // children conductor are provided with the parent props.\n        // you can choose to mutate this object for the child conductor.\n\n        // note that mutating inheritedProps does NOT affect the parent\n        // conductor's props!\n        inheritedProps.count += 1;\n        inheritedProps.candies = inheritedProps.candies.concat('butterfinger');\n\n        // like the parent conductor, this returned value will become immutable\n        return inheritedProps;\n    },\n    handlers: [\n        function render(req, res, next) {\n            var props = rc.getProps(req);\n\n            res.render(props, next);\n            // =\u003e will render:\n            // {\n            //      count: 1,\n            //      candies: [ 'twix', 'snickers', 'kit kat', 'butterfinger' ]\n            // }\n        }\n    ]\n});\n```\n\nHandlers can also be inherited, and appended to:\n\n```js\nvar parentConductor = rc.createConductor({\n    name: 'parent',\n    handlers: [ addName ]\n});\n\nvar childConductor = rc.createConductor({\n    name: 'child',\n    deps: [ parentConductor ],\n    handlers: [ render ]\n});\n\n// =\u003e resulting handler stack:\n// [ addName, render ]\n```\n\nIt is possible to prepend and insert handlers arbitrarily into the handler\nstack, by using the concept of handler 'blocks'. By changing handlers to an\narray of arrays, we can implicitly specify ordering of different handler stacks\nwhen doing inheritance:\n\n\n```js\nvar parentConductor = rc.createConductor({\n    name: 'parent',\n    handlers: [\n        [],\n        addName\n    ]\n});\n\nvar childConductor = rc.createConductor({\n    name: 'child',\n    deps: [ parentConductor ],\n    handlers: [\n        addRequestId,\n        [],\n        render\n    ]\n});\n\n// =\u003e resulting handler stack:\n// [ addRequestId, addName, render ]\n\n```\n\nHowever, using the array index as an implicit ordering mechanism can be a bit\nconfusing, so it is recommended to use an object with numerical keys. Using\nnumerical keys also makes it easy to insert handlers inbetween existing\n'blocks'. Note that duplicated keys are appended to:\n\n\n```js\nvar parentConductor = rc.createConductor({\n    name: 'parent',\n    handlers: {\n        10: [ addName ]\n    }\n});\n\nvar childConductor = rc.createConductor({\n    name: 'child',\n    deps: [ parentConductor ],\n    handlers: {\n        5:  [ addRequestId ],\n        10: [ addTimestamp ],\n        15: [ render ]\n    }\n});\n\n\n// =\u003e the merged handlers:\n// {\n//      5:  [ addRequestId ],\n//      10: [ addName, addTimestamp ],\n//      15: [ render ]\n//\n// }\n\n// =\u003e and the resulting execution order of the handler stack:\n// [ addRequestId, addName, addTimestamp, render ]\n\n```\n\n\n## Composition\n\nBecause `deps` is an array, you can also opt for flatter trees using more\ncompositional conductors:\n\n```js\nvar compositionConductor = new Conductor({\n    name: 'compositionConductor',\n    deps: [ baseCondcutor, anotherConductor, yetAnotherConductor ]\n});\n```\n\nUsing a compositional pattern may make easier to see at a glance what the\nhandler stacks look like, with the trade off of being slightly less DRY. It will\nbe up to you to determine what works best for your application.\n\nIn any case, both these inheritance and composition pattern allow for some very\npowerful constructs. It also allows you to easily move conductors from one URL\npath to another completely transparently.\n\n\n## Conductor Sharding\n\nYou may sometimes want to render a different page under the same URL. A great\nexample is the root URL, '/'. If the user is logged in, you want to be able to\nserve a logged in experience. If the user is logged out, you want to serve them\na login page. However, the URL needs to stay the same in both cases.\n\nrestify-constructor provides this capability through sharding. Consider the two\npages above, let's mount the logged in experience at /home, and the logged out\nexperience at /login:\n\n```js\nrc.get('/home', homeConductor);\nrc.get('/login', loginConductor);\n\n// how do we handle this scenario?\n// rc.get('/', ?)\n```\n\nWith shards, you can reuse existing conductors by simply \"sharding\" to them:\n\n```js\nvar shardConductor = rc.createConductor({\n    name: 'shardConductor',\n    models: [ userInfo ],\n    handlers: {\n        10: [\n            rc.buildModels()\n        ],\n        20: [\n            function shard(req, res, next) {\n                // fetch the userInfo model we just built.\n                var userModel = rc.getModels(req, 'userInfo');\n\n                if (userModel.data.isLoggedIn === true) {\n                    rc.shardConductor(req, homeConductor);\n                } else {\n                    rc.shardConductor(req, loginConductor);\n                }\n\n                return next();\n            }\n        ]\n    }\n})\n\nrc.get('/', shardConductor);\n```\n\nNote that in this example we sharded the conductor at index 20. That means the\nrequest will continue to flow through the handler stacks defined at\nhomeConductor and loginConductor starting from the next index _higher_ than 20.\nIn other words, __if homeConductor or loginConductor have handlers defined at\nany indicies 20 and lower, they will NOT be run__. They will only be run in\nthe non sharded scenario, where the user directly hits /home or /login.\n\nOne of the main advantages of sharding is that there is no redirect. You can\nserve the desired experience directly within the same request, by simply\nreusing existing conductors.\n\n\n## API\n\n_(Coming soon)_\n\n\n## Contributing\n\nAdd unit tests for any new or changed functionality. Ensure that lint and style\nchecks pass.\n\nTo start contributing, install the git preush hooks:\n\n```sh\nmake githooks\n```\n\nBefore committing, run the prepush hook:\n\n```sh\nmake prepush\n```\n\nIf you have style errors, you can auto fix whitespace issues by running:\n\n```sh\nmake codestyle-fix\n```\n\n\n## License\n\nCopyright (c) 2015 Netflix, Inc.\n\nLicensed under the MIT license.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frestify%2Fconductor","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frestify%2Fconductor","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frestify%2Fconductor/lists"}