Skip to content

Commit 7fc785c

Browse files
karlhorkyporsager
authored andcommitted
Add nested transforms (#460)
* Add first version of nested transforms * Fix access before initialization * Add trailing EOL * Add higher-order function to reduce repetition * Fix data structures * Fix data structures * Format config * Add first tests for new transform options * Pass column * Update documentation * Update types * Document undefined transform option
1 parent 1356b37 commit 7fc785c

File tree

6 files changed

+254
-35
lines changed

6 files changed

+254
-35
lines changed

README.md

Lines changed: 99 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -575,32 +575,19 @@ Do note that you can often achieve the same result using [`WITH` queries (Common
575575

576576
## Data Transformation
577577

578-
Postgres.js comes with a number of built-in data transformation functions that can be used to transform the data returned from a query or when inserting data. They are available under `transform` option in the `postgres()` function connection options.
578+
Postgres.js allows for transformation of the data passed to or returned from a query by using the `transform` option.
579579

580-
Like - `postgres('connectionURL', { transform: {...} })`
581-
582-
### Parameters
583-
* `to`: The function to transform the outgoing query column name to, i.e `SELECT ${ sql('aName') }` to `SELECT a_name` when using `postgres.toCamel`.
584-
* `from`: The function to transform the incoming query result column name to, see example below.
580+
Built in transformation functions are:
585581

586-
> Both parameters are optional, if not provided, the default transformation function will be used.
582+
* For camelCase - `postgres.camel`, `postgres.toCamel`, `postgres.fromCamel`
583+
* For PascalCase - `postgres.pascal`, `postgres.toPascal`, `postgres.fromPascal`
584+
* For Kebab-Case - `postgres.kebab`, `postgres.toKebab`, `postgres.fromKebab`
587585

588-
Built in transformation functions are:
589-
* For camelCase - `postgres.toCamel` and `postgres.fromCamel`
590-
* For PascalCase - `postgres.toPascal` and `postgres.fromPascal`
591-
* For Kebab-Case - `postgres.toKebab` and `postgres.fromKebab`
586+
By default, using `postgres.camel`, `postgres.pascal` and `postgres.kebab` will perform a two-way transformation - both the data passed to the query and the data returned by the query will be transformed:
592587

593-
These functions can be passed in as options when calling `postgres()`, for example:
594588
```js
595589
// Transform the column names to and from camel case
596-
const sql = postgres('connectionURL', {
597-
transform: {
598-
column: {
599-
to: postgres.fromCamel,
600-
from: postgres.toCamel,
601-
},
602-
},
603-
})
590+
const sql = postgres({ transform: postgres.camel })
604591

605592
await sql`CREATE TABLE IF NOT EXISTS camel_case (a_test INTEGER, b_test TEXT)`
606593
await sql`INSERT INTO camel_case ${ sql([{ aTest: 1, bTest: 1 }]) }`
@@ -609,7 +596,98 @@ const data = await sql`SELECT ${ sql('aTest', 'bTest') } FROM camel_case`
609596
console.log(data) // [ { aTest: 1, bTest: '1' } ]
610597
```
611598

612-
> Note that if a column name is originally registered as snake_case in the database then to tranform it from camelCase to snake_case when querying or inserting, the column camelCase name must be put in `sql('columnName')` as it's done in the above example, Postgres.js does not rewrite anything inside the static parts of the tagged templates.
599+
To only perform half of the transformation (eg. only the transformation **to** or **from** camel case), use the other transformation functions:
600+
601+
```js
602+
// Transform the column names only to camel case
603+
// (for the results that are returned from the query)
604+
postgres({ transform: postgres.toCamel })
605+
606+
await sql`CREATE TABLE IF NOT EXISTS camel_case (a_test INTEGER)`
607+
await sql`INSERT INTO camel_case ${ sql([{ a_test: 1 }]) }`
608+
const data = await sql`SELECT a_test FROM camel_case`
609+
610+
console.log(data) // [ { aTest: 1 } ]
611+
```
612+
613+
```js
614+
// Transform the column names only from camel case
615+
// (for interpolated inserts, updates, and selects)
616+
const sql = postgres({ transform: postgres.fromCamel })
617+
618+
await sql`CREATE TABLE IF NOT EXISTS camel_case (a_test INTEGER)`
619+
await sql`INSERT INTO camel_case ${ sql([{ aTest: 1 }]) }`
620+
const data = await sql`SELECT ${ sql('aTest') } FROM camel_case`
621+
622+
console.log(data) // [ { a_test: 1 } ]
623+
```
624+
625+
> Note that Postgres.js does not rewrite the static parts of the tagged template strings. So to transform column names in your queries, the `sql()` helper must be used - eg. `${ sql('columnName') }` as in the examples above.
626+
627+
### Transform `undefined` Values
628+
629+
By default, Postgres.js will throw the error `UNDEFINED_VALUE: Undefined values are not allowed` when undefined values are passed
630+
631+
```js
632+
// Transform the column names to and from camel case
633+
const sql = postgres({
634+
transform: {
635+
undefined: null
636+
}
637+
})
638+
639+
await sql`CREATE TABLE IF NOT EXISTS transform_undefined (a_test INTEGER)`
640+
await sql`INSERT INTO transform_undefined ${ sql([{ a_test: undefined }]) }`
641+
const data = await sql`SELECT a_test FROM transform_undefined`
642+
643+
console.log(data) // [ { a_test: null } ]
644+
```
645+
646+
To combine with the built in transform functions, spread the transform in the `transform` object:
647+
648+
```js
649+
// Transform the column names to and from camel case
650+
const sql = postgres({
651+
transform: {
652+
...postgres.camel,
653+
undefined: null
654+
}
655+
})
656+
657+
await sql`CREATE TABLE IF NOT EXISTS transform_undefined (a_test INTEGER)`
658+
await sql`INSERT INTO transform_undefined ${ sql([{ aTest: undefined }]) }`
659+
const data = await sql`SELECT ${ sql('aTest') } FROM transform_undefined`
660+
661+
console.log(data) // [ { aTest: null } ]
662+
```
663+
664+
### Custom Transform Functions
665+
666+
To specify your own transformation functions, you can use the `column`, `value` and `row` options inside of `transform`, each an object possibly including `to` and `from` keys:
667+
668+
* `to`: The function to transform the outgoing query column name to, i.e `SELECT ${ sql('aName') }` to `SELECT a_name` when using `postgres.toCamel`.
669+
* `from`: The function to transform the incoming query result column name to, see example below.
670+
671+
> Both parameters are optional, if not provided, the default transformation function will be used.
672+
673+
```js
674+
// Implement your own functions, look at postgres.toCamel, etc
675+
// as a reference:
676+
// https://github.com/porsager/postgres/blob/4241824ffd7aa94ffb482e54ca9f585d9d0a4eea/src/types.js#L310-L328
677+
function transformColumnToDatabase() { /* ... */ }
678+
function transformColumnFromDatabase() { /* ... */ }
679+
680+
const sql = postgres({
681+
transform: {
682+
column: {
683+
to: transformColumnToDatabase,
684+
from: transformColumnFromDatabase,
685+
},
686+
value: { /* ... */ },
687+
row: { /* ... */ }
688+
}
689+
})
690+
```
613691

614692
## Listen & notify
615693

src/connection.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -493,8 +493,8 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
493493
query.isRaw
494494
? (row[i] = query.isRaw === true
495495
? value
496-
: transform.value.from ? transform.value.from(value) : value)
497-
: (row[column.name] = transform.value.from ? transform.value.from(value) : value)
496+
: transform.value.from ? transform.value.from(value, column) : value)
497+
: (row[column.name] = transform.value.from ? transform.value.from(value, column) : value)
498498
}
499499

500500
query.forEachFn

src/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@ import {
88
Identifier,
99
Builder,
1010
toPascal,
11+
pascal,
1112
toCamel,
13+
camel,
1214
toKebab,
15+
kebab,
1316
fromPascal,
1417
fromCamel,
1518
fromKebab
@@ -25,8 +28,11 @@ import largeObject from './large.js'
2528
Object.assign(Postgres, {
2629
PostgresError,
2730
toPascal,
31+
pascal,
2832
toCamel,
33+
camel,
2934
toKebab,
35+
kebab,
3036
fromPascal,
3137
fromCamel,
3238
fromKebab,

src/types.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,3 +322,34 @@ export const toKebab = x => x.replace(/_/g, '-')
322322
export const fromCamel = x => x.replace(/([A-Z])/g, '_$1').toLowerCase()
323323
export const fromPascal = x => (x.slice(0, 1) + x.slice(1).replace(/([A-Z])/g, '_$1')).toLowerCase()
324324
export const fromKebab = x => x.replace(/-/g, '_')
325+
326+
function createJsonTransform(fn) {
327+
return function jsonTransform(x, column) {
328+
return column.type === 114 || column.type === 3802
329+
? Array.isArray(x)
330+
? x.map(jsonTransform)
331+
: Object.entries(x).reduce((acc, [k, v]) => Object.assign(acc, { [fn(k)]: v }), {})
332+
: x
333+
}
334+
}
335+
336+
toCamel.column = { from: toCamel }
337+
toCamel.value = { from: createJsonTransform(toCamel) }
338+
fromCamel.column = { to: fromCamel }
339+
340+
export const camel = { ...toCamel }
341+
camel.column.to = fromCamel;
342+
343+
toPascal.column = { from: toPascal }
344+
toPascal.value = { from: createJsonTransform(toPascal) }
345+
fromPascal.column = { to: fromPascal }
346+
347+
export const pascal = { ...toPascal }
348+
pascal.column.to = fromPascal
349+
350+
toKebab.column = { from: toKebab }
351+
toKebab.value = { from: createJsonTransform(toKebab) }
352+
fromKebab.column = { to: fromKebab }
353+
354+
export const kebab = { ...toKebab }
355+
kebab.column.to = fromKebab

tests/index.js

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1317,7 +1317,60 @@ t('Transform value', async() => {
13171317
})
13181318

13191319
t('Transform columns from', async() => {
1320-
const sql = postgres({ ...options, transform: { column: { to: postgres.fromCamel, from: postgres.toCamel } } })
1320+
const sql = postgres({
1321+
...options,
1322+
transform: postgres.fromCamel
1323+
})
1324+
await sql`create table test (a_test int, b_test text)`
1325+
await sql`insert into test ${ sql([{ aTest: 1, bTest: 1 }]) }`
1326+
await sql`update test set ${ sql({ aTest: 2, bTest: 2 }) }`
1327+
return [
1328+
2,
1329+
(await sql`select ${ sql('aTest', 'bTest') } from test`)[0].a_test,
1330+
await sql`drop table test`
1331+
]
1332+
})
1333+
1334+
t('Transform columns to', async() => {
1335+
const sql = postgres({
1336+
...options,
1337+
transform: postgres.toCamel
1338+
})
1339+
await sql`create table test (a_test int, b_test text)`
1340+
await sql`insert into test ${ sql([{ a_test: 1, b_test: 1 }]) }`
1341+
await sql`update test set ${ sql({ a_test: 2, b_test: 2 }) }`
1342+
return [
1343+
2,
1344+
(await sql`select a_test, b_test from test`)[0].aTest,
1345+
await sql`drop table test`
1346+
]
1347+
})
1348+
1349+
t('Transform columns from and to', async() => {
1350+
const sql = postgres({
1351+
...options,
1352+
transform: postgres.camel
1353+
})
1354+
await sql`create table test (a_test int, b_test text)`
1355+
await sql`insert into test ${ sql([{ aTest: 1, bTest: 1 }]) }`
1356+
await sql`update test set ${ sql({ aTest: 2, bTest: 2 }) }`
1357+
return [
1358+
2,
1359+
(await sql`select ${ sql('aTest', 'bTest') } from test`)[0].aTest,
1360+
await sql`drop table test`
1361+
]
1362+
})
1363+
1364+
t('Transform columns from and to (legacy)', async() => {
1365+
const sql = postgres({
1366+
...options,
1367+
transform: {
1368+
column: {
1369+
to: postgres.fromCamel,
1370+
from: postgres.toCamel
1371+
}
1372+
}
1373+
})
13211374
await sql`create table test (a_test int, b_test text)`
13221375
await sql`insert into test ${ sql([{ aTest: 1, bTest: 1 }]) }`
13231376
await sql`update test set ${ sql({ aTest: 2, bTest: 2 }) }`

0 commit comments

Comments
 (0)