From 728218909adb2cef933ae455d5e2200e70822663 Mon Sep 17 00:00:00 2001 From: mike-eason Date: Fri, 28 Jul 2017 15:08:38 +0100 Subject: [PATCH 1/2] Major refactoring and support for SQL transactions --- Data.cs | 290 ++++++++++++++++++++++++++++------------------ README.md | 74 +++++++++--- index.js | 99 ++++++++++++---- package-lock.json | 20 +++- 4 files changed, 336 insertions(+), 147 deletions(-) diff --git a/Data.cs b/Data.cs index 70071fc..42ce820 100644 --- a/Data.cs +++ b/Data.cs @@ -15,7 +15,6 @@ public class Startup { public async Task Invoke(IDictionary parameters) { - //Convert the input parameters to a useable object. ParameterCollection pcol = new ParameterCollection(parameters); using (DbConnection connection = CreateConnection(pcol.ConnectionString, pcol.ConnectionType)) @@ -24,19 +23,32 @@ public async Task Invoke(IDictionary parameters) { await connection.OpenAsync(); - //Work out which query type to execute. - switch (pcol.QueryType) + using (var command = connection.CreateCommand()) { - case QueryTypes.query: - return await ExecuteQuery(connection, pcol.Query, pcol.Parameters); - case QueryTypes.scalar: - return await ExecuteScalar(connection, pcol.Query, pcol.Parameters); - case QueryTypes.command: - return await ExecuteNonQuery(connection, pcol.Query, pcol.Parameters); - case QueryTypes.procedure: - return await ExecuteProcedure(connection, pcol.Query, pcol.Parameters, pcol.ReturnParameter); - default: - throw new InvalidOperationException("Unsupported type of SQL command. Only 'query', 'scalar' and 'command' are supported."); + //If there is only one command then execute it on it's own. + //Otherwise run all commands as a single transaction. + if (pcol.Commands.Count == 1) + { + var com = pcol.Commands[0]; + + switch (com.type) + { + case QueryTypes.query: + return await ExecuteQuery(command, com); + case QueryTypes.scalar: + return await ExecuteScalar(command, com); + case QueryTypes.command: + return await ExecuteNonQuery(command, com); + case QueryTypes.procedure: + return await ExecuteProcedure(command, com); + default: + throw new NotSupportedException("Unsupported type of database command. Only 'query', 'scalar', 'command' and 'procedure' are supported."); + } + } + else + { + return await ExecuteTransaction(connection, command, pcol.Commands); + } } } finally @@ -61,88 +73,126 @@ private DbConnection CreateConnection(string connectionString, ConnectionTypes t throw new NotImplementedException(); } - private async Task ExecuteQuery(DbConnection connection, string query, object[] parameters) + private async Task ExecuteQuery(DbCommand command, Command com) { - using (var command = connection.CreateCommand()) - { - command.CommandText = query; - - AddCommandParameters(command, parameters); + command.CommandText = com.query; - using (DbDataReader reader = command.ExecuteReader()) - { - List results = new List(); + AddCommandParameters(command, com.@params); - do - { - results.Add(await ParseReaderRow(reader)); - } - while (await reader.NextResultAsync()); + using (DbDataReader reader = command.ExecuteReader()) + { + List results = new List(); - return results; + do + { + results.Add(await ParseReaderRow(reader)); } + while (await reader.NextResultAsync()); + + return results; } } - private async Task ExecuteScalar(DbConnection connection, string query, object[] parameters) + private async Task ExecuteScalar(DbCommand command, Command com) { - using (var command = connection.CreateCommand()) - { - command.CommandText = query; + command.CommandText = com.query; - AddCommandParameters(command, parameters); + AddCommandParameters(command, com.@params); - return await command.ExecuteScalarAsync(); - } + return await command.ExecuteScalarAsync(); } - private async Task ExecuteNonQuery(DbConnection connection, string query, object[] parameters) + private async Task ExecuteNonQuery(DbCommand command, Command com) { - using (var command = connection.CreateCommand()) - { - command.CommandText = query; + command.CommandText = com.query; - AddCommandParameters(command, parameters); + AddCommandParameters(command, com.@params); - return await command.ExecuteNonQueryAsync(); + return await command.ExecuteNonQueryAsync(); + } + + private async Task ExecuteProcedure(DbCommand command, Command com) + { + bool hasReturnParameter = com.returns != null; + + command.CommandText = com.query; + command.CommandType = CommandType.StoredProcedure; + + AddCommandParameters(command, com.@params); + + if (hasReturnParameter) + { + DbParameter returnParam = command.CreateParameter(); + returnParam.ParameterName = com.returns.parameterName; + returnParam.Direction = ParameterDirection.ReturnValue; + returnParam.Value = com.returns.value; + + if (com.returns.precision != null) + returnParam.Precision = (byte)com.returns.precision; + if (com.returns.scale != null) + returnParam.Scale = (byte)com.returns.scale; + if (com.returns.size != null) + returnParam.Size = (byte)com.returns.size; + + command.Parameters.Add(returnParam); } + + object result = await command.ExecuteScalarAsync(); + + if (hasReturnParameter) + return command.Parameters[com.returns.parameterName].Value; + else + return result; } - private async Task ExecuteProcedure(DbConnection connection, string query, object[] parameters, ReturnParameter returns) + private async Task ExecuteTransaction(DbConnection connection, DbCommand command, List commands) { - bool hasReturnParameter = returns != null; + DbTransaction transaction = null; - using (var command = connection.CreateCommand()) + try { - command.CommandText = query; - command.CommandType = CommandType.StoredProcedure; + transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted); - AddCommandParameters(command, parameters); + command.Transaction = transaction; - if (hasReturnParameter) + foreach (Command com in commands) { - DbParameter returnParam = command.CreateParameter(); - returnParam.ParameterName = returns.ParameterName; - returnParam.Direction = ParameterDirection.ReturnValue; - returnParam.Value = returns.Value; - - if (returns.Precision != null) - returnParam.Precision = (byte)returns.Precision; - if (returns.Scale != null) - returnParam.Scale = (byte)returns.Scale; - if (returns.Size != null) - returnParam.Size = (byte)returns.Size; - - command.Parameters.Add(returnParam); + switch (com.type) + { + case QueryTypes.command: + com.result = await ExecuteNonQuery(command, com); + break; + case QueryTypes.query: + com.result = await ExecuteQuery(command, com); + break; + case QueryTypes.scalar: + com.result = await ExecuteScalar(command, com); + break; + case QueryTypes.procedure: + com.result = await ExecuteProcedure(command, com); + break; + default: + throw new NotSupportedException("Unsupported type of database command. Only 'query', 'scalar', 'command' and 'procedure' are supported."); + } } - object result = await command.ExecuteScalarAsync(); + transaction.Commit(); + } + catch + { + try + { + transaction.Rollback(); + } + catch + { + //Do nothing here; transaction is not active. + } - if (hasReturnParameter) - return command.Parameters[returns.ParameterName].Value; - else - return result; + throw; } + + return commands; } private async Task> ParseReaderRow(DbDataReader reader) @@ -180,7 +230,8 @@ private async Task> ParseReaderRow(DbDataReader reader) private void AddCommandParameters(DbCommand command, object[] parameters) { - //Generate names for each parameter and add them to the parameter collection. + command.Parameters.Clear(); + for (int i = 0; i < parameters.Length; i++) { string name = string.Format("@p{0}", i + 1); @@ -219,16 +270,16 @@ public class ParameterCollection public string ConnectionString { get; private set; } public ConnectionTypes ConnectionType { get; private set; } - public QueryTypes QueryType { get; private set; } - public string Query { get; private set; } - public object[] Parameters { get; private set; } - public ReturnParameter ReturnParameter { get; private set; } + + public List Commands { get; private set; } public ParameterCollection(IDictionary parameters) { if (parameters == null) throw new ArgumentNullException("parameters"); + Commands = new List(); + _Raw = parameters; ParseRawParameters(); } @@ -241,12 +292,6 @@ private void ParseRawParameters() if (string.IsNullOrWhiteSpace(ConnectionString)) throw new ArgumentNullException("constring"); - //Extract the query - Query = _Raw["query"].ToString(); - - if (string.IsNullOrWhiteSpace(Query)) - throw new ArgumentNullException("query"); - //Extract the connection type (optional) object connectionType = null; @@ -257,62 +302,87 @@ private void ParseRawParameters() ConnectionType = (ConnectionTypes)Enum.Parse(typeof(ConnectionTypes), connectionType.ToString().ToLower()); - //Extract and command type (optional) - object commandType = null; + //Extract the commands array. + dynamic commands = null; - _Raw.TryGetValue("type", out commandType); + _Raw.TryGetValue("commands", out commands); - if (commandType == null) - commandType = "query"; + if (commands == null) + throw new ArgumentException("The commands field is required."); - QueryType = (QueryTypes)Enum.Parse(typeof(QueryTypes), commandType.ToString().ToLower()); - - //Extract the parameters (optional) - object parameters = null; - - _Raw.TryGetValue("params", out parameters); + for (int i = 0; i < commands.Length; i++) + { + dynamic com = commands[i]; - if (parameters == null) - parameters = new object[0]; + if (!IsPropertyExist(com, "query")) + throw new ArgumentException("The query field is required on transaction object."); - Parameters = (object[])parameters; + Command newCom = new Command() + { + query = com.query + }; - //Extract the return parameters (optional) - dynamic returnParameter = null; + if (IsPropertyExist(com, "params")) + newCom.@params = com.@params; + else + newCom.@params = new object[] { }; - _Raw.TryGetValue("returns", out returnParameter); + if (IsPropertyExist(com, "type")) + newCom.type = (QueryTypes)Enum.Parse(typeof(QueryTypes), com.type.ToString().ToLower()); + else + newCom.type = QueryTypes.command; - if (returnParameter != null) - { - ReturnParameter = new ReturnParameter() + if (IsPropertyExist(com, "returns")) { - Name = returnParameter.name - }; + newCom.returns = new ReturnParameter() + { + name = com.returns.name + }; + + try { newCom.returns.precision = (byte)com.returns.precision; } catch { } + try { newCom.returns.scale = (byte)com.returns.scale; } catch { } + try { newCom.returns.size = (byte)com.returns.size; } catch { } + try { newCom.returns.value = com.returns.value; } catch { } + } - try { ReturnParameter.Precision = (byte)returnParameter.precision; } catch { } - try { ReturnParameter.Scale = (byte)returnParameter.scale; } catch { } - try { ReturnParameter.Size = (byte)returnParameter.size; } catch { } - try { ReturnParameter.Value = returnParameter.value; } catch { } + Commands.Add(newCom); } } + + private bool IsPropertyExist(dynamic settings, string name) + { + if (settings is ExpandoObject) + return ((IDictionary)settings).ContainsKey(name); + + return settings.GetType().GetProperty(name) != null; + } } public class ReturnParameter { - public string Name { get; set; } + public string name { get; set; } - public string ParameterName + public string parameterName { get { - string name = this.Name.Replace("@", ""); + string name = this.name.Replace("@", ""); return "@" + name; } } - public byte? Precision { get; set; } - public byte? Scale { get; set; } - public byte? Size { get; set; } - public object Value { get; set; } + public byte? precision { get; set; } + public byte? scale { get; set; } + public byte? size { get; set; } + public object value { get; set; } +} + +public class Command +{ + public string query { get; set; } + public object[] @params { get; set; } + public QueryTypes type { get; set; } + public ReturnParameter returns { get; set; } + public object result { get; set; } } \ No newline at end of file diff --git a/README.md b/README.md index 0714666..7894fcf 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # oledb.js -[![npm version](https://img.shields.io/badge/npm-v1.4.4-blue.svg)](https://www.npmjs.com/package/oledb) +[![npm version](https://img.shields.io/badge/npm-v1.5.0-blue.svg)](https://www.npmjs.com/package/oledb) [![license](https://img.shields.io/badge/license-MIT-orange.svg)](LICENSE) [![tips](https://img.shields.io/badge/tips-bitcoin-brightgreen.svg)](https://www.coinbase.com/blahyourhamster) @@ -50,19 +50,23 @@ const db = oledb.oledbConnection(connectionString); ... ``` +--- + ## Promises -There are 3 available promises that can be used to send commands and queries to a database connection: +There are a number available promises that can be used to send commands and queries to a database connection: - `.query(command, [parameters])` - Executes a query and returns the result set returned by the query as an `Array`. - `.execute(command, [parameters])` - Executes a query command and returns the **number of rows affected**. - `.scalar(command, [parameters])` - Executes a query and returns the first column of the first row in the result set returned by the query. All other columns and rows are ignored. - `.procedure(command, [parameters], [returns])` - Excutes a stored procedure and returns the result, otherwise the return parameter value if defined. +- `.transaction(commands)` - Excutes an array of queries in a single transaction and returns the result of each. Each parameter is described below: - `command` - The string query command to be executed. - `parameters` - An **Array** of parameter values. This is an **optional** parameter. - `returns` - A return value object, see the *Stored Procedure* section below. This is an **optional** parameter. +- `commands` - A parameter used for transactions, see the *Transactions* section below. ## Query Parameters Parameters are also supported and use positional parameters that are marked with a question mark (?). Here is an example: @@ -87,6 +91,27 @@ err => { }); ``` +## Multiple Data Sets +The `.query` promise has support for multiple data sets that can be returned in a single query. Here is an example: + +```js +let command = ` + select * from account; + select * from address; +`; + +db.query(command) +.then(results => { + console.log(results[0]); //1st data set + console.log(results[1]); //2nd data set +}, +err => { + console.error(err); +}); +``` + +--- + ## Stored Procedures Stored procedures can be executed using the `.procedure` function with optional parameters and return value. Here is an example: @@ -137,24 +162,47 @@ err => { }); ``` -## Multiple Data Sets -The `.query` promise has support for multiple data sets that can be returned in a single query. Here is an example: +--- -```js -let command = ` - select * from account; - select * from address; -`; +## Transactions +The `.transaction` promise will execute multiple commands in a **single** transaction, this is useful for if you want to insert records across different tables +and need to ensure that they all are inserted successfully, or not at all. **All** query types are supported, including `procedure`. Here is an example: -db.query(command) +```js +let commands = [ + { + query: 'insert into account (name) values (?)', + params: [ 'Bob' ] + }, + { + query: 'select * from account where name = ?', + type: oledb.COMMAND_TYPES.QUERY, + params: [ 'Bob' ] + } +]; + +db.transaction(commands) .then(results => { - console.log(results[0]); //1st data set - console.log(results[1]); //2nd data set + console.log(results[0].result); //Insert query result. + console.log(results[1][0].result); //Select query result. (First result set) }, err => { - console.error(err); + console.log(err); }); ``` +*Note: The result field will contain an array of results if using a `query` command as multiple query results are supported by each executed query. See Multiple Data Sets above.* + +All commands must follow the following structure: + +```js +{ + query: string, //The query string - Required + params: Array, //The query parameters - Optional + type: string, //The query type, use one of the oledb.COMMAND_TYPES enumerations. - Optional - Defaults to 'command' + returns: Object //The return parameter if applicable (see Stored Procedures). - Optional +} +``` + ## License This project is licensed under [MIT](LICENSE). diff --git a/index.js b/index.js index f5abb60..150e30d 100644 --- a/index.js +++ b/index.js @@ -1,31 +1,53 @@ const edge = require('edge'); const data = edge.func(__dirname + '/Data.cs'); -function executePromise(constring, contype, command, type, params, returns) { - if (command == null || command.length === 0) - return Promise.reject('Command string cannot be null or empty.'); +const COMMAND_TYPES = { + QUERY: 'query', + SCALAR: 'scalar', + COMMAND: 'command', + PROCEDURE: 'procedure' +}; - if (params != null && !Array.isArray(params)) - params = [params]; +const CONNECTION_TYPES = { + OLEDB: 'oledb', + SQL: 'sql', + ODBC: 'odbc' +}; - if (params) { - if (!Array.isArray(params)) - return Promise.reject('Params must be an array type.'); +function executePromise(constring, contype, commands) { + if (!commands) + return Promise.reject('The commands argument is required.'); + if (!Array.isArray(commands)) + return Promise.reject('Commands argument must be an array type.'); + if (commands.length === 0) + return Promise.reject('There must be more than one transaction to execute.'); - for(let i = 0; i < params.length; i++) { - if (Array.isArray(params[i])) - return Promise.reject('Params cannot contain sub-arrays.'); + for(let i = 0; i < commands.length; i++) { + let command = commands[i]; + + if (command.query.length === 0) + return Promise.reject('Command string cannot be null or empty.'); + if (command.params != null && !Array.isArray(command.params)) + command.params = [command.params]; + + if (command.params) { + if (!Array.isArray(command.params)) + return Promise.reject('Params must be an array type.'); + + for(let i = 0; i < command.params.length; i++) { + if (Array.isArray(command.params[i])) + return Promise.reject('Params cannot contain sub-arrays.'); + } } + else + command.params = []; } return new Promise((resolve, reject) => { let options = { constring: constring, connection: contype, - query: command, - type: type, - params: params || [], - returns: returns + commands: commands }; data(options, (err, data) => { @@ -42,37 +64,68 @@ class Connection { if (constring == null || constring.trim() === '') throw new Error('constring must not be null or empty'); if (contype == null || contype.trim() === '') - contype = 'oledb'; + contype = CONNECTION_TYPES.OLEDB; this.connectionString = constring; this.connectionType = contype; } query(command, params) { - return executePromise(this.connectionString, this.connectionType, command, 'query', params); + return executePromise(this.connectionString, this.connectionType, [ + { + query: command, + params: params, + type: COMMAND_TYPES.QUERY + } + ]); } scalar(command, params) { - return executePromise(this.connectionString, this.connectionType, command, 'scalar', params); + return executePromise(this.connectionString, this.connectionType, [ + { + query: command, + params: params, + type: COMMAND_TYPES.SCALAR + } + ]); } execute(command, params) { - return executePromise(this.connectionString, this.connectionType, command, 'command', params); + return executePromise(this.connectionString, this.connectionType, [ + { + query: command, + params: params, + type: COMMAND_TYPES.COMMAND + } + ]); } procedure(command, params, returns) { - return executePromise(this.connectionString, this.connectionType, command, 'procedure', params, returns); + return executePromise(this.connectionString, this.connectionType, [ + { + query: command, + params: params, + type: COMMAND_TYPES.PROCEDURE, + returns: returns + } + ]); + } + + transaction(commands) { + return executePromise(this.connectionString, this.connectionType, commands) } } module.exports = { + COMMAND_TYPES: COMMAND_TYPES, + oledbConnection(connectionString) { - return new Connection(connectionString, 'oledb'); + return new Connection(connectionString, CONNECTION_TYPES.OLEDB); }, odbcConnection(connectionString) { - return new Connection(connectionString, 'odbc'); + return new Connection(connectionString, CONNECTION_TYPES.ODBC); }, sqlConnection(connectionString) { - return new Connection(connectionString, 'sql'); + return new Connection(connectionString, CONNECTION_TYPES.SQL); } }; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 79d7866..2e335ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "oledb", - "version": "1.4.3", + "version": "1.4.4", "lockfileVersion": 1, "dependencies": { "ansi-regex": { @@ -88,6 +88,12 @@ "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", "dev": true }, + "caller-id": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/caller-id/-/caller-id-0.1.0.tgz", + "integrity": "sha1-Wb2sCJPRLDhxQIJ5Ix+XRYNk8Hs=", + "dev": true + }, "caseless": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", @@ -482,6 +488,12 @@ "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true }, + "mock-require": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mock-require/-/mock-require-2.0.2.tgz", + "integrity": "sha1-HqpxqtIwE3c9En3H6Ro/u0g31g0=", + "dev": true + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -1719,6 +1731,12 @@ } } }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", + "dev": true + }, "stack-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.1.tgz", From d9f3df62d339e4d19e754e6884dc9880270c9983 Mon Sep 17 00:00:00 2001 From: Mike Eason Date: Sun, 30 Jul 2017 20:50:48 +0100 Subject: [PATCH 2/2] Massive refactoring and major improvements including full support for transactions --- Data.cs | 299 +++++++++++++++++++++++++++++++++--------------------- README.md | 170 +++++++++++++++++++++++-------- index.js | 15 ++- test.js | 46 +++++++++ 4 files changed, 368 insertions(+), 162 deletions(-) diff --git a/Data.cs b/Data.cs index 42ce820..7a21090 100644 --- a/Data.cs +++ b/Data.cs @@ -10,13 +10,14 @@ using System.Data.SqlClient; using System.Dynamic; using System.Threading.Tasks; +using System.Linq; public class Startup { public async Task Invoke(IDictionary parameters) { - ParameterCollection pcol = new ParameterCollection(parameters); - + JsParameterCollection pcol = new JsParameterCollection(parameters); + using (DbConnection connection = CreateConnection(pcol.ConnectionString, pcol.ConnectionType)) { try @@ -33,13 +34,13 @@ public async Task Invoke(IDictionary parameters) switch (com.type) { - case QueryTypes.query: + case JsQueryTypes.query: return await ExecuteQuery(command, com); - case QueryTypes.scalar: + case JsQueryTypes.scalar: return await ExecuteScalar(command, com); - case QueryTypes.command: + case JsQueryTypes.command: return await ExecuteNonQuery(command, com); - case QueryTypes.procedure: + case JsQueryTypes.procedure: return await ExecuteProcedure(command, com); default: throw new NotSupportedException("Unsupported type of database command. Only 'query', 'scalar', 'command' and 'procedure' are supported."); @@ -58,28 +59,28 @@ public async Task Invoke(IDictionary parameters) } } - private DbConnection CreateConnection(string connectionString, ConnectionTypes type) + private DbConnection CreateConnection(string connectionString, JsConnectionTypes type) { switch (type) { - case ConnectionTypes.oledb: + case JsConnectionTypes.oledb: return new OleDbConnection(connectionString); - case ConnectionTypes.odbc: + case JsConnectionTypes.odbc: return new OdbcConnection(connectionString); - case ConnectionTypes.sql: + case JsConnectionTypes.sql: return new SqlConnection(connectionString); } throw new NotImplementedException(); } - private async Task ExecuteQuery(DbCommand command, Command com) + private async Task ExecuteQuery(DbCommand dbCommand, JsCommand jsCommand, object prev = null) { - command.CommandText = com.query; + dbCommand.CommandText = jsCommand.query; - AddCommandParameters(command, com.@params); + AddCommandParameters(dbCommand, jsCommand, prev); - using (DbDataReader reader = command.ExecuteReader()) + using (DbDataReader reader = dbCommand.ExecuteReader()) { List results = new List(); @@ -89,63 +90,55 @@ private async Task ExecuteQuery(DbCommand command, Command com) } while (await reader.NextResultAsync()); - return results; + jsCommand.result = results; } + + UpdateCommandParameters(dbCommand, jsCommand); + + return jsCommand; } - private async Task ExecuteScalar(DbCommand command, Command com) + private async Task ExecuteScalar(DbCommand dbCommand, JsCommand jsCommand, object prev = null) { - command.CommandText = com.query; + dbCommand.CommandText = jsCommand.query; - AddCommandParameters(command, com.@params); + AddCommandParameters(dbCommand, jsCommand, prev); - return await command.ExecuteScalarAsync(); + jsCommand.result = await dbCommand.ExecuteScalarAsync(); + + UpdateCommandParameters(dbCommand, jsCommand); + + return jsCommand; } - private async Task ExecuteNonQuery(DbCommand command, Command com) + private async Task ExecuteNonQuery(DbCommand dbCommand, JsCommand jsCommand, object prev = null) { - command.CommandText = com.query; + dbCommand.CommandText = jsCommand.query; - AddCommandParameters(command, com.@params); + AddCommandParameters(dbCommand, jsCommand, prev); - return await command.ExecuteNonQueryAsync(); + jsCommand.result = await dbCommand.ExecuteNonQueryAsync(); + + UpdateCommandParameters(dbCommand, jsCommand); + + return jsCommand; } - private async Task ExecuteProcedure(DbCommand command, Command com) + private async Task ExecuteProcedure(DbCommand dbCommand, JsCommand jsCommand, object prev = null) { - bool hasReturnParameter = com.returns != null; - - command.CommandText = com.query; - command.CommandType = CommandType.StoredProcedure; + dbCommand.CommandText = jsCommand.query; + dbCommand.CommandType = CommandType.StoredProcedure; - AddCommandParameters(command, com.@params); + AddCommandParameters(dbCommand, jsCommand, prev); - if (hasReturnParameter) - { - DbParameter returnParam = command.CreateParameter(); - returnParam.ParameterName = com.returns.parameterName; - returnParam.Direction = ParameterDirection.ReturnValue; - returnParam.Value = com.returns.value; - - if (com.returns.precision != null) - returnParam.Precision = (byte)com.returns.precision; - if (com.returns.scale != null) - returnParam.Scale = (byte)com.returns.scale; - if (com.returns.size != null) - returnParam.Size = (byte)com.returns.size; - - command.Parameters.Add(returnParam); - } + jsCommand.result = await dbCommand.ExecuteNonQueryAsync(); + + UpdateCommandParameters(dbCommand, jsCommand); - object result = await command.ExecuteScalarAsync(); - - if (hasReturnParameter) - return command.Parameters[com.returns.parameterName].Value; - else - return result; + return jsCommand; } - private async Task ExecuteTransaction(DbConnection connection, DbCommand command, List commands) + private async Task ExecuteTransaction(DbConnection connection, DbCommand dbCommand, List jsCommands) { DbTransaction transaction = null; @@ -153,27 +146,31 @@ private async Task ExecuteTransaction(DbConnection connection, DbCommand { transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted); - command.Transaction = transaction; + dbCommand.Transaction = transaction; + + object prevResult = null; - foreach (Command com in commands) + foreach (JsCommand jsCommand in jsCommands) { - switch (com.type) + switch (jsCommand.type) { - case QueryTypes.command: - com.result = await ExecuteNonQuery(command, com); + case JsQueryTypes.command: + await ExecuteNonQuery(dbCommand, jsCommand, prevResult); break; - case QueryTypes.query: - com.result = await ExecuteQuery(command, com); + case JsQueryTypes.query: + await ExecuteQuery(dbCommand, jsCommand, prevResult); break; - case QueryTypes.scalar: - com.result = await ExecuteScalar(command, com); + case JsQueryTypes.scalar: + await ExecuteScalar(dbCommand, jsCommand, prevResult); break; - case QueryTypes.procedure: - com.result = await ExecuteProcedure(command, com); + case JsQueryTypes.procedure: + await ExecuteProcedure(dbCommand, jsCommand, prevResult); break; default: throw new NotSupportedException("Unsupported type of database command. Only 'query', 'scalar', 'command' and 'procedure' are supported."); } + + prevResult = jsCommand.result; } transaction.Commit(); @@ -192,7 +189,7 @@ private async Task ExecuteTransaction(DbConnection connection, DbCommand throw; } - return commands; + return jsCommands; } private async Task> ParseReaderRow(DbDataReader reader) @@ -228,28 +225,58 @@ private async Task> ParseReaderRow(DbDataReader reader) return rows; } - private void AddCommandParameters(DbCommand command, object[] parameters) + private void AddCommandParameters(DbCommand dbCommand, JsCommand jsCommand, object prev = null) { - command.Parameters.Clear(); + dbCommand.Parameters.Clear(); - for (int i = 0; i < parameters.Length; i++) + for (int i = 0; i < jsCommand.@params.Count; i++) { - string name = string.Format("@p{0}", i + 1); + JsCommandParameter cp = jsCommand.@params[i]; + + DbParameter param = dbCommand.CreateParameter(); + param.ParameterName = cp.name; - DbParameter param = command.CreateParameter(); - param.ParameterName = name; + object paramVal = cp.value; - if (parameters[i] == null) + //Check if the parameter is a special $prev parameter. + //If so, then use the prev argument. + if (paramVal != null && paramVal.ToString().ToLower() == "$prev") + paramVal = prev; + + if (paramVal == null) param.Value = DBNull.Value; else - param.Value = parameters[i]; + param.Value = paramVal; + + param.Direction = (ParameterDirection)cp.direction; + param.IsNullable = cp.isNullable; - command.Parameters.Add(param); + if (cp.precision != null) + param.Precision = (byte)cp.precision; + if (cp.scale != null) + param.Scale = (byte)cp.scale; + if (cp.size != null) + param.Size = (byte)cp.size; + + dbCommand.Parameters.Add(param); + } + } + + private void UpdateCommandParameters(DbCommand dbCommand, JsCommand jsCommand) + { + foreach(DbParameter param in dbCommand.Parameters) + { + JsCommandParameter jparam = jsCommand.@params.FirstOrDefault(x => x.name == param.ParameterName); + + if (jparam == null) + continue; + + jparam.value = param.Value; } } } -public enum QueryTypes +public enum JsQueryTypes { query, scalar, @@ -257,28 +284,28 @@ public enum QueryTypes procedure } -public enum ConnectionTypes +public enum JsConnectionTypes { oledb, odbc, sql } -public class ParameterCollection +public class JsParameterCollection { private IDictionary _Raw; public string ConnectionString { get; private set; } - public ConnectionTypes ConnectionType { get; private set; } + public JsConnectionTypes ConnectionType { get; private set; } - public List Commands { get; private set; } + public List Commands { get; private set; } - public ParameterCollection(IDictionary parameters) + public JsParameterCollection(IDictionary parameters) { if (parameters == null) throw new ArgumentNullException("parameters"); - Commands = new List(); + Commands = new List(); _Raw = parameters; ParseRawParameters(); @@ -300,7 +327,7 @@ private void ParseRawParameters() if (connectionType == null) connectionType = "oledb"; - ConnectionType = (ConnectionTypes)Enum.Parse(typeof(ConnectionTypes), connectionType.ToString().ToLower()); + ConnectionType = (JsConnectionTypes)Enum.Parse(typeof(JsConnectionTypes), connectionType.ToString().ToLower()); //Extract the commands array. dynamic commands = null; @@ -317,33 +344,20 @@ private void ParseRawParameters() if (!IsPropertyExist(com, "query")) throw new ArgumentException("The query field is required on transaction object."); - Command newCom = new Command() + JsCommand newCom = new JsCommand() { query = com.query }; if (IsPropertyExist(com, "params")) - newCom.@params = com.@params; + newCom.rawParameters = com.@params; else - newCom.@params = new object[] { }; + newCom.rawParameters = new object[] { }; if (IsPropertyExist(com, "type")) - newCom.type = (QueryTypes)Enum.Parse(typeof(QueryTypes), com.type.ToString().ToLower()); + newCom.type = (JsQueryTypes)Enum.Parse(typeof(JsQueryTypes), com.type.ToString().ToLower()); else - newCom.type = QueryTypes.command; - - if (IsPropertyExist(com, "returns")) - { - newCom.returns = new ReturnParameter() - { - name = com.returns.name - }; - - try { newCom.returns.precision = (byte)com.returns.precision; } catch { } - try { newCom.returns.scale = (byte)com.returns.scale; } catch { } - try { newCom.returns.size = (byte)com.returns.size; } catch { } - try { newCom.returns.value = com.returns.value; } catch { } - } + newCom.type = JsQueryTypes.command; Commands.Add(newCom); } @@ -358,31 +372,86 @@ private bool IsPropertyExist(dynamic settings, string name) } } -public class ReturnParameter +public class JsCommand { - public string name { get; set; } + public string query { get; set; } + public JsQueryTypes type { get; set; } + public List @params { get; set; } + public object result { get; set; } + + private object[] _rawParameters; - public string parameterName + internal object[] rawParameters { - get + get { - string name = this.name.Replace("@", ""); + return _rawParameters; + } + set + { + _rawParameters = value; + + @params = new List(); + + //Go through each command parameter and build up the command parameter + //array. Work out wether to use named parameters (@myParam, @myOtherParam) + //or index named parameters (@p1, @p2 ect). + for(int i = 0; i < _rawParameters.Length; i++) + { + dynamic p = _rawParameters[i]; + JsCommandParameter cp = new JsCommandParameter(); + + //Check if it is an expando object + //if it is then extract the name and value from it. + if (p is ExpandoObject) + { + if (IsPropertyExist(p, "name")) + cp.name = p.name.ToString(); + else + cp.name = "@p" + (i + 1).ToString(); + + if (IsPropertyExist(p, "value")) + cp.value = p.value; + else + cp.value = null; - return "@" + name; + if (!cp.name.StartsWith("@")) + cp.name = "@" + cp.name; + + try { cp.direction = (int)p.direction; } catch { cp.direction = (int)ParameterDirection.Input; } + try { cp.isNullable = (bool)p.isNullable; } catch { cp.isNullable = true; } + try { cp.precision = (byte)p.precision; } catch { } + try { cp.scale = (byte)p.scale; } catch { } + try { cp.size = (byte)p.size; } catch { } + } + else + { + cp.name = "@p" + (i + 1).ToString(); + cp.value = p; + } + + @params.Add(cp); + } } } - public byte? precision { get; set; } - public byte? scale { get; set; } - public byte? size { get; set; } - public object value { get; set; } + private bool IsPropertyExist(dynamic obj, string name) + { + if (obj is ExpandoObject) + return ((IDictionary)obj).ContainsKey(name); + + return obj.GetType().GetProperty(name) != null; + } } -public class Command +public class JsCommandParameter { - public string query { get; set; } - public object[] @params { get; set; } - public QueryTypes type { get; set; } - public ReturnParameter returns { get; set; } - public object result { get; set; } + public string name { get; set; } + public object value { get; set; } + + public int direction { get; set; } + public bool isNullable { get; set; } + public byte? precision { get; set; } + public byte? scale { get; set; } + public byte? size { get; set; } } \ No newline at end of file diff --git a/README.md b/README.md index 7894fcf..544d10e 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ A small **promise based** module which uses [Edge](https://github.com/tjanczuk/e ## Example ```js -const connectionString = 'provider=vfpoledb;data source=C:/MyDatabase.dbc'; +const connectionString = '...'; const oledb = require('oledb'); const db = oledb.oledbConnection(connectionString); @@ -17,14 +17,41 @@ const db = oledb.oledbConnection(connectionString); let command = 'select * from account;'; db.query(command) -.then(results => { - console.log(results[0]); +.then(result => { + console.log(result); }, err => { console.error(err); }); ``` +The result will look like this: +```js +{ + query: 'select count(*) from account where name = @p1', + type: 'query', + params:[ + { + name: '@p1', + value: 'Mike', + direction: 0, + isNullable: false, + precision: null, + scale: null, + size: null + } + ], + 'result': [ + [ + { + id: 1, + name: 'Bob' + } + ] + ] +} +``` + ## Installation ``` npm install oledb --save @@ -39,37 +66,23 @@ The module exposes three functions to initialize database connections: - `oledb.odbcConnection(connectionString)` - Initializes a connection to an **ODBC** database. - `oledb.sqlConnection(connectionString)` - Initializes a connection to an **SQL** database. -Here is an example: - -```js -const connectionString = 'Server=myServerAddress;Database=myDataBase;Trusted_Connection=True;'; - -const oledb = require('oledb'); -const db = oledb.oledbConnection(connectionString); - -... -``` - ---- - ## Promises There are a number available promises that can be used to send commands and queries to a database connection: -- `.query(command, [parameters])` - Executes a query and returns the result set returned by the query as an `Array`. -- `.execute(command, [parameters])` - Executes a query command and returns the **number of rows affected**. -- `.scalar(command, [parameters])` - Executes a query and returns the first column of the first row in the result set returned by the query. All other columns and rows are ignored. -- `.procedure(command, [parameters], [returns])` - Excutes a stored procedure and returns the result, otherwise the return parameter value if defined. -- `.transaction(commands)` - Excutes an array of queries in a single transaction and returns the result of each. +- `.query(command, [parameters])` - Executes a query and returns an is the result set returned by the query as an `Array`. +- `.execute(command, [parameters])` - Executes a query command and returns an is the the **number of rows affected**. +- `.scalar(command, [parameters])` - Executes a query and returns an is the first column of the first row in the result set returned by the query. All other columns and rows are ignored. +- `.procedure(command, [parameters])` - Excutes a stored procedure and returns the result. +- `.transaction(commands)` - Excutes an array of commands in a single transaction and returns the result of each. Each parameter is described below: - `command` - The string query command to be executed. - `parameters` - An **Array** of parameter values. This is an **optional** parameter. -- `returns` - A return value object, see the *Stored Procedure* section below. This is an **optional** parameter. - `commands` - A parameter used for transactions, see the *Transactions* section below. ## Query Parameters -Parameters are also supported and use positional parameters that are marked with a question mark (?). Here is an example: +Parameters are also supported and use positional parameters that are marked with a question mark (?) **OR** named parameters, i.e `@parameter1`. Here is an example: ```js let command = ` @@ -91,6 +104,27 @@ err => { }); ``` +### Query Parameter Options +There are a number of additional options for query parameters, a query parameter can either be a **single value** or an object: + +```js +let parameters = [ + 'Bob', //Declare a single parameter value. Defaults to: { name: '@p1', value: 'Bob' } + //Or use an object to specify additional options... + { + name: 'myParameter', //OPTIONAL - Parameter name. Defaults to index based parameter names, i.e @p1, @p2, @p3 ect. Note that the @ symbols are optional. + value: 123, //OPTIONAL - Defaults to null. + direction: string, //OPTIONAL - The parameter direction, (Input, Input/Output, Output, Return Value). See oledb.PARAMETER_DIRECTIONS enum. + isNullable: bool, //OPTIONAL - Whether to treat the paramter as non-nullable. + precision: byte, //OPTIONAL - The precision of the parameter value in bytes. + scale: byte, //OPTIONAL - The scale of the parameter value in bytes. + size: byte //OPTIONAL - The size of the parameter value in bytes. + } +]; +``` + +--- + ## Multiple Data Sets The `.query` promise has support for multiple data sets that can be returned in a single query. Here is an example: @@ -102,8 +136,8 @@ let command = ` db.query(command) .then(results => { - console.log(results[0]); //1st data set - console.log(results[1]); //2nd data set + console.log(results[0]); //1st query result + console.log(results[1]); //2nd query result }, err => { console.error(err); @@ -130,32 +164,41 @@ err => { ``` ### Stored Procedure Return Values -You can use a return value parameter with the `.procedure` function. The parameter must look like this: +You can use a return value or output parameter with the `.procedure` function. The parameter might look like this: ```js { - name: 'myParameter', //String - Required - value: 'hello world', //Object - Optional - precision: 0, //Byte - Optional - scale: 1, //Byte - Optional - size: 2 //Byte - Optional + name: 'sum', + direction: oledb.PARAMETER_DIRECTIONS.OUTPUT } ``` +*for more options, see **Query Parameter Options** section.* + Here is an example: ```js let procedureName = `addNumbers`; -let parameters = [1, 2]; -let returns = { - name: 'result', - value: 0 -}; +let parameters = [ + { + name: 'num1', + value: 1 + }, + { + name: 'num2', + value: 2 + }, + { + name: 'sum', + direction: oledb.PARAMETER_DIRECTIONS.OUTPUT + } +]; -db.procedure(procedureName, parameters, returns) +db.procedure(procedureName, parameters) .then(result => { console.log(result); + console.log(result.params[2].value); //The output value returned by addNumbers stored procedure. }, err => { console.error(err); @@ -183,8 +226,7 @@ let commands = [ db.transaction(commands) .then(results => { - console.log(results[0].result); //Insert query result. - console.log(results[1][0].result); //Select query result. (First result set) + console.log(results); //An array of query results. }, err => { console.log(err); @@ -197,12 +239,54 @@ All commands must follow the following structure: ```js { - query: string, //The query string - Required - params: Array, //The query parameters - Optional - type: string, //The query type, use one of the oledb.COMMAND_TYPES enumerations. - Optional - Defaults to 'command' - returns: Object //The return parameter if applicable (see Stored Procedures). - Optional + query: string, //REQUIRED - The query string + params: Array, //OPTIONAL - The query parameters + type: string //OPTIONAL - The query type, use one of the oledb.COMMAND_TYPES enumerations. Defaults to 'command'. } ``` +### $prev Parameter +With **transactions**, you can use the special `'$prev'` parameter to inject the **previous** command's result into the **next** executing query. For example: + +```js +let commands = [ + //First query, executes a stored procedure and returns an account id. + { + query: 'insert_account (@name)`, + params: [ + { + name: '@name', + value: 'Bob' + }, + { + name: '@accountId', + direction: oledb.PARAMETER_DIRECTIONS.RETURN_VALUE + } + ], + type: oledb.COMMAND_TYPES.PROCEDURE + }, + //Second query, executes a select query with the returned value from the previous query. + { + query: 'select * from account where id = @accountId', + type: oledb.COMMAND_TYPES.QUERY, + params: [ + { + name: '@accountId', + value: '$prev' //Note: This value must be a string. + } + ] + } +]; + +db.transaction(commands) +.then(results => { + console.log(results[0]); //Insert stored procedure result. Returns the ID of the account. + console.log(results[1]); //Select query result. Returns the account 'Bob' record. +}, +err => { + console.log(err); +}); +``` + ## License This project is licensed under [MIT](LICENSE). diff --git a/index.js b/index.js index 150e30d..b6225e6 100644 --- a/index.js +++ b/index.js @@ -8,6 +8,13 @@ const COMMAND_TYPES = { PROCEDURE: 'procedure' }; +const PARAMETER_DIRECTIONS = { + INPUT: 1, + OUTPUT: 2, + INPUT_OUTPUT: 3, + RETURN_VALUE: 6 +}; + const CONNECTION_TYPES = { OLEDB: 'oledb', SQL: 'sql', @@ -25,7 +32,7 @@ function executePromise(constring, contype, commands) { for(let i = 0; i < commands.length; i++) { let command = commands[i]; - if (command.query.length === 0) + if (command.query == null || command.query.length === 0) return Promise.reject('Command string cannot be null or empty.'); if (command.params != null && !Array.isArray(command.params)) command.params = [command.params]; @@ -100,13 +107,12 @@ class Connection { ]); } - procedure(command, params, returns) { + procedure(command, params) { return executePromise(this.connectionString, this.connectionType, [ { query: command, params: params, - type: COMMAND_TYPES.PROCEDURE, - returns: returns + type: COMMAND_TYPES.PROCEDURE } ]); } @@ -118,6 +124,7 @@ class Connection { module.exports = { COMMAND_TYPES: COMMAND_TYPES, + PARAMETER_DIRECTIONS: PARAMETER_DIRECTIONS, oledbConnection(connectionString) { return new Connection(connectionString, CONNECTION_TYPES.OLEDB); diff --git a/test.js b/test.js index 8578a60..45f6a39 100644 --- a/test.js +++ b/test.js @@ -11,6 +11,8 @@ mock('edge', { return (options, callback) => { if (options.constring != connectionString) return callback('Connection strings do not match.'); + if (options.commands == null || options.commands.length === 0) + return callback('Commands parameter must be an array of at least one command.'); return callback(null, []); }; @@ -543,3 +545,47 @@ tap('fails if params contains sub-arrays.', (t) => { t.end(); }); }); + +tap('fails if transaction commands is an empty array', (t) => { + let db = oledb.oledbConnection(connectionString); + let commands = []; + + db.transaction(commands) + .then(result => { + t.fail('should have not been a successful command.'); + }) + .catch(err => { + t.end(); + }); +}); + +tap('fails if transaction commands is null', (t) => { + let db = oledb.oledbConnection(connectionString); + let commands = null; + + db.transaction(commands) + .then(result => { + t.fail('should have not been a successful command.'); + }) + .catch(err => { + t.end(); + }); +}); + +tap('succeeds if transaction commands has at least one valid command', (t) => { + let db = oledb.oledbConnection(connectionString); + let commands = [ + { + query: 'select * from test', + params: [] + } + ]; + + db.transaction(commands) + .then(result => { + t.end(); + }) + .catch(err => { + t.fail(err); + }); +}); \ No newline at end of file