diff --git a/README.md b/README.md index 3d2b306..29aff7a 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,23 @@ where You can see some more examples in the tests here [here](test/errors/index.js) +## Sync API + +sql-stamp has a synchronous API. This is useful for processing SQL templates and exporting from node modules. + +```js + var sqlStamp = require("sql-stamp/sync"); + var templater = sqlStamp([ + /* Pass a list of SQL templates */ + __dirname+"/friends.sql", + __dirname+"/example.sql" + ]); + templater1(__dirname+"/example.sql", {foo: "bar"}); // => {sql: "select...", args: ["bar"]} + + var files = glob.sync("./sql/**/*.sql") + var templater2 = sqlStamp(files); + templater2(__dirname+"../lib/sql/foo.sql", {foo: "bar"}); // => {sql: "select...", args: ["bar"]} +``` ## Test Run the unit tests diff --git a/index.js b/index.js index e7baccf..ab906fd 100644 --- a/index.js +++ b/index.js @@ -37,4 +37,4 @@ module.exports = function(files, opts, callback) { } }) .nodeify(callback); -} +}; diff --git a/lib/operators/require.js b/lib/operators/require.js index 53379e9..0f3ceb9 100644 --- a/lib/operators/require.js +++ b/lib/operators/require.js @@ -1,20 +1,19 @@ -var assign = require("lodash.assign"); -var Bluebird = require("bluebird"); +var path = require("path"); var objByPath = require("obj-by-path"); -var path = require("path"); +var assign = require("lodash.assign"); module.exports = { parse: function(relFilepath) { var filepath = path.resolve(path.dirname(this.path), relFilepath); if(!this.templates.hasOwnProperty(filepath)) { - return Bluebird.props({ + return { template: this.recurse(filepath) - }); + }; } else { - return Bluebird.props({ + return { template: this.templates[filepath] - }); + }; } }, run: function(relFilepath, dataKey) { diff --git a/lib/parser/index.js b/lib/parser/index.js index 854dcc3..f3e1f78 100644 --- a/lib/parser/index.js +++ b/lib/parser/index.js @@ -1,10 +1,10 @@ var Bluebird = require("bluebird"); -var util = require("../util"); +var errors = require("../errors"); var operators = require("../operators"); +var parserRegex = require("./regex"); var runner = require("./runner"); var stack = require("./stack"); -var errors = require("../errors"); -var parserRegex = require("./regex"); +var util = require("../util"); var fs = Bluebird.promisifyAll( require("fs") @@ -22,7 +22,7 @@ function parseRule(templates, filepath, raw, type, args, indent, ln, opts) { return Bluebird.resolve() .then(function() { if(operator.parse) { - return operator.parse.apply({ + var obj = operator.parse.apply({ templates: templates, recurse: function(filepath) { return parse(filepath, templates, opts) @@ -34,6 +34,10 @@ function parseRule(templates, filepath, raw, type, args, indent, ln, opts) { }, path: filepath }, operatorArgs); + + if (obj) { + return Bluebird.props(obj); + } } }) .then(function(_args) { @@ -44,7 +48,7 @@ function parseRule(templates, filepath, raw, type, args, indent, ln, opts) { var out = operator.run.apply({templates: templates, data: ctx, parseArgs: _args}, operatorArgs); // TODO: Nasty - out.sql = out.sql.split("\n").map(function(line, idx) { + out.sql = String(out.sql).split("\n").map(function(line, idx) { if(idx > 0) { return indent + line; } else { @@ -71,42 +75,6 @@ function parse(filepath, templates, opts) { var indent = ""; var foundChar = false; - /** - * Ok so this regexp is a little confusing, so lets break it down - * - * Firstly its aim is to walk over a string (using RegExp#exec). The RegExp will always match something and the grouping tells us what that something is - * - * It'll be either a - * - * - COMMAND, with a nested: - * - COMMAND_TYPE - * - COMMAND_ARGS - * - NEWLINE - * - SQL - * - * Which is mapped in 'posMap' below - * - * These are commented in the broken apart regexp below - * - * # group but ignore in output - * /(?: - * # Is the current part a known templating [COMMAND] - * ( - * \{ - * # Capture the [COMMAND_TYPE] - * ([#=?!>]) - * # Capture the [COMMAND_ARGS], up to the next '{' or '}' - * ([^}{]*) - * \} - * ) - * # OR is it a [NEWLINE] - * |([\n]) - * # ELSE its some [SQL] match upto the next '{' or '}' this is kind of a hack to allow braces that are not a part of the syntax - * |(.[^}{\n]*) - * # Global and multiline - * )/gm - * - */ var re = parserRegex.re(); var ln = 0; diff --git a/lib/parser/sync.js b/lib/parser/sync.js new file mode 100644 index 0000000..5627ec6 --- /dev/null +++ b/lib/parser/sync.js @@ -0,0 +1,143 @@ +var errors = require("../errors"); +var fs = require("fs"); +var operators = require("../operators"); +var parserRegex = require("./regex"); +var runner = require("./runner"); +var stack = require("./stack"); +var util = require("../util"); + +function parseRuleSync(templates, filepath, raw, type, args, indent, ln, opts) { + var operator = operators[type]; + var operatorArgs = args.split(",") + .map(util.chomp) + .map(util.removeQuotes) + .filter(util.removeEmpty); + + var _args; + // Operator + if(operator.parse) { + _args = operator.parse.apply({ + templates: templates, + recurse: function(filepath) { + var data = parseSync(filepath, templates, opts); + // As a cache so we don't need to parse twice + templates[filepath] = data; + return data; + }, + path: filepath + }, operatorArgs); + } + + return { + sql: raw, + ln: ln, + fn: function(ctx) { + var out = operator.run.apply({templates: templates, data: ctx, parseArgs: _args}, operatorArgs); + + // TODO: Nasty + out.sql = String(out.sql).split("\n").map(function(line, idx) { + if(idx > 0) { + return indent + line; + } else { + return line; + } + }).join("\n"); + return out; + } + }; +} + +function parseSync(filepath, templates, opts) { + try { + var sqlRaw = util.chomp( + fs.readFileSync(filepath) + ); + + var tokens = []; + var errStack = []; + var indent = ""; + var foundChar = false; + + var ln = 0; + + var re = parserRegex.re(); + var posMap = parserRegex.posMap; + + var idx = -1; + var m; + + while(m = re.exec(sqlRaw)) { + idx++; + if(!foundChar && m[0].match(/^\s*$/m)) { + indent += m[0]; + } else { + foundChar = true; + } + + if(m[posMap.sql]) { + tokens.push({ + sql: m[posMap.sql], + ln: ln, + idx: idx + }); + } else if(m[posMap.cmd]) { + var _m = m; + var _ln = ln; + var _idx = idx; + + if(m[posMap.cmdType].match(/^[=!>?]/)) { + + try { + var token = parseRuleSync(templates, filepath, m[posMap.cmd], m[posMap.cmdType], m[posMap.cmdArgs], indent, ln, opts); + tokens.push(token); + } catch (err) { + errStack.push({ + indent: indent, + idx: _idx, + err: err + }); + + tokens.push({ + ln: _ln, + sql: _m[0], + idx: _idx + }); + } + } else { + tokens.push({ + sql: m[posMap.cmd], + ln: ln, + idx: idx + }); + } + + } else if(m[posMap.nl]) { + tokens.push({ + sql: m[posMap.nl], + ln: ln, + idx: idx + }); + indent = ""; + foundChar = false; + ln++; + } + } + + if(errStack.length > 0) { + // TODO: Should be tracing all errStack here... + throw errStack[0].err + stack.trace(tokens, errStack[0], 100); + } + + // Return a template runner + var ret = runner.bind(null, tokens, opts); + return ret; + + } catch (err) { + if(err.code === "ENOENT") { + throw new errors.SQLError("No such template '"+filepath+"'"); + } + throw err; + } +} + +module.exports = parseSync; diff --git a/sync.js b/sync.js new file mode 100644 index 0000000..6b22275 --- /dev/null +++ b/sync.js @@ -0,0 +1,34 @@ +var assign = require("lodash.assign"); +var path = require("path"); +var parser = require("./lib/parser/sync"); +var errors = require("./lib/errors"); + +/** + * Initialize a SQL templater synchronously + * @param {Array} files + * @param {Object} [opts] + * @param {Function} [callback] + * @return {Promise} + */ +module.exports = function(files, opts) { + opts = assign({ + prettyErrors: false + }, opts); + + // Generate the templates + var templates = {}; + files.forEach(function(filepath) { + filepath = path.resolve(filepath); + templates[filepath] = parser(filepath, templates, opts); + }); + + return function(key, data) { + key = path.normalize(key); + + if(templates.hasOwnProperty(key)) { + return templates[key](data); + } else { + throw new errors.NoSuchTemplate(key); + } + } +} diff --git a/test/functional/async.js b/test/functional/async.js new file mode 100644 index 0000000..bd7955c --- /dev/null +++ b/test/functional/async.js @@ -0,0 +1,39 @@ +var assert = require("assert"); +var sqlStamp = require("../../"); +var util = require("../util"); + +var results = util.readSync([ + "./out.sql" +], __dirname); + +describe("end-to-end", function() { + + describe("async", function () { + var tmpl; + + before(function(done) { + sqlStamp([ + __dirname+"/example.sql", + __dirname+"/friends.sql", + ], {}, function(err, _tmpl) { + tmpl = _tmpl; + done(); + }); + }); + + + it("should work", function() { + var out = tmpl(__dirname+"/example.sql", { + accountId: 1, + filterDisabled: false, + filterKey: "role", + filterVal: "dev" + }); + + assert.equal(out.args.length, 2); + assert.equal(out.args[0], 1); + assert.equal(out.args[1], "dev"); + assert.equal(out.sql, results["./out.sql"]); + }); + }); +}); diff --git a/test/functional/index.js b/test/functional/index.js index 09b3275..eb6a616 100644 --- a/test/functional/index.js +++ b/test/functional/index.js @@ -1,36 +1,2 @@ -var assert = require("assert"); -var sqlStamp = require("../../"); -var util = require("../util"); - -var results = util.readSync([ - "./out.sql" -], __dirname); - -describe("end-to-end", function() { - var tmpl; - - before(function(done) { - sqlStamp([ - __dirname+"/example.sql", - __dirname+"/friends.sql", - ], {}, function(err, _tmpl) { - tmpl = _tmpl; - done(); - }); - }); - - - it("should work", function() { - var out = tmpl(__dirname+"/example.sql", { - accountId: 1, - filterDisabled: false, - filterKey: "role", - filterVal: "dev" - }); - - assert.equal(out.args.length, 2); - assert.equal(out.args[0], 1); - assert.equal(out.args[1], "dev"); - assert.equal(out.sql, results["./out.sql"]); - }); -}); +require("./async"); +require("./sync"); diff --git a/test/functional/sync.js b/test/functional/sync.js new file mode 100644 index 0000000..e412d38 --- /dev/null +++ b/test/functional/sync.js @@ -0,0 +1,36 @@ +var assert = require("assert"); +var sqlStampSync = require("../../sync"); +var util = require("../util"); + +var results = util.readSync([ + "./out.sql" +], __dirname); + +describe("end-to-end", function() { + + describe("sync", function () { + var tmpl; + + before(function() { + tmpl = sqlStampSync([ + __dirname+"/example.sql", + __dirname+"/friends.sql", + ]); + }); + + + it("should work", function() { + var out = tmpl(__dirname+"/example.sql", { + accountId: 1, + filterDisabled: false, + filterKey: "role", + filterVal: "dev" + }); + + assert.equal(out.args.length, 2); + assert.equal(out.args[0], 1); + assert.equal(out.args[1], "dev"); + assert.equal(out.sql, results["./out.sql"]); + }); + }); +});