diff --git a/src/api/ShlinkApiClient.js b/src/api/ShlinkApiClient.js index 6c35b84a..f2439bba 100644 --- a/src/api/ShlinkApiClient.js +++ b/src/api/ShlinkApiClient.js @@ -1,26 +1,18 @@ -import axios from 'axios'; import qs from 'qs'; import { isEmpty, isNil, reject } from 'ramda'; const API_VERSION = '1'; const STATUS_UNAUTHORIZED = 401; +const buildRestUrl = (url) => url ? `${url}/rest/v${API_VERSION}` : ''; -export class ShlinkApiClient { - constructor(axios) { +export default class ShlinkApiClient { + constructor(axios, baseUrl, apiKey) { this.axios = axios; - this._baseUrl = ''; - this._apiKey = ''; + this._baseUrl = buildRestUrl(baseUrl); + this._apiKey = apiKey || ''; this._token = ''; } - /** - * Sets the base URL to be used on any request - */ - setConfig = ({ url, apiKey }) => { - this._baseUrl = `${url}/rest/v${API_VERSION}`; - this._apiKey = apiKey; - }; - listShortUrls = (options = {}) => this._performRequest('/short-codes', 'GET', options) .then((resp) => resp.data.shortUrls) @@ -113,7 +105,3 @@ export class ShlinkApiClient { return Promise.reject(e); }; } - -const shlinkApiClient = new ShlinkApiClient(axios); - -export default shlinkApiClient; diff --git a/src/api/ShlinkApiClientBuilder.js b/src/api/ShlinkApiClientBuilder.js new file mode 100644 index 00000000..23b050c8 --- /dev/null +++ b/src/api/ShlinkApiClientBuilder.js @@ -0,0 +1,18 @@ +import * as axios from 'axios'; +import ShlinkApiClient from './ShlinkApiClient'; + +const apiClients = {}; + +const buildShlinkApiClient = (axios) => ({ url, apiKey }) => { + const clientKey = `${url}_${apiKey}`; + + if (!apiClients[clientKey]) { + apiClients[clientKey] = new ShlinkApiClient(axios, url, apiKey); + } + + return apiClients[clientKey]; +}; + +export default buildShlinkApiClient; + +export const buildShlinkApiClientWithAxios = buildShlinkApiClient(axios); diff --git a/src/container/index.js b/src/container/index.js index 88f8b6a3..88593418 100644 --- a/src/container/index.js +++ b/src/container/index.js @@ -25,7 +25,7 @@ import { ColorGenerator } from '../utils/ColorGenerator'; import { Storage } from '../utils/Storage'; import ShortUrlsRow from '../short-urls/helpers/ShortUrlsRow'; import ShortUrlsRowMenu from '../short-urls/helpers/ShortUrlsRowMenu'; -import { ShlinkApiClient } from '../api/ShlinkApiClient'; +import ShlinkApiClient from '../api/ShlinkApiClient'; import DeleteServerModal from '../servers/DeleteServerModal'; import DeleteServerButton from '../servers/DeleteServerButton'; import AsideMenu from '../common/AsideMenu'; @@ -40,16 +40,17 @@ import DeleteShortUrlModal from '../short-urls/helpers/DeleteShortUrlModal'; import { deleteShortUrl, resetDeleteShortUrl, shortUrlDeleted } from '../short-urls/reducers/shortUrlDeletion'; import EditTagsModal from '../short-urls/helpers/EditTagsModal'; import { editShortUrlTags, resetShortUrlsTags, shortUrlTagsEdited } from '../short-urls/reducers/shortUrlTags'; +import buildShlinkApiClient from '../api/ShlinkApiClientBuilder'; const bottle = new Bottle(); const { container } = bottle; -const mapActionService = (map, actionName) => { - // Wrap actual action service in a function so that it is lazily created the first time it is called - map[actionName] = (...args) => container[actionName](...args); +const mapActionService = (map, actionName) => ({ + ...map, - return map; -}; + // Wrap actual action service in a function so that it is lazily created the first time it is called + [actionName]: (...args) => container[actionName](...args), +}); const connectDecorator = (propsFromState, actionServiceNames) => connect( pick(propsFromState), @@ -144,8 +145,10 @@ bottle.decorator('EditTagsModal', connectDecorator( [ 'editShortUrlTags', 'resetShortUrlsTags', 'shortUrlTagsEdited' ] )); -bottle.serviceFactory('editShortUrlTags', editShortUrlTags, 'ShlinkApiClient'); +bottle.serviceFactory('editShortUrlTags', editShortUrlTags, 'buildShlinkApiClient'); bottle.serviceFactory('resetShortUrlsTags', () => resetShortUrlsTags); bottle.serviceFactory('shortUrlTagsEdited', () => shortUrlTagsEdited); +bottle.serviceFactory('buildShlinkApiClient', buildShlinkApiClient, 'axios'); + export default container; diff --git a/src/servers/reducers/selectedServer.js b/src/servers/reducers/selectedServer.js index eca53474..d53f21c3 100644 --- a/src/servers/reducers/selectedServer.js +++ b/src/servers/reducers/selectedServer.js @@ -1,4 +1,3 @@ -import shlinkApiClient from '../../api/ShlinkApiClient'; import serversService from '../../servers/services/ServersService'; import { resetShortUrlParams } from '../../short-urls/reducers/shortUrlsListParams'; @@ -22,17 +21,15 @@ export default function reducer(state = defaultState, action) { export const resetSelectedServer = () => ({ type: RESET_SELECTED_SERVER }); -export const _selectServer = (shlinkApiClient, serversService) => (serverId) => (dispatch) => { +export const _selectServer = (serversService) => (serverId) => (dispatch) => { dispatch(resetShortUrlParams()); const selectedServer = serversService.findServerById(serverId); - shlinkApiClient.setConfig(selectedServer); - dispatch({ type: SELECT_SERVER, selectedServer, }); }; -export const selectServer = _selectServer(shlinkApiClient, serversService); +export const selectServer = _selectServer(serversService); diff --git a/src/short-urls/reducers/shortUrlCreation.js b/src/short-urls/reducers/shortUrlCreation.js index 9f01a8ee..80150550 100644 --- a/src/short-urls/reducers/shortUrlCreation.js +++ b/src/short-urls/reducers/shortUrlCreation.js @@ -1,6 +1,6 @@ import { curry } from 'ramda'; import PropTypes from 'prop-types'; -import shlinkApiClient from '../../api/ShlinkApiClient'; +import { buildShlinkApiClientWithAxios as buildShlinkApiClient } from '../../api/ShlinkApiClientBuilder'; /* eslint-disable padding-line-between-statements, newline-after-var */ export const CREATE_SHORT_URL_START = 'shlink/createShortUrl/CREATE_SHORT_URL_START'; @@ -50,9 +50,12 @@ export default function reducer(state = defaultState, action) { } } -export const _createShortUrl = (shlinkApiClient, data) => async (dispatch) => { +export const _createShortUrl = (buildShlinkApiClient, data) => async (dispatch, getState) => { dispatch({ type: CREATE_SHORT_URL_START }); + const { selectedServer } = getState(); + const shlinkApiClient = buildShlinkApiClient(selectedServer); + try { const result = await shlinkApiClient.createShortUrl(data); @@ -62,6 +65,6 @@ export const _createShortUrl = (shlinkApiClient, data) => async (dispatch) => { } }; -export const createShortUrl = curry(_createShortUrl)(shlinkApiClient); +export const createShortUrl = curry(_createShortUrl)(buildShlinkApiClient); export const resetCreateShortUrl = () => ({ type: RESET_CREATE_SHORT_URL }); diff --git a/src/short-urls/reducers/shortUrlDeletion.js b/src/short-urls/reducers/shortUrlDeletion.js index 60cdedbe..20812079 100644 --- a/src/short-urls/reducers/shortUrlDeletion.js +++ b/src/short-urls/reducers/shortUrlDeletion.js @@ -1,6 +1,6 @@ import { curry } from 'ramda'; import PropTypes from 'prop-types'; -import shlinkApiClient from '../../api/ShlinkApiClient'; +import { buildShlinkApiClientWithAxios as buildShlinkApiClient } from '../../api/ShlinkApiClientBuilder'; /* eslint-disable padding-line-between-statements, newline-after-var */ const DELETE_SHORT_URL_START = 'shlink/deleteShortUrl/DELETE_SHORT_URL_START'; @@ -56,9 +56,12 @@ export default function reducer(state = defaultState, action) { } } -export const _deleteShortUrl = (shlinkApiClient, shortCode) => async (dispatch) => { +export const _deleteShortUrl = (buildShlinkApiClient, shortCode) => async (dispatch, getState) => { dispatch({ type: DELETE_SHORT_URL_START }); + const { selectedServer } = getState(); + const shlinkApiClient = buildShlinkApiClient(selectedServer); + try { await shlinkApiClient.deleteShortUrl(shortCode); dispatch({ type: DELETE_SHORT_URL, shortCode }); @@ -69,7 +72,7 @@ export const _deleteShortUrl = (shlinkApiClient, shortCode) => async (dispatch) } }; -export const deleteShortUrl = curry(_deleteShortUrl)(shlinkApiClient); +export const deleteShortUrl = curry(_deleteShortUrl)(buildShlinkApiClient); export const resetDeleteShortUrl = () => ({ type: RESET_DELETE_SHORT_URL }); diff --git a/src/short-urls/reducers/shortUrlTags.js b/src/short-urls/reducers/shortUrlTags.js index c1f5646d..a0390a60 100644 --- a/src/short-urls/reducers/shortUrlTags.js +++ b/src/short-urls/reducers/shortUrlTags.js @@ -50,8 +50,10 @@ export default function reducer(state = defaultState, action) { } } -export const editShortUrlTags = (shlinkApiClient) => (shortCode, tags) => async (dispatch) => { +export const editShortUrlTags = (buildShlinkApiClient) => (shortCode, tags) => async (dispatch, getState) => { dispatch({ type: EDIT_SHORT_URL_TAGS_START }); + const { selectedServer } = getState(); + const shlinkApiClient = buildShlinkApiClient(selectedServer); try { const normalizedTags = await shlinkApiClient.updateShortUrlTags(shortCode, tags); diff --git a/src/short-urls/reducers/shortUrlsList.js b/src/short-urls/reducers/shortUrlsList.js index 95e4a7fc..de1df19e 100644 --- a/src/short-urls/reducers/shortUrlsList.js +++ b/src/short-urls/reducers/shortUrlsList.js @@ -1,6 +1,6 @@ import { assoc, assocPath, reject } from 'ramda'; import PropTypes from 'prop-types'; -import shlinkApiClient from '../../api/ShlinkApiClient'; +import { buildShlinkApiClientWithAxios as buildShlinkApiClient } from '../../api/ShlinkApiClientBuilder'; import { SHORT_URL_TAGS_EDITED } from './shortUrlTags'; import { SHORT_URL_DELETED } from './shortUrlDeletion'; @@ -55,9 +55,12 @@ export default function reducer(state = initialState, action) { } } -export const _listShortUrls = (shlinkApiClient, params = {}) => async (dispatch) => { +export const _listShortUrls = (buildShlinkApiClient, params = {}) => async (dispatch, getState) => { dispatch({ type: LIST_SHORT_URLS_START }); + const { selectedServer } = getState(); + const shlinkApiClient = buildShlinkApiClient(selectedServer); + try { const shortUrls = await shlinkApiClient.listShortUrls(params); @@ -67,4 +70,4 @@ export const _listShortUrls = (shlinkApiClient, params = {}) => async (dispatch) } }; -export const listShortUrls = (params = {}) => _listShortUrls(shlinkApiClient, params); +export const listShortUrls = (params = {}) => _listShortUrls(buildShlinkApiClient, params); diff --git a/src/tags/reducers/tagDelete.js b/src/tags/reducers/tagDelete.js index dfd2bf4a..e4ea9410 100644 --- a/src/tags/reducers/tagDelete.js +++ b/src/tags/reducers/tagDelete.js @@ -1,6 +1,6 @@ import { curry } from 'ramda'; import PropTypes from 'prop-types'; -import shlinkApiClient from '../../api/ShlinkApiClient'; +import { buildShlinkApiClientWithAxios as buildShlinkApiClient } from '../../api/ShlinkApiClientBuilder'; /* eslint-disable padding-line-between-statements, newline-after-var */ export const DELETE_TAG_START = 'shlink/deleteTag/DELETE_TAG_START'; @@ -41,9 +41,12 @@ export default function reducer(state = defaultState, action) { } } -export const _deleteTag = (shlinkApiClient, tag) => async (dispatch) => { +export const _deleteTag = (buildShlinkApiClient, tag) => async (dispatch, getState) => { dispatch({ type: DELETE_TAG_START }); + const { selectedServer } = getState(); + const shlinkApiClient = buildShlinkApiClient(selectedServer); + try { await shlinkApiClient.deleteTags([ tag ]); dispatch({ type: DELETE_TAG }); @@ -54,6 +57,6 @@ export const _deleteTag = (shlinkApiClient, tag) => async (dispatch) => { } }; -export const deleteTag = curry(_deleteTag)(shlinkApiClient); +export const deleteTag = curry(_deleteTag)(buildShlinkApiClient); export const tagDeleted = (tag) => ({ type: TAG_DELETED, tag }); diff --git a/src/tags/reducers/tagEdit.js b/src/tags/reducers/tagEdit.js index 650f0ba1..53bb25c4 100644 --- a/src/tags/reducers/tagEdit.js +++ b/src/tags/reducers/tagEdit.js @@ -1,5 +1,5 @@ import { curry, pick } from 'ramda'; -import shlinkApiClient from '../../api/ShlinkApiClient'; +import { buildShlinkApiClientWithAxios as buildShlinkApiClient } from '../../api/ShlinkApiClientBuilder'; import colorGenerator from '../../utils/ColorGenerator'; /* eslint-disable padding-line-between-statements, newline-after-var */ @@ -42,9 +42,15 @@ export default function reducer(state = defaultState, action) { } } -export const _editTag = (shlinkApiClient, colorGenerator, oldName, newName, color) => async (dispatch) => { +export const _editTag = (buildShlinkApiClient, colorGenerator, oldName, newName, color) => async ( + dispatch, + getState +) => { dispatch({ type: EDIT_TAG_START }); + const { selectedServer } = getState(); + const shlinkApiClient = buildShlinkApiClient(selectedServer); + try { await shlinkApiClient.editTag(oldName, newName); colorGenerator.setColorForKey(newName, color); @@ -56,7 +62,7 @@ export const _editTag = (shlinkApiClient, colorGenerator, oldName, newName, colo } }; -export const editTag = curry(_editTag)(shlinkApiClient, colorGenerator); +export const editTag = curry(_editTag)(buildShlinkApiClient, colorGenerator); export const tagEdited = (oldName, newName, color) => ({ type: TAG_EDITED, diff --git a/src/tags/reducers/tagsList.js b/src/tags/reducers/tagsList.js index cb415902..94338266 100644 --- a/src/tags/reducers/tagsList.js +++ b/src/tags/reducers/tagsList.js @@ -1,5 +1,5 @@ import { isEmpty, reject } from 'ramda'; -import shlinkApiClient from '../../api/ShlinkApiClient'; +import { buildShlinkApiClientWithAxios as buildShlinkApiClient } from '../../api/ShlinkApiClientBuilder'; import { TAG_DELETED } from './tagDelete'; import { TAG_EDITED } from './tagEdit'; @@ -66,8 +66,8 @@ export default function reducer(state = defaultState, action) { } } -export const _listTags = (shlinkApiClient, force = false) => async (dispatch, getState) => { - const { tagsList } = getState(); +export const _listTags = (buildShlinkApiClient, force = false) => async (dispatch, getState) => { + const { tagsList, selectedServer } = getState(); if (!force && (tagsList.loading || !isEmpty(tagsList.tags))) { return; @@ -76,6 +76,7 @@ export const _listTags = (shlinkApiClient, force = false) => async (dispatch, ge dispatch({ type: LIST_TAGS_START }); try { + const shlinkApiClient = buildShlinkApiClient(selectedServer); const tags = await shlinkApiClient.listTags(); dispatch({ tags, type: LIST_TAGS }); @@ -84,9 +85,9 @@ export const _listTags = (shlinkApiClient, force = false) => async (dispatch, ge } }; -export const listTags = () => _listTags(shlinkApiClient); +export const listTags = () => _listTags(buildShlinkApiClient); -export const forceListTags = () => _listTags(shlinkApiClient, true); +export const forceListTags = () => _listTags(buildShlinkApiClient, true); export const filterTags = (searchTerm) => ({ type: FILTER_TAGS, diff --git a/src/visits/reducers/shortUrlDetail.js b/src/visits/reducers/shortUrlDetail.js index cb86e040..b62c99ce 100644 --- a/src/visits/reducers/shortUrlDetail.js +++ b/src/visits/reducers/shortUrlDetail.js @@ -1,6 +1,6 @@ import { curry } from 'ramda'; import PropTypes from 'prop-types'; -import shlinkApiClient from '../../api/ShlinkApiClient'; +import { buildShlinkApiClientWithAxios as buildShlinkApiClient } from '../../api/ShlinkApiClientBuilder'; import { shortUrlType } from '../../short-urls/reducers/shortUrlsList'; /* eslint-disable padding-line-between-statements, newline-after-var */ @@ -45,9 +45,12 @@ export default function reducer(state = initialState, action) { } } -export const _getShortUrlDetail = (shlinkApiClient, shortCode) => async (dispatch) => { +export const _getShortUrlDetail = (buildShlinkApiClient, shortCode) => async (dispatch, getState) => { dispatch({ type: GET_SHORT_URL_DETAIL_START }); + const { selectedServer } = getState(); + const shlinkApiClient = buildShlinkApiClient(selectedServer); + try { const shortUrl = await shlinkApiClient.getShortUrl(shortCode); @@ -57,4 +60,4 @@ export const _getShortUrlDetail = (shlinkApiClient, shortCode) => async (dispatc } }; -export const getShortUrlDetail = curry(_getShortUrlDetail)(shlinkApiClient); +export const getShortUrlDetail = curry(_getShortUrlDetail)(buildShlinkApiClient); diff --git a/src/visits/reducers/shortUrlVisits.js b/src/visits/reducers/shortUrlVisits.js index 1bb724ab..4df1a09c 100644 --- a/src/visits/reducers/shortUrlVisits.js +++ b/src/visits/reducers/shortUrlVisits.js @@ -1,6 +1,6 @@ import { curry } from 'ramda'; import PropTypes from 'prop-types'; -import shlinkApiClient from '../../api/ShlinkApiClient'; +import { buildShlinkApiClientWithAxios as buildShlinkApiClient } from '../../api/ShlinkApiClientBuilder'; /* eslint-disable padding-line-between-statements, newline-after-var */ export const GET_SHORT_URL_VISITS_START = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_START'; @@ -44,9 +44,12 @@ export default function reducer(state = initialState, action) { } } -export const _getShortUrlVisits = (shlinkApiClient, shortCode, dates) => async (dispatch) => { +export const _getShortUrlVisits = (buildShlinkApiClient, shortCode, dates) => async (dispatch, getState) => { dispatch({ type: GET_SHORT_URL_VISITS_START }); + const { selectedServer } = getState(); + const shlinkApiClient = buildShlinkApiClient(selectedServer); + try { const visits = await shlinkApiClient.getShortUrlVisits(shortCode, dates); @@ -56,4 +59,4 @@ export const _getShortUrlVisits = (shlinkApiClient, shortCode, dates) => async ( } }; -export const getShortUrlVisits = curry(_getShortUrlVisits)(shlinkApiClient); +export const getShortUrlVisits = curry(_getShortUrlVisits)(buildShlinkApiClient); diff --git a/test/api/ShlinkApiClient.test.js b/test/api/ShlinkApiClient.test.js index 68894fb0..02935f1d 100644 --- a/test/api/ShlinkApiClient.test.js +++ b/test/api/ShlinkApiClient.test.js @@ -1,6 +1,6 @@ import sinon from 'sinon'; import { head, last } from 'ramda'; -import { ShlinkApiClient } from '../../src/api/ShlinkApiClient'; +import ShlinkApiClient from '../../src/api/ShlinkApiClient'; describe('ShlinkApiClient', () => { const createAxiosMock = (extraData) => () =>