From aaa78bc5340f4094c806846d04b36e9cf8322fab Mon Sep 17 00:00:00 2001 From: Adam Grieger Date: Sat, 27 Jun 2020 13:07:13 -0700 Subject: [PATCH] fix: stringify array query parameters using the comma format The default format that 'axios' uses for stringifying array query parameters doesn't match what Spotify's API endpoints expect. --- package.json | 4 +++- src/apis/AlbumsApi.spec.ts | 4 ++-- src/apis/AlbumsApi.ts | 2 +- src/helpers/getAuthorizationUrl.spec.ts | 10 +++++----- src/helpers/getAuthorizationUrl.ts | 7 +++---- src/helpers/spotifyAxios.spec.ts | 10 +++++++++- src/helpers/spotifyAxios.ts | 8 +++++++- src/helpers/stringifyParams.spec.ts | 20 -------------------- src/helpers/stringifyParams.ts | 5 ----- src/index.ts | 8 ++++---- yarn.lock | 10 ++++++++++ 11 files changed, 44 insertions(+), 44 deletions(-) delete mode 100644 src/helpers/stringifyParams.spec.ts delete mode 100644 src/helpers/stringifyParams.ts diff --git a/package.json b/package.json index 8c6d04f..363c48d 100644 --- a/package.json +++ b/package.json @@ -32,10 +32,12 @@ }, "devDependencies": { "@spotify/web-scripts": "^7.0.1", + "@types/qs": "^6.9.3", "husky": "^4.2.5", "typedoc": "^0.17.7" }, "dependencies": { - "axios": "^0.19.2" + "axios": "^0.19.2", + "qs": "^6.9.4" } } diff --git a/src/apis/AlbumsApi.spec.ts b/src/apis/AlbumsApi.spec.ts index 4944b81..e914001 100644 --- a/src/apis/AlbumsApi.spec.ts +++ b/src/apis/AlbumsApi.spec.ts @@ -63,7 +63,7 @@ describe('AlbumsApi', () => { expect(response).toEqual(getAlbumsFixture.albums); expect(httpMock.get).toBeCalledWith('/albums', { params: { - ids: 'foo,bar', + ids: ['foo', 'bar'], }, }); }); @@ -78,7 +78,7 @@ describe('AlbumsApi', () => { expect(response).toEqual(getAlbumsFixture.albums); expect(httpMock.get).toBeCalledWith('/albums', { params: { - ids: 'foo,bar', + ids: ['foo', 'bar'], market: 'baz', }, }); diff --git a/src/apis/AlbumsApi.ts b/src/apis/AlbumsApi.ts index 8e5065a..9b7935f 100644 --- a/src/apis/AlbumsApi.ts +++ b/src/apis/AlbumsApi.ts @@ -61,7 +61,7 @@ export class AlbumsApi { const response = await this.http.get('/albums', { params: { ...options, - ids: albumIds.join(','), + ids: albumIds, }, }); return response.albums; diff --git a/src/helpers/getAuthorizationUrl.spec.ts b/src/helpers/getAuthorizationUrl.spec.ts index 9f6ee12..f5b40c9 100644 --- a/src/helpers/getAuthorizationUrl.spec.ts +++ b/src/helpers/getAuthorizationUrl.spec.ts @@ -18,12 +18,12 @@ describe('getAuthorizationUrl', () => { state: 'baz', }), ).toBe( - AUTHORIZE_URL.concat('?client_id=foo') - .concat('&redirect_uri=bar') - .concat('&response_type=token') - .concat('&scope=streaming%20app-remote-control') + AUTHORIZE_URL.concat('?scope=streaming%20app-remote-control') .concat('&show_dialog=true') - .concat('&state=baz'), + .concat('&state=baz') + .concat('&client_id=foo') + .concat('&redirect_uri=bar') + .concat('&response_type=token'), ); }); }); diff --git a/src/helpers/getAuthorizationUrl.ts b/src/helpers/getAuthorizationUrl.ts index 697bdd7..b45815e 100644 --- a/src/helpers/getAuthorizationUrl.ts +++ b/src/helpers/getAuthorizationUrl.ts @@ -1,6 +1,6 @@ +import qs from 'qs'; import { AUTHORIZE_URL } from '../constants'; import { AuthorizationScope } from '../types/SpotifyAuthorization'; -import { stringifyParams } from './stringifyParams'; export type GetAuthorizationUrlOptions = { scope?: AuthorizationScope[]; @@ -14,12 +14,11 @@ export function getAuthorizationUrl( responseType: 'code' | 'token', options?: GetAuthorizationUrlOptions, ) { - return `${AUTHORIZE_URL}?${stringifyParams({ + return `${AUTHORIZE_URL}?${qs.stringify({ + ...options, client_id: clientId, redirect_uri: redirectUri, response_type: responseType, ...(options?.scope && { scope: options.scope.join(' ') }), - ...(options?.show_dialog && { show_dialog: options.show_dialog }), - ...(options?.state && { state: options.state }), })}`; } diff --git a/src/helpers/spotifyAxios.spec.ts b/src/helpers/spotifyAxios.spec.ts index af72635..161b120 100644 --- a/src/helpers/spotifyAxios.spec.ts +++ b/src/helpers/spotifyAxios.spec.ts @@ -1,6 +1,6 @@ import axios from 'axios'; -import { spotifyAxios } from './spotifyAxios'; import { BASE_API_URL } from '../constants'; +import { paramsSerializer, spotifyAxios } from './spotifyAxios'; jest.mock('axios'); @@ -27,6 +27,7 @@ describe('spotifyAxios', () => { Authorization: 'Bearer token', 'Content-Type': 'application/json', }, + paramsSerializer, url: 'foo', method: 'GET', }); @@ -45,6 +46,7 @@ describe('spotifyAxios', () => { Authorization: 'Bearer token', 'Content-Type': 'image/jpeg', }, + paramsSerializer, url: 'foo', method: 'GET', }); @@ -56,3 +58,9 @@ describe('spotifyAxios', () => { await expect(spotifyAxios('bar', 'GET', 'token')).rejects.toThrow('foo'); }); }); + +describe('paramsSerializer', () => { + it('should stringify arrays using the comma format', () => { + expect(paramsSerializer({ foo: ['bar', 'baz'] })).toEqual('foo=bar%2Cbaz'); + }); +}); diff --git a/src/helpers/spotifyAxios.ts b/src/helpers/spotifyAxios.ts index 6136432..0aaf816 100644 --- a/src/helpers/spotifyAxios.ts +++ b/src/helpers/spotifyAxios.ts @@ -1,4 +1,5 @@ -import axios, { Method, AxiosRequestConfig, AxiosError } from 'axios'; +import axios, { AxiosError, AxiosRequestConfig, Method } from 'axios'; +import qs from 'qs'; import { BASE_API_URL } from '../constants'; export type SpotifyAxiosConfig = AxiosRequestConfig & { contentType?: string }; @@ -18,6 +19,7 @@ export async function spotifyAxios( Authorization: `Bearer ${accessToken}`, 'Content-Type': contentType ?? 'application/json', }, + paramsSerializer, url, method, }); @@ -28,3 +30,7 @@ export async function spotifyAxios( throw new Error(err.message); } } + +export function paramsSerializer(params: any) { + return qs.stringify(params, { arrayFormat: 'comma' }); +} diff --git a/src/helpers/stringifyParams.spec.ts b/src/helpers/stringifyParams.spec.ts deleted file mode 100644 index 1430079..0000000 --- a/src/helpers/stringifyParams.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { stringifyParams } from './stringifyParams'; - -describe('stringifyParams', () => { - it('should return an empty string when given an empty object', () => { - expect(stringifyParams({})).toEqual(''); - }); - - it('should stringify an object with one key', () => { - expect(stringifyParams({ fooKey: 'fooValue' })).toEqual('fooKey=fooValue'); - }); - - it('should stringify an object with multiple keys', () => { - expect( - stringifyParams({ - fooKey: 'fooValue', - barKey: 'barValue', - }), - ).toEqual('fooKey=fooValue&barKey=barValue'); - }); -}); diff --git a/src/helpers/stringifyParams.ts b/src/helpers/stringifyParams.ts deleted file mode 100644 index d8ec3e2..0000000 --- a/src/helpers/stringifyParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -export function stringifyParams(params: Record) { - return Object.entries(params) - .map(([key, value]) => `${key}=${encodeURIComponent(value)}`) - .join('&'); -} diff --git a/src/index.ts b/src/index.ts index b1bd93b..1279574 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ import axios from 'axios'; +import qs from 'qs'; import { AlbumsApi } from './apis/AlbumsApi'; import { ArtistsApi } from './apis/ArtistsApi'; import { BrowseApi } from './apis/BrowseApi'; @@ -24,7 +25,6 @@ import { GetRefreshedAccessTokenResponse, GetTemporaryAppTokensResponse, } from './types/SpotifyAuthorization'; -import { stringifyParams } from './helpers/stringifyParams'; type SpotifyWebApiOptions = { accessToken?: string; @@ -150,7 +150,7 @@ export class SpotifyWebApi { ): Promise { const response = await axios.post( TOKEN_URL, - stringifyParams({ + qs.stringify({ code, grant_type: 'authorization_code', redirect_uri: this.redirectUri, @@ -179,7 +179,7 @@ export class SpotifyWebApi { ): Promise { const response = await axios.post( TOKEN_URL, - stringifyParams({ + qs.stringify({ grant_type: 'refresh_token', refresh_token: refreshToken, }), @@ -210,7 +210,7 @@ export class SpotifyWebApi { async getTemporaryAppTokens(): Promise { const response = await axios.post( TOKEN_URL, - stringifyParams({ + qs.stringify({ grant_type: 'client_credentials', }), { diff --git a/yarn.lock b/yarn.lock index 877cf11..edaa4f0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -907,6 +907,11 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== +"@types/qs@^6.9.3": + version "6.9.3" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.3.tgz#b755a0934564a200d3efdf88546ec93c369abd03" + integrity sha512-7s9EQWupR1fTc2pSMtXRQ9w9gLOcrJn+h7HOXw4evxyvVqMi4f+q7d2tnFe3ng3SNHjtK+0EzGMGFUQX4/AQRA== + "@types/react-dom@^16.8.4": version "16.9.6" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.6.tgz#9e7f83d90566521cc2083be2277c6712dcaf754c" @@ -6674,6 +6679,11 @@ qrcode-terminal@^0.12.0: resolved "https://registry.yarnpkg.com/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz#bb5b699ef7f9f0505092a3748be4464fe71b5819" integrity sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ== +qs@^6.9.4: + version "6.9.4" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.4.tgz#9090b290d1f91728d3c22e54843ca44aea5ab687" + integrity sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ== + qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"