Skip to content

Commit

Permalink
Merge pull request #4 from scoutforpets/master
Browse files Browse the repository at this point in the history
updating master
  • Loading branch information
alechirsch authored Dec 6, 2017
2 parents 88c272f + a529e3d commit cdb4cea
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 37 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,12 @@ If you're returning a single resource from a call such as `GET /customers/1`, ma
`options` | Description
:------------- | :-------------
filter _object_ | Filters a result set based specific field. Example: `/pets?filter[name]=max` would only return pets named max. Keywords can be added to filters to give more control over the results. Example: `/pets?filterType[pet][like]=ax` would only return pets that have "ax" in their name. The supported types are "like", "not", "lt", "lte", "gt", and "gte". Both "like" and "not" support multiple values by comma separation. NOTE: This is not supported by JSON API spec.
fields _object_ | Limits the fields returned as part of the record. Example: `/pets?fields[pets]=name` would return pet records with only the name field rather than every field.
filter _object_ | Filters a result set based specific field. Example: `/pets?filter[name]=max` would only return pets named max. Keywords can be added to filters to give more control over the results. Example: `/pets?filterType[like][pet]=ax` would only return pets that have "ax" in their name. The supported types are "like", "not", "lt", "lte", "gt", and "gte". Both "like" and "not" support multiple values by comma separation. Also, if your data has a string with a comma, you can filter for that comma by escaping the character with two backslashes. NOTE: This is not supported by JSON API spec.
fields _object_ | Limits the fields returned as part of the record. Example: `/pets?fields[pets]=name` would return pet records with only the name field rather than every field. _Note:_ you may use aggregate functions such as `/pets?fields[pets]=count(id)`. Supported aggregate functions are "count", "sum", "avg", "max", "min".
include _array_ | Returns relationships as part of the payload. Example: `/pets?include=owner` would return the pet record in addition to the full record of its owner. _Note:_ you may override an `include` parameter with your own Knex function rather than just a string representing the relationship name.
page _object/false_ | Paginates the result set. Example: `/pets?page[limit]=25&page[offset]=0` would return the first 25 records. If you've passed default pagination parameters to the plugin, but would like to disable paging on a specific call, just set `page` to `false`.
sort _array_ | Sorts the result set by specific fields. Example: `/pets?sort=-weight,birthDate` would return the records sorted by `weight` descending, then `birthDate` ascending
group _array_ | Use it with `fields` param to group your results. Example: `/pets?fields[pets]=avg(age),gender&group=gender` would return return the average age of pets per gender. NOTE: This is not supported by JSON API spec.
See the **[specific section of the JSON API spec](http://jsonapi.org/format/#fetching-includes)** that deals with these parameters for more information.
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"dependencies": {
"bookshelf-page": "0.3.2",
"inflection": "^1.10.0",
"lodash": "4.13.1"
"lodash": "4.13.1",
"split-string": "^0.1.1"
}
}
110 changes: 91 additions & 19 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@ import {
isArray as _isArray,
isObject as _isObject,
isObjectLike as _isObjectLike,
isNull as _isNull,
forIn as _forIn,
keys as _keys,
map as _map,
zipObject as _zipObject
} from 'lodash';

import split from 'split-string';

import inflection from 'inflection';

