diff --git a/__tests__/index.test.ts b/__tests__/index.test.ts index e9063d4..eba7e7b 100644 --- a/__tests__/index.test.ts +++ b/__tests__/index.test.ts @@ -129,6 +129,7 @@ describe('execute', () => { const want: ExecutedQuery = { headers: [':vtg1'], types: { ':vtg1': 'INT32' }, + fields: [{ name: ':vtg1', type: 'INT32' }], rows: [{ ':vtg1': 1 }], size: 1, statement: 'SELECT 1 from dual;', @@ -163,6 +164,41 @@ describe('execute', () => { expect(got2).toEqual(want) }) + test('it properly returns and decodes a select query with rows as array when designated', async () => { + const mockResponse = { + session: mockSession, + result: { + fields: [{ name: ':vtg1', type: 'INT32' }], + rows: [{ lengths: ['1'], values: 'MQ==' }] + } + } + + const want: ExecutedQuery = { + headers: [':vtg1'], + types: { ':vtg1': 'INT32' }, + rows: [[1]], + fields: [{ name: ':vtg1', type: 'INT32' }], + size: 1, + statement: 'SELECT 1 from dual;', + 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 1 from dual;', null, { as: 'array' }) + got.time = 1 + + expect(got).toEqual(want) + }) + test('it properly returns an executed query for a DDL statement', async () => { const mockResponse = { session: mockSession, @@ -175,6 +211,7 @@ describe('execute', () => { const want: ExecutedQuery = { headers: [], types: {}, + fields: [], rows: [], rowsAffected: null, insertId: null, @@ -204,6 +241,7 @@ describe('execute', () => { const want: ExecutedQuery = { headers: [], types: {}, + fields: [], rows: [], rowsAffected: 1, insertId: null, @@ -234,6 +272,7 @@ describe('execute', () => { const want: ExecutedQuery = { headers: [], types: {}, + fields: [], rows: [], rowsAffected: 1, insertId: '2', @@ -318,6 +357,7 @@ describe('execute', () => { headers: [':vtg1'], rows: [{ ':vtg1': 1 }], types: { ':vtg1': 'INT32' }, + fields: [{ name: ':vtg1', type: 'INT32' }], size: 1, insertId: null, rowsAffected: null, @@ -350,6 +390,7 @@ describe('execute', () => { const want: ExecutedQuery = { headers: [':vtg1'], types: { ':vtg1': 'INT32' }, + fields: [{ name: ':vtg1', type: 'INT32' }], rows: [{ ':vtg1': 1 }], size: 1, insertId: null, @@ -383,6 +424,7 @@ describe('execute', () => { const want: ExecutedQuery = { headers: [':vtg1'], types: { ':vtg1': 'INT64' }, + fields: [{ name: ':vtg1', type: 'INT64' }], rows: [{ ':vtg1': BigInt(1) }], size: 1, insertId: null, @@ -419,6 +461,7 @@ describe('execute', () => { const want: ExecutedQuery = { headers: ['document'], types: { document: 'JSON' }, + fields: [{ name: 'document', type: 'JSON' }], rows: [{ document: JSON.parse(document) }], size: 1, insertId: null, diff --git a/src/index.ts b/src/index.ts index c1048a9..125b3bb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,7 @@ export { hex } from './text.js' import { decode } from './text.js' import { Version } from './version.js' -type Row = Record +type Row = Record | any[] interface VitessError { message: string @@ -28,6 +28,7 @@ export interface ExecutedQuery { headers: string[] types: Types rows: Row[] + fields: Field[] size: number statement: string insertId: string | null @@ -97,6 +98,18 @@ interface QueryResult { rows?: QueryResultRow[] } +type ExecuteAs = 'array' | 'object' + +type ExecuteOptions = { + as?: ExecuteAs +} + +type ExecuteArgs = object | any[] | null + +const defaultExecuteOptions: ExecuteOptions = { + as: 'object' +} + export class Client { private config: Config @@ -108,8 +121,12 @@ export class Client { return this.connection().transaction(fn) } - async execute(query: string, args?: object | any[]): Promise { - return this.connection().execute(query, args) + async execute( + query: string, + args: ExecuteArgs = null, + options: ExecuteOptions = defaultExecuteOptions + ): Promise { + return this.connection().execute(query, args, options) } connection(): Connection { @@ -126,8 +143,12 @@ class Tx { this.conn = conn } - async execute(query: string, args?: object | any[]): Promise { - return this.conn.execute(query, args) + async execute( + query: string, + args: ExecuteArgs = null, + options: ExecuteOptions = defaultExecuteOptions + ): Promise { + return this.conn.execute(query, args, options) } } @@ -171,7 +192,11 @@ export class Connection { await this.createSession() } - async execute(query: string, args?: any): Promise { + async execute( + query: string, + args: ExecuteArgs = null, + options: ExecuteOptions = defaultExecuteOptions + ): Promise { const url = new URL('/psdb.v1alpha1.Database/Execute', `https://${this.config.host}`) const formatter = this.config.format || format @@ -191,15 +216,17 @@ export class Connection { this.session = session - const rows = result ? parse(result, this.config.cast || cast) : [] - const headers = result ? result.fields?.map((f) => f.name) ?? [] : [] + const fields = result?.fields ?? [] + const rows = result ? parse(result, this.config.cast || cast, options.as || 'object') : [] + const headers = fields.map((f) => f.name) const typeByName = (acc, { name, type }) => ({ ...acc, [name]: type }) - const types = result ? result.fields?.reduce(typeByName, {}) ?? {} : {} + const types = fields.reduce(typeByName, {}) return { headers, types, + fields, rows, rowsAffected, insertId, @@ -251,18 +278,29 @@ export function connect(config: Config): Connection { return new Connection(config) } -function parseRow(fields: Field[], rawRow: QueryResultRow, cast: Cast): Row { +function parseArrayRow(fields: Field[], rawRow: QueryResultRow, cast: Cast): Row { const row = decodeRow(rawRow) + + return fields.map((field, ix) => { + return cast(field, row[ix]) + }) +} + +function parseObjectRow(fields: Field[], rawRow: QueryResultRow, cast: Cast): Row { + const row = decodeRow(rawRow) + return fields.reduce((acc, field, ix) => { acc[field.name] = cast(field, row[ix]) return acc }, {} as Row) } -function parse(result: QueryResult, cast: Cast): Row[] { +function parse(result: QueryResult, cast: Cast, returnAs: ExecuteAs): Row[] { const fields = result.fields const rows = result.rows ?? [] - return rows.map((row) => parseRow(fields, row, cast)) + return rows.map((row) => + returnAs === 'array' ? parseArrayRow(fields, row, cast) : parseObjectRow(fields, row, cast) + ) } function decodeRow(row: QueryResultRow): Array {