From cbfe1a449ad2f9b9e252f66668d1da3b629ea709 Mon Sep 17 00:00:00 2001 From: Gajus Kuizinas Date: Mon, 4 Nov 2019 16:03:10 +0000 Subject: [PATCH] feat: make compatible with the latest Slonik --- src/routines/update.js | 35 ++++++------ src/routines/updateDistinct.js | 36 ++++++------ src/routines/upsert.js | 57 +++++++------------ src/types.js | 10 ++++ src/utilities/assignmentList.js | 24 ++++++++ src/utilities/index.js | 3 +- src/utilities/normalizeIdentifier.js | 9 +++ test/slonik-utilities/routines/update.js | 2 +- .../routines/updateDistinct.js | 8 +-- test/slonik-utilities/routines/upsert.js | 12 ++-- 10 files changed, 111 insertions(+), 85 deletions(-) create mode 100644 src/types.js create mode 100644 src/utilities/assignmentList.js create mode 100644 src/utilities/normalizeIdentifier.js diff --git a/src/routines/update.js b/src/routines/update.js index 9c71f1f..56b0654 100644 --- a/src/routines/update.js +++ b/src/routines/update.js @@ -8,58 +8,55 @@ import { } from 'slonik'; import type { DatabaseConnectionType, - ValueExpressionType, } from 'slonik'; - -type NamedValueBindingsType = { - +[key: string]: ValueExpressionType, - ..., -}; +import { + assignmentList, + normalizeIdentifier, +} from '../utilities'; +import type { + NamedAssignmentPayloadType, +} from '../types'; export default async ( connection: DatabaseConnectionType, tableName: string, - namedValueBindings: NamedValueBindingsType, + namedAssignmentPayload: NamedAssignmentPayloadType, // eslint-disable-next-line flowtype/no-weak-types booleanExpressionValues: Object = null, ) => { if (booleanExpressionValues) { - const nonOverlappingNamedValueBindings = pickBy(namedValueBindings, (value, key) => { + const nonOverlappingNamedAssignmentBindings = pickBy(namedAssignmentPayload, (value, key) => { return value !== booleanExpressionValues[key]; }); - if (Object.keys(nonOverlappingNamedValueBindings).length === 0) { + if (Object.keys(nonOverlappingNamedAssignmentBindings).length === 0) { return; } - const assignmentList = sql.assignmentList(nonOverlappingNamedValueBindings); - - const booleanExpression = sql.booleanExpression( + const booleanExpression = sql.join( Object .entries(booleanExpressionValues) .map(([key, value]) => { // $FlowFixMe - return sql.comparisonPredicate(sql.identifier([key]), '=', value); + return sql`${sql.identifier([normalizeIdentifier(key)])} = ${value}`; }), - 'AND', + sql` AND `, ); await connection.query(sql` UPDATE ${sql.identifier([tableName])} - SET ${assignmentList} + SET ${assignmentList(nonOverlappingNamedAssignmentBindings)} WHERE ${booleanExpression} `); } else { - if (Object.keys(namedValueBindings).length === 0) { + if (Object.keys(namedAssignmentPayload).length === 0) { return; } - const assignmentList = sql.assignmentList(namedValueBindings); - await connection.query(sql` UPDATE ${sql.identifier([tableName])} - SET ${assignmentList} + SET ${assignmentList(namedAssignmentPayload)} `); } }; diff --git a/src/routines/updateDistinct.js b/src/routines/updateDistinct.js index 3e18898..722ce57 100644 --- a/src/routines/updateDistinct.js +++ b/src/routines/updateDistinct.js @@ -6,55 +6,53 @@ import { } from 'slonik'; import type { DatabaseConnectionType, - ValueExpressionType, } from 'slonik'; - -type NamedValueBindingsType = { - +[key: string]: ValueExpressionType, - ..., -}; +import { + assignmentList, +} from '../utilities'; +import type { + NamedAssignmentPayloadType, +} from '../types'; export default async ( connection: DatabaseConnectionType, tableName: string, - namedValueBindings: NamedValueBindingsType, + namedAssignmentPayload: NamedAssignmentPayloadType, // eslint-disable-next-line flowtype/no-weak-types booleanExpressionValues: Object = null, ) => { - const assignmentList = sql.assignmentList(namedValueBindings); - - let booleanExpression = sql.booleanExpression( + let booleanExpression = sql.join( Object - .entries(namedValueBindings) + .entries(namedAssignmentPayload) .map(([key, value]) => { // $FlowFixMe - return sql.raw('$1 IS DISTINCT FROM $2', [sql.identifier([normalizeIdentifier(key)]), value]); + return sql`${sql.identifier([normalizeIdentifier(key)])} IS DISTINCT FROM ${value}`; }), - 'OR', + sql` OR `, ); if (booleanExpressionValues) { - booleanExpression = sql.booleanExpression( + booleanExpression = sql.join( [ booleanExpression, - sql.booleanExpression( + sql.join( Object .entries(booleanExpressionValues) .map(([key, value]) => { // $FlowFixMe - return sql.comparisonPredicate(sql.identifier([key]), '=', value); + return sql`${sql.identifier([normalizeIdentifier(key)])} = ${value}`; }), - 'AND', + sql` AND `, ), ], - 'AND', + sql` AND `, ); } await connection.query(sql` UPDATE ${sql.identifier([tableName])} - SET ${assignmentList} + SET ${assignmentList(namedAssignmentPayload)} WHERE ${booleanExpression} `); }; diff --git a/src/routines/upsert.js b/src/routines/upsert.js index 7d2b022..382e9f2 100644 --- a/src/routines/upsert.js +++ b/src/routines/upsert.js @@ -13,9 +13,6 @@ import { mapKeys, snakeCase, } from 'lodash'; -import { - escapeIdentifier, -} from '../utilities'; import Logger from '../Logger'; type NamedValueBindingsType = { @@ -86,34 +83,30 @@ export default async ( throw new Error('Named value bindings object must have properties.'); } - const columnIdentifiers = sql.identifierList( + const columnIdentifiers = sql.join( columnNames .map((columnName) => { - return [ - columnName, - ]; + return sql.identifier([columnName]); }), + sql`, `, ); - const values = sql.valueList(boundValues); - - const conflictColumnIdentifiers = sql.identifierList( + const conflictColumnIdentifiers = sql.join( uniqueConstraintColumnNames.map((uniqueConstraintColumnName) => { - return [ - uniqueConstraintColumnName, - ]; + return sql.identifier([uniqueConstraintColumnName]); }), + sql`, `, ); let updateClause; if (updateColumnNames.length) { - updateClause = sql.raw( + updateClause = sql.join( updateColumnNames .map((updateColumnName) => { - return escapeIdentifier(updateColumnName) + ' = EXCLUDED.' + escapeIdentifier(updateColumnName); - }) - .join(', '), + return sql`${sql.identifier([updateColumnName])} = ${sql.identifier(['EXCLUDED', updateColumnName])}`; + }), + sql`, `, ); } @@ -122,24 +115,18 @@ export default async ( ...updateColumnNames, ]); - const whereClause = sql.booleanExpression(targetColumnNames.map((targetColumnName) => { - const value = normalizedNamedValueBindings[normalizeNamedValueBindingName(targetColumnName)]; + const whereClause = sql.join( + targetColumnNames.map((targetColumnName) => { + const value = normalizedNamedValueBindings[normalizeNamedValueBindingName(targetColumnName)]; - if (value === null) { - return sql.raw( - '$1 IS NULL', - [ - sql.identifier([targetColumnName]), - ], - ); - } + if (value === null) { + return sql`${sql.identifier([targetColumnName])} IS NULL`; + } - return sql.comparisonPredicate( - sql.identifier([targetColumnName]), - '=', - value, - ); - }), 'AND'); + return sql`${sql.identifier([targetColumnName])} = ${value}`; + }), + sql` AND `, + ); const selectQuery = sql` SELECT ${sql.identifier([configuration.identifierName])} @@ -159,7 +146,7 @@ export default async ( if (updateClause) { return connection.oneFirst(sql` INSERT INTO ${sql.identifier([tableName])} (${columnIdentifiers}) - VALUES (${values}) + VALUES (${sql.join(boundValues, sql`, `)}) ON CONFLICT (${conflictColumnIdentifiers}) DO UPDATE SET @@ -170,7 +157,7 @@ export default async ( maybeId = await connection.maybeOneFirst(sql` INSERT INTO ${sql.identifier([tableName])} (${columnIdentifiers}) - VALUES (${values}) + VALUES (${sql.join(boundValues, sql`, `)}) ON CONFLICT (${conflictColumnIdentifiers}) DO NOTHING `); diff --git a/src/types.js b/src/types.js new file mode 100644 index 0000000..ce9680b --- /dev/null +++ b/src/types.js @@ -0,0 +1,10 @@ +// @flow + +import type { + ValueExpressionType, +} from 'slonik'; + +export type NamedAssignmentPayloadType = { + [key: string]: ValueExpressionType, + ..., +}; diff --git a/src/utilities/assignmentList.js b/src/utilities/assignmentList.js new file mode 100644 index 0000000..e49960a --- /dev/null +++ b/src/utilities/assignmentList.js @@ -0,0 +1,24 @@ +// @flow + +import { + sql, +} from 'slonik'; +import type { + ListSqlTokenType, +} from 'slonik'; +import type { + NamedAssignmentPayloadType, +} from '../types'; +import normalizeIdentifier from './normalizeIdentifier'; + +export default (namedAssignment: NamedAssignmentPayloadType): ListSqlTokenType => { + const values = Object.values(Object + .entries(namedAssignment) + .map(([column, value]) => { + // $FlowFixMe + return sql`${sql.identifier([normalizeIdentifier(column)])} = ${value}`; + })); + + // $FlowFixMe + return sql.join(values, sql`, `); +}; diff --git a/src/utilities/index.js b/src/utilities/index.js index 5f3fb88..9e7a8b4 100644 --- a/src/utilities/index.js +++ b/src/utilities/index.js @@ -1,3 +1,4 @@ // @flow -export {default as escapeIdentifier} from './escapeIdentifier'; +export {default as assignmentList} from './assignmentList'; +export {default as normalizeIdentifier} from './normalizeIdentifier'; diff --git a/src/utilities/normalizeIdentifier.js b/src/utilities/normalizeIdentifier.js new file mode 100644 index 0000000..863edf1 --- /dev/null +++ b/src/utilities/normalizeIdentifier.js @@ -0,0 +1,9 @@ +// @flow + +import { + snakeCase, +} from 'lodash'; + +export default (propertyName: string): string => { + return snakeCase(propertyName); +}; diff --git a/test/slonik-utilities/routines/update.js b/test/slonik-utilities/routines/update.js index c82419f..bb30b43 100644 --- a/test/slonik-utilities/routines/update.js +++ b/test/slonik-utilities/routines/update.js @@ -128,7 +128,7 @@ test('executes UPDATE query with WHERE condition (AND boolean expression short-h t.is(connection.query.callCount, 1); - t.is(normalizeQuery(connection.query.firstCall.args[0].sql), 'UPDATE "foo" SET "bar" = $1 WHERE ("qux" = $2)'); + t.is(normalizeQuery(connection.query.firstCall.args[0].sql), 'UPDATE "foo" SET "bar" = $1 WHERE "qux" = $2'); t.deepEqual(connection.query.firstCall.args[0].values, [ 'baz', 'quux', diff --git a/test/slonik-utilities/routines/updateDistinct.js b/test/slonik-utilities/routines/updateDistinct.js index 7e0486e..e96a259 100644 --- a/test/slonik-utilities/routines/updateDistinct.js +++ b/test/slonik-utilities/routines/updateDistinct.js @@ -31,7 +31,7 @@ test('executes UPDATE query without WHERE condition (single column)', async (t) t.is(connection.query.callCount, 1); - t.is(normalizeQuery(connection.query.firstCall.args[0].sql), 'UPDATE "foo" SET "bar" = $1 WHERE ("bar" IS DISTINCT FROM $2)'); + t.is(normalizeQuery(connection.query.firstCall.args[0].sql), 'UPDATE "foo" SET "bar" = $1 WHERE "bar" IS DISTINCT FROM $2'); t.deepEqual(connection.query.firstCall.args[0].values, [ 'baz', 'baz', @@ -53,7 +53,7 @@ test('executes UPDATE query without WHERE condition (multiple columns)', async ( t.is(connection.query.callCount, 1); - t.is(normalizeQuery(connection.query.firstCall.args[0].sql), 'UPDATE "foo" SET "bar_0" = $1, "bar_1" = $2, "bar_2" = $3 WHERE ("bar_0" IS DISTINCT FROM $4 OR "bar_1" IS DISTINCT FROM $5 OR "bar_2" IS DISTINCT FROM $6)'); + t.is(normalizeQuery(connection.query.firstCall.args[0].sql), 'UPDATE "foo" SET "bar_0" = $1, "bar_1" = $2, "bar_2" = $3 WHERE "bar_0" IS DISTINCT FROM $4 OR "bar_1" IS DISTINCT FROM $5 OR "bar_2" IS DISTINCT FROM $6'); t.deepEqual(connection.query.firstCall.args[0].values, [ 'baz0', 'baz1', @@ -79,7 +79,7 @@ test('executes UPDATE query without WHERE condition (SQL token)', async (t) => { t.is(connection.query.callCount, 1); - t.is(normalizeQuery(connection.query.firstCall.args[0].sql), 'UPDATE "foo" SET "bar_0" = $1, "bar_1" = to_timestamp($2), "bar_2" = $3 WHERE ("bar_0" IS DISTINCT FROM $4 OR "bar_1" IS DISTINCT FROM to_timestamp($5) OR "bar_2" IS DISTINCT FROM $6)'); + t.is(normalizeQuery(connection.query.firstCall.args[0].sql), 'UPDATE "foo" SET "bar_0" = $1, "bar_1" = to_timestamp($2), "bar_2" = $3 WHERE "bar_0" IS DISTINCT FROM $4 OR "bar_1" IS DISTINCT FROM to_timestamp($5) OR "bar_2" IS DISTINCT FROM $6'); t.deepEqual(connection.query.firstCall.args[0].values, [ 'baz0', 'baz1', @@ -106,7 +106,7 @@ test('executes UPDATE query with WHERE condition (AND boolean expression short-h t.is(connection.query.callCount, 1); - t.is(normalizeQuery(connection.query.firstCall.args[0].sql), 'UPDATE "foo" SET "bar" = $1 WHERE (("bar" IS DISTINCT FROM $2) AND ("qux" = $3))'); + t.is(normalizeQuery(connection.query.firstCall.args[0].sql), 'UPDATE "foo" SET "bar" = $1 WHERE "bar" IS DISTINCT FROM $2 AND "qux" = $3'); t.deepEqual(connection.query.firstCall.args[0].values, [ 'baz', 'baz', diff --git a/test/slonik-utilities/routines/upsert.js b/test/slonik-utilities/routines/upsert.js index bbf4b08..2d24bbd 100644 --- a/test/slonik-utilities/routines/upsert.js +++ b/test/slonik-utilities/routines/upsert.js @@ -37,7 +37,7 @@ test('first attempts SELECT using named value bindings', async (t) => { t.is(connection.query.callCount, 1); - t.is(normalizeQuery(connection.query.firstCall.args[0].sql), 'SELECT "id" FROM "foo" WHERE ("bar" = $1)'); + t.is(normalizeQuery(connection.query.firstCall.args[0].sql), 'SELECT "id" FROM "foo" WHERE "bar" = $1'); t.deepEqual(connection.query.firstCall.args[0].values, [ 'baz', ]); @@ -83,7 +83,7 @@ test('executes INSERT .. DO UPDATE if SELECT returns NULL and update column name t.is(connection.query.callCount, 2); - t.is(normalizeQuery(connection.query.secondCall.args[0].sql), 'INSERT INTO "foo" ("bar", "qux") VALUES ($1, $2) ON CONFLICT ("bar") DO UPDATE SET "qux" = EXCLUDED."qux" RETURNING "id"'); + t.is(normalizeQuery(connection.query.secondCall.args[0].sql), 'INSERT INTO "foo" ("bar", "qux") VALUES ($1, $2) ON CONFLICT ("bar") DO UPDATE SET "qux" = "EXCLUDED"."qux" RETURNING "id"'); t.deepEqual(connection.query.secondCall.args[0].values, [ 'baz', 'quux', @@ -117,7 +117,7 @@ test('executes INSERT .. DO NOTHING followed by SELECT if SELECT returns NULL an 'baz', ]); - t.is(normalizeQuery(connection.query.thirdCall.args[0].sql), 'SELECT "id" FROM "foo" WHERE ("bar" = $1)'); + t.is(normalizeQuery(connection.query.thirdCall.args[0].sql), 'SELECT "id" FROM "foo" WHERE "bar" = $1'); t.deepEqual(connection.query.thirdCall.args[0].values, [ 'baz', ]); @@ -144,7 +144,7 @@ test('uses unique constraint column name values and update column name values to t.is(connection.query.callCount, 1); - t.is(normalizeQuery(connection.query.firstCall.args[0].sql), 'SELECT "id" FROM "foo" WHERE ("bar_0" = $1 AND "bar_1" = $2 AND "bar_2" = $3)'); + t.is(normalizeQuery(connection.query.firstCall.args[0].sql), 'SELECT "id" FROM "foo" WHERE "bar_0" = $1 AND "bar_1" = $2 AND "bar_2" = $3'); t.deepEqual(connection.query.firstCall.args[0].values, [ 'baz0', 'baz1', @@ -170,7 +170,7 @@ test('converts named value bindings to snake case (SELECT)', async (t) => { t.is(connection.query.callCount, 1); - t.is(normalizeQuery(connection.query.firstCall.args[0].sql), 'SELECT "id" FROM "foo" WHERE ("bar_baz" = $1)'); + t.is(normalizeQuery(connection.query.firstCall.args[0].sql), 'SELECT "id" FROM "foo" WHERE "bar_baz" = $1'); t.deepEqual(connection.query.firstCall.args[0].values, [ 'baz', ]); @@ -196,7 +196,7 @@ test('converts named value bindings to snake case (INSERT)', async (t) => { t.is(connection.query.callCount, 2); - t.is(normalizeQuery(connection.query.secondCall.args[0].sql), 'INSERT INTO "foo" ("bar_baz", "qux_quux") VALUES ($1, $2) ON CONFLICT ("bar_baz") DO UPDATE SET "qux_quux" = EXCLUDED."qux_quux" RETURNING "id"'); + t.is(normalizeQuery(connection.query.secondCall.args[0].sql), 'INSERT INTO "foo" ("bar_baz", "qux_quux") VALUES ($1, $2) ON CONFLICT ("bar_baz") DO UPDATE SET "qux_quux" = "EXCLUDED"."qux_quux" RETURNING "id"'); t.deepEqual(connection.query.secondCall.args[0].values, [ 'baz', 'quux',