diff --git a/frontend/package.json b/frontend/package.json index 60e40b9..fce5238 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,7 +16,6 @@ "codeflask": "^1.4.1", "core-js": "^3.12.1", "dayjs": "^1.10.4", - "humps": "^2.0.1", "indent.js": "^0.3.5", "qs": "^6.10.1", "textversionjs": "^1.1.3", diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index 4caf513..41dff58 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -1,9 +1,9 @@ import { ToastProgrammatic as Toast } from 'buefy'; import axios from 'axios'; -import humps from 'humps'; import qs from 'qs'; import store from '../store'; import { models } from '../constants'; +import Utils from '../utils'; const http = axios.create({ baseURL: process.env.VUE_APP_ROOT_URL || '/', @@ -15,6 +15,7 @@ const http = axios.create({ paramsSerializer: (params) => qs.stringify(params, { arrayFormat: 'repeat' }), }); +const utils = new Utils(); // Intercept requests to set the 'loading' state of a model. http.interceptors.request.use((config) => { @@ -34,28 +35,25 @@ http.interceptors.response.use((resp) => { let data = {}; if (typeof resp.data.data === 'object') { - data = { ...resp.data.data }; - if (!resp.config.preserveCase) { - // Transform field case. - data = humps.camelizeKeys(resp.data.data); + if (resp.data.data.constructor === Object) { + data = { ...resp.data.data }; + } else { + data = [...resp.data.data]; } - if (resp.config.preserveCase && resp.config.preserveResultsCase) { - // For each key in preserveResultsCase, get the values out in an array of arrays - // and save them as stringified JSON. - const save = resp.data.data.results.map( - (r) => resp.config.preserveResultsCase.map((k) => JSON.stringify(r[k])), - ); - - // Camelcase everything. - data = humps.camelizeKeys(resp.data.data); - - // Put the saved results back. - data.results.forEach((r, n) => { - resp.config.preserveResultsCase.forEach((k, i) => { - data.results[n][k] = JSON.parse(save[n][i]); - }); - }); + // Transform keys to camelCase. + switch (typeof resp.config.camelCase) { + case 'function': + data = utils.camelKeys(data, resp.config.camelCase); + break; + case 'boolean': + if (resp.config.camelCase) { + data = utils.camelKeys(data); + } + break; + default: + data = utils.camelKeys(data); + break; } } else { data = resp.data.data; @@ -136,8 +134,7 @@ export const getSubscribers = async (params) => http.get('/api/subscribers', params, loading: models.subscribers, store: models.subscribers, - preserveCase: true, - preserveResultsCase: ['attribs'], + camelCase: (keyPath) => !keyPath.startsWith('.results.*.attribs'), }); export const getSubscriber = async (id) => http.get(`/api/subscribers/${id}`, @@ -188,7 +185,7 @@ export const importSubscribers = (data) => http.post('/api/import/subscribers', export const getImportStatus = () => http.get('/api/import/subscribers'); export const getImportLogs = async () => http.get('/api/import/subscribers/logs', - { preserveCase: true }); + { camelCase: false }); export const stopImport = () => http.delete('/api/import/subscribers'); @@ -197,11 +194,17 @@ export const getBounces = async (params) => http.get('/api/bounces', { params, loading: models.bounces }); // Campaigns. -export const getCampaigns = async (params) => http.get('/api/campaigns', - { params, loading: models.campaigns, store: models.campaigns }); +export const getCampaigns = async (params) => http.get('/api/campaigns', { + params, + loading: models.campaigns, + store: models.campaigns, + camelCase: (keyPath) => !keyPath.startsWith('.results.*.headers'), +}); -export const getCampaign = async (id) => http.get(`/api/campaigns/${id}`, - { loading: models.campaigns }); +export const getCampaign = async (id) => http.get(`/api/campaigns/${id}`, { + loading: models.campaigns, + camelCase: (keyPath) => !keyPath.startsWith('.headers'), +}); export const getCampaignStats = async () => http.get('/api/campaigns/running/stats', {}); @@ -263,19 +266,19 @@ export const deleteTemplate = async (id) => http.delete(`/api/templates/${id}`, // Settings. export const getServerConfig = async () => http.get('/api/config', - { loading: models.serverConfig, store: models.serverConfig, preserveCase: true }); + { loading: models.serverConfig, store: models.serverConfig, camelCase: false }); export const getSettings = async () => http.get('/api/settings', - { loading: models.settings, store: models.settings, preserveCase: true }); + { loading: models.settings, store: models.settings, camelCase: false }); export const updateSettings = async (data) => http.put('/api/settings', data, { loading: models.settings }); export const getLogs = async () => http.get('/api/logs', - { loading: models.logs }); + { loading: models.logs, camelCase: false }); export const getLang = async (lang) => http.get(`/api/lang/${lang}`, - { loading: models.lang, preserveCase: true }); + { loading: models.lang, camelCase: false }); export const logout = async () => http.get('/api/logout', { auth: { username: 'wrong', password: 'wrong' }, diff --git a/frontend/src/utils.js b/frontend/src/utils.js index 2a7652d..a88188e 100644 --- a/frontend/src/utils.js +++ b/frontend/src/utils.js @@ -147,4 +147,45 @@ export default class Utils { // Takes a props.row from a Buefy b-column template and // returns a `data-id` attribute which Buefy then applies to the td. tdID = (row) => ({ 'data-id': row.id.toString() }); + + camelString = (str) => { + const s = str.replace(/[-_\s]+(.)?/g, (match, chr) => (chr ? chr.toUpperCase() : '')); + return s.substr(0, 1).toLowerCase() + s.substr(1); + } + + // camelKeys recursively camelCases all keys in a given object (array or {}). + // For each key it traverses, it passes a dot separated key path to an optional testFunc() bool. + // so that it can camelcase or leave a particular key alone based on what testFunc() returns. + // eg: The keypath for {"data": {"results": ["created_at": 123]}} is + // .data.results.*.created_at (array indices become *) + // testFunc() can examine this key and return true to convert it to camelcase + // or false to leave it as-is. + camelKeys = (obj, testFunc, keys) => { + if (obj === null) { + return obj; + } + + if (Array.isArray(obj)) { + return obj.map((o) => this.camelKeys(o, testFunc, `${keys || ''}.*`)); + } + + if (obj.constructor === Object) { + return Object.keys(obj).reduce((result, key) => { + const keyPath = `${keys || ''}.${key}`; + let k = key; + + // If there's no testfunc or if a function is defined and it returns true, convert. + if (testFunc === undefined || testFunc(keyPath)) { + k = this.camelString(key); + } + + return { + ...result, + [k]: this.camelKeys(obj[key], testFunc, keyPath), + }; + }, {}); + } + + return obj; + }; } diff --git a/frontend/yarn.lock b/frontend/yarn.lock index b916838..c62d635 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -5623,11 +5623,6 @@ human-signals@^1.1.1: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== -humps@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/humps/-/humps-2.0.1.tgz#dd02ea6081bd0568dc5d073184463957ba9ef9aa" - integrity sha1-3QLqYIG9BWjcXQcxhEY5V7qe+ao= - iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"