Skip to content

Commit 1aea110

Browse files
committed
Add option for column name transformation
1 parent 2dbeeb0 commit 1aea110

File tree

6 files changed

+56
-18
lines changed

6 files changed

+56
-18
lines changed

CHANGES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
In the next release ...
22

3+
- The query options now supports an optional `transform` parameter which takes
4+
a column name input, allowing the transformation of column names into for
5+
example camelcase.
6+
37
- The `query` method now accepts a `QueryParameter` object as the
48
first value, in addition to the query string, making it easier to
59
make a query with additional configuration.

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,13 @@ The copy commands are not supported.
260260
pool.use(...)
261261
```
262262

263+
2. _How do I convert column names to camelcase?_ Use the `transform` option:
264+
265+
```typescript
266+
const camelcase = (s: string) => s.replace(/(_\w)/g, k => k[1].toUpperCase());
267+
const result = client.query({text: ..., transform: camelcase})
268+
```
269+
263270
## Benchmarking
264271

265272
Use the following environment variable to run tests in "benchmark" mode.

src/client.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -515,10 +515,11 @@ export class Client {
515515
}
516516

517517
prepare<T = ResultRecord>(
518-
text: string,
518+
text: QueryParameter | string,
519519
name?: string,
520520
types?: DataType[]): Promise<PreparedStatement<T>> {
521521

522+
const query = typeof text === 'string' ? {text} : text;
522523
const providedNameOrGenerated = name || (
523524
(this.config.preparedStatementPrefix ||
524525
defaults.preparedStatementPrefix) + (
@@ -529,7 +530,7 @@ export class Client {
529530
(resolve, reject) => {
530531
const errorHandler: ErrorHandler = (error) => reject(error);
531532
this.errorHandlerQueue.push(errorHandler);
532-
this.writer.parse(providedNameOrGenerated, text, types || []);
533+
this.writer.parse(providedNameOrGenerated, query.text, types || query.types || []);
533534
this.writer.describe(providedNameOrGenerated, 'S');
534535
this.preFlightQueue.push({
535536
descriptionHandler: (description: RowDescription) => {
@@ -557,7 +558,7 @@ export class Client {
557558
format?: DataFormat | DataFormat[],
558559
streams?: Record<string, Writable>,
559560
) => {
560-
const result = makeResult<T>();
561+
const result = makeResult<T>(query?.transform);
561562
result.nameHandler(description.names);
562563
const info = {
563564
handler: {
@@ -568,11 +569,11 @@ export class Client {
568569
};
569570
this.bindAndExecute(info, {
570571
name: providedNameOrGenerated,
571-
portal: portal || '',
572-
format: format || DataFormat.Binary,
572+
portal: portal || query.portal || '',
573+
format: format || query.format || DataFormat.Binary,
573574
values: values || [],
574575
close: false
575-
}, types);
576+
}, types || query.types);
576577

577578
return result.iterator
578579
}
@@ -667,7 +668,7 @@ export class Client {
667668
const types = options?.types;
668669
const streams = options?.streams;
669670
const portal = options?.portal || '';
670-
const result = makeResult<T>();
671+
const result = makeResult<T>(options?.transform);
671672

672673
const descriptionHandler = (description: RowDescription) => {
673674
result.nameHandler(description.names);

src/query.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,30 @@ import {
55
} from './types';
66

77
export interface QueryOptions {
8+
/** The query name. */
89
readonly name: string;
10+
/** Whether to use the default portal (i.e. unnamed) or provide a name. */
911
readonly portal: string;
12+
/** Allows making the database native type explicit for some or all columns. */
1013
readonly types: DataType[];
14+
/** Whether column data should be transferred using text or binary mode. */
1115
readonly format: DataFormat | DataFormat[];
16+
/** A mapping from column name to a socket, e.g. an open file. */
1217
readonly streams: Record<string, Writable>;
18+
/** Allows the transformation of column names as returned by the database. */
19+
readonly transform: (name: string) => string;
1320
}
1421

22+
/**
23+
* A query parameter can be used in place of a query text as the first argument
24+
* to the {@link Client.query} method.
25+
* @interface
26+
*/
1527
export type QueryParameter = Partial<QueryOptions> & { text: string };
1628

29+
/**
30+
* A complete query object, ready to send to the database.
31+
*/
1732
export class Query {
1833
public readonly text: string;
1934
public readonly values?: any[];

src/result.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ export type NameHandler = Callback<string[]>;
209209

210210
ResultIterator.prototype.constructor = Promise
211211

212-
export function makeResult<T>() {
212+
export function makeResult<T>(transform?: (name: string) => string) {
213213
let dataHandler: DataHandler | null = null;
214214

215215
const names: string[] = [];
@@ -232,6 +232,9 @@ export function makeResult<T>() {
232232

233233
const nameHandler = (ns: string[]) => {
234234
names.length = 0;
235+
if (transform) {
236+
ns = ns.map(transform);
237+
}
235238
names.push(...ns);
236239
}
237240

test/client.test.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -239,16 +239,11 @@ describe('Query', () => {
239239
expect(result.rows.length).toEqual(1);
240240
});
241241

242-
testWithClient('Prepared statement', async (client) => {
243-
const count = 5;
244-
expect.assertions(count * 2);
245-
await client.query('prepare test (int) as select $1');
246-
for (let i = 0; i < count; i++) {
247-
const result = await client.query('execute test(1)');
248-
const rows = result.rows;
249-
expect(rows.length).toEqual(1);
250-
expect(rows[0]).toEqual([1]);
251-
}
242+
testWithClient('Name transform', async (client) => {
243+
expect.assertions(1);
244+
const query = {text: 'select 1 as foo', transform: (s: string) => s.toUpperCase()};
245+
const result = await client.query(query);
246+
expect(result.names).toEqual(['FOO']);
252247
});
253248

254249
testWithClient('Listen/notify', async (client) => {
@@ -487,6 +482,19 @@ describe('Query', () => {
487482
await stmt.close();
488483
});
489484

485+
testWithClient(
486+
'Prepare and execute (SELECT)',
487+
async (client) => {
488+
const query = {text: 'select $1::int as i', transform: (s: string) => s.toUpperCase()};
489+
const stmt = await client.prepare(query);
490+
await expect(stmt.execute([1])).resolves.toEqual(
491+
{ names: ['I'], rows: [[1]], status: 'SELECT 1' }
492+
);
493+
const result = await stmt.execute([2]);
494+
expect(result.rows).toEqual([[2]]);
495+
await stmt.close();
496+
});
497+
490498
testWithClient(
491499
'Prepare and execute (INSERT)',
492500
async (client) => {

0 commit comments

Comments
 (0)