├── Interpolation.md ├── test ├── sample.txt ├── sample.json ├── malformed.json ├── workflow1.js ├── task-delay.js ├── task-readText-test.js ├── workflow-createAdapter-test.js ├── workflow-task-resolver-test.js ├── workflow-set-test.js ├── task-random.js ├── task-readJson-test.js ├── workflow-code-test.js ├── workflow-condition-test.js ├── workflow-map-test.js ├── workflow-stats.js ├── task-regex-test.js ├── task-while.js ├── task-eachSeries.js ├── workflow-logging-test.js ├── task-workflow-test.js ├── workflow-terminate.js ├── workflow-finally-test.js ├── interpolator.test.js ├── workflow-onError-test.js └── workflow-api-test.js ├── tutorial ├── 01hello.js ├── 02context.js ├── 03sequence.js ├── 04map.js └── 05customtask.js ├── tasks ├── random.js ├── http.js ├── parallel.js ├── readText.js ├── raise.js ├── warezSequence.js ├── delay.js ├── readJson.js ├── sql.js ├── set.js ├── workflow.js ├── sequence.js ├── log.js ├── code.js ├── eachSeries.js ├── regex.js ├── while.js ├── map.js └── sql │ └── pg.js ├── examples ├── workflow-template-demo.js ├── workflow-sf-insert.js ├── workflow-sf-delete.js ├── workflow-warez-seq-demo.js ├── workflow-sql-soft-delete-demo.js ├── workflow-regex-demo.js ├── workflow-demo.js └── workflow-sql-insert-demo.js ├── .gitignore ├── stress-test ├── tasks │ └── createDeepObjects.js ├── stress-workflows │ └── workflow1.js └── stress-test.js ├── package.json ├── src └── interpolation │ ├── index.js │ ├── Parser.js │ └── Interpolator.js ├── ReleaseNotes.md ├── README.md ├── typings └── mocha │ └── mocha.d.ts ├── TUTORIAL.md └── index.js /Interpolation.md: -------------------------------------------------------------------------------- 1 | #worksmith interpolation guide 2 | 3 | -------------------------------------------------------------------------------- /test/sample.txt: -------------------------------------------------------------------------------- 1 | ever talked to kurtz? you don't talk to kurtz. you listen to him. -------------------------------------------------------------------------------- /test/sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "ever talked to kurtz?" :"you don't talk to kurtz. you listen to him." 3 | } -------------------------------------------------------------------------------- /test/malformed.json: -------------------------------------------------------------------------------- 1 | { 2 | "ever talked to kurtz? :"you don't talk to kurtz. you listen to him." 3 | } -------------------------------------------------------------------------------- /test/workflow1.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | task:"set", 3 | name:"result", 4 | value:"hello" 5 | } -------------------------------------------------------------------------------- /tutorial/01hello.js: -------------------------------------------------------------------------------- 1 | var worksmith = require('..') 2 | 3 | var workflow = worksmith({task:"log", message:"hello"}) 4 | workflow({}, function(err, res) { 5 | console.log("workflow executed") 6 | }) -------------------------------------------------------------------------------- /tutorial/02context.js: -------------------------------------------------------------------------------- 1 | var worksmith = require('..') 2 | 3 | var context = { helloMessage:"hello world" } 4 | var workflow = worksmith({task:"log", message:"@helloMessage"}) 5 | workflow(context, function(err, res) { 6 | console.log("workflow executed") 7 | }) -------------------------------------------------------------------------------- /tasks/random.js: -------------------------------------------------------------------------------- 1 | module.exports = function(definition) { 2 | return function(context) { 3 | return function(done) { 4 | var res = context.get(definition.return) == "string" ? Math.random().toString() : Math.random(); 5 | done(null, res) 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /tasks/http.js: -------------------------------------------------------------------------------- 1 | var request = require('request') 2 | 3 | 4 | 5 | function HttpActivity(definition) { 6 | return function(context) { 7 | return function(done) { 8 | var method = context.get(definition.method) || 'GET' 9 | done() 10 | } 11 | } 12 | } 13 | 14 | module.exports = HttpActivity -------------------------------------------------------------------------------- /tutorial/03sequence.js: -------------------------------------------------------------------------------- 1 | var worksmith = require('..') 2 | 3 | var workflow = worksmith({ 4 | task:"sequence", 5 | items: [ 6 | { task:"log", message:"@p1" }, 7 | { task:"log", message:"@p2" } 8 | ] 9 | }); 10 | 11 | workflow({p1:"answer", p2:42}, function(err, res) { 12 | console.log("workflow executed") 13 | }) -------------------------------------------------------------------------------- /tasks/parallel.js: -------------------------------------------------------------------------------- 1 | var async = require('async') 2 | var workflow = require('../') 3 | 4 | module.exports = function(node) { 5 | return function(context) { 6 | return function(done) { 7 | var tasks = node.items.map(function(item) { 8 | return workflow.define(item)(context) 9 | }) 10 | async.parallel(tasks,done) 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tasks/readText.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash') 2 | var fs = require('fs') 3 | 4 | var path = require('path') 5 | module.exports = function(definition) { 6 | return function(context) { 7 | return function(done) { 8 | var fsPath = context.get(definition.path) 9 | var content = fs.readFileSync(fsPath).toString() 10 | done(null, content) 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /tasks/raise.js: -------------------------------------------------------------------------------- 1 | var workflow = require('../') 2 | var debug = require('debug')('workflow:activities:log') 3 | 4 | 5 | //TODO:set logger framework parametrizable 6 | //params 7 | //message: the dynatext to display 8 | 9 | module.exports = function (node) { 10 | 11 | return function (context) { 12 | 13 | return function(done) { 14 | var a; 15 | return a.a; 16 | } 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /tasks/warezSequence.js: -------------------------------------------------------------------------------- 1 | var async = require('async') 2 | var workflow = require('./') 3 | 4 | module.exports = function(node) { 5 | return function(context) { 6 | return function(done) { 7 | var tasks = node.items.map(function(item) { 8 | return workflow.define(node.warez[item])(context) 9 | }) 10 | async.series(tasks,done) 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tasks/delay.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('workflow:activities:delay') 2 | module.exports = function(node) { 3 | return function(context) { 4 | 5 | execute.annotations = {inject: ["duration"]} 6 | 7 | function execute(duration, done) { 8 | debug("delay activity started %s", duration) 9 | setTimeout(done, parseInt(duration)); 10 | } 11 | 12 | return execute 13 | } 14 | } -------------------------------------------------------------------------------- /tutorial/04map.js: -------------------------------------------------------------------------------- 1 | var worksmith = require('..') 2 | 3 | var workflow = worksmith({ 4 | task:"sequence", 5 | items: [ 6 | { task:"map", map: { f1:"@p1", f2:"@p2" }, resultTo:"mapData.d1" }, 7 | { task:"map", ">mapData.d2": ["@p1","Hello"] }, 8 | { task:"log", message:"@mapData" } 9 | ] 10 | }); 11 | 12 | workflow({p1:"answer", p2:42}, function(err, res) { 13 | console.log("workflow executed") 14 | }) -------------------------------------------------------------------------------- /tasks/readJson.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash') 2 | 3 | var path = require('path') 4 | module.exports = function(definition) { 5 | return function(context) { 6 | return function(done) { 7 | var jsonPath = context.get(definition.path) 8 | jsonPath = path.resolve(jsonPath) 9 | var content = require(jsonPath) 10 | content = _.assignIn({}, content) 11 | done(null, content) 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /tasks/sql.js: -------------------------------------------------------------------------------- 1 | var pg = require('pg') 2 | 3 | function executeSqlActivity(definition) { 4 | return function(context) { 5 | return function(done) { 6 | var sqlText = context.get(definition.command), 7 | params = context.get(definition.params), 8 | cnStr = context.get(definition.connection) 9 | console.log("@@@", pg.connect) 10 | } 11 | } 12 | } 13 | 14 | module.exports = executeSqlActivity -------------------------------------------------------------------------------- /examples/workflow-template-demo.js: -------------------------------------------------------------------------------- 1 | 2 | var workflow = require('./src/tasks') 3 | 4 | var taskDef = workflow.define({ 5 | task:"sequence", 6 | items:[ 7 | {task:"set",name:"a", value:"value_a" }, 8 | {task:"set",name:"b", value:"value_b" }, 9 | {task:"log", message: { template: "{{a}}/{{b}}"}} 10 | ] 11 | }) 12 | 13 | 14 | var ctx = { } 15 | var task = taskDef(ctx); 16 | 17 | task(function(err, result) { 18 | console.log("!!@@done", err) 19 | }) 20 | -------------------------------------------------------------------------------- /examples/workflow-sf-insert.js: -------------------------------------------------------------------------------- 1 | 2 | var workflow = require('./src/tasks') 3 | 4 | var taskDef = workflow('./config/insert-sf-entities.json') 5 | 6 | var ctx = { 7 | config: require('./config/default.json'), 8 | content: { 9 | id: "sf-id" + Math.random(), 10 | version:1 11 | }, 12 | message: { 13 | routingKey: "salesforce.v1.notifications.order.created" 14 | } 15 | } 16 | var task = taskDef(ctx); 17 | 18 | task(function(err, result) { 19 | console.log("!!@@done", err) 20 | }) 21 | -------------------------------------------------------------------------------- /examples/workflow-sf-delete.js: -------------------------------------------------------------------------------- 1 | 2 | var workflow = require('./src/tasks') 3 | 4 | var taskDef = workflow('./config/delete-sf-entities.json') 5 | 6 | var ctx = { 7 | config: require('./config/default.json'), 8 | content: { 9 | id: "sf-id" + Math.random(), 10 | version:1 11 | }, 12 | message: { 13 | routingKey: "salesforce.v1.notifications.order.cancelled" 14 | } 15 | } 16 | var task = taskDef(ctx); 17 | 18 | task(function(err, result) { 19 | console.log("!!@@done", err) 20 | }) 21 | -------------------------------------------------------------------------------- /tasks/set.js: -------------------------------------------------------------------------------- 1 | var hb = require('handlebars') 2 | var workflow = require('../') 3 | var debug = require('debug')('workflow:activities:set') 4 | 5 | setActivity.annotations = {inject: ["name","value"]} 6 | function setActivity(node) { 7 | return function (context) { 8 | return function(name, value, done) { 9 | debug("setting value %s as %s", value, node.name) 10 | context.set(name, value) 11 | done(); 12 | } 13 | } 14 | } 15 | module.exports = setActivity -------------------------------------------------------------------------------- /tasks/workflow.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('worksmith:activities:workflow') 2 | 3 | var worksmith = require('../') 4 | 5 | module.exports = function(node) { 6 | 7 | return function build(context) { 8 | 9 | function execute(done) { 10 | var wfs = context.get(node.source); 11 | var wf = worksmith(wfs); 12 | var ctx = context.get(node.context) || context; 13 | wf(ctx, done) 14 | } 15 | 16 | return execute; 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/task-delay.js: -------------------------------------------------------------------------------- 1 | var worksmith = require('..') 2 | var assert = require('assert') 3 | 4 | describe("delay activity", function() { 5 | 6 | it("should delay workflow", function(done) { 7 | this.slow() 8 | var wf = worksmith({task:"delay", duration:500 }); 9 | var before = Date.now() 10 | wf({}, function(err, result, ctx) { 11 | var after = Date.now() 12 | assert.ok(after - before >= 500, 'The workflow was not delayed') 13 | done(null, result) 14 | }) 15 | }) 16 | }) -------------------------------------------------------------------------------- /tasks/sequence.js: -------------------------------------------------------------------------------- 1 | var async = require('async') 2 | var workflow = require('../') 3 | 4 | module.exports = function(node) { 5 | 6 | var steps = node.items.map(function(item) { 7 | return workflow.define(item) 8 | }) 9 | 10 | return function build(context) { 11 | 12 | var tasks = steps.map(function(step) { 13 | return step(context); 14 | }) 15 | 16 | function execute(done) { 17 | async.series(tasks,done) 18 | } 19 | 20 | return execute; 21 | 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/workflow-warez-seq-demo.js: -------------------------------------------------------------------------------- 1 | var workflow = require('./src/tasks') 2 | 3 | var taskDefinition = workflow.define( 4 | { task: "warezSequence", items: ["log1","log2","delay","set"], 5 | warez: { 6 | log1: {task:"log", message:"hello there"}, 7 | log2: {task:"log"}, 8 | delay: {task:"delay", duration:1200}, 9 | set: {task:"set", name:"a", value:"b"} 10 | } 11 | }); 12 | 13 | var ctx = {} 14 | var task = taskDefinition(ctx); 15 | task(function(err, result) { 16 | console.log("executed", ctx); 17 | }); 18 | 19 | //demo_flow_of_tasks(); 20 | 21 | 22 | -------------------------------------------------------------------------------- /test/task-readText-test.js: -------------------------------------------------------------------------------- 1 | var worksmith = require('..') 2 | var assert = require('assert') 3 | describe("readText activity", function() { 4 | var expected = "ever talked to kurtz? you don\'t talk to kurtz. you listen to him." 5 | 6 | it("should read up the specified file", function(done) { 7 | var wf = worksmith({task:"readText", resultTo:"r", path:"./test/sample.txt" }); 8 | wf({}, function(err, result, ctx) { 9 | assert.ifError(err, err); 10 | assert.equal(ctx.r,expected, "result does not match") 11 | done(null, result) 12 | }) 13 | }) 14 | 15 | 16 | 17 | }) -------------------------------------------------------------------------------- /tasks/log.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('workflow:activities:log') 2 | 3 | LogActivity.annotations = { inject: ["message", "level"] } 4 | 5 | function LogActivity(node) { 6 | var worksmith = this; 7 | return function (context) { 8 | return function(message, level, done) { 9 | level = level || (worksmith.hasLogLevel("info") ? "info" : "log") 10 | if (!worksmith.hasLogLevel(level)) return done(new Error("The configured logger has no method " + level)) 11 | worksmith.log(level, message || "Log activity") 12 | done() 13 | } 14 | } 15 | 16 | } 17 | module.exports = LogActivity -------------------------------------------------------------------------------- /tutorial/05customtask.js: -------------------------------------------------------------------------------- 1 | var worksmith = require('..') 2 | 3 | var wf = { 4 | task : function define(params) { 5 | //params will contain the tasks json config eg 6 | // {task:"job2", message:"@p1", "level":"warn"} 7 | return function(context) { 8 | //context will hold the execute state e.g. 9 | //{p1:"hello"} 10 | return function(done) { 11 | console[context.get(params.level)](context.get(params.message)); 12 | done() 13 | } 14 | 15 | } 16 | }, 17 | message:"@p1", 18 | level:"error" 19 | } 20 | 21 | worksmith(wf)({p1:"hello"}, console.log) -------------------------------------------------------------------------------- /tasks/code.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('workflow:activities:code') 2 | 3 | 4 | function codeActivity(definition) { 5 | 6 | return function(context) { 7 | execute.annotations = {inject: ["execute","inject"]} 8 | function execute(execute, inject, done) { 9 | var args = []; 10 | if (execute.length > 1 && Array.isArray(inject)) { 11 | inject.forEach(function(name) { 12 | args.push(context.get(name)) 13 | }) 14 | } 15 | args.push(done) 16 | execute.apply(this, args) 17 | } 18 | return execute; 19 | } 20 | } 21 | 22 | 23 | 24 | module.exports = codeActivity -------------------------------------------------------------------------------- /test/workflow-createAdapter-test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var worksmith = require('../') 3 | 4 | describe("WorkSmith API - createAdapter", function() { 5 | 6 | it("should invoke lodash.filter", function(done) { 7 | worksmith.use("_", worksmith.createAdapter(require('lodash'))) 8 | var context = {items:[{field:"value"}, {field2:"value2"}]} 9 | var workflow = worksmith({ 10 | task: "_/filter", 11 | arguments: ["@items", { field:"value"} ], 12 | resultTo: "myresult" 13 | }) 14 | workflow(context, function wfresult(err, res, context) { 15 | assert.equal(context.myresult[0], context.items[0]) 16 | done(); 17 | }); 18 | }); 19 | }); -------------------------------------------------------------------------------- /examples/workflow-sql-soft-delete-demo.js: -------------------------------------------------------------------------------- 1 | var workflow = require('./src/tasks') 2 | 3 | var config = require('./config/default.json'); 4 | var workflowDefinition = { task: "sequence", items: [ 5 | {task:"set", name:"keys", value:{ external_id: "abc", version:1, type:1 }}, 6 | {task:"softDeleteDbRecord", 7 | table:"billing_record", 8 | keys:"@keys", 9 | connection: "@config.connectionString", 10 | resultTo:"result" }, 11 | {task:"log", message: "@result"} 12 | ] 13 | } 14 | 15 | var taskDef = workflow.define(workflowDefinition); 16 | var ctx = {} 17 | var task = taskDef(ctx) 18 | 19 | task(function(err, result) { 20 | console.log("workflow completed", err); 21 | }) 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | # Visual Studio files 30 | 31 | *.sln 32 | *.suo 33 | *.njsproj 34 | 35 | # JavaScript symbol cache 36 | 37 | .ntvs_analysis.dat 38 | *.tmp 39 | -------------------------------------------------------------------------------- /tasks/eachSeries.js: -------------------------------------------------------------------------------- 1 | var workflow = require('../') 2 | var async = require('async') 3 | 4 | function eachSeriesActivity(node) { 5 | return function (context) { 6 | execute.inject = [{name: "subflow", interpolationPolicy: false } ] 7 | function execute(subflow, done) { 8 | var items = context.get(node.items) 9 | var itemKey = context.get(node.itemKey) || 'item' 10 | var subContext = Object.create(context) 11 | async.eachSeries(items, function(item, cb) { 12 | setImmediate(function() { 13 | subContext.set(itemKey, item) 14 | workflow(subflow)(subContext, cb) 15 | }) 16 | }, done) 17 | } 18 | return execute; 19 | } 20 | } 21 | 22 | module.exports = eachSeriesActivity -------------------------------------------------------------------------------- /stress-test/tasks/createDeepObjects.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash') 2 | 3 | module.exports = function(definition) { 4 | return function build(context) { 5 | 6 | execute.annotations = { inject: ['size', 'depth', 'prefix'] } 7 | 8 | function execute(size, depth, prefix, done) { 9 | var objects = [] 10 | _.times(size, function(n) { 11 | objects.push(createDeepObject(depth)) 12 | }) 13 | done(null, objects) 14 | 15 | function createDeepObject(level) { 16 | if (level < 0) return { value: _.random(0, 10000)} 17 | var newLevel = {} 18 | newLevel[prefix + level] = createDeepObject(level - 1) 19 | return newLevel 20 | } 21 | } 22 | 23 | 24 | return execute 25 | } 26 | } -------------------------------------------------------------------------------- /test/workflow-task-resolver-test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var worksmith = require('../') 3 | 4 | 5 | describe("WorkSmith task resolution", function() { 6 | 7 | it("should set a context field to a constant value", function(done) { 8 | worksmith.use("myNS", function(taskName) { 9 | return function(definition) { 10 | return function(context) { 11 | return function(done) { 12 | done(null, taskName) 13 | } 14 | } 15 | } 16 | }) 17 | var context = {}; 18 | var wi = worksmith({ "task":"myNS/abc", resultTo:"abc" })(context); 19 | 20 | wi(function(err, result) { 21 | assert.equal(context.abc, "abc", "context field must be set") 22 | done(); 23 | }) 24 | }) 25 | 26 | 27 | }) -------------------------------------------------------------------------------- /examples/workflow-regex-demo.js: -------------------------------------------------------------------------------- 1 | var workflow = require('./src/tasks') 2 | 3 | var taskDefinition = workflow.define({ 4 | task: "warezSequence", 5 | items: ["log1","regex","log2"], 6 | warez: { 7 | log1: {task:"log", message:"hello there"}, 8 | regex: {task:"regex", 9 | value:"@message.routingKey", 10 | pattern:"(?[^.]*).\ 11 | (?[^.]*).\ 12 | (?[^.]*).\ 13 | (?[^.]*).\ 14 | (?[^.]*).", 15 | resultTo:"result"}, 16 | log2: {task:"log", message:"@result"} 17 | } 18 | }); 19 | 20 | var ctx = { 21 | message: { 22 | routingKey: "salesforce.v1.notifications.order.created" 23 | } 24 | } 25 | var task = taskDefinition(ctx); 26 | task(function(err, result) { 27 | //console.log("executed", ctx); 28 | }); 29 | 30 | //demo_flow_of_tasks(); 31 | 32 | 33 | -------------------------------------------------------------------------------- /tasks/regex.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('workflow:activities:log') 2 | var xregexp = require('xregexp') 3 | var XRegExp = xregexp.XRegExp 4 | 5 | module.exports = function (node) { 6 | 7 | //TODO: refactor and place this to wf creator level 8 | Object.keys(node).forEach(function (key) { 9 | if (key[0] === ">") { 10 | node.value = node[key]; 11 | node.resultTo = key.slice(1); 12 | delete node[key] 13 | } 14 | }); 15 | //for performance considerations the regex pattern should be constructed w/o using context variables 16 | var p = XRegExp(node.pattern,'x'); 17 | 18 | return function(context) { 19 | return function(done) { 20 | var str = context.get(node.value); 21 | var match = XRegExp.exec(str, p); 22 | done(null, match); 23 | } 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /tasks/while.js: -------------------------------------------------------------------------------- 1 | var workflow = require('../') 2 | var async = require('async') 3 | 4 | function whileActivity(node) { 5 | return function (context) { 6 | execute.inject = [{name: "subflow", interpolationPolicy: false } ] 7 | function execute(subflow, done) { 8 | var result 9 | async.whilst(function() { 10 | return context.get(node.test) 11 | }, function(cb) { 12 | setImmediate(function() { 13 | workflow(subflow)(context, function(err, _result) { 14 | if (err) return cb(err) 15 | result = _result 16 | cb() 17 | }) 18 | }) 19 | }, function(err) { 20 | done(err, result) 21 | }) 22 | } 23 | return execute; 24 | } 25 | } 26 | 27 | module.exports = whileActivity -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "worksmith", 3 | "version": "1.0.1", 4 | "description": "A purely functional workflow engine ", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha --exit" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/guidesmiths/worksmith.git" 12 | }, 13 | "keywords": [ 14 | "workflow", 15 | "tasks", 16 | "parallel" 17 | ], 18 | "author": "GuideSmiths - PeterAronZentai", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/guidesmiths/worksmith/issues" 22 | }, 23 | "homepage": "https://github.com/guidesmiths/worksmith", 24 | "dependencies": { 25 | "async": "^0.9.0", 26 | "debug": "^2.1.3", 27 | "handlebars": "^4.1.0", 28 | "lodash": "^4.17.11", 29 | "pg": "^4.3.0", 30 | "xregexp": "^2.0.0" 31 | }, 32 | "devDependencies": { 33 | "mocha": "^5.2.0", 34 | "require-version": "^1.1.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/workflow-set-test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var workflow = require('../') 3 | 4 | describe("WorkSmith setActivity", function() { 5 | 6 | 7 | it("should set a context field to a constant value", function(done) { 8 | var context = {}; 9 | var wi = workflow({ "task":"set", "name":"field", "value":"value" })(context); 10 | 11 | wi(function(err, result) { 12 | assert.equal(context.field, "value", "context field must be set") 13 | done(); 14 | }) 15 | }) 16 | 17 | it("should set a context field to a reference value", function(done) { 18 | var context = { field1: "value1" }; 19 | var wi = workflow({ "task":"set", "name":"field2", "value":"@field1" })(context); 20 | 21 | wi(function(err, result) { 22 | assert.equal(context.field2, "value1", "context field must be set") 23 | done(); 24 | }) 25 | }) 26 | 27 | 28 | }) -------------------------------------------------------------------------------- /test/task-random.js: -------------------------------------------------------------------------------- 1 | var worksmith = require('..') 2 | var assert = require('assert') 3 | describe("random activity", function() { 4 | it("should produce a random num", function(done) { 5 | var wf = worksmith({task:"random", resultTo:"r" }); 6 | wf({}, function(err, result, ctx) { 7 | assert.ifError(err, err); 8 | assert.ok(ctx.r, "result must be set") 9 | assert.equal(typeof ctx.r, "number") 10 | done(null, result) 11 | }) 12 | }) 13 | it("should produce a random string", function(done) { 14 | var wf = worksmith({task:"random", resultTo:"r", return:"string" }); 15 | wf({}, function(err, result, ctx) { 16 | assert.ifError(err, err); 17 | assert.ok(ctx.r, "result must be set") 18 | assert.ok(ctx.r.match(/^[0-9\.]+$/), "result must be number") 19 | assert.equal(typeof ctx.r, "string") 20 | done(null, result) 21 | }) 22 | }) 23 | }) -------------------------------------------------------------------------------- /test/task-readJson-test.js: -------------------------------------------------------------------------------- 1 | var worksmith = require('..') 2 | var assert = require('assert') 3 | describe("readJson activity", function() { 4 | it("should read up the specified file", function(done) { 5 | var wf = worksmith({task:"readJson", resultTo:"r", path:"./test/sample.json" }); 6 | wf({}, function(err, result, ctx) { 7 | assert.ifError(err, err); 8 | assert.equal(ctx.r["ever talked to kurtz?"], 9 | "you don't talk to kurtz. you listen to him.", 10 | "result does not match") 11 | done(null, result) 12 | }) 13 | }) 14 | 15 | it("should throw up if the file is malformed", function(done) { 16 | var wf = worksmith({task:"readJson", resultTo:"r", path:"./test/malformed.json" }); 17 | wf({}, function(err, result, ctx) { 18 | assert.ok(err, "error must be set"); 19 | done(null) 20 | }) 21 | }) 22 | 23 | }) -------------------------------------------------------------------------------- /src/interpolation/index.js: -------------------------------------------------------------------------------- 1 | var handlebars = require('handlebars') 2 | 3 | var Parser = require('./Parser.js') 4 | var Interpolator = require('./Interpolator.js') 5 | 6 | var defaults = { 7 | tagSearcher : /\[([^\]]*)\](.+?)\[\/\1\]/g, 8 | markupHandlers: { 9 | 'eval': function(context, source) { 10 | with(context) { 11 | return eval(source) 12 | } 13 | }, 14 | 'hbs': function(context, source) { 15 | //we need hash based caching here 16 | var compiled = handlebars.compile(source); 17 | return compiled(context); 18 | } 19 | } 20 | } 21 | 22 | var parserInstance = new Parser(defaults.tagSearcher, defaults.markupHandlers) 23 | var interpolatorInstance = new Interpolator(parserInstance) 24 | 25 | module.exports = { 26 | defaults: defaults, 27 | Parser: Parser, 28 | Interpolator: Interpolator, 29 | parse: parserInstance.parse, 30 | interpolate: interpolatorInstance.interpolate 31 | } -------------------------------------------------------------------------------- /examples/workflow-demo.js: -------------------------------------------------------------------------------- 1 | var workflow = require('../') 2 | 3 | var taskDefinition = workflow.define( 4 | { task: "sequence", items: [ 5 | {task:"log", message:"hello there"}, 6 | {task:"log"}, 7 | {task:"delay", duration:1200}, 8 | {task:"set", name:"a", value:"b"}, 9 | {task:"log", message:"@a" }, 10 | {task:"sequence", items: [ 11 | {task:"log", message:"@"}, 12 | {task:"log"} 13 | ]}, 14 | { task:"parallel", items: [ 15 | {task:"delay", duration:1200}, 16 | {task:"log"}, 17 | {task:"log"} 18 | ]} 19 | ] 20 | }); 21 | 22 | var ctx = {} 23 | var task = taskDefinition(ctx, function(err, result) { 24 | console.log("executed", ctx); 25 | }); 26 | 27 | //demo_flow_of_tasks(); 28 | 29 | 30 | -------------------------------------------------------------------------------- /test/workflow-code-test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var workflow = require('../') 3 | 4 | describe("codeActivity", function () { 5 | 6 | 7 | it("should execute the code block", function (done) { 8 | var context = {}; 9 | var flag; 10 | 11 | var wi = workflow({ 12 | "task": "code", 13 | "execute": function (done) { 14 | flag = true 15 | done() 16 | } 17 | })(context); 18 | 19 | wi(function (err, result) { 20 | assert.ok(flag, "code must be executed") 21 | done(); 22 | }) 23 | }) 24 | 25 | it("should pass its result", function (done) { 26 | var context = {}; 27 | var flag; 28 | 29 | var wi = workflow({ 30 | "task": "code", 31 | "execute": function (done) { 32 | flag = true 33 | done(null, 42) 34 | }, 35 | "resultTo":"theAnswer" 36 | })(context); 37 | 38 | wi(function (err, result) { 39 | assert.equal(context.theAnswer, 42, "result must be passed") 40 | done(); 41 | }) 42 | }) 43 | 44 | }) -------------------------------------------------------------------------------- /test/workflow-condition-test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var workflow = require('../') 3 | 4 | describe("codeActivity", function () { 5 | 6 | 7 | it("should execute the code block", function (done) { 8 | var context = {}; 9 | var flag; 10 | 11 | var wi = workflow({ 12 | "task": "code", 13 | "execute": function (done) { 14 | flag = true 15 | done() 16 | } 17 | })(context); 18 | 19 | wi(function (err, result) { 20 | assert.ok(flag, "code must be executed") 21 | done(); 22 | }) 23 | }) 24 | 25 | it("should pass its result", function (done) { 26 | var context = {}; 27 | var flag; 28 | 29 | var wi = workflow({ 30 | "task": "code", 31 | "execute": function (done) { 32 | flag = true 33 | done(null, 42) 34 | }, 35 | "resultTo":"theAnswer" 36 | })(context); 37 | 38 | wi(function (err, result) { 39 | assert.equal(context.theAnswer, 42, "result must be passed") 40 | done(); 41 | }) 42 | }) 43 | 44 | }) -------------------------------------------------------------------------------- /tasks/map.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('workflow:activities:map') 2 | 3 | module.exports = function define(definition) { 4 | 5 | //TODO: refactor and place this to wf creator level 6 | Object.keys(definition).forEach(function(key) { 7 | if (key[0] === ">") { 8 | definition.map = definition[key]; 9 | definition.resultTo = key.slice(1); 10 | delete definition[key] 11 | } 12 | }); 13 | return function build(context) { 14 | return function run(done) { 15 | var mapDef = context.get(definition.map) 16 | var result 17 | var builders = { 18 | "[object Array]": function(o) { 19 | return o.map(context.get) 20 | }, 21 | "[object Object]": function(o) { 22 | var result = {} 23 | Object.keys(o).forEach(function(key) { 24 | result[key] = context.get(o[key]) 25 | }) 26 | return result 27 | } 28 | } 29 | result = builders[Object.prototype.toString.call(mapDef)](mapDef) 30 | done(null, result) 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /examples/workflow-sql-insert-demo.js: -------------------------------------------------------------------------------- 1 | var workflow = require('./src/tasks') 2 | 3 | var config = require('./config/default.json'); 4 | var workflowDefinition = { task: "sequence", items: [ 5 | {task:"log"}, 6 | //mapping content --> datarecord 7 | {task:"set", name:"record", value:{}}, 8 | {task:"parallel", items: [ 9 | {task:"set", name:"record.external_id", value:"@content.id" }, 10 | {task:"set", name:"record.version", value:"@content.version" }, 11 | {task:"set", name:"record.type", value: 1 }, 12 | {task:"set", name:"record.data", value:{ foo:'bar' }} 13 | ]}, 14 | //end map 15 | 16 | {task:"insertDbRecord", 17 | condition:"config.env != 'dev'", 18 | table:"billing_record", 19 | data:"@record", 20 | connection: "@config.connectionString", 21 | resultTo:"insert_result" }, 22 | {task:"log", message: "@insert_result.rows"} 23 | 24 | ] 25 | } 26 | 27 | var taskDef = workflow.define(workflowDefinition); 28 | var ctx = { 29 | message: {}, 30 | content: { id: 'some_funky_id' + Math.random(), version:1 }, 31 | config: config 32 | } 33 | 34 | var task = taskDef(ctx) 35 | 36 | task(function(err, result) { 37 | console.log("workflow completed", err); 38 | }) 39 | -------------------------------------------------------------------------------- /test/workflow-map-test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var workflow = require('../') 3 | 4 | describe("mapActivity", function() { 5 | 6 | 7 | it("should map a simple object", function(done) { 8 | var context = {}; 9 | 10 | var wi = workflow({ "task":"map", "map": {"a":1} , resultTo:"result" })(context); 11 | 12 | wi(function(err, result) { 13 | assert.deepEqual(context.result, {"a":1}, "map value incorrect") 14 | done(); 15 | }) 16 | }) 17 | 18 | it("should map an object with context references", function(done) { 19 | var context = {"input":3}; 20 | 21 | var wi = workflow({ "task":"map", "map": {"a":"@input"} , resultTo:"result" })(context); 22 | 23 | wi(function(err, result) { 24 | assert.deepEqual(context.result, {"a":3}, "map value incorrect") 25 | done(); 26 | }) 27 | }) 28 | 29 | it("should map an object with resultTo shortcut", function(done) { 30 | var context = {"input":3}; 31 | 32 | var wi = workflow({ "task":"map", ">result": {"a":"@input"} })(context); 33 | 34 | wi(function(err, result) { 35 | assert.deepEqual(context.result, {"a":3}, "map value incorrect") 36 | done(); 37 | }) 38 | }) 39 | 40 | }) -------------------------------------------------------------------------------- /tasks/sql/pg.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('workflow:activities:sql:pg') 2 | var pg = require('pg') 3 | var _ = require('lodash') 4 | 5 | 6 | function executeSqlActivity(definition) { 7 | return function build(context) { 8 | 9 | execute.annotations = {inject: ["command", "params", "connection"]}; 10 | function execute(command, params, connection, done) { 11 | 12 | pg.connect(connection, function(err, client, dbDone) { 13 | if (err) { 14 | debug("Connection error", err) 15 | return done(err) 16 | } 17 | function handleResult(err, result) { 18 | if (!err) { 19 | dbDone(); 20 | return done(undefined, result) 21 | } 22 | dbDone(client) 23 | return done(err); 24 | } 25 | 26 | params = params || []; 27 | var p = params.map(function(value) { 28 | return context.get(value); 29 | }) 30 | debug("executing sql command %s ", command, p) 31 | client.query(command, p, handleResult) 32 | }); 33 | } 34 | return execute; 35 | } 36 | } 37 | 38 | module.exports = executeSqlActivity -------------------------------------------------------------------------------- /stress-test/stress-workflows/workflow1.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "task": "sequence", 3 | "items": [ 4 | { 5 | "task": "set", 6 | "name": "startTime", 7 | "value": "[eval](new Date()).getTime()[/eval]" 8 | }, 9 | { 10 | "task": "createDeepObjects", 11 | "taskPath": "/stress-test/tasks", 12 | "size": 1000, 13 | "depth": 3, 14 | "prefix": "level", 15 | "resultTo": "objectList" 16 | }, 17 | { 18 | "task": "while", 19 | "test": "[eval]objectList.length > 0[/eval]", 20 | "subflow": [ 21 | { 22 | "task": "lodash/last", 23 | "arguments": [ "@objectList" ], 24 | "resultTo": "object" 25 | }, 26 | { 27 | "task": "set", 28 | "name": "extractedValue", 29 | "value": "@object.level3.level2.level1.level0" 30 | }, 31 | { 32 | "task": "lodash/initial", 33 | "arguments": [ "@objectList" ], 34 | "resultTo": "objectList" 35 | } 36 | ] 37 | }, 38 | { 39 | "task": "set", 40 | "name": "duration", 41 | "value": "[eval](new Date()).getTime() - startTime[/eval]" 42 | } 43 | ] 44 | } -------------------------------------------------------------------------------- /test/workflow-stats.js: -------------------------------------------------------------------------------- 1 | var worksmith = require('..') 2 | var assert = require('assert') 3 | 4 | describe("workflow status", function() { 5 | it("should be created", function(done) { 6 | this.slow() 7 | var wf = worksmith([ 8 | { 9 | name:"t1", 10 | taskName:"t1", 11 | task: function(def) { 12 | return function build(context) { 13 | return function execute(d) { 14 | 15 | //console.log("d1") 16 | setTimeout(function() { 17 | d() 18 | }, 500) 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | taskName:"t2", 25 | name: "t2", 26 | task: function(def) { 27 | return function build(context) { 28 | return function execute(d) { 29 | //console.log("d2") 30 | setTimeout(function() { 31 | d() 32 | }, 500) 33 | } 34 | } 35 | } 36 | } 37 | 38 | ]); 39 | var before = Date.now() 40 | var ctx = { $$$stats: []} 41 | wf(ctx, function(err, result, ctx) { 42 | assert(ctx.$$$stats.join("").match(/t1 execution time: [0-9]{3}mst2 execution time: [0-9]{3}mssequence execution time: [0-9]{4}ms/)) 43 | done(null, result) 44 | }) 45 | }) 46 | }) -------------------------------------------------------------------------------- /stress-test/stress-test.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash') 2 | var async = require('async') 3 | var requireVersion = require('require-version') 4 | var myModule = 'worksmith' 5 | 6 | var SAMPLE_SIZE = 10 7 | var VERSION_LIMIT = 3 8 | 9 | requireVersion.runLast(myModule, VERSION_LIMIT, function(module, version, next) { 10 | console.log('Executing tests for ', myModule, ' v', version) 11 | runTests(module, next) 12 | }, function(err) { 13 | if (err) console.log('Error: ', err) 14 | console.log('END') 15 | }) 16 | 17 | function runTests(worksmith, next) { 18 | try { 19 | worksmith.use("lodash", worksmith.createAdapter(require('lodash'))) 20 | var workflow = worksmith("./stress-test/stress-workflows/workflow1.js") 21 | console.log('About to execute workflow1') 22 | calculateAverageExecTime(workflow, function(average) { 23 | console.log('The average execution time is: ', average, ' ms') 24 | next() 25 | }) 26 | } catch(e) { 27 | next(e) 28 | } 29 | } 30 | 31 | function calculateAverageExecTime(workflow, next) { 32 | var average = counter = 0 33 | async.whilst( 34 | function () { return counter < SAMPLE_SIZE }, 35 | function (cb) { 36 | counter++; 37 | var ctx = {} 38 | workflow(ctx, function(err, results) { 39 | if (err) return cb(err) 40 | average += ctx.duration 41 | cb() 42 | }) 43 | }, 44 | function (err) { 45 | if (err) console.error('There was an error running a workflow: ', err.toString()) 46 | next(average / SAMPLE_SIZE) 47 | } 48 | ) 49 | } -------------------------------------------------------------------------------- /src/interpolation/Parser.js: -------------------------------------------------------------------------------- 1 | function Parser(tagSearcher, markupHandlers) { 2 | 3 | function getStringPartThunk(stringValue, start, end) { 4 | return function() { 5 | return stringValue.substr(start, end) 6 | } 7 | } 8 | 9 | function getTagHandlerThunk(match, tag, content, handlers){ 10 | function tagHandlerThunk(context) { 11 | var handler = handlers[tag]; 12 | if (!handler) { 13 | return match; 14 | } 15 | return handler(context, content) 16 | } 17 | tagHandlerThunk.source = { 18 | tag:tag, 19 | content: content 20 | } 21 | return tagHandlerThunk 22 | } 23 | 24 | this.parse = function(stringValue, handlers) { 25 | var pointer = 0 26 | var chops = [] 27 | handlers = handlers || markupHandlers 28 | stringValue.replace(tagSearcher, function(match, tag, content, position) { 29 | if (position > pointer) { 30 | chops.push(getStringPartThunk(stringValue, pointer, position)) 31 | } 32 | chops.push(getTagHandlerThunk(match, tag, content, handlers)) 33 | pointer = position + match.length; 34 | return match 35 | }) 36 | if (pointer < stringValue.length) { 37 | chops.push(function() { return stringValue.substr(pointer, stringValue.length) }) 38 | } 39 | return chops; 40 | } 41 | 42 | this.assemble = function(context, parserResult) { 43 | var chops = parserResult 44 | if (chops.length == 1) { 45 | return chops[0](context); 46 | } 47 | return chops.map(function(piece) { 48 | return piece(context) 49 | }).join(''); 50 | } 51 | 52 | } 53 | 54 | module.exports = Parser -------------------------------------------------------------------------------- /test/task-regex-test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var workflow = require('../') 3 | 4 | describe("regexActivity", function () { 5 | 6 | 7 | it("should match and split a constant string and return result", function (done) { 8 | var context = {}; 9 | var flag; 10 | 11 | var wi = workflow({ 12 | "task": "regex", 13 | "pattern": "(?[^.]*).(?[^.]*).(?[^.]*)", 14 | "value": "abc.def.ghi", 15 | "resultTo": "result" 16 | })(context); 17 | 18 | wi(function (err, result) { 19 | assert.equal(context.result.part1, "abc", "regex field must match") 20 | done(); 21 | }) 22 | }) 23 | 24 | 25 | it("should match and split a context reference and return result", function (done) { 26 | var context = {"stringToChop":"how-much-wood"}; 27 | var flag; 28 | 29 | var wi = workflow({ 30 | "task": "regex", 31 | "pattern": "(?[^.]*)-(?[^.]*)-(?[^.]*)", 32 | "value": "@stringToChop", 33 | "resultTo": "result" 34 | })(context); 35 | 36 | wi(function (err, result) { 37 | assert.equal(context.result.part1, "how", "regex field must match") 38 | done(); 39 | }) 40 | }) 41 | 42 | it("should provide resultTo shortcut", function (done) { 43 | var context = { "stringToChop": "how-much-wood" }; 44 | var flag; 45 | 46 | var wi = workflow({ 47 | "task": "regex", 48 | "pattern": "(?[^.]*)-(?[^.]*)-(?[^.]*)", 49 | ">result": "@stringToChop", 50 | })(context); 51 | 52 | wi(function (err, result) { 53 | assert.equal(context.result.part1, "how", "regex field must match") 54 | assert.equal(context.result.part2, "much", "regex field must match") 55 | done(); 56 | }) 57 | }) 58 | 59 | 60 | }) -------------------------------------------------------------------------------- /ReleaseNotes.md: -------------------------------------------------------------------------------- 1 | # Worksmith release notes 2 | ## 0.2.3 3 | An array in place of a task or workflow variable means an implicit "sequence" task 4 | ```javascript 5 | var wf = worksmith([ 6 | { task: "set", name:"p1", value:"hello" }, 7 | { task:"log" }, 8 | { task:"eachSeries", item:[1,2,3], subflow: [ 9 | { task:"log" message:"@item" }, 10 | {task:"delay", duration:"[eval]item * 250[/eval]" } 11 | ]} 12 | ]); 13 | wf({}, function(err, res) { }); 14 | ``` 15 | 16 | ## 0.2.2 17 | Totally revamped interpolation logic, deep tree interpolation, array interpolation 18 | 19 | tag support to drive interpolation mode: {{hbs}} and {{eval}} are support out of the box 20 | 21 | ## 0.1.7 22 | eachSeriesActivity and whileActivity 23 | 24 | ## 0.1.5 25 | Terminate a workflow or sequence from code (a programmable alternative to conditional="predicate" attribute) 26 | 27 | in execute scope `this` holds workflow execution api. you do pass your done, and not invoke it yourself. 28 | 29 | Note: in later versions you will not have to provide done 30 | ```javascript 31 | //terminate a workflow 32 | function execute(done) { 33 | this.workflow.terminate(/*err*/, /*result*/, done) 34 | } 35 | //terminate the current parent scope 36 | function execute(done) { 37 | this.workflow.terminateParent(/*err*/, /*result*/, done) 38 | } 39 | ``` 40 | 41 | ## 0.1.4 42 | Error logging temporalily removed 43 | 44 | ## 0.1.2, 0.1.3 45 | Readme update 46 | 47 | ## 0.1.1 48 | 49 | ### worksmith.createAdapter 50 | connect existing functionality into a workflow using worksmith.use + worksmith.createAdapter 51 | 52 | ```javascript 53 | worksmith.use("_", worksmith.createAdapter(require('lodash'))) 54 | 55 | var context = {items:[{field:"value"}, {field2:"value2"}]} 56 | var workflow = worksmith({ 57 | task: "_/filter", 58 | arguments: ["@items", { field:"value"} ], 59 | resultTo: "myresult" 60 | }) 61 | workflow(context, function wfresult(err, res, context) { 62 | assert.equal(context.myresult[0], context.items[0]) 63 | done(); 64 | }); 65 | ``` 66 | -------------------------------------------------------------------------------- /test/task-while.js: -------------------------------------------------------------------------------- 1 | var workflow = require('../') 2 | var assert = require('assert') 3 | 4 | describe("whileActivity", function () { 5 | 6 | it("should iterate until condition is falsey", function (done) { 7 | var context = { flag: true, count: 0 }; 8 | 9 | var wi = workflow({ 10 | task: "while", 11 | test: "@flag", 12 | subflow: { 13 | task: function(definition) { 14 | return function(context) { 15 | return function(done) { 16 | context.count >= 10 ? context.flag = false : context.count++ 17 | done() 18 | } 19 | } 20 | } 21 | } 22 | })(context) 23 | 24 | wi(function (err, result) { 25 | assert.ifError(err) 26 | assert.equal(context.count, 10) 27 | done() 28 | }) 29 | }) 30 | 31 | it("should not barf on infinte loops", function (done) { 32 | var context = {}; 33 | 34 | var wi = workflow({ 35 | task: "while", 36 | test: true, 37 | subflow: { 38 | task: function(definition) { 39 | return function(context) { 40 | return function(done) { 41 | done() 42 | } 43 | } 44 | } 45 | } 46 | })(context) 47 | 48 | wi(function (err, result) { 49 | assert.ifError(err) 50 | assert.ok(false, 'Loop was not infinite enough') 51 | }) 52 | 53 | setTimeout(done, 1000) 54 | }) 55 | 56 | it("should yield the result of the subflow", function (done) { 57 | var context = { flag: true, count: 0 }; 58 | 59 | var wi = workflow({ 60 | task: "while", 61 | test: "@flag", 62 | subflow: { 63 | task: function(definition) { 64 | return function(context) { 65 | return function(done) { 66 | context.count >= 10 ? context.flag = false : context.count++ 67 | done(null, context.count) 68 | } 69 | } 70 | } 71 | }, 72 | resultTo: 'result' 73 | })(context) 74 | 75 | wi(function (err, result) { 76 | assert.ifError(err) 77 | console.log(context) 78 | assert.equal(context.result, 10) 79 | assert.equal(result, 10) 80 | done() 81 | }) 82 | }) 83 | }) -------------------------------------------------------------------------------- /test/task-eachSeries.js: -------------------------------------------------------------------------------- 1 | var workflow = require('../') 2 | var assert = require('assert') 3 | var _ = require('lodash') 4 | 5 | describe("eachSeriesActivity", function () { 6 | 7 | it("should iterate over a list of items", function (done) { 8 | var context = { counter: {count: 0 }}; 9 | 10 | var wi = workflow({ 11 | task: "eachSeries", 12 | items: [1, 2, 3], 13 | subflow: { 14 | task: function(definition) { 15 | return function(context) { 16 | return function(done) { 17 | context.counter.count++ 18 | done() 19 | } 20 | } 21 | } 22 | } 23 | })(context) 24 | 25 | wi(function (err, result) { 26 | assert.ifError(err) 27 | assert.equal(context.counter.count, 3) 28 | done() 29 | }) 30 | }) 31 | 32 | it("should not barf on large loops", function (done) { 33 | 34 | this.timeout(20000) 35 | var context = { counter: { count: 0 } } 36 | 37 | var wi = workflow({ 38 | task: "eachSeries", 39 | items: _.range(0, 99999), 40 | subflow: { 41 | task: function(definition) { 42 | return function(context) { 43 | return function(done) { 44 | context.counter.count++ 45 | done() 46 | } 47 | } 48 | } 49 | } 50 | })(context) 51 | 52 | wi(function (err, result) { 53 | assert.ifError(err) 54 | assert.equal(context.counter.count, 99999) 55 | done() 56 | }) 57 | }) 58 | 59 | it("should work in nested loops", function (done) { 60 | var context = { counter: {count: 0 } }; 61 | 62 | var wi = workflow({ 63 | task: "eachSeries", 64 | items: [1, 2, 3], 65 | subflow: { 66 | task: "eachSeries", 67 | items: [1, 2, 3], 68 | subflow: { 69 | task: function(definition) { 70 | return function(context) { 71 | return function(done) { 72 | context.counter.count++ 73 | done() 74 | } 75 | } 76 | } 77 | } 78 | } 79 | })(context) 80 | 81 | wi(function (err, result) { 82 | assert.ifError(err) 83 | assert.equal(context.counter.count, 9) 84 | done() 85 | }) 86 | }) 87 | }) -------------------------------------------------------------------------------- /test/workflow-logging-test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var workflow = require('../') 3 | 4 | describe("WorkSmith logging", function() { 5 | 6 | it("should provide a log method for activities", function(done) { 7 | var flags = {}; 8 | 9 | var def = { 10 | task: function(def) { 11 | flags.log = this.log; 12 | return function build(context) { 13 | return function execute(done) { 14 | done(); 15 | } 16 | } 17 | } 18 | }; 19 | 20 | var wf = workflow(def) 21 | wf({},function(err, res) { 22 | assert.ok(flags.log, "log function must be there") 23 | done(); 24 | }) 25 | }) 26 | 27 | it("should support reconfiguring logger", function(done) { 28 | var flags = {}; 29 | var logs = []; 30 | workflow.configure( 31 | { 32 | logger: { 33 | log: function() { 34 | logs.push(Array.prototype.join.call(arguments,"|")) 35 | } 36 | } 37 | } 38 | ) 39 | var def = { 40 | task: "log", 41 | message:"hello" 42 | }; 43 | 44 | var wf = workflow(def) 45 | wf({},function(err, res) { 46 | assert.equal(logs.join(),"hello", "log function must be configurable") 47 | done(); 48 | }) 49 | }) 50 | 51 | //this test is left pending until logErrors config setting is implemented 52 | xit("should support reconfiguring logger for worksmith errors", function(done) { 53 | var flags = {}; 54 | var logs = []; 55 | workflow.configure( 56 | { 57 | logger: { 58 | error: function(message,error) { 59 | flags.message = message; 60 | flags.wf = wf; 61 | flags.error = error; 62 | } 63 | } 64 | } 65 | ) 66 | var def = { 67 | task: function(def) { 68 | return function build(context) { 69 | return function execute(done) { 70 | done({"message":"error"}); 71 | } 72 | } 73 | } 74 | }; 75 | 76 | var wf = workflow(def) 77 | wf({},function(err, res) { 78 | assert.ok(err, "Error must be set") 79 | assert.deepEqual(flags.error, {"message":"error"},"error must match") 80 | assert.ok(flags.message,"message must be set") 81 | assert.ok(flags.wf, "workflow must be set") 82 | done(); 83 | }) 84 | }) 85 | }); -------------------------------------------------------------------------------- /test/task-workflow-test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var worksmith = require('../') 3 | 4 | describe("workflowActivity", function () { 5 | 6 | 7 | it("should execute the workflow specified by path", function (done) { 8 | var context = {}; 9 | var flag; 10 | var innerContext = {}; 11 | worksmith({ 12 | "task": "workflow", 13 | "source":"./test/workflow1.js", 14 | "context": { _np_: innerContext } 15 | })({}, function (err, result) { 16 | assert.equal(innerContext.result, "hello", "inner context field must match") 17 | done(); 18 | }) 19 | }) 20 | 21 | it("should execute the workflow specified by instance", function (done) { 22 | var context = {}; 23 | var flag = {}; 24 | var innerContext = {}; 25 | worksmith({ 26 | "task": "workflow", 27 | "source": { 28 | task:function(define) { 29 | return function build(context) { 30 | return function execute(_d){ 31 | flag.inner = true; 32 | _d(); 33 | } 34 | } 35 | }, 36 | }, 37 | "context": innerContext 38 | })({}, function (err, result) { 39 | assert.equal(flag.inner, true, "inner workflow must run") 40 | done(); 41 | })}) 42 | 43 | it("should provide outer context if context is not specified", function (done) { 44 | var outerContext = { field:"value"}; 45 | var flag = {}; 46 | worksmith({ 47 | "task": "workflow", 48 | "source": { 49 | task:function(define) { 50 | return function build(context) { 51 | return function execute(_d){ 52 | flag.result = context.field; 53 | _d(); 54 | } 55 | } 56 | }, 57 | } 58 | })(outerContext, function (err, result) { 59 | assert.equal(flag.result, outerContext.field, "inner workflow must run") 60 | done(); 61 | })}) 62 | 63 | it("should support channelling workflow result into resultTo", function (done) { 64 | var outerContext = { field:"value"}; 65 | var wfresult = {some:"result"}; 66 | var flag = {}; 67 | worksmith({ 68 | "resultTo":"wfresult", 69 | "task": "workflow", 70 | "source": { 71 | task:function(define) { 72 | return function build(context) { 73 | return function execute(_d){ 74 | _d(null, wfresult); 75 | } 76 | } 77 | }, 78 | } 79 | })(outerContext, function (err, result) { 80 | assert.equal(outerContext.wfresult, wfresult, "inner workflow result must match") 81 | done(); 82 | })}) 83 | 84 | }); 85 | -------------------------------------------------------------------------------- /test/workflow-terminate.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var worksmith = require('../') 3 | 4 | describe("WorkSmith API - termination/goto functions", function() { 5 | 6 | function createWf(err, message, steps, checker, finisher) { 7 | return function TestTask(def) { 8 | return function build(context) { 9 | return function execute(wfdone) { 10 | //console.log(message) 11 | checker && checker.apply(this) 12 | steps.push(message) 13 | wfdone(err) 14 | } 15 | } 16 | } 17 | } 18 | 19 | var stdError = { "message":"error", "supressMessage":true, stack: "some stack trace"}; 20 | it("should be provided", function(done) { 21 | 22 | var steps = []; 23 | var def = { 24 | task:"sequence", 25 | items:[ 26 | { 27 | task: function(step) { 28 | return function(context) { 29 | return function(done) { 30 | steps.push("wf1") 31 | this.workflow.terminate(undefined, {a:1}, done) 32 | } 33 | } 34 | }, 35 | "finally": { 36 | task: createWf(undefined, "wf1.fin", steps) 37 | } 38 | },{ 39 | task: createWf(undefined, "wf2", steps, function() { 40 | steps.ths = this 41 | }) 42 | }], 43 | "finally": { 44 | task: createWf(undefined, "wf.fin", steps) 45 | } 46 | }; 47 | 48 | var workflow = worksmith(def); 49 | var ctx = {} 50 | workflow(ctx, function wfresult() { 51 | steps.push("wfcomplete") 52 | assert.equal(steps.join("|"),"wf1|wf1.fin|wfcomplete") 53 | done(); 54 | }); 55 | }); 56 | 57 | 58 | 59 | it("should be provided", function(done) { 60 | 61 | var steps = []; 62 | var def = { 63 | task:"sequence", 64 | items:[ 65 | { 66 | task: createWf(undefined, "wf1", steps), 67 | "finally": { 68 | task: createWf(undefined, "wf1.fin", steps) 69 | } 70 | },{ 71 | task:"sequence", 72 | items: [ 73 | { 74 | task:function() { 75 | return function() { 76 | return function(done) { 77 | steps.push("sub0") 78 | this.workflow.terminateParent(null, {a:1}, done) 79 | } 80 | } 81 | } 82 | }, 83 | { 84 | "task": createWf("undefined","sub1", steps) 85 | }] 86 | },{ 87 | task: createWf(undefined, "wf2", steps) 88 | }], 89 | "finally": { 90 | task: createWf(undefined, "wf.fin", steps) 91 | } 92 | }; 93 | 94 | var workflow = worksmith(def); 95 | var ctx = {} 96 | workflow(ctx, function wfresult() { 97 | assert.equal(steps.join("|"),"wf1|wf1.fin|sub0|wf2|wf.fin") 98 | done(); 99 | }); 100 | }); 101 | 102 | 103 | 104 | }); -------------------------------------------------------------------------------- /src/interpolation/Interpolator.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('workflow:interpolation:interpolator') 2 | var handlebars = require('handlebars') 3 | 4 | function Interpolator(parser) { 5 | var self = this 6 | 7 | 8 | this.readContextPath = function(context, path) { 9 | path = path.replace(/\[/g, ".").replace(/\]/g, "") 10 | var parts = path.split('.'); 11 | var data = context; 12 | var part; 13 | while (part = parts.shift()) { 14 | data = data[part]; 15 | if (data === undefined) { 16 | return undefined; 17 | } 18 | } 19 | return data; 20 | } 21 | 22 | this.interpolateString = function(context, value) { 23 | return parser.assemble(context, parser.parse(value)) 24 | }, 25 | 26 | 27 | this.interpolate = function(context, value, interpolationPolicy) { 28 | var index = 0 29 | var matched = false 30 | if (interpolationPolicy === undefined) { 31 | interpolationPolicy = true; 32 | } 33 | if (!interpolationPolicy) { 34 | return value; 35 | } 36 | var interpolationRules = self.rules[Object.prototype.toString.call(value)] 37 | if (!interpolationRules) 38 | return value 39 | 40 | while(index < interpolationRules.length && !( matched = interpolationRules[index].match(value) )) index++ 41 | if (!matched) 42 | return value 43 | return interpolationRules[index].action(self, context, value) 44 | } 45 | } 46 | 47 | Interpolator.prototype.rules = { 48 | "[object String]": [ 49 | { 50 | name: "context reference", 51 | match: function(value) {("@" === value) }, 52 | action: function(interpolator, context, value) { return context } 53 | }, 54 | { 55 | name: "eval shorthand", 56 | match: function(value) { return value[0] == '#' }, 57 | action: function(interpolator, context, value) { 58 | with(context) { 59 | debug('evaluating code', { code: value.slice(1) }) 60 | return eval(value.slice(1)) 61 | } 62 | } 63 | }, 64 | { 65 | name: "context member reference", 66 | match: function(value) { return value[0] == '@' }, 67 | action: function(interpolator, context, value) { 68 | return interpolator.readContextPath(context, value.slice(1)) 69 | } 70 | }, 71 | { 72 | name: "markup resolver", 73 | match: function(value) { return value.indexOf("[") > -1 }, 74 | action: function(interpolator, context, value) { 75 | return interpolator.interpolateString(context, value) 76 | } 77 | }], 78 | "[object Array]": [ 79 | { 80 | name: "object field interpolator", 81 | match: function(value) { return true }, 82 | action: function(interpolator, context, value) { 83 | var result = [] 84 | for (var index = 0; index < value.length; index++) { 85 | result.push(interpolator.interpolate(context, value[index])) 86 | } 87 | return result 88 | } 89 | }], 90 | "[object Object]": [ 91 | { 92 | name:"no-process wrapper", 93 | match: function(value) { return "_np_" in value}, 94 | action: function(interpolator, context, value) { return value["_np_"] } 95 | }, 96 | { 97 | name: "legacy {template:'...'} field resolver", 98 | match: function(value) { return Object.keys(value).length === 1 && "template" in value }, 99 | action: function(interpolator, context, value) { 100 | console.warn("obsoleted functionality: { template: value }, use [hbs]..[/hbs] instead") 101 | var template = value.template 102 | var compiled = handlebars.compile(template) 103 | return compiled(context); 104 | } 105 | }, 106 | { 107 | name: "object field interpolator", 108 | match: function(value) { return true }, 109 | action: function(interpolator, context, value) { 110 | var result = {} 111 | var keys = Object.keys(value) 112 | for (var index = 0; index < keys.length; index++) { 113 | var key = keys[index] 114 | result[key] = interpolator.interpolate(context, value[key]) 115 | } 116 | return result 117 | } 118 | }] 119 | } 120 | 121 | 122 | module.exports = Interpolator 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # worksmith 2 | A seriously `functional` workflow library, that lets you build composable and configurable process definitions for various reasons. 3 | 4 | ```npm i worksmith --save``` 5 | 6 | 7 | For a step by step tutorial click [here](https://github.com/guidesmiths/worksmith/blob/master/TUTORIAL.md) 8 | 9 | [Release notes](https://github.com/guidesmiths/worksmith/blob/master/ReleaseNotes.md) 10 | 11 | ##Worksmith activities / task types 12 | 13 | Worksmith comes with an extensible task library built up from the `core` and the `extension modules`. 14 | 15 | ### Core activities 16 | | group | activities | description | 17 | | ----- | ---------- | ----------- | 18 | 19 | - Control flow: ```sequence``` , ```parallel``` and ```warezSequence``` 20 | - IO: ```log```,```sql/pg``` 21 | - Tansformation: ```map```, ```regex```, ```set``` 22 | - Extensibitly: ```code``` activity , create custom task types by creating files in the tasks folder 23 | 24 | ### Extension modules 25 | 26 | | name | description | 27 | | --- | ----------- | 28 | | worksmith_salesforce | Interact with salesforce in a workflow | 29 | | worksmith_etcd | Use network based locking via etcd service | 30 | | coming | soon | 31 | | worksmith_postgres | Execute SQL statements as part of the workflow, supports transactions | 32 | | worksmith_assert | An assertion library to be used conventional workflows or workflows built for testing | 33 | | worksmith_fs | Read/write files from a workflow | 34 | 35 | - with worksmith you can build a complex async process chain from functional steps (tasks) - yet keep the application easy to understand and each functional step easy to developer and maintain. forget ```if(err) return next(err)``` 36 | - workflow steps are unaware about each other - they communicate over a shared context. WorkSmith provides an intuitive expression syntax for working with the context in a workflow definitions 37 | 38 | 39 | 40 | ## usage 41 | 42 | ### A workflow definition: 43 | This can be in a config file, or as part of your js code as a variable. 44 | 45 | ```javascript 46 | { "task": "sequence", 47 | "items": [ 48 | { 49 | task:"log", message:"hello workflow" 50 | }, 51 | { 52 | task: "map", 53 | ">insertParams": ["@req.params.id", 1, 1] 54 | }, 55 | { 56 | task:"sql/pg", 57 | connection: "@connection", 58 | command: "insert into order (order_id, version, type) \ 59 | values ($1, $2, $3) returning id", 60 | params: "@insertParams", 61 | resultTo: "insertResult" 62 | }, 63 | {...} 64 | ] 65 | } 66 | ``` 67 | 68 | ### The code: 69 | 70 | ```javascript 71 | 72 | var worksmith = require('worksmith') 73 | 74 | var workflow = worksmith('./workflow.json') 75 | 76 | 77 | var context = { 78 | connection:"postgres://login:pw@host/db", 79 | other:"data" 80 | } 81 | 82 | workflow(context, function(err, result) { 83 | console.log("workflow completed, %", context.insertResult) 84 | }) 85 | 86 | 87 | ``` 88 | 89 | ## How to create your own activity 90 | 91 | worksmith lets you build your activities on a super easy way 92 | Place the following code as ```"hello-world.js"``` in the ```tasks``` folder 93 | 94 | ```javascript 95 | var utils = require('worksmith') 96 | module.exports = function (node) { 97 | //use the node variable to access workflow params 98 | return function(context) { 99 | //use the context to access workflow execution state 100 | return function(done) { 101 | //set done when your acitivity finished its job 102 | //read and write data from the context 103 | console.log("Hello world", context.get(node.inParam)) 104 | context.set("myresult","myvalue") 105 | done(); 106 | } 107 | } 108 | 109 | } 110 | ``` 111 | Now you can use it the same way as the core activities 112 | ```javascript 113 | var wf = workflow( {"task":"hello-world", "inParam":"some thing"} ); 114 | 115 | var ctx = {"some":"value"}; 116 | wf(ctx, function(err) { 117 | console.log(ctx) 118 | }) 119 | ``` 120 | 121 | ## List of core activities 122 | 123 | ### log 124 | Write a log to the console 125 | #### options 126 | message 127 | 128 | ### delay 129 | Waits a bit 130 | #### options 131 | duration 132 | 133 | ### insertDbRecord 134 | Like the name suggests 135 | #### options 136 | table 137 | data 138 | connection 139 | resultTo 140 | 141 | ### softDeleteDbRecord 142 | 143 | ### deleteDbRecord 144 | 145 | ### parallel 146 | Execute sub tasks in parallel 147 | #### options 148 | items array 149 | 150 | ### sequence 151 | Execute sub tasks in sequence 152 | #### options 153 | items array 154 | 155 | ### warezSequence 156 | Define a sequence on a patching compatible way 157 | #### options 158 | items array 159 | 160 | 161 | ### set 162 | Set variable on the workflow context 163 | #### options 164 | name 165 | value 166 | 167 | 168 | -------------------------------------------------------------------------------- /test/workflow-finally-test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var worksmith = require('../') 3 | 4 | describe("WorkSmith API - finally workflow", function() { 5 | 6 | function createWf(err, message, steps) { 7 | return function TestTask(def) { 8 | return function build(context) { 9 | return function execute(wfdone) { 10 | steps.push(message) 11 | wfdone(err); 12 | } 13 | } 14 | } 15 | } 16 | 17 | var stdError = { "message":"error", "supressMessage":true, stack: "some stack trace"}; 18 | 19 | it("should be executed on good run", function(done) { 20 | 21 | var steps = []; 22 | var def = { 23 | task: createWf(undefined, "wf1", steps), 24 | "finally": { 25 | task: createWf(undefined, "wf2", steps) 26 | } 27 | }; 28 | 29 | var workflow = worksmith(def); 30 | workflow({}, function wfresult() { 31 | steps.push("wf3") 32 | assert.equal(steps.join("|"),"wf1|wf2|wf3", "workflow steps must be correct") 33 | done(); 34 | }); 35 | }); 36 | 37 | 38 | it("should be executed on run with error", function(done) { 39 | var steps = []; 40 | var def = { 41 | task: createWf(stdError, "wf1", steps), 42 | "finally": { 43 | task:createWf(undefined, "wf2", steps) 44 | } 45 | }; 46 | 47 | var workflow = worksmith(def); 48 | workflow({}, function() { 49 | steps.push("wf3") 50 | assert.equal(steps.join("|"),"wf1|wf2|wf3", "workflow steps must be correct") 51 | done(); 52 | }); 53 | }); 54 | 55 | 56 | 57 | it("should be executed on error and the workflow should quit with error", function(done) { 58 | var flags = {}; 59 | var steps = []; 60 | var def = 61 | { 62 | task:"sequence", 63 | items: [{ 64 | task:createWf(stdError, "wf1", steps), 65 | "finally": { 66 | task:createWf(undefined, "wf2", steps) 67 | } 68 | }, 69 | { 70 | task:createWf(undefined, "wf3", steps) 71 | }] 72 | }; 73 | 74 | 75 | var workflow = worksmith(def); 76 | workflow({}, function wfresult() { 77 | steps.push("wf4") 78 | assert.equal(steps.join("|"),"wf1|wf2|wf4", "workflow steps must be correct") 79 | done(); 80 | }); 81 | }); 82 | 83 | 84 | it("should be executed on error and the workflow should quit with error from deep", function(done) { 85 | var flags = {}; 86 | var steps = []; 87 | var def = 88 | { 89 | task:"sequence", 90 | items: [ 91 | { 92 | task:"sequence", 93 | items: [{ 94 | task: createWf({"message":"error"}, "wf1", steps), 95 | "finally": { 96 | task: createWf(undefined, "wf2", steps) 97 | } 98 | }], 99 | "finally": { 100 | task:function(fdef) { 101 | return function build(fcontext) { 102 | return function execute(fdone) { 103 | steps.push("wf3") 104 | fdone(); 105 | } 106 | } 107 | } 108 | } 109 | }, 110 | { 111 | task: createWf(undefined, "wf4", steps) 112 | } 113 | ]}; 114 | 115 | 116 | var workflow = worksmith(def); 117 | workflow({}, function wfresult() { 118 | steps.push("wf5") 119 | assert.equal(steps.join("|"),"wf1|wf2|wf3|wf5", "workflow steps must be correct") 120 | done(); 121 | }); 122 | }); 123 | 124 | 125 | it("should be executed on handled error ", function(done) { 126 | var flags = {}; 127 | var steps = []; 128 | var def = { 129 | task:"sequence", 130 | items: [ 131 | { 132 | task: function(def) { 133 | return function build(context) { 134 | return function execute(wfdone) { 135 | steps.push("wf1") 136 | wfdone({"some":"error","supressMessage":true}); 137 | } 138 | } 139 | }, 140 | onError: { 141 | handleError:true, 142 | task: function(def) { 143 | return function build(context) { 144 | return function execute(wfdone) { 145 | steps.push("wf2") 146 | wfdone(); 147 | } 148 | } 149 | } 150 | } 151 | }, 152 | { 153 | task: function(def) { 154 | return function build(context) { 155 | return function execute(wfdone) { 156 | steps.push("wf3") 157 | wfdone(); 158 | } 159 | } 160 | } 161 | } 162 | ], 163 | "finally": { 164 | task:function(fdef) { 165 | return function build(fcontext) { 166 | return function execute(fdone) { 167 | steps.push("wf4") 168 | fdone(); 169 | } 170 | } 171 | } 172 | } 173 | }; 174 | 175 | var workflow = worksmith(def); 176 | workflow({}, function() { 177 | steps.push("wf5") 178 | assert.equal(steps.join("|"),"wf1|wf2|wf3|wf4|wf5", "workflow steps must be correct") 179 | done(); 180 | }); 181 | }); 182 | 183 | }); -------------------------------------------------------------------------------- /test/interpolator.test.js: -------------------------------------------------------------------------------- 1 | /// 2 | var assert = require('assert') 3 | var interpolator = require('../src/interpolation') 4 | var worksmith = require('../') 5 | 6 | describe("interpolator", function() { 7 | describe("interpolator parser", function() { 8 | it("should find tags in a text", function() { 9 | var mark = "abc[123]def[/123]hij" 10 | var parsed = interpolator.parse(mark) 11 | assert.equal(parsed[0](),"abc") 12 | assert.deepEqual(parsed[1].source,{tag:"123", content:"def"}) 13 | assert.equal(parsed[2](),"hij") 14 | }) 15 | it("should work with a sole tag", function() { 16 | var mark = "[123]def[/123]" 17 | var parsed = interpolator.parse(mark) 18 | assert.deepEqual(parsed[0].source,{tag:"123", content:"def"}) 19 | }) 20 | }) 21 | describe("interpolator v1 interpolation", function() { 22 | it("should return a string constant", function() { 23 | assert.equal(interpolator.interpolate({a:1},"alma"),"alma") 24 | }) 25 | it("should return a simple context reference", function() { 26 | assert.equal(interpolator.interpolate({a:1},"@a"),1) 27 | }) 28 | it("should return the context for '@'", function() { 29 | var ctx = {a:1}; 30 | assert.equal(interpolator.interpolate(ctx,"@"),ctx) 31 | }) 32 | it("should support the {template:...} clause", function() { 33 | var ctx = {a:1}; 34 | assert.ok(interpolator.interpolate(ctx,{template:"{{a}}"}) === "1") 35 | }) 36 | it("should return a number constant", function() { 37 | var ctx = {a:1}; 38 | assert.equal(interpolator.interpolate(ctx,100),100) 39 | }) 40 | it("should return a regex constant", function() { 41 | var ctx = {} 42 | var v = /x/ 43 | assert.equal(interpolator.interpolate(ctx,v),v) 44 | }) 45 | 46 | }) 47 | 48 | describe("interpolator - object interpolation", function() { 49 | it("should support object type", function() { 50 | assert.equal(interpolator.interpolate({a:1},{ abc: "@a" }).abc,1) 51 | }) 52 | it("should support nested object type", function() { 53 | assert.equal(interpolator.interpolate({a:1, b:2},{ abc: "@a", def:{ghi: "@b"} }).abc,1) 54 | assert.equal(interpolator.interpolate({a:1, b:2},{ abc: "@a", def:{ghi: "@b"} }).def.ghi,2) 55 | }) 56 | it("should support nested object value and markup", function() { 57 | assert.equal(interpolator.interpolate({a:1, b:2}, { 58 | abc: "[eval]a[/eval]", 59 | def:{ghi: "[eval]b-1[/eval]"} }).abc,1) 60 | assert.equal(interpolator.interpolate({a:1, b:2}, { 61 | abc: "[eval]a[/eval]", 62 | def:{ghi: "[eval]b-1[/eval]"} }).def.ghi,1) 63 | }) 64 | }) 65 | 66 | describe("interpolator - array interpolation", function() { 67 | it("should support array type", function() { 68 | assert.deepEqual(interpolator.interpolate({a:1, b:2},{ abc: ["@a","@b"] }).abc,[1,2]) 69 | }) 70 | it("should support array type and markup", function() { 71 | assert.deepEqual(interpolator.interpolate({a:1, b:2},{ abc: ["[eval]a[/eval]","[eval]b-1[/eval]"] }).abc,[1,1]) 72 | }) 73 | }) 74 | 75 | describe("interpolator - non-processed values", function() { 76 | it("should support array type", function() { 77 | assert.deepEqual(interpolator.interpolate({a:1, b:2}, 78 | {_np_: { abc: ["@a","@b"] }}).abc,["@a","@b"]) 79 | }) 80 | }) 81 | 82 | describe("interpolator [hbs]", function() { 83 | it("should support constant values", function() { 84 | var mark = "[hbs]42[/hbs]" 85 | var parsed = interpolator.parse(mark) 86 | assert.equal(parsed[0]({a:1}), "42") 87 | }) 88 | it("should support context values", function() { 89 | assert.equal(interpolator.interpolate({a:1},"[hbs]a={{a}}[/hbs]"),"a=1") 90 | }) 91 | }) 92 | describe("interpolator # (eval shortcut)", function() { 93 | it("should support constant values", function() { 94 | assert.ok(interpolator.interpolate({a:1},"#a+1") === 2) 95 | }) 96 | }) 97 | 98 | describe("interpolator [eval]", function() { 99 | it("should support constant values", function() { 100 | assert.equal(interpolator.interpolate({a:1},"[eval]42[/eval]"),42) 101 | }) 102 | it("should support solo markup with type return value", function() { 103 | assert.equal(interpolator.interpolate({a:1},"[eval]a[/eval]"),1) 104 | }) 105 | it("should support multiple markup with string concat", function() { 106 | assert.equal(interpolator.interpolate({a:1},"[eval]a[/eval][eval]a[/eval]"),"11") 107 | }) 108 | it("should support complex expressions", function() { 109 | var mark = "[eval]a[f][/eval]" 110 | var parsed = interpolator.parse(mark) 111 | assert.equal(parsed[0]({f : 1, a:['x','y','z']}),'y') 112 | }) 113 | }) 114 | 115 | describe("embedded workflow content", function() { 116 | it("should not be interpolate too early", function(done) { 117 | var cache = {}; 118 | var wf = { 119 | task:"while", 120 | test:"#fo == 'bar'", 121 | subflow: { 122 | task:"sequence", 123 | items: [ 124 | { 125 | task: function define(node) { 126 | return function build(context) { 127 | return function execute(done) { 128 | context.fo = "baz"; 129 | done(undefined, "hello there") 130 | } 131 | } 132 | }, 133 | resultTo:"f1" 134 | }, 135 | { 136 | task: function define(node) { 137 | return function(context) { 138 | execute.annotations = { inject:["p1"] } 139 | function execute(p1, done) { 140 | cache.p1 = p1; 141 | done(); 142 | } 143 | return execute; 144 | } 145 | }, 146 | p1:"@f1" 147 | } 148 | ] 149 | } 150 | } 151 | worksmith(wf)({fo:'bar'}, function() { 152 | done() 153 | }); 154 | }) 155 | 156 | }) 157 | 158 | }) 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /typings/mocha/mocha.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for mocha 2.2.5 2 | // Project: http://mochajs.org/ 3 | // Definitions by: Kazi Manzur Rashid , otiai10 , jt000 , Vadim Macagon 4 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 5 | 6 | interface MochaSetupOptions { 7 | //milliseconds to wait before considering a test slow 8 | slow?: number; 9 | 10 | // timeout in milliseconds 11 | timeout?: number; 12 | 13 | // ui name "bdd", "tdd", "exports" etc 14 | ui?: string; 15 | 16 | //array of accepted globals 17 | globals?: any[]; 18 | 19 | // reporter instance (function or string), defaults to `mocha.reporters.Spec` 20 | reporter?: any; 21 | 22 | // bail on the first test failure 23 | bail?: boolean; 24 | 25 | // ignore global leaks 26 | ignoreLeaks?: boolean; 27 | 28 | // grep string or regexp to filter tests with 29 | grep?: any; 30 | } 31 | 32 | interface MochaDone { 33 | (error?: Error): void; 34 | } 35 | 36 | declare var mocha: Mocha; 37 | declare var describe: Mocha.IContextDefinition; 38 | declare var xdescribe: Mocha.IContextDefinition; 39 | // alias for `describe` 40 | declare var context: Mocha.IContextDefinition; 41 | // alias for `describe` 42 | declare var suite: Mocha.IContextDefinition; 43 | declare var it: Mocha.ITestDefinition; 44 | declare var xit: Mocha.ITestDefinition; 45 | // alias for `it` 46 | declare var test: Mocha.ITestDefinition; 47 | 48 | declare function before(action: () => void): void; 49 | 50 | declare function before(action: (done: MochaDone) => void): void; 51 | 52 | declare function setup(action: () => void): void; 53 | 54 | declare function setup(action: (done: MochaDone) => void): void; 55 | 56 | declare function after(action: () => void): void; 57 | 58 | declare function after(action: (done: MochaDone) => void): void; 59 | 60 | declare function teardown(action: () => void): void; 61 | 62 | declare function teardown(action: (done: MochaDone) => void): void; 63 | 64 | declare function beforeEach(action: () => void): void; 65 | 66 | declare function beforeEach(action: (done: MochaDone) => void): void; 67 | 68 | declare function suiteSetup(action: () => void): void; 69 | 70 | declare function suiteSetup(action: (done: MochaDone) => void): void; 71 | 72 | declare function afterEach(action: () => void): void; 73 | 74 | declare function afterEach(action: (done: MochaDone) => void): void; 75 | 76 | declare function suiteTeardown(action: () => void): void; 77 | 78 | declare function suiteTeardown(action: (done: MochaDone) => void): void; 79 | 80 | declare class Mocha { 81 | constructor(options?: { 82 | grep?: RegExp; 83 | ui?: string; 84 | reporter?: string; 85 | timeout?: number; 86 | bail?: boolean; 87 | }); 88 | 89 | /** Setup mocha with the given options. */ 90 | setup(options: MochaSetupOptions): Mocha; 91 | bail(value?: boolean): Mocha; 92 | addFile(file: string): Mocha; 93 | /** Sets reporter by name, defaults to "spec". */ 94 | reporter(name: string): Mocha; 95 | /** Sets reporter constructor, defaults to mocha.reporters.Spec. */ 96 | reporter(reporter: (runner: Mocha.IRunner, options: any) => any): Mocha; 97 | ui(value: string): Mocha; 98 | grep(value: string): Mocha; 99 | grep(value: RegExp): Mocha; 100 | invert(): Mocha; 101 | ignoreLeaks(value: boolean): Mocha; 102 | checkLeaks(): Mocha; 103 | /** Enables growl support. */ 104 | growl(): Mocha; 105 | globals(value: string): Mocha; 106 | globals(values: string[]): Mocha; 107 | useColors(value: boolean): Mocha; 108 | useInlineDiffs(value: boolean): Mocha; 109 | timeout(value: number): Mocha; 110 | slow(value: number): Mocha; 111 | enableTimeouts(value: boolean): Mocha; 112 | asyncOnly(value: boolean): Mocha; 113 | noHighlighting(value: boolean): Mocha; 114 | /** Runs tests and invokes `onComplete()` when finished. */ 115 | run(onComplete?: (failures: number) => void): Mocha.IRunner; 116 | } 117 | 118 | // merge the Mocha class declaration with a module 119 | declare module Mocha { 120 | /** Partial interface for Mocha's `Runnable` class. */ 121 | interface IRunnable { 122 | title: string; 123 | fn: Function; 124 | async: boolean; 125 | sync: boolean; 126 | timedOut: boolean; 127 | } 128 | 129 | /** Partial interface for Mocha's `Suite` class. */ 130 | interface ISuite { 131 | parent: ISuite; 132 | title: string; 133 | 134 | fullTitle(): string; 135 | } 136 | 137 | /** Partial interface for Mocha's `Test` class. */ 138 | interface ITest extends IRunnable { 139 | parent: ISuite; 140 | pending: boolean; 141 | 142 | fullTitle(): string; 143 | } 144 | 145 | /** Partial interface for Mocha's `Runner` class. */ 146 | interface IRunner {} 147 | 148 | interface IContextDefinition { 149 | (description: string, spec: () => void): ISuite; 150 | only(description: string, spec: () => void): ISuite; 151 | skip(description: string, spec: () => void): void; 152 | timeout(ms: number): void; 153 | } 154 | 155 | interface ITestDefinition { 156 | (expectation: string, assertion?: () => void): ITest; 157 | (expectation: string, assertion?: (done: MochaDone) => void): ITest; 158 | only(expectation: string, assertion?: () => void): ITest; 159 | only(expectation: string, assertion?: (done: MochaDone) => void): ITest; 160 | skip(expectation: string, assertion?: () => void): void; 161 | skip(expectation: string, assertion?: (done: MochaDone) => void): void; 162 | timeout(ms: number): void; 163 | } 164 | 165 | export module reporters { 166 | export class Base { 167 | stats: { 168 | suites: number; 169 | tests: number; 170 | passes: number; 171 | pending: number; 172 | failures: number; 173 | }; 174 | 175 | constructor(runner: IRunner); 176 | } 177 | 178 | export class Doc extends Base {} 179 | export class Dot extends Base {} 180 | export class HTML extends Base {} 181 | export class HTMLCov extends Base {} 182 | export class JSON extends Base {} 183 | export class JSONCov extends Base {} 184 | export class JSONStream extends Base {} 185 | export class Landing extends Base {} 186 | export class List extends Base {} 187 | export class Markdown extends Base {} 188 | export class Min extends Base {} 189 | export class Nyan extends Base {} 190 | export class Progress extends Base { 191 | /** 192 | * @param options.open String used to indicate the start of the progress bar. 193 | * @param options.complete String used to indicate a complete test on the progress bar. 194 | * @param options.incomplete String used to indicate an incomplete test on the progress bar. 195 | * @param options.close String used to indicate the end of the progress bar. 196 | */ 197 | constructor(runner: IRunner, options?: { 198 | open?: string; 199 | complete?: string; 200 | incomplete?: string; 201 | close?: string; 202 | }); 203 | } 204 | export class Spec extends Base {} 205 | export class TAP extends Base {} 206 | export class XUnit extends Base { 207 | constructor(runner: IRunner, options?: any); 208 | } 209 | } 210 | } 211 | 212 | declare module "mocha" { 213 | export = Mocha; 214 | } 215 | -------------------------------------------------------------------------------- /test/workflow-onError-test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var worksmith = require('../') 3 | 4 | describe("WorkSmith API - error handler workflow", function() { 5 | 6 | it("should be executed on an asyn error", function(done) { 7 | var flags = {}; 8 | 9 | var def = { 10 | task: function(def) { 11 | return function build(context) { 12 | return function execute(done) { 13 | done({"some":"err", "supressMessage":true}); 14 | } 15 | } 16 | }, 17 | onError: { 18 | task:function(def) { 19 | return function builde(context) { 20 | return function execute(done) { 21 | flags.errorSet = true; 22 | done(); 23 | } 24 | } 25 | } 26 | } 27 | }; 28 | 29 | var workflow = worksmith(def); 30 | workflow({}, function() { 31 | assert.ok(flags.errorSet, "error handler must be invoked") 32 | done(); 33 | }); 34 | }); 35 | 36 | it("should prevent executing the next workflow", function(done) { 37 | var flags = {}; 38 | 39 | var def = { 40 | task:"sequence", 41 | items: [{ 42 | task: function(def) { 43 | return function build(context) { 44 | return function execute(done) { 45 | //throw "alma"; 46 | done({"some":"err", "supressMessage":true}); 47 | } 48 | } 49 | }, 50 | onError: { 51 | task:function(def) { 52 | return function builde(context) { 53 | return function execute(done) { 54 | flags.errorSet = true; 55 | done(); 56 | } 57 | } 58 | } 59 | } 60 | }, 61 | {task:function(def) { return function(context) {return function(done) { flags.nextWfRun = true; done() }}}} 62 | 63 | ] 64 | }; 65 | 66 | var workflow = worksmith(def); 67 | workflow({}, function() { 68 | assert.notEqual(flags.nextWfRun, true, "next wf should not be invoked") 69 | done(); 70 | }); 71 | }); 72 | 73 | it("should allow executing the next workflow if handleError is true", function(done) { 74 | var flags = {}; 75 | 76 | var def = { 77 | task:"sequence", 78 | items: [{ 79 | task: function(def) { 80 | return function build(context) { 81 | return function execute(done) { 82 | //throw "alma"; 83 | done({"some":"err", "supressMessage":true}); 84 | } 85 | } 86 | }, 87 | onError: { 88 | handleError: true, 89 | task:function(def) { 90 | return function builde(context) { 91 | return function execute(done) { 92 | done(); 93 | } 94 | } 95 | } 96 | } 97 | }, 98 | {task:function(def) { return function(context) {return function(done) { flags.nextWfRun = true; done() }}}} 99 | 100 | ] 101 | }; 102 | 103 | var workflow = worksmith(def); 104 | workflow({}, function() { 105 | assert.equal(flags.nextWfRun, true, "next wf should be invoked") 106 | done(); 107 | }); 108 | }); 109 | 110 | 111 | it("should prevent executing the next workflow if handleError is true but errorWf errs", function(done) { 112 | var flags = {}; 113 | 114 | var def = { 115 | task:"sequence", 116 | items: [{ 117 | task: function(def) { 118 | return function build(context) { 119 | return function execute(done) { 120 | //throw "alma"; 121 | done({"some":"err", "supressMessage":true}); 122 | } 123 | } 124 | }, 125 | onError: { 126 | handleError: true, 127 | task:function(def) { 128 | return function builde(context) { 129 | return function execute(done) { 130 | done({some:"error", "supressMessage":true}); 131 | } 132 | } 133 | } 134 | } 135 | }, 136 | {task:function(def) { return function(context) {return function(done) { flags.nextWfRun = true; done() }}}} 137 | 138 | ] 139 | }; 140 | 141 | var workflow = worksmith(def); 142 | workflow({}, function() { 143 | assert.notEqual(flags.nextWfRun, true, "next wf should not be invoked") 144 | done(); 145 | }); 146 | }); 147 | 148 | 149 | it("should provide a correct err object", function(done) { 150 | var flags = {}; 151 | 152 | var def = { 153 | task: function(def) { 154 | return function build(context) { 155 | return function execute(done) { 156 | //throw "alma"; 157 | done({"message":"err", "supressMessage":true}); 158 | } 159 | } 160 | }, 161 | onError: { 162 | task:function(def) { 163 | return function builde(context) { 164 | return function execute(done) { 165 | done({message:"hello", supressMessage:true}); 166 | } 167 | } 168 | } 169 | } 170 | }; 171 | 172 | var workflow = worksmith(def); 173 | workflow({}, function(err) { 174 | assert.equal(err.message, "err", "error must be provided") 175 | done(); 176 | }); 177 | }); 178 | 179 | it("should work with a sequence activity", function(done) { 180 | var flags = {}; 181 | 182 | var def = { 183 | task: "sequence", 184 | items: [ 185 | { 186 | task: function(def) { 187 | return function build(context) { 188 | return function execute(done) { 189 | //throw "alma"; 190 | flags.step1 = true; 191 | done({"message":"err", "supressMessage":true}); 192 | } 193 | } 194 | } 195 | },{ 196 | task: function(def) { 197 | return function build(context) { 198 | return function execute(done) { 199 | flags.step2 = true; 200 | done() 201 | //throw "alma"; 202 | } 203 | } 204 | } 205 | }], 206 | onError: { 207 | task:function(def) { 208 | return function builde(context) { 209 | return function execute(done) { 210 | flags.step3 = true; 211 | done({message:"hello", supressMessage:true}); 212 | } 213 | } 214 | } 215 | } 216 | }; 217 | 218 | var workflow = worksmith(def); 219 | workflow({}, function(err) { 220 | assert.equal(flags.step1, true, "first step must be done") 221 | assert.notEqual(flags.step2, true, "second step mustnt be done") 222 | assert.equal(flags.step3, true, "third step must be done") 223 | done(); 224 | }); 225 | }); 226 | }); -------------------------------------------------------------------------------- /TUTORIAL.md: -------------------------------------------------------------------------------- 1 | # worksmith tutorial 2 | 3 | ## npm install --save worksmith 4 | 5 | ## The hello world workflow 6 | One of the simplest activities in worksmith is the `logActivity` that outputs its message to the JavaScript console. 7 | ```javascript 8 | var worksmith = require('worksmith') 9 | 10 | var workflow = worksmith({task:"log", message:"hello"}) 11 | workflow({}, function(err, res) { 12 | console.log("workflow executed") 13 | }) 14 | ``` 15 | ## Workflow as a json 16 | You can also keep your workflow definitions in separate files. 17 | 18 | json file 19 | ```json 20 | { 21 | "task":"log", "message":"hello" 22 | } 23 | ``` 24 | javascript file 25 | ```javascript 26 | module.exports = { 27 | task:"log", message:"hello" 28 | } 29 | ``` 30 | ```javascript 31 | var workflow = require('worksmith')('./definition.json') 32 | workflow({}, function(err, res) { 33 | console.log("workflow executed") 34 | }) 35 | ``` 36 | 37 | ## The workflow context 38 | Workflow steps communicate with the world using the workflow context, a shared state between individual tasks. 39 | They get input and write output from and to the context. 40 | 41 | Tasks can reference values from this shared context via their parameters using '@' as the first character in parameter values. 42 | 43 | ```javascript 44 | var worksmith = require('worksmith') 45 | 46 | var context = { helloMessage:"hello world" } 47 | var workflow = worksmith({task:"log", message:"@helloMessage"}) 48 | workflow(context, function(err, res) { 49 | console.log("workflow executed") 50 | }) 51 | ``` 52 | 53 | ## Multiple workflow steps 54 | A typical workflow will have multiple tasks to execute. Use one of the flow control activities to schedule them. 55 | The `sequence` activity will execute its steps one after the other. 56 | The `parallel` activity runs them well parallel. 57 | 58 | ```javascript 59 | var workflow = worksmith({ 60 | task:"sequence", 61 | items: [ 62 | { task:"log", message:"@p1" }, 63 | { task:"log", message:"@p2" } 64 | ] 65 | }); 66 | 67 | workflow({p1:"answer", p2:42}, function(err, res) { 68 | console.log("workflow executed") 69 | }) 70 | ``` 71 | 72 | ## Task results and the `map` activity 73 | Tasks may have a "return value" - a result that other workflow steps that come later can access. 74 | The context is there to store this return value, if you specify a context field name in the `resultTo` task parameter. 75 | 76 | The `map` activity lets you produce a new value on the context based on existing context values - or workflow service method results. 77 | It also supports the `>` resultTo shortcut in the place of the map parameter 78 | 79 | ```javascript 80 | var workflow = worksmith({ 81 | task:"sequence", 82 | items: [ 83 | { task:"map", map: { f1:"@p1", f2:"@p2" }, resultTo:"mapData.d1" }, 84 | { task:"map", ">mapData.d2": ["@p1","Hello"] }, 85 | { task:"log", message:"@mapData.d2[1]" } 86 | ] 87 | }); 88 | 89 | workflow({p1:"answer", p2:42}, function(err, res) { 90 | console.log("workflow executed") 91 | }) 92 | ``` 93 | 94 | ## How to create a custom task type (activity) 95 | Tasks are referenced by their names in workflow definitions and are looked up in the `~/src/tasks` folder of your application. 96 | To create a smarter logger activity place a file named `log.js` in your tasks folder. With this you override the default log activity. 97 | 98 | `src/tasks/log.js` 99 | ```javascript 100 | module.exports = function define(params) { 101 | //params will contain the tasks json config eg 102 | // {task:"job", message:"@p1", "level":"warn"} 103 | return function(context) { 104 | //context will hold the execution state e.g. 105 | //{p1:"hello"} 106 | return function(done) { 107 | console[context.get(params.level) || 'log'](context.get(params.message)); 108 | done() 109 | } 110 | 111 | } 112 | } 113 | ``` 114 | 115 | `src/app.js` 116 | ```javascript 117 | var workflow = worksmith({ 118 | task:"sequence", 119 | items: [ 120 | { task:"map", map: { f1:"@p1", f2:"@p2" }, resultTo:"mapData.d1" }, 121 | { task:"log", message:"@mapData.d1.f1", level:"warn" } 122 | ] 123 | }); 124 | 125 | workflow({p1:"answer"}, function(err, res) { 126 | console.log("workflow executed") 127 | }) 128 | ``` 129 | ## Getting task params injected 130 | Use injection rather then context.get - because 1) it is more sexy, 2) it will support async injection of params in remote workflows 131 | 132 | `src/tasks/log.js` 133 | ```javascript 134 | module.exports = function define(params) { 135 | //params will contain the tasks json config eg 136 | // {task:"job2", message:"@p1", "level":"warn"} 137 | return function(context) { 138 | //context will hold the execution state e.g. 139 | //{p1:"hello"} 140 | execute.inject = ["message","level"] 141 | function execute(msg, lvl, done) { 142 | console[lvl || 'log'](msg); 143 | done() 144 | } 145 | return execute 146 | } 147 | } 148 | ``` 149 | ## Returning values 150 | To set the value for the field name specified in `resultTo` just provide data for the done() handler. 151 | `src/resulter.js` 152 | ```javascript 153 | module.exports = function define(params) { 154 | 155 | return function(context) { 156 | 157 | function execute(done) { 158 | 159 | setTimeout(function() { //async process 160 | done(undefined, {some:"value"}) 161 | }, 100); 162 | 163 | } 164 | return execute 165 | } 166 | } 167 | ``` 168 | ``` 169 | var workflow = worksmith({ 170 | task:"sequence", 171 | items: [ 172 | { task:"resulter", resultTo:"result" }, 173 | { task:"log", message:"@result" } 174 | ] 175 | }); 176 | ``` 177 | 178 | ## Errors in a workflow 179 | To signal error in a workflow simply set value for the error argument in the done() callback. 180 | No subsequent tasks will be executed. 181 | 182 | ```javascript 183 | module.exports = function define(params) { 184 | 185 | return function(context) { 186 | 187 | function execute(done) { 188 | 189 | setTimeout(function() { //async process 190 | done(new Error("something")) 191 | }, 100); 192 | 193 | } 194 | return execute 195 | } 196 | } 197 | ``` 198 | ### Error receiver 199 | You can have a workflow assigned to an error event. This will receive the error in `context.error`. 200 | 201 | ```javascript 202 | var workflow = worksmith({ 203 | task:"sequence", 204 | items: [ 205 | { 206 | task:"some_erring_workflow", 207 | onError: { 208 | task: "some_error_receiver" 209 | } 210 | }, 211 | { 212 | task:"some_task_that_will_never_run" 213 | } 214 | ] 215 | }); 216 | ``` 217 | 218 | ### Error handler 219 | You can have a workflow assigned to an error event. This can handle the error in `context.error`. The error handler workflow has the 220 | option to swallow the error and let the whole main workflow to continue. Just call `done()` w/o an error to have that happen. 221 | 222 | ```javascript 223 | var workflow = worksmith({ 224 | task:"sequence", 225 | items: [ 226 | { 227 | task:"some_erring_workflow", 228 | onError: { 229 | handleError: true, 230 | task: "some_error_handler" 231 | } 232 | }, 233 | { 234 | task:"some_task_that_might_still_run" 235 | } 236 | ] 237 | }); 238 | 239 | ``` 240 | ## Using a task collection module 241 | 242 | ```javascript 243 | var worksmith = require('worksmith') 244 | worksmith.use("more", require('worksmith_example_module')) 245 | var wf = worksmith({ 246 | "task":"more/random", 247 | resultTo: "rnd" 248 | }); 249 | wf({},console.log) 250 | ``` 251 | 252 | ## How to create a task collection module 253 | Check out an example module [here](https://github.com/PeterAronZentai/worksmith_example_module). 254 | 255 | 256 | ## The `workflow' task type 257 | You can execute a complete workflow as a task. 258 | Its good for shared workflow steps. 259 | 260 | Use the `workflow` task type and 261 | specify workflow `source` with its path or its object definition. 262 | Subworkflows inherit the parent context by default. 263 | You can optionally set a new `context` for the subworkflow 264 | by specifying value for the `context` param. 265 | 266 | ```javascript 267 | var workflow = worksmith({ 268 | task:"sequence", 269 | items: [ 270 | { 271 | task:"set", 272 | name:"param", 273 | value:{ abc:"def" } 274 | }, 275 | { 276 | task:"workflow", 277 | source:"./src/workflows/myworkflow.js", 278 | context: "@param" 279 | } 280 | ] 281 | }); 282 | ``` 283 | 284 | 285 | -------------------------------------------------------------------------------- /test/workflow-api-test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var workflow = require('../') 3 | 4 | describe("WorkSmith API", function() { 5 | 6 | it("should run a workflow", function(done) { 7 | var flags = {}; 8 | 9 | var def = { 10 | task: function(def) { 11 | flags.define = true; 12 | return function build(context) { 13 | flags.build = true; 14 | return function execute(done) { 15 | flags.run = true; 16 | done(); 17 | } 18 | } 19 | }, 20 | param1: "value1" 21 | }; 22 | 23 | var wf = workflow(def) 24 | var wi = wf({}); 25 | assert.ok(flags.define, "definition function must have run") 26 | assert.ok(flags.build, "build function must have run") 27 | wi(function(err, res) { 28 | assert.ok(flags.run, "execute function must have run") 29 | assert.ok(true,"always") 30 | done(); 31 | }) 32 | }) 33 | 34 | it("should provide worksmith as 'this' for definer", function(done) { 35 | var flags = {}; 36 | 37 | var def = { 38 | task: function(def) { 39 | flags.definerThis = this; 40 | return function build(context) { 41 | return function execute(done) { 42 | done(); 43 | } 44 | } 45 | } 46 | }; 47 | 48 | var wf = workflow(def) 49 | var wi = wf({}); 50 | wi(function(err, res) { 51 | assert.equal(flags.definerThis,workflow, "worksmith instance does not match") 52 | done(); 53 | }) 54 | }) 55 | 56 | it("should pass definition to builder", function(done) { 57 | var flags = {}; 58 | 59 | var def = { 60 | task: function(def) { 61 | flags.definition = def; 62 | return function build(context) { 63 | return function execute(done) { 64 | done(); 65 | } 66 | } 67 | }, 68 | param1: "value1" 69 | }; 70 | 71 | var wf = workflow(def) 72 | var wi = wf({}); 73 | wi(function(err, res) { 74 | assert.equal(flags.definition,def, "definition must be passed correctly") 75 | done(); 76 | }) 77 | }) 78 | 79 | it("should pass context to executor", function(done) { 80 | var flags = {}; 81 | 82 | var def = { 83 | task: function(def) { 84 | return function build(context) { 85 | flags.context = context; 86 | return function execute(done) { 87 | done(); 88 | } 89 | } 90 | }, 91 | param1: "value1" 92 | }; 93 | 94 | var wf = workflow(def) 95 | var context = {}; 96 | var wi = wf(context); 97 | wi(function(err, res) { 98 | assert.equal(flags.context,context, "context must be passed correctly") 99 | done(); 100 | }) 101 | }) 102 | 103 | it("should handle 'resultTo' pipeline", function(done) { 104 | var def = { 105 | task: function(def) { 106 | return function build(context) { 107 | return function execute(done) { 108 | done(null, 42); 109 | } 110 | } 111 | }, 112 | resultTo:"result" 113 | }; 114 | var wf = workflow(def) 115 | var context = {}; 116 | var wi = wf(context); 117 | wi(function(err, res) { 118 | assert.equal(context.result,42, "result must be passed correctly") 119 | done(); 120 | }) 121 | 122 | }) 123 | 124 | it("should provide 3 args to result callbacks", function(done) { 125 | var def = { 126 | task: function(def) { 127 | return function build(context) { 128 | return function execute(done) { 129 | done(null, 42); 130 | } 131 | } 132 | }, 133 | resultTo:"result" 134 | }; 135 | var wf = workflow(def) 136 | var context = {}; 137 | var wi = wf(context); 138 | wi(function(err, res, ctx) { 139 | assert.equal(arguments.length, 3, "args count must match") 140 | assert.equal(ctx, context, "context must match") 141 | done(); 142 | }) 143 | 144 | }) 145 | 146 | 147 | 148 | describe("parameter injector", function() { 149 | it("should inject definition fields correctly - static value", function(done) { 150 | var flags = {}; 151 | 152 | var def = { 153 | 154 | task: function(def) { 155 | function build(context) { 156 | function execute(param1, done) { 157 | flags.param1 = param1; 158 | done(); 159 | } 160 | execute.annotations = { inject:["param1"] } 161 | return execute 162 | } 163 | return build 164 | }, 165 | param1: "value1" 166 | }; 167 | 168 | var wf = workflow(def) 169 | var context = { field1:"value1" }; 170 | var wi = wf(context); 171 | wi(function(err, res) { 172 | assert.equal(flags.param1,"value1", "injected must be passed correctly") 173 | done(); 174 | }) 175 | }) 176 | 177 | it("should inject context fields correctly", function(done) { 178 | var flags = {}; 179 | 180 | var def = { 181 | 182 | task: function(def) { 183 | function build(context) { 184 | function execute(param1, done) { 185 | flags.param1 = param1; 186 | done(); 187 | } 188 | execute.annotations = { inject:["@field1"] } 189 | return execute 190 | } 191 | return build 192 | } 193 | }; 194 | 195 | var wf = workflow(def) 196 | var context = { field1:"value1" }; 197 | var wi = wf(context); 198 | wi(function(err, res) { 199 | assert.equal(flags.param1,"value1", "injected must be passed correctly") 200 | done(); 201 | }) 202 | }) 203 | }) 204 | 205 | it("should handle inject shortcut ", function(done) { 206 | var flags = {}; 207 | 208 | var def = { 209 | 210 | task: function(def) { 211 | function build(context) { 212 | function execute(param1, done) { 213 | flags.param1 = param1; 214 | done(); 215 | } 216 | execute.inject = ["@field1"] 217 | return execute 218 | } 219 | return build 220 | } 221 | }; 222 | 223 | var wf = workflow(def) 224 | var context = { field1:"value1" }; 225 | var wi = wf(context); 226 | wi(function(err, res) { 227 | assert.equal(flags.param1,"value1", "injected must be passed correctly") 228 | done(); 229 | }) 230 | }) 231 | it("should provide shortcut for sequence acitivity", function(done) { 232 | var steps = []; 233 | var counter = 0; 234 | steps.push({task: function() { return function() { return function(done) { counter++; done(); }}}}); 235 | steps.push({task: function() { return function() { return function(done) { counter++; done(); }}}}); 236 | workflow(steps)({}, function() { 237 | assert.equal(counter, 2) 238 | done(); 239 | }) 240 | }) 241 | it("should handle injecting arrays ", function(done) { 242 | var flags = {}; 243 | 244 | var def = { 245 | 246 | task: function(def) { 247 | function build(context) { 248 | function execute(f1, f2, done) { 249 | flags.f1 = f1; 250 | flags.f2 = f2; 251 | done(); 252 | } 253 | execute.annotations = { inject:["f1","f2"] } 254 | return execute 255 | } 256 | return build 257 | }, 258 | f1: "@field1", 259 | f2: "@field2" 260 | }; 261 | 262 | var wf = workflow(def) 263 | var context = { field1:"value1", field2:["a","b","c"] }; 264 | var wi = wf(context); 265 | wi(function(err, res) { 266 | assert.equal(flags.f1,"value1", "injected must be passed correctly") 267 | assert.equal(flags.f2, context.field2, "injected must be passed correctly") 268 | done(); 269 | }) 270 | }) 271 | 272 | xit("should not call wf.complete twice on an throw", function(done) { 273 | var def = [ 274 | { 275 | task: function(step) { 276 | return function(context) { 277 | return function execute(done) { 278 | done(); 279 | } 280 | } 281 | } 282 | } 283 | ] 284 | var flag = []; 285 | var wf = workflow(def) 286 | wf({}, function(err, res) { 287 | flag.push({}) 288 | throw new Error("error") 289 | }); 290 | //flag.length == 1 291 | }) 292 | }) -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('workflow:common') 2 | var _ = require('lodash') 3 | var path = require('path') 4 | var handlebars = require('handlebars') 5 | var fs = require('fs') 6 | var async = require('async') 7 | var util = require('util') 8 | 9 | var taskTypeCache = {}; 10 | var DEFAULT_TASK_PATH = "/src/tasks/" 11 | var taskPath 12 | var resolvers = {}; 13 | 14 | var settings = { 15 | logger: console 16 | } 17 | 18 | var interpolate = require('./src/interpolation').interpolate 19 | 20 | var worksmith 21 | 22 | function wfLoader(wf) { 23 | if ("string" === typeof wf) { 24 | wf = path.resolve(wf) 25 | debug("loading workflow file: %s", wf) 26 | wf = require(wf); 27 | } 28 | if(Array.isArray(wf)) { 29 | wf = { 30 | task: "sequence", 31 | items: wf 32 | } 33 | } 34 | return worksmith.define(wf); 35 | } 36 | 37 | worksmith = wfLoader 38 | 39 | function getStepName(step) { 40 | if (step.name) { return step.name } 41 | var name = step.name || step.task; 42 | if ("function" === typeof name) { 43 | name = "" + name.name 44 | } 45 | return step.name = name; 46 | } 47 | 48 | var workflow = { 49 | 50 | use: function(ns, taskLibrary) { 51 | resolvers[ns] = taskLibrary; 52 | }, 53 | 54 | createAdapter: function(object) { 55 | return function getType(name) { 56 | var method = object[name]; 57 | return function define(step) { 58 | return function build(context) { 59 | return function execute(done) { 60 | var args = (context.get(step.arguments) || []) 61 | var result = method.apply(object, args) 62 | done(undefined, result); 63 | } 64 | } 65 | } 66 | } 67 | }, 68 | configure: function(options) { 69 | _.assignIn(settings, options); 70 | }, 71 | 72 | hasLogLevel: function(level) { 73 | return settings.logger[level] !== undefined 74 | }, 75 | 76 | log: function() { 77 | var level = arguments[0] 78 | settings.logger[level].apply(settings.logger, Array.prototype.slice.call(arguments,1)) 79 | }, 80 | discoverTaskType: function(taskType) { 81 | var processRelativePath = path.join(process.cwd(), taskPath, taskType + ".js"); 82 | return fs.existsSync(processRelativePath) ? processRelativePath : "./tasks/" + taskType + ".js"; 83 | }, 84 | 85 | getTaskType: function(taskType) { 86 | if (taskType.indexOf("/") > -1) { 87 | var taskSpec = taskType.split("/"); 88 | var ns = taskSpec[0]; 89 | if (resolvers[ns]) { 90 | return resolvers[ns](taskSpec[1]); 91 | } 92 | } 93 | var taskFile = taskTypeCache[taskType] || (taskTypeCache[taskType] = workflow.discoverTaskType(taskType)) 94 | return require(taskFile); 95 | }, 96 | 97 | getWorkflow: function(task) { 98 | if ("string" === typeof task) return workflow.getTaskType(task); 99 | return task; 100 | }, 101 | 102 | 103 | 104 | define: function (workflowDefinition) { 105 | 106 | debug("defining: %s", getStepName(workflowDefinition)) 107 | taskPath = workflowDefinition.taskPath || DEFAULT_TASK_PATH 108 | var WorkflowType = workflow.getWorkflow(workflowDefinition.task) 109 | 110 | var wfInstance = WorkflowType.call(wfLoader, workflowDefinition) 111 | 112 | function checkCondition(context) { 113 | if (!("condition" in workflowDefinition)) { 114 | return true; 115 | } 116 | with(context) { 117 | if (eval(workflowDefinition.condition)) { 118 | return true; 119 | } 120 | return false; 121 | } 122 | 123 | } 124 | 125 | function initializeContext(context) { 126 | if (!context.initialized) { 127 | context.get = function(name, interpolate) { 128 | return workflow.readValue(name, this, interpolate) 129 | }, 130 | context.set = function(name, value) { 131 | return workflow.setValue(this, name, value) 132 | } 133 | context.initialized = true; 134 | } 135 | 136 | } 137 | 138 | function getArgumentsFromAnnotations(context, execute, build) { 139 | var args = []; 140 | //TODO: this line is reallly just interim. annotations should be merged or something 141 | var annotations = execute.annotations || build.annotations || WorkflowType.annotations; 142 | annotations = annotations || {}; 143 | execute.inject && (annotations.inject = execute.inject); 144 | annotations.inject && annotations.inject.forEach(function(injectable) { 145 | if ("string" === typeof injectable) { 146 | injectable = { 147 | name: injectable, 148 | interpolationPolicy: true, 149 | } 150 | } 151 | var arg; 152 | switch(injectable.name[0]) { 153 | case '@': arg = context.get(injectable.name); break; 154 | default: arg = context.get(workflowDefinition[injectable.name],injectable.interpolationPolicy); break; 155 | } 156 | args.push(arg) 157 | }) 158 | return args; 159 | 160 | } 161 | 162 | return function build(context) { 163 | if (arguments.length == 2) { 164 | return build(arguments[0])(arguments[1]) 165 | } 166 | initializeContext(context) 167 | var decorated = wfInstance(context) 168 | debug("preparing: %s", getStepName(workflowDefinition)) 169 | 170 | 171 | var markWorkflowTerminate = function(done) { 172 | context.completionStack = context.completionStack || [] 173 | context.completionStack.push(done) 174 | if (!context.workflowTerminate) { 175 | context.workflowTerminate = done 176 | } 177 | } 178 | 179 | 180 | return function execute(done) { 181 | var wfStartTime = new Date().getTime() 182 | 183 | if (!checkCondition(context)) 184 | return done() 185 | 186 | markWorkflowTerminate(done) 187 | 188 | function invokeDecorated(err, res, next) { 189 | function createExecutionThisContext() { 190 | return { 191 | workflow: { 192 | terminate: function(err, res, next) { 193 | context.originalTerminate = done 194 | done = context.workflowTerminate 195 | next(err, res) 196 | }, 197 | terminateParent: function(err, res, next, step) { 198 | context.originalTerminate = done 199 | done = context.completionStack[context.completionStack.length - (step || 2)] 200 | next(err, res) 201 | } 202 | } 203 | } 204 | } 205 | 206 | var args = getArgumentsFromAnnotations(context, decorated, wfInstance) 207 | args.push(next) 208 | try { 209 | decorated.apply(createExecutionThisContext(), args) 210 | } catch(ex) { 211 | next(ex) 212 | } 213 | } 214 | 215 | function onError(err, res, next) { 216 | if (!err) { return next(err, res) } 217 | var errorWfDef = context.get(workflowDefinition.onError); 218 | var errorWf = workflow.define(errorWfDef); 219 | context.error = err; 220 | errorWf(context, function(errHandlerErr, errRes) { 221 | if (errorWfDef.handleError) err = errHandlerErr; 222 | next(err, res); 223 | }) 224 | } 225 | 226 | function onComplete(err, res, next) { 227 | var finallyDef = context.get(workflowDefinition.finally); 228 | var finallyWf = workflow.define(finallyDef); 229 | finallyWf(context, function(finErr, finRes) { 230 | next(err, res) 231 | }) 232 | } 233 | function logErrors(err, result, next) { 234 | if (err) { 235 | debug("error in workflow %s, error is %o", getStepName(workflowDefinition), err.message || err) 236 | //make logging errors a configation options 237 | //if (!err.supressMessage) { 238 | // worksmith.log("error",util.format("Error in WF <%s>, error is:<%s> ", getStepName(workflowDefinition), err.message),err) 239 | //} 240 | } 241 | next(err, result) 242 | } 243 | 244 | function setWorkflowResultTo(err, result, next) { 245 | if (err) { return next(err, result) } 246 | process.env.WSDEBUGPARAMS && debug("...result is", result) 247 | context.set(workflowDefinition.resultTo, result) 248 | next(err, result) 249 | } 250 | 251 | function buildUpMicroworkflow() { 252 | var tasks = [invokeDecorated]; 253 | workflowDefinition.onError && tasks.push(onError) 254 | workflowDefinition.finally && tasks.push(onComplete) 255 | workflowDefinition.resultTo && tasks.push(setWorkflowResultTo) 256 | tasks.push(logErrors) 257 | return tasks; 258 | } 259 | 260 | var tasks = buildUpMicroworkflow(); 261 | 262 | function executeNextThunkOrComplete(err, res) { 263 | var thunk = tasks.shift(); 264 | if (thunk) { 265 | return thunk( err, res, executeNextThunkOrComplete) 266 | } 267 | 268 | debug("Finished executing WF %s", getStepName(workflowDefinition)) 269 | if (context.$$$stats) { 270 | context.$$$stats.push(getStepName(workflowDefinition) + " execution time: " + (new Date().getTime() - wfStartTime) +"ms") 271 | } 272 | var originalDone = context.originalTerminate || done; 273 | var donePosition = context.completionStack.indexOf(originalDone); 274 | context.completionStack.splice(donePosition, 1) 275 | setImmediate(function() { 276 | done(err, res, context) 277 | }) 278 | } 279 | 280 | debug("Executing WF %s", getStepName(workflowDefinition)) 281 | return executeNextThunkOrComplete() 282 | 283 | } 284 | } 285 | }, 286 | 287 | readValue: function(pathOrValue, context, interpolationPolicy) { 288 | return interpolate(context, pathOrValue, interpolationPolicy) 289 | }, 290 | 291 | setValue: function (object, path, value) { 292 | var parts = path.split('.'); 293 | path = path.replace(/\[/g, ".").replace(/\]/g, "") 294 | var part; 295 | while (part = parts.shift()) { 296 | if (parts.length) { 297 | object[part] = object[part] || {}; 298 | object = object[part]; 299 | } else { 300 | object[part] = value; 301 | } 302 | } 303 | } 304 | 305 | 306 | } 307 | 308 | 309 | _.assignIn(wfLoader, workflow); 310 | 311 | module.exports = wfLoader; --------------------------------------------------------------------------------