Skip to content

Commit

Permalink
Added template support( if/when )
Browse files Browse the repository at this point in the history
  • Loading branch information
digitalBush committed Mar 7, 2019
1 parent dabe6b8 commit 52cba61
Show file tree
Hide file tree
Showing 6 changed files with 348 additions and 27 deletions.
63 changes: 58 additions & 5 deletions spec/behavior/api.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,25 @@ describe( "api", () => {
beforeEach( async () => {
const Api = proxyquire( "src/api", {} );
api = new Api();
api.withConnection = sinon.stub();
bulk = {};
conn = {
execSql: sinon.spy( request => {
request.callback();
} ),
newBulkLoad: sinon.stub().returns( bulk ),
execBulkLoad: sinon.stub().callsFake( () => {
bulk.callback( null, 0 );
} )
};
api.withConnection = sinon.spy( async function( action ) {
const result = await action( conn );
return result;
} );
} );

describe( "when creating a new bulk load without tedious options", () => {
beforeEach( async () => {
api.bulkLoad( "the table", { schema: {}, rows: [] } );
await api.withConnection.getCall( 0 ).args[ 0 ]( conn );
await api.bulkLoad( "the table", { schema: {}, rows: [] } );
} );

it( "should pass the default tedious options to newBulkLoad", () => {
Expand All @@ -31,14 +36,62 @@ describe( "api", () => {

describe( "when creating a new bulk load with tedious options", () => {
beforeEach( async () => {
api.bulkLoad( "the table", { schema: {}, rows: [], checkConstraints: true, fireTriggers: true, keepNulls: true, tableLock: true } );
await api.withConnection.getCall( 0 ).args[ 0 ]( conn );
await api.bulkLoad( "the table", { schema: {}, rows: [], checkConstraints: true, fireTriggers: true, keepNulls: true, tableLock: true } );
} );

it( "should pass the tedious options to newBulkLoad", () => {
conn.newBulkLoad.should.be.calledOnce()
.and.calledWithExactly( "the table", { checkConstraints: true, fireTriggers: true, keepNulls: true, tableLock: true }, sinon.match.func );
} );
} );

describe( "when sql is a function", () => {
let sql;
beforeEach( () => {
sql = sinon.stub().resolves( "SELECT 1" );
} );
[
"execute",
"querySets",
"query",
"queryFirst",
"queryValue"
].forEach( method => {
it( `${ method }: should invoke function with object built from parameter values`, async () => {
await api[ method ]( sql, {
lol: {
type: "bigint",
val: "funny"
}
} );
sql.should.be.calledOnce()
.and.calledWithExactly( { lol: "funny" } );

conn.execSql.should.be.calledOnce()
.and.calledWithMatch( {
sqlTextOrProcedure: "SELECT 1"
} );
} );
} );

it( "queryStream: should invoke function with object built from parameter values", done => {
api.queryStream( sql, {
lol: {
type: "bigint",
val: "funny"
}
} );
setTimeout( () => {
sql.should.be.calledOnce()
.and.calledWithExactly( { lol: "funny" } );

conn.execSql.should.be.calledOnce()
.and.calledWithMatch( {
sqlTextOrProcedure: "SELECT 1"
} );
done();
}, 1 );
} );
} );
} );
} );
2 changes: 1 addition & 1 deletion spec/behavior/fileLoader.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ describe( "fileLoader", () => {

const loader = proxyquire( "src/fileLoader", {
fs
} );
} )( x => x );

await loader( "A" );
await loader( "A" );
Expand Down
179 changes: 179 additions & 0 deletions spec/behavior/templateBuilder.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
const { expect } = testHelpers;

const templateBuilder = require( "src/templateBuilder" );

describe( "templateBuilder", () => {
describe( "if", () => {
let template;
before( async () => {
template = await templateBuilder( `
SELECT *
FROM lol
WHERE 1=1
-- if @foo
AND 2=2
-- endif
-- if @bar
-- hi!
-- if @ham
AND 3=3
-- endif
-- endif
AND 5=5` );
} );

function runTest( data, expected ) {
const output = template( data );
const actual = output.split( "\n" ).map( x => x.trim() ).join( "\n" );
expected = expected.split( "\n" ).map( x => x.trim() ).join( "\n" );
actual.should.equal( expected );
}

const truthy = [
true,
1,
1.1,
new Date(),
"yay",
[ ]
];
truthy.forEach( hasValue => {
it( `should include fragment if expression evaluates ${ JSON.stringify( hasValue ) }`, () => {
runTest( { foo: hasValue }, `
SELECT *
FROM lol
WHERE 1=1
AND 2=2
AND 5=5` );
} );
} );

const falsey = [
false,
0,
null,
undefined
];
falsey.forEach( noValue => {
it( `should NOT include fragment if expression evaluates ${ JSON.stringify( noValue ) }`, () => {
runTest( { foo: noValue }, `
SELECT *
FROM lol
WHERE 1=1
AND 5=5` );
} );
} );

it( "should include fragment if expression evaluates true, but exclude unmatched inner fragments", () => {
runTest( { bar: true }, `
SELECT *
FROM lol
WHERE 1=1
-- hi!
AND 5=5` );
} );

it( "should include inner fragment if both inner and outer expressions are true", () => {
runTest( { bar: true, ham: true }, `
SELECT *
FROM lol
WHERE 1=1
-- hi!
AND 3=3
AND 5=5` );
} );

it( "should exclude inner fragment if outer expression is false and inner expression is true", () => {
runTest( { bar: false, ham: true }, `
SELECT *
FROM lol
WHERE 1=1
AND 5=5` );
} );
} );

describe( "when", () => {
let template;
before( async () => {
template = await templateBuilder( `
SELECT *
FROM lol
WHERE 1=1
AND 2=2 -- when @foo
-- if @bar
-- hi!
AND 3=3 -- when @ham
-- endif
AND 5=5` );
} );

function runTest( data, expected ) {
const output = template( data );
const actual = output.split( "\n" ).map( x => x.trim() ).join( "\n" );
expected = expected.split( "\n" ).map( x => x.trim() ).join( "\n" );
actual.should.equal( expected );
}

const truthy = [
true,
1,
1.1,
new Date(),
"yay",
[ ]
];
truthy.forEach( hasValue => {
it( `should include fragment if expression evaluates ${ JSON.stringify( hasValue ) }`, () => {
runTest( { foo: hasValue }, `
SELECT *
FROM lol
WHERE 1=1
AND 2=2
AND 5=5` );
} );
} );

const falsey = [
false,
0,
null,
undefined
];
falsey.forEach( noValue => {
it( `should NOT include fragment if expression evaluates ${ JSON.stringify( noValue ) }`, () => {
runTest( { foo: noValue }, `
SELECT *
FROM lol
WHERE 1=1
AND 5=5` );
} );
} );

it( "should include inner fragment if both inner and outer expressions are true", () => {
runTest( { bar: true, ham: true }, `
SELECT *
FROM lol
WHERE 1=1
-- hi!
AND 3=3
AND 5=5` );
} );

it( "should exclude inner fragment if outer expression is false and inner expression is true", () => {
runTest( { bar: false, ham: true }, `
SELECT *
FROM lol
WHERE 1=1
AND 5=5` );
} );
} );

describe( "bad templates", () => {
it( "should error when template has more ifs than endifs", () => {
expect( () => templateBuilder( "--if" ) ).to.throw( "if missing closing endif" );
} );
it( "should error when template has more endifs than ifs", () => {
expect( () => templateBuilder( "--endif" ) ).to.throw( "endif without matching if" );
} );
} );
} );
31 changes: 23 additions & 8 deletions src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const { Request, ISOLATION_LEVEL } = require( "tedious" );
const { addRequestParams, addBulkLoadParam } = require( "./parameterBuilder" );
const types = require( "./types" );
const fileLoader = require( "./fileLoader" );
const templateBuilder = require( "./templateBuilder" );
const transformRow = require( "./transformRow" );
const RequestStream = require( "./RequestStream" );

Expand Down Expand Up @@ -38,11 +39,24 @@ async function _query( conn, sql, params ) {
} );
}

async function buildSql( sql, params ) {
let _sql = await sql;
if ( typeof( _sql ) === "function" ) {
const obj = {};
/* eslint-disable guard-for-in */
for ( const prop in params ) {
Object.assign( obj, { [ prop ]: params[ prop ].val } );
}
_sql = _sql( obj );
}
return _sql;
}

class Api {

async execute( sql, params ) {
const callStack = new Error().stack;
const _sql = await sql;
const _sql = await buildSql( sql, params );
return this.withConnection( conn => {
return new Promise( ( resolve, reject ) => {
const request = new Request( _sql, ( err, rowCount ) => {
Expand All @@ -59,7 +73,7 @@ class Api {

async executeBatch( sql ) {
const callStack = new Error().stack;
const _sql = await sql;
const _sql = await buildSql( sql, {} );
return this.withConnection( conn => {
return new Promise( ( resolve, reject ) => {
const request = new Request( _sql, ( err, rowCount ) => {
Expand All @@ -76,7 +90,7 @@ class Api {

async querySets( sql, params ) {
const callStack = new Error().stack;
const _sql = await sql;
const _sql = await buildSql( sql, params );
return this.withConnection( async conn => {
try {
const sets = await _query( conn, _sql, params );
Expand All @@ -89,7 +103,7 @@ class Api {

async query( sql, params ) {
const callStack = new Error().stack;
const _sql = await sql;
const _sql = await buildSql( sql, params );
return this.withConnection( async conn => {
let sets;
try {
Expand All @@ -106,7 +120,7 @@ class Api {

async queryFirst( sql, params ) {
const callStack = new Error().stack;
const _sql = await sql;
const _sql = await buildSql( sql, params );
return this.withConnection( async conn => {
let sets;
try {
Expand All @@ -128,7 +142,7 @@ class Api {
// TODO: Would we rather listen for the 'columnMetadata' event also and take better control of this?
async queryValue( sql, params ) {
const callStack = new Error().stack;
const _sql = await sql;
const _sql = await buildSql( sql, params );
return this.withConnection( async conn => {
let sets;
try {
Expand All @@ -155,7 +169,7 @@ class Api {
queryStream( sql, params ) {
const stream = new RequestStream( );
this.withConnection( async conn => {
const _sql = await sql;
const _sql = await buildSql( sql, params );
stream.request.sqlTextOrProcedure = _sql;

addRequestParams( stream.request, params );
Expand Down Expand Up @@ -218,6 +232,7 @@ Object.keys( ISOLATION_LEVEL ).forEach( k => {
Api.prototype[ k.toLowerCase() ] = ISOLATION_LEVEL[ k ];
} );

Api.prototype.fromFile = fileLoader;
Api.prototype.fromFile = fileLoader( x => x );
Api.prototype.fromTemplate = fileLoader( templateBuilder );

module.exports = Api;
Loading

0 comments on commit 52cba61

Please sign in to comment.