identifierMapping.js 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. // Super fast memoize for single argument functions.
  2. function memoize(func) {
  3. const cache = new Map();
  4. return input => {
  5. let output = cache.get(input);
  6. if (output === undefined) {
  7. output = func(input);
  8. cache.set(input, output);
  9. }
  10. return output;
  11. };
  12. }
  13. // camelCase to snake_case converter that also works with
  14. // non-ascii characters.
  15. function snakeCase(str) {
  16. if (str.length === 0) {
  17. return str;
  18. }
  19. const upper = str.toUpperCase();
  20. const lower = str.toLowerCase();
  21. let out = lower[0];
  22. for (let i = 1, l = str.length; i < l; ++i) {
  23. const char = str[i];
  24. const prevChar = str[i - 1];
  25. const upperChar = upper[i];
  26. const prevUpperChar = upper[i - 1];
  27. const lowerChar = lower[i];
  28. const prevLowerChar = lower[i - 1];
  29. // Test if `char` is an upper-case character and that the character
  30. // actually has different upper and lower case versions.
  31. if (char === upperChar && upperChar !== lowerChar) {
  32. // Multiple consecutive upper case characters shouldn't add underscores.
  33. // For example "fooBAR" should be converted to "foo_bar".
  34. if (prevChar === prevUpperChar && prevUpperChar !== prevLowerChar) {
  35. out += lowerChar;
  36. } else {
  37. out += '_' + lowerChar;
  38. }
  39. } else {
  40. out += char;
  41. }
  42. }
  43. return out;
  44. }
  45. // snake_case to camelCase converter that simply reverses
  46. // the actions done by `snakeCase` function.
  47. function camelCase(str) {
  48. if (str.length === 0) {
  49. return str;
  50. }
  51. let out = str[0];
  52. for (let i = 1, l = str.length; i < l; ++i) {
  53. const char = str[i];
  54. const prevChar = str[i - 1];
  55. if (char !== '_') {
  56. if (prevChar === '_') {
  57. out += char.toUpperCase();
  58. } else {
  59. out += char;
  60. }
  61. }
  62. }
  63. return out;
  64. }
  65. // Returns a function that splits the inputs string into pieces using `separator`,
  66. // only calls `mapper` for the last part and concatenates the string back together.
  67. // If no separators are found, `mapper` is called for the entire string.
  68. function mapLastPart(mapper, separator) {
  69. return str => {
  70. const idx = str.lastIndexOf(separator);
  71. const mapped = mapper(str.slice(idx + separator.length));
  72. return str.slice(0, idx + separator.length) + mapped;
  73. };
  74. }
  75. // Returns a function that takes an object as an input and maps the object's keys
  76. // using `mapper`. If the input is not an object, the input is returned unchanged.
  77. function keyMapper(mapper) {
  78. return obj => {
  79. if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
  80. return obj;
  81. }
  82. const keys = Object.keys(obj);
  83. const out = {};
  84. for (let i = 0, l = keys.length; i < l; ++i) {
  85. const key = keys[i];
  86. out[mapper(key)] = obj[key];
  87. }
  88. return out;
  89. };
  90. }
  91. function snakeCaseMappers() {
  92. return {
  93. parse: keyMapper(memoize(camelCase)),
  94. format: keyMapper(memoize(snakeCase))
  95. };
  96. }
  97. function knexIdentifierMappers({ parse, format, idSeparator = ':' } = {}) {
  98. const formatId = memoize(mapLastPart(format, idSeparator));
  99. const parseId = memoize(mapLastPart(parse, idSeparator));
  100. const parseKeys = keyMapper(parseId);
  101. return {
  102. wrapIdentifier(identifier, origWrap) {
  103. return origWrap(formatId(identifier));
  104. },
  105. postProcessResponse(result) {
  106. if (Array.isArray(result)) {
  107. const output = new Array(result.length);
  108. for (let i = 0, l = result.length; i < l; ++i) {
  109. output[i] = parseKeys(result[i]);
  110. }
  111. return output;
  112. } else {
  113. return parseKeys(result);
  114. }
  115. }
  116. };
  117. }
  118. function knexSnakeCaseMappers() {
  119. return knexIdentifierMappers({
  120. parse: camelCase,
  121. format: snakeCase
  122. });
  123. }
  124. function knexIdentifierMapping(colToProp) {
  125. const propToCol = Object.keys(colToProp).reduce((propToCol, column) => {
  126. propToCol[colToProp[column]] = column;
  127. return propToCol;
  128. }, {});
  129. return knexIdentifierMappers({
  130. parse: column => colToProp[column] || column,
  131. format: prop => propToCol[prop] || prop
  132. });
  133. }
  134. module.exports = {
  135. snakeCase,
  136. camelCase,
  137. snakeCaseMappers,
  138. knexSnakeCaseMappers,
  139. knexIdentifierMappers,
  140. knexIdentifierMapping,
  141. camelCaseKeys: keyMapper(memoize(camelCase)),
  142. snakeCaseKeys: keyMapper(memoize(snakeCase))
  143. };