├── .babelrc ├── .bablerc ├── .flowconfig ├── .gitignore ├── README.md ├── circle.yml ├── index.js ├── lib └── index.js ├── package.json ├── src ├── __tests__ │ └── test-spec.js └── index.js └── tests ├── environment.js └── jasmine.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | presets: ['blueflag'] 3 | } 4 | -------------------------------------------------------------------------------- /.bablerc: -------------------------------------------------------------------------------- 1 | { "presets": ["es2015"] } -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [options] 2 | module.system.node.resolve_dirname=node_modules 3 | module.system.node.resolve_dirname=.. 4 | 5 | [libs] 6 | node_modules/immutable-js/dist/immutable.js.flow 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphql-aggregate 2 | 3 | [![CircleCI](https://circleci.com/gh/blueflag/graphql-aggregate.svg?style=shield)](https://circleci.com/gh/blueflag/graphql-aggregate) 4 | [![Coverage Status](https://coveralls.io/repos/github/blueflag/graphql-aggregate/badge.svg?branch=master)](https://coveralls.io/github/blueflag/graphql-aggregate?branch=master) 5 | 6 | 7 | Generates an aggregation schema for graphql that can be used to perform aggregation functions 8 | via a graphql query on Arrays of GraphQL types. 9 | 10 | ## Usage 11 | 12 | To get access to an aggregation schema on a graphql you require an array of objects 13 | 14 | ### Sample Code 15 | 16 | A small sample, using express-graphql and some sample data exists [here](https://github.com/thepont/graphql-aggregate-sample) 17 | 18 | ### Example 19 | 20 | The following `GraphQLFieldConfig` defines a list of answers 21 | 22 | ```javascript 23 | answers : { 24 | type: new GraphQLList(AnswerType) 25 | } 26 | ``` 27 | 28 | It can be turned into an aggregate type using the following `GraphQLFieldConfig` 29 | 30 | _see GraphQL's documentation on field configuration of the [GraphQLObjectType](http://graphql.org/graphql-js/type/#graphqlobjecttype)_ 31 | 32 | ```javascript 33 | import {AggregationType} from 'graphql-aggregate' 34 | 35 | // Creates an AggregationType with based on the AnswerType 36 | // The resolver must return an Array that can be resolved into AnswerTypes 37 | 38 | aggregateAnswers: { 39 | type: AggregationType(AnswerType), 40 | resolve: (obj) => obj.answers 41 | } 42 | ``` 43 | 44 | after this is done, the schema will allow the user to aggregate tusing the fields 45 | in the answer type. 46 | 47 | for instance if the AnswerType had the following definition. 48 | 49 | ``` 50 | type AnswerType { 51 | id: ID, 52 | username: String, 53 | usersAnswer: Int! 54 | } 55 | ``` 56 | 57 | The following query would be a valid way of finding the amount of answers attributed to each user. 58 | 59 | ```graphql 60 | aggregateAnswers { 61 | groupedBy { 62 | username { 63 | keys 64 | values { 65 | count 66 | } 67 | } 68 | } 69 | } 70 | } 71 | } 72 | 73 | ``` 74 | 75 | You can also further apply aggregations on the aggregation. 76 | 77 | ```graphql 78 | aggregateAnswers { 79 | groupedBy { 80 | username { 81 | keys 82 | values { 83 | groupedBy { 84 | usersAnswer { 85 | asMap 86 | } 87 | } 88 | } 89 | } 90 | } 91 | } 92 | } 93 | } 94 | 95 | ``` 96 | 97 | ### Supplied Schema 98 | 99 | Aggregation types will be named based on the type the were created from, for instance if our type was named `Answer` our aggregation type would be named `AnswerAggregation`. 100 | 101 | #### Fields Provided 102 | 103 | The follow is the fields that are provided via the api, the best way of seeing exactly how it works is by using GraphiQL to investigate the fields. 104 | 105 | The Aggregation provides fields dependent on the type that it is created with, this allows a more natural syntax then using arguments to lookup preknown values, and allows us to use graphql type checking simply on the queries. 106 | 107 | ``` 108 | Aggregation : { 109 | values: [T], // Values in aggregation. 110 | count: Int, // Amount of values in aggregation. 111 | groupedBy: GroupedByAggregation : { 112 | fields in T...: KeyedList: { 113 | asMap: Scaler //Map of key/values. 114 | keys: List //List of keys in order. 115 | values: Aggregation //Returns the aggregation functions to be used on the values of the current aggregation 116 | }, 117 | }, 118 | filter: FilterAggregation : { //Filter aggregation methods 119 | int fields in T...args:(gt: int, lt: int, gte: int, equal int): Aggregation 120 | string fields in T...args:(gt: string, lt: string, gte: string, equal string): Aggregation 121 | }, 122 | sum: { 123 | float or int fields in T...: int // sum of the all the values in the field. 124 | }, 125 | average: { 126 | float or int fields in T...: int // average of the all the values in the field. 127 | }, 128 | min: { 129 | float or int fields in T...: int // minimum value in the field. 130 | }, 131 | max: { 132 | float or int fields in T...: int // maximum value in the field. 133 | } 134 | } 135 | 136 | ``` 137 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | test: 2 | post: 3 | - npm run coverage -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib'); 2 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.KeyedListAggregation = KeyedListAggregation; 7 | exports.KeyedList = KeyedList; 8 | exports.isFloat = isFloat; 9 | exports.isInt = isInt; 10 | exports.isString = isString; 11 | exports.AggregationType = AggregationType; 12 | 13 | var _immutable = require('immutable'); 14 | 15 | var _graphql = require('graphql'); 16 | 17 | var GeneralType = new _graphql.GraphQLScalarType({ 18 | name: 'GeneralType', 19 | serialize: function serialize(value) { 20 | return value; 21 | }, 22 | parseValue: function parseValue(value) { 23 | return value; 24 | }, 25 | parseLiteral: function parseLiteral(ast) { 26 | return ast.value; 27 | } 28 | }); 29 | 30 | var imMath = require('immutable-math'); 31 | 32 | var INT_TYPE_NAME = 'Int'; 33 | var FLOAT_TYPE_NAME = 'Float'; 34 | var STRING_TYPE_NAME = 'String'; 35 | 36 | /** 37 | * A list that has an associated group of keys, 38 | * @param {GraphQLOutputType} type that the keyed list is based on 39 | * @returns a GraphQLObjectType that is specific for the graphql object type being aggregated. 40 | */ 41 | 42 | // Because there is no way other then other then returning a 43 | // GraphQLScalarType to have key value pairs, and then 44 | // we have no way of adding more aggregations 45 | 46 | var keyedAggregationType = {}; 47 | function KeyedListAggregation(type) { 48 | if (!keyedAggregationType[type.name]) { 49 | keyedAggregationType[type.name] = new _graphql.GraphQLObjectType({ 50 | name: type.name + 'AggregationKeyedList', 51 | fields: function fields() { 52 | return { 53 | key: { 54 | type: _graphql.GraphQLString, 55 | description: 'Key after aggregation', 56 | resolve: function resolve(obj) { 57 | return obj.key; 58 | } 59 | }, 60 | aggregate: { 61 | type: new AggregationType(type), 62 | description: 'Further aggregaion ' + type.name, 63 | resolve: function resolve(obj) { 64 | return obj.value; 65 | } 66 | }, 67 | values: { 68 | type: new _graphql.GraphQLList(type), 69 | description: 'Values after aggregation ' + type.name, 70 | resolve: function resolve(obj) { 71 | return obj.value; 72 | } 73 | } 74 | }; 75 | } 76 | }); 77 | } 78 | return keyedAggregationType[type.name]; 79 | } 80 | 81 | var keyedListTypes = {}; 82 | function KeyedList(type) { 83 | if (!keyedListTypes[type.name]) { 84 | keyedListTypes[type.name] = new _graphql.GraphQLObjectType({ 85 | name: type.name + 'KeyedList', 86 | fields: function fields() { 87 | return { 88 | asMap: { 89 | type: GeneralType, 90 | description: 'Return an unstructed map', 91 | resolve: function resolve(obj) { 92 | return obj; 93 | } 94 | }, 95 | keys: { 96 | type: new _graphql.GraphQLList(_graphql.GraphQLString), 97 | description: 'Keys after aggregation', 98 | resolve: function resolve(obj) { 99 | return (0, _immutable.Map)(obj).keySeq().toArray(); 100 | } 101 | }, 102 | values: { 103 | type: new _graphql.GraphQLList(AggregationType(type)), 104 | description: 'Values after aggregation ' + type.name, 105 | resolve: function resolve(obj) { 106 | return (0, _immutable.Map)(obj).valueSeq().toArray(); 107 | } 108 | }, 109 | keyValue: { 110 | type: new _graphql.GraphQLList(KeyedListAggregation(type)), 111 | description: 'Key-Values after aggregation ' + type.name, 112 | resolve: function resolve(obj) { 113 | return (0, _immutable.Map)(obj).reduce(function (rr, value, key) { 114 | return rr.push({ 115 | key: key, 116 | value: value 117 | }); 118 | }, (0, _immutable.List)()).toArray(); 119 | } 120 | } 121 | }; 122 | } 123 | }); 124 | } 125 | return keyedListTypes[type.name]; 126 | } 127 | 128 | /* 129 | * Checks if a Map from a graphql schema is a float 130 | * @params {Map} field immutable map from GraphQLFieldConfig 131 | * @returns {boolean} true if the field is a Float (GraphQLFloat) 132 | */ 133 | 134 | function isFloat(field) { 135 | return field.get('type').name === FLOAT_TYPE_NAME; 136 | } 137 | 138 | /* 139 | * Checks if a Map from a graphql schema is a int 140 | * @params {Map} field immutable map from GraphQLFieldConfig 141 | * @returns {boolean} true if the field is a Int (GraphQLInt) 142 | */ 143 | function isInt(field) { 144 | return field.get('type').name === INT_TYPE_NAME; 145 | } 146 | 147 | /* 148 | * Checks if a Map from a graphql schema is a string 149 | * Checks if a Map from a graphql schema is a string 150 | * @returns {boolean} true if the field is a String (GraphQLString) 151 | */ 152 | 153 | function isString(field) { 154 | return field.get('type').name === STRING_TYPE_NAME; 155 | } 156 | 157 | /** 158 | * Default resolver for when fields have no resolver attached. 159 | * 160 | * by default graphql takes the key from the object that corresponds to the field being looked up. 161 | */ 162 | var defaultFieldResolver = function defaultFieldResolver(fieldName) { 163 | return function (obj) { 164 | return obj[fieldName]; 165 | }; 166 | }; 167 | 168 | /** 169 | * Resolves fields using custom resolver associated with the field or reverts to using obj.key 170 | * 171 | * @returns {function} partically applied function for creating resolver using args, context and the graphql resolve info. 172 | */ 173 | 174 | function fieldResolver(field, fieldName) { 175 | return function resolve(args, context, info) { 176 | /** 177 | * @params source - source object to use for resolving the field 178 | * @returns {Promise} promise for resolving the field 179 | */ 180 | return function (source) { 181 | return Promise.resolve(field.get('resolve', defaultFieldResolver(fieldName))(source, args, context, info)); 182 | }; 183 | }; 184 | } 185 | 186 | // var fieldResolver = (field, fieldName) => (args, ctx, root) => (obj): Promise<*> => { 187 | // return Promise.resolve(() => field.get('resolve', defaultFieldResolver(fieldName))(obj, args, ctx, root)) 188 | // } 189 | 190 | 191 | //fieldResolver resolver for the type that we are creating the filds for. 192 | function createFields(type, returnType, resolver) { 193 | 194 | var fields = type._typeConfig.fields(); 195 | 196 | return (0, _immutable.fromJS)(fields).reduce(function (resultFields, field, key) { 197 | return resultFields.set(key, (0, _immutable.Map)({ 198 | type: returnType, 199 | resolve: function resolve(obj, args, ctx, info) { 200 | return resolver(fieldResolver(field, key)(args, ctx, info), key, obj, field); 201 | } 202 | })); 203 | }, (0, _immutable.Map)()).toJS(); 204 | } 205 | 206 | function createFieldsFromFieldList(fields, returnType, resolver) { 207 | return (0, _immutable.fromJS)(fields).reduce(function (resultFields, field, key) { 208 | return resultFields.set(key, (0, _immutable.Map)({ 209 | type: returnType, 210 | resolve: function resolve(obj, args, ctx, root) { 211 | return resolver(fieldResolver(field, key)(args, ctx, root), key, obj, field); 212 | } 213 | })); 214 | }, (0, _immutable.Map)()).toJS(); 215 | } 216 | 217 | var FilterIntOperations = new _graphql.GraphQLInputObjectType({ 218 | name: "FilterIntOperations", 219 | description: 'Filter operations for int', 220 | fields: function fields() { 221 | return filterIntArgs; 222 | } 223 | }); 224 | 225 | var filterIntArgs = { 226 | gt: { 227 | type: _graphql.GraphQLInt, 228 | description: 'Filter only values greater then value.' 229 | }, 230 | lt: { 231 | type: _graphql.GraphQLInt, 232 | description: 'Filter only values less then value.' 233 | }, 234 | gte: { 235 | type: _graphql.GraphQLInt, 236 | description: 'Filter only values greater then or equal to value' 237 | }, 238 | lte: { 239 | type: _graphql.GraphQLInt, 240 | description: 'Filter only values less then or equal to value' 241 | }, 242 | equal: { 243 | type: _graphql.GraphQLInt, 244 | description: 'Filter only values equal to value.' 245 | }, 246 | not: { 247 | type: FilterIntOperations, 248 | description: 'Filter only values equal to value.' 249 | }, 250 | or: { 251 | type: new _graphql.GraphQLList(FilterIntOperations), 252 | description: 'Filter only values equal to value.' 253 | } 254 | }; 255 | 256 | var FilterStringOperations = new _graphql.GraphQLInputObjectType({ 257 | name: "FilterStringOperations", 258 | description: 'Filter operations for strings', 259 | fields: function fields() { 260 | return filterStringArgs; 261 | } 262 | }); 263 | 264 | var filterStringArgs = { 265 | gt: { 266 | type: _graphql.GraphQLString, 267 | description: 'Filter only values greater then value.' 268 | }, 269 | lt: { 270 | type: _graphql.GraphQLString, 271 | description: 'Filter only values less then value.' 272 | }, 273 | gte: { 274 | type: _graphql.GraphQLString, 275 | description: 'Filter only values greater then or equal to value' 276 | }, 277 | lte: { 278 | type: _graphql.GraphQLString, 279 | description: 'Filter only values less then or equal to value' 280 | }, 281 | equal: { 282 | type: _graphql.GraphQLString, 283 | description: 'Filter only values equal to value.' 284 | }, 285 | not: { 286 | type: FilterStringOperations, 287 | description: 'Filter only values equal to value.' 288 | }, 289 | or: { 290 | type: new _graphql.GraphQLList(FilterStringOperations), 291 | description: 'Filter only values equal to value.' 292 | } 293 | }; 294 | 295 | var filterFunctions = function filterFunctions(field, key) { 296 | return { 297 | gt: function gt(_ref, value) { 298 | var _gt = _ref.gt; 299 | 300 | return _gt == null || _gt < value; 301 | }, 302 | lt: function lt(_ref2, value) { 303 | var _lt = _ref2.lt; 304 | 305 | return _lt == null || _lt > value; 306 | }, 307 | gte: function gte(_ref3, value) { 308 | var _gte = _ref3.gte; 309 | 310 | return _gte == null || _gte <= value; 311 | }, 312 | lte: function lte(_ref4, value) { 313 | var _lte = _ref4.lte; 314 | 315 | return _lte == null || _lte >= value; 316 | }, 317 | equal: function equal(_ref5, value) { 318 | var _equal = _ref5.equal; 319 | 320 | return _equal == null || _equal === value; 321 | }, 322 | not: function not(_ref6, value, obj) { 323 | var _not = _ref6.not; 324 | 325 | return _not == null || !runFilterFunction(field, key)(_not, obj); 326 | } 327 | }; 328 | }; 329 | 330 | var runFilterFunction = function runFilterFunction(field, key) { 331 | return function (args, value) { 332 | var _filterFunctions = filterFunctions(field, key), 333 | gt = _filterFunctions.gt, 334 | lt = _filterFunctions.lt, 335 | gte = _filterFunctions.gte, 336 | lte = _filterFunctions.lte, 337 | equal = _filterFunctions.equal, 338 | not = _filterFunctions.not; 339 | 340 | return gt(args, value) && lt(args, value) && gte(args, value) && lte(args, value) && equal(args, value); 341 | }; 342 | }; 343 | 344 | var resolveIntFilter = function resolveIntFilter(field, key) { 345 | return function (obj, args, ctx, root) { 346 | var resolver = fieldResolver(field, key)({}, ctx, root); 347 | var asList = (0, _immutable.List)(obj); 348 | return resolveFromArray(asList, resolver).then(function (resolvedValues) { 349 | return asList.filter(function (value, ii) { 350 | return runFilterFunction(field, key)(args, resolvedValues[ii]); 351 | }); 352 | }); 353 | }; 354 | }; 355 | 356 | function filterFieldConfigFactory(fields, field, key, type) { 357 | if (isInt(field)) { 358 | return fields.set(key, (0, _immutable.Map)({ 359 | type: AggregationType(type), 360 | args: filterIntArgs, 361 | resolve: resolveIntFilter(field, key), 362 | description: 'Filters ' + key + ' via args.' 363 | })); 364 | } 365 | if (isString(field)) { 366 | return fields.set(key, (0, _immutable.Map)({ 367 | type: AggregationType(type), 368 | args: filterStringArgs, 369 | resolve: resolveIntFilter(field, key), 370 | description: 'Filters ' + key + ' via args.' 371 | })); 372 | } 373 | return fields; 374 | } 375 | 376 | function containsType(fields, typeCheck) { 377 | return (0, _immutable.fromJS)(fields); 378 | } 379 | /** 380 | * Returns a list of resolved values from array using field resolver 381 | */ 382 | function resolveFromArray(arr, fieldResolver) { 383 | return Promise.resolve((0, _immutable.List)(arr)).then(function (list) { 384 | return Promise.all(list.map(function (obj) { 385 | return fieldResolver(obj); 386 | }).toArray()); 387 | }); 388 | } 389 | 390 | /** 391 | * Creates an AggregationType with it based on the GraphQLOutputType requested, 392 | * Objects that wish to be resolved this way must be a Array of the requested type. 393 | * 394 | * @param {GraphQLOutputType} type - type to create the aggregaion functions for 395 | */ 396 | 397 | var aggregationTypes = {}; 398 | function AggregationType(type) { 399 | if (!aggregationTypes[type.name]) { 400 | aggregationTypes[type.name] = new _graphql.GraphQLObjectType({ 401 | name: type.name + 'Aggregation', 402 | description: 'Preform aggregation methods on ' + type.name, 403 | fields: function fields() { 404 | var typeFields = (0, _immutable.fromJS)(type._typeConfig.fields()); 405 | var intFields = typeFields.filter(function (field) { 406 | return isFloat(field) || isInt(field); 407 | }); 408 | var stringFields = typeFields.filter(function (field) { 409 | return isString(field); 410 | }); 411 | 412 | var fields = (0, _immutable.Map)({ 413 | values: { 414 | description: 'List of ' + type.name, 415 | type: new _graphql.GraphQLList(type), 416 | resolve: function resolve(obj) { 417 | return (0, _immutable.List)(obj).toArray(); 418 | } 419 | }, 420 | count: { 421 | description: 'The amount of items in the aggregaion', 422 | type: _graphql.GraphQLInt, 423 | resolve: function resolve(obj) { 424 | return (0, _immutable.List)(obj).count(); 425 | } 426 | 427 | }, 428 | first: { 429 | description: 'Return the first item in the aggregaion', 430 | type: type, 431 | resolve: function resolve(obj) { 432 | return (0, _immutable.List)(obj).first(); 433 | } 434 | }, 435 | last: { // return fromJS() 436 | description: 'Return the last item in the aggregaion', 437 | type: type, 438 | resolve: function resolve(obj) { 439 | return (0, _immutable.List)(obj).last(); 440 | } 441 | }, 442 | reverse: { 443 | description: 'Reverse the order of the items in the aggregaion', 444 | type: AggregationType(type), 445 | resolve: function resolve(obj) { 446 | return (0, _immutable.List)(obj).reverse(); 447 | } 448 | }, 449 | //slice: {} 450 | // sort: { 451 | // description: 'Sort the list via the parameter', 452 | // fields: () => 453 | // createFields(type, 454 | // AggregationType(type), 455 | // (fieldResolver: * , key: string, obj: *) => { 456 | // return fromJS(obj).sort((a, b) => { 457 | // return fieldResolver(a) > fieldResolver(b) 458 | // }) 459 | // }, 460 | // () => true) 461 | // }, 462 | //flattern 463 | //flattern should allow a CSV to be generated. 464 | groupedBy: { 465 | type: new _graphql.GraphQLObjectType({ 466 | name: type.name + 'GroupedByAggregation', 467 | description: 'Preform groupBy aggregation methods on ' + type.name, 468 | fields: function fields() { 469 | return createFields(type, KeyedList(type), function (fieldResolver, key, obj) { 470 | // return Promise.resolve(List(obj)) 471 | // .then((obj) => { 472 | // return Promise.all(obj.map((obj) => fieldResolver(obj)).toArray()) 473 | // }) 474 | return resolveFromArray(obj, fieldResolver).then(function (result) { 475 | var groups = (0, _immutable.List)(result); 476 | return (0, _immutable.List)(obj).groupBy(function (item, ii) { 477 | return groups.get(ii); 478 | }); 479 | }); 480 | }); 481 | } 482 | }), 483 | description: 'Group items in aggregaion by the value of a field.', 484 | resolve: function resolve(obj) { 485 | return obj; 486 | } 487 | } 488 | 489 | }); 490 | if (stringFields.count() > 0 || intFields.count() > 0) { 491 | fields = fields.set('filter', { 492 | description: 'Preform filter aggregation methods on ' + type.name, 493 | type: new _graphql.GraphQLObjectType({ 494 | name: type.name + 'FilterAggregation', 495 | description: 'Preform filter aggregation methods on ' + type.name, 496 | args: filterIntArgs, 497 | fields: function fields() { 498 | return stringFields.merge(intFields).reduce(function (fields, typeField, key) { 499 | return filterFieldConfigFactory(fields, typeField, key, type); 500 | }, (0, _immutable.Map)()).toJS(); 501 | } 502 | }), 503 | resolve: function resolve(obj) { 504 | return obj; 505 | } 506 | }); 507 | if (intFields.count() > 0) { 508 | // add integer operations 509 | fields = fields.merge((0, _immutable.Map)({ 510 | sum: { 511 | description: 'Sum the values of a field on ' + type.name, 512 | type: new _graphql.GraphQLObjectType({ 513 | name: type.name + 'Sum', 514 | description: 'Perform sum on ' + type.name, 515 | fields: function fields() { 516 | return createFieldsFromFieldList(intFields, _graphql.GraphQLFloat, function (fieldResolver, key, obj) { 517 | return resolveFromArray(obj, fieldResolver).then(function (values) { 518 | return (0, _immutable.List)(values).update(imMath.sum()); 519 | }); 520 | }, function (field) { 521 | return isFloat(field) || isInt(field); 522 | }); 523 | } 524 | }), 525 | resolve: function resolve(obj) { 526 | return obj; 527 | } 528 | }, 529 | average: { 530 | description: 'Returns the average of a field on ' + type.name, 531 | type: new _graphql.GraphQLObjectType({ 532 | name: type.name + 'Average', 533 | description: 'Perform averages on ' + type.name, 534 | fields: function fields() { 535 | return createFieldsFromFieldList(intFields, _graphql.GraphQLFloat, function (fieldResolver, key, obj) { 536 | return resolveFromArray(obj, fieldResolver).then(function (values) { 537 | return (0, _immutable.List)(values).update(imMath.average()); 538 | }); 539 | }, function (field) { 540 | return isFloat(field) || isInt(field); 541 | }); 542 | } 543 | }), 544 | resolve: function resolve(obj) { 545 | return obj; 546 | } 547 | }, 548 | median: { 549 | description: 'Returns the median value of a field on ' + type.name, 550 | type: new _graphql.GraphQLObjectType({ 551 | name: type.name + 'Median', 552 | description: 'Perform median calculation on ' + type.name, 553 | fields: function fields() { 554 | return createFieldsFromFieldList(intFields, _graphql.GraphQLFloat, function (fieldResolver, key, obj) { 555 | return resolveFromArray(obj, fieldResolver).then(function (values) { 556 | return (0, _immutable.List)(values).update(imMath.median()); 557 | }); 558 | }, function (field) { 559 | return isFloat(field) || isInt(field); 560 | }); 561 | } 562 | }), 563 | resolve: function resolve(obj) { 564 | return obj; 565 | } 566 | }, 567 | min: { 568 | description: 'Returns the minium value of all the items on a field on ' + type.name, 569 | type: new _graphql.GraphQLObjectType({ 570 | name: type.name + 'Min', 571 | description: 'minium on value of a field for ' + type.name, 572 | fields: function fields() { 573 | return createFieldsFromFieldList(intFields, _graphql.GraphQLFloat, function (fieldResolver, key, obj) { 574 | return resolveFromArray(obj, fieldResolver).then(function (values) { 575 | return (0, _immutable.List)(values).update(imMath.min()); 576 | }); 577 | }, function (field) { 578 | return isFloat(field) || isInt(field); 579 | }); 580 | } 581 | }), 582 | resolve: function resolve(obj) { 583 | return obj; 584 | } 585 | }, 586 | max: { 587 | description: 'Returns the maximum value of all the items on a field on' + type.name, 588 | type: new _graphql.GraphQLObjectType({ 589 | name: type.name + 'Max', 590 | description: 'maximum on value of a field for ' + type.name, 591 | fields: function fields() { 592 | return createFieldsFromFieldList(intFields, _graphql.GraphQLFloat, function (fieldResolver, key, obj) { 593 | return resolveFromArray(obj, fieldResolver).then(function (values) { 594 | return (0, _immutable.List)(values).update(imMath.max()); 595 | }); 596 | }, function (field) { 597 | return isFloat(field) || isInt(field); 598 | }); 599 | } 600 | }), 601 | resolve: function resolve(obj) { 602 | return obj; 603 | } 604 | } 605 | })); 606 | } 607 | } 608 | return fields.toObject(); 609 | } 610 | 611 | }); 612 | } 613 | return aggregationTypes[type.name]; 614 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-aggregate", 3 | "version": "0.2.0", 4 | "description": "Aggregation functions for lists in graphql", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "npm run test-jasmine-coverage", 8 | "test-jasmine": "node tests/jasmine.js", 9 | "test-jasmine-coverage": "nyc npm run test-jasmine", 10 | "coverage": "nyc report --reporter=text-lcov | coveralls", 11 | "build": "babel src --out-dir lib" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/blueflag/graphql-aggregate.git" 16 | }, 17 | "author": "", 18 | "license": "ISC", 19 | "bugs": { 20 | "url": "https://github.com/blueflag/graphql-aggregate/issues" 21 | }, 22 | "homepage": "https://github.com/blueflag/graphql-aggregate#readme", 23 | "devDependencies": { 24 | "babel-cli": "^6.18.0", 25 | "babel-core": "^6.18.2", 26 | "babel-register": "^6.18.0", 27 | "coveralls": "^2.11.14", 28 | "eslint-config-blueflag": "^0.1.0", 29 | "flow-bin": "^0.34.0", 30 | "jasmine": "^2.5.2", 31 | "jasmine-reporters": "^2.2.0", 32 | "nyc": "^8.4.0" 33 | }, 34 | "dependencies": { 35 | "babel-preset-blueflag": "0.0.1", 36 | "babel-register": "^6.18.0", 37 | "graphql": "^0.7.2", 38 | "immutable": "^3.8.1", 39 | "immutable-math": "^0.1.2" 40 | }, 41 | "nyc": { 42 | "all": true, 43 | "include": [ 44 | "src/**" 45 | ], 46 | "exclude": [ 47 | "src/**/__mocks__/**", 48 | "src/**/*-spec.js", 49 | "src/**/*-tests.js", 50 | "public/**", 51 | "src/@auditr/components/**" 52 | ], 53 | "reporter": [ 54 | "lcov", 55 | "text" 56 | ], 57 | "extension": [ 58 | ".jsx" 59 | ], 60 | "require": [ 61 | "./tests/environment.js" 62 | ] 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/__tests__/test-spec.js: -------------------------------------------------------------------------------- 1 | 2 | import {isFloat, 3 | isInt, 4 | isString, 5 | AggregationType, 6 | KeyedList} from '../index.js'; 7 | import {fromJS, Map, List} from 'immutable' 8 | import {GraphQLFloat, 9 | GraphQLInt, 10 | GraphQLString, 11 | GraphQLObjectType} from 'graphql'; 12 | 13 | describe('TypeTests', () => { 14 | describe('isFloat', () => { 15 | it('Returns true if the type is a float', () => { 16 | expect(isFloat(fromJS({type: GraphQLFloat}))).toBeTruthy(); 17 | }); 18 | }); 19 | describe('isInt', () => { 20 | it('Returns true if the type is an int', () => { 21 | expect(isInt(fromJS({type: GraphQLInt}))).toBeTruthy(); 22 | }); 23 | }); 24 | describe('isString', () => { 25 | it('Returns true if the type is a string', () => { 26 | expect(isString(fromJS({type: GraphQLString}))).toBeTruthy(); 27 | }); 28 | }); 29 | }); 30 | 31 | describe('KeyedList', () => { 32 | describe('asMap', () => { 33 | it('Returns a map of the values', () => { 34 | 35 | var testType = new GraphQLObjectType({ 36 | name: 'KeyListAsMapTest', 37 | fields: () => ({ 38 | id: { 39 | type: GraphQLString 40 | }, 41 | name: { 42 | type: GraphQLString 43 | } 44 | }) 45 | }) 46 | var keyedList = KeyedList(testType); 47 | expect(keyedList._typeConfig.fields().asMap.resolve({ 48 | 'test1': { 49 | 'id' : "test1" 50 | }, 51 | 'test2': { 52 | 'id' : "test1" 53 | } 54 | })) 55 | .toEqual({ 56 | 'test1': { 57 | 'id' : "test1" 58 | }, 59 | 'test2': { 60 | 'id' : "test1" 61 | } 62 | }) 63 | }) 64 | }); 65 | 66 | 67 | describe('keys', () => { 68 | it('Returns a list of the keys after aggregation', () => { 69 | 70 | var testType = new GraphQLObjectType({ 71 | name: 'KeyListAsMapTest', 72 | fields: () => ({ 73 | id: { 74 | type: GraphQLString 75 | }, 76 | name: { 77 | type: GraphQLString 78 | } 79 | }) 80 | }) 81 | var keyedList = KeyedList(testType); 82 | expect(keyedList._typeConfig.fields().keys.resolve(fromJS({ 83 | 'test1': { 84 | 'id' : "test1" 85 | }, 86 | 'test2': { 87 | 'id' : "test1" 88 | } 89 | }))) 90 | .toEqual([ 91 | 'test1', 92 | 'test2' 93 | ]) 94 | }) 95 | }); 96 | 97 | describe('values', () => { 98 | it('Returns the values in arrays', () => { 99 | var testType = new GraphQLObjectType({ 100 | name: 'KeyListAsMapTest', 101 | fields: () => ({ 102 | id: { 103 | type: GraphQLString 104 | }, 105 | name: { 106 | type: GraphQLString 107 | } 108 | }) 109 | }) 110 | 111 | var keyedList = KeyedList(testType); 112 | expect(keyedList._typeConfig.fields().values.resolve(Map({ 113 | 'test1': { 114 | 'id' : "test1", 115 | 'name' : "paul" 116 | }, 117 | 'test2': { 118 | 'id' : "test1", 119 | 'name' : "paul" 120 | } 121 | }))) 122 | .toEqual([ 123 | { 124 | 'id' : "test1", 125 | 'name' : "paul" 126 | }, 127 | { 128 | 'id' : "test1", 129 | 'name' : "paul" 130 | } 131 | ]) 132 | }) 133 | }) 134 | }); 135 | 136 | 137 | describe('AggregationType', () => { 138 | describe('values', () => { 139 | it('Returns a list of values', () => { 140 | var testType = new GraphQLObjectType({ 141 | name: 'TestType', 142 | fields: () => ({ 143 | id: { 144 | type: GraphQLString 145 | }, 146 | name: { 147 | type: GraphQLString 148 | } 149 | }) 150 | }) 151 | var myAggregation = AggregationType(testType); 152 | expect(myAggregation._typeConfig.fields().values.resolve([{ 153 | id: 'test', 154 | name: 'John', 155 | },{ 156 | id: 'test2', 157 | name: 'Richard', 158 | }])).toEqual([{ 159 | id: 'test', 160 | name: 'John', 161 | },{ 162 | id: 'test2', 163 | name: 'Richard', 164 | }]) 165 | }); 166 | }); 167 | 168 | describe('count', () => { 169 | it('Returns a amount of values in the supplied array', () => { 170 | var testType = new GraphQLObjectType({ 171 | name: 'TestType', 172 | fields: () => ({ 173 | id: { 174 | type: GraphQLString 175 | }, 176 | name: { 177 | type: GraphQLString 178 | } 179 | }) 180 | }) 181 | var myAggregation = AggregationType(testType); 182 | expect(myAggregation._typeConfig.fields().count.resolve([{ 183 | id: 'test', 184 | name: 'John', 185 | },{ 186 | id: 'test2', 187 | name: 'Richard', 188 | }])).toEqual(2); 189 | }); 190 | }); 191 | 192 | describe('first', () => { 193 | it('Returns the first item in the array', () => { 194 | var testType = new GraphQLObjectType({ 195 | name: 'TestType', 196 | fields: () => ({ 197 | id: { 198 | type: GraphQLString 199 | }, 200 | name: { 201 | type: GraphQLString 202 | } 203 | }) 204 | }) 205 | var myAggregation = AggregationType(testType); 206 | expect(myAggregation._typeConfig.fields().first.resolve([{ 207 | id: 'test', 208 | name: 'John' 209 | },{ 210 | id: 'test2', 211 | name: 'Richard' 212 | }])).toEqual({ 213 | id: 'test', 214 | name: 'John' 215 | }); 216 | }); 217 | }); 218 | 219 | describe('last', () => { 220 | it('Returns the last item in the array', () => { 221 | var testType = new GraphQLObjectType({ 222 | name: 'TestType', 223 | fields: () => ({ 224 | id: { 225 | type: GraphQLString 226 | }, 227 | name: { 228 | type: GraphQLString 229 | } 230 | }) 231 | }) 232 | var myAggregation = AggregationType(testType); 233 | expect(myAggregation._typeConfig.fields().last.resolve([{ 234 | id: 'test', 235 | name: 'John', 236 | },{ 237 | id: 'test2', 238 | name: 'Richard', 239 | }])).toEqual({ 240 | id: 'test2', 241 | name: 'Richard', 242 | }); 243 | }); 244 | }); 245 | 246 | describe('reverse', () => { 247 | it('Returns the collection in reverse order.', () => { 248 | var testType = new GraphQLObjectType({ 249 | name: 'TestType', 250 | fields: () => ({ 251 | id: { 252 | type: GraphQLString 253 | }, 254 | name: { 255 | type: GraphQLString 256 | } 257 | }) 258 | }) 259 | var myAggregation = AggregationType(testType); 260 | let result = myAggregation._typeConfig.fields().reverse.resolve([{ 261 | id: 'test', 262 | name: 'John' 263 | },{ 264 | id: 'test2', 265 | name: 'Richard' 266 | }]) 267 | expect(result.toArray()).toEqual([{ 268 | id: 'test2', 269 | name: 'Richard' 270 | },{ 271 | id: 'test', 272 | name: 'John' 273 | }]) 274 | }) 275 | }); 276 | 277 | describe('groupedBy', () => { 278 | it('Returns the collection mapped by a field', (done) => { 279 | var testType = new GraphQLObjectType({ 280 | name: 'TestTypeGroupedBy', 281 | fields: () => ({ 282 | id: { 283 | type: GraphQLString, 284 | resolve: obj => obj.id 285 | }, 286 | name: { 287 | type: GraphQLString, 288 | resolve: obj => obj.name 289 | } 290 | }) 291 | }) 292 | 293 | var myAggregation = AggregationType(testType); 294 | var groupByObj = myAggregation._typeConfig.fields().groupedBy.resolve([{ 295 | id: 'test', 296 | name: 'John', 297 | },{ 298 | id: 'test2', 299 | name: 'Richard', 300 | },{ 301 | id: 'test3', 302 | name: 'Richard', 303 | }]) 304 | return myAggregation._typeConfig 305 | .fields() 306 | .groupedBy 307 | .type 308 | ._typeConfig 309 | .fields() 310 | .name 311 | .resolve(groupByObj) 312 | .then(result => { 313 | expect(result.toObject()).toEqual({ 314 | John: List([ 315 | { 316 | id: 'test', 317 | name: 'John', 318 | } 319 | ]), 320 | Richard: List([ 321 | { 322 | id: 'test2', 323 | name: 'Richard', 324 | },{ 325 | id: 'test3', 326 | name: 'Richard', 327 | } 328 | ]) 329 | }) 330 | done(); 331 | }) 332 | .catch(done.fail) 333 | }) 334 | }); 335 | 336 | describe('sum', () => { 337 | it('Returns the sum of a field', (done) => { 338 | var testType = new GraphQLObjectType({ 339 | name: 'TestNumbers', 340 | fields: () => ({ 341 | id: { 342 | type: GraphQLString, 343 | resolve: obj => obj.id 344 | }, 345 | number: { 346 | type: GraphQLFloat, 347 | resolve: obj => obj.number 348 | } 349 | }) 350 | }) 351 | 352 | var myAggregation = AggregationType(testType); 353 | var sumObj = myAggregation._typeConfig.fields().sum.resolve([{ 354 | id: 'test', 355 | number: 3, 356 | },{ 357 | id: 'test2', 358 | number: 3, 359 | },{ 360 | id: 'test3', 361 | number: 4, 362 | }]) 363 | return myAggregation 364 | ._typeConfig.fields().sum.type 365 | ._typeConfig.fields().number.resolve(sumObj) 366 | .then(result => { 367 | expect(result).toEqual(10) 368 | done() 369 | }) 370 | .catch(done.fail) 371 | }) 372 | }); 373 | 374 | describe('average', () => { 375 | it('Returns the average of a field', (done) => { 376 | var testType = new GraphQLObjectType({ 377 | name: 'TestNumbers', 378 | fields: () => ({ 379 | id: { 380 | type: GraphQLString, 381 | resolve: obj => obj.id 382 | }, 383 | number: { 384 | type: GraphQLFloat, 385 | resolve: obj => obj.number 386 | } 387 | }) 388 | }) 389 | 390 | var myAggregation = AggregationType(testType); 391 | var averageObj = myAggregation._typeConfig.fields().average.resolve([{ 392 | id: 'test', 393 | number: 1, 394 | },{ 395 | id: 'test2', 396 | number: 5, 397 | },{ 398 | id: 'test3', 399 | number: 9, 400 | }]) 401 | return myAggregation 402 | ._typeConfig.fields().average.type 403 | ._typeConfig.fields().number.resolve(averageObj) 404 | .then(result => { 405 | expect(result).toEqual(5) 406 | done() 407 | }) 408 | .catch(done.fail) 409 | }) 410 | }); 411 | 412 | describe('min', () => { 413 | it('Returns the smallest number in a field', (done) => { 414 | var testType = new GraphQLObjectType({ 415 | name: 'TestNumbers', 416 | fields: () => ({ 417 | id: { 418 | type: GraphQLString, 419 | resolve: obj => obj.id 420 | }, 421 | number: { 422 | type: GraphQLFloat, 423 | resolve: obj => obj.number 424 | } 425 | }) 426 | }) 427 | 428 | var myAggregation = AggregationType(testType); 429 | var minObj = myAggregation._typeConfig.fields().min.resolve([{ 430 | id: 'test', 431 | number: 1, 432 | },{ 433 | id: 'test2', 434 | number: 5, 435 | },{ 436 | id: 'test3', 437 | number: 9, 438 | }]) 439 | return myAggregation 440 | ._typeConfig.fields().min.type 441 | ._typeConfig.fields().number.resolve(minObj) 442 | .then(result => { 443 | expect(result).toEqual(1) 444 | done() 445 | }) 446 | .catch(done.fail) 447 | }) 448 | }); 449 | 450 | describe('max', () => { 451 | it('Returns the largest number in a field', (done) => { 452 | var testType = new GraphQLObjectType({ 453 | name: 'TestNumbers', 454 | fields: () => ({ 455 | id: { 456 | type: GraphQLString, 457 | resolve: obj => obj.id 458 | }, 459 | number: { 460 | type: GraphQLFloat, 461 | resolve: obj => obj.number 462 | } 463 | }) 464 | }) 465 | 466 | var myAggregation = AggregationType(testType); 467 | var maxObj = myAggregation._typeConfig.fields().max.resolve([{ 468 | id: 'test', 469 | number: 1, 470 | },{ 471 | id: 'test2', 472 | number: 5, 473 | },{ 474 | id: 'test3', 475 | number: 9, 476 | }]) 477 | 478 | return myAggregation 479 | ._typeConfig.fields().max.type 480 | ._typeConfig.fields().number.resolve(maxObj) 481 | .then(result => { 482 | expect(result).toEqual(9); 483 | done() 484 | }) 485 | .catch(done.fail) 486 | }) 487 | }); 488 | 489 | 490 | describe('filter', () => { 491 | 492 | describe('gt', () => { 493 | it('Filters by values greater then. ', (done) => { 494 | var testType = new GraphQLObjectType({ 495 | name: 'TestNumberInt', 496 | fields: () => ({ 497 | id: { 498 | type: GraphQLString, 499 | resolve: obj => obj.id 500 | }, 501 | number: { 502 | type: GraphQLInt, 503 | resolve: obj => obj.number 504 | } 505 | }) 506 | }) 507 | 508 | var myAggregation = AggregationType(testType); 509 | var maxObj = myAggregation._typeConfig.fields().filter.resolve([{ 510 | id: 'test', 511 | number: 1, 512 | },{ 513 | id: 'test2', 514 | number: 5, 515 | },{ 516 | id: 'test3', 517 | number: 9, 518 | }]) 519 | return myAggregation 520 | ._typeConfig.fields().filter.type 521 | ._typeConfig.fields().number.resolve(maxObj, {gt: 5}) 522 | .then(result => { 523 | expect(result.toArray()).toEqual([{ 524 | id: 'test3', 525 | number: 9 526 | }]) 527 | done() 528 | }) 529 | .catch(done.fail) 530 | }) 531 | }); 532 | describe('lt', () => { 533 | it('Filters by values less then. ', (done) => { 534 | var testType = new GraphQLObjectType({ 535 | name: 'TestNumberInt', 536 | fields: () => ({ 537 | id: { 538 | type: GraphQLString, 539 | resolve: obj => obj.id 540 | }, 541 | number: { 542 | type: GraphQLInt, 543 | resolve: obj => obj.number 544 | } 545 | }) 546 | }) 547 | 548 | var myAggregation = AggregationType(testType); 549 | var maxObj = myAggregation._typeConfig.fields().filter.resolve([{ 550 | id: 'test', 551 | number: 1, 552 | },{ 553 | id: 'test2', 554 | number: 5, 555 | },{ 556 | id: 'test3', 557 | number: 9, 558 | }]) 559 | myAggregation 560 | ._typeConfig.fields().filter.type 561 | ._typeConfig.fields().number.resolve(maxObj, {lt: 5}) 562 | .then(result => { 563 | expect(result.toArray()) 564 | .toEqual([{ 565 | id: 'test', 566 | number: 1 567 | }]) 568 | done() 569 | }) 570 | .catch(done.fail) 571 | }) 572 | }); 573 | describe('gte', () => { 574 | it('Filters by values greater then or equal to. ', (done) => { 575 | var testType = new GraphQLObjectType({ 576 | name: 'TestNumberInt', 577 | fields: () => ({ 578 | id: { 579 | type: GraphQLString, 580 | resolve: obj => obj.id 581 | }, 582 | number: { 583 | type: GraphQLInt, 584 | resolve: obj => obj.number 585 | } 586 | }) 587 | }) 588 | 589 | var myAggregation = AggregationType(testType); 590 | var maxObj = myAggregation._typeConfig.fields().filter.resolve([{ 591 | id: 'test', 592 | number: 1, 593 | },{ 594 | id: 'test2', 595 | number: 5, 596 | },{ 597 | id: 'test3', 598 | number: 9, 599 | }]) 600 | 601 | 602 | 603 | myAggregation 604 | ._typeConfig.fields().filter.type 605 | ._typeConfig.fields().number.resolve(maxObj, {gte: 5}) 606 | .then(result => { 607 | expect(result.toArray()) 608 | .toEqual([{ 609 | id: 'test2', 610 | number: 5, 611 | },{ 612 | id: 'test3', 613 | number: 9 614 | }]) 615 | done() 616 | }) 617 | .catch(done.fail) 618 | }) 619 | }); 620 | describe('lte', () => { 621 | it('Filters by values less then or equal to. ', (done) => { 622 | var testType = new GraphQLObjectType({ 623 | name: 'TestNumberInt', 624 | fields: () => ({ 625 | id: { 626 | type: GraphQLString, 627 | resolve: obj => obj.id 628 | }, 629 | number: { 630 | type: GraphQLInt, 631 | resolve: obj => obj.number 632 | } 633 | }) 634 | }) 635 | 636 | var myAggregation = AggregationType(testType); 637 | var maxObj = myAggregation._typeConfig.fields().filter.resolve([{ 638 | id: 'test', 639 | number: 1, 640 | },{ 641 | id: 'test2', 642 | number: 5, 643 | },{ 644 | id: 'test3', 645 | number: 9, 646 | }]) 647 | 648 | myAggregation 649 | ._typeConfig.fields().filter.type 650 | ._typeConfig.fields().number.resolve(maxObj, {lte: 5}) 651 | .then(result => { 652 | expect(result.toArray()) 653 | .toEqual([{ 654 | id: 'test', 655 | number: 1, 656 | },{ 657 | id: 'test2', 658 | number: 5 659 | }]) 660 | done() 661 | }) 662 | .catch(done.fail) 663 | }) 664 | }); 665 | describe('equal', () => { 666 | it('Filters by values equal to ', (done) => { 667 | var testType = new GraphQLObjectType({ 668 | name: 'TestNumberInt', 669 | fields: () => ({ 670 | id: { 671 | type: GraphQLString, 672 | resolve: obj => obj.id 673 | }, 674 | number: { 675 | type: GraphQLInt, 676 | resolve: obj => obj.number 677 | } 678 | }) 679 | }) 680 | 681 | var myAggregation = AggregationType(testType); 682 | var maxObj = myAggregation._typeConfig.fields().filter.resolve([{ 683 | id: 'test', 684 | number: 1, 685 | },{ 686 | id: 'test2', 687 | number: 5, 688 | },{ 689 | id: 'test3', 690 | number: 9, 691 | }]) 692 | 693 | myAggregation 694 | ._typeConfig.fields().filter.type 695 | ._typeConfig.fields().number.resolve(maxObj, {equal: 5}) 696 | .then(result => { 697 | expect(result.toArray()) 698 | .toEqual([{ 699 | id: 'test2', 700 | number: 5 701 | }]) 702 | done() 703 | }) 704 | .catch(done.fail) 705 | }); 706 | }) 707 | // describe('not', () => { 708 | // it('Returns values that don\'t match the filter ', () => { 709 | // var testType = new GraphQLObjectType({ 710 | // name: 'TestNumberInt', 711 | // fields: () => ({ 712 | // id: { 713 | // type: GraphQLString, 714 | // resolve: obj => obj.id 715 | // }, 716 | // number: { 717 | // type: GraphQLInt, 718 | // resolve: obj => obj.number 719 | // } 720 | // }) 721 | // }) 722 | 723 | // var myAggregation = AggregationType(testType); 724 | // var maxObj = myAggregation._typeConfig.fields().filter.resolve([{ 725 | // id: 'test', 726 | // number: 1, 727 | // },{ 728 | // id: 'test2', 729 | // number: 5, 730 | // },{ 731 | // id: 'test3', 732 | // number: 9, 733 | // }]) 734 | 735 | 736 | // myAggregation 737 | // ._typeConfig.fields().filter.type 738 | // ._typeConfig.fields().number.resolve(maxObj, {equal: 5}) 739 | // .then(result => { 740 | // expect(result.toArray()) 741 | // .toEqual([{ 742 | // id: 'test2', 743 | // number: 5 744 | // }]) 745 | // done() 746 | // }) 747 | // .catch(done.fail) 748 | 749 | 750 | // expect( 751 | // myAggregation 752 | // ._typeConfig.fields().filter.type 753 | // ._typeConfig.fields().number.resolve(maxObj, {not: {equal: 5}}).toArray()) 754 | // .toEqual([{ 755 | // id: 'test', 756 | // number: 1, 757 | // },{ 758 | // id: 'test3', 759 | // number: 9, 760 | // }]) 761 | // }) 762 | // }) 763 | 764 | // describe('FilterStringOperations', () => { 765 | // it('Contains arguments for filtering via strings.', () => { 766 | // var testType = new GraphQLObjectType({ 767 | // name: 'TestNumberInt', 768 | // fields: () => ({ 769 | // id: { 770 | // type: GraphQLString, 771 | // resolve: obj => obj.id 772 | // }, 773 | // number: { 774 | // type: GraphQLInt, 775 | // resolve: obj => obj.number 776 | // } 777 | // }) 778 | // }) 779 | // var myAggregation = AggregationType(testType); 780 | 781 | // console.log(myAggregation 782 | // ._typeConfig.fields().filter.type 783 | // ._typeConfig.fields().id.args); 784 | 785 | // }); 786 | // }); 787 | }); 788 | }); -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {Map, fromJS, Iterable, List} from 'immutable'; 4 | import {GraphQLList, 5 | GraphQLFloat, 6 | GraphQLScalarType, 7 | GraphQLObjectType, 8 | GraphQLString, 9 | GraphQLOutputType, 10 | GraphQLFieldConfig, 11 | GraphQLInt, 12 | GraphQLInputObjectType} from 'graphql'; 13 | 14 | import type {GraphQLResolveInfo, 15 | GraphQLFieldConfigMapThunk, 16 | GraphQLFieldConfigMap} from 'graphql'; 17 | 18 | const GeneralType = new GraphQLScalarType({ 19 | name: 'GeneralType', 20 | serialize: (value) => value, 21 | parseValue: (value) => value, 22 | parseLiteral: (ast) => ast.value 23 | }); 24 | 25 | const imMath = require('immutable-math'); 26 | 27 | const INT_TYPE_NAME = 'Int'; 28 | const FLOAT_TYPE_NAME = 'Float'; 29 | const STRING_TYPE_NAME = 'String'; 30 | 31 | 32 | 33 | /** 34 | * A list that has an associated group of keys, 35 | * @param {GraphQLOutputType} type that the keyed list is based on 36 | * @returns a GraphQLObjectType that is specific for the graphql object type being aggregated. 37 | */ 38 | 39 | // Because there is no way other then other then returning a 40 | // GraphQLScalarType to have key value pairs, and then 41 | // we have no way of adding more aggregations 42 | 43 | var keyedAggregationType = {}; 44 | export function KeyedListAggregation(type: GraphQLOutputType): GraphQLObjectType{ 45 | if(!keyedAggregationType[type.name]){ 46 | keyedAggregationType[type.name] = new GraphQLObjectType({ 47 | name: `${type.name}AggregationKeyedList`, 48 | fields: () => ({ 49 | key : { 50 | type: GraphQLString, 51 | description: `Key after aggregation`, 52 | resolve: (obj) => obj.key 53 | }, 54 | aggregate: { 55 | type: new AggregationType(type), 56 | description: `Further aggregaion ${type.name}`, 57 | resolve: (obj) => { 58 | return obj.value 59 | } 60 | }, 61 | values: { 62 | type: new GraphQLList(type), 63 | description: `Values after aggregation ${type.name}`, 64 | resolve: (obj) => { 65 | return obj.value 66 | } 67 | } 68 | }) 69 | }); 70 | } 71 | return keyedAggregationType[type.name] 72 | } 73 | 74 | var keyedListTypes = {}; 75 | export function KeyedList(type: GraphQLOutputType): GraphQLObjectType{ 76 | if(!keyedListTypes[type.name]){ 77 | keyedListTypes[type.name] = new GraphQLObjectType({ 78 | name: `${type.name}KeyedList`, 79 | fields: () => ({ 80 | asMap : { 81 | type: GeneralType, 82 | description: `Return an unstructed map`, 83 | resolve: (obj) => obj 84 | }, 85 | keys : { 86 | type: new GraphQLList(GraphQLString), 87 | description: `Keys after aggregation`, 88 | resolve: (obj) => Map(obj) 89 | .keySeq() 90 | .toArray() 91 | }, 92 | values: { 93 | type: new GraphQLList(AggregationType(type)), 94 | description: `Values after aggregation ${type.name}`, 95 | resolve: (obj) => { 96 | return Map(obj) 97 | .valueSeq() 98 | .toArray() 99 | } 100 | }, 101 | keyValue: { 102 | type: new GraphQLList(KeyedListAggregation(type)), 103 | description: `Key-Values after aggregation ${type.name}`, 104 | resolve: (obj) => { 105 | return Map(obj).reduce((rr, value, key) => { 106 | return rr.push({ 107 | key: key, 108 | value: value 109 | }) 110 | }, List()).toArray() 111 | } 112 | } 113 | }) 114 | }); 115 | } 116 | return keyedListTypes[type.name] 117 | } 118 | 119 | 120 | 121 | /* 122 | * Checks if a Map from a graphql schema is a float 123 | * @params {Map} field immutable map from GraphQLFieldConfig 124 | * @returns {boolean} true if the field is a Float (GraphQLFloat) 125 | */ 126 | 127 | export function isFloat(field: Map): boolean { 128 | return field.get('type').name === FLOAT_TYPE_NAME 129 | } 130 | 131 | /* 132 | * Checks if a Map from a graphql schema is a int 133 | * @params {Map} field immutable map from GraphQLFieldConfig 134 | * @returns {boolean} true if the field is a Int (GraphQLInt) 135 | */ 136 | export function isInt(field: Map): boolean { 137 | return field.get('type').name === INT_TYPE_NAME 138 | } 139 | 140 | /* 141 | * Checks if a Map from a graphql schema is a string 142 | * Checks if a Map from a graphql schema is a string 143 | * @returns {boolean} true if the field is a String (GraphQLString) 144 | */ 145 | 146 | export function isString(field: Map): boolean { 147 | return field.get('type').name === STRING_TYPE_NAME 148 | } 149 | 150 | /** 151 | * Default resolver for when fields have no resolver attached. 152 | * 153 | * by default graphql takes the key from the object that corresponds to the field being looked up. 154 | */ 155 | var defaultFieldResolver = (fieldName: string) => (obj: Object) => { 156 | return obj[fieldName] 157 | } 158 | 159 | /** 160 | * Resolves fields using custom resolver associated with the field or reverts to using obj.key 161 | * 162 | * @returns {function} partically applied function for creating resolver using args, context and the graphql resolve info. 163 | */ 164 | 165 | function fieldResolver(field: Map, fieldName: string){ 166 | return function resolve(args: {[argName: string]: any}, context: *, info: GraphQLResolveInfo){ 167 | /** 168 | * @params source - source object to use for resolving the field 169 | * @returns {Promise} promise for resolving the field 170 | */ 171 | return function (source: *){ 172 | return Promise.resolve(field.get('resolve', defaultFieldResolver(fieldName))(source, args, context, info)) 173 | } 174 | } 175 | } 176 | 177 | // var fieldResolver = (field, fieldName) => (args, ctx, root) => (obj): Promise<*> => { 178 | // return Promise.resolve(() => field.get('resolve', defaultFieldResolver(fieldName))(obj, args, ctx, root)) 179 | // } 180 | 181 | 182 | 183 | //fieldResolver resolver for the type that we are creating the filds for. 184 | function createFields(type: GraphQLOutputType, 185 | returnType: GraphQLOutputType, 186 | resolver: (fieldResolver: * , key: string, obj: *, field: Map) => GraphQLFieldConfig 187 | ): GraphQLFieldConfigMap { 188 | 189 | let fields = type._typeConfig.fields() 190 | 191 | return fromJS(fields) 192 | .reduce((resultFields: Map, field: Map, key: string): Map => { 193 | return resultFields.set(key, 194 | Map({ 195 | type: returnType, 196 | resolve: (obj, args, ctx, info): * => { 197 | return resolver(fieldResolver(field, key)(args, ctx, info), key, obj, field) 198 | } 199 | })) 200 | }, Map()).toJS(); 201 | } 202 | 203 | function createFieldsFromFieldList(fields, returnType: GraphQLOutputType, resolver: (fieldResolver: * , key: string, obj: *, field: Map) => GraphQLFieldConfig ){ 204 | return fromJS(fields) 205 | .reduce((resultFields: Map, field: Map, key: string): Map => { 206 | return resultFields.set(key, Map({ 207 | type: returnType, 208 | resolve: (obj, args, ctx, root): GraphQLFieldConfig => { 209 | return resolver(fieldResolver(field, key)(args, ctx, root), key, obj, field) 210 | } 211 | })) 212 | }, Map()).toJS(); 213 | } 214 | 215 | 216 | const FilterIntOperations = new GraphQLInputObjectType({ 217 | name: "FilterIntOperations", 218 | description: 'Filter operations for int', 219 | fields : () => filterIntArgs 220 | }) 221 | 222 | 223 | var filterIntArgs = { 224 | gt: { 225 | type: GraphQLInt, 226 | description: 'Filter only values greater then value.' 227 | }, 228 | lt: { 229 | type: GraphQLInt, 230 | description: 'Filter only values less then value.' 231 | }, 232 | gte: { 233 | type: GraphQLInt, 234 | description: 'Filter only values greater then or equal to value' 235 | }, 236 | lte: { 237 | type: GraphQLInt, 238 | description: 'Filter only values less then or equal to value' 239 | }, 240 | equal: { 241 | type: GraphQLInt, 242 | description: 'Filter only values equal to value.' 243 | }, 244 | not: { 245 | type: FilterIntOperations, 246 | description: 'Filter only values equal to value.' 247 | }, 248 | or: { 249 | type: new GraphQLList(FilterIntOperations), 250 | description: 'Filter only values equal to value.' 251 | } 252 | } 253 | 254 | 255 | const FilterStringOperations = new GraphQLInputObjectType({ 256 | name: "FilterStringOperations", 257 | description: 'Filter operations for strings', 258 | fields : () => filterStringArgs 259 | }) 260 | 261 | 262 | var filterStringArgs = { 263 | gt: { 264 | type: GraphQLString, 265 | description: 'Filter only values greater then value.' 266 | }, 267 | lt: { 268 | type: GraphQLString, 269 | description: 'Filter only values less then value.' 270 | }, 271 | gte: { 272 | type: GraphQLString, 273 | description: 'Filter only values greater then or equal to value' 274 | }, 275 | lte: { 276 | type: GraphQLString, 277 | description: 'Filter only values less then or equal to value' 278 | }, 279 | equal: { 280 | type: GraphQLString, 281 | description: 'Filter only values equal to value.' 282 | }, 283 | not: { 284 | type: FilterStringOperations, 285 | description: 'Filter only values equal to value.' 286 | }, 287 | or: { 288 | type: new GraphQLList(FilterStringOperations), 289 | description: 'Filter only values equal to value.' 290 | } 291 | } 292 | 293 | 294 | const filterFunctions = (field, key) => ({ 295 | gt: ({gt}, value: number ): boolean => { 296 | return gt == null || gt < value; 297 | }, 298 | lt: ({lt}, value: number ): boolean => { 299 | return lt == null || lt > value; 300 | }, 301 | gte: ({gte}, value: number ): boolean => { 302 | return gte == null || gte <= value; 303 | }, 304 | lte: ({lte}, value: number ): boolean => { 305 | return lte == null || lte >= value; 306 | }, 307 | equal: ({equal}, value: number ): boolean => { 308 | return equal == null || equal === value; 309 | }, 310 | not: ({not}, value: *, obj: *): boolean => { 311 | return not == null || !runFilterFunction(field, key)(not, obj) 312 | }, 313 | // or: ({or}, value: number): boolean => { 314 | // return true //or != null && //runFilterFunction(not, value) 315 | // } 316 | 317 | }) 318 | 319 | var runFilterFunction = (field, key) => (args, value) => { 320 | let {gt, lt, gte, lte, equal, not} = filterFunctions(field, key); 321 | return gt(args, value) 322 | && lt(args, value) 323 | && gte(args, value) 324 | && lte(args, value) 325 | && equal(args, value) 326 | } 327 | 328 | var resolveIntFilter = (field, key) => { 329 | return (obj, args, ctx, root) => { 330 | let resolver = fieldResolver(field, key)({}, ctx, root); 331 | let asList = List(obj); 332 | return resolveFromArray(asList, resolver) 333 | .then(resolvedValues => { 334 | return asList.filter((value, ii) => runFilterFunction(field,key)(args, resolvedValues[ii])) 335 | }) 336 | } 337 | } 338 | 339 | function filterFieldConfigFactory(fields, field: Map, key: string, type: GraphQLObjectType): GraphQLFieldConfig{ 340 | if(isInt(field)){ 341 | return fields.set(key, Map({ 342 | type: AggregationType(type), 343 | args: filterIntArgs, 344 | resolve: resolveIntFilter(field, key), 345 | description: `Filters ${key} via args.` 346 | })) 347 | } 348 | if(isString(field)){ 349 | return fields.set(key, Map({ 350 | type: AggregationType(type), 351 | args: filterStringArgs, 352 | resolve: resolveIntFilter(field, key), 353 | description: `Filters ${key} via args.` 354 | })) 355 | } 356 | return fields; 357 | } 358 | 359 | function containsType(fields, typeCheck) { 360 | return fromJS(fields) 361 | } 362 | /** 363 | * Returns a list of resolved values from array using field resolver 364 | */ 365 | function resolveFromArray(arr, fieldResolver){ 366 | return Promise.resolve(List(arr)).then(list => { 367 | return Promise.all(list.map((obj) => fieldResolver(obj)).toArray()) 368 | }) 369 | } 370 | 371 | 372 | /** 373 | * Creates an AggregationType with it based on the GraphQLOutputType requested, 374 | * Objects that wish to be resolved this way must be a Array of the requested type. 375 | * 376 | * @param {GraphQLOutputType} type - type to create the aggregaion functions for 377 | */ 378 | 379 | 380 | var aggregationTypes = {}; 381 | export function AggregationType(type: GraphQLObjectType): GraphQLObjectType { 382 | if(!aggregationTypes[type.name]){ 383 | aggregationTypes[type.name] = new GraphQLObjectType({ 384 | name: `${type.name}Aggregation`, 385 | description: `Preform aggregation methods on ${type.name}`, 386 | fields: (): GraphQLFieldConfigMap => { 387 | let typeFields = fromJS(type._typeConfig.fields()) 388 | let intFields = typeFields.filter(field => isFloat(field) || isInt(field)) 389 | let stringFields = typeFields.filter(field => isString(field)) 390 | 391 | let fields = Map( 392 | { 393 | values : { 394 | description: `List of ${type.name}`, 395 | type: new GraphQLList(type), 396 | resolve: (obj: Array<*>|List<*>): GraphQLList<*> => { 397 | return List(obj).toArray() 398 | } 399 | }, 400 | count : { 401 | description: 'The amount of items in the aggregaion', 402 | type: GraphQLInt, 403 | resolve: (obj: Array<*>): number => List(obj).count() 404 | 405 | }, 406 | first: { 407 | description: 'Return the first item in the aggregaion', 408 | type: type, 409 | resolve: (obj: Array<*>): * => List(obj).first() 410 | }, 411 | last: { // return fromJS() 412 | description: 'Return the last item in the aggregaion', 413 | type: type, 414 | resolve: (obj: Array<*>): * => List(obj).last() 415 | }, 416 | reverse: { 417 | description: 'Reverse the order of the items in the aggregaion', 418 | type: AggregationType(type), 419 | resolve: (obj: Array<*>): * => List(obj).reverse() 420 | }, 421 | //slice: {} 422 | // sort: { 423 | // description: 'Sort the list via the parameter', 424 | // fields: () => 425 | // createFields(type, 426 | // AggregationType(type), 427 | // (fieldResolver: * , key: string, obj: *) => { 428 | // return fromJS(obj).sort((a, b) => { 429 | // return fieldResolver(a) > fieldResolver(b) 430 | // }) 431 | // }, 432 | // () => true) 433 | // }, 434 | //flattern 435 | //flattern should allow a CSV to be generated. 436 | groupedBy : { 437 | type: new GraphQLObjectType({ 438 | name: `${type.name}GroupedByAggregation`, 439 | description: `Preform groupBy aggregation methods on ${type.name}`, 440 | fields: () => { 441 | return createFields(type, 442 | KeyedList(type), 443 | (fieldResolver, key: string, obj: *) => { 444 | // return Promise.resolve(List(obj)) 445 | // .then((obj) => { 446 | // return Promise.all(obj.map((obj) => fieldResolver(obj)).toArray()) 447 | // }) 448 | return resolveFromArray(obj, fieldResolver) 449 | .then(result => { 450 | let groups = List(result); 451 | return List(obj).groupBy((item, ii) => groups.get(ii)); 452 | }) 453 | } 454 | ) 455 | } 456 | }), 457 | description: `Group items in aggregaion by the value of a field.`, 458 | resolve: (obj) => obj 459 | }, 460 | 461 | }) 462 | if(stringFields.count() > 0 || intFields.count() > 0){ 463 | fields = fields.set('filter', 464 | { 465 | description: `Preform filter aggregation methods on ${type.name}`, 466 | type: new GraphQLObjectType({ 467 | name: `${type.name}FilterAggregation`, 468 | description: `Preform filter aggregation methods on ${type.name}`, 469 | args: filterIntArgs, 470 | fields: () => { 471 | return stringFields.merge(intFields) 472 | .reduce((fields, typeField, key) => { 473 | return filterFieldConfigFactory(fields, typeField, key, type) 474 | }, Map()).toJS() 475 | } 476 | }), 477 | resolve: (obj) => obj 478 | }) 479 | if(intFields.count() > 0){ 480 | // add integer operations 481 | fields = fields.merge(Map( 482 | { 483 | sum: { 484 | description: `Sum the values of a field on ${type.name}`, 485 | type: new GraphQLObjectType({ 486 | name: `${type.name}Sum`, 487 | description: `Perform sum on ${type.name}`, 488 | fields: () => { 489 | return createFieldsFromFieldList(intFields, 490 | GraphQLFloat, 491 | (fieldResolver: * , key: string, obj: *) => { 492 | return resolveFromArray(obj, fieldResolver).then((values) => { 493 | return List(values).update(imMath.sum()) 494 | }) 495 | }, 496 | (field) => isFloat(field) || isInt(field)) 497 | } 498 | }), 499 | resolve: (obj) => obj 500 | }, 501 | average: { 502 | description: `Returns the average of a field on ${type.name}`, 503 | type: new GraphQLObjectType({ 504 | name: `${type.name}Average`, 505 | description: `Perform averages on ${type.name}`, 506 | fields: () => { 507 | return createFieldsFromFieldList(intFields, 508 | GraphQLFloat, 509 | (fieldResolver: * , key: string, obj: *) => { 510 | return resolveFromArray(obj, fieldResolver).then((values) => { 511 | return List(values).update(imMath.average()) 512 | }) 513 | }, 514 | (field) => isFloat(field) || isInt(field)) 515 | } 516 | }), 517 | resolve: (obj) => obj 518 | }, 519 | median: { 520 | description: `Returns the median value of a field on ${type.name}`, 521 | type: new GraphQLObjectType({ 522 | name: `${type.name}Median`, 523 | description: `Perform median calculation on ${type.name}`, 524 | fields: () => { 525 | return createFieldsFromFieldList(intFields, 526 | GraphQLFloat, 527 | (fieldResolver: * , key: string, obj: *) => { 528 | return resolveFromArray(obj, fieldResolver).then((values) => { 529 | return List(values).update(imMath.median()) 530 | }) 531 | }, 532 | (field) => isFloat(field) || isInt(field)) 533 | } 534 | }), 535 | resolve: (obj) => obj 536 | }, 537 | min: { 538 | description: `Returns the minium value of all the items on a field on ${type.name}`, 539 | type: new GraphQLObjectType({ 540 | name: `${type.name}Min`, 541 | description: `minium on value of a field for ${type.name}`, 542 | fields: () => { 543 | return createFieldsFromFieldList(intFields, 544 | GraphQLFloat, 545 | (fieldResolver: * , key: string, obj: *) => { 546 | return resolveFromArray(obj, fieldResolver).then((values) => { 547 | return List(values).update(imMath.min()) 548 | }) 549 | }, 550 | (field) => isFloat(field) || isInt(field)) 551 | } 552 | }), 553 | resolve: (obj) => obj 554 | }, 555 | max: { 556 | description: `Returns the maximum value of all the items on a field on${type.name}`, 557 | type: new GraphQLObjectType({ 558 | name: `${type.name}Max`, 559 | description: `maximum on value of a field for ${type.name}`, 560 | fields: () => { 561 | return createFieldsFromFieldList(intFields, 562 | GraphQLFloat, 563 | (fieldResolver: * , key: string, obj: *) => { 564 | return resolveFromArray(obj, fieldResolver).then((values) => { 565 | return List(values).update(imMath.max()) 566 | }) 567 | }, 568 | (field) => isFloat(field) || isInt(field)) 569 | } 570 | }), 571 | resolve: (obj) => obj 572 | } 573 | })) 574 | } 575 | } 576 | return fields.toObject(); 577 | } 578 | 579 | }) 580 | } 581 | return aggregationTypes[type.name]; 582 | } 583 | -------------------------------------------------------------------------------- /tests/environment.js: -------------------------------------------------------------------------------- 1 | require('babel-core/register')({ 2 | }); 3 | -------------------------------------------------------------------------------- /tests/jasmine.js: -------------------------------------------------------------------------------- 1 | require('./environment.js'); 2 | var jasmine = require('jasmine'); 3 | var testRunner = new jasmine(); 4 | var reporters = require('jasmine-reporters'); 5 | 6 | testRunner.loadConfig({ 7 | spec_dir: './src/', 8 | spec_files: [ 9 | '**/__tests__/*-spec.js' 10 | ] 11 | }); 12 | 13 | testRunner.execute(); 14 | 15 | --------------------------------------------------------------------------------