Refactor automatic camel casing of API response fields.
- Remove `humps` lib dependency with a new util function. - Replace hacky way of excluding certain fields in responses with a proper key filtering mechanism.
This commit is contained in:
parent
dd061f56d4
commit
9e9ea0ef15
4 changed files with 76 additions and 38 deletions
1
frontend/package.json
vendored
1
frontend/package.json
vendored
|
@ -16,7 +16,6 @@
|
||||||
"codeflask": "^1.4.1",
|
"codeflask": "^1.4.1",
|
||||||
"core-js": "^3.12.1",
|
"core-js": "^3.12.1",
|
||||||
"dayjs": "^1.10.4",
|
"dayjs": "^1.10.4",
|
||||||
"humps": "^2.0.1",
|
|
||||||
"indent.js": "^0.3.5",
|
"indent.js": "^0.3.5",
|
||||||
"qs": "^6.10.1",
|
"qs": "^6.10.1",
|
||||||
"textversionjs": "^1.1.3",
|
"textversionjs": "^1.1.3",
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { ToastProgrammatic as Toast } from 'buefy';
|
import { ToastProgrammatic as Toast } from 'buefy';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import humps from 'humps';
|
|
||||||
import qs from 'qs';
|
import qs from 'qs';
|
||||||
import store from '../store';
|
import store from '../store';
|
||||||
import { models } from '../constants';
|
import { models } from '../constants';
|
||||||
|
import Utils from '../utils';
|
||||||
|
|
||||||
const http = axios.create({
|
const http = axios.create({
|
||||||
baseURL: process.env.VUE_APP_ROOT_URL || '/',
|
baseURL: process.env.VUE_APP_ROOT_URL || '/',
|
||||||
|
@ -15,6 +15,7 @@ const http = axios.create({
|
||||||
paramsSerializer: (params) => qs.stringify(params, { arrayFormat: 'repeat' }),
|
paramsSerializer: (params) => qs.stringify(params, { arrayFormat: 'repeat' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const utils = new Utils();
|
||||||
|
|
||||||
// Intercept requests to set the 'loading' state of a model.
|
// Intercept requests to set the 'loading' state of a model.
|
||||||
http.interceptors.request.use((config) => {
|
http.interceptors.request.use((config) => {
|
||||||
|
@ -34,28 +35,25 @@ http.interceptors.response.use((resp) => {
|
||||||
|
|
||||||
let data = {};
|
let data = {};
|
||||||
if (typeof resp.data.data === 'object') {
|
if (typeof resp.data.data === 'object') {
|
||||||
data = { ...resp.data.data };
|
if (resp.data.data.constructor === Object) {
|
||||||
if (!resp.config.preserveCase) {
|
data = { ...resp.data.data };
|
||||||
// Transform field case.
|
} else {
|
||||||
data = humps.camelizeKeys(resp.data.data);
|
data = [...resp.data.data];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resp.config.preserveCase && resp.config.preserveResultsCase) {
|
// Transform keys to camelCase.
|
||||||
// For each key in preserveResultsCase, get the values out in an array of arrays
|
switch (typeof resp.config.camelCase) {
|
||||||
// and save them as stringified JSON.
|
case 'function':
|
||||||
const save = resp.data.data.results.map(
|
data = utils.camelKeys(data, resp.config.camelCase);
|
||||||
(r) => resp.config.preserveResultsCase.map((k) => JSON.stringify(r[k])),
|
break;
|
||||||
);
|
case 'boolean':
|
||||||
|
if (resp.config.camelCase) {
|
||||||
// Camelcase everything.
|
data = utils.camelKeys(data);
|
||||||
data = humps.camelizeKeys(resp.data.data);
|
}
|
||||||
|
break;
|
||||||
// Put the saved results back.
|
default:
|
||||||
data.results.forEach((r, n) => {
|
data = utils.camelKeys(data);
|
||||||
resp.config.preserveResultsCase.forEach((k, i) => {
|
break;
|
||||||
data.results[n][k] = JSON.parse(save[n][i]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
data = resp.data.data;
|
data = resp.data.data;
|
||||||
|
@ -136,8 +134,7 @@ export const getSubscribers = async (params) => http.get('/api/subscribers',
|
||||||
params,
|
params,
|
||||||
loading: models.subscribers,
|
loading: models.subscribers,
|
||||||
store: models.subscribers,
|
store: models.subscribers,
|
||||||
preserveCase: true,
|
camelCase: (keyPath) => !keyPath.startsWith('.results.*.attribs'),
|
||||||
preserveResultsCase: ['attribs'],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getSubscriber = async (id) => http.get(`/api/subscribers/${id}`,
|
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 getImportStatus = () => http.get('/api/import/subscribers');
|
||||||
|
|
||||||
export const getImportLogs = async () => http.get('/api/import/subscribers/logs',
|
export const getImportLogs = async () => http.get('/api/import/subscribers/logs',
|
||||||
{ preserveCase: true });
|
{ camelCase: false });
|
||||||
|
|
||||||
export const stopImport = () => http.delete('/api/import/subscribers');
|
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 });
|
{ params, loading: models.bounces });
|
||||||
|
|
||||||
// Campaigns.
|
// Campaigns.
|
||||||
export const getCampaigns = async (params) => http.get('/api/campaigns',
|
export const getCampaigns = async (params) => http.get('/api/campaigns', {
|
||||||
{ params, loading: models.campaigns, store: models.campaigns });
|
params,
|
||||||
|
loading: models.campaigns,
|
||||||
|
store: models.campaigns,
|
||||||
|
camelCase: (keyPath) => !keyPath.startsWith('.results.*.headers'),
|
||||||
|
});
|
||||||
|
|
||||||
export const getCampaign = async (id) => http.get(`/api/campaigns/${id}`,
|
export const getCampaign = async (id) => http.get(`/api/campaigns/${id}`, {
|
||||||
{ loading: models.campaigns });
|
loading: models.campaigns,
|
||||||
|
camelCase: (keyPath) => !keyPath.startsWith('.headers'),
|
||||||
|
});
|
||||||
|
|
||||||
export const getCampaignStats = async () => http.get('/api/campaigns/running/stats', {});
|
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.
|
// Settings.
|
||||||
export const getServerConfig = async () => http.get('/api/config',
|
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',
|
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,
|
export const updateSettings = async (data) => http.put('/api/settings', data,
|
||||||
{ loading: models.settings });
|
{ loading: models.settings });
|
||||||
|
|
||||||
export const getLogs = async () => http.get('/api/logs',
|
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}`,
|
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', {
|
export const logout = async () => http.get('/api/logout', {
|
||||||
auth: { username: 'wrong', password: 'wrong' },
|
auth: { username: 'wrong', password: 'wrong' },
|
||||||
|
|
|
@ -147,4 +147,45 @@ export default class Utils {
|
||||||
// Takes a props.row from a Buefy b-column <td> template and
|
// Takes a props.row from a Buefy b-column <td> template and
|
||||||
// returns a `data-id` attribute which Buefy then applies to the td.
|
// returns a `data-id` attribute which Buefy then applies to the td.
|
||||||
tdID = (row) => ({ 'data-id': row.id.toString() });
|
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;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
5
frontend/yarn.lock
vendored
5
frontend/yarn.lock
vendored
|
@ -5623,11 +5623,6 @@ human-signals@^1.1.1:
|
||||||
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
|
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
|
||||||
integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
|
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:
|
iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.24:
|
||||||
version "0.4.24"
|
version "0.4.24"
|
||||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||||
|
|
Loading…
Reference in a new issue