Skip to content

Instantly share code, notes, and snippets.

@elnygren
Created September 22, 2018 11:29
Show Gist options
  • Save elnygren/a2607c213e62a42c2b9578667094239b to your computer and use it in GitHub Desktop.
Save elnygren/a2607c213e62a42c2b9578667094239b to your computer and use it in GitHub Desktop.
Knex with query logging and camelCasing
/// <reference path="./identifierMappings-types.d.ts" />
import * as knex from 'knex';
import * as config from '../config'
import { knexSnakeCaseMappers } from './identifierMappings'
const db: knex = knex({
client: 'pg',
connection: config.DATABASE_URL,
...knexSnakeCaseMappers(),
})
if (config.NODE_ENV === 'development') {
db.on('query', (data: any) => {
console.log('SQL:', data.sql, `(${data.bindings})`)
})
}
export default db
export interface KnexMappers {
wrapIdentifier(identifier: string, origWrap: (identifier: string) => string): string;
postProcessResponse(response: any): any;
}
// https://github.com/Vincit/objection.js/blob/master/lib/utils/identifierMapping.js
// tslint:disable
// Super fast memoize for single argument functions.
function memoize(func) {
const cache = new Map();
return input => {
let output = cache.get(input);
if (output === undefined) {
output = func(input);
cache.set(input, output);
}
return output;
};
}
// camelCase to snake_case converter that also works with
// non-ascii characters.
function snakeCase(str, upperCase = false) {
if (str.length === 0) {
return str;
}
const upper = str.toUpperCase();
const lower = str.toLowerCase();
let out = lower[0];
for (let i = 1, l = str.length; i < l; ++i) {
const char = str[i];
const prevChar = str[i - 1];
const upperChar = upper[i];
const prevUpperChar = upper[i - 1];
const lowerChar = lower[i];
const prevLowerChar = lower[i - 1];
// Test if `char` is an upper-case character and that the character
// actually has different upper and lower case versions.
if (char === upperChar && upperChar !== lowerChar) {
// Multiple consecutive upper case characters shouldn't add underscores.
// For example "fooBAR" should be converted to "foo_bar".
if (prevChar === prevUpperChar && prevUpperChar !== prevLowerChar) {
out += lowerChar;
} else {
out += '_' + lowerChar;
}
} else {
out += char;
}
}
if (upperCase) {
return out.toUpperCase();
} else {
return out;
}
}
// snake_case to camelCase converter that simply reverses
// the actions done by `snakeCase` function.
function camelCase(str, upperCase = false) {
if (str.length === 0) {
return str;
}
if (upperCase && isAllUpperCaseSnakeCase(str)) {
// Only convert to lower case if the string is all upper
// case snake_case. This allowes camelCase strings to go
// through without changing.
str = str.toLowerCase();
}
let out = str[0];
for (let i = 1, l = str.length; i < l; ++i) {
const char = str[i];
const prevChar = str[i - 1];
if (char !== '_') {
if (prevChar === '_') {
out += char.toUpperCase();
} else {
out += char;
}
}
}
return out;
}
function isAllUpperCaseSnakeCase(str) {
for (let i = 1, l = str.length; i < l; ++i) {
const char = str[i];
if (char !== '_' && char !== char.toUpperCase()) {
return false;
}
}
return true;
}
// Returns a function that splits the inputs string into pieces using `separator`,
// only calls `mapper` for the last part and concatenates the string back together.
// If no separators are found, `mapper` is called for the entire string.
function mapLastPart(mapper, separator) {
return str => {
const idx = str.lastIndexOf(separator);
const mapped = mapper(str.slice(idx + separator.length));
return str.slice(0, idx + separator.length) + mapped;
};
}
// Returns a function that takes an object as an input and maps the object's keys
// using `mapper`. If the input is not an object, the input is returned unchanged.
function keyMapper(mapper) {
return obj => {
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
return obj;
}
const keys = Object.keys(obj);
const out = {};
for (let i = 0, l = keys.length; i < l; ++i) {
const key = keys[i];
out[mapper(key)] = obj[key];
}
return out;
};
}
function snakeCaseMappers({ upperCase = false } = {}) {
return {
parse: keyMapper(memoize(str => camelCase(str, upperCase))),
format: keyMapper(memoize(str => snakeCase(str, upperCase)))
};
}
function knexIdentifierMappers({ parse, format, idSeparator = ':' } = {}) {
const formatId = memoize(mapLastPart(format, idSeparator));
const parseId = memoize(mapLastPart(parse, idSeparator));
const parseKeys = keyMapper(parseId);
return {
wrapIdentifier(identifier, origWrap) {
return origWrap(formatId(identifier));
},
postProcessResponse(result) {
if (Array.isArray(result)) {
const output = new Array(result.length);
for (let i = 0, l = result.length; i < l; ++i) {
output[i] = parseKeys(result[i]);
}
return output;
} else {
return parseKeys(result);
}
}
};
}
function knexSnakeCaseMappers({ upperCase = false } = {}) {
return knexIdentifierMappers({
parse: str => camelCase(str, upperCase),
format: str => snakeCase(str, upperCase)
});
}
function knexIdentifierMapping(colToProp) {
const propToCol = Object.keys(colToProp).reduce((propToCol, column) => {
propToCol[colToProp[column]] = column;
return propToCol;
}, {});
return knexIdentifierMappers({
parse: column => colToProp[column] || column,
format: prop => propToCol[prop] || prop
});
}
module.exports = {
snakeCase,
camelCase,
snakeCaseMappers,
knexSnakeCaseMappers,
knexIdentifierMappers,
knexIdentifierMapping,
camelCaseKeys: keyMapper(memoize(camelCase)),
snakeCaseKeys: keyMapper(memoize(snakeCase))
};
// tslint:enable
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment