diff --git a/__tests__/index.test.ts b/__tests__/index.test.ts index eba7e7b..9300d7b 100644 --- a/__tests__/index.test.ts +++ b/__tests__/index.test.ts @@ -121,18 +121,21 @@ describe('execute', () => { const mockResponse = { session: mockSession, result: { - fields: [{ name: ':vtg1', type: 'INT32' }], - rows: [{ lengths: ['1'], values: 'MQ==' }] + fields: [{ name: ':vtg1', type: 'INT32' }, { name: 'null' }], + rows: [{ lengths: ['1', '-1'], values: 'MQ==' }] } } const want: ExecutedQuery = { - headers: [':vtg1'], - types: { ':vtg1': 'INT32' }, - fields: [{ name: ':vtg1', type: 'INT32' }], - rows: [{ ':vtg1': 1 }], + headers: [':vtg1', 'null'], + types: { ':vtg1': 'INT32', null: 'NULL' }, + fields: [ + { name: ':vtg1', type: 'INT32' }, + { name: 'null', type: 'NULL' } + ], + rows: [{ ':vtg1': 1, null: null }], size: 1, - statement: 'SELECT 1 from dual;', + statement: 'SELECT 1, null from dual;', time: 1, rowsAffected: null, insertId: null @@ -146,7 +149,54 @@ describe('execute', () => { }) const connection = connect(config) - const got = await connection.execute('SELECT 1 from dual;') + const got = await connection.execute('SELECT 1, null from dual;') + got.time = 1 + + expect(got).toEqual(want) + + mockPool.intercept({ path: EXECUTE_PATH, method: 'POST' }).reply(200, (opts) => { + expect(opts.headers['authorization']).toMatch(/Basic /) + const bodyObj = JSON.parse(opts.body.toString()) + expect(bodyObj.session).toEqual(mockSession) + return mockResponse + }) + + const got2 = await connection.execute('SELECT 1, null from dual;') + got2.time = 1 + + expect(got2).toEqual(want) + }) + + test('it properly returns and decodes a select query (select null)', async () => { + const mockResponse = { + session: mockSession, + result: { + fields: [{ name: 'null' }], + rows: [{ lengths: ['-1'] }] + } + } + + const want: ExecutedQuery = { + headers: ['null'], + types: { null: 'NULL' }, + fields: [{ name: 'null', type: 'NULL' }], + rows: [{ null: null }], + size: 1, + statement: 'SELECT null', + time: 1, + rowsAffected: null, + insertId: null + } + + mockPool.intercept({ path: EXECUTE_PATH, method: 'POST' }).reply(200, (opts) => { + expect(opts.headers['authorization']).toMatch(/Basic /) + const bodyObj = JSON.parse(opts.body.toString()) + expect(bodyObj.session).toEqual(null) + return mockResponse + }) + + const connection = connect(config) + const got = await connection.execute('SELECT null') got.time = 1 expect(got).toEqual(want) @@ -158,7 +208,7 @@ describe('execute', () => { return mockResponse }) - const got2 = await connection.execute('SELECT 1 from dual;') + const got2 = await connection.execute('SELECT null') got2.time = 1 expect(got2).toEqual(want) diff --git a/src/index.ts b/src/index.ts index 125b3bb..99caf71 100644 --- a/src/index.ts +++ b/src/index.ts @@ -64,7 +64,7 @@ export interface Config { interface QueryResultRow { lengths: string[] - values: string + values?: string } export interface Field { @@ -217,6 +217,15 @@ export class Connection { this.session = session const fields = result?.fields ?? [] + // ensure each field has a type assigned, + // the only case it would be omitted is in the case of + // NULL due to the protojson spec. NULL in our enum + // is 0, and empty fields are omitted from the JSON response, + // so we should backfill an expected type. + for (const field of fields) { + field.type ||= 'NULL' + } + const rows = result ? parse(result, this.config.cast || cast, options.as || 'object') : [] const headers = fields.map((f) => f.name) @@ -304,7 +313,7 @@ function parse(result: QueryResult, cast: Cast, returnAs: ExecuteAs): Row[] { } function decodeRow(row: QueryResultRow): Array { - const values = atob(row.values) + const values = row.values ? atob(row.values) : '' let offset = 0 return row.lengths.map((size) => { const width = parseInt(size, 10)