import Paginator from 'bookshelf-page';
Expand Down Expand Up @@ -62,30 +65,31 @@ export default (Bookshelf, options = {}) => {
opts = opts || {};

const internals = {};
const { include, fields, sort, page = {}, filter } = opts;
const { include, fields, sort, page = {}, filter, group } = opts;
const filterTypes = ['like', 'not', 'lt', 'gt', 'lte', 'gte'];

// Get a reference to the field being used as the id
internals.idAttribute = this.constructor.prototype.idAttribute ?
this.constructor.prototype.idAttribute : 'id';

// Get a reference to the current model name. Note that if no type is
// explcitly passed, the tableName will be used
// explicitly passed, the tableName will be used
internals.modelName = type ? type : this.constructor.prototype.tableName;

// Initialize an instance of the current model and clone the initial query
internals.model =
this.constructor.forge().query((qb) => _assign(qb, this.query().clone()));

/**
* Build a query for relational dependencies of filtering and sorting
* Build a query for relational dependencies of filtering, grouping and sorting
* @param filterValues {object}
* @param groupValues {object}
* @param sortValues {object}
*/
internals.buildDependencies = (filterValues, sortValues) => {
internals.buildDependencies = (filterValues, groupValues, sortValues) => {

const relationHash = {};
// Find relations in fitlerValues
// Find relations in filterValues
if (_isObjectLike(filterValues) && !_isEmpty(filterValues)){

// Loop through each filter value
Expand Down Expand Up @@ -123,10 +127,20 @@ export default (Bookshelf, options = {}) => {
});
}

// Find relations in groupValues
if (_isObjectLike(groupValues) && !_isEmpty(groupValues)){

// Loop through each group value
_forEach(groupValues, (value) => {

// Add relations to the relationHash
internals.buildDependenciesHelper(value, relationHash);
});
}

// Need to select model.* so all of the relations are not returned, also check if there is anything in fields object
if (_keys(relationHash).length && !_keys(fields).length){
internals.model.query((qb) => {

qb.select(`${internals.modelName}.*`);
});
}
Expand Down Expand Up @@ -251,25 +265,47 @@ export default (Bookshelf, options = {}) => {
// Add qualifying table name to avoid ambiguous columns
fieldNames[fieldKey] = _map(fieldNames[fieldKey], (value) => {

if (!fieldKey){
return value;
// Extract any aggregate function around the column name
let column = value;
let aggregateFunction = null;
const regex = new RegExp(/(count|sum|avg|max|min)\((.+)\)/g);
const match = regex.exec(value);

if (match) {
aggregateFunction = match[1];
column = match[2];
}
return `${fieldKey}.${value}`;

if (!fieldKey) {
if (!_includes(column, '.')) {
column = `${internals.modelName}.${column}`;
}
} else {
column = `${fieldKey}.${column}`;
}

return aggregateFunction ? { aggregateFunction, column } : column;
});

// Only process the field if it's not a relation. Fields
// for relations are processed in `buildIncludes()`
if (!_includes(include, fieldKey)) {


// Add columns to query
internals.model.query((qb) => {

if (!fieldKey){
qb.distinct();
}

qb.select(fieldNames[fieldKey]);
_forEach(fieldNames[fieldKey], (column) => {

if (column.aggregateFunction) {
qb[column.aggregateFunction](`${column.column} as ${column.aggregateFunction}`);
} else {
qb.select([column]);
}
});

// JSON API considers relationships as fields, so we
// need to make sure the id of the relation is selected
Expand All @@ -278,7 +314,7 @@ export default (Bookshelf, options = {}) => {
if (internals.isBelongsToRelation(relation, this)) {
const relatedData = this.related(relation).relatedData;
const relationId = relatedData.foreignKey ? relatedData.foreignKey : `${inflection.singularize(relatedData.parentTableName)}_${relatedData.parentIdAttribute}`;
qb.select(relationId);
qb.select(`${internals.modelName}.${relationId}`);
}
});
});
Expand Down Expand Up @@ -318,7 +354,13 @@ export default (Bookshelf, options = {}) => {
typeKey = internals.formatRelation(internals.formatColumnNames([typeKey])[0]);

// Determine if there are multiple filters to be applied
const valueArray = typeValue.toString().indexOf(',') !== -1 ? typeValue.split(',') : typeValue;
let valueArray = null;
if (!_isArray(typeValue)){
valueArray = split(typeValue.toString(), ',');
}
else {
valueArray = typeValue;
}

// Attach different query for each type
if (key === 'like'){
Expand Down Expand Up @@ -382,9 +424,17 @@ export default (Bookshelf, options = {}) => {
// Remove all but the last table name, need to get number of dots
key = internals.formatRelation(internals.formatColumnNames([key])[0]);

// Determine if there are multiple filters to be applied
value = value.toString().indexOf(',') !== -1 ? value.split(',') : value;
qb.whereIn(key, value);

if (_isNull(value)){
qb.where(key, value);
}
else {
// Determine if there are multiple filters to be applied
if (!_isArray(value)){
value = split(value.toString(), ',');
}
qb.whereIn(key, value);
}
}
}
});
Expand Down Expand Up @@ -500,6 +550,26 @@ export default (Bookshelf, options = {}) => {
}
};

/**
* Build a query based on the `group` parameter.
* @param groupValues {array}
*/
internals.buildGroup = (groupValues = []) => {

if (_isArray(groupValues) && !_isEmpty(groupValues)) {

groupValues = internals.formatColumnNames(groupValues);

internals.model.query((qb) => {

_forEach(groupValues, (groupBy) => {

qb.groupBy(groupBy);
});
});
}
};

/**
* Processes incoming parameters that represent columns names and
* formats them using the internal {@link Model#format} function.
Expand Down Expand Up @@ -612,19 +682,21 @@ export default (Bookshelf, options = {}) => {
/// Process parameters
////////////////////////////////

// Apply relational dependencies for filters and sorting
internals.buildDependencies(filter, sort);
// Apply relational dependencies for filters, grouping and sorting
internals.buildDependencies(filter, group, sort);

// Apply filters
internals.buildFilters(filter);

// Apply grouping
internals.buildGroup(group);

// Apply sorting
internals.buildSort(sort);

// Apply relations
internals.buildIncludes(include);


// Apply sparse fieldsets
internals.buildFields(fields);

Expand Down
Loading

0 comments on commit cdb4cea

Please sign in to comment.