diff --git a/lib/mssql.js b/lib/mssql.js index 947bf90..57ae89c 100644 --- a/lib/mssql.js +++ b/lib/mssql.js @@ -3,6 +3,7 @@ import JSONStream from "JSONStream"; import {json} from "micro"; import mssql from "mssql"; import {failedCheck, badRequest, notImplemented} from "./errors.js"; +import {Transform} from "stream"; const TYPES = mssql.TYPES; @@ -64,12 +65,16 @@ export async function queryStream(req, res, pool) { try { await new Promise((resolve, reject) => { const request = new mssql.Request(db); + // We are using the arrayRowMode to handle potential duplicated column name. + // See: https://github.com/tediousjs/node-mssql#handling-duplicate-column-names + request.arrayRowMode = true; const stream = request.toReadableStream(); params.forEach((param, idx) => { request.input(`${idx + 1}`, param); }); + const columnNameMap = new Map(); request.query(sql); request.once("recordset", () => clearInterval(keepAlive)); request.on("recordset", (columns) => { @@ -77,15 +82,13 @@ export async function queryStream(req, res, pool) { type: "array", items: { type: "object", - properties: Object.entries(columns).reduce( - (schema, [name, props]) => { - return { - ...schema, - ...{[name]: dataTypeSchema({type: props.type.name})}, - }; - }, - {} - ), + properties: columns.reduce((schema, col, idx) => { + columnNameMap.set(idx, col.name); + return { + ...schema, + ...{[col.name]: dataTypeSchema({type: col.type.name})}, + }; + }, {}), }, }; @@ -93,7 +96,22 @@ export async function queryStream(req, res, pool) { res.write("\n"); }); - stream.pipe(JSONStream.stringify("", "\n", "\n")).pipe(res); + stream + .pipe( + new Transform({ + objectMode: true, + transform(chunk, encoding, cb) { + const row = chunk.reduce((acc, r, idx) => { + const key = columnNameMap.get(idx); + return {...acc, [key]: r}; + }, {}); + + cb(null, row); + }, + }) + ) + .pipe(JSONStream.stringify("", "\n", "\n")) + .pipe(res); stream.on("done", () => { resolve(); }); diff --git a/test/mssql.test.js b/test/mssql.test.js index 7020dc1..3b12f4c 100644 --- a/test/mssql.test.js +++ b/test/mssql.test.js @@ -119,6 +119,67 @@ describe("mssql", () => { ); expect(row).to.equal(JSON.stringify({CustomerID: testCustomerId})); + resolve(); + } + }); + }); + it("should handle duplicated column names", () => { + return new Promise(async (resolve, reject) => { + const req = new MockReq({method: "POST", url: "/query-stream"}).end({ + sql: "SELECT 1 as _a1, 2 as _a1 FROM test.SalesLT.SalesOrderDetail", + params: [], + }); + + const res = new MockRes(onEnd); + + const index = mssql(credentials); + await index(req, res); + + function onEnd() { + const [schema, row] = this._getString().split("\n"); + + expect(row).to.equal( + JSON.stringify({ + _a1: 2, + }) + ); + + resolve(); + } + }); + }); + it("should select the last value of any detected duplicated columns", () => { + return new Promise(async (resolve, reject) => { + const req = new MockReq({method: "POST", url: "/query-stream"}).end({ + sql: "SELECT TOP 1 ModifiedDate, ModifiedDate FROM test.SalesLT.SalesOrderDetail", + params: [], + }); + + const res = new MockRes(onEnd); + + const index = mssql(credentials); + await index(req, res); + + function onEnd() { + const [schema, row] = this._getString().split("\n"); + + expect(schema).to.equal( + JSON.stringify({ + type: "array", + items: { + type: "object", + properties: { + ModifiedDate: {type: ["null", "string"], date: true}, + }, + }, + }) + ); + expect(row).to.equal( + JSON.stringify({ + ModifiedDate: "2008-06-01T00:00:00.000Z", + }) + ); + resolve(); } });