From 80f0b9bae6f637946e53d5ada8e3ecccc537828e Mon Sep 17 00:00:00 2001 From: Omer Lachish Date: Mon, 8 Apr 2019 08:26:21 +0300 Subject: [PATCH 01/10] export object from query-string --- client/app/components/queries/visualization-embed.js | 2 +- client/app/services/query-string.js | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/client/app/components/queries/visualization-embed.js b/client/app/components/queries/visualization-embed.js index 0e36ec0fe3..5d5678ad15 100644 --- a/client/app/components/queries/visualization-embed.js +++ b/client/app/components/queries/visualization-embed.js @@ -38,7 +38,7 @@ export default function init(ngModule) { return session($http, $route, Auth).then(() => { const queryId = $route.current.params.queryId; const query = $http.get(`api/queries/${queryId}`).then(response => response.data); - const queryResult = $http.post(`api/queries/${queryId}/results`, { parameters: queryStringParameters() }).then(response => response.data); + const queryResult = $http.post(`api/queries/${queryId}/results`, { parameters: queryStringParameters.fromUrl() }).then(response => response.data); return $q.all([query, queryResult]); }); } diff --git a/client/app/services/query-string.js b/client/app/services/query-string.js index 4f67c0494c..a2f8c852a1 100644 --- a/client/app/services/query-string.js +++ b/client/app/services/query-string.js @@ -5,7 +5,9 @@ const onlyParameters = ([k]) => k.startsWith('p_'); const removePrefix = ([k, v]) => [k.slice(2), v]; const toObject = (obj, [k, v]) => Object.assign(obj, { [k]: v }); -export default () => Object.entries(parse(location.search)) - .filter(onlyParameters) - .map(removePrefix) - .reduce(toObject, {}); +export default { + fromUrl: () => Object.entries(parse(location.search)) + .filter(onlyParameters) + .map(removePrefix) + .reduce(toObject, {}), +}; From 3b2053ed5651328cb8130449ba6c8d8f2a498c40 Mon Sep 17 00:00:00 2001 From: Omer Lachish Date: Mon, 8 Apr 2019 10:22:56 +0300 Subject: [PATCH 02/10] add a couple of tests to query-string.js --- client/app/services/query-string.js | 13 +++++++++---- client/app/services/query-string.test.js | 9 +++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 client/app/services/query-string.test.js diff --git a/client/app/services/query-string.js b/client/app/services/query-string.js index a2f8c852a1..30c2f745c3 100644 --- a/client/app/services/query-string.js +++ b/client/app/services/query-string.js @@ -5,9 +5,14 @@ const onlyParameters = ([k]) => k.startsWith('p_'); const removePrefix = ([k, v]) => [k.slice(2), v]; const toObject = (obj, [k, v]) => Object.assign(obj, { [k]: v }); +const fromString = queryString => Object.entries(parse(queryString)) + .filter(onlyParameters) + .map(removePrefix) + .reduce(toObject, {}); + +const fromUrl = () => fromString(location.search); + export default { - fromUrl: () => Object.entries(parse(location.search)) - .filter(onlyParameters) - .map(removePrefix) - .reduce(toObject, {}), + fromString, + fromUrl, }; diff --git a/client/app/services/query-string.test.js b/client/app/services/query-string.test.js new file mode 100644 index 0000000000..3ca1bf7f95 --- /dev/null +++ b/client/app/services/query-string.test.js @@ -0,0 +1,9 @@ +import queryString from '@/services/query-string'; + +it('removes prefixes', () => { + expect(queryString.fromString('p_a=b')).toHaveProperty('a'); +}); + +it('ignores non-parameters', () => { + expect(queryString.fromString('p_a=b&utm=123')).not.toHaveProperty('utm'); +}); From 6cf862023b57108589a19fc4c2bbe75047720e37 Mon Sep 17 00:00:00 2001 From: Omer Lachish Date: Mon, 8 Apr 2019 10:44:51 +0300 Subject: [PATCH 03/10] put tests inside a describe block --- client/app/services/query-string.test.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/client/app/services/query-string.test.js b/client/app/services/query-string.test.js index 3ca1bf7f95..6f635f042c 100644 --- a/client/app/services/query-string.test.js +++ b/client/app/services/query-string.test.js @@ -1,9 +1,12 @@ import queryString from '@/services/query-string'; -it('removes prefixes', () => { - expect(queryString.fromString('p_a=b')).toHaveProperty('a'); -}); +describe('fromString', () => { + it('removes prefixes', () => { + expect(queryString.fromString('p_a=b')).toHaveProperty('a'); + }); -it('ignores non-parameters', () => { - expect(queryString.fromString('p_a=b&utm=123')).not.toHaveProperty('utm'); + it('ignores non-parameters', () => { + expect(queryString.fromString('p_a=b&utm=123')).not.toHaveProperty('utm'); + }); }); + From f1f1d8d80a3f4ce41f3092d115d86accb50ce920 Mon Sep 17 00:00:00 2001 From: Omer Lachish Date: Mon, 8 Apr 2019 10:59:21 +0300 Subject: [PATCH 04/10] extract prefix and option constants --- client/app/services/query-string.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/client/app/services/query-string.js b/client/app/services/query-string.js index 30c2f745c3..463eec7b1d 100644 --- a/client/app/services/query-string.js +++ b/client/app/services/query-string.js @@ -1,8 +1,12 @@ import qs from 'qs'; -const parse = queryString => qs.parse(queryString, { allowDots: true, ignoreQueryPrefix: true }); -const onlyParameters = ([k]) => k.startsWith('p_'); -const removePrefix = ([k, v]) => [k.slice(2), v]; +const prefix = 'p_'; +const options = { allowDots: true, ignoreQueryPrefix: true }; + +const parse = queryString => qs.parse(queryString, options); +const onlyParameters = ([k]) => k.startsWith(prefix); +const removePrefix = ([k, v]) => [k.slice(prefix.length), v]; +const addPrefix = ([k, v]) => [`${prefix}${k}`, v]; const toObject = (obj, [k, v]) => Object.assign(obj, { [k]: v }); const fromString = queryString => Object.entries(parse(queryString)) From c18743e7a1b2aefe5d70b2202e852af34eb92533 Mon Sep 17 00:00:00 2001 From: Omer Lachish Date: Mon, 8 Apr 2019 10:59:31 +0300 Subject: [PATCH 05/10] add queryString.toString() --- client/app/services/query-string.js | 7 +++++++ client/app/services/query-string.test.js | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/client/app/services/query-string.js b/client/app/services/query-string.js index 463eec7b1d..bf9f9c2753 100644 --- a/client/app/services/query-string.js +++ b/client/app/services/query-string.js @@ -16,7 +16,14 @@ const fromString = queryString => Object.entries(parse(queryString)) const fromUrl = () => fromString(location.search); +const toString = obj => qs.stringify( + Object.entries(obj) + .map(addPrefix) + .reduce(toObject, {}), options, +); + export default { fromString, fromUrl, + toString, }; diff --git a/client/app/services/query-string.test.js b/client/app/services/query-string.test.js index 6f635f042c..e0f378d057 100644 --- a/client/app/services/query-string.test.js +++ b/client/app/services/query-string.test.js @@ -10,3 +10,9 @@ describe('fromString', () => { }); }); +describe('toString', () => { + it('adds prefixes', () => { + expect(queryString.toString({ a: 'b' })).toEqual('p_a=b'); + }); +}); + From 2b46bd8612c7b1728662ed58c7259d559c7940c2 Mon Sep 17 00:00:00 2001 From: Omer Lachish Date: Mon, 8 Apr 2019 11:20:29 +0300 Subject: [PATCH 06/10] add date ranges tests --- client/app/services/query-string.test.js | 29 ++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/client/app/services/query-string.test.js b/client/app/services/query-string.test.js index e0f378d057..cbaa8a0f5a 100644 --- a/client/app/services/query-string.test.js +++ b/client/app/services/query-string.test.js @@ -2,17 +2,38 @@ import queryString from '@/services/query-string'; describe('fromString', () => { it('removes prefixes', () => { - expect(queryString.fromString('p_a=b')).toHaveProperty('a'); + expect(queryString.fromString('p_a=b')) + .toHaveProperty('a'); }); it('ignores non-parameters', () => { - expect(queryString.fromString('p_a=b&utm=123')).not.toHaveProperty('utm'); + expect(queryString.fromString('p_a=b&utm=123')) + .not.toHaveProperty('utm'); + }); + + it('handles date ranges', () => { + expect(queryString.fromString('p_created_at.start=2019-01-01&p_created_at.end=2019-02-01')).toEqual({ + created_at: { + start: '2019-01-01', + end: '2019-02-01', + }, + }); }); }); describe('toString', () => { it('adds prefixes', () => { - expect(queryString.toString({ a: 'b' })).toEqual('p_a=b'); + expect(queryString.toString({ + a: 'b', + })).toEqual('p_a=b'); }); -}); + it('handles date ranges', () => { + expect(queryString.toString({ + created_at: { + start: '2019-01-01', + end: '2019-02-01', + }, + })).toEqual('p_created_at.start=2019-01-01&p_created_at.end=2019-02-01'); + }); +}); From c41788dbbb76b88d230da331ad49ce7b16d99946 Mon Sep 17 00:00:00 2001 From: Omer Lachish Date: Mon, 8 Apr 2019 12:58:54 +0300 Subject: [PATCH 07/10] move Parameter.toUrlParam() and Parameters.toUrlParam() to use the query-string module --- client/app/services/query.js | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/client/app/services/query.js b/client/app/services/query.js index 15d148f65f..555663adb8 100644 --- a/client/app/services/query.js +++ b/client/app/services/query.js @@ -1,5 +1,6 @@ import moment from 'moment'; import debug from 'debug'; +import qs from '@/services/query-string'; import Mustache from 'mustache'; import { zipObject, isEmpty, map, filter, includes, union, uniq, has, @@ -164,19 +165,7 @@ export class Parameter { } toUrlParams() { - if (this.isEmpty) { - return {}; - } - const prefix = this.urlPrefix; - if (isDateRangeParameter(this.type)) { - return { - [`${prefix}${this.name}.start`]: this.value.start, - [`${prefix}${this.name}.end`]: this.value.end, - }; - } - return { - [`${prefix}${this.name}`]: this.value, - }; + return this.isEmpty ? {} : { [this.name]: this.value }; } fromUrlParams(query) { @@ -300,11 +289,7 @@ class Parameters { return ''; } - const params = Object.assign(...this.get().map(p => p.toUrlParams())); - return Object - .keys(params) - .map(k => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`) - .join('&'); + return qs.toString(Object.assign(...this.get().map(p => p.toUrlParams()))); } } From 48ec52de3c7b7fe320b625e30b2b9cf1b6ac7083 Mon Sep 17 00:00:00 2001 From: Omer Lachish Date: Wed, 10 Apr 2019 11:30:09 +0300 Subject: [PATCH 08/10] use qs --- client/app/components/parameters.js | 5 +++-- client/app/services/query.js | 16 +++------------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/client/app/components/parameters.js b/client/app/components/parameters.js index 5777d52f7c..86f8c8ea87 100644 --- a/client/app/components/parameters.js +++ b/client/app/components/parameters.js @@ -1,5 +1,6 @@ import { extend } from 'lodash'; import template from './parameters.html'; +import qs from '@/services/query-string'; import EditParameterSettingsDialog from './EditParameterSettingsDialog'; function ParametersDirective($location) { @@ -23,11 +24,11 @@ function ParametersDirective($location) { if (scope.changed) { scope.changed({}); } - const params = extend({}, $location.search()); + const params = qs.fromUrl(); scope.parameters.forEach((param) => { extend(params, param.toUrlParams()); }); - $location.search(params); + $location.search(qs.toString(params)); }, true, ); diff --git a/client/app/services/query.js b/client/app/services/query.js index 555663adb8..2596badcac 100644 --- a/client/app/services/query.js +++ b/client/app/services/query.js @@ -4,7 +4,7 @@ import qs from '@/services/query-string'; import Mustache from 'mustache'; import { zipObject, isEmpty, map, filter, includes, union, uniq, has, - isNull, isUndefined, isArray, isObject, identity, extend, each, + isNull, isUndefined, isArray, isObject, identity, each, } from 'lodash'; Mustache.escape = identity; // do not html-escape values @@ -285,11 +285,7 @@ class Parameters { } toUrlParams() { - if (this.get().length === 0) { - return ''; - } - - return qs.toString(Object.assign(...this.get().map(p => p.toUrlParams()))); + return qs.toString(this.getValues()); } } @@ -531,13 +527,7 @@ function QueryResource( url += '/source'; } - let params = {}; - if (this.getParameters().isRequired()) { - this.getParametersDefs().forEach((param) => { - extend(params, param.toUrlParams()); - }); - } - params = map(params, (value, name) => `${encodeURIComponent(name)}=${encodeURIComponent(value)}`).join('&'); + const params = this.getParameters().toUrlParams(); if (params !== '') { url += `?${params}`; From 442467c79bb66514e5765247c7146798e741c0e0 Mon Sep 17 00:00:00 2001 From: Omer Lachish Date: Sun, 14 Apr 2019 14:19:08 +0300 Subject: [PATCH 09/10] bucket all query params under --- client/app/components/parameters.js | 2 +- .../components/queries/visualization-embed.js | 3 +- client/app/services/query-string.js | 32 ++++++++++++----- client/app/services/query-string.test.js | 36 +++++++++++++------ client/app/services/query.js | 2 +- 5 files changed, 53 insertions(+), 22 deletions(-) diff --git a/client/app/components/parameters.js b/client/app/components/parameters.js index 86f8c8ea87..00e0e226d7 100644 --- a/client/app/components/parameters.js +++ b/client/app/components/parameters.js @@ -26,7 +26,7 @@ function ParametersDirective($location) { } const params = qs.fromUrl(); scope.parameters.forEach((param) => { - extend(params, param.toUrlParams()); + extend(params.queryParameters, param.toUrlParams()); }); $location.search(qs.toString(params)); }, diff --git a/client/app/components/queries/visualization-embed.js b/client/app/components/queries/visualization-embed.js index 5d5678ad15..46c21a5820 100644 --- a/client/app/components/queries/visualization-embed.js +++ b/client/app/components/queries/visualization-embed.js @@ -38,7 +38,8 @@ export default function init(ngModule) { return session($http, $route, Auth).then(() => { const queryId = $route.current.params.queryId; const query = $http.get(`api/queries/${queryId}`).then(response => response.data); - const queryResult = $http.post(`api/queries/${queryId}/results`, { parameters: queryStringParameters.fromUrl() }).then(response => response.data); + const parameters = queryStringParameters.fromUrl().queryParameters; + const queryResult = $http.post(`api/queries/${queryId}/results`, { parameters }).then(response => response.data); return $q.all([query, queryResult]); }); } diff --git a/client/app/services/query-string.js b/client/app/services/query-string.js index bf9f9c2753..b7c05dd2f9 100644 --- a/client/app/services/query-string.js +++ b/client/app/services/query-string.js @@ -1,26 +1,40 @@ import qs from 'qs'; +import _ from 'lodash'; const prefix = 'p_'; const options = { allowDots: true, ignoreQueryPrefix: true }; const parse = queryString => qs.parse(queryString, options); +const stringify = obj => qs.stringify(obj, options); const onlyParameters = ([k]) => k.startsWith(prefix); const removePrefix = ([k, v]) => [k.slice(prefix.length), v]; const addPrefix = ([k, v]) => [`${prefix}${k}`, v]; const toObject = (obj, [k, v]) => Object.assign(obj, { [k]: v }); -const fromString = queryString => Object.entries(parse(queryString)) - .filter(onlyParameters) - .map(removePrefix) - .reduce(toObject, {}); +const fromString = (queryString) => { + const obj = parse(queryString); + const entries = Object.entries(obj); + const [queryParameters, rest] = _.partition(entries, onlyParameters); + + return { + queryParameters: { ...queryParameters.map(removePrefix).reduce(toObject, {}) }, + ...rest.reduce(toObject, {}), + }; +}; const fromUrl = () => fromString(location.search); -const toString = obj => qs.stringify( - Object.entries(obj) - .map(addPrefix) - .reduce(toObject, {}), options, -); +const toString = (obj) => { + let { queryParameters, ...rest } = obj; + + queryParameters = Object.entries(queryParameters || {}) + .map(addPrefix); + rest = Object.entries(rest, {}); + + return stringify(queryParameters + .concat(rest) + .reduce(toObject, {}), options); +}; export default { fromString, diff --git a/client/app/services/query-string.test.js b/client/app/services/query-string.test.js index cbaa8a0f5a..d5c1529d66 100644 --- a/client/app/services/query-string.test.js +++ b/client/app/services/query-string.test.js @@ -1,18 +1,24 @@ import queryString from '@/services/query-string'; describe('fromString', () => { - it('removes prefixes', () => { + it('groups query parameters', () => { expect(queryString.fromString('p_a=b')) + .toHaveProperty('queryParameters'); + }); + + it('retains non-query parameters', () => { + expect(queryString.fromString('a=b')) .toHaveProperty('a'); }); - it('ignores non-parameters', () => { - expect(queryString.fromString('p_a=b&utm=123')) - .not.toHaveProperty('utm'); + it('removes prefixes', () => { + expect(queryString.fromString('p_a=b').queryParameters) + .toHaveProperty('a'); }); it('handles date ranges', () => { - expect(queryString.fromString('p_created_at.start=2019-01-01&p_created_at.end=2019-02-01')).toEqual({ + const fromString = queryString.fromString('p_created_at.start=2019-01-01&p_created_at.end=2019-02-01'); + expect(fromString.queryParameters).toEqual({ created_at: { start: '2019-01-01', end: '2019-02-01', @@ -22,17 +28,27 @@ describe('fromString', () => { }); describe('toString', () => { - it('adds prefixes', () => { + it('adds prefixes to query parameters', () => { expect(queryString.toString({ - a: 'b', + queryParameters: { + a: 'b', + }, })).toEqual('p_a=b'); }); + it('does not add prefixes to non-query parameters', () => { + expect(queryString.toString({ + a: 'b', + })).toEqual('a=b'); + }); + it('handles date ranges', () => { expect(queryString.toString({ - created_at: { - start: '2019-01-01', - end: '2019-02-01', + queryParameters: { + created_at: { + start: '2019-01-01', + end: '2019-02-01', + }, }, })).toEqual('p_created_at.start=2019-01-01&p_created_at.end=2019-02-01'); }); diff --git a/client/app/services/query.js b/client/app/services/query.js index 2596badcac..7ee499179f 100644 --- a/client/app/services/query.js +++ b/client/app/services/query.js @@ -285,7 +285,7 @@ class Parameters { } toUrlParams() { - return qs.toString(this.getValues()); + return qs.toString({ queryParameters: this.getValues() }); } } From d556cb43404e175a8e2cda8234f3f3810c8559a2 Mon Sep 17 00:00:00 2001 From: Omer Lachish Date: Sun, 14 Apr 2019 14:24:17 +0300 Subject: [PATCH 10/10] set exported default name to match module name --- client/app/components/parameters.js | 6 +++--- client/app/components/queries/visualization-embed.js | 4 ++-- client/app/services/query-string.js | 4 +++- client/app/services/query.js | 8 ++++---- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/client/app/components/parameters.js b/client/app/components/parameters.js index 00e0e226d7..4a2bde75b1 100644 --- a/client/app/components/parameters.js +++ b/client/app/components/parameters.js @@ -1,6 +1,6 @@ import { extend } from 'lodash'; import template from './parameters.html'; -import qs from '@/services/query-string'; +import queryString from '@/services/query-string'; import EditParameterSettingsDialog from './EditParameterSettingsDialog'; function ParametersDirective($location) { @@ -24,11 +24,11 @@ function ParametersDirective($location) { if (scope.changed) { scope.changed({}); } - const params = qs.fromUrl(); + const params = queryString.fromUrl(); scope.parameters.forEach((param) => { extend(params.queryParameters, param.toUrlParams()); }); - $location.search(qs.toString(params)); + $location.search(queryString.toString(params)); }, true, ); diff --git a/client/app/components/queries/visualization-embed.js b/client/app/components/queries/visualization-embed.js index 46c21a5820..b9132428f8 100644 --- a/client/app/components/queries/visualization-embed.js +++ b/client/app/components/queries/visualization-embed.js @@ -1,5 +1,5 @@ import { find } from 'lodash'; -import queryStringParameters from '@/services/query-string'; +import queryString from '@/services/query-string'; import logoUrl from '@/assets/images/redash_icon_small.png'; import template from './visualization-embed.html'; @@ -38,7 +38,7 @@ export default function init(ngModule) { return session($http, $route, Auth).then(() => { const queryId = $route.current.params.queryId; const query = $http.get(`api/queries/${queryId}`).then(response => response.data); - const parameters = queryStringParameters.fromUrl().queryParameters; + const parameters = queryString.fromUrl().queryParameters; const queryResult = $http.post(`api/queries/${queryId}/results`, { parameters }).then(response => response.data); return $q.all([query, queryResult]); }); diff --git a/client/app/services/query-string.js b/client/app/services/query-string.js index b7c05dd2f9..2767bf912b 100644 --- a/client/app/services/query-string.js +++ b/client/app/services/query-string.js @@ -36,8 +36,10 @@ const toString = (obj) => { .reduce(toObject, {}), options); }; -export default { +const queryString = { fromString, fromUrl, toString, }; + +export default queryString; diff --git a/client/app/services/query.js b/client/app/services/query.js index 7ee499179f..a894ec7c5c 100644 --- a/client/app/services/query.js +++ b/client/app/services/query.js @@ -1,6 +1,6 @@ import moment from 'moment'; import debug from 'debug'; -import qs from '@/services/query-string'; +import queryString from '@/services/query-string'; import Mustache from 'mustache'; import { zipObject, isEmpty, map, filter, includes, union, uniq, has, @@ -201,10 +201,10 @@ export class Parameter { } class Parameters { - constructor(query, queryString) { + constructor(query, queryStringParameters) { this.query = query; this.updateParameters(); - this.initFromQueryString(queryString); + this.initFromQueryString(queryStringParameters); } parseQuery() { @@ -285,7 +285,7 @@ class Parameters { } toUrlParams() { - return qs.toString({ queryParameters: this.getValues() }); + return queryString.toString({ queryParameters: this.getValues() }); } }