// 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) { 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; } } return out; } // snake_case to camelCase converter that simply reverses // the actions done by `snakeCase` function. function camelCase(str) { if (str.length === 0) { return str; } 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; } // 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() { return { parse: keyMapper(memoize(camelCase)), format: keyMapper(memoize(snakeCase)) }; } 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() { return knexIdentifierMappers({ parse: camelCase, format: snakeCase }); } 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)) };