Skip to content

Commit

Permalink
Merge pull request #43 from amiel/feature/support-nested-query-params
Browse files Browse the repository at this point in the history
Support nested query params
  • Loading branch information
Amiel Martin authored Apr 16, 2018
2 parents 89ef236 + 5a4a0b2 commit 8d495f2
Show file tree
Hide file tree
Showing 13 changed files with 196 additions and 3 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## Unreleased

### Added

- Support for nested queries (#43)

## [0.4.0] - 2017-09-04

### Added
Expand Down
3 changes: 2 additions & 1 deletion addon/mixins/url-templates.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Ember from 'ember';
import UriTemplate from 'uri-templates';
import { LINK_PREFIX } from "ember-data-url-templates/mixins/url-templates-serializer";
import flattenQueryParams from 'ember-data-url-templates/utils/flatten-query-params';

const { isArray, copy, typeOf } = Ember;

Expand Down Expand Up @@ -88,7 +89,7 @@ export default Ember.Mixin.create({
if (newQuery[param] === null ) { newQuery[param] = ""; }
}

return newQuery;
return flattenQueryParams(newQuery);
},

// TODO: Support automatic relationship ids through snapshots api.
Expand Down
28 changes: 28 additions & 0 deletions addon/utils/flatten-query-params.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// This was inspired by https://stackoverflow.com/a/34514143/223519
// and ultimately reconfigured by @basz
// https://github.com/amiel/ember-data-url-templates/issues/17#issuecomment-379232452

export default function flattenQueryParams(arr) {
let newObj = {};
dive('', arr, newObj);
return newObj;
}

function dive(currentKey, into, target) {
for (let i in into) {
if (into.hasOwnProperty(i)) {
let newKey = i;
let newVal = into[i];

if (currentKey.length > 0) {
newKey = `${currentKey}[${i}]`;
}

if (typeof newVal === 'object') {
dive(newKey, newVal, target);
} else {
target[newKey] = newVal;
}
}
}
}
1 change: 1 addition & 0 deletions app/utils/flatten-query-params.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from 'ember-data-url-templates/utils/flatten-query-params';
29 changes: 29 additions & 0 deletions tests/acceptance/basic-url-template-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,32 @@ test('it can use a specific template for one type of call (queryRecord)', functi
});
});


test('it can query with params', function(assert) {
server.create('post', {
id: 1,
slug: 'my-first-post',
title: 'This is my first post',
});

server.create('post', {
id: 2,
slug: 'my-second-post',
title: 'This is my second post',
});

server.create('post', {
id: 3,
slug: 'my-third-post',
title: 'This is another post',
});

visit('/search/my');

andThen(() => {
assert.equal(find('#post-1').length, 1);
assert.equal(find('#post-2').length, 1);
assert.equal(find('#post-3').length, 0);
});
});

2 changes: 1 addition & 1 deletion tests/dummy/app/adapters/post.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import ApplicationAdapter from './application';

export default ApplicationAdapter.extend({
urlTemplate: "{+host}{/namespace}/my-posts",
urlTemplate: "{+host}{/namespace}/my-posts{?query*}",
findRecordUrlTemplate: "{+host}{/namespace}/posts/{id}",
queryRecordUrlTemplate: "{+host}{/namespace}/my-posts/{slug}",
updateRecordUrlTemplate: "{+host}{/namespace}/my-posts/{slug}",
Expand Down
1 change: 1 addition & 0 deletions tests/dummy/app/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const Router = Ember.Router.extend({

Router.map(function() {
this.route('posts');
this.route('search', { path: '/search/:term' });
this.route('post', { path: '/posts/:slug' });
});

Expand Down
8 changes: 8 additions & 0 deletions tests/dummy/app/routes/search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Ember from 'ember';

export default Ember.Route.extend({
model({ term }) {
return this.store.query('post', { filter: { term } });
},
});

9 changes: 9 additions & 0 deletions tests/dummy/app/templates/search.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<h1>Search Results</h1>

<div id="posts">
{{#each model as |post|}}
<div id="post-{{post.id}}" class="post">
{{#link-to 'post' post.slug}}{{post.title}}{{/link-to}}
</div>
{{/each}}
</div>
74 changes: 74 additions & 0 deletions tests/dummy/app/utils/deparam.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/* NOTE: This was taken from https://github.com/canjs/can-deparam/blob/master/can-deparam.js */

/**
* @module {function} can-deparam can-deparam
* @parent can-routing
* @collection can-infrastructure
* @description Deserialize a query string into an array or object.
* @signature `deparam(params)`
*
* @param {String} params a form-urlencoded string of key-value pairs
* @return {Object} The params formatted into an object
*
* Takes a string of name value pairs and returns a Object literal that represents those params.
*
* ```js
* console.log(JSON.stringify(deparam("?foo=bar&number=1234"))); // -> '{"foo" : "bar", "number": 1234}'
* console.log(JSON.stringify(deparam("#foo[]=bar&foo[]=baz"))); // -> '{"foo" : ["bar", "baz"]}'
* console.log(JSON.stringify(deparam("foo=bar%20%26%20baz"))); // -> '{"foo" : "bar & baz"}'
* ```
* @body
*
* ## Try it
*
* Use this JS Bin to play around with this package:
*
* <a class="jsbin-embed" href="https://jsbin.com/mobimok/3/embed?js,console">can-deparam on jsbin.com</a>
* <script src="https://static.jsbin.com/js/embed.min.js?4.0.4"></script>
*/
var digitTest = /^\d+$/,
keyBreaker = /([^\[\]]+)|(\[\])/g,
paramTest = /([^?#]*)(#.*)?$/,
entityRegex = /%([^0-9a-f][0-9a-f]|[0-9a-f][^0-9a-f]|[^0-9a-f][^0-9a-f])/i,
prep = function (str) {
str = str.replace(/\+/g, ' ');

try {
return decodeURIComponent(str);
}
catch (e) {
return decodeURIComponent(str.replace(entityRegex, function(match, hex) {
return '%25' + hex;
}));
}
};

export default function(params) {
var data = {}, pairs, lastPart;
if (params && paramTest.test(params)) {
pairs = params.split('&');
pairs.forEach(function (pair) {
var parts = pair.split('='),
key = prep(parts.shift()),
value = prep(parts.join('=')),
current = data;
if (key) {
parts = key.match(keyBreaker);
for (var j = 0, l = parts.length - 1; j < l; j++) {
if (!current[parts[j]]) {
// If what we are pointing to looks like an `array`
current[parts[j]] = digitTest.test(parts[j + 1]) || parts[j + 1] === '[]' ? [] : {};
}
current = current[parts[j]];
}
lastPart = parts.pop();
if (lastPart === '[]') {
current.push(value);
} else {
current[lastPart] = value;
}
}
});
}
return data;
}
15 changes: 14 additions & 1 deletion tests/dummy/mirage/config.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
import deparam from 'dummy/utils/deparam';

export default function() {
this.timing = 1000;
this.namespace = '/api';

this.get('/my-posts', 'post');
this.get('/my-posts', (schema, request) => {
const paramString = request.url.split('?')[1];
const queryParams = deparam(paramString);
const posts = schema.posts.all();

if (queryParams.filter) {
return posts.filter((post) => post.title.match(queryParams.filter.term));
} else {
return posts;
}
});

this.get('/my-posts/:slug', (schema, request) => {
return schema.posts.findBy({ slug: request.params.slug });
});
Expand Down
7 changes: 7 additions & 0 deletions tests/unit/mixins/url-templates-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,10 @@ test('it does not mutate null query param', function(assert) {
subject.buildURL('post', null, null, 'query', params);
assert.equal(params.tag, null);
});

test('it parameterizes nested queryParams as would normally be expected', function(assert) {
const subject = BasicAdapter.create({ urlTemplate: '/posts{?query*}' });
const params = { filter: { term: 'my' } };
const url = subject.buildURL('post', null, null, 'query', params);
assert.equal(url, '/posts?filter%5Bterm%5D=my');
});
16 changes: 16 additions & 0 deletions tests/unit/utils/flatten-query-params-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import flattenQueryParams from 'dummy/utils/flatten-query-params';
import { module, test } from 'qunit';

module('Unit | Utility | flatten query params');

test('does not alter a simple object', function(assert) {
const object = { foo: 'bar' };
const result = flattenQueryParams(object);
assert.deepEqual(object, result);
});

test('can flatten a simple nested query', function(assert) {
const object = { filter: { foo: 'bar' } };
const result = flattenQueryParams(object);
assert.deepEqual(result, { 'filter[foo]': 'bar' });
});

0 comments on commit 8d495f2

Please sign in to comment